mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-04 13:48:56 +02:00
feat: rename lifecycle stages (#9102)
Name names for "lifecycle" stages, and aligning frontend types.
This commit is contained in:
parent
846dae66bd
commit
50ab2c9d61
@ -9,23 +9,39 @@ import { ReactComponent as Stage2 } from 'assets/icons/lifecycle/stage-2.svg';
|
|||||||
import { ReactComponent as Stage3 } from 'assets/icons/lifecycle/stage-3.svg';
|
import { ReactComponent as Stage3 } from 'assets/icons/lifecycle/stage-3.svg';
|
||||||
import { ReactComponent as Stage4 } from 'assets/icons/lifecycle/stage-4.svg';
|
import { ReactComponent as Stage4 } from 'assets/icons/lifecycle/stage-4.svg';
|
||||||
import { ReactComponent as Stage5 } from 'assets/icons/lifecycle/stage-5.svg';
|
import { ReactComponent as Stage5 } from 'assets/icons/lifecycle/stage-5.svg';
|
||||||
import type { LifecycleStage } from './LifecycleStage';
|
import type { LifecycleStage } from '../../feature/FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
export const FeatureLifecycleStageIcon: FC<{
|
export const FeatureLifecycleStageIcon: FC<{
|
||||||
stage: Pick<LifecycleStage, 'name'>;
|
stage: Pick<LifecycleStage, 'name'>;
|
||||||
}> = ({ stage }) => {
|
}> = ({ stage, ...props }) => {
|
||||||
const newIcons = useUiFlag('lifecycleImprovements');
|
const newIcons = useUiFlag('lifecycleImprovements');
|
||||||
|
|
||||||
if (stage.name === 'archived') {
|
if (stage.name === 'archived') {
|
||||||
return newIcons ? <Stage5 /> : <ArchivedStageIcon />;
|
return newIcons ? (
|
||||||
|
<Stage5 {...props} />
|
||||||
|
) : (
|
||||||
|
<ArchivedStageIcon {...props} />
|
||||||
|
);
|
||||||
} else if (stage.name === 'pre-live') {
|
} else if (stage.name === 'pre-live') {
|
||||||
return newIcons ? <Stage2 /> : <PreLiveStageIcon />;
|
return newIcons ? (
|
||||||
|
<Stage2 {...props} />
|
||||||
|
) : (
|
||||||
|
<PreLiveStageIcon {...props} />
|
||||||
|
);
|
||||||
} else if (stage.name === 'live') {
|
} else if (stage.name === 'live') {
|
||||||
return newIcons ? <Stage3 /> : <LiveStageIcon />;
|
return newIcons ? <Stage3 {...props} /> : <LiveStageIcon {...props} />;
|
||||||
} else if (stage.name === 'completed') {
|
} else if (stage.name === 'completed') {
|
||||||
return newIcons ? <Stage4 /> : <CompletedStageIcon />;
|
return newIcons ? (
|
||||||
|
<Stage4 {...props} />
|
||||||
|
) : (
|
||||||
|
<CompletedStageIcon {...props} />
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return newIcons ? <Stage1 /> : <InitialStageIcon />;
|
return newIcons ? (
|
||||||
|
<Stage1 {...props} />
|
||||||
|
) : (
|
||||||
|
<InitialStageIcon {...props} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
import type { Lifecycle } from 'interfaces/featureToggle';
|
||||||
|
|
||||||
|
export const getFeatureLifecycleName = (stage: Lifecycle['stage']): string => {
|
||||||
|
if (stage === 'initial') {
|
||||||
|
return 'Define';
|
||||||
|
}
|
||||||
|
if (stage === 'pre-live') {
|
||||||
|
return 'Develop';
|
||||||
|
}
|
||||||
|
if (stage === 'live') {
|
||||||
|
return 'Production';
|
||||||
|
}
|
||||||
|
if (stage === 'completed') {
|
||||||
|
return 'Cleanup';
|
||||||
|
}
|
||||||
|
if (stage === 'archived') {
|
||||||
|
return 'Archived';
|
||||||
|
}
|
||||||
|
|
||||||
|
return stage;
|
||||||
|
};
|
@ -1,10 +1,12 @@
|
|||||||
import { FeatureLifecycleStageIcon } from './FeatureLifecycleStageIcon';
|
import { FeatureLifecycleStageIcon } from 'component/common/FeatureLifecycle/FeatureLifecycleStageIcon';
|
||||||
|
import { FeatureLifecycleTooltip as LegacyFeatureLifecycleTooltip } from './LegacyFeatureLifecycleTooltip';
|
||||||
import { FeatureLifecycleTooltip } from './FeatureLifecycleTooltip';
|
import { FeatureLifecycleTooltip } from './FeatureLifecycleTooltip';
|
||||||
import useFeatureLifecycleApi from 'hooks/api/actions/useFeatureLifecycleApi/useFeatureLifecycleApi';
|
import useFeatureLifecycleApi from 'hooks/api/actions/useFeatureLifecycleApi/useFeatureLifecycleApi';
|
||||||
import { populateCurrentStage } from './populateCurrentStage';
|
import { populateCurrentStage } from './populateCurrentStage';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import type { Lifecycle } from 'interfaces/featureToggle';
|
import type { Lifecycle } from 'interfaces/featureToggle';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
export interface LifecycleFeature {
|
export interface LifecycleFeature {
|
||||||
lifecycle?: Lifecycle;
|
lifecycle?: Lifecycle;
|
||||||
@ -25,10 +27,9 @@ export const FeatureLifecycle: FC<{
|
|||||||
feature: LifecycleFeature;
|
feature: LifecycleFeature;
|
||||||
}> = ({ feature, onComplete, onUncomplete, onArchive }) => {
|
}> = ({ feature, onComplete, onUncomplete, onArchive }) => {
|
||||||
const currentStage = populateCurrentStage(feature);
|
const currentStage = populateCurrentStage(feature);
|
||||||
|
|
||||||
const { markFeatureUncompleted, loading } = useFeatureLifecycleApi();
|
const { markFeatureUncompleted, loading } = useFeatureLifecycleApi();
|
||||||
|
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
const isLifecycleImprovementsEnabled = useUiFlag('lifecycleImprovements');
|
||||||
|
|
||||||
const onUncompleteHandler = async () => {
|
const onUncompleteHandler = async () => {
|
||||||
await markFeatureUncompleted(feature.name, feature.project);
|
await markFeatureUncompleted(feature.name, feature.project);
|
||||||
@ -40,8 +41,23 @@ export const FeatureLifecycle: FC<{
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isLifecycleImprovementsEnabled) {
|
||||||
|
return currentStage ? (
|
||||||
|
<FeatureLifecycleTooltip
|
||||||
|
stage={currentStage!}
|
||||||
|
project={feature.project}
|
||||||
|
onArchive={onArchive}
|
||||||
|
onComplete={onComplete}
|
||||||
|
onUncomplete={onUncompleteHandler}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
<FeatureLifecycleStageIcon stage={currentStage} />
|
||||||
|
</FeatureLifecycleTooltip>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
return currentStage ? (
|
return currentStage ? (
|
||||||
<FeatureLifecycleTooltip
|
<LegacyFeatureLifecycleTooltip
|
||||||
stage={currentStage!}
|
stage={currentStage!}
|
||||||
project={feature.project}
|
project={feature.project}
|
||||||
onArchive={onArchive}
|
onArchive={onArchive}
|
||||||
@ -49,7 +65,7 @@ export const FeatureLifecycle: FC<{
|
|||||||
onUncomplete={onUncompleteHandler}
|
onUncomplete={onUncompleteHandler}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
>
|
>
|
||||||
<FeatureLifecycleStageIcon stage={currentStage!} />
|
<FeatureLifecycleStageIcon stage={currentStage} />
|
||||||
</FeatureLifecycleTooltip>
|
</LegacyFeatureLifecycleTooltip>
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
@ -51,7 +51,7 @@ test('render initial stage', async () => {
|
|||||||
|
|
||||||
renderOpenTooltip({ name: 'initial', enteredStageAt });
|
renderOpenTooltip({ name: 'initial', enteredStageAt });
|
||||||
|
|
||||||
await screen.findByText('initial');
|
await screen.findByText('Define');
|
||||||
await screen.findByText('2 minutes');
|
await screen.findByText('2 minutes');
|
||||||
await screen.findByText(
|
await screen.findByText(
|
||||||
'This feature flag is currently in the initial phase of its lifecycle.',
|
'This feature flag is currently in the initial phase of its lifecycle.',
|
||||||
@ -69,7 +69,7 @@ test('render pre-live stage', async () => {
|
|||||||
enteredStageAt,
|
enteredStageAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
await screen.findByText('pre-live');
|
await screen.findByText('Develop');
|
||||||
await screen.findByText('development');
|
await screen.findByText('development');
|
||||||
await screen.findByText('1 hour ago');
|
await screen.findByText('1 hour ago');
|
||||||
});
|
});
|
||||||
@ -86,8 +86,8 @@ test('render live stage', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await screen.findByText('Is this feature complete?');
|
await screen.findByText('Is this feature complete?');
|
||||||
await screen.findByText('live');
|
await screen.findByText('Production');
|
||||||
await screen.findByText('production');
|
// await screen.findByText('production');
|
||||||
await screen.findByText('2 hours ago');
|
await screen.findByText('2 hours ago');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ test('render completed stage with still active', async () => {
|
|||||||
enteredStageAt,
|
enteredStageAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
await screen.findByText('completed');
|
await screen.findByText('Cleanup');
|
||||||
await screen.findByText('production');
|
await screen.findByText('production');
|
||||||
await screen.findByText('2 hours ago');
|
await screen.findByText('2 hours ago');
|
||||||
expect(screen.queryByText('Archive feature')).not.toBeInTheDocument();
|
expect(screen.queryByText('Archive feature')).not.toBeInTheDocument();
|
||||||
@ -127,7 +127,7 @@ test('render completed stage safe to archive', async () => {
|
|||||||
onArchive,
|
onArchive,
|
||||||
);
|
);
|
||||||
|
|
||||||
await screen.findByText('completed');
|
await screen.findByText('Cleanup');
|
||||||
const button = await screen.findByText('Archive feature');
|
const button = await screen.findByText('Archive feature');
|
||||||
button.click();
|
button.click();
|
||||||
|
|
||||||
@ -153,7 +153,7 @@ test('mark completed button gets activated', async () => {
|
|||||||
onComplete,
|
onComplete,
|
||||||
);
|
);
|
||||||
|
|
||||||
await screen.findByText('live');
|
await screen.findByText('Production');
|
||||||
const button = await screen.findByText('Mark completed');
|
const button = await screen.findByText('Mark completed');
|
||||||
button.click();
|
button.click();
|
||||||
|
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import { Box, styled, Typography } from '@mui/material';
|
import { Box, styled, Typography } from '@mui/material';
|
||||||
import { Badge } from 'component/common/Badge/Badge';
|
|
||||||
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||||
import type * as React from 'react';
|
import type * as React from 'react';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import CloudCircle from '@mui/icons-material/CloudCircle';
|
import CloudCircle from '@mui/icons-material/CloudCircle';
|
||||||
import { ReactComponent as UsageRate } from 'assets/icons/usage-rate.svg';
|
import { ReactComponent as UsageRate } from 'assets/icons/usage-rate.svg';
|
||||||
import { FeatureLifecycleStageIcon } from './FeatureLifecycleStageIcon';
|
import { FeatureLifecycleStageIcon } from 'component/common/FeatureLifecycle/FeatureLifecycleStageIcon';
|
||||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||||
import { StyledIconWrapper } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen';
|
import { StyledIconWrapper } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen';
|
||||||
import { useLastSeenColors } from '../../FeatureEnvironmentSeen/useLastSeenColors';
|
import { useLastSeenColors } from '../../FeatureEnvironmentSeen/useLastSeenColors';
|
||||||
@ -20,6 +19,7 @@ import { isSafeToArchive } from './isSafeToArchive';
|
|||||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
import { formatDateYMDHMS } from 'utils/formatDate';
|
import { formatDateYMDHMS } from 'utils/formatDate';
|
||||||
import { formatDistanceToNow, parseISO } from 'date-fns';
|
import { formatDistanceToNow, parseISO } from 'date-fns';
|
||||||
|
import { getFeatureLifecycleName } from 'component/common/FeatureLifecycle/getFeatureLifecycleName';
|
||||||
|
|
||||||
const TimeLabel = styled('span')(({ theme }) => ({
|
const TimeLabel = styled('span')(({ theme }) => ({
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
@ -472,9 +472,9 @@ export const FeatureLifecycleTooltip: FC<{
|
|||||||
gap: 1,
|
gap: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Badge sx={{ textTransform: 'capitalize' }}>
|
<Typography variant='body2'>
|
||||||
{stage.name}
|
{getFeatureLifecycleName(stage.name)}
|
||||||
</Badge>
|
</Typography>
|
||||||
<FeatureLifecycleStageIcon stage={stage} />
|
<FeatureLifecycleStageIcon stage={stage} />
|
||||||
</Box>
|
</Box>
|
||||||
</MainLifecycleRow>
|
</MainLifecycleRow>
|
||||||
@ -487,7 +487,6 @@ export const FeatureLifecycleTooltip: FC<{
|
|||||||
<TimeLabel>Time spent in stage</TimeLabel>
|
<TimeLabel>Time spent in stage</TimeLabel>
|
||||||
<FormatElapsedTime time={stage.enteredStageAt} />
|
<FormatElapsedTime time={stage.enteredStageAt} />
|
||||||
</TimeLifecycleRow>
|
</TimeLifecycleRow>
|
||||||
<StageTimeline stage={stage} />
|
|
||||||
</Box>
|
</Box>
|
||||||
<ColorFill>
|
<ColorFill>
|
||||||
{stage.name === 'initial' && <InitialStageDescription />}
|
{stage.name === 'initial' && <InitialStageDescription />}
|
||||||
|
@ -0,0 +1,526 @@
|
|||||||
|
import { Box, styled, Typography } from '@mui/material';
|
||||||
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
|
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||||
|
import type * as React from 'react';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import CloudCircle from '@mui/icons-material/CloudCircle';
|
||||||
|
import { ReactComponent as UsageRate } from 'assets/icons/usage-rate.svg';
|
||||||
|
import { FeatureLifecycleStageIcon } from 'component/common/FeatureLifecycle/FeatureLifecycleStageIcon';
|
||||||
|
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||||
|
import { StyledIconWrapper } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen';
|
||||||
|
import { useLastSeenColors } from '../../FeatureEnvironmentSeen/useLastSeenColors';
|
||||||
|
import type { LifecycleStage } from './LifecycleStage';
|
||||||
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
|
import {
|
||||||
|
DELETE_FEATURE,
|
||||||
|
UPDATE_FEATURE,
|
||||||
|
} from 'component/providers/AccessProvider/permissions';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { isSafeToArchive } from './isSafeToArchive';
|
||||||
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
|
import { formatDateYMDHMS } from 'utils/formatDate';
|
||||||
|
import { formatDistanceToNow, parseISO } from 'date-fns';
|
||||||
|
|
||||||
|
const TimeLabel = styled('span')(({ theme }) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const InfoText = styled('p')(({ theme }) => ({
|
||||||
|
paddingBottom: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const MainLifecycleRow = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const TimeLifecycleRow = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: theme.spacing(1.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const IconsRow = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: theme.spacing(4),
|
||||||
|
marginBottom: theme.spacing(6),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Line = styled(Box)(({ theme }) => ({
|
||||||
|
height: '1px',
|
||||||
|
background: theme.palette.divider,
|
||||||
|
flex: 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StageBox = styled(Box, {
|
||||||
|
shouldForwardProp: (prop) => prop !== 'active',
|
||||||
|
})<{
|
||||||
|
active?: boolean;
|
||||||
|
}>(({ theme, active }) => ({
|
||||||
|
position: 'relative',
|
||||||
|
// speech bubble triangle for active stage
|
||||||
|
...(active && {
|
||||||
|
'&:before': {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
display: 'block',
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderColor: `${theme.palette.primary.light} transparent`,
|
||||||
|
borderWidth: '0 6px 6px',
|
||||||
|
top: theme.spacing(3.25),
|
||||||
|
left: theme.spacing(1.75),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// stage name text
|
||||||
|
'&:after': {
|
||||||
|
content: 'attr(data-after-content)',
|
||||||
|
display: 'block',
|
||||||
|
position: 'absolute',
|
||||||
|
top: theme.spacing(4),
|
||||||
|
left: theme.spacing(-1.25),
|
||||||
|
right: theme.spacing(-1.25),
|
||||||
|
textAlign: 'center',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
fontSize: theme.spacing(1.25),
|
||||||
|
padding: theme.spacing(0.25, 0),
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
// active wrapper for stage name text
|
||||||
|
...(active && {
|
||||||
|
backgroundColor: theme.palette.primary.light,
|
||||||
|
color: theme.palette.primary.contrastText,
|
||||||
|
fontWeight: theme.typography.fontWeightBold,
|
||||||
|
borderRadius: theme.spacing(0.5),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ColorFill = styled(Box)(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.primary.light,
|
||||||
|
color: theme.palette.primary.contrastText,
|
||||||
|
borderRadius: `0 0 ${theme.shape.borderRadiusMedium}px ${theme.shape.borderRadiusMedium}px`, // has to match the parent tooltip container
|
||||||
|
margin: theme.spacing(-1, -1.5), // has to match the parent tooltip container
|
||||||
|
padding: theme.spacing(2, 3),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const LastSeenIcon: FC<{
|
||||||
|
lastSeen: string;
|
||||||
|
}> = ({ lastSeen }) => {
|
||||||
|
const getColor = useLastSeenColors();
|
||||||
|
const { text, background } = getColor(lastSeen);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledIconWrapper style={{ background }}>
|
||||||
|
<UsageRate stroke={text} />
|
||||||
|
</StyledIconWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InitialStageDescription: FC = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InfoText>
|
||||||
|
This feature flag is currently in the initial phase of its
|
||||||
|
lifecycle.
|
||||||
|
</InfoText>
|
||||||
|
<InfoText>
|
||||||
|
This means that the flag has been created, but it has not yet
|
||||||
|
been seen in any environment.
|
||||||
|
</InfoText>
|
||||||
|
<InfoText>
|
||||||
|
Once we detect metrics for a non-production environment it will
|
||||||
|
move into pre-live.
|
||||||
|
</InfoText>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const StageTimeline: FC<{
|
||||||
|
stage: LifecycleStage;
|
||||||
|
}> = ({ stage }) => {
|
||||||
|
return (
|
||||||
|
<IconsRow>
|
||||||
|
<StageBox
|
||||||
|
data-after-content='Initial'
|
||||||
|
active={stage.name === 'initial'}
|
||||||
|
>
|
||||||
|
<FeatureLifecycleStageIcon stage={{ name: 'initial' }} />
|
||||||
|
</StageBox>
|
||||||
|
|
||||||
|
<Line />
|
||||||
|
|
||||||
|
<StageBox
|
||||||
|
data-after-content='Pre-live'
|
||||||
|
active={stage.name === 'pre-live'}
|
||||||
|
>
|
||||||
|
<FeatureLifecycleStageIcon stage={{ name: 'pre-live' }} />
|
||||||
|
</StageBox>
|
||||||
|
|
||||||
|
<Line />
|
||||||
|
|
||||||
|
<StageBox data-after-content='Live' active={stage.name === 'live'}>
|
||||||
|
<FeatureLifecycleStageIcon stage={{ name: 'live' }} />
|
||||||
|
</StageBox>
|
||||||
|
|
||||||
|
<Line />
|
||||||
|
|
||||||
|
<StageBox
|
||||||
|
data-after-content='Completed'
|
||||||
|
active={stage.name === 'completed'}
|
||||||
|
>
|
||||||
|
<FeatureLifecycleStageIcon stage={{ name: 'completed' }} />
|
||||||
|
</StageBox>
|
||||||
|
|
||||||
|
<Line />
|
||||||
|
|
||||||
|
<StageBox
|
||||||
|
data-after-content='Archived'
|
||||||
|
active={stage.name === 'archived'}
|
||||||
|
>
|
||||||
|
<FeatureLifecycleStageIcon stage={{ name: 'archived' }} />
|
||||||
|
</StageBox>
|
||||||
|
</IconsRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EnvironmentLine = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const CenteredBox = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Environments: FC<{
|
||||||
|
environments: Array<{
|
||||||
|
name: string;
|
||||||
|
lastSeenAt: string;
|
||||||
|
}>;
|
||||||
|
}> = ({ environments }) => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{environments.map((environment) => {
|
||||||
|
return (
|
||||||
|
<EnvironmentLine key={environment.name}>
|
||||||
|
<CenteredBox>
|
||||||
|
<CloudCircle />
|
||||||
|
<Box>{environment.name}</Box>
|
||||||
|
</CenteredBox>
|
||||||
|
<CenteredBox>
|
||||||
|
<TimeAgo date={environment.lastSeenAt} />
|
||||||
|
<LastSeenIcon lastSeen={environment.lastSeenAt} />
|
||||||
|
</CenteredBox>
|
||||||
|
</EnvironmentLine>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PreLiveStageDescription: FC<{ children?: React.ReactNode }> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InfoText>
|
||||||
|
We've seen the feature flag in the following environments:
|
||||||
|
</InfoText>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ArchivedStageDescription = () => {
|
||||||
|
return (
|
||||||
|
<InfoText>
|
||||||
|
Your feature has been archived, it is now safe to delete it.
|
||||||
|
</InfoText>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BoldTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
fontSize: theme.typography.body2.fontSize,
|
||||||
|
fontWeight: theme.typography.fontWeightBold,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const LiveStageDescription: FC<{
|
||||||
|
onComplete: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
project: string;
|
||||||
|
}> = ({ children, onComplete, loading, project }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BoldTitle>Is this feature complete?</BoldTitle>
|
||||||
|
<InfoText sx={{ mb: 1 }}>
|
||||||
|
Marking the feature flag as complete does not affect any
|
||||||
|
configuration; however, it moves the feature flag to its next
|
||||||
|
lifecycle stage and indicates that you have learned what you
|
||||||
|
needed in order to progress with the feature. It serves as a
|
||||||
|
reminder to start cleaning up the feature flag and removing it
|
||||||
|
from the code.
|
||||||
|
</InfoText>
|
||||||
|
<PermissionButton
|
||||||
|
color='inherit'
|
||||||
|
variant='outlined'
|
||||||
|
permission={UPDATE_FEATURE}
|
||||||
|
size='small'
|
||||||
|
onClick={onComplete}
|
||||||
|
disabled={loading}
|
||||||
|
projectId={project}
|
||||||
|
>
|
||||||
|
Mark completed
|
||||||
|
</PermissionButton>
|
||||||
|
<InfoText sx={{ mt: 3 }}>
|
||||||
|
Users have been exposed to this feature in the following
|
||||||
|
production environments:
|
||||||
|
</InfoText>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SafeToArchive: FC<{
|
||||||
|
onArchive: () => void;
|
||||||
|
onUncomplete: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
project: string;
|
||||||
|
}> = ({ onArchive, onUncomplete, loading, project }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BoldTitle>Safe to archive</BoldTitle>
|
||||||
|
<InfoText
|
||||||
|
sx={{
|
||||||
|
mt: 2,
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
We haven’t seen this feature flag in any environment for at
|
||||||
|
least two days. It’s likely that it’s safe to archive this flag.
|
||||||
|
</InfoText>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PermissionButton
|
||||||
|
color='inherit'
|
||||||
|
variant='outlined'
|
||||||
|
permission={UPDATE_FEATURE}
|
||||||
|
size='small'
|
||||||
|
onClick={onUncomplete}
|
||||||
|
disabled={loading}
|
||||||
|
projectId={project}
|
||||||
|
>
|
||||||
|
Revert to live
|
||||||
|
</PermissionButton>
|
||||||
|
<PermissionButton
|
||||||
|
color='inherit'
|
||||||
|
variant='outlined'
|
||||||
|
permission={DELETE_FEATURE}
|
||||||
|
size='small'
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
onClick={onArchive}
|
||||||
|
projectId={project}
|
||||||
|
>
|
||||||
|
Archive feature
|
||||||
|
</PermissionButton>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ActivelyUsed: FC<{
|
||||||
|
onUncomplete: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}> = ({ children, onUncomplete, loading }) => (
|
||||||
|
<>
|
||||||
|
<InfoText
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
This feature has been successfully completed, but we are still
|
||||||
|
seeing usage. Clean up the feature flag from your code before
|
||||||
|
archiving it:
|
||||||
|
</InfoText>
|
||||||
|
{children}
|
||||||
|
<InfoText
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
If you think this feature was completed too early you can revert to
|
||||||
|
the live stage:
|
||||||
|
</InfoText>
|
||||||
|
<PermissionButton
|
||||||
|
color='inherit'
|
||||||
|
variant='outlined'
|
||||||
|
permission={UPDATE_FEATURE}
|
||||||
|
size='small'
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
onClick={onUncomplete}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Revert to live
|
||||||
|
</PermissionButton>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const CompletedStageDescription: FC<{
|
||||||
|
onArchive: () => void;
|
||||||
|
onUncomplete: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
environments: Array<{
|
||||||
|
name: string;
|
||||||
|
lastSeenAt: string;
|
||||||
|
}>;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
project: string;
|
||||||
|
}> = ({
|
||||||
|
children,
|
||||||
|
environments,
|
||||||
|
onArchive,
|
||||||
|
onUncomplete,
|
||||||
|
loading,
|
||||||
|
project,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={isSafeToArchive(environments)}
|
||||||
|
show={
|
||||||
|
<SafeToArchive
|
||||||
|
onArchive={onArchive}
|
||||||
|
onUncomplete={onUncomplete}
|
||||||
|
loading={loading}
|
||||||
|
project={project}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<ActivelyUsed onUncomplete={onUncomplete} loading={loading}>
|
||||||
|
{children}
|
||||||
|
</ActivelyUsed>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormatTime: FC<{
|
||||||
|
time: string;
|
||||||
|
}> = ({ time }) => {
|
||||||
|
const { locationSettings } = useLocationSettings();
|
||||||
|
|
||||||
|
return <span>{formatDateYMDHMS(time, locationSettings.locale)}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormatElapsedTime: FC<{
|
||||||
|
time: string;
|
||||||
|
}> = ({ time }) => {
|
||||||
|
const pastTime = parseISO(time);
|
||||||
|
const elapsedTime = formatDistanceToNow(pastTime, { addSuffix: false });
|
||||||
|
return <span>{elapsedTime}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FeatureLifecycleTooltip: FC<{
|
||||||
|
children: React.ReactElement<any, any>;
|
||||||
|
stage: LifecycleStage;
|
||||||
|
project: string;
|
||||||
|
onArchive: () => void;
|
||||||
|
onComplete: () => void;
|
||||||
|
onUncomplete: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
}> = ({
|
||||||
|
children,
|
||||||
|
stage,
|
||||||
|
project,
|
||||||
|
onArchive,
|
||||||
|
onComplete,
|
||||||
|
onUncomplete,
|
||||||
|
loading,
|
||||||
|
}) => (
|
||||||
|
<HtmlTooltip
|
||||||
|
maxHeight={800}
|
||||||
|
maxWidth={350}
|
||||||
|
arrow
|
||||||
|
tabIndex={0}
|
||||||
|
title={
|
||||||
|
<Box>
|
||||||
|
<Box sx={(theme) => ({ padding: theme.spacing(2) })}>
|
||||||
|
<MainLifecycleRow>
|
||||||
|
<Typography variant='h3'>Lifecycle</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Badge sx={{ textTransform: 'capitalize' }}>
|
||||||
|
{stage.name}
|
||||||
|
</Badge>
|
||||||
|
<FeatureLifecycleStageIcon stage={stage} />
|
||||||
|
</Box>
|
||||||
|
</MainLifecycleRow>
|
||||||
|
<TimeLifecycleRow>
|
||||||
|
<TimeLabel>Stage entered at</TimeLabel>
|
||||||
|
|
||||||
|
<FormatTime time={stage.enteredStageAt} />
|
||||||
|
</TimeLifecycleRow>
|
||||||
|
<TimeLifecycleRow>
|
||||||
|
<TimeLabel>Time spent in stage</TimeLabel>
|
||||||
|
<FormatElapsedTime time={stage.enteredStageAt} />
|
||||||
|
</TimeLifecycleRow>
|
||||||
|
<StageTimeline stage={stage} />
|
||||||
|
</Box>
|
||||||
|
<ColorFill>
|
||||||
|
{stage.name === 'initial' && <InitialStageDescription />}
|
||||||
|
{stage.name === 'pre-live' && (
|
||||||
|
<PreLiveStageDescription>
|
||||||
|
<Environments environments={stage.environments} />
|
||||||
|
</PreLiveStageDescription>
|
||||||
|
)}
|
||||||
|
{stage.name === 'live' && (
|
||||||
|
<LiveStageDescription
|
||||||
|
onComplete={onComplete}
|
||||||
|
loading={loading}
|
||||||
|
project={project}
|
||||||
|
>
|
||||||
|
<Environments environments={stage.environments} />
|
||||||
|
</LiveStageDescription>
|
||||||
|
)}
|
||||||
|
{stage.name === 'completed' && (
|
||||||
|
<CompletedStageDescription
|
||||||
|
environments={stage.environments}
|
||||||
|
onArchive={onArchive}
|
||||||
|
onUncomplete={onUncomplete}
|
||||||
|
loading={loading}
|
||||||
|
project={project}
|
||||||
|
>
|
||||||
|
<Environments environments={stage.environments} />
|
||||||
|
</CompletedStageDescription>
|
||||||
|
)}
|
||||||
|
{stage.name === 'archived' && <ArchivedStageDescription />}
|
||||||
|
</ColorFill>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CenteredBox>{children}</CenteredBox>
|
||||||
|
</HtmlTooltip>
|
||||||
|
);
|
@ -1,19 +1,21 @@
|
|||||||
|
import type { Lifecycle } from 'interfaces/featureToggle';
|
||||||
|
|
||||||
type TimedStage = { enteredStageAt: string };
|
type TimedStage = { enteredStageAt: string };
|
||||||
export type LifecycleStage = TimedStage &
|
export type LifecycleStage = TimedStage &
|
||||||
(
|
(
|
||||||
| { name: 'initial' }
|
| { name: 'initial' & Lifecycle['stage'] }
|
||||||
| {
|
| {
|
||||||
name: 'pre-live';
|
name: 'pre-live' & Lifecycle['stage'];
|
||||||
environments: Array<{ name: string; lastSeenAt: string }>;
|
environments: Array<{ name: string; lastSeenAt: string }>;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
name: 'live';
|
name: 'live' & Lifecycle['stage'];
|
||||||
environments: Array<{ name: string; lastSeenAt: string }>;
|
environments: Array<{ name: string; lastSeenAt: string }>;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
name: 'completed';
|
name: 'completed' & Lifecycle['stage'];
|
||||||
environments: Array<{ name: string; lastSeenAt: string }>;
|
environments: Array<{ name: string; lastSeenAt: string }>;
|
||||||
status: 'kept' | 'discarded';
|
status: 'kept' | 'discarded';
|
||||||
}
|
}
|
||||||
| { name: 'archived' }
|
| { name: 'archived' & Lifecycle['stage'] }
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import { FeatureLifecycleStageIcon } from 'component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleStageIcon';
|
import { FeatureLifecycleStageIcon } from 'component/common/FeatureLifecycle/FeatureLifecycleStageIcon';
|
||||||
import { useProjectStatus } from 'hooks/api/getters/useProjectStatus/useProjectStatus';
|
import { useProjectStatus } from 'hooks/api/getters/useProjectStatus/useProjectStatus';
|
||||||
import useLoading from 'hooks/useLoading';
|
import useLoading from 'hooks/useLoading';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
@ -9,10 +9,13 @@ import type { ProjectStatusSchemaLifecycleSummary } from 'openapi';
|
|||||||
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||||
import { lifecycleMessages } from './LifecycleMessages';
|
import { lifecycleMessages } from './LifecycleMessages';
|
||||||
import InfoIcon from '@mui/icons-material/Info';
|
import InfoIcon from '@mui/icons-material/Info';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import { getFeatureLifecycleName } from 'component/common/FeatureLifecycle/getFeatureLifecycleName';
|
||||||
|
|
||||||
const LifecycleBoxContent = styled('div')(({ theme }) => ({
|
const LifecycleBoxContent = styled('div')(({ theme }) => ({
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
gap: theme.spacing(4),
|
gap: theme.spacing(2),
|
||||||
|
minHeight: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexFlow: 'column',
|
flexFlow: 'column',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
@ -96,6 +99,10 @@ const Stats = styled('dl')(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledStageTitle = styled('span')(({ theme }) => ({
|
||||||
|
fontSize: theme.typography.body2.fontSize,
|
||||||
|
}));
|
||||||
|
|
||||||
const NoData = styled('span')({
|
const NoData = styled('span')({
|
||||||
fontWeight: 'normal',
|
fontWeight: 'normal',
|
||||||
});
|
});
|
||||||
@ -134,21 +141,41 @@ const BigNumber: FC<{ value?: number }> = ({ value }) => {
|
|||||||
</BigText>
|
</BigText>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProjectLifecycleSummary = () => {
|
export const ProjectLifecycleSummary = () => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const { data, loading } = useProjectStatus(projectId);
|
const { data, loading } = useProjectStatus(projectId);
|
||||||
|
const isLifecycleImprovementsEnabled = useUiFlag('lifecycleImprovements');
|
||||||
|
|
||||||
const loadingRef = useLoading<HTMLUListElement>(
|
const loadingRef = useLoading<HTMLUListElement>(
|
||||||
loading,
|
loading,
|
||||||
'[data-loading-project-lifecycle-summary=true]',
|
'[data-loading-project-lifecycle-summary=true]',
|
||||||
);
|
);
|
||||||
|
|
||||||
const flagWord = (stage: keyof ProjectStatusSchemaLifecycleSummary) => {
|
const flagWord = (stage: keyof ProjectStatusSchemaLifecycleSummary) => {
|
||||||
if (data?.lifecycleSummary[stage].currentFlags === 1) {
|
const hasOneFlag = data?.lifecycleSummary[stage].currentFlags === 1;
|
||||||
return 'flag';
|
|
||||||
} else {
|
if (hasOneFlag) {
|
||||||
return 'flags';
|
return isLifecycleImprovementsEnabled ? 'Flag' : 'flag';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return isLifecycleImprovementsEnabled ? 'Flags' : 'flags';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stageName = (stage: keyof ProjectStatusSchemaLifecycleSummary) => {
|
||||||
|
if (!isLifecycleImprovementsEnabled) {
|
||||||
|
return `${flagWord('initial')} in ${stage}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lifecycleStageName = stage === 'preLive' ? 'pre-live' : stage;
|
||||||
|
return (
|
||||||
|
<StyledStageTitle>
|
||||||
|
{flagWord(stage)} in{' '}
|
||||||
|
{getFeatureLifecycleName(lifecycleStageName)} stage
|
||||||
|
</StyledStageTitle>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LifecycleList ref={loadingRef}>
|
<LifecycleList ref={loadingRef}>
|
||||||
<LifecycleBox tooltipText={lifecycleMessages.initial}>
|
<LifecycleBox tooltipText={lifecycleMessages.initial}>
|
||||||
@ -163,7 +190,7 @@ export const ProjectLifecycleSummary = () => {
|
|||||||
stage={{ name: 'initial' }}
|
stage={{ name: 'initial' }}
|
||||||
/>
|
/>
|
||||||
</Counter>
|
</Counter>
|
||||||
<span>{flagWord('initial')} in initial</span>
|
<span>{stageName('initial')}</span>
|
||||||
</p>
|
</p>
|
||||||
<AverageDaysStat
|
<AverageDaysStat
|
||||||
averageDays={data?.lifecycleSummary.initial.averageDays}
|
averageDays={data?.lifecycleSummary.initial.averageDays}
|
||||||
@ -181,7 +208,7 @@ export const ProjectLifecycleSummary = () => {
|
|||||||
stage={{ name: 'pre-live' }}
|
stage={{ name: 'pre-live' }}
|
||||||
/>
|
/>
|
||||||
</Counter>
|
</Counter>
|
||||||
<span>{flagWord('preLive')} in pre-live</span>
|
<span>{stageName('preLive')}</span>
|
||||||
</p>
|
</p>
|
||||||
<AverageDaysStat
|
<AverageDaysStat
|
||||||
averageDays={data?.lifecycleSummary.preLive.averageDays}
|
averageDays={data?.lifecycleSummary.preLive.averageDays}
|
||||||
@ -199,7 +226,7 @@ export const ProjectLifecycleSummary = () => {
|
|||||||
stage={{ name: 'live' }}
|
stage={{ name: 'live' }}
|
||||||
/>
|
/>
|
||||||
</Counter>
|
</Counter>
|
||||||
<span>{flagWord('live')} in live</span>
|
<span>{stageName('live')}</span>
|
||||||
</p>
|
</p>
|
||||||
<AverageDaysStat
|
<AverageDaysStat
|
||||||
averageDays={data?.lifecycleSummary.live.averageDays}
|
averageDays={data?.lifecycleSummary.live.averageDays}
|
||||||
@ -219,7 +246,7 @@ export const ProjectLifecycleSummary = () => {
|
|||||||
stage={{ name: 'completed' }}
|
stage={{ name: 'completed' }}
|
||||||
/>
|
/>
|
||||||
</Counter>
|
</Counter>
|
||||||
<span>{flagWord('completed')} in completed</span>
|
<span>{stageName('completed')}</span>
|
||||||
</p>
|
</p>
|
||||||
<AverageDaysStat
|
<AverageDaysStat
|
||||||
averageDays={data?.lifecycleSummary.completed.averageDays}
|
averageDays={data?.lifecycleSummary.completed.averageDays}
|
||||||
@ -237,7 +264,7 @@ export const ProjectLifecycleSummary = () => {
|
|||||||
stage={{ name: 'archived' }}
|
stage={{ name: 'archived' }}
|
||||||
/>
|
/>
|
||||||
</Counter>
|
</Counter>
|
||||||
<span>{flagWord('archived')} in archived</span>
|
<span>{stageName('archived')}</span>
|
||||||
</p>
|
</p>
|
||||||
<Stats>
|
<Stats>
|
||||||
<dt>Last 30 days</dt>
|
<dt>Last 30 days</dt>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { CreateFeatureSchemaType } from 'openapi';
|
import type { CreateFeatureSchemaType, FeatureSchema } from 'openapi';
|
||||||
import type { IFeatureStrategy } from './strategy';
|
import type { IFeatureStrategy } from './strategy';
|
||||||
import type { ITag } from './tags';
|
import type { ITag } from './tags';
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ export type ILastSeenEnvironments = Pick<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export type Lifecycle = {
|
export type Lifecycle = {
|
||||||
stage: 'initial' | 'pre-live' | 'live' | 'completed' | 'archived';
|
stage: Required<FeatureSchema>['lifecycle']['stage'];
|
||||||
status?: string;
|
status?: string;
|
||||||
enteredStageAt: string;
|
enteredStageAt: string;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user