1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00
unleash.unleash/frontend/src/component/common/FeatureArchiveDialog/FeatureArchiveDialog.tsx
Thomas Heartman b2c58102dd
chore(unl-204): remove uses of toast text and confetti (#8941)
As of PR #8935, we no longer support both text and title, and confetti
has been removed.

This PR:
- removes `confetti` from the toast interface
- merges `text` and `title` into `text` and updates its uses across the
codebase.
- readjusts the text where necessary.
2024-12-10 13:38:04 +00:00

458 lines
15 KiB
TypeScript

import { useEffect, useState, type VFC } from 'react';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
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 { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
import { useHighestPermissionChangeRequestEnvironment } from 'hooks/useHighestPermissionChangeRequestEnvironment';
import { useScheduledChangeRequestsWithFlags } from 'hooks/api/getters/useScheduledChangeRequestsWithFlags/useScheduledChangeRequestsWithFlags';
import type { ScheduledChangeRequestViewModel } from 'hooks/api/getters/useScheduledChangeRequestsWithStrategy/useScheduledChangeRequestsWithStrategy';
interface IFeatureArchiveDialogProps {
isOpen: boolean;
onConfirm: () => void;
onClose: () => void;
projectId: string;
featureIds: string[];
featuresWithUsage?: string[];
}
const RemovedDependenciesAlert = () => {
return (
<Alert severity='warning' sx={{ m: (theme) => theme.spacing(2, 0) }}>
Archiving features with dependencies will also remove those
dependencies.
</Alert>
);
};
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 flags `}
</Typography>
<span>
have usage from applications. If you archive these feature
flags 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;
};
const ArchiveParentError = ({
ids,
projectId,
}: {
ids?: string[];
projectId: string;
}) => {
const formatPath = (id: string) => {
return `/projects/${projectId}/features/${id}`;
};
if (ids && ids.length > 1) {
return (
<Alert
severity={'error'}
sx={{ m: (theme) => theme.spacing(2, 0) }}
>
<Typography
fontWeight={'bold'}
variant={'body2'}
display='inline'
>
{`${ids.length} feature flags `}
</Typography>
<span>
have child features that depend on them and are not part of
the archive operation. These parent features can not be
archived:
</span>
<ul>
{ids?.map((id) => (
<li key={id}>
{<Link to={formatPath(id)}>{id}</Link>}
</li>
))}
</ul>
</Alert>
);
}
if (ids && ids.length === 1) {
return (
<Alert
severity={'error'}
sx={{ m: (theme) => theme.spacing(2, 0) }}
>
<Link to={formatPath(ids[0])}>{ids[0]}</Link> has child features
that depend on it and are not part of the archive operation.
</Alert>
);
}
return null;
};
const ScheduledChangeRequestAlert: VFC<{
changeRequests?: ScheduledChangeRequestViewModel[];
projectId: string;
}> = ({ changeRequests, projectId }) => {
if (changeRequests && changeRequests.length > 0) {
return (
<Alert
severity='warning'
sx={{ m: (theme) => theme.spacing(2, 0) }}
>
<p>
This archive operation would conflict with{' '}
{changeRequests.length} scheduled change request(s). The
change request(s) that would be affected by this are:
</p>
<ul>
{changeRequests.map(({ id, title }) => {
const text = title
? `#${id} (${title})`
: `Change request #${id}`;
return (
<li key={id}>
<Link
to={`/projects/${projectId}/change-requests/${id}`}
target='_blank'
rel='noopener noreferrer'
title={`Change request ${id}`}
>
{text}
</Link>
</li>
);
})}
</ul>
</Alert>
);
} else if (changeRequests === undefined) {
return (
<Alert severity='warning'>
<p>
This archive operation might conflict with one or more
scheduled change requests. If you complete it, those change
requests can no longer be applied.
</p>
</Alert>
);
}
// all good, we have nothing to show
return null;
};
const useActionButtonText = (projectId: string, isBulkArchive: boolean) => {
const getHighestEnvironment =
useHighestPermissionChangeRequestEnvironment(projectId);
const environment = getHighestEnvironment();
const { isChangeRequestConfiguredForReview } =
useChangeRequestsEnabled(projectId);
if (
environment &&
isChangeRequestConfiguredForReview(environment) &&
isBulkArchive
) {
return 'Add to change request';
}
if (environment && isChangeRequestConfiguredForReview(environment)) {
return 'Add change to draft';
}
if (isBulkArchive) {
return 'Archive flags';
}
return 'Archive flag';
};
const useArchiveAction = ({
projectId,
featureIds,
onSuccess,
onError,
}: {
projectId: string;
featureIds: string[];
onSuccess: () => void;
onError: () => void;
}) => {
const { setToastData, setToastApiError } = useToast();
const { archiveFeatureToggle } = useFeatureApi();
const { archiveFeatures } = useProjectApi();
const { isChangeRequestConfiguredForReview } =
useChangeRequestsEnabled(projectId);
const { addChange } = useChangeRequestApi();
const { refetch: refetchChangeRequests } =
usePendingChangeRequests(projectId);
const getHighestEnvironment =
useHighestPermissionChangeRequestEnvironment(projectId);
const isBulkArchive = featureIds?.length > 1;
const environment = getHighestEnvironment();
const addArchiveToggleToChangeRequest = async () => {
if (!environment) {
console.error('No change request environment');
return;
}
await addChange(
projectId,
environment,
featureIds.map((feature) => ({
action: 'archiveFeature',
feature: feature,
payload: undefined,
})),
);
refetchChangeRequests();
setToastData({
type: 'success',
text: isBulkArchive
? 'Changes added to a draft'
: 'Change added to a draft',
});
};
const archiveToggle = async () => {
await archiveFeatureToggle(projectId, featureIds[0]);
setToastData({
type: 'success',
text: 'Feature flag archived',
});
};
const archiveToggles = async () => {
await archiveFeatures(projectId, featureIds);
setToastData({
type: 'success',
text: 'Feature flags archived',
});
};
return async () => {
try {
if (
environment &&
isChangeRequestConfiguredForReview(environment)
) {
await addArchiveToggleToChangeRequest();
} else if (isBulkArchive) {
await archiveToggles();
} else {
await archiveToggle();
}
onSuccess();
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
onError();
}
};
};
const useVerifyArchive = (
featureIds: string[],
projectId: string,
isOpen: boolean,
) => {
const [disableArchive, setDisableArchive] = useState(true);
const [offendingParents, setOffendingParents] = useState<string[]>([]);
const [hasDeletedDependencies, setHasDeletedDependencies] = useState(false);
const { verifyArchiveFeatures } = useProjectApi();
useEffect(() => {
if (isOpen) {
verifyArchiveFeatures(projectId, featureIds)
.then((res) => res.json())
.then(
({ hasDeletedDependencies, parentsWithChildFeatures }) => {
if (parentsWithChildFeatures.length === 0) {
setDisableArchive(false);
setOffendingParents(parentsWithChildFeatures);
} else {
setDisableArchive(true);
setOffendingParents(parentsWithChildFeatures);
}
setHasDeletedDependencies(hasDeletedDependencies);
},
);
}
}, [
JSON.stringify(featureIds),
isOpen,
projectId,
setOffendingParents,
setDisableArchive,
setHasDeletedDependencies,
]);
return { disableArchive, offendingParents, hasDeletedDependencies };
};
export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
isOpen,
onClose,
onConfirm,
projectId,
featureIds,
featuresWithUsage,
}) => {
const isBulkArchive = featureIds?.length > 1;
const buttonText = useActionButtonText(projectId, isBulkArchive);
const dialogTitle = isBulkArchive
? 'Archive feature flags'
: 'Archive feature flag';
const archiveAction = useArchiveAction({
projectId,
featureIds,
onSuccess() {
onConfirm();
onClose();
},
onError() {
onClose();
},
});
const { changeRequests } = useScheduledChangeRequestsWithFlags(
projectId,
featureIds,
);
const { disableArchive, offendingParents, hasDeletedDependencies } =
useVerifyArchive(featureIds, projectId, isOpen);
const removeDependenciesWarning =
offendingParents.length === 0 && hasDeletedDependencies;
return (
<Dialogue
onClick={archiveAction}
open={isOpen}
onClose={onClose}
primaryButtonText={buttonText}
secondaryButtonText='Cancel'
title={dialogTitle}
disabledPrimaryButton={disableArchive}
>
<ConditionallyRender
condition={isBulkArchive}
show={
<>
<p>
Are you sure you want to archive{' '}
<strong>{featureIds?.length}</strong> feature flags?
</p>
<ConditionallyRender
condition={Boolean(
featuresWithUsage &&
featuresWithUsage?.length > 0,
)}
show={
<UsageWarning
ids={featuresWithUsage}
projectId={projectId}
/>
}
/>
<ConditionallyRender
condition={offendingParents.length > 0}
show={
<ArchiveParentError
ids={offendingParents}
projectId={projectId}
/>
}
/>
<ConditionallyRender
condition={removeDependenciesWarning}
show={<RemovedDependenciesAlert />}
/>
<ScheduledChangeRequestAlert
changeRequests={changeRequests}
projectId={projectId}
/>
<ConditionallyRender
condition={featureIds?.length <= 5}
show={
<ul>
{featureIds?.map((id) => (
<li key={id}>{id}</li>
))}
</ul>
}
/>
</>
}
elseShow={
<>
<p>
Are you sure you want to archive{' '}
{isBulkArchive
? 'these feature flags'
: 'this feature flag'}
?
</p>
<ConditionallyRender
condition={offendingParents.length > 0}
show={
<ArchiveParentError
ids={offendingParents}
projectId={projectId}
/>
}
/>
<ConditionallyRender
condition={removeDependenciesWarning}
show={<RemovedDependenciesAlert />}
/>
<ScheduledChangeRequestAlert
changeRequests={changeRequests}
projectId={projectId}
/>
</>
}
/>
</Dialogue>
);
};