mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-01 01:18:10 +02:00
feat: display potential conflicts in existing change requests (#5521)
This update displays schedule conflicts in active change requests (any CR that isn't applied, canceled, or rejected). 
This commit is contained in:
parent
a9363efec1
commit
a0a15416c4
@ -0,0 +1,135 @@
|
||||
import React from 'react';
|
||||
import { render } from 'utils/testRenderer';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { FeatureChange } from './FeatureChange';
|
||||
import {
|
||||
ChangeRequestState,
|
||||
IChangeRequestFeature,
|
||||
IFeatureChange,
|
||||
} from 'component/changeRequest/changeRequest.types';
|
||||
|
||||
describe('Schedule conflicts', () => {
|
||||
const change = {
|
||||
id: 15,
|
||||
action: 'deleteStrategy' as const,
|
||||
payload: {
|
||||
id: 'b3ac8595-8ad3-419e-aa18-4d82f2b6bc4c',
|
||||
name: 'flexibleRollout',
|
||||
},
|
||||
createdAt: new Date(),
|
||||
createdBy: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
imageUrl: '',
|
||||
},
|
||||
scheduleConflicts: {
|
||||
changeRequests: [
|
||||
{
|
||||
id: 73,
|
||||
},
|
||||
{
|
||||
id: 80,
|
||||
title: 'Adjust rollout percentage',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const feature = (change: IFeatureChange): IChangeRequestFeature => ({
|
||||
name: 'conflict-test',
|
||||
changes: [change],
|
||||
});
|
||||
|
||||
const changeRequest =
|
||||
(feature: IChangeRequestFeature) => (state: ChangeRequestState) => ({
|
||||
id: 1,
|
||||
state,
|
||||
title: '',
|
||||
project: 'default',
|
||||
environment: 'default',
|
||||
minApprovals: 1,
|
||||
createdBy: { id: 1, username: 'user1', imageUrl: '' },
|
||||
createdAt: new Date(),
|
||||
features: [feature],
|
||||
segments: [],
|
||||
approvals: [],
|
||||
rejections: [],
|
||||
comments: [],
|
||||
});
|
||||
|
||||
it.each(['Draft', 'Scheduled', 'In review', 'Approved'])(
|
||||
'should show schedule conflicts (when they exist) for change request in the %s state',
|
||||
async (changeRequestState) => {
|
||||
const flag = feature(change);
|
||||
render(
|
||||
<FeatureChange
|
||||
actions={null}
|
||||
index={0}
|
||||
changeRequest={changeRequest(flag)(
|
||||
changeRequestState as ChangeRequestState,
|
||||
)}
|
||||
change={change}
|
||||
feature={flag}
|
||||
/>,
|
||||
);
|
||||
|
||||
const alert = await screen.findByRole('alert');
|
||||
|
||||
expect(
|
||||
alert.textContent!.startsWith('Potential conflict'),
|
||||
).toBeTruthy();
|
||||
|
||||
const links = await screen.findAllByRole('link');
|
||||
|
||||
expect(links).toHaveLength(
|
||||
change.scheduleConflicts.changeRequests.length,
|
||||
);
|
||||
|
||||
const [link1, link2] = links;
|
||||
|
||||
expect(link1).toHaveTextContent('#73');
|
||||
expect(link1).toHaveAccessibleDescription('Change request 73');
|
||||
expect(link1).toHaveAttribute(
|
||||
'href',
|
||||
`/projects/default/change-requests/73`,
|
||||
);
|
||||
|
||||
expect(link2).toHaveTextContent('#80 (Adjust rollout percentage)');
|
||||
expect(link2).toHaveAccessibleDescription('Change request 80');
|
||||
expect(link2).toHaveAttribute(
|
||||
'href',
|
||||
`/projects/default/change-requests/80`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['Draft', 'Scheduled', 'In review', 'Approved'])(
|
||||
'should not show schedule conflicts when they do not exist for change request in the %s state',
|
||||
async (changeRequestState) => {
|
||||
const { scheduleConflicts, ...changeWithNoScheduleConflicts } =
|
||||
change;
|
||||
|
||||
const flag = feature(changeWithNoScheduleConflicts);
|
||||
|
||||
render(
|
||||
<FeatureChange
|
||||
actions={null}
|
||||
index={0}
|
||||
changeRequest={changeRequest(flag)(
|
||||
changeRequestState as ChangeRequestState,
|
||||
)}
|
||||
change={changeWithNoScheduleConflicts}
|
||||
feature={flag}
|
||||
/>,
|
||||
);
|
||||
|
||||
const links = screen.queryByRole('link');
|
||||
|
||||
expect(links).toBe(null);
|
||||
|
||||
const alert = screen.queryByRole('alert');
|
||||
|
||||
expect(alert).toBe(null);
|
||||
},
|
||||
);
|
||||
});
|
@ -13,6 +13,7 @@ import { VariantPatch } from './VariantPatch/VariantPatch';
|
||||
import { EnvironmentStrategyExecutionOrder } from './EnvironmentStrategyExecutionOrder/EnvironmentStrategyExecutionOrder';
|
||||
import { ArchiveFeatureChange } from './ArchiveFeatureChange';
|
||||
import { DependencyChange } from './DependencyChange';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const StyledSingleChangeBox = styled(Box, {
|
||||
shouldForwardProp: (prop: string) => !prop.startsWith('$'),
|
||||
@ -56,6 +57,15 @@ const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const InlineList = styled('ul')(({ theme }) => ({
|
||||
display: 'inline',
|
||||
padding: 0,
|
||||
li: { display: 'inline' },
|
||||
'li + li::before': {
|
||||
content: '", "',
|
||||
},
|
||||
}));
|
||||
|
||||
export const FeatureChange: FC<{
|
||||
actions: ReactNode;
|
||||
index: number;
|
||||
@ -71,9 +81,12 @@ export const FeatureChange: FC<{
|
||||
return (
|
||||
<StyledSingleChangeBox
|
||||
key={objectId(change)}
|
||||
$hasConflict={Boolean(change.conflict)}
|
||||
$hasConflict={Boolean(change.conflict || change.scheduleConflicts)}
|
||||
$isInConflictFeature={Boolean(feature.conflict)}
|
||||
$isAfterWarning={Boolean(feature.changes[index - 1]?.conflict)}
|
||||
$isAfterWarning={Boolean(
|
||||
feature.changes[index - 1]?.conflict ||
|
||||
feature.changes[index - 1]?.scheduleConflicts,
|
||||
)}
|
||||
$isLast={index + 1 === lastIndex}
|
||||
>
|
||||
<ConditionallyRender
|
||||
@ -86,6 +99,41 @@ export const FeatureChange: FC<{
|
||||
}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={Boolean(change.scheduleConflicts)}
|
||||
show={
|
||||
<StyledAlert severity='warning'>
|
||||
<strong>Potential conflict!</strong> This change would
|
||||
create conflicts with the following scheduled change
|
||||
request(s):{' '}
|
||||
<InlineList>
|
||||
{(
|
||||
change.scheduleConflicts ?? {
|
||||
changeRequests: [],
|
||||
}
|
||||
).changeRequests.map(({ id, title }) => {
|
||||
const text = title
|
||||
? `#${id} (${title})`
|
||||
: `#${id}`;
|
||||
return (
|
||||
<li key={id}>
|
||||
<Link
|
||||
to={`/projects/${changeRequest.project}/change-requests/${id}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
title={`Change request ${id}`}
|
||||
>
|
||||
{text}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
.
|
||||
</InlineList>
|
||||
</StyledAlert>
|
||||
}
|
||||
/>
|
||||
|
||||
<Box sx={(theme) => ({ padding: theme.spacing(3) })}>
|
||||
{(change.action === 'addDependency' ||
|
||||
change.action === 'deleteDependency') && (
|
||||
|
@ -66,6 +66,9 @@ export interface IChangeRequestChangeBase {
|
||||
conflict?: string;
|
||||
createdBy?: Pick<IUser, 'id' | 'username' | 'imageUrl'>;
|
||||
createdAt?: Date;
|
||||
scheduleConflicts?: {
|
||||
changeRequests: { id: number; title?: string }[];
|
||||
};
|
||||
}
|
||||
|
||||
export type ChangeRequestState =
|
||||
|
Loading…
Reference in New Issue
Block a user