mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-13 13:48:59 +02:00
feat: project-level cleanup reminders (#10464)
This commit is contained in:
parent
b0f4f1b3b9
commit
ac67a50693
@ -17,7 +17,7 @@ import type { IFeatureToggle } from 'interfaces/featureToggle';
|
||||
import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog';
|
||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useFlagReminders } from './useFlagReminders.ts';
|
||||
import { useReminders } from './useReminders.ts';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { useUncomplete } from '../FeatureOverview/FeatureLifecycle/useUncomplete.ts';
|
||||
|
||||
@ -59,7 +59,7 @@ export const CleanupReminder: FC<{
|
||||
const daysInStage = enteredStageAt
|
||||
? differenceInDays(new Date(), parseISO(enteredStageAt))
|
||||
: 0;
|
||||
const { shouldShowReminder, snoozeReminder } = useFlagReminders();
|
||||
const { shouldShowReminder, snoozeReminder } = useReminders();
|
||||
|
||||
const determineReminder = (): ReminderType => {
|
||||
if (!currentStage || !isRelevantType) return null;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useFlagReminders } from './useFlagReminders.ts';
|
||||
import { useReminders } from './useReminders.ts';
|
||||
|
||||
const TestComponent = ({
|
||||
days = 7,
|
||||
@ -9,7 +9,7 @@ const TestComponent = ({
|
||||
days?: number;
|
||||
maxReminders?: number;
|
||||
}) => {
|
||||
const { shouldShowReminder, snoozeReminder } = useFlagReminders({
|
||||
const { shouldShowReminder, snoozeReminder } = useReminders({
|
||||
days,
|
||||
maxReminders,
|
||||
});
|
@ -7,7 +7,7 @@ const REMINDER_KEY = 'flag-reminders:v1';
|
||||
const MAX_REMINDERS = 50;
|
||||
const DAYS = 7;
|
||||
|
||||
export const useFlagReminders = ({
|
||||
export const useReminders = ({
|
||||
days = DAYS,
|
||||
maxReminders = MAX_REMINDERS,
|
||||
}: {
|
||||
@ -19,16 +19,16 @@ export const useFlagReminders = ({
|
||||
{},
|
||||
);
|
||||
|
||||
const shouldShowReminder = (flagName: string): boolean => {
|
||||
const snoozedUntil = reminders[flagName];
|
||||
const shouldShowReminder = (key: string): boolean => {
|
||||
const snoozedUntil = reminders[key];
|
||||
return !snoozedUntil || isAfter(new Date(), new Date(snoozedUntil));
|
||||
};
|
||||
|
||||
const snoozeReminder = (flagName: string, snoozeDays: number = days) => {
|
||||
const snoozeReminder = (key: string, snoozeDays: number = days) => {
|
||||
const snoozedUntil = addDays(new Date(), snoozeDays).getTime();
|
||||
|
||||
setReminders((prev) => {
|
||||
const updated = { ...prev, [flagName]: snoozedUntil };
|
||||
const updated = { ...prev, [key]: snoozedUntil };
|
||||
|
||||
const entries = Object.entries(updated);
|
||||
|
@ -0,0 +1,89 @@
|
||||
import type { FC } from 'react';
|
||||
import { Alert, Box, Button, styled } from '@mui/material';
|
||||
import CleaningServicesIcon from '@mui/icons-material/CleaningServices';
|
||||
import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { subDays, formatISO } from 'date-fns';
|
||||
import { useReminders } from 'component/feature/FeatureView/CleanupReminder/useReminders';
|
||||
import { useHasRootAccess } from 'hooks/useHasAccess';
|
||||
import { DELETE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||
|
||||
const StyledBox = styled(Box)(({ theme }) => ({
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const ActionsBox = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
const lastSeenAtDays = 7;
|
||||
const refreshInterval = 60 * 1_000;
|
||||
const snoozeReminderDays = 7;
|
||||
|
||||
const getQuery = (projectId: string) => {
|
||||
const date = subDays(new Date(), lastSeenAtDays);
|
||||
const formattedDate = formatISO(date, { representation: 'date' });
|
||||
return {
|
||||
project: `IS:${projectId}`,
|
||||
lifecycle: 'IS:completed',
|
||||
lastSeenAt: `IS_BEFORE:${formattedDate}`,
|
||||
};
|
||||
};
|
||||
|
||||
export const ProjectCleanupReminder: FC<{
|
||||
projectId: string;
|
||||
}> = ({ projectId }) => {
|
||||
const navigate = useNavigate();
|
||||
const { shouldShowReminder, snoozeReminder } = useReminders();
|
||||
const hasAccess = useHasRootAccess(DELETE_FEATURE, projectId);
|
||||
|
||||
const reminderKey = `project-cleanup-${projectId}`;
|
||||
const query = getQuery(projectId);
|
||||
|
||||
const { total, loading } = useFeatureSearch(query, {
|
||||
refreshInterval,
|
||||
});
|
||||
|
||||
if (!hasAccess || !shouldShowReminder(reminderKey) || loading || !total) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleViewFlags = () => {
|
||||
navigate(
|
||||
`/projects/${projectId}/features?${new URLSearchParams(query).toString()}`,
|
||||
);
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
snoozeReminder(reminderKey, snoozeReminderDays);
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
severity='warning'
|
||||
icon={<CleaningServicesIcon />}
|
||||
action={
|
||||
<ActionsBox>
|
||||
<Button size='medium' onClick={handleDismiss}>
|
||||
Remind me later
|
||||
</Button>
|
||||
<Button
|
||||
variant='contained'
|
||||
size='medium'
|
||||
onClick={handleViewFlags}
|
||||
>
|
||||
View flags
|
||||
</Button>
|
||||
</ActionsBox>
|
||||
}
|
||||
>
|
||||
<b>Time to clean up technical debt?</b>
|
||||
<p>
|
||||
We haven't observed any metrics for {total} of the flags lately.
|
||||
Can {total > 1 ? 'they' : 'it'} be archived?
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
};
|
@ -54,6 +54,8 @@ import PermissionIconButton from 'component/common/PermissionIconButton/Permissi
|
||||
import { UPDATE_FEATURE } from '@server/types/permissions';
|
||||
import { ImportModal } from '../Import/ImportModal.tsx';
|
||||
import { IMPORT_BUTTON } from 'utils/testIds';
|
||||
import { ProjectCleanupReminder } from './ProjectCleanupReminder/ProjectCleanupReminder.tsx';
|
||||
import { useUiFlag } from 'hooks/useUiFlag.ts';
|
||||
|
||||
interface IPaginatedProjectFeatureTogglesProps {
|
||||
environments: string[];
|
||||
@ -92,6 +94,7 @@ export const ProjectFeatureToggles = ({
|
||||
const { project } = useProjectOverview(projectId);
|
||||
const [connectSdkOpen, setConnectSdkOpen] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const isFilterFlagsToArchiveEnabled = useUiFlag('filterFlagsToArchive');
|
||||
|
||||
const {
|
||||
features,
|
||||
@ -466,6 +469,11 @@ export const ProjectFeatureToggles = ({
|
||||
|
||||
const selectedData = useSelectedData(features, rowSelection);
|
||||
|
||||
const showCleanupReminder =
|
||||
isFilterFlagsToArchiveEnabled &&
|
||||
!tableState.lastSeenAt &&
|
||||
!tableState.lifecycle;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ConditionallyRender
|
||||
@ -479,19 +487,17 @@ export const ProjectFeatureToggles = ({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
setupCompletedState === 'show-setup' && !isOnboarding
|
||||
}
|
||||
show={
|
||||
{setupCompletedState === 'show-setup' && !isOnboarding ? (
|
||||
<ProjectOnboarded
|
||||
projectId={projectId}
|
||||
onClose={() => {
|
||||
setSetupCompletedState('hide-setup');
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{showCleanupReminder ? (
|
||||
<ProjectCleanupReminder projectId={projectId} />
|
||||
) : null}
|
||||
<PageContent
|
||||
disableLoading
|
||||
disablePadding
|
||||
@ -598,7 +604,6 @@ export const ProjectFeatureToggles = ({
|
||||
{featureToggleModals}
|
||||
</div>
|
||||
</PageContent>
|
||||
|
||||
<ConnectSdkDialog
|
||||
open={connectSdkOpen}
|
||||
onClose={() => {
|
||||
@ -637,7 +642,6 @@ export const ProjectFeatureToggles = ({
|
||||
/>
|
||||
)}
|
||||
</BatchSelectionActionsBar>
|
||||
|
||||
<ImportModal
|
||||
open={modalOpen}
|
||||
setOpen={setModalOpen}
|
||||
|
Loading…
Reference in New Issue
Block a user