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:
parent
45fc547049
commit
96f7f2f1bf
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -148,6 +148,7 @@ export const ReleasePlanMilestoneItem = ({
|
|||||||
index,
|
index,
|
||||||
activeIndex,
|
activeIndex,
|
||||||
environmentIsDisabled,
|
environmentIsDisabled,
|
||||||
|
milestones,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { pendingProgressionChange, effectiveTransitionCondition } =
|
const { pendingProgressionChange, effectiveTransitionCondition } =
|
||||||
|
|||||||
@ -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,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user