diff --git a/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.test.tsx b/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.test.tsx new file mode 100644 index 0000000000..d5a94edec8 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.test.tsx @@ -0,0 +1,71 @@ +import { vi } from 'vitest'; +import { CleanupReminder } from './CleanupReminder'; +import { render } from 'utils/testRenderer'; +import type { IFeatureToggle } from 'interfaces/featureToggle'; +import { screen } from '@testing-library/react'; +import { UPDATE_FEATURE } from '../../../providers/AccessProvider/permissions'; + +const currentTime = '2024-04-25T08:05:00.000Z'; +const monthAgo = '2024-03-25T06:05:00.000Z'; + +test('render complete feature reminder', async () => { + vi.setSystemTime(currentTime); + const feature = { + name: 'feature', + project: 'default', + type: 'release', + lifecycle: { stage: 'live', enteredStageAt: monthAgo }, + environments: [{ name: 'prod', type: 'production', enabled: true }], + } as IFeatureToggle; + + render( {}} />, { + permissions: [{ permission: UPDATE_FEATURE }], + }); + + const button = await screen.findByText('Mark completed'); + await screen.findByText('31 days'); + + button.click(); + await screen.findByText('Cancel'); +}); + +test('render remove flag from code reminder', async () => { + vi.setSystemTime(currentTime); + const feature = { + name: 'feature', + project: 'default', + type: 'release', + lifecycle: { stage: 'completed', enteredStageAt: monthAgo }, + environments: [ + { + name: 'prod', + type: 'production', + enabled: true, + lastSeenAt: currentTime, + }, + ], + } as IFeatureToggle; + + render( {}} />, { + permissions: [{ permission: UPDATE_FEATURE }], + }); + + await screen.findByText('Time to remove flag from code?'); +}); + +test('render archive flag reminder', async () => { + vi.setSystemTime(currentTime); + const feature = { + name: 'feature', + project: 'default', + type: 'release', + lifecycle: { stage: 'completed', enteredStageAt: monthAgo }, + environments: [{ name: 'prod', type: 'production', enabled: true }], + } as IFeatureToggle; + + render( {}} />, { + permissions: [{ permission: UPDATE_FEATURE }], + }); + + await screen.findByText('Time to clean up technical debt?'); +}); diff --git a/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.tsx b/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.tsx new file mode 100644 index 0000000000..67af14440b --- /dev/null +++ b/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.tsx @@ -0,0 +1,118 @@ +import { type FC, useState } from 'react'; +import { Alert, Box, styled } from '@mui/material'; +import FlagIcon from '@mui/icons-material/OutlinedFlag'; +import { parseISO } from 'date-fns'; +import differenceInDays from 'date-fns/differenceInDays'; + +import PermissionButton from 'component/common/PermissionButton/PermissionButton'; +import { UPDATE_FEATURE } from '../../../providers/AccessProvider/permissions'; +import { MarkCompletedDialogue } from '../FeatureOverview/FeatureLifecycle/MarkCompletedDialogue'; +import { populateCurrentStage } from '../FeatureOverview/FeatureLifecycle/populateCurrentStage'; +import { isSafeToArchive } from '../FeatureOverview/FeatureLifecycle/isSafeToArchive'; +import type { IFeatureToggle } from 'interfaces/featureToggle'; + +const StyledBox = styled(Box)(({ theme }) => ({ + marginRight: theme.spacing(2), + marginBottom: theme.spacing(2), +})); + +type ReminderType = 'complete' | 'removeCode' | 'archive' | null; + +export const CleanupReminder: FC<{ + feature: IFeatureToggle; + onChange: () => void; +}> = ({ feature, onChange }) => { + const [markCompleteDialogueOpen, setMarkCompleteDialogueOpen] = + useState(false); + + const currentStage = populateCurrentStage(feature); + const isRelevantType = + feature.type === 'release' || feature.type === 'experiment'; + const enteredStageAt = currentStage?.enteredStageAt; + const daysInStage = enteredStageAt + ? differenceInDays(new Date(), parseISO(enteredStageAt)) + : 0; + + const determineReminder = (): ReminderType => { + if (!currentStage || !isRelevantType) return null; + + if (currentStage.name === 'live' && daysInStage > 30) { + return 'complete'; + } + if (currentStage.name === 'completed') { + if (isSafeToArchive(currentStage.environments)) { + return 'archive'; + } + if (daysInStage > 2) { + return 'removeCode'; + } + } + + return null; + }; + + const reminder = determineReminder(); + + if (!reminder) return null; + + return ( + + {reminder === 'complete' && ( + <> + } + action={ + + setMarkCompleteDialogueOpen(true) + } + projectId={feature.project} + > + Mark completed + + } + > + Is this flag ready to be completed? +

+ This flag has been in production for{' '} + {daysInStage} days. Can it be removed from + the code? +

+
+ + + )} + + {reminder === 'archive' && ( + + Time to clean up technical debt? +

+ We haven't observed any metrics for this flag lately. + Can it be archived? +

+
+ )} + + {reminder === 'removeCode' && ( + + Time to remove flag from code? +

+ This flag was marked as complete and ready for cleanup. + We're still seeing it being used within the last 2 days. + Have you removed the flag from your code? +

+
+ )} +
+ ); +}; 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 e552851713..9dddc08e9d 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.test.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.test.tsx @@ -104,7 +104,7 @@ test('render completed stage with still active', async () => { }); await screen.findByText('Cleanup'); - await screen.findByText(/production/); + await screen.findByText('production'); await screen.findByText('2 hours ago'); expect(screen.queryByText('Archive feature')).not.toBeInTheDocument(); }); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx index a04a2ea2ee..d4d21974ab 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx @@ -194,7 +194,7 @@ const SafeToArchive: FC<{ disabled={loading} projectId={project} > - Revert to live + Revert to production - Revert to live + Revert to production ); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverview.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverview.tsx index 964162878f..34132340e2 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverview.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverview.tsx @@ -18,6 +18,8 @@ import { useEnvironmentVisibility } from './FeatureOverviewMetaData/EnvironmentV import useSplashApi from 'hooks/api/actions/useSplashApi/useSplashApi'; import { useAuthSplash } from 'hooks/api/getters/useAuth/useAuthSplash'; import { StrategyDragTooltip } from './StrategyDragTooltip'; +import { CleanupReminder } from '../CleanupReminder/CleanupReminder'; +import { useFeature } from '../../../../hooks/api/getters/useFeature/useFeature'; const StyledContainer = styled('div')(({ theme }) => ({ display: 'flex', @@ -53,6 +55,8 @@ export const FeatureOverview = () => { const { splash } = useAuthSplash(); const [showTooltip, setShowTooltip] = useState(false); const [hasClosedTooltip, setHasClosedTooltip] = useState(false); + const { feature, refetchFeature } = useFeature(projectId, featureId); + const cleanupReminderEnabled = useUiFlag('cleanupReminder'); if (!flagOverviewRedesign) { return ; @@ -71,49 +75,57 @@ export const FeatureOverview = () => { }; return ( - -
- -
- - - - - - - - } - /> - - - - } - /> - +
+ {cleanupReminderEnabled ? ( + + ) : null} + +
+ +
+ + + + + + + + } + /> + + + + } + /> + - -
+ + +
); };