1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-11-10 01:19:53 +01:00

refactor: explicit scheduled milestone modelling (#10900)

This commit is contained in:
Mateusz Kwasniewski 2025-10-31 13:28:37 +01:00 committed by GitHub
parent 45fc547049
commit 96f7f2f1bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 64 additions and 68 deletions

View File

@ -37,7 +37,7 @@ const MilestoneListRendererCore = ({
onUpdateAutomation, onUpdateAutomation,
onDeleteAutomation, onDeleteAutomation,
}: MilestoneListRendererCoreProps) => { }: MilestoneListRendererCoreProps) => {
const status: MilestoneStatus = 'not-started'; const status: MilestoneStatus = { type: 'not-started' };
return ( return (
<> <>

View File

@ -4,13 +4,13 @@ import type { MilestoneStatus } from './ReleasePlanMilestoneStatus.tsx';
const StyledAutomationContainer = styled('div', { const StyledAutomationContainer = styled('div', {
shouldForwardProp: (prop) => prop !== 'status', shouldForwardProp: (prop) => prop !== 'status',
})<{ status?: MilestoneStatus }>(({ theme, status }) => ({ })<{ status?: MilestoneStatus }>(({ theme, status }) => ({
border: `${status === 'active' ? '1.5px' : '1px'} solid ${status === 'active' ? theme.palette.success.border : theme.palette.divider}`, border: `${status?.type === 'active' ? '1.5px' : '1px'} solid ${status?.type === 'active' ? theme.palette.success.border : theme.palette.divider}`,
borderTop: `1px solid ${theme.palette.divider}`, borderTop: `1px solid ${theme.palette.divider}`,
borderRadius: `0 0 ${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px`, borderRadius: `0 0 ${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px`,
padding: theme.spacing(1.5, 2), padding: theme.spacing(1.5, 2),
paddingLeft: theme.spacing(2.25), paddingLeft: theme.spacing(2.25),
backgroundColor: backgroundColor:
status === 'completed' status?.type === 'completed'
? theme.palette.background.default ? theme.palette.background.default
: theme.palette.background.paper, : theme.palette.background.paper,
display: 'flex', display: 'flex',

View File

@ -1,9 +1,8 @@
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined'; import HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined';
import type { IReleasePlanMilestone } from 'interfaces/releasePlans';
import { isToday, isTomorrow, format, addMinutes } from 'date-fns'; import { isToday, isTomorrow, format, addMinutes } from 'date-fns';
import { calculateMilestoneStartTime } from '../utils/calculateMilestoneStartTime.ts';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
import type { MilestoneStatus } from './ReleasePlanMilestoneStatus.tsx';
export const formatSmartDate = (date: Date): string => { export const formatSmartDate = (date: Date): string => {
const startTime = format(date, 'HH:mm'); const startTime = format(date, 'HH:mm');
@ -40,15 +39,11 @@ const StyledHourglassIcon = styled(HourglassEmptyOutlinedIcon)(({ theme }) => ({
})); }));
interface IMilestoneNextStartTimeProps { interface IMilestoneNextStartTimeProps {
milestone: IReleasePlanMilestone; status: MilestoneStatus;
allMilestones: IReleasePlanMilestone[];
activeMilestoneId?: string;
} }
export const MilestoneNextStartTime = ({ export const MilestoneNextStartTime = ({
milestone, status,
allMilestones,
activeMilestoneId,
}: IMilestoneNextStartTimeProps) => { }: IMilestoneNextStartTimeProps) => {
const milestoneProgressionEnabled = useUiFlag('milestoneProgression'); const milestoneProgressionEnabled = useUiFlag('milestoneProgression');
@ -56,24 +51,12 @@ export const MilestoneNextStartTime = ({
return null; return null;
} }
const activeIndex = allMilestones.findIndex( // Only show for not-started milestones with scheduledAt
(milestone) => milestone.id === activeMilestoneId, if (status.type !== 'not-started' || !status.scheduledAt) {
);
const currentIndex = allMilestones.findIndex((m) => m.id === milestone.id);
const isActiveMilestone = milestone.id === activeMilestoneId;
const isBehindActiveMilestone =
activeIndex !== -1 && currentIndex !== -1 && currentIndex < activeIndex;
if (isActiveMilestone || isBehindActiveMilestone) {
return null; return null;
} }
const projectedStartTime = calculateMilestoneStartTime( const projectedStartTime = status.scheduledAt;
allMilestones,
milestone.id,
activeMilestoneId,
);
if (!projectedStartTime) return null; if (!projectedStartTime) return null;

View File

@ -53,7 +53,7 @@ const StyledIcon = styled(BoltIcon, {
fontSize: 18, fontSize: 18,
flexShrink: 0, flexShrink: 0,
backgroundColor: backgroundColor:
status === 'completed' status?.type === 'completed'
? theme.palette.neutral.border ? theme.palette.neutral.border
: theme.palette.primary.main, : theme.palette.primary.main,
borderRadius: '50%', borderRadius: '50%',
@ -64,7 +64,7 @@ const StyledLabel = styled('span', {
shouldForwardProp: (prop) => prop !== 'status', shouldForwardProp: (prop) => prop !== 'status',
})<{ status?: MilestoneStatus }>(({ theme, status }) => ({ })<{ status?: MilestoneStatus }>(({ theme, status }) => ({
color: color:
status === 'completed' status?.type === 'completed'
? theme.palette.text.secondary ? theme.palette.text.secondary
: theme.palette.text.primary, : theme.palette.text.primary,
fontSize: theme.typography.body2.fontSize, fontSize: theme.typography.body2.fontSize,

View File

@ -23,15 +23,15 @@ const StyledAccordion = styled(Accordion, {
shouldForwardProp: (prop) => prop !== 'status' && prop !== 'hasAutomation', shouldForwardProp: (prop) => prop !== 'status' && prop !== 'hasAutomation',
})<{ status: MilestoneStatus; hasAutomation?: boolean }>( })<{ status: MilestoneStatus; hasAutomation?: boolean }>(
({ theme, status, hasAutomation }) => ({ ({ theme, status, hasAutomation }) => ({
border: `${status === 'active' ? '1.5px' : '1px'} solid ${status === 'active' ? theme.palette.success.border : theme.palette.divider}`, border: `${status.type === 'active' ? '1.5px' : '1px'} solid ${status.type === 'active' ? theme.palette.success.border : theme.palette.divider}`,
borderBottom: hasAutomation borderBottom: hasAutomation
? 'none' ? 'none'
: `${status === 'active' ? '1.5px' : '1px'} solid ${status === 'active' ? theme.palette.success.border : theme.palette.divider}`, : `${status.type === 'active' ? '1.5px' : '1px'} solid ${status.type === 'active' ? theme.palette.success.border : theme.palette.divider}`,
overflow: 'hidden', overflow: 'hidden',
boxShadow: 'none', boxShadow: 'none',
margin: 0, margin: 0,
backgroundColor: backgroundColor:
status === 'completed' status.type === 'completed'
? theme.palette.background.default ? theme.palette.background.default
: theme.palette.background.paper, : theme.palette.background.paper,
borderRadius: hasAutomation borderRadius: hasAutomation
@ -68,7 +68,7 @@ const StyledTitle = styled('span', {
})<{ status?: MilestoneStatus }>(({ theme, status }) => ({ })<{ status?: MilestoneStatus }>(({ theme, status }) => ({
fontWeight: theme.fontWeight.bold, fontWeight: theme.fontWeight.bold,
color: color:
status === 'completed' status?.type === 'completed'
? theme.palette.text.secondary ? theme.palette.text.secondary
: theme.palette.text.primary, : theme.palette.text.primary,
})); }));
@ -110,7 +110,7 @@ interface IReleasePlanMilestoneProps {
export const ReleasePlanMilestone = ({ export const ReleasePlanMilestone = ({
milestone, milestone,
status = 'not-started', status = { type: 'not-started' },
onStartMilestone, onStartMilestone,
readonly, readonly,
automationSection, automationSection,
@ -133,15 +133,12 @@ export const ReleasePlanMilestone = ({
{milestone.name} {milestone.name}
</StyledTitle> </StyledTitle>
{(!readonly && onStartMilestone) || {(!readonly && onStartMilestone) ||
(status === 'active' && milestone.startedAt) ? ( (status.type === 'active' &&
milestone.startedAt) ? (
<StyledStatusRow> <StyledStatusRow>
{!readonly && ( {!readonly && (
<MilestoneNextStartTime <MilestoneNextStartTime
milestone={milestone} status={status}
allMilestones={allMilestones}
activeMilestoneId={
activeMilestoneId
}
/> />
)} )}
{!readonly && onStartMilestone && ( {!readonly && onStartMilestone && (
@ -152,7 +149,7 @@ export const ReleasePlanMilestone = ({
} }
/> />
)} )}
{status === 'active' && {status.type === 'active' &&
milestone.startedAt && ( milestone.startedAt && (
<StyledStartedAt> <StyledStartedAt>
Started{' '} Started{' '}
@ -188,14 +185,10 @@ export const ReleasePlanMilestone = ({
{milestone.name} {milestone.name}
</StyledTitle> </StyledTitle>
{(!readonly && onStartMilestone) || {(!readonly && onStartMilestone) ||
(status === 'active' && milestone.startedAt) ? ( (status.type === 'active' && milestone.startedAt) ? (
<StyledStatusRow> <StyledStatusRow>
{!readonly && ( {!readonly && (
<MilestoneNextStartTime <MilestoneNextStartTime status={status} />
milestone={milestone}
allMilestones={allMilestones}
activeMilestoneId={activeMilestoneId}
/>
)} )}
{!readonly && onStartMilestone && ( {!readonly && onStartMilestone && (
<ReleasePlanMilestoneStatus <ReleasePlanMilestoneStatus
@ -205,10 +198,13 @@ export const ReleasePlanMilestone = ({
} }
/> />
)} )}
{status === 'active' && milestone.startedAt && ( {status.type === 'active' &&
milestone.startedAt && (
<StyledStartedAt> <StyledStartedAt>
Started{' '} Started{' '}
{formatDateYMDHMS(milestone.startedAt)} {formatDateYMDHMS(
milestone.startedAt,
)}
</StyledStartedAt> </StyledStartedAt>
)} )}
</StyledStatusRow> </StyledStatusRow>

View File

@ -4,7 +4,11 @@ import PauseCircleIcon from '@mui/icons-material/PauseCircle';
import TripOriginIcon from '@mui/icons-material/TripOrigin'; import TripOriginIcon from '@mui/icons-material/TripOrigin';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
export type MilestoneStatus = 'not-started' | 'active' | 'paused' | 'completed'; export type MilestoneStatus =
| { type: 'not-started'; scheduledAt?: Date }
| { type: 'active' }
| { type: 'paused' }
| { type: 'completed' };
const StyledStatusButton = styled('button', { const StyledStatusButton = styled('button', {
shouldForwardProp: (prop) => prop !== 'status', shouldForwardProp: (prop) => prop !== 'status',
@ -18,7 +22,7 @@ const StyledStatusButton = styled('button', {
paddingRight: theme.spacing(1), paddingRight: theme.spacing(1),
cursor: 'pointer', cursor: 'pointer',
backgroundColor: backgroundColor:
status === 'active' status.type === 'active'
? theme.palette.success.light ? theme.palette.success.light
: theme.palette.neutral.light, : theme.palette.neutral.light,
'&:focus-visible': { '&:focus-visible': {
@ -26,9 +30,9 @@ const StyledStatusButton = styled('button', {
}, },
'&:hover': { '&:hover': {
backgroundColor: backgroundColor:
status === 'active' status.type === 'active'
? theme.palette.success.light ? theme.palette.success.light
: status === 'paused' : status.type === 'paused'
? 'transparent' ? 'transparent'
: theme.palette.neutral.light, : theme.palette.neutral.light,
textDecoration: 'none', textDecoration: 'none',
@ -38,18 +42,18 @@ const StyledStatusButton = styled('button', {
fontWeight: theme.fontWeight.medium, fontWeight: theme.fontWeight.medium,
borderRadius: theme.shape.borderRadiusMedium, borderRadius: theme.shape.borderRadiusMedium,
color: color:
status === 'active' status.type === 'active'
? theme.palette.success.contrastText ? theme.palette.success.contrastText
: status === 'paused' : status.type === 'paused'
? theme.palette.text.primary ? theme.palette.text.primary
: theme.palette.primary.main, : theme.palette.primary.main,
'& svg': { '& svg': {
color: color:
status === 'active' status.type === 'active'
? theme.palette.success.main ? theme.palette.success.main
: status === 'paused' : status.type === 'paused'
? theme.palette.text.disabled ? theme.palette.text.disabled
: status === 'completed' : status.type === 'completed'
? theme.palette.neutral.border ? theme.palette.neutral.border
: theme.palette.primary.main, : theme.palette.primary.main,
height: theme.spacing(3), height: theme.spacing(3),
@ -70,7 +74,7 @@ const getStatusText = (
status: MilestoneStatus, status: MilestoneStatus,
progressionsEnabled: boolean, progressionsEnabled: boolean,
): string => { ): string => {
switch (status) { switch (status.type) {
case 'active': case 'active':
return 'Running'; return 'Running';
case 'paused': case 'paused':
@ -83,7 +87,7 @@ const getStatusText = (
}; };
const getStatusIcon = (status: MilestoneStatus) => { const getStatusIcon = (status: MilestoneStatus) => {
switch (status) { switch (status.type) {
case 'active': case 'active':
return <TripOriginIcon />; return <TripOriginIcon />;
case 'paused': case 'paused':
@ -101,7 +105,7 @@ export const ReleasePlanMilestoneStatus = ({
const statusText = getStatusText(status, milestoneProgressionsEnabled); const statusText = getStatusText(status, milestoneProgressionsEnabled);
const statusIcon = getStatusIcon(status); const statusIcon = getStatusIcon(status);
const disabled = status === 'active' || status === 'paused'; const disabled = status.type === 'active' || status.type === 'paused';
return ( return (
<StyledStatusButton <StyledStatusButton

View File

@ -148,6 +148,7 @@ export const ReleasePlanMilestoneItem = ({
index, index,
activeIndex, activeIndex,
environmentIsDisabled, environmentIsDisabled,
milestones,
); );
const { pendingProgressionChange, effectiveTransitionCondition } = const { pendingProgressionChange, effectiveTransitionCondition } =

View File

@ -1,5 +1,6 @@
import type { IReleasePlanMilestone } from 'interfaces/releasePlans'; import type { IReleasePlanMilestone } from 'interfaces/releasePlans';
import type { MilestoneStatus } from '../ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx'; import type { MilestoneStatus } from '../ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx';
import { calculateMilestoneStartTime } from '../utils/calculateMilestoneStartTime.js';
export const calculateMilestoneStatus = ( export const calculateMilestoneStatus = (
milestone: IReleasePlanMilestone, milestone: IReleasePlanMilestone,
@ -7,14 +8,24 @@ export const calculateMilestoneStatus = (
index: number, index: number,
activeIndex: number, activeIndex: number,
environmentIsDisabled: boolean | undefined, environmentIsDisabled: boolean | undefined,
allMilestones: IReleasePlanMilestone[],
): MilestoneStatus => { ): MilestoneStatus => {
if (milestone.id === activeMilestoneId) { if (milestone.id === activeMilestoneId) {
return environmentIsDisabled ? 'paused' : 'active'; return environmentIsDisabled ? { type: 'paused' } : { type: 'active' };
} }
if (index < activeIndex) { if (index < activeIndex) {
return 'completed'; return { type: 'completed' };
} }
return 'not-started'; const scheduledAt = calculateMilestoneStartTime(
allMilestones,
milestone.id,
activeMilestoneId,
);
return {
type: 'not-started',
scheduledAt: scheduledAt || undefined,
};
}; };

View File

@ -88,7 +88,8 @@ export const useMilestoneProgressionForm = (
if ( if (
sourceMilestoneStartedAt && sourceMilestoneStartedAt &&
total > 0 && total > 0 &&
(status === 'active' || status === 'paused') status &&
(status.type === 'active' || status.type === 'paused')
) { ) {
const startDate = new Date(sourceMilestoneStartedAt); const startDate = new Date(sourceMilestoneStartedAt);
const nextMilestoneDate = addMinutes(startDate, total); const nextMilestoneDate = addMinutes(startDate, total);