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:
parent
ab594f5c29
commit
bf8a9b31b3
@ -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?');
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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();
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user