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,
onDeleteAutomation,
}: MilestoneListRendererCoreProps) => {
const status: MilestoneStatus = 'not-started';
const status: MilestoneStatus = { type: 'not-started' };
return (
<>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import type { IReleasePlanMilestone } from 'interfaces/releasePlans';
import type { MilestoneStatus } from '../ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx';
import { calculateMilestoneStartTime } from '../utils/calculateMilestoneStartTime.js';
export const calculateMilestoneStatus = (
milestone: IReleasePlanMilestone,
@ -7,14 +8,24 @@ export const calculateMilestoneStatus = (
index: number,
activeIndex: number,
environmentIsDisabled: boolean | undefined,
allMilestones: IReleasePlanMilestone[],
): MilestoneStatus => {
if (milestone.id === activeMilestoneId) {
return environmentIsDisabled ? 'paused' : 'active';
return environmentIsDisabled ? { type: 'paused' } : { type: 'active' };
}
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 (
sourceMilestoneStartedAt &&
total > 0 &&
(status === 'active' || status === 'paused')
status &&
(status.type === 'active' || status.type === 'paused')
) {
const startDate = new Date(sourceMilestoneStartedAt);
const nextMilestoneDate = addMinutes(startDate, total);