mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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 Stage4 } from 'assets/icons/lifecycle/stage-4.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';
 | 
			
		||||
 | 
			
		||||
export const FeatureLifecycleStageIcon: FC<{
 | 
			
		||||
    stage: Pick<LifecycleStage, 'name'>;
 | 
			
		||||
}> = ({ stage }) => {
 | 
			
		||||
}> = ({ stage, ...props }) => {
 | 
			
		||||
    const newIcons = useUiFlag('lifecycleImprovements');
 | 
			
		||||
 | 
			
		||||
    if (stage.name === 'archived') {
 | 
			
		||||
        return newIcons ? <Stage5 /> : <ArchivedStageIcon />;
 | 
			
		||||
        return newIcons ? (
 | 
			
		||||
            <Stage5 {...props} />
 | 
			
		||||
        ) : (
 | 
			
		||||
            <ArchivedStageIcon {...props} />
 | 
			
		||||
        );
 | 
			
		||||
    } else if (stage.name === 'pre-live') {
 | 
			
		||||
        return newIcons ? <Stage2 /> : <PreLiveStageIcon />;
 | 
			
		||||
        return newIcons ? (
 | 
			
		||||
            <Stage2 {...props} />
 | 
			
		||||
        ) : (
 | 
			
		||||
            <PreLiveStageIcon {...props} />
 | 
			
		||||
        );
 | 
			
		||||
    } else if (stage.name === 'live') {
 | 
			
		||||
        return newIcons ? <Stage3 /> : <LiveStageIcon />;
 | 
			
		||||
        return newIcons ? <Stage3 {...props} /> : <LiveStageIcon {...props} />;
 | 
			
		||||
    } else if (stage.name === 'completed') {
 | 
			
		||||
        return newIcons ? <Stage4 /> : <CompletedStageIcon />;
 | 
			
		||||
        return newIcons ? (
 | 
			
		||||
            <Stage4 {...props} />
 | 
			
		||||
        ) : (
 | 
			
		||||
            <CompletedStageIcon {...props} />
 | 
			
		||||
        );
 | 
			
		||||
    } 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 useFeatureLifecycleApi from 'hooks/api/actions/useFeatureLifecycleApi/useFeatureLifecycleApi';
 | 
			
		||||
import { populateCurrentStage } from './populateCurrentStage';
 | 
			
		||||
import type { FC } from 'react';
 | 
			
		||||
import type { Lifecycle } from 'interfaces/featureToggle';
 | 
			
		||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
 | 
			
		||||
import { useUiFlag } from 'hooks/useUiFlag';
 | 
			
		||||
 | 
			
		||||
export interface LifecycleFeature {
 | 
			
		||||
    lifecycle?: Lifecycle;
 | 
			
		||||
@ -25,10 +27,9 @@ export const FeatureLifecycle: FC<{
 | 
			
		||||
    feature: LifecycleFeature;
 | 
			
		||||
}> = ({ feature, onComplete, onUncomplete, onArchive }) => {
 | 
			
		||||
    const currentStage = populateCurrentStage(feature);
 | 
			
		||||
 | 
			
		||||
    const { markFeatureUncompleted, loading } = useFeatureLifecycleApi();
 | 
			
		||||
 | 
			
		||||
    const { trackEvent } = usePlausibleTracker();
 | 
			
		||||
    const isLifecycleImprovementsEnabled = useUiFlag('lifecycleImprovements');
 | 
			
		||||
 | 
			
		||||
    const onUncompleteHandler = async () => {
 | 
			
		||||
        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 ? (
 | 
			
		||||
        <FeatureLifecycleTooltip
 | 
			
		||||
        <LegacyFeatureLifecycleTooltip
 | 
			
		||||
            stage={currentStage!}
 | 
			
		||||
            project={feature.project}
 | 
			
		||||
            onArchive={onArchive}
 | 
			
		||||
@ -49,7 +65,7 @@ export const FeatureLifecycle: FC<{
 | 
			
		||||
            onUncomplete={onUncompleteHandler}
 | 
			
		||||
            loading={loading}
 | 
			
		||||
        >
 | 
			
		||||
            <FeatureLifecycleStageIcon stage={currentStage!} />
 | 
			
		||||
        </FeatureLifecycleTooltip>
 | 
			
		||||
            <FeatureLifecycleStageIcon stage={currentStage} />
 | 
			
		||||
        </LegacyFeatureLifecycleTooltip>
 | 
			
		||||
    ) : null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -51,7 +51,7 @@ test('render initial stage', async () => {
 | 
			
		||||
 | 
			
		||||
    renderOpenTooltip({ name: 'initial', enteredStageAt });
 | 
			
		||||
 | 
			
		||||
    await screen.findByText('initial');
 | 
			
		||||
    await screen.findByText('Define');
 | 
			
		||||
    await screen.findByText('2 minutes');
 | 
			
		||||
    await screen.findByText(
 | 
			
		||||
        'This feature flag is currently in the initial phase of its lifecycle.',
 | 
			
		||||
@ -69,7 +69,7 @@ test('render pre-live stage', async () => {
 | 
			
		||||
        enteredStageAt,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await screen.findByText('pre-live');
 | 
			
		||||
    await screen.findByText('Develop');
 | 
			
		||||
    await screen.findByText('development');
 | 
			
		||||
    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('live');
 | 
			
		||||
    await screen.findByText('production');
 | 
			
		||||
    await screen.findByText('Production');
 | 
			
		||||
    // await screen.findByText('production');
 | 
			
		||||
    await screen.findByText('2 hours ago');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -103,7 +103,7 @@ test('render completed stage with still active', async () => {
 | 
			
		||||
        enteredStageAt,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await screen.findByText('completed');
 | 
			
		||||
    await screen.findByText('Cleanup');
 | 
			
		||||
    await screen.findByText('production');
 | 
			
		||||
    await screen.findByText('2 hours ago');
 | 
			
		||||
    expect(screen.queryByText('Archive feature')).not.toBeInTheDocument();
 | 
			
		||||
@ -127,7 +127,7 @@ test('render completed stage safe to archive', async () => {
 | 
			
		||||
        onArchive,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    await screen.findByText('completed');
 | 
			
		||||
    await screen.findByText('Cleanup');
 | 
			
		||||
    const button = await screen.findByText('Archive feature');
 | 
			
		||||
    button.click();
 | 
			
		||||
 | 
			
		||||
@ -153,7 +153,7 @@ test('mark completed button gets activated', async () => {
 | 
			
		||||
        onComplete,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    await screen.findByText('live');
 | 
			
		||||
    await screen.findByText('Production');
 | 
			
		||||
    const button = await screen.findByText('Mark completed');
 | 
			
		||||
    button.click();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,10 @@
 | 
			
		||||
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 './FeatureLifecycleStageIcon';
 | 
			
		||||
import { FeatureLifecycleStageIcon } from 'component/common/FeatureLifecycle/FeatureLifecycleStageIcon';
 | 
			
		||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
 | 
			
		||||
import { StyledIconWrapper } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen';
 | 
			
		||||
import { useLastSeenColors } from '../../FeatureEnvironmentSeen/useLastSeenColors';
 | 
			
		||||
@ -20,6 +19,7 @@ import { isSafeToArchive } from './isSafeToArchive';
 | 
			
		||||
import { useLocationSettings } from 'hooks/useLocationSettings';
 | 
			
		||||
import { formatDateYMDHMS } from 'utils/formatDate';
 | 
			
		||||
import { formatDistanceToNow, parseISO } from 'date-fns';
 | 
			
		||||
import { getFeatureLifecycleName } from 'component/common/FeatureLifecycle/getFeatureLifecycleName';
 | 
			
		||||
 | 
			
		||||
const TimeLabel = styled('span')(({ theme }) => ({
 | 
			
		||||
    color: theme.palette.text.secondary,
 | 
			
		||||
@ -472,9 +472,9 @@ export const FeatureLifecycleTooltip: FC<{
 | 
			
		||||
                                gap: 1,
 | 
			
		||||
                            }}
 | 
			
		||||
                        >
 | 
			
		||||
                            <Badge sx={{ textTransform: 'capitalize' }}>
 | 
			
		||||
                                {stage.name}
 | 
			
		||||
                            </Badge>
 | 
			
		||||
                            <Typography variant='body2'>
 | 
			
		||||
                                {getFeatureLifecycleName(stage.name)}
 | 
			
		||||
                            </Typography>
 | 
			
		||||
                            <FeatureLifecycleStageIcon stage={stage} />
 | 
			
		||||
                        </Box>
 | 
			
		||||
                    </MainLifecycleRow>
 | 
			
		||||
@ -487,7 +487,6 @@ export const FeatureLifecycleTooltip: FC<{
 | 
			
		||||
                        <TimeLabel>Time spent in stage</TimeLabel>
 | 
			
		||||
                        <FormatElapsedTime time={stage.enteredStageAt} />
 | 
			
		||||
                    </TimeLifecycleRow>
 | 
			
		||||
                    <StageTimeline stage={stage} />
 | 
			
		||||
                </Box>
 | 
			
		||||
                <ColorFill>
 | 
			
		||||
                    {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 };
 | 
			
		||||
export type LifecycleStage = TimedStage &
 | 
			
		||||
    (
 | 
			
		||||
        | { name: 'initial' }
 | 
			
		||||
        | { name: 'initial' & Lifecycle['stage'] }
 | 
			
		||||
        | {
 | 
			
		||||
              name: 'pre-live';
 | 
			
		||||
              name: 'pre-live' & Lifecycle['stage'];
 | 
			
		||||
              environments: Array<{ name: string; lastSeenAt: string }>;
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
              name: 'live';
 | 
			
		||||
              name: 'live' & Lifecycle['stage'];
 | 
			
		||||
              environments: Array<{ name: string; lastSeenAt: string }>;
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
              name: 'completed';
 | 
			
		||||
              name: 'completed' & Lifecycle['stage'];
 | 
			
		||||
              environments: Array<{ name: string; lastSeenAt: string }>;
 | 
			
		||||
              status: 'kept' | 'discarded';
 | 
			
		||||
          }
 | 
			
		||||
        | { name: 'archived' }
 | 
			
		||||
        | { name: 'archived' & Lifecycle['stage'] }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
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 useLoading from 'hooks/useLoading';
 | 
			
		||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
			
		||||
@ -9,10 +9,13 @@ import type { ProjectStatusSchemaLifecycleSummary } from 'openapi';
 | 
			
		||||
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
 | 
			
		||||
import { lifecycleMessages } from './LifecycleMessages';
 | 
			
		||||
import InfoIcon from '@mui/icons-material/Info';
 | 
			
		||||
import { useUiFlag } from 'hooks/useUiFlag';
 | 
			
		||||
import { getFeatureLifecycleName } from 'component/common/FeatureLifecycle/getFeatureLifecycleName';
 | 
			
		||||
 | 
			
		||||
const LifecycleBoxContent = styled('div')(({ theme }) => ({
 | 
			
		||||
    padding: theme.spacing(2),
 | 
			
		||||
    gap: theme.spacing(4),
 | 
			
		||||
    gap: theme.spacing(2),
 | 
			
		||||
    minHeight: '100%',
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexFlow: 'column',
 | 
			
		||||
    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')({
 | 
			
		||||
    fontWeight: 'normal',
 | 
			
		||||
});
 | 
			
		||||
@ -134,21 +141,41 @@ const BigNumber: FC<{ value?: number }> = ({ value }) => {
 | 
			
		||||
        </BigText>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ProjectLifecycleSummary = () => {
 | 
			
		||||
    const projectId = useRequiredPathParam('projectId');
 | 
			
		||||
    const { data, loading } = useProjectStatus(projectId);
 | 
			
		||||
    const isLifecycleImprovementsEnabled = useUiFlag('lifecycleImprovements');
 | 
			
		||||
 | 
			
		||||
    const loadingRef = useLoading<HTMLUListElement>(
 | 
			
		||||
        loading,
 | 
			
		||||
        '[data-loading-project-lifecycle-summary=true]',
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const flagWord = (stage: keyof ProjectStatusSchemaLifecycleSummary) => {
 | 
			
		||||
        if (data?.lifecycleSummary[stage].currentFlags === 1) {
 | 
			
		||||
            return 'flag';
 | 
			
		||||
        } else {
 | 
			
		||||
            return 'flags';
 | 
			
		||||
        const hasOneFlag = data?.lifecycleSummary[stage].currentFlags === 1;
 | 
			
		||||
 | 
			
		||||
        if (hasOneFlag) {
 | 
			
		||||
            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 (
 | 
			
		||||
        <LifecycleList ref={loadingRef}>
 | 
			
		||||
            <LifecycleBox tooltipText={lifecycleMessages.initial}>
 | 
			
		||||
@ -163,7 +190,7 @@ export const ProjectLifecycleSummary = () => {
 | 
			
		||||
                            stage={{ name: 'initial' }}
 | 
			
		||||
                        />
 | 
			
		||||
                    </Counter>
 | 
			
		||||
                    <span>{flagWord('initial')} in initial</span>
 | 
			
		||||
                    <span>{stageName('initial')}</span>
 | 
			
		||||
                </p>
 | 
			
		||||
                <AverageDaysStat
 | 
			
		||||
                    averageDays={data?.lifecycleSummary.initial.averageDays}
 | 
			
		||||
@ -181,7 +208,7 @@ export const ProjectLifecycleSummary = () => {
 | 
			
		||||
                            stage={{ name: 'pre-live' }}
 | 
			
		||||
                        />
 | 
			
		||||
                    </Counter>
 | 
			
		||||
                    <span>{flagWord('preLive')} in pre-live</span>
 | 
			
		||||
                    <span>{stageName('preLive')}</span>
 | 
			
		||||
                </p>
 | 
			
		||||
                <AverageDaysStat
 | 
			
		||||
                    averageDays={data?.lifecycleSummary.preLive.averageDays}
 | 
			
		||||
@ -199,7 +226,7 @@ export const ProjectLifecycleSummary = () => {
 | 
			
		||||
                            stage={{ name: 'live' }}
 | 
			
		||||
                        />
 | 
			
		||||
                    </Counter>
 | 
			
		||||
                    <span>{flagWord('live')} in live</span>
 | 
			
		||||
                    <span>{stageName('live')}</span>
 | 
			
		||||
                </p>
 | 
			
		||||
                <AverageDaysStat
 | 
			
		||||
                    averageDays={data?.lifecycleSummary.live.averageDays}
 | 
			
		||||
@ -219,7 +246,7 @@ export const ProjectLifecycleSummary = () => {
 | 
			
		||||
                            stage={{ name: 'completed' }}
 | 
			
		||||
                        />
 | 
			
		||||
                    </Counter>
 | 
			
		||||
                    <span>{flagWord('completed')} in completed</span>
 | 
			
		||||
                    <span>{stageName('completed')}</span>
 | 
			
		||||
                </p>
 | 
			
		||||
                <AverageDaysStat
 | 
			
		||||
                    averageDays={data?.lifecycleSummary.completed.averageDays}
 | 
			
		||||
@ -237,7 +264,7 @@ export const ProjectLifecycleSummary = () => {
 | 
			
		||||
                            stage={{ name: 'archived' }}
 | 
			
		||||
                        />
 | 
			
		||||
                    </Counter>
 | 
			
		||||
                    <span>{flagWord('archived')} in archived</span>
 | 
			
		||||
                    <span>{stageName('archived')}</span>
 | 
			
		||||
                </p>
 | 
			
		||||
                <Stats>
 | 
			
		||||
                    <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 { ITag } from './tags';
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,7 @@ export type ILastSeenEnvironments = Pick<
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export type Lifecycle = {
 | 
			
		||||
    stage: 'initial' | 'pre-live' | 'live' | 'completed' | 'archived';
 | 
			
		||||
    stage: Required<FeatureSchema>['lifecycle']['stage'];
 | 
			
		||||
    status?: string;
 | 
			
		||||
    enteredStageAt: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user