1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-13 13:48:59 +02:00

Show change request stage timestamps in UI

Respects user's locale, uses `time` element.

Does not address potential collisions with scheduled change requests, which have additional time information.
This commit is contained in:
Thomas Heartman 2025-07-22 14:01:56 +02:00
parent 64050121db
commit ca128cc7f6
No known key found for this signature in database
GPG Key ID: BD1F880DAED1EE78
2 changed files with 93 additions and 44 deletions

View File

@ -349,7 +349,13 @@ export const ChangeRequestOverview: FC = () => {
<ChangeRequestHeader changeRequest={changeRequest} /> <ChangeRequestHeader changeRequest={changeRequest} />
<ChangeRequestBody> <ChangeRequestBody>
<StyledAsideBox> <StyledAsideBox>
<ChangeRequestTimeline {...timelineProps} /> <ChangeRequestTimeline
{...timelineProps}
timestamps={
//@ts-expect-error This hasn't been put on the model yet
changeRequest.stateTransitionTimestamps || {}
}
/>
<ConditionallyRender <ConditionallyRender
condition={approversEnabled} condition={approversEnabled}
show={ show={

View File

@ -1,7 +1,7 @@
import type { FC } from 'react'; import type { FC } from 'react';
import { Box, Paper, styled, Typography } from '@mui/material'; import { Box, Paper, styled, Typography } from '@mui/material';
import Timeline from '@mui/lab/Timeline'; import Timeline from '@mui/lab/Timeline';
import TimelineItem, { timelineItemClasses } from '@mui/lab/TimelineItem'; import MuiTimelineItem, { timelineItemClasses } from '@mui/lab/TimelineItem';
import TimelineSeparator from '@mui/lab/TimelineSeparator'; 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';
@ -12,13 +12,13 @@ import type {
} from '../../changeRequest.types'; } from '../../changeRequest.types';
import { HtmlTooltip } from '../../../common/HtmlTooltip/HtmlTooltip.tsx'; import { HtmlTooltip } from '../../../common/HtmlTooltip/HtmlTooltip.tsx';
import ErrorIcon from '@mui/icons-material/Error'; import ErrorIcon from '@mui/icons-material/Error';
import { import { useLocationSettings } from 'hooks/useLocationSettings';
type ILocationSettings, import { formatDateYMDHM } from 'utils/formatDate';
useLocationSettings, import type { ReactNode } from 'react-markdown/lib/react-markdown';
} from 'hooks/useLocationSettings';
import { formatDateYMDHMS } from 'utils/formatDate';
export type ISuggestChangeTimelineProps = export type ISuggestChangeTimelineProps = {
timestamps?: Record<ChangeRequestState, string>;
} & (
| { | {
state: Exclude<ChangeRequestState, 'Scheduled'>; state: Exclude<ChangeRequestState, 'Scheduled'>;
schedule?: undefined; schedule?: undefined;
@ -26,7 +26,8 @@ export type ISuggestChangeTimelineProps =
| { | {
state: 'Scheduled'; state: 'Scheduled';
schedule: ChangeRequestSchedule; schedule: ChangeRequestSchedule;
}; }
);
const StyledPaper = styled(Paper)(({ theme }) => ({ const StyledPaper = styled(Paper)(({ theme }) => ({
marginTop: theme.spacing(2), marginTop: theme.spacing(2),
@ -89,6 +90,7 @@ export const determineColor = (
export const ChangeRequestTimeline: FC<ISuggestChangeTimelineProps> = ({ export const ChangeRequestTimeline: FC<ISuggestChangeTimelineProps> = ({
state, state,
schedule, schedule,
timestamps,
}) => { }) => {
let data: ChangeRequestState[]; let data: ChangeRequestState[];
switch (state) { switch (state) {
@ -109,10 +111,17 @@ export const ChangeRequestTimeline: FC<ISuggestChangeTimelineProps> = ({
<StyledBox> <StyledBox>
<StyledTimeline> <StyledTimeline>
{data.map((title, index) => { {data.map((title, index) => {
const timestampComponent = timestamps?.[title] ? (
<Timestamp timestamp={timestamps?.[title]} />
) : undefined;
if (schedule && title === 'Scheduled') { if (schedule && title === 'Scheduled') {
return createTimelineScheduleItem( return (
schedule, <TimelineScheduleItem
locationSettings, key={title}
schedule={schedule}
timestamp={timestampComponent}
/>
); );
} }
@ -132,11 +141,17 @@ export const ChangeRequestTimeline: FC<ISuggestChangeTimelineProps> = ({
timelineDotProps = { variant: 'outlined' }; timelineDotProps = { variant: 'outlined' };
} }
return createTimelineItem( return (
color, <TimelineItem
title, key={title}
index < data.length - 1, color={color}
timelineDotProps, title={title}
shouldConnectToNextItem={
index < data.length - 1
}
timestamp={timestampComponent}
timelineDotProps={timelineDotProps}
/>
); );
})} })}
</StyledTimeline> </StyledTimeline>
@ -145,24 +160,53 @@ export const ChangeRequestTimeline: FC<ISuggestChangeTimelineProps> = ({
); );
}; };
const createTimelineItem = ( const Timestamp = styled(({ timestamp, ...props }: { timestamp: string }) => {
color: 'primary' | 'success' | 'grey' | 'error' | 'warning', const { locationSettings } = useLocationSettings();
title: string, const displayTime = formatDateYMDHM(
shouldConnectToNextItem: boolean, new Date(timestamp || ''),
timelineDotProps: { [key: string]: string | undefined } = {}, locationSettings.locale,
) => ( );
<TimelineItem key={title}> return (
<time {...props} dateTime={timestamp}>
{displayTime}
</time>
);
})(({ theme }) => ({
color: theme.palette.text.secondary,
fontSize: theme.typography.body2.fontSize,
display: 'block',
}));
const TimelineItem = ({
color,
title,
shouldConnectToNextItem,
timestamp,
timelineDotProps = {},
}: {
color: 'primary' | 'success' | 'grey' | 'error' | 'warning';
title: string;
shouldConnectToNextItem: boolean;
timestamp?: ReactNode;
timelineDotProps: { [key: string]: string | undefined };
}) => {
return (
<MuiTimelineItem key={title}>
<TimelineSeparator> <TimelineSeparator>
<TimelineDot color={color} {...timelineDotProps} /> <TimelineDot color={color} {...timelineDotProps} />
{shouldConnectToNextItem && <TimelineConnector />} {shouldConnectToNextItem && <TimelineConnector />}
</TimelineSeparator> </TimelineSeparator>
<TimelineContent>{title}</TimelineContent> <TimelineContent>
</TimelineItem> {title}
); {timestamp}
</TimelineContent>
</MuiTimelineItem>
);
};
export const getScheduleProps = ( export const getScheduleProps = (
schedule: ChangeRequestSchedule, schedule: ChangeRequestSchedule,
formattedTime: string, formattedTime: string = '2025/09/22 12:27',
) => { ) => {
switch (schedule.status) { switch (schedule.status) {
case 'suspended': case 'suspended':
@ -202,25 +246,24 @@ export const getScheduleProps = (
} }
}; };
const createTimelineScheduleItem = ( const TimelineScheduleItem = ({
schedule: ChangeRequestSchedule, schedule,
locationSettings: ILocationSettings, timestamp,
) => { }: {
const time = formatDateYMDHMS( schedule: ChangeRequestSchedule;
new Date(schedule.scheduledAt), timestamp: ReactNode;
locationSettings?.locale, }) => {
); const { title, subtitle, color, reason } = getScheduleProps(schedule);
const { title, subtitle, color, reason } = getScheduleProps(schedule, time);
return ( return (
<TimelineItem key={title}> <MuiTimelineItem key={title}>
<TimelineSeparator> <TimelineSeparator>
<TimelineDot color={color} /> <TimelineDot color={color} />
<TimelineConnector /> <TimelineConnector />
</TimelineSeparator> </TimelineSeparator>
<TimelineContent> <TimelineContent>
{title} {title}
{timestamp}
<StyledSubtitle> <StyledSubtitle>
<Typography <Typography
color={'text.secondary'} color={'text.secondary'}
@ -229,6 +272,6 @@ const createTimelineScheduleItem = (
{reason} {reason}
</StyledSubtitle> </StyledSubtitle>
</TimelineContent> </TimelineContent>
</TimelineItem> </MuiTimelineItem>
); );
}; };