From 879e4c98e503d32a7ef58c587e3c51fe5db86f7e Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Mon, 11 Dec 2023 10:45:45 +0100 Subject: [PATCH] feat: show potential schedule conflicts when you archive flags (#5575) Show change requests that would be impacted by an archive operation ![image](https://github.com/Unleash/unleash/assets/17786332/7b2af89a-7292-4b90-b7a4-768df375e0fb) --- .../FeatureArchiveDialog.test.tsx | 49 ++++++++++++ .../FeatureArchiveDialog.tsx | 76 ++++++++++++++++++- .../useScheduledChangeRequestsWithFlags.ts | 38 ++++++++++ 3 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 frontend/src/hooks/api/getters/useScheduledChangeRequestsWithFlags/useScheduledChangeRequestsWithFlags.ts diff --git a/frontend/src/component/common/FeatureArchiveDialog/FeatureArchiveDialog.test.tsx b/frontend/src/component/common/FeatureArchiveDialog/FeatureArchiveDialog.test.tsx index 81afb8b7c4..9c937ca569 100644 --- a/frontend/src/component/common/FeatureArchiveDialog/FeatureArchiveDialog.test.tsx +++ b/frontend/src/component/common/FeatureArchiveDialog/FeatureArchiveDialog.test.tsx @@ -43,6 +43,16 @@ const setupArchiveValidation = (orphanParents: string[]) => { ); }; +const setupFlagScheduleConflicts = ( + scheduledCRs: { id: number; title?: string }[], +) => { + testServerRoute( + server, + '/api/admin/projects/projectId/change-requests/scheduled', + scheduledCRs, + ); +}; + test('Add single archive feature change to change request', async () => { const onClose = vi.fn(); const onConfirm = vi.fn(); @@ -183,3 +193,42 @@ test('Show error message when 1 parent of orphaned children is archived', async ), ).not.toBeInTheDocument(); }); + +describe('schedule conflicts', () => { + test.each([1, 2, 5, 10])( + 'Shows a warning when archiving %s flag(s) with change request schedule conflicts', + async (numberOfFlags) => { + setupArchiveValidation([]); + const featureIds = new Array(numberOfFlags) + .fill(0) + .map((_, i) => `feature-flag-${i + 1}`); + + const conflicts = [{ id: 5, title: 'crTitle' }, { id: 6 }]; + setupFlagScheduleConflicts(conflicts); + + render( + , + ); + + const links = await screen.findAllByRole('link'); + expect(links).toHaveLength(2); + expect(links[0]).toHaveTextContent('#5 (crTitle)'); + expect(links[0]).toHaveAccessibleDescription('Change request 5'); + expect(links[1]).toHaveTextContent('Change request #6'); + expect(links[1]).toHaveAccessibleDescription('Change request 6'); + + const alerts = await screen.findAllByRole('alert'); + expect(alerts).toHaveLength(2); + expect(alerts[1]).toHaveTextContent( + 'This archive operation would conflict with 2 scheduled change request(s).', + ); + }, + ); +}); diff --git a/frontend/src/component/common/FeatureArchiveDialog/FeatureArchiveDialog.tsx b/frontend/src/component/common/FeatureArchiveDialog/FeatureArchiveDialog.tsx index c2ac11e8d9..e64c8fb1db 100644 --- a/frontend/src/component/common/FeatureArchiveDialog/FeatureArchiveDialog.tsx +++ b/frontend/src/component/common/FeatureArchiveDialog/FeatureArchiveDialog.tsx @@ -7,12 +7,14 @@ 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'; 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 { useUiFlag } from '../../../hooks/useUiFlag'; +import { + ChangeRequestIdentityData, + useScheduledChangeRequestsWithFlags, +} from 'hooks/api/getters/useScheduledChangeRequestsWithFlags/useScheduledChangeRequestsWithFlags'; interface IFeatureArchiveDialogProps { isOpen: boolean; @@ -125,6 +127,58 @@ const ArchiveParentError = ({ return null; }; +const ScheduledChangeRequestAlert: VFC<{ + changeRequests?: ChangeRequestIdentityData[]; + projectId: string; +}> = ({ changeRequests, projectId }) => { + if (changeRequests && changeRequests.length > 0) { + return ( + theme.spacing(2, 0) }} + > +

+ This archive operation would conflict with{' '} + {changeRequests.length} scheduled change request(s). The + change request(s) that would be affected by this are: +

+
    + {changeRequests.map(({ id, title }) => { + const text = title + ? `#${id} (${title})` + : `Change request #${id}`; + return ( +
  • + + {text} + +
  • + ); + })} +
+
+ ); + } else if (changeRequests === undefined) { + return ( + +

+ This archive operation might conflict with one or more + scheduled change requests. If you complete it, those change + requests can no longer be applied. +

+
+ ); + } + + // all good, we have nothing to show + return null; +}; + const useActionButtonText = (projectId: string, isBulkArchive: boolean) => { const getHighestEnvironment = useHighestPermissionChangeRequestEnvironment(projectId); @@ -277,8 +331,6 @@ export const FeatureArchiveDialog: VFC = ({ featureIds, featuresWithUsage, }) => { - const { uiConfig } = useUiConfig(); - const isBulkArchive = featureIds?.length > 1; const buttonText = useActionButtonText(projectId, isBulkArchive); @@ -299,6 +351,11 @@ export const FeatureArchiveDialog: VFC = ({ }, }); + const { changeRequests } = useScheduledChangeRequestsWithFlags( + projectId, + featureIds, + ); + const { disableArchive, offendingParents, hasDeletedDependencies } = useVerifyArchive(featureIds, projectId, isOpen); @@ -350,6 +407,12 @@ export const FeatureArchiveDialog: VFC = ({ condition={removeDependenciesWarning} show={} /> + + + = ({ condition={removeDependenciesWarning} show={} /> + + } /> diff --git a/frontend/src/hooks/api/getters/useScheduledChangeRequestsWithFlags/useScheduledChangeRequestsWithFlags.ts b/frontend/src/hooks/api/getters/useScheduledChangeRequestsWithFlags/useScheduledChangeRequestsWithFlags.ts new file mode 100644 index 0000000000..678e547b4f --- /dev/null +++ b/frontend/src/hooks/api/getters/useScheduledChangeRequestsWithFlags/useScheduledChangeRequestsWithFlags.ts @@ -0,0 +1,38 @@ +import { formatApiPath } from 'utils/formatPath'; +import handleErrorResponses from '../httpErrorResponseHandler'; +import { useEnterpriseSWR } from '../useEnterpriseSWR/useEnterpriseSWR'; + +const fetcher = (path: string) => { + return fetch(path) + .then(handleErrorResponses('ChangeRequest')) + .then((res) => res.json()); +}; + +export type ChangeRequestIdentityData = { + id: number; + title?: string; +}; + +export const useScheduledChangeRequestsWithFlags = ( + project: string, + flags: string[], +) => { + const queryString = flags.map((flag) => `feature=${flag}`).join('&'); + + const { data, error, mutate } = useEnterpriseSWR< + ChangeRequestIdentityData[] + >( + [], + formatApiPath( + `api/admin/projects/${project}/change-requests/scheduled?${queryString}`, + ), + fetcher, + ); + + return { + changeRequests: data, + loading: !error && !data, + refetch: mutate, + error, + }; +};