mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-31 13:47:02 +02:00
feat: bulk archive usage warning (#4448)
Adds a warning when about to archive features that have lastSeenAt of less than a week (green usage) Closes # [1-224](https://linear.app/unleash/issue/1-1224/bulk-edit-show-last-seen-usage-warning)  --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
parent
839c36d547
commit
32954e8168
@ -5,6 +5,9 @@ import useToast from 'hooks/useToast';
|
|||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
|
||||||
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
|
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
|
||||||
|
import { Alert, Typography } from '@mui/material';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
interface IFeatureArchiveDialogProps {
|
interface IFeatureArchiveDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -12,18 +15,61 @@ interface IFeatureArchiveDialogProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
featureIds: string[];
|
featureIds: string[];
|
||||||
|
featuresWithUsage?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UsageWarning = ({
|
||||||
|
ids,
|
||||||
|
projectId,
|
||||||
|
}: {
|
||||||
|
ids?: string[];
|
||||||
|
projectId: string;
|
||||||
|
}) => {
|
||||||
|
const formatPath = (id: string) => {
|
||||||
|
return `/projects/${projectId}/features/${id}`;
|
||||||
|
};
|
||||||
|
if (ids) {
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
severity={'warning'}
|
||||||
|
sx={{ m: theme => theme.spacing(2, 0) }}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
fontWeight={'bold'}
|
||||||
|
variant={'body2'}
|
||||||
|
display="inline"
|
||||||
|
>
|
||||||
|
{`${ids.length} feature toggles `}
|
||||||
|
</Typography>
|
||||||
|
<span>
|
||||||
|
have usage from applications. If you archive these feature
|
||||||
|
toggles they will not be available to Client SDKs:
|
||||||
|
</span>
|
||||||
|
<ul>
|
||||||
|
{ids?.map(id => (
|
||||||
|
<li key={id}>
|
||||||
|
{<Link to={formatPath(id)}>{id}</Link>}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
|
export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
projectId,
|
projectId,
|
||||||
featureIds,
|
featureIds,
|
||||||
|
featuresWithUsage,
|
||||||
}) => {
|
}) => {
|
||||||
const { archiveFeatureToggle } = useFeatureApi();
|
const { archiveFeatureToggle } = useFeatureApi();
|
||||||
const { archiveFeatures } = useProjectApi();
|
const { archiveFeatures } = useProjectApi();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
const isBulkArchive = featureIds?.length > 1;
|
const isBulkArchive = featureIds?.length > 1;
|
||||||
|
|
||||||
const archiveToggle = async () => {
|
const archiveToggle = async () => {
|
||||||
@ -82,6 +128,19 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
|
|||||||
<strong>{featureIds?.length}</strong> feature
|
<strong>{featureIds?.length}</strong> feature
|
||||||
toggles?
|
toggles?
|
||||||
</p>
|
</p>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(
|
||||||
|
uiConfig.flags.lastSeenByEnvironment &&
|
||||||
|
featuresWithUsage &&
|
||||||
|
featuresWithUsage?.length > 0
|
||||||
|
)}
|
||||||
|
show={
|
||||||
|
<UsageWarning
|
||||||
|
ids={featuresWithUsage}
|
||||||
|
projectId={projectId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={featureIds?.length <= 5}
|
condition={featureIds?.length <= 5}
|
||||||
show={
|
show={
|
||||||
|
@ -1,24 +1,46 @@
|
|||||||
import { useState, VFC } from 'react';
|
import { useMemo, useState, VFC } from 'react';
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import { PermissionHOC } from 'component/common/PermissionHOC/PermissionHOC';
|
import { PermissionHOC } from 'component/common/PermissionHOC/PermissionHOC';
|
||||||
import { DELETE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
import { DELETE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||||
import useProject from 'hooks/api/getters/useProject/useProject';
|
import useProject from 'hooks/api/getters/useProject/useProject';
|
||||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
import { FeatureSchema } from 'openapi';
|
||||||
|
import { addDays, isBefore } from 'date-fns';
|
||||||
|
|
||||||
interface IArchiveButtonProps {
|
interface IArchiveButtonProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
features: string[];
|
featureIds: string[];
|
||||||
|
features: FeatureSchema[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_USAGE_THRESHOLD_DAYS = 7;
|
||||||
|
|
||||||
|
const isFeatureInUse = (feature?: FeatureSchema): boolean => {
|
||||||
|
const aWeekAgo = addDays(new Date(), -DEFAULT_USAGE_THRESHOLD_DAYS);
|
||||||
|
return !!(
|
||||||
|
feature &&
|
||||||
|
feature.lastSeenAt &&
|
||||||
|
isBefore(new Date(feature.lastSeenAt), aWeekAgo)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ArchiveButton: VFC<IArchiveButtonProps> = ({
|
export const ArchiveButton: VFC<IArchiveButtonProps> = ({
|
||||||
projectId,
|
projectId,
|
||||||
|
featureIds,
|
||||||
features,
|
features,
|
||||||
}) => {
|
}) => {
|
||||||
const { refetch } = useProject(projectId);
|
const { refetch } = useProject(projectId);
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
|
||||||
|
const featuresWithUsage = useMemo(() => {
|
||||||
|
return featureIds.filter(name => {
|
||||||
|
const feature = features.find(f => f.name === name);
|
||||||
|
return isFeatureInUse(feature);
|
||||||
|
});
|
||||||
|
}, [JSON.stringify(features), featureIds]);
|
||||||
|
|
||||||
const onConfirm = async () => {
|
const onConfirm = async () => {
|
||||||
setIsDialogOpen(false);
|
setIsDialogOpen(false);
|
||||||
await refetch();
|
await refetch();
|
||||||
@ -45,7 +67,8 @@ export const ArchiveButton: VFC<IArchiveButtonProps> = ({
|
|||||||
</PermissionHOC>
|
</PermissionHOC>
|
||||||
<FeatureArchiveDialog
|
<FeatureArchiveDialog
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
featureIds={features}
|
featureIds={featureIds}
|
||||||
|
featuresWithUsage={featuresWithUsage}
|
||||||
onConfirm={onConfirm}
|
onConfirm={onConfirm}
|
||||||
isOpen={isDialogOpen}
|
isOpen={isDialogOpen}
|
||||||
onClose={() => setIsDialogOpen(false)}
|
onClose={() => setIsDialogOpen(false)}
|
||||||
|
@ -88,7 +88,11 @@ export const ProjectFeaturesBatchActions: FC<
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ArchiveButton projectId={projectId} features={selectedIds} />
|
<ArchiveButton
|
||||||
|
projectId={projectId}
|
||||||
|
featureIds={selectedIds}
|
||||||
|
features={data}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
|
Loading…
Reference in New Issue
Block a user