From 0eaf725e82600f80f990eeecac5819f61e82e516 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Thu, 25 Apr 2024 13:30:00 +0200 Subject: [PATCH] feat: lifecycle stage dates (#6926) --- .../FeatureLifecycleTooltip.tsx | 20 +++++++++-- .../FeatureLifecycle/LifecycleStage.tsx | 35 ++++++++++--------- .../populateCurrentStage.test.ts | 19 ++++++---- .../FeatureLifecycle/populateCurrentStage.ts | 9 +++-- 4 files changed, 56 insertions(+), 27 deletions(-) diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx index 31fd43c0591d..38505bba8af9 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx @@ -23,6 +23,9 @@ import { } 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, @@ -330,6 +333,18 @@ const CompletedStageDescription: FC<{ ); }; +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; @@ -358,11 +373,12 @@ export const FeatureLifecycleTooltip: FC<{ Stage entered at - 14/01/2024 + + Time spent in stage - 3 days + diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage.tsx index c9abaa6aa5e9..e3d14f0a1e83 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage.tsx @@ -1,16 +1,19 @@ -export type LifecycleStage = - | { name: 'initial' } - | { - name: 'pre-live'; - environments: Array<{ name: string; lastSeenAt: string }>; - } - | { - name: 'live'; - environments: Array<{ name: string; lastSeenAt: string }>; - } - | { - name: 'completed'; - environments: Array<{ name: string; lastSeenAt: string }>; - status: 'kept' | 'discarded'; - } - | { name: 'archived' }; +type TimedStage = { enteredStageAt: string }; +export type LifecycleStage = TimedStage & + ( + | { name: 'initial' } + | { + name: 'pre-live'; + environments: Array<{ name: string; lastSeenAt: string }>; + } + | { + name: 'live'; + environments: Array<{ name: string; lastSeenAt: string }>; + } + | { + name: 'completed'; + environments: Array<{ name: string; lastSeenAt: string }>; + status: 'kept' | 'discarded'; + } + | { name: 'archived' } + ); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/populateCurrentStage.test.ts b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/populateCurrentStage.test.ts index cbe6ff611f5a..f8bc32879f96 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/populateCurrentStage.test.ts +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/populateCurrentStage.test.ts @@ -1,6 +1,8 @@ import { populateCurrentStage } from './populateCurrentStage'; import type { IFeatureToggle } from '../../../../../interfaces/featureToggle'; +const enteredStageAt = 'date'; + describe('populateCurrentStage', () => { it('should return undefined if lifecycle is not defined', () => { const feature = {}; @@ -10,16 +12,16 @@ describe('populateCurrentStage', () => { it('should return initial stage when lifecycle stage is initial', () => { const feature = { - lifecycle: { stage: 'initial' }, + lifecycle: { stage: 'initial', enteredStageAt }, }; - const expected = { name: 'initial' }; + const expected = { name: 'initial', enteredStageAt }; const result = populateCurrentStage(feature as IFeatureToggle); expect(result).toEqual(expected); }); it('should correctly populate pre-live stage with dev environments', () => { const feature = { - lifecycle: { stage: 'pre-live' }, + lifecycle: { stage: 'pre-live', enteredStageAt }, environments: [ { name: 'test', type: 'development', lastSeenAt: null }, { name: 'test1', type: 'production', lastSeenAt: '2022-08-01' }, @@ -29,6 +31,7 @@ describe('populateCurrentStage', () => { const expected = { name: 'pre-live', environments: [{ name: 'dev', lastSeenAt: '2022-08-01' }], + enteredStageAt, }; const result = populateCurrentStage(feature); expect(result).toEqual(expected); @@ -36,7 +39,7 @@ describe('populateCurrentStage', () => { it('should handle live stage with production environments', () => { const feature = { - lifecycle: { stage: 'live' }, + lifecycle: { stage: 'live', enteredStageAt }, environments: [ { name: 'prod', type: 'production', lastSeenAt: '2022-08-01' }, ], @@ -44,6 +47,7 @@ describe('populateCurrentStage', () => { const expected = { name: 'live', environments: [{ name: 'prod', lastSeenAt: '2022-08-01' }], + enteredStageAt, }; const result = populateCurrentStage(feature); expect(result).toEqual(expected); @@ -51,7 +55,7 @@ describe('populateCurrentStage', () => { it('should return completed stage with production environments', () => { const feature = { - lifecycle: { stage: 'completed' }, + lifecycle: { stage: 'completed', enteredStageAt }, environments: [ { name: 'prod', type: 'production', lastSeenAt: '2022-08-01' }, ], @@ -60,6 +64,7 @@ describe('populateCurrentStage', () => { name: 'completed', status: 'kept', environments: [{ name: 'prod', lastSeenAt: '2022-08-01' }], + enteredStageAt, }; const result = populateCurrentStage(feature); expect(result).toEqual(expected); @@ -67,9 +72,9 @@ describe('populateCurrentStage', () => { it('should return archived stage when lifecycle stage is archived', () => { const feature = { - lifecycle: { stage: 'archived' }, + lifecycle: { stage: 'archived', enteredStageAt }, } as IFeatureToggle; - const expected = { name: 'archived' }; + const expected = { name: 'archived', enteredStageAt }; const result = populateCurrentStage(feature); expect(result).toEqual(expected); }); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/populateCurrentStage.ts b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/populateCurrentStage.ts index e2bc152693fb..451d54fd93c7 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/populateCurrentStage.ts +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/populateCurrentStage.ts @@ -15,15 +15,18 @@ export const populateCurrentStage = ( })); }; + const enteredStageAt = feature.lifecycle.enteredStageAt; + switch (feature.lifecycle.stage) { case 'initial': - return { name: 'initial' }; + return { name: 'initial', enteredStageAt }; case 'pre-live': return { name: 'pre-live', environments: getFilteredEnvironments( (type) => type !== 'production', ), + enteredStageAt, }; case 'live': return { @@ -31,6 +34,7 @@ export const populateCurrentStage = ( environments: getFilteredEnvironments( (type) => type === 'production', ), + enteredStageAt, }; case 'completed': return { @@ -39,9 +43,10 @@ export const populateCurrentStage = ( environments: getFilteredEnvironments( (type) => type === 'production', ), + enteredStageAt, }; case 'archived': - return { name: 'archived' }; + return { name: 'archived', enteredStageAt }; default: return undefined; }