mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01: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) ![Screenshot 2023-08-08 at 15 10 26](https://github.com/Unleash/unleash/assets/104830839/7783c751-dcdf-4d80-a6fb-b441fb034b70) --------- 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 { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
|
||||
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 {
|
||||
isOpen: boolean;
|
||||
@ -12,18 +15,61 @@ interface IFeatureArchiveDialogProps {
|
||||
onClose: () => void;
|
||||
projectId: 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> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
projectId,
|
||||
featureIds,
|
||||
featuresWithUsage,
|
||||
}) => {
|
||||
const { archiveFeatureToggle } = useFeatureApi();
|
||||
const { archiveFeatures } = useProjectApi();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const isBulkArchive = featureIds?.length > 1;
|
||||
|
||||
const archiveToggle = async () => {
|
||||
@ -82,6 +128,19 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
|
||||
<strong>{featureIds?.length}</strong> feature
|
||||
toggles?
|
||||
</p>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(
|
||||
uiConfig.flags.lastSeenByEnvironment &&
|
||||
featuresWithUsage &&
|
||||
featuresWithUsage?.length > 0
|
||||
)}
|
||||
show={
|
||||
<UsageWarning
|
||||
ids={featuresWithUsage}
|
||||
projectId={projectId}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={featureIds?.length <= 5}
|
||||
show={
|
||||
|
@ -1,24 +1,46 @@
|
||||
import { useState, VFC } from 'react';
|
||||
import { useMemo, useState, VFC } from 'react';
|
||||
import { Button } from '@mui/material';
|
||||
import { PermissionHOC } from 'component/common/PermissionHOC/PermissionHOC';
|
||||
import { DELETE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||
import useProject from 'hooks/api/getters/useProject/useProject';
|
||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { FeatureSchema } from 'openapi';
|
||||
import { addDays, isBefore } from 'date-fns';
|
||||
|
||||
interface IArchiveButtonProps {
|
||||
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> = ({
|
||||
projectId,
|
||||
featureIds,
|
||||
features,
|
||||
}) => {
|
||||
const { refetch } = useProject(projectId);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
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 () => {
|
||||
setIsDialogOpen(false);
|
||||
await refetch();
|
||||
@ -45,7 +67,8 @@ export const ArchiveButton: VFC<IArchiveButtonProps> = ({
|
||||
</PermissionHOC>
|
||||
<FeatureArchiveDialog
|
||||
projectId={projectId}
|
||||
featureIds={features}
|
||||
featureIds={featureIds}
|
||||
featuresWithUsage={featuresWithUsage}
|
||||
onConfirm={onConfirm}
|
||||
isOpen={isDialogOpen}
|
||||
onClose={() => setIsDialogOpen(false)}
|
||||
|
@ -88,7 +88,11 @@ export const ProjectFeaturesBatchActions: FC<
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<ArchiveButton projectId={projectId} features={selectedIds} />
|
||||
<ArchiveButton
|
||||
projectId={projectId}
|
||||
featureIds={selectedIds}
|
||||
features={data}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
|
Loading…
Reference in New Issue
Block a user