mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-12 13:48:35 +02:00
feat: show potential schedule conflicts when you archive flags (#5575)
Show change requests that would be impacted by an archive operation 
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 () => {
|
test('Add single archive feature change to change request', async () => {
|
||||||
const onClose = vi.fn();
|
const onClose = vi.fn();
|
||||||
const onConfirm = 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();
|
).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 useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
|
||||||
import { Alert, Typography } from '@mui/material';
|
import { Alert, Typography } from '@mui/material';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||||
import { useHighestPermissionChangeRequestEnvironment } from 'hooks/useHighestPermissionChangeRequestEnvironment';
|
import { useHighestPermissionChangeRequestEnvironment } from 'hooks/useHighestPermissionChangeRequestEnvironment';
|
||||||
import { useUiFlag } from '../../../hooks/useUiFlag';
|
import {
|
||||||
|
ChangeRequestIdentityData,
|
||||||
|
useScheduledChangeRequestsWithFlags,
|
||||||
|
} from 'hooks/api/getters/useScheduledChangeRequestsWithFlags/useScheduledChangeRequestsWithFlags';
|
||||||
|
|
||||||
interface IFeatureArchiveDialogProps {
|
interface IFeatureArchiveDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -125,6 +127,58 @@ const ArchiveParentError = ({
|
|||||||
return null;
|
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 useActionButtonText = (projectId: string, isBulkArchive: boolean) => {
|
||||||
const getHighestEnvironment =
|
const getHighestEnvironment =
|
||||||
useHighestPermissionChangeRequestEnvironment(projectId);
|
useHighestPermissionChangeRequestEnvironment(projectId);
|
||||||
@ -277,8 +331,6 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
|
|||||||
featureIds,
|
featureIds,
|
||||||
featuresWithUsage,
|
featuresWithUsage,
|
||||||
}) => {
|
}) => {
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
|
|
||||||
const isBulkArchive = featureIds?.length > 1;
|
const isBulkArchive = featureIds?.length > 1;
|
||||||
|
|
||||||
const buttonText = useActionButtonText(projectId, isBulkArchive);
|
const buttonText = useActionButtonText(projectId, isBulkArchive);
|
||||||
@ -299,6 +351,11 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { changeRequests } = useScheduledChangeRequestsWithFlags(
|
||||||
|
projectId,
|
||||||
|
featureIds,
|
||||||
|
);
|
||||||
|
|
||||||
const { disableArchive, offendingParents, hasDeletedDependencies } =
|
const { disableArchive, offendingParents, hasDeletedDependencies } =
|
||||||
useVerifyArchive(featureIds, projectId, isOpen);
|
useVerifyArchive(featureIds, projectId, isOpen);
|
||||||
|
|
||||||
@ -350,6 +407,12 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
|
|||||||
condition={removeDependenciesWarning}
|
condition={removeDependenciesWarning}
|
||||||
show={<RemovedDependenciesAlert />}
|
show={<RemovedDependenciesAlert />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ScheduledChangeRequestAlert
|
||||||
|
changeRequests={changeRequests}
|
||||||
|
projectId={projectId}
|
||||||
|
/>
|
||||||
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={featureIds?.length <= 5}
|
condition={featureIds?.length <= 5}
|
||||||
show={
|
show={
|
||||||
@ -384,6 +447,11 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
|
|||||||
condition={removeDependenciesWarning}
|
condition={removeDependenciesWarning}
|
||||||
show={<RemovedDependenciesAlert />}
|
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