diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleStageIcon.tsx b/frontend/src/component/common/FeatureLifecycle/FeatureLifecycleStageIcon.tsx
similarity index 62%
rename from frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleStageIcon.tsx
rename to frontend/src/component/common/FeatureLifecycle/FeatureLifecycleStageIcon.tsx
index c3edba31e2b9..856e99b61835 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleStageIcon.tsx
+++ b/frontend/src/component/common/FeatureLifecycle/FeatureLifecycleStageIcon.tsx
@@ -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;
-}> = ({ stage }) => {
+}> = ({ stage, ...props }) => {
const newIcons = useUiFlag('lifecycleImprovements');
if (stage.name === 'archived') {
- return newIcons ? : ;
+ return newIcons ? (
+
+ ) : (
+
+ );
} else if (stage.name === 'pre-live') {
- return newIcons ? : ;
+ return newIcons ? (
+
+ ) : (
+
+ );
} else if (stage.name === 'live') {
- return newIcons ? : ;
+ return newIcons ? : ;
} else if (stage.name === 'completed') {
- return newIcons ? : ;
+ return newIcons ? (
+
+ ) : (
+
+ );
} else {
- return newIcons ? : ;
+ return newIcons ? (
+
+ ) : (
+
+ );
}
};
diff --git a/frontend/src/component/common/FeatureLifecycle/getFeatureLifecycleName.ts b/frontend/src/component/common/FeatureLifecycle/getFeatureLifecycleName.ts
new file mode 100644
index 000000000000..8dd4d8e4560c
--- /dev/null
+++ b/frontend/src/component/common/FeatureLifecycle/getFeatureLifecycleName.ts
@@ -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;
+};
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycle.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycle.tsx
index 3df3893dce8f..e8a10ae42ae7 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycle.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycle.tsx
@@ -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 ? (
+
+
+
+ ) : null;
+ }
+
return currentStage ? (
-
-
-
+
+
) : null;
};
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.test.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.test.tsx
index 0504e27f78e8..5ea50e5c802a 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.test.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.test.tsx
@@ -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();
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx
index 4cd4c9545217..11551d728cc1 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx
@@ -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,
}}
>
-
- {stage.name}
-
+
+ {getFeatureLifecycleName(stage.name)}
+
@@ -487,7 +487,6 @@ export const FeatureLifecycleTooltip: FC<{
Time spent in stage
-
{stage.name === 'initial' && }
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/LegacyFeatureLifecycleTooltip.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/LegacyFeatureLifecycleTooltip.tsx
new file mode 100644
index 000000000000..f4cb1a6b8d8b
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/LegacyFeatureLifecycleTooltip.tsx
@@ -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 (
+
+
+
+ );
+};
+
+const InitialStageDescription: FC = () => {
+ return (
+ <>
+
+ This feature flag is currently in the initial phase of its
+ lifecycle.
+
+
+ This means that the flag has been created, but it has not yet
+ been seen in any environment.
+
+
+ Once we detect metrics for a non-production environment it will
+ move into pre-live.
+
+ >
+ );
+};
+
+const StageTimeline: FC<{
+ stage: LifecycleStage;
+}> = ({ stage }) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+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 (
+
+ {environments.map((environment) => {
+ return (
+
+
+
+ {environment.name}
+
+
+
+
+
+
+ );
+ })}
+
+ );
+};
+
+const PreLiveStageDescription: FC<{ children?: React.ReactNode }> = ({
+ children,
+}) => {
+ return (
+ <>
+
+ We've seen the feature flag in the following environments:
+
+
+ {children}
+ >
+ );
+};
+
+const ArchivedStageDescription = () => {
+ return (
+
+ Your feature has been archived, it is now safe to delete it.
+
+ );
+};
+
+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 (
+ <>
+ Is this feature complete?
+
+ 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.
+
+
+ Mark completed
+
+
+ Users have been exposed to this feature in the following
+ production environments:
+
+
+ {children}
+ >
+ );
+};
+
+const SafeToArchive: FC<{
+ onArchive: () => void;
+ onUncomplete: () => void;
+ loading: boolean;
+ project: string;
+}> = ({ onArchive, onUncomplete, loading, project }) => {
+ return (
+ <>
+ Safe to archive
+
+ 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.
+
+
+
+ Revert to live
+
+
+ Archive feature
+
+
+ >
+ );
+};
+
+const ActivelyUsed: FC<{
+ onUncomplete: () => void;
+ loading: boolean;
+ children?: React.ReactNode;
+}> = ({ children, onUncomplete, loading }) => (
+ <>
+
+ This feature has been successfully completed, but we are still
+ seeing usage. Clean up the feature flag from your code before
+ archiving it:
+
+ {children}
+
+ If you think this feature was completed too early you can revert to
+ the live stage:
+
+
+ Revert to live
+
+ >
+);
+
+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 (
+
+ }
+ elseShow={
+
+ {children}
+
+ }
+ />
+ );
+};
+
+const FormatTime: FC<{
+ time: string;
+}> = ({ time }) => {
+ const { locationSettings } = useLocationSettings();
+
+ return {formatDateYMDHMS(time, locationSettings.locale)};
+};
+
+const FormatElapsedTime: FC<{
+ time: string;
+}> = ({ time }) => {
+ const pastTime = parseISO(time);
+ const elapsedTime = formatDistanceToNow(pastTime, { addSuffix: false });
+ return {elapsedTime};
+};
+
+export const FeatureLifecycleTooltip: FC<{
+ children: React.ReactElement;
+ stage: LifecycleStage;
+ project: string;
+ onArchive: () => void;
+ onComplete: () => void;
+ onUncomplete: () => void;
+ loading: boolean;
+}> = ({
+ children,
+ stage,
+ project,
+ onArchive,
+ onComplete,
+ onUncomplete,
+ loading,
+}) => (
+
+ ({ padding: theme.spacing(2) })}>
+
+ Lifecycle
+
+
+ {stage.name}
+
+
+
+
+
+ Stage entered at
+
+
+
+
+ Time spent in stage
+
+
+
+
+
+ {stage.name === 'initial' && }
+ {stage.name === 'pre-live' && (
+
+
+
+ )}
+ {stage.name === 'live' && (
+
+
+
+ )}
+ {stage.name === 'completed' && (
+
+
+
+ )}
+ {stage.name === 'archived' && }
+
+
+ }
+ >
+ {children}
+
+);
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage.tsx
index e3d14f0a1e83..2a994dead7e3 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage.tsx
@@ -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'] }
);
diff --git a/frontend/src/component/project/Project/ProjectStatus/ProjectLifecycleSummary.tsx b/frontend/src/component/project/Project/ProjectStatus/ProjectLifecycleSummary.tsx
index a9da09ae141e..da3c0b75b346 100644
--- a/frontend/src/component/project/Project/ProjectStatus/ProjectLifecycleSummary.tsx
+++ b/frontend/src/component/project/Project/ProjectStatus/ProjectLifecycleSummary.tsx
@@ -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 }) => {
);
};
+
export const ProjectLifecycleSummary = () => {
const projectId = useRequiredPathParam('projectId');
const { data, loading } = useProjectStatus(projectId);
+ const isLifecycleImprovementsEnabled = useUiFlag('lifecycleImprovements');
const loadingRef = useLoading(
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 (
+
+ {flagWord(stage)} in{' '}
+ {getFeatureLifecycleName(lifecycleStageName)} stage
+
+ );
};
+
return (
@@ -163,7 +190,7 @@ export const ProjectLifecycleSummary = () => {
stage={{ name: 'initial' }}
/>
- {flagWord('initial')} in initial
+ {stageName('initial')}
{
stage={{ name: 'pre-live' }}
/>
- {flagWord('preLive')} in pre-live
+ {stageName('preLive')}
{
stage={{ name: 'live' }}
/>
- {flagWord('live')} in live
+ {stageName('live')}
{
stage={{ name: 'completed' }}
/>
- {flagWord('completed')} in completed
+ {stageName('completed')}
{
stage={{ name: 'archived' }}
/>
- {flagWord('archived')} in archived
+ {stageName('archived')}
Last 30 days
diff --git a/frontend/src/interfaces/featureToggle.ts b/frontend/src/interfaces/featureToggle.ts
index 22658f25859c..6dde6cd31cd2 100644
--- a/frontend/src/interfaces/featureToggle.ts
+++ b/frontend/src/interfaces/featureToggle.ts
@@ -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['lifecycle']['stage'];
status?: string;
enteredStageAt: string;
};