diff --git a/packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.tsx b/packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.tsx index b238a26d8..572cf16ec 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.tsx +++ b/packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.tsx @@ -188,7 +188,7 @@ export const Default: Story = { }, execution: { stageStatus: { - duration: 'SLA: None', + slaText: 'SLA: None', }, }, onTaskAdd: () => { @@ -212,7 +212,7 @@ export const Default: Story = { }, execution: { stageStatus: { - duration: 'SLA: None', + slaText: 'SLA: None', }, }, onAddTaskFromToolbox: (taskItem: ListItem) => { @@ -391,7 +391,7 @@ export const ExecutionStatus: Story = { execution: { stageStatus: { status: 'Completed', - duration: 'SLA: 4h', + slaText: 'SLA: 4h', }, taskStatus: { '1': { status: 'Completed', label: 'KYC and AML Checks', duration: '2h 15m' }, @@ -427,7 +427,7 @@ export const ExecutionStatus: Story = { execution: { stageStatus: { status: 'Completed', - duration: 'SLA: 6h 15m', + slaText: 'SLA: 6h 15m', }, taskStatus: { '1': { @@ -480,7 +480,7 @@ export const ExecutionStatus: Story = { stageStatus: { status: 'InProgress', label: 'In progress', - duration: 'SLA: 2h 15m', + slaText: 'SLA: 2h 15m', }, taskStatus: { '1': { status: 'Completed', label: 'Report Ordering', duration: '2h 15m' }, @@ -567,6 +567,110 @@ export const ExecutionStatus: Story = { }, }; +export const SLAStates: Story = { + name: 'SLA States', + parameters: { + nodes: [ + { + id: '0', + type: 'stage', + position: { x: 48, y: 96 }, + width: 304, + data: { + stageDetails: { + label: 'Stage 1', + isReadOnly: true, + tasks: [], + }, + execution: { + stageStatus: { + slaText: 'SLA: None', + }, + taskStatus: {}, + }, + }, + }, + { + id: '1', + type: 'stage', + position: { x: 400, y: 96 }, + width: 304, + data: { + stageDetails: { + label: 'Closing', + isReadOnly: true, + tasks: [ + [{ id: '1', label: 'Prepare closing docs', icon: }], + [{ id: '2', label: 'eSign envelope', icon: }], + [{ id: '3', label: 'Review closing docs', icon: }], + ], + }, + execution: { + stageStatus: { + status: 'InProgress', + label: 'In progress', + slaText: 'SLA: 10 days remaining', + }, + taskStatus: {}, + }, + }, + }, + { + id: '2', + type: 'stage', + position: { x: 752, y: 96 }, + width: 304, + data: { + stageDetails: { + label: 'Closing', + isReadOnly: true, + tasks: [ + [{ id: '1', label: 'Prepare closing docs', icon: }], + [{ id: '2', label: 'eSign envelope', icon: }], + [{ id: '3', label: 'Review closing docs', icon: }], + ], + }, + execution: { + stageStatus: { + status: 'InProgress', + label: 'In progress', + slaText: 'SLA: 1 day remaining', + slaIcon: 'warning', + }, + taskStatus: {}, + }, + }, + }, + { + id: '3', + type: 'stage', + position: { x: 1104, y: 96 }, + width: 304, + data: { + stageDetails: { + label: 'Closing', + isReadOnly: true, + tasks: [ + [{ id: '1', label: 'Prepare closing docs', icon: }], + [{ id: '2', label: 'eSign envelope', icon: }], + [{ id: '3', label: 'Review closing docs', icon: }], + ], + }, + execution: { + stageStatus: { + status: 'InProgress', + label: 'In progress', + slaText: 'SLA: 1 day overdue', + slaIcon: 'error', + }, + taskStatus: {}, + }, + }, + }, + ], + }, +}; + export const InteractiveTaskManagement: Story = { name: 'Interactive Task Management', parameters: { @@ -695,6 +799,92 @@ export const InteractiveTaskManagement: Story = { }, }; +export const ExecutionModeWithSla: Story = { + name: 'Execution Mode - Runtime vs SLA', + parameters: { + nodes: [ + { + id: 'exec-runtime-only', + type: 'stage', + position: { x: 48, y: 96 }, + width: 304, + data: { + stageDetails: { + label: 'Runtime only', + isReadOnly: true, + tasks: [ + [{ id: '1', label: 'Prepare closing docs', icon: }], + [{ id: '2', label: 'eSign envelope', icon: }], + [{ id: '3', label: 'Review closing docs', icon: }], + ], + }, + execution: { + stageStatus: { + status: 'InProgress', + label: 'In progress', + duration: 'Duration: 2h 15m', + }, + taskStatus: {}, + }, + }, + }, + { + id: 'exec-sla-only', + type: 'stage', + position: { x: 400, y: 96 }, + width: 304, + data: { + stageDetails: { + label: 'SLA only', + isReadOnly: true, + tasks: [ + [{ id: '1', label: 'Prepare closing docs', icon: }], + [{ id: '2', label: 'eSign envelope', icon: }], + [{ id: '3', label: 'Review closing docs', icon: }], + ], + }, + execution: { + stageStatus: { + status: 'InProgress', + label: 'In progress', + slaText: 'SLA: 1 day remaining', + slaIcon: 'warning', + }, + taskStatus: {}, + }, + }, + }, + { + id: 'exec-runtime-and-sla', + type: 'stage', + position: { x: 752, y: 96 }, + width: 304, + data: { + stageDetails: { + label: 'Runtime + SLA (both)', + isReadOnly: true, + tasks: [ + [{ id: '1', label: 'Prepare closing docs', icon: }], + [{ id: '2', label: 'eSign envelope', icon: }], + [{ id: '3', label: 'Review closing docs', icon: }], + ], + }, + execution: { + stageStatus: { + status: 'InProgress', + label: 'In progress', + duration: 'Duration: 2h 15m', + slaText: 'SLA: 1 day remaining', + slaIcon: 'warning', + }, + taskStatus: {}, + }, + }, + }, + ], + }, +}; + export const LoanProcessingWorkflow: Story = { name: 'Loan Processing Workflow', parameters: { @@ -1929,7 +2119,7 @@ export const AdhocTasks: Story = { stageStatus: { status: 'InProgress', label: 'In progress', - duration: 'SLA: 3h 45m', + slaText: 'SLA: 3h 45m', }, taskStatus: { '1': { @@ -2205,7 +2395,7 @@ export const TasksBySection: Story = { stageStatus: { status: 'InProgress', label: 'In progress', - duration: 'SLA: 3h 45m', + slaText: 'SLA: 3h 45m', }, taskStatus: { '1': { @@ -2444,7 +2634,7 @@ export const WithRulesTags: Story = { ], }, execution: { - stageStatus: { status: 'Completed', label: 'Completed', duration: 'SLA: 4h' }, + stageStatus: { status: 'Completed', label: 'Completed', slaText: 'SLA: 4h' }, }, }, }, @@ -2477,7 +2667,7 @@ export const WithRulesTags: Story = { ], }, execution: { - stageStatus: { status: 'InProgress', label: 'In progress', duration: 'SLA: 2h' }, + stageStatus: { status: 'InProgress', label: 'In progress', slaText: 'SLA: 2h' }, }, }, }, diff --git a/packages/apollo-react/src/canvas/components/StageNode/StageNode.styles.ts b/packages/apollo-react/src/canvas/components/StageNode/StageNode.styles.ts index 6720cd461..900c41a66 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/StageNode.styles.ts +++ b/packages/apollo-react/src/canvas/components/StageNode/StageNode.styles.ts @@ -69,7 +69,7 @@ export const StageContainer = styled.div<{ export const StageHeader = styled.div<{ isException?: boolean }>` position: relative; display: flex; - justify-content: space-between; + flex-direction: column; padding: ${Spacing.SpacingS} ${Spacing.SpacingBase}; border-bottom: solid 1px var(--canvas-border-de-emp); background: ${(props) => (props.isException ? 'var(--color-background-secondary)' : 'var(--canvas-background)')}; diff --git a/packages/apollo-react/src/canvas/components/StageNode/StageNode.test.tsx b/packages/apollo-react/src/canvas/components/StageNode/StageNode.test.tsx index 230c37433..4467ce57e 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/StageNode.test.tsx +++ b/packages/apollo-react/src/canvas/components/StageNode/StageNode.test.tsx @@ -848,6 +848,93 @@ describe('StageNode - ReadOnly Mode', () => { }); }); +describe('StageNode - SLA Indicator', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const getSlaIndicator = () => screen.getByTestId('stage-sla-stage-1'); + + it('does not render the SLA indicator when slaText is undefined', () => { + renderStageNode({ + execution: { stageStatus: {}, taskStatus: {} }, + }); + + expect(screen.queryByTestId('stage-sla-stage-1')).not.toBeInTheDocument(); + }); + + it('does not render the SLA indicator when only duration is provided', () => { + renderStageNode({ + execution: { + stageStatus: { duration: 'Duration: 1h 30m' }, + taskStatus: {}, + }, + }); + + expect(screen.queryByTestId('stage-sla-stage-1')).not.toBeInTheDocument(); + }); + + it('renders slaText without an icon when slaIcon is omitted', () => { + renderStageNode({ + execution: { + stageStatus: { slaText: 'SLA: 10 days remaining' }, + taskStatus: {}, + }, + }); + + const indicator = getSlaIndicator(); + expect(indicator).toHaveTextContent('SLA: 10 days remaining'); + expect(indicator).not.toHaveAttribute('data-sla-icon'); + expect(indicator.querySelector('svg')).toBeNull(); + }); + + it('renders a warning icon and text when slaIcon is "warning"', () => { + renderStageNode({ + execution: { + stageStatus: { slaText: 'SLA: 1 day remaining', slaIcon: 'warning' }, + taskStatus: {}, + }, + }); + + const indicator = getSlaIndicator(); + expect(indicator).toHaveTextContent('SLA: 1 day remaining'); + expect(indicator).toHaveAttribute('data-sla-icon', 'warning'); + expect(indicator.querySelector('svg')).not.toBeNull(); + }); + + it('renders an error icon and text when slaIcon is "error"', () => { + renderStageNode({ + execution: { + stageStatus: { slaText: 'SLA: 1 day overdue', slaIcon: 'error' }, + taskStatus: {}, + }, + }); + + const indicator = getSlaIndicator(); + expect(indicator).toHaveTextContent('SLA: 1 day overdue'); + expect(indicator).toHaveAttribute('data-sla-icon', 'error'); + expect(indicator.querySelector('svg')).not.toBeNull(); + }); + + it('renders both duration and slaText as independent lines when both are provided', () => { + renderStageNode({ + execution: { + stageStatus: { + duration: 'Duration: 1h 30m', + slaText: 'SLA: 1 day remaining', + slaIcon: 'warning', + }, + taskStatus: {}, + }, + }); + + expect(screen.getByText('Duration: 1h 30m')).toBeInTheDocument(); + const indicator = getSlaIndicator(); + expect(indicator).toHaveTextContent('SLA: 1 day remaining'); + expect(indicator).toHaveAttribute('data-sla-icon', 'warning'); + }); +}); + describe('StageNode - Header Chips', () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/packages/apollo-react/src/canvas/components/StageNode/StageNode.types.ts b/packages/apollo-react/src/canvas/components/StageNode/StageNode.types.ts index dc49c952d..ac3765fe5 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/StageNode.types.ts +++ b/packages/apollo-react/src/canvas/components/StageNode/StageNode.types.ts @@ -18,6 +18,8 @@ enum ElementStatusValues { export type StageStatus = `${ElementStatusValues}`; export type StageTaskStatus = `${ElementStatusValues}`; +export type StageSlaIcon = 'warning' | 'error'; + export interface StageTaskItem { id: string; label: string; @@ -66,6 +68,8 @@ export interface StageNodeBaseProps { status?: StageStatus; label?: string; duration?: string; + slaText?: string; + slaIcon?: StageSlaIcon; }; taskStatus: Record; }; diff --git a/packages/apollo-react/src/canvas/components/StageNode/StageNodeHeader.tsx b/packages/apollo-react/src/canvas/components/StageNode/StageNodeHeader.tsx index 39298513d..f00eac41c 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/StageNodeHeader.tsx +++ b/packages/apollo-react/src/canvas/components/StageNode/StageNodeHeader.tsx @@ -1,5 +1,5 @@ import { Icon, Padding, Spacing } from '@uipath/apollo-core'; -import { Column, Row } from '@uipath/apollo-react/canvas/layouts'; +import { Row } from '@uipath/apollo-react/canvas/layouts'; import { Button, cn } from '@uipath/apollo-wind'; import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { EntryConditionIcon, ExitConditionIcon, ReturnToOriginIcon } from '../../icons'; @@ -7,16 +7,21 @@ import { CanvasIcon } from '../../utils/icon-registry'; import { CanvasTooltip } from '../CanvasTooltip'; import { ExecutionStatusIcon } from '../ExecutionStatusIcon'; import { getExecutionStatusColor } from '../ExecutionStatusIcon/ExecutionStatusIcon'; -import { - StageChip, - StageHeader, - StageHeaderChipsRow, - StageTitleContainer, - StageTitleInput, -} from './StageNode.styles'; -import type { StageNodeProps, StageStatus } from './StageNode.types'; +import { StageChip, StageHeader, StageTitleContainer, StageTitleInput } from './StageNode.styles'; +import type { StageNodeProps, StageSlaIcon, StageStatus } from './StageNode.types'; import { StageHeaderChipType } from './StageNode.types'; +const SLA_ICON_CONFIG: Record = { + warning: { + icon: 'triangle-alert', + iconColor: 'var(--canvas-warning-icon)', + }, + error: { + icon: 'circle-alert', + iconColor: 'var(--canvas-error-icon)', + }, +}; + const CHIP_ICONS: Record = { [StageHeaderChipType.Entry]: , [StageHeaderChipType.Exit]: , @@ -54,6 +59,9 @@ const StageNodeHeaderInner = ({ const icon = stageDetails?.icon; const statusLabel = execution?.stageStatus?.label; const stageDuration = execution?.stageStatus?.duration; + const slaText = execution?.stageStatus?.slaText; + const slaIcon = execution?.stageStatus?.slaIcon; + const slaIndicator = slaIcon ? SLA_ICON_CONFIG[slaIcon] : undefined; const isStageTitleEditable = !!onStageTitleChange && !isReadOnly; @@ -121,10 +129,12 @@ const StageNodeHeaderInner = ({ return ( - - {icon} - - +
+ + {icon} + - {stageDuration && {stageDuration}} + + + {status && ( + + + + )} + {(onTaskAdd || onAddTaskFromToolbox) && !isReadOnly && ( + + + + )} + +
+ {(slaText || (stageDetails.headerChips && stageDetails.headerChips.length > 0)) && ( +
+ {slaText && ( + + {slaIndicator && ( + + )} + {slaText} + + )} {stageDetails.headerChips && stageDetails.headerChips.length > 0 && ( - +
{stageDetails.headerChips.map((chip) => { const button = ( +
)} - - - - {status && ( - - - - )} - {(onTaskAdd || onAddTaskFromToolbox) && !isReadOnly && ( - - - - )} - +
+ )} + {stageDuration && {stageDuration}}
); };