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:
parent
9508c79451
commit
879e4c98e5
@ -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).',
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user