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 (
+
+ 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 (