Skip to content

Commit

Permalink
feat: show suspended schedules in timeline (#5873)
Browse files Browse the repository at this point in the history
This PR updates the timeline to show suspended schedules. It also adds
"schedule failed" when the schedule has failed.

<img width="465" alt="image"
src="https://github.com/Unleash/unleash/assets/17786332/aabbee02-b407-4653-959b-92bec8a1fa66">

<img width="465" alt="image"
src="https://github.com/Unleash/unleash/assets/17786332/7242c34a-1b1e-4efc-a778-a360e3bc4428">
  • Loading branch information
thomasheartman committed Jan 12, 2024
1 parent c816ffd commit 8ae267e
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 97 deletions.
Expand Up @@ -2,7 +2,10 @@ import { Alert, Box, Button, styled, Typography } from '@mui/material';
import { FC, useContext, useState } from 'react';
import { useChangeRequest } from 'hooks/api/getters/useChangeRequest/useChangeRequest';
import { ChangeRequestHeader } from './ChangeRequestHeader/ChangeRequestHeader';
import { ChangeRequestTimeline } from './ChangeRequestTimeline/ChangeRequestTimeline';
import {
ChangeRequestTimeline,
ISuggestChangeTimelineProps,
} from './ChangeRequestTimeline/ChangeRequestTimeline';
import { ChangeRequest } from '../ChangeRequest/ChangeRequest';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
Expand Down Expand Up @@ -285,20 +288,23 @@ export const ChangeRequestOverview: FC = () => {
? changeRequest.schedule.scheduledAt
: undefined;

const timelineProps: ISuggestChangeTimelineProps =
changeRequest.state === 'Scheduled'
? {
state: 'Scheduled',
schedule: changeRequest.schedule,
}
: {
state: changeRequest.state,
schedule: undefined,
};

return (
<>
<ChangeRequestHeader changeRequest={changeRequest} />
<ChangeRequestBody>
<StyledAsideBox>
<ChangeRequestTimeline
state={changeRequest.state}
scheduledAt={
'schedule' in changeRequest
? changeRequest.schedule.scheduledAt
: undefined
}
failureReason={reason}
/>
<ChangeRequestTimeline {...timelineProps} />
<ChangeRequestReviewers changeRequest={changeRequest} />
</StyledAsideBox>
<StyledPaper elevation={0}>
Expand Down
@@ -1,6 +1,10 @@
import { screen } from '@testing-library/react';
import { render } from 'utils/testRenderer';
import { ChangeRequestTimeline, determineColor } from './ChangeRequestTimeline';
import {
ChangeRequestTimeline,
determineColor,
getScheduleProps,
} from './ChangeRequestTimeline';
import { ChangeRequestState } from '../../changeRequest.types';

test('cancelled timeline shows all states', () => {
Expand Down Expand Up @@ -44,7 +48,10 @@ test('scheduled timeline shows all states', () => {
render(
<ChangeRequestTimeline
state={'Scheduled'}
scheduledAt={new Date().toISOString()}
schedule={{
scheduledAt: new Date().toISOString(),
status: 'pending',
}}
/>,
);

Expand Down Expand Up @@ -101,27 +108,64 @@ test('returns success for stages other than Rejected in Rejected state', () => {
),
).toBe('success');
});
test('returns warning for Scheduled stage in Scheduled state', () => {
expect(
determineColor(
'Scheduled',
irrelevantIndex,
'Scheduled',
irrelevantIndex,
),
).toBe('warning');
});

test('returns error for Scheduled stage in Scheduled state with failure reason', () => {
expect(
determineColor(
'Scheduled',
irrelevantIndex,
'Scheduled',
irrelevantIndex,
'conflicts',
),
).toBe('error');
describe('changeRequestScheduleProps', () => {
test('returns correct props for a pending schedule', () => {
const schedule = {
scheduledAt: new Date().toISOString(),
status: 'pending' as const,
};

const time = 'some time string';

const { title, subtitle, color, reason } = getScheduleProps(
schedule,
time,
);
expect(title).toBe('Scheduled');
expect(subtitle).toBe(`for ${time}`);
expect(color).toBe('warning');
expect(reason).toBeNull();
});

test('returns correct props for a failed schedule', () => {
const schedule = {
scheduledAt: new Date().toISOString(),
status: 'failed' as const,
reason: 'reason',
failureReason: 'failure reason',
};

const time = 'some time string';

const { title, subtitle, color, reason } = getScheduleProps(
schedule,
time,
);
expect(title).toBe('Schedule failed');
expect(subtitle).toBe(`at ${time}`);
expect(color).toBe('error');
expect(reason).toBeTruthy();
});

test('returns correct props for a suspended schedule', () => {
const schedule = {
scheduledAt: new Date().toISOString(),
status: 'suspended' as const,
reason: 'reason',
};

const time = 'some time string';

const { title, subtitle, color, reason } = getScheduleProps(
schedule,
time,
);
expect(title).toBe('Schedule suspended');
expect(subtitle).toBe(`was ${time}`);
expect(color).toBe('grey');
expect(reason).toBeTruthy();
});
});

test('returns success for stages at or before activeIndex', () => {
Expand Down
Expand Up @@ -6,18 +6,24 @@ import TimelineSeparator from '@mui/lab/TimelineSeparator';
import TimelineDot from '@mui/lab/TimelineDot';
import TimelineConnector from '@mui/lab/TimelineConnector';
import TimelineContent from '@mui/lab/TimelineContent';
import { ChangeRequestState } from '../../changeRequest.types';
import { ConditionallyRender } from '../../../common/ConditionallyRender/ConditionallyRender';
import {
ChangeRequestSchedule,
ChangeRequestState,
} from '../../changeRequest.types';
import { HtmlTooltip } from '../../../common/HtmlTooltip/HtmlTooltip';
import { Error as ErrorIcon } from '@mui/icons-material';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { formatDateYMDHMS } from 'utils/formatDate';

interface ISuggestChangeTimelineProps {
state: ChangeRequestState;
scheduledAt?: string;
failureReason?: string;
}
export type ISuggestChangeTimelineProps =
| {
state: Exclude<ChangeRequestState, 'Scheduled'>;
schedule?: undefined;
}
| {
state: 'Scheduled';
schedule: ChangeRequestSchedule;
};

const StyledPaper = styled(Paper)(({ theme }) => ({
marginTop: theme.spacing(2),
Expand Down Expand Up @@ -62,36 +68,24 @@ export const determineColor = (
changeRequestStateIndex: number,
displayStage: ChangeRequestState,
displayStageIndex: number,
failureReason?: string,
) => {
if (changeRequestState === 'Cancelled') return 'grey';

if (changeRequestState === 'Rejected')
return displayStage === 'Rejected' ? 'error' : 'success';
if (
changeRequestStateIndex !== -1 &&
changeRequestStateIndex > displayStageIndex
changeRequestStateIndex >= displayStageIndex
)
return 'success';
if (
changeRequestStateIndex !== -1 &&
changeRequestStateIndex === displayStageIndex
) {
return changeRequestState === 'Scheduled'
? failureReason
? 'error'
: 'warning'
: 'success';
}

if (changeRequestStateIndex + 1 === displayStageIndex) return 'primary';
return 'grey';
};

export const ChangeRequestTimeline: FC<ISuggestChangeTimelineProps> = ({
state,
scheduledAt,
failureReason,
schedule,
}) => {
let data: ChangeRequestState[];
switch (state) {
Expand All @@ -106,28 +100,20 @@ export const ChangeRequestTimeline: FC<ISuggestChangeTimelineProps> = ({
}
const activeIndex = data.findIndex((item) => item === state);

const { locationSettings } = useLocationSettings();

return (
<StyledPaper elevation={0}>
<StyledBox>
<StyledTimeline>
{data.map((title, index) => {
const subtitle =
scheduledAt &&
state === 'Scheduled' &&
state === title
? formatDateYMDHMS(
new Date(scheduledAt),
locationSettings?.locale,
)
: undefined;
if (schedule && title === 'Scheduled') {
return createTimelineScheduleItem(schedule);
}

const color = determineColor(
state,
activeIndex,
title,
index,
failureReason,
);
let timelineDotProps = {};

Expand All @@ -142,8 +128,6 @@ export const ChangeRequestTimeline: FC<ISuggestChangeTimelineProps> = ({
return createTimelineItem(
color,
title,
subtitle,
failureReason,
index < data.length - 1,
timelineDotProps,
);
Expand All @@ -157,8 +141,6 @@ export const ChangeRequestTimeline: FC<ISuggestChangeTimelineProps> = ({
const createTimelineItem = (
color: 'primary' | 'success' | 'grey' | 'error' | 'warning',
title: string,
subtitle: string | undefined,
failureReason: string | undefined,
shouldConnectToNextItem: boolean,
timelineDotProps: { [key: string]: string | undefined } = {},
) => (
Expand All @@ -167,33 +149,78 @@ const createTimelineItem = (
<TimelineDot color={color} {...timelineDotProps} />
{shouldConnectToNextItem && <TimelineConnector />}
</TimelineSeparator>
<TimelineContent>
{title}
<ConditionallyRender
condition={Boolean(subtitle)}
show={
<StyledSubtitle>
<Typography
color={'text.secondary'}
sx={{ mr: 1 }}
>{`(for ${subtitle})`}</Typography>
<ConditionallyRender
condition={Boolean(failureReason)}
show={
<HtmlTooltip
title={`Schedule failed because of ${failureReason}`}
arrow
>
<ErrorIcon
color={'error'}
fontSize={'small'}
/>
</HtmlTooltip>
}
/>
</StyledSubtitle>
}
/>
</TimelineContent>
<TimelineContent>{title}</TimelineContent>
</TimelineItem>
);

export const getScheduleProps = (
schedule: ChangeRequestSchedule,
formattedTime: string,
) => {
switch (schedule.status) {
case 'suspended':
return {
title: 'Schedule suspended',
subtitle: `was ${formattedTime}`,
color: 'grey' as const,
reason: (
<HtmlTooltip title={schedule.reason} arrow>
<ErrorIcon color={'disabled'} fontSize={'small'} />
</HtmlTooltip>
),
};
case 'failed':
return {
title: 'Schedule failed',
subtitle: `at ${formattedTime}`,
color: 'error' as const,
reason: (
<HtmlTooltip
title={`Schedule failed because of ${
schedule.reason || schedule.failureReason
}`}
arrow
>
<ErrorIcon color={'error'} fontSize={'small'} />
</HtmlTooltip>
),
};
default:
return {
title: 'Scheduled',
subtitle: `for ${formattedTime}`,
color: 'warning' as const,
reason: null,
};
}
};

const createTimelineScheduleItem = (schedule: ChangeRequestSchedule) => {
const { locationSettings } = useLocationSettings();

const time = formatDateYMDHMS(
new Date(schedule.scheduledAt),
locationSettings?.locale,
);

const { title, subtitle, color, reason } = getScheduleProps(schedule, time);

return (
<TimelineItem key={title}>
<TimelineSeparator>
<TimelineDot color={color} />
<TimelineConnector />
</TimelineSeparator>
<TimelineContent>
{title}
<StyledSubtitle>
<Typography
color={'text.secondary'}
sx={{ mr: 1 }}
>{`(${subtitle})`}</Typography>
{reason}
</StyledSubtitle>
</TimelineContent>
</TimelineItem>
);
};

0 comments on commit 8ae267e

Please sign in to comment.