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