1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

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)
This commit is contained in:
Thomas Heartman 2023-12-11 10:45:45 +01:00 committed by GitHub
parent 9508c79451
commit 879e4c98e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 159 additions and 4 deletions

View File

@ -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(
<FeatureArchiveDialog
featureIds={featureIds}
projectId={'projectId'}
isOpen={true}
onClose={vi.fn()}
onConfirm={vi.fn()}
featuresWithUsage={[]}
/>,
);
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).',
);
},
);
});

View File

@ -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 (
<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);
@ -277,8 +331,6 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
featureIds,
featuresWithUsage,
}) => {
const { uiConfig } = useUiConfig();
const isBulkArchive = featureIds?.length > 1;
const buttonText = useActionButtonText(projectId, isBulkArchive);
@ -299,6 +351,11 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
},
});
const { changeRequests } = useScheduledChangeRequestsWithFlags(
projectId,
featureIds,
);
const { disableArchive, offendingParents, hasDeletedDependencies } =
useVerifyArchive(featureIds, projectId, isOpen);
@ -350,6 +407,12 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
condition={removeDependenciesWarning}
show={<RemovedDependenciesAlert />}
/>
<ScheduledChangeRequestAlert
changeRequests={changeRequests}
projectId={projectId}
/>
<ConditionallyRender
condition={featureIds?.length <= 5}
show={
@ -384,6 +447,11 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
condition={removeDependenciesWarning}
show={<RemovedDependenciesAlert />}
/>
<ScheduledChangeRequestAlert
changeRequests={changeRequests}
projectId={projectId}
/>
</>
}
/>

View File

@ -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,
};
};