1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-31 01:16:01 +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(/production/);
await screen.findByText('production');
await screen.findByText('2 hours ago');
expect(screen.queryByText('Archive feature')).not.toBeInTheDocument();
});

View File

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

View File

@ -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 <LegacyFleatureOverview />;
@ -71,49 +75,57 @@ export const FeatureOverview = () => {
};
return (
<StyledContainer>
<div>
<FeatureOverviewMetaData
hiddenEnvironments={hiddenEnvironments}
onEnvironmentVisibilityChange={
onEnvironmentVisibilityChange
}
/>
</div>
<StyledMainContent>
<FeatureOverviewEnvironments
onToggleEnvOpen={toggleShowTooltip}
hiddenEnvironments={hiddenEnvironments}
/>
</StyledMainContent>
<Routes>
<Route
path='strategies/create'
element={
<SidebarModal
label='Create feature strategy'
onClose={onSidebarClose}
open
>
<FeatureStrategyCreate />
</SidebarModal>
}
/>
<Route
path='strategies/edit'
element={
<SidebarModal
label='Edit feature strategy'
onClose={onSidebarClose}
open
>
<FeatureStrategyEdit />
</SidebarModal>
}
/>
</Routes>
<div>
{cleanupReminderEnabled ? (
<CleanupReminder feature={feature} onChange={refetchFeature} />
) : null}
<StyledContainer>
<div>
<FeatureOverviewMetaData
hiddenEnvironments={hiddenEnvironments}
onEnvironmentVisibilityChange={
onEnvironmentVisibilityChange
}
/>
</div>
<StyledMainContent>
<FeatureOverviewEnvironments
onToggleEnvOpen={toggleShowTooltip}
hiddenEnvironments={hiddenEnvironments}
/>
</StyledMainContent>
<Routes>
<Route
path='strategies/create'
element={
<SidebarModal
label='Create feature strategy'
onClose={onSidebarClose}
open
>
<FeatureStrategyCreate />
</SidebarModal>
}
/>
<Route
path='strategies/edit'
element={
<SidebarModal
label='Edit feature strategy'
onClose={onSidebarClose}
open
>
<FeatureStrategyEdit />
</SidebarModal>
}
/>
</Routes>
<StrategyDragTooltip show={showTooltip} onClose={onTooltipClose} />
</StyledContainer>
<StrategyDragTooltip
show={showTooltip}
onClose={onTooltipClose}
/>
</StyledContainer>
</div>
);
};