mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-15 17:50:48 +02:00
feat: show suspended schedules in timeline (#5873)
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">
This commit is contained in:
parent
c816ffd49d
commit
8ae267ea25
@ -2,7 +2,10 @@ import { Alert, Box, Button, styled, Typography } from '@mui/material';
|
|||||||
import { FC, useContext, useState } from 'react';
|
import { FC, useContext, useState } from 'react';
|
||||||
import { useChangeRequest } from 'hooks/api/getters/useChangeRequest/useChangeRequest';
|
import { useChangeRequest } from 'hooks/api/getters/useChangeRequest/useChangeRequest';
|
||||||
import { ChangeRequestHeader } from './ChangeRequestHeader/ChangeRequestHeader';
|
import { ChangeRequestHeader } from './ChangeRequestHeader/ChangeRequestHeader';
|
||||||
import { ChangeRequestTimeline } from './ChangeRequestTimeline/ChangeRequestTimeline';
|
import {
|
||||||
|
ChangeRequestTimeline,
|
||||||
|
ISuggestChangeTimelineProps,
|
||||||
|
} from './ChangeRequestTimeline/ChangeRequestTimeline';
|
||||||
import { ChangeRequest } from '../ChangeRequest/ChangeRequest';
|
import { ChangeRequest } from '../ChangeRequest/ChangeRequest';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||||
@ -285,20 +288,23 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
? changeRequest.schedule.scheduledAt
|
? changeRequest.schedule.scheduledAt
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const timelineProps: ISuggestChangeTimelineProps =
|
||||||
|
changeRequest.state === 'Scheduled'
|
||||||
|
? {
|
||||||
|
state: 'Scheduled',
|
||||||
|
schedule: changeRequest.schedule,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
state: changeRequest.state,
|
||||||
|
schedule: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ChangeRequestHeader changeRequest={changeRequest} />
|
<ChangeRequestHeader changeRequest={changeRequest} />
|
||||||
<ChangeRequestBody>
|
<ChangeRequestBody>
|
||||||
<StyledAsideBox>
|
<StyledAsideBox>
|
||||||
<ChangeRequestTimeline
|
<ChangeRequestTimeline {...timelineProps} />
|
||||||
state={changeRequest.state}
|
|
||||||
scheduledAt={
|
|
||||||
'schedule' in changeRequest
|
|
||||||
? changeRequest.schedule.scheduledAt
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
failureReason={reason}
|
|
||||||
/>
|
|
||||||
<ChangeRequestReviewers changeRequest={changeRequest} />
|
<ChangeRequestReviewers changeRequest={changeRequest} />
|
||||||
</StyledAsideBox>
|
</StyledAsideBox>
|
||||||
<StyledPaper elevation={0}>
|
<StyledPaper elevation={0}>
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { screen } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
import { render } from 'utils/testRenderer';
|
import { render } from 'utils/testRenderer';
|
||||||
import { ChangeRequestTimeline, determineColor } from './ChangeRequestTimeline';
|
import {
|
||||||
|
ChangeRequestTimeline,
|
||||||
|
determineColor,
|
||||||
|
getScheduleProps,
|
||||||
|
} from './ChangeRequestTimeline';
|
||||||
import { ChangeRequestState } from '../../changeRequest.types';
|
import { ChangeRequestState } from '../../changeRequest.types';
|
||||||
|
|
||||||
test('cancelled timeline shows all states', () => {
|
test('cancelled timeline shows all states', () => {
|
||||||
@ -44,7 +48,10 @@ test('scheduled timeline shows all states', () => {
|
|||||||
render(
|
render(
|
||||||
<ChangeRequestTimeline
|
<ChangeRequestTimeline
|
||||||
state={'Scheduled'}
|
state={'Scheduled'}
|
||||||
scheduledAt={new Date().toISOString()}
|
schedule={{
|
||||||
|
scheduledAt: new Date().toISOString(),
|
||||||
|
status: 'pending',
|
||||||
|
}}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -101,27 +108,64 @@ test('returns success for stages other than Rejected in Rejected state', () => {
|
|||||||
),
|
),
|
||||||
).toBe('success');
|
).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', () => {
|
describe('changeRequestScheduleProps', () => {
|
||||||
expect(
|
test('returns correct props for a pending schedule', () => {
|
||||||
determineColor(
|
const schedule = {
|
||||||
'Scheduled',
|
scheduledAt: new Date().toISOString(),
|
||||||
irrelevantIndex,
|
status: 'pending' as const,
|
||||||
'Scheduled',
|
};
|
||||||
irrelevantIndex,
|
|
||||||
'conflicts',
|
const time = 'some time string';
|
||||||
),
|
|
||||||
).toBe('error');
|
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', () => {
|
test('returns success for stages at or before activeIndex', () => {
|
||||||
|
@ -6,18 +6,24 @@ import TimelineSeparator from '@mui/lab/TimelineSeparator';
|
|||||||
import TimelineDot from '@mui/lab/TimelineDot';
|
import TimelineDot from '@mui/lab/TimelineDot';
|
||||||
import TimelineConnector from '@mui/lab/TimelineConnector';
|
import TimelineConnector from '@mui/lab/TimelineConnector';
|
||||||
import TimelineContent from '@mui/lab/TimelineContent';
|
import TimelineContent from '@mui/lab/TimelineContent';
|
||||||
import { ChangeRequestState } from '../../changeRequest.types';
|
import {
|
||||||
import { ConditionallyRender } from '../../../common/ConditionallyRender/ConditionallyRender';
|
ChangeRequestSchedule,
|
||||||
|
ChangeRequestState,
|
||||||
|
} from '../../changeRequest.types';
|
||||||
import { HtmlTooltip } from '../../../common/HtmlTooltip/HtmlTooltip';
|
import { HtmlTooltip } from '../../../common/HtmlTooltip/HtmlTooltip';
|
||||||
import { Error as ErrorIcon } from '@mui/icons-material';
|
import { Error as ErrorIcon } from '@mui/icons-material';
|
||||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
import { formatDateYMDHMS } from 'utils/formatDate';
|
import { formatDateYMDHMS } from 'utils/formatDate';
|
||||||
|
|
||||||
interface ISuggestChangeTimelineProps {
|
export type ISuggestChangeTimelineProps =
|
||||||
state: ChangeRequestState;
|
| {
|
||||||
scheduledAt?: string;
|
state: Exclude<ChangeRequestState, 'Scheduled'>;
|
||||||
failureReason?: string;
|
schedule?: undefined;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
state: 'Scheduled';
|
||||||
|
schedule: ChangeRequestSchedule;
|
||||||
|
};
|
||||||
|
|
||||||
const StyledPaper = styled(Paper)(({ theme }) => ({
|
const StyledPaper = styled(Paper)(({ theme }) => ({
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
@ -62,7 +68,6 @@ export const determineColor = (
|
|||||||
changeRequestStateIndex: number,
|
changeRequestStateIndex: number,
|
||||||
displayStage: ChangeRequestState,
|
displayStage: ChangeRequestState,
|
||||||
displayStageIndex: number,
|
displayStageIndex: number,
|
||||||
failureReason?: string,
|
|
||||||
) => {
|
) => {
|
||||||
if (changeRequestState === 'Cancelled') return 'grey';
|
if (changeRequestState === 'Cancelled') return 'grey';
|
||||||
|
|
||||||
@ -70,19 +75,9 @@ export const determineColor = (
|
|||||||
return displayStage === 'Rejected' ? 'error' : 'success';
|
return displayStage === 'Rejected' ? 'error' : 'success';
|
||||||
if (
|
if (
|
||||||
changeRequestStateIndex !== -1 &&
|
changeRequestStateIndex !== -1 &&
|
||||||
changeRequestStateIndex > displayStageIndex
|
changeRequestStateIndex >= displayStageIndex
|
||||||
)
|
)
|
||||||
return 'success';
|
return 'success';
|
||||||
if (
|
|
||||||
changeRequestStateIndex !== -1 &&
|
|
||||||
changeRequestStateIndex === displayStageIndex
|
|
||||||
) {
|
|
||||||
return changeRequestState === 'Scheduled'
|
|
||||||
? failureReason
|
|
||||||
? 'error'
|
|
||||||
: 'warning'
|
|
||||||
: 'success';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changeRequestStateIndex + 1 === displayStageIndex) return 'primary';
|
if (changeRequestStateIndex + 1 === displayStageIndex) return 'primary';
|
||||||
return 'grey';
|
return 'grey';
|
||||||
@ -90,8 +85,7 @@ export const determineColor = (
|
|||||||
|
|
||||||
export const ChangeRequestTimeline: FC<ISuggestChangeTimelineProps> = ({
|
export const ChangeRequestTimeline: FC<ISuggestChangeTimelineProps> = ({
|
||||||
state,
|
state,
|
||||||
scheduledAt,
|
schedule,
|
||||||
failureReason,
|
|
||||||
}) => {
|
}) => {
|
||||||
let data: ChangeRequestState[];
|
let data: ChangeRequestState[];
|
||||||
switch (state) {
|
switch (state) {
|
||||||
@ -106,28 +100,20 @@ export const ChangeRequestTimeline: FC<ISuggestChangeTimelineProps> = ({
|
|||||||
}
|
}
|
||||||
const activeIndex = data.findIndex((item) => item === state);
|
const activeIndex = data.findIndex((item) => item === state);
|
||||||
|
|
||||||
const { locationSettings } = useLocationSettings();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledPaper elevation={0}>
|
<StyledPaper elevation={0}>
|
||||||
<StyledBox>
|
<StyledBox>
|
||||||
<StyledTimeline>
|
<StyledTimeline>
|
||||||
{data.map((title, index) => {
|
{data.map((title, index) => {
|
||||||
const subtitle =
|
if (schedule && title === 'Scheduled') {
|
||||||
scheduledAt &&
|
return createTimelineScheduleItem(schedule);
|
||||||
state === 'Scheduled' &&
|
}
|
||||||
state === title
|
|
||||||
? formatDateYMDHMS(
|
|
||||||
new Date(scheduledAt),
|
|
||||||
locationSettings?.locale,
|
|
||||||
)
|
|
||||||
: undefined;
|
|
||||||
const color = determineColor(
|
const color = determineColor(
|
||||||
state,
|
state,
|
||||||
activeIndex,
|
activeIndex,
|
||||||
title,
|
title,
|
||||||
index,
|
index,
|
||||||
failureReason,
|
|
||||||
);
|
);
|
||||||
let timelineDotProps = {};
|
let timelineDotProps = {};
|
||||||
|
|
||||||
@ -142,8 +128,6 @@ export const ChangeRequestTimeline: FC<ISuggestChangeTimelineProps> = ({
|
|||||||
return createTimelineItem(
|
return createTimelineItem(
|
||||||
color,
|
color,
|
||||||
title,
|
title,
|
||||||
subtitle,
|
|
||||||
failureReason,
|
|
||||||
index < data.length - 1,
|
index < data.length - 1,
|
||||||
timelineDotProps,
|
timelineDotProps,
|
||||||
);
|
);
|
||||||
@ -157,8 +141,6 @@ export const ChangeRequestTimeline: FC<ISuggestChangeTimelineProps> = ({
|
|||||||
const createTimelineItem = (
|
const createTimelineItem = (
|
||||||
color: 'primary' | 'success' | 'grey' | 'error' | 'warning',
|
color: 'primary' | 'success' | 'grey' | 'error' | 'warning',
|
||||||
title: string,
|
title: string,
|
||||||
subtitle: string | undefined,
|
|
||||||
failureReason: string | undefined,
|
|
||||||
shouldConnectToNextItem: boolean,
|
shouldConnectToNextItem: boolean,
|
||||||
timelineDotProps: { [key: string]: string | undefined } = {},
|
timelineDotProps: { [key: string]: string | undefined } = {},
|
||||||
) => (
|
) => (
|
||||||
@ -167,33 +149,78 @@ const createTimelineItem = (
|
|||||||
<TimelineDot color={color} {...timelineDotProps} />
|
<TimelineDot color={color} {...timelineDotProps} />
|
||||||
{shouldConnectToNextItem && <TimelineConnector />}
|
{shouldConnectToNextItem && <TimelineConnector />}
|
||||||
</TimelineSeparator>
|
</TimelineSeparator>
|
||||||
|
<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>
|
<TimelineContent>
|
||||||
{title}
|
{title}
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(subtitle)}
|
|
||||||
show={
|
|
||||||
<StyledSubtitle>
|
<StyledSubtitle>
|
||||||
<Typography
|
<Typography
|
||||||
color={'text.secondary'}
|
color={'text.secondary'}
|
||||||
sx={{ mr: 1 }}
|
sx={{ mr: 1 }}
|
||||||
>{`(for ${subtitle})`}</Typography>
|
>{`(${subtitle})`}</Typography>
|
||||||
<ConditionallyRender
|
{reason}
|
||||||
condition={Boolean(failureReason)}
|
|
||||||
show={
|
|
||||||
<HtmlTooltip
|
|
||||||
title={`Schedule failed because of ${failureReason}`}
|
|
||||||
arrow
|
|
||||||
>
|
|
||||||
<ErrorIcon
|
|
||||||
color={'error'}
|
|
||||||
fontSize={'small'}
|
|
||||||
/>
|
|
||||||
</HtmlTooltip>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StyledSubtitle>
|
</StyledSubtitle>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TimelineContent>
|
</TimelineContent>
|
||||||
</TimelineItem>
|
</TimelineItem>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user