1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-18 01:18:23 +02:00

feat: cleanup reminder (#9776)

This commit is contained in:
Mateusz Kwasniewski 2025-04-16 15:01:07 +02:00 committed by GitHub
parent ab594f5c29
commit bf8a9b31b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 247 additions and 46 deletions

View File

@ -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(<CleanupReminder feature={feature} onChange={() => {}} />, {
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(<CleanupReminder feature={feature} onChange={() => {}} />, {
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(<CleanupReminder feature={feature} onChange={() => {}} />, {
permissions: [{ permission: UPDATE_FEATURE }],
});
await screen.findByText('Time to clean up technical debt?');
});

View File

@ -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 (
<StyledBox>
{reminder === 'complete' && (
<>
<Alert
severity='info'
icon={<FlagIcon />}
action={
<PermissionButton
variant='contained'
permission={UPDATE_FEATURE}
size='small'
onClick={() =>
setMarkCompleteDialogueOpen(true)
}
projectId={feature.project}
>
Mark completed
</PermissionButton>
}
>
<b>Is this flag ready to be completed?</b>
<p>
This flag has been in production for{' '}
<b>{daysInStage} days</b>. Can it be removed from
the code?
</p>
</Alert>
<MarkCompletedDialogue
isOpen={markCompleteDialogueOpen}
setIsOpen={setMarkCompleteDialogueOpen}
projectId={feature.project}
featureId={feature.name}
onComplete={onChange}
/>
</>
)}
{reminder === 'archive' && (
<Alert severity='warning'>
<b>Time to clean up technical debt?</b>
<p>
We haven't observed any metrics for this flag lately.
Can it be archived?
</p>
</Alert>
)}
{reminder === 'removeCode' && (
<Alert severity='warning'>
<b>Time to remove flag from code?</b>
<p>
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?
</p>
</Alert>
)}
</StyledBox>
);
};

View File

@ -104,7 +104,7 @@ test('render completed stage with still active', async () => {
}); });
await screen.findByText('Cleanup'); await screen.findByText('Cleanup');
await screen.findByText(/production/); await screen.findByText('production');
await screen.findByText('2 hours ago'); await screen.findByText('2 hours ago');
expect(screen.queryByText('Archive feature')).not.toBeInTheDocument(); expect(screen.queryByText('Archive feature')).not.toBeInTheDocument();
}); });

View File

@ -194,7 +194,7 @@ const SafeToArchive: FC<{
disabled={loading} disabled={loading}
projectId={project} projectId={project}
> >
Revert to live Revert to production
</PermissionButton> </PermissionButton>
<PermissionButton <PermissionButton
variant='outlined' variant='outlined'
@ -233,7 +233,7 @@ const ActivelyUsed: FC<{
onClick={onUncomplete} onClick={onUncomplete}
disabled={loading} disabled={loading}
> >
Revert to live Revert to production
</PermissionButton> </PermissionButton>
</StyledStageAction> </StyledStageAction>
); );

View File

@ -18,6 +18,8 @@ import { useEnvironmentVisibility } from './FeatureOverviewMetaData/EnvironmentV
import useSplashApi from 'hooks/api/actions/useSplashApi/useSplashApi'; import useSplashApi from 'hooks/api/actions/useSplashApi/useSplashApi';
import { useAuthSplash } from 'hooks/api/getters/useAuth/useAuthSplash'; import { useAuthSplash } from 'hooks/api/getters/useAuth/useAuthSplash';
import { StrategyDragTooltip } from './StrategyDragTooltip'; import { StrategyDragTooltip } from './StrategyDragTooltip';
import { CleanupReminder } from '../CleanupReminder/CleanupReminder';
import { useFeature } from '../../../../hooks/api/getters/useFeature/useFeature';
const StyledContainer = styled('div')(({ theme }) => ({ const StyledContainer = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -53,6 +55,8 @@ export const FeatureOverview = () => {
const { splash } = useAuthSplash(); const { splash } = useAuthSplash();
const [showTooltip, setShowTooltip] = useState(false); const [showTooltip, setShowTooltip] = useState(false);
const [hasClosedTooltip, setHasClosedTooltip] = useState(false); const [hasClosedTooltip, setHasClosedTooltip] = useState(false);
const { feature, refetchFeature } = useFeature(projectId, featureId);
const cleanupReminderEnabled = useUiFlag('cleanupReminder');
if (!flagOverviewRedesign) { if (!flagOverviewRedesign) {
return <LegacyFleatureOverview />; return <LegacyFleatureOverview />;
@ -71,49 +75,57 @@ export const FeatureOverview = () => {
}; };
return ( return (
<StyledContainer> <div>
<div> {cleanupReminderEnabled ? (
<FeatureOverviewMetaData <CleanupReminder feature={feature} onChange={refetchFeature} />
hiddenEnvironments={hiddenEnvironments} ) : null}
onEnvironmentVisibilityChange={ <StyledContainer>
onEnvironmentVisibilityChange <div>
} <FeatureOverviewMetaData
/> hiddenEnvironments={hiddenEnvironments}
</div> onEnvironmentVisibilityChange={
<StyledMainContent> onEnvironmentVisibilityChange
<FeatureOverviewEnvironments }
onToggleEnvOpen={toggleShowTooltip} />
hiddenEnvironments={hiddenEnvironments} </div>
/> <StyledMainContent>
</StyledMainContent> <FeatureOverviewEnvironments
<Routes> onToggleEnvOpen={toggleShowTooltip}
<Route hiddenEnvironments={hiddenEnvironments}
path='strategies/create' />
element={ </StyledMainContent>
<SidebarModal <Routes>
label='Create feature strategy' <Route
onClose={onSidebarClose} path='strategies/create'
open element={
> <SidebarModal
<FeatureStrategyCreate /> label='Create feature strategy'
</SidebarModal> onClose={onSidebarClose}
} open
/> >
<Route <FeatureStrategyCreate />
path='strategies/edit' </SidebarModal>
element={ }
<SidebarModal />
label='Edit feature strategy' <Route
onClose={onSidebarClose} path='strategies/edit'
open element={
> <SidebarModal
<FeatureStrategyEdit /> label='Edit feature strategy'
</SidebarModal> onClose={onSidebarClose}
} open
/> >
</Routes> <FeatureStrategyEdit />
</SidebarModal>
}
/>
</Routes>
<StrategyDragTooltip show={showTooltip} onClose={onTooltipClose} /> <StrategyDragTooltip
</StyledContainer> show={showTooltip}
onClose={onTooltipClose}
/>
</StyledContainer>
</div>
); );
}; };