mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02:00
feat: show scheduled CRs using strategies when removing it (#5560)
Show a warning about how deleting a strategy might mess up scheduled change requests. If there are change requests, list them. If there are no conflicts, show nothing. If we don't know (because of no successful response from the API), say that it might cause issues. 
This commit is contained in:
parent
87ebbb0fa2
commit
a228f54344
@ -117,7 +117,12 @@ const renderHeaderChildren = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (draftChange) {
|
if (draftChange) {
|
||||||
badges.push(<ChangeRequestStatusBadge change={draftChange.change} />);
|
badges.push(
|
||||||
|
<ChangeRequestStatusBadge
|
||||||
|
key={`draft-change#${draftChange.change.id}`}
|
||||||
|
change={draftChange.change}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scheduledChanges = changes.filter(
|
const scheduledChanges = changes.filter(
|
||||||
@ -127,6 +132,7 @@ const renderHeaderChildren = (
|
|||||||
if (scheduledChanges.length > 0) {
|
if (scheduledChanges.length > 0) {
|
||||||
badges.push(
|
badges.push(
|
||||||
<ChangesScheduledBadge
|
<ChangesScheduledBadge
|
||||||
|
key='scheduled-changes'
|
||||||
scheduledChangeRequestIds={scheduledChanges.map(
|
scheduledChangeRequestIds={scheduledChanges.map(
|
||||||
(scheduledChange) => scheduledChange.changeRequestId,
|
(scheduledChange) => scheduledChange.changeRequestId,
|
||||||
)}
|
)}
|
||||||
|
@ -0,0 +1,141 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render } from 'utils/testRenderer';
|
||||||
|
import { screen } from '@testing-library/react';
|
||||||
|
import {
|
||||||
|
FeatureStrategyRemoveDialogue,
|
||||||
|
SuggestFeatureStrategyRemoveDialogue,
|
||||||
|
} from './DialogStrategyRemove';
|
||||||
|
import {
|
||||||
|
ChangeRequestState,
|
||||||
|
IChangeRequestFeature,
|
||||||
|
IFeatureChange,
|
||||||
|
} from 'component/changeRequest/changeRequest.types';
|
||||||
|
|
||||||
|
const strategyId = 'c81e3a1d-e91c-4083-bd0f-75bb8a9e32a2';
|
||||||
|
const projectId = 'default';
|
||||||
|
const environment = 'development';
|
||||||
|
const featureId = 'bb1d79e0-95b0-4393-b248-64d1e0294ee3';
|
||||||
|
|
||||||
|
describe('Use in scheduled change requests', () => {
|
||||||
|
it.each(['enabled', 'disabled'])(
|
||||||
|
'should show usage in scheduled change requests with change requests %s for the project',
|
||||||
|
async (changeRequestsEnabled) => {
|
||||||
|
const changeRequestWithTitle = { id: 1, title: 'My CR' };
|
||||||
|
const changeRequestWithoutTitle = { id: 2 };
|
||||||
|
const scheduledChangeRequests = [
|
||||||
|
changeRequestWithTitle,
|
||||||
|
changeRequestWithoutTitle,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (changeRequestsEnabled === 'enabled') {
|
||||||
|
render(
|
||||||
|
<SuggestFeatureStrategyRemoveDialogue
|
||||||
|
onRemove={async () => {}}
|
||||||
|
onClose={() => {}}
|
||||||
|
isOpen={true}
|
||||||
|
scheduledChangeRequestsForStrategy={{
|
||||||
|
projectId,
|
||||||
|
changeRequests: scheduledChangeRequests,
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
render(
|
||||||
|
<FeatureStrategyRemoveDialogue
|
||||||
|
onRemove={async () => {}}
|
||||||
|
onClose={() => {}}
|
||||||
|
isOpen={true}
|
||||||
|
scheduledChangeRequestsForStrategy={{
|
||||||
|
projectId,
|
||||||
|
changeRequests: scheduledChangeRequests,
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const alerts = await screen.findAllByRole('alert');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
alerts.find((alert) =>
|
||||||
|
alert.textContent!.startsWith('This strategy'),
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
const links = await screen.findAllByRole('link');
|
||||||
|
|
||||||
|
expect(links).toHaveLength(scheduledChangeRequests.length);
|
||||||
|
|
||||||
|
const [link1, link2] = links;
|
||||||
|
|
||||||
|
expect(link1).toHaveTextContent('#1 (My CR)');
|
||||||
|
expect(link1).toHaveAccessibleDescription('Change request 1');
|
||||||
|
expect(link1).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
`/projects/default/change-requests/1`,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(link2).toHaveTextContent('#2');
|
||||||
|
expect(link2).toHaveAccessibleDescription('Change request 2');
|
||||||
|
expect(link2).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
`/projects/default/change-requests/2`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should not render scheduled change requests warning when there are no scheduled change requests', async () => {
|
||||||
|
render(
|
||||||
|
<SuggestFeatureStrategyRemoveDialogue
|
||||||
|
onRemove={async () => {}}
|
||||||
|
onClose={() => {}}
|
||||||
|
isOpen={true}
|
||||||
|
scheduledChangeRequestsForStrategy={{
|
||||||
|
projectId,
|
||||||
|
changeRequests: [],
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const alerts = await screen.findAllByRole('alert');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
alerts.find((alert) =>
|
||||||
|
alert.textContent!.startsWith('This strategy'),
|
||||||
|
),
|
||||||
|
).toBeFalsy();
|
||||||
|
|
||||||
|
expect(alerts).toHaveLength(1);
|
||||||
|
|
||||||
|
const link = screen.queryByRole('link');
|
||||||
|
|
||||||
|
expect(link).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("It should render a warning saying there might be scheduled change requests if it doesn't get a successful API response", async () => {
|
||||||
|
render(
|
||||||
|
<SuggestFeatureStrategyRemoveDialogue
|
||||||
|
onRemove={async () => {}}
|
||||||
|
onClose={() => {}}
|
||||||
|
isOpen={true}
|
||||||
|
scheduledChangeRequestsForStrategy={{
|
||||||
|
projectId,
|
||||||
|
changeRequests: undefined,
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const alerts = await screen.findAllByRole('alert');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
alerts.find((alert) =>
|
||||||
|
alert.textContent!.startsWith('This strategy'),
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
expect(alerts).toHaveLength(2);
|
||||||
|
|
||||||
|
const link = screen.queryByRole('link');
|
||||||
|
|
||||||
|
expect(link).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
@ -1,7 +1,7 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
|
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { formatFeaturePath } from '../../../../../../../../FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
|
import { formatFeaturePath } from '../../../../../../../../FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
|
||||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
@ -10,7 +10,8 @@ import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
|||||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { useScheduledChangeRequestsWithStrategy } from 'hooks/api/getters/useScheduledChangeRequestsWithStrategy/useScheduledChangeRequestsWithStrategy';
|
||||||
interface IFeatureStrategyRemoveProps {
|
interface IFeatureStrategyRemoveProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
featureId: string;
|
featureId: string;
|
||||||
@ -18,13 +19,23 @@ interface IFeatureStrategyRemoveProps {
|
|||||||
strategyId: string;
|
strategyId: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
icon?: boolean;
|
icon?: boolean;
|
||||||
text?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChangeRequest = {
|
||||||
|
id: number;
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ScheduledChangeRequestData = {
|
||||||
|
changeRequests?: ChangeRequest[];
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface IFeatureStrategyRemoveDialogueProps {
|
interface IFeatureStrategyRemoveDialogueProps {
|
||||||
onRemove: (event: React.FormEvent) => Promise<void>;
|
onRemove: (event: React.FormEvent) => Promise<void>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
scheduledChangeRequestsForStrategy: ScheduledChangeRequestData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RemoveAlert: FC = () => (
|
const RemoveAlert: FC = () => (
|
||||||
@ -34,30 +45,103 @@ const RemoveAlert: FC = () => (
|
|||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
|
|
||||||
const FeatureStrategyRemoveDialogue: FC<IFeatureStrategyRemoveDialogueProps> =
|
const AlertContainer = styled('div')(({ theme }) => ({
|
||||||
({ onRemove, onClose, isOpen }) => {
|
'> * + *': {
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StrategyInScheduledChangeRequestsWarning: FC<{
|
||||||
|
changeRequests?: ChangeRequest[];
|
||||||
|
projectId: string;
|
||||||
|
}> = ({ changeRequests, projectId }) => {
|
||||||
|
if (changeRequests && changeRequests.length > 0) {
|
||||||
return (
|
return (
|
||||||
<Dialogue
|
<Alert severity='warning'>
|
||||||
title='Are you sure you want to delete this strategy?'
|
<p>
|
||||||
open={isOpen}
|
This strategy is in use by at least one scheduled change
|
||||||
primaryButtonText='Remove strategy'
|
request. If you remove it, those change requests can no
|
||||||
secondaryButtonText='Cancel'
|
longer be applied.
|
||||||
onClick={onRemove}
|
</p>
|
||||||
onClose={onClose}
|
<p>
|
||||||
>
|
The following scheduled change requests use this strategy:
|
||||||
<RemoveAlert />
|
</p>
|
||||||
</Dialogue>
|
<ul>
|
||||||
|
{changeRequests.map(({ id, title }) => {
|
||||||
|
const text = title ? `#${id} (${title})` : `#${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 strategy may be in use by one or more scheduled change
|
||||||
|
requests. If you remove it, those change requests can no
|
||||||
|
longer be applied.
|
||||||
|
</p>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// all good, we have nothing to show
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Alerts: FC<{
|
||||||
|
scheduledChangeRequestsForStrategy: ScheduledChangeRequestData;
|
||||||
|
}> = ({ scheduledChangeRequestsForStrategy }) => (
|
||||||
|
<AlertContainer>
|
||||||
|
<RemoveAlert />
|
||||||
|
<StrategyInScheduledChangeRequestsWarning
|
||||||
|
projectId={scheduledChangeRequestsForStrategy.projectId}
|
||||||
|
changeRequests={scheduledChangeRequestsForStrategy.changeRequests}
|
||||||
|
/>
|
||||||
|
</AlertContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FeatureStrategyRemoveDialogue: FC<
|
||||||
|
IFeatureStrategyRemoveDialogueProps
|
||||||
|
> = ({ onRemove, onClose, isOpen, scheduledChangeRequestsForStrategy }) => {
|
||||||
|
return (
|
||||||
|
<Dialogue
|
||||||
|
title='Are you sure you want to delete this strategy?'
|
||||||
|
open={isOpen}
|
||||||
|
primaryButtonText='Remove strategy'
|
||||||
|
secondaryButtonText='Cancel'
|
||||||
|
onClick={onRemove}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<Alerts
|
||||||
|
scheduledChangeRequestsForStrategy={
|
||||||
|
scheduledChangeRequestsForStrategy
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Dialogue>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const MsgContainer = styled('div')(({ theme }) => ({
|
const MsgContainer = styled('div')(({ theme }) => ({
|
||||||
marginTop: theme.spacing(3),
|
marginTop: theme.spacing(3),
|
||||||
marginBottom: theme.spacing(1),
|
marginBottom: theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const SuggestFeatureStrategyRemoveDialogue: FC<
|
export const SuggestFeatureStrategyRemoveDialogue: FC<
|
||||||
IFeatureStrategyRemoveDialogueProps
|
IFeatureStrategyRemoveDialogueProps
|
||||||
> = ({ onRemove, onClose, isOpen }) => {
|
> = ({ onRemove, onClose, isOpen, scheduledChangeRequestsForStrategy }) => {
|
||||||
return (
|
return (
|
||||||
<Dialogue
|
<Dialogue
|
||||||
title='Suggest changes'
|
title='Suggest changes'
|
||||||
@ -67,7 +151,11 @@ const SuggestFeatureStrategyRemoveDialogue: FC<
|
|||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<RemoveAlert />
|
<Alerts
|
||||||
|
scheduledChangeRequestsForStrategy={
|
||||||
|
scheduledChangeRequestsForStrategy
|
||||||
|
}
|
||||||
|
/>
|
||||||
<MsgContainer>
|
<MsgContainer>
|
||||||
<Typography variant='body2' color='text.secondary'>
|
<Typography variant='body2' color='text.secondary'>
|
||||||
Your suggestion:
|
Your suggestion:
|
||||||
@ -155,7 +243,6 @@ export const DialogStrategyRemove = ({
|
|||||||
featureId,
|
featureId,
|
||||||
environmentId,
|
environmentId,
|
||||||
strategyId,
|
strategyId,
|
||||||
text,
|
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
}: IFeatureStrategyRemoveProps & {
|
}: IFeatureStrategyRemoveProps & {
|
||||||
@ -164,6 +251,16 @@ export const DialogStrategyRemove = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||||
|
|
||||||
|
const { changeRequests } = useScheduledChangeRequestsWithStrategy(
|
||||||
|
projectId,
|
||||||
|
strategyId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const changeRequestData = {
|
||||||
|
changeRequests,
|
||||||
|
projectId,
|
||||||
|
};
|
||||||
|
|
||||||
const onRemove = useOnRemove({
|
const onRemove = useOnRemove({
|
||||||
featureId,
|
featureId,
|
||||||
projectId,
|
projectId,
|
||||||
@ -186,6 +283,7 @@ export const DialogStrategyRemove = ({
|
|||||||
await onSuggestRemove(e);
|
await onSuggestRemove(e);
|
||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
|
scheduledChangeRequestsForStrategy={changeRequestData}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -195,6 +293,7 @@ export const DialogStrategyRemove = ({
|
|||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={() => onClose()}
|
onClose={() => onClose()}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
|
scheduledChangeRequestsForStrategy={changeRequestData}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
|
import { IChangeRequest } from 'component/changeRequest/changeRequest.types';
|
||||||
|
import { useEnterpriseSWR } from '../useEnterpriseSWR/useEnterpriseSWR';
|
||||||
|
|
||||||
|
const fetcher = (path: string) => {
|
||||||
|
return fetch(path)
|
||||||
|
.then(handleErrorResponses('ChangeRequest'))
|
||||||
|
.then((res) => res.json());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useScheduledChangeRequestsWithStrategy = (
|
||||||
|
project: string,
|
||||||
|
strategyId: string,
|
||||||
|
) => {
|
||||||
|
const { data, error, mutate } = useEnterpriseSWR<IChangeRequest[]>(
|
||||||
|
[],
|
||||||
|
formatApiPath(
|
||||||
|
`api/admin/projects/${project}/change-requests/scheduled/with-strategy/${strategyId}`,
|
||||||
|
),
|
||||||
|
fetcher,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
changeRequests: data,
|
||||||
|
loading: !error && !data,
|
||||||
|
refetch: mutate,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user