diff --git a/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.tsx b/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.tsx index bde35ee347..a7ca02255e 100644 --- a/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.tsx +++ b/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.tsx @@ -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; diff --git a/frontend/src/component/feature/FeatureView/CleanupReminder/useFlagReminders.test.tsx b/frontend/src/component/feature/FeatureView/CleanupReminder/useReminders.test.tsx similarity index 94% rename from frontend/src/component/feature/FeatureView/CleanupReminder/useFlagReminders.test.tsx rename to frontend/src/component/feature/FeatureView/CleanupReminder/useReminders.test.tsx index 703a7b2a91..37402edfd3 100644 --- a/frontend/src/component/feature/FeatureView/CleanupReminder/useFlagReminders.test.tsx +++ b/frontend/src/component/feature/FeatureView/CleanupReminder/useReminders.test.tsx @@ -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, }); diff --git a/frontend/src/component/feature/FeatureView/CleanupReminder/useFlagReminders.ts b/frontend/src/component/feature/FeatureView/CleanupReminder/useReminders.ts similarity index 80% rename from frontend/src/component/feature/FeatureView/CleanupReminder/useFlagReminders.ts rename to frontend/src/component/feature/FeatureView/CleanupReminder/useReminders.ts index b1da8ed977..c6180ea1c1 100644 --- a/frontend/src/component/feature/FeatureView/CleanupReminder/useFlagReminders.ts +++ b/frontend/src/component/feature/FeatureView/CleanupReminder/useReminders.ts @@ -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); diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectCleanupReminder/ProjectCleanupReminder.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectCleanupReminder/ProjectCleanupReminder.tsx new file mode 100644 index 0000000000..7e5ebfc2a7 --- /dev/null +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectCleanupReminder/ProjectCleanupReminder.tsx @@ -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 ( + } + action={ + + + + + } + > + Time to clean up technical debt? +

+ We haven't observed any metrics for {total} of the flags lately. + Can {total > 1 ? 'they' : 'it'} be archived? +

+
+ ); +}; diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx index 7a9dc0245c..9703abfd71 100644 --- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -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 ( } /> - { - setSetupCompletedState('hide-setup'); - }} - /> - } - /> + {setupCompletedState === 'show-setup' && !isOnboarding ? ( + { + setSetupCompletedState('hide-setup'); + }} + /> + ) : null} + {showCleanupReminder ? ( + + ) : null} - { @@ -637,7 +642,6 @@ export const ProjectFeatureToggles = ({ /> )} -