From 9c883ca37dfd11cd1e0c201acf531b46d365d36b Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Wed, 24 Apr 2024 11:00:21 +0200 Subject: [PATCH] feat: Completed stage UI (#6917) --- .../FeatureLifecycleTooltip.tsx | 60 ++++++++++++++++++- .../FeatureLifecycle/LifecycleStage.tsx | 1 + .../FeatureLifecycle/isSafeToArchive.test.ts | 44 ++++++++++++++ .../FeatureLifecycle/isSafeToArchive.ts | 13 ++++ .../FeatureOverviewMetaData.tsx | 3 +- 5 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/isSafeToArchive.test.ts create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/isSafeToArchive.ts diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx index 53c28f8e2e..31fd43c059 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx @@ -17,7 +17,12 @@ import { StyledIconWrapper } from '../../FeatureEnvironmentSeen/FeatureEnvironme import { useLastSeenColors } from '../../FeatureEnvironmentSeen/useLastSeenColors'; import type { LifecycleStage } from './LifecycleStage'; import PermissionButton from 'component/common/PermissionButton/PermissionButton'; -import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions'; +import { + DELETE_FEATURE, + UPDATE_FEATURE, +} from 'component/providers/AccessProvider/permissions'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { isSafeToArchive } from './isSafeToArchive'; const TimeLabel = styled('span')(({ theme }) => ({ color: theme.palette.text.secondary, @@ -279,6 +284,52 @@ const LiveStageDescription: FC = ({ children }) => { ); }; +const SafeToArchive: FC = () => { + return ( + <> + Safe to archive + + We haven’t seen this feature flag in production for at least two + days. It’s likely that it’s safe to archive this flag. + + + Archive feature + + + ); +}; + +const ActivelyUsed: FC = ({ children }) => { + return ( + <> + + This feature has been successfully completed, but we are still + seeing usage in production. Clean up the feature flag from your + code before archiving it: + + {children} + + ); +}; + +const CompletedStageDescription: FC<{ + environments: Array<{ name: string; lastSeenAt: string }>; +}> = ({ children, environments }) => { + return ( + } + elseShow={{children}} + /> + ); +}; + export const FeatureLifecycleTooltip: FC<{ children: React.ReactElement; stage: LifecycleStage; @@ -327,6 +378,13 @@ export const FeatureLifecycleTooltip: FC<{ )} + {stage.name === 'completed' && ( + + + + )} } diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage.tsx index 79a1bf78e7..c9abaa6aa5 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage.tsx @@ -10,6 +10,7 @@ export type LifecycleStage = } | { name: 'completed'; + environments: Array<{ name: string; lastSeenAt: string }>; status: 'kept' | 'discarded'; } | { name: 'archived' }; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/isSafeToArchive.test.ts b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/isSafeToArchive.test.ts new file mode 100644 index 0000000000..43867d4b5b --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/isSafeToArchive.test.ts @@ -0,0 +1,44 @@ +import { isSafeToArchive } from './isSafeToArchive'; // Update the import path accordingly +import { subDays } from 'date-fns'; + +describe('isSafeToArchive', () => { + it('should return true if all environments were last seen more than two days ago', () => { + const now = new Date(); + const environments = [ + { name: 'Production', lastSeenAt: subDays(now, 3).toISOString() }, + { name: 'Staging', lastSeenAt: subDays(now, 4).toISOString() }, + ]; + + const result = isSafeToArchive(environments); + expect(result).toBe(true); + }); + + it('should return false if any environment was seen within the last two days', () => { + const now = new Date(); + const environments = [ + { name: 'Production', lastSeenAt: subDays(now, 3).toISOString() }, + { name: 'Staging', lastSeenAt: subDays(now, 1).toISOString() }, + ]; + + const result = isSafeToArchive(environments); + expect(result).toBe(false); + }); + + it('should return false if all environments were seen within the last two days', () => { + const now = new Date(); + const environments = [ + { name: 'Production', lastSeenAt: subDays(now, 0).toISOString() }, + { name: 'Staging', lastSeenAt: subDays(now, 1).toISOString() }, + ]; + + const result = isSafeToArchive(environments); + expect(result).toBe(false); + }); + + it('should return true for an empty array of environments', () => { + const environments: Array<{ name: string; lastSeenAt: string }> = []; + + const result = isSafeToArchive(environments); + expect(result).toBe(true); + }); +}); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/isSafeToArchive.ts b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/isSafeToArchive.ts new file mode 100644 index 0000000000..7a04953378 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/isSafeToArchive.ts @@ -0,0 +1,13 @@ +import { isBefore, parseISO, subDays } from 'date-fns'; + +export function isSafeToArchive( + environments: Array<{ name: string; lastSeenAt: string }>, +) { + const twoDaysAgo = subDays(new Date(), 2); + + return environments.every((env) => { + const lastSeenDate = parseISO(env.lastSeenAt); + + return isBefore(lastSeenDate, twoDaysAgo); + }); +} diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx index f391dfd8c1..3d01a9b58b 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx @@ -83,7 +83,8 @@ const FeatureOverviewMetaData = () => { const IconComponent = getFeatureTypeIcons(type); const currentStage: LifecycleStage = { - name: 'live', + name: 'completed', + status: 'kept', environments: [ { name: 'production', lastSeenAt: new Date().toISOString() }, {