1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-17 13:46:47 +02:00

Show conflicts in change requests (#2389)

## About the changes
Show warnings about incompatible changes in changesets.

Closes
[1-352/display-conflicts](https://linear.app/unleash/issue/1-352/display-conflicts)

Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item:
#2251
This commit is contained in:
Tymoteusz Czech 2022-11-15 09:53:38 +01:00 committed by GitHub
parent 89f2d81253
commit 8b057a1466
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 172 additions and 84 deletions

View File

@ -1,5 +1,5 @@
import { VFC } from 'react'; import { VFC } from 'react';
import { Box } from '@mui/material'; import { Alert, Box, styled } from '@mui/material';
import { ChangeRequestFeatureToggleChange } from '../ChangeRequestOverview/ChangeRequestFeatureToggleChange/ChangeRequestFeatureToggleChange'; import { ChangeRequestFeatureToggleChange } from '../ChangeRequestOverview/ChangeRequestFeatureToggleChange/ChangeRequestFeatureToggleChange';
import { objectId } from 'utils/objectId'; import { objectId } from 'utils/objectId';
import { ToggleStatusChange } from '../ChangeRequestOverview/ChangeRequestFeatureToggleChange/ToggleStatusChange'; import { ToggleStatusChange } from '../ChangeRequestOverview/ChangeRequestFeatureToggleChange/ToggleStatusChange';
@ -17,6 +17,7 @@ import {
GetFeatureStrategyIcon, GetFeatureStrategyIcon,
} from 'utils/strategyNames'; } from 'utils/strategyNames';
import { hasNameField } from '../changeRequest.types'; import { hasNameField } from '../changeRequest.types';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
interface IChangeRequestProps { interface IChangeRequestProps {
changeRequest: IChangeRequest; changeRequest: IChangeRequest;
@ -24,6 +25,38 @@ interface IChangeRequestProps {
onNavigate?: () => void; onNavigate?: () => void;
} }
const StyledSingleChangeBox = styled(Box)<{
hasConflict: boolean;
isAfterWarning: boolean;
isLast: boolean;
inInConflictFeature: boolean;
}>(({ theme, hasConflict, inInConflictFeature, isAfterWarning, isLast }) => ({
borderLeft: '1px solid',
borderRight: '1px solid',
borderTop: '1px solid',
borderBottom: isLast ? '1px solid' : 'none',
borderRadius: isLast
? `0 0
${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px`
: 0,
borderColor:
hasConflict || inInConflictFeature
? theme.palette.warning.border
: theme.palette.dividerAlternative,
borderTopColor:
(hasConflict || isAfterWarning) && !inInConflictFeature
? theme.palette.warning.border
: theme.palette.dividerAlternative,
}));
const StyledAlert = styled(Alert)(({ theme }) => ({
borderRadius: 0,
padding: theme.spacing(0, 2),
'&.MuiAlert-standardWarning': {
borderStyle: 'none none solid none',
},
}));
export const ChangeRequest: VFC<IChangeRequestProps> = ({ export const ChangeRequest: VFC<IChangeRequestProps> = ({
changeRequest, changeRequest,
onRefetch, onRefetch,
@ -56,63 +89,86 @@ export const ChangeRequest: VFC<IChangeRequestProps> = ({
featureName={featureToggleChange.name} featureName={featureToggleChange.name}
projectId={changeRequest.project} projectId={changeRequest.project}
onNavigate={onNavigate} onNavigate={onNavigate}
conflict={featureToggleChange.conflict}
> >
{featureToggleChange.changes.map(change => ( {featureToggleChange.changes.map((change, index) => (
<Box <StyledSingleChangeBox
key={objectId(change)} key={objectId(change)}
sx={theme => ({ hasConflict={Boolean(change.conflict)}
padding: 2, inInConflictFeature={Boolean(
borderTop: '1px solid', featureToggleChange.conflict
borderColor: theme => )}
theme.palette.dividerAlternative, isAfterWarning={Boolean(
})} featureToggleChange.changes[index - 1]?.conflict
)}
isLast={
index + 1 === featureToggleChange.changes.length
}
> >
{change.action === 'updateEnabled' && ( <ConditionallyRender
<ToggleStatusChange condition={
enabled={change.payload.enabled} Boolean(change.conflict) &&
onDiscard={onDiscard(change.id)} !featureToggleChange.conflict
/> }
)} show={
{change.action === 'addStrategy' && ( <StyledAlert severity="warning">
<StrategyAddedChange <strong>Conflict!</strong> This change
onDiscard={onDiscard(change.id)} cant be applied. {change.conflict}.
> </StyledAlert>
<GetFeatureStrategyIcon }
strategyName={change.payload.name} />
<Box sx={{ p: 2 }}>
{change.action === 'updateEnabled' && (
<ToggleStatusChange
enabled={change.payload.enabled}
onDiscard={onDiscard(change.id)}
/> />
)}
{change.action === 'addStrategy' && (
<StrategyAddedChange
onDiscard={onDiscard(change.id)}
>
<GetFeatureStrategyIcon
strategyName={change.payload.name}
/>
{formatStrategyName(change.payload.name)} {formatStrategyName(
</StrategyAddedChange> change.payload.name
)} )}
{change.action === 'deleteStrategy' && ( </StrategyAddedChange>
<StrategyDeletedChange )}
onDiscard={onDiscard(change.id)} {change.action === 'deleteStrategy' && (
> <StrategyDeletedChange
{hasNameField(change.payload) && ( onDiscard={onDiscard(change.id)}
<> >
<GetFeatureStrategyIcon {hasNameField(change.payload) && (
strategyName={ <>
<GetFeatureStrategyIcon
strategyName={
change.payload.name
}
/>
{formatStrategyName(
change.payload.name change.payload.name
} )}
/> </>
{formatStrategyName( )}
change.payload.name </StrategyDeletedChange>
)} )}
</> {change.action === 'updateStrategy' && (
)} <StrategyEditedChange
</StrategyDeletedChange> onDiscard={onDiscard(change.id)}
)} >
{change.action === 'updateStrategy' && ( <GetFeatureStrategyIcon
<StrategyEditedChange strategyName={change.payload.name}
onDiscard={onDiscard(change.id)} />
> {formatStrategyName(
<GetFeatureStrategyIcon change.payload.name
strategyName={change.payload.name} )}
/> </StrategyEditedChange>
{formatStrategyName(change.payload.name)} )}
</StrategyEditedChange> </Box>
)} </StyledSingleChangeBox>
</Box>
))} ))}
</ChangeRequestFeatureToggleChange> </ChangeRequestFeatureToggleChange>
))} ))}

View File

@ -1,48 +1,79 @@
import { FC } from 'react'; import { FC } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Box, Card, Typography } from '@mui/material'; import { Alert, Box, Card, Typography } from '@mui/material';
import ToggleOnIcon from '@mui/icons-material/ToggleOn'; import ToggleOnIcon from '@mui/icons-material/ToggleOn';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
interface IChangeRequestToggleChange { interface IChangeRequestToggleChange {
featureName: string; featureName: string;
projectId: string; projectId: string;
conflict?: string;
onNavigate?: () => void; onNavigate?: () => void;
} }
export const ChangeRequestFeatureToggleChange: FC< export const ChangeRequestFeatureToggleChange: FC<
IChangeRequestToggleChange IChangeRequestToggleChange
> = ({ featureName, projectId, onNavigate, children }) => { > = ({ featureName, projectId, conflict, onNavigate, children }) => (
return ( <Card
<Card elevation={0}
elevation={0} sx={theme => ({
marginTop: theme.spacing(2),
overflow: 'hidden',
})}
>
<Box
sx={theme => ({ sx={theme => ({
marginTop: theme.spacing(2), backgroundColor: Boolean(conflict)
borderRadius: theme => `${theme.shape.borderRadiusLarge}px`, ? theme.palette.warning.light
overflow: 'hidden', : theme.palette.tableHeaderBackground,
borderRadius: theme =>
`${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px 0 0`,
border: '1px solid', border: '1px solid',
borderColor: theme => theme.palette.dividerAlternative, borderColor: theme =>
conflict
? theme.palette.warning.border
: theme.palette.dividerAlternative,
borderBottom: 'none',
overflow: 'hidden',
})} })}
> >
<Box <ConditionallyRender
sx={theme => ({ condition={Boolean(conflict)}
backgroundColor: theme.palette.tableHeaderBackground, show={
padding: theme.spacing(3), <Alert
})} severity="warning"
> sx={{
<Box sx={{ display: 'flex', gap: 1 }}> mx: 1,
<ToggleOnIcon color="disabled" /> '&.MuiAlert-standardWarning': {
<Typography borderStyle: 'none',
component={Link} },
to={`/projects/${projectId}/features/${featureName}`} }}
color="primary"
sx={{ textDecoration: 'none' }}
onClick={onNavigate}
> >
{featureName} <strong>Conflict!</strong> {conflict}.
</Typography> </Alert>
</Box> }
/>
<Box
sx={{
display: 'flex',
gap: 1,
pt: conflict ? 0 : 2,
pb: 2,
px: 3,
}}
>
<ToggleOnIcon color="disabled" />
<Typography
component={Link}
to={`/projects/${projectId}/features/${featureName}`}
color="primary"
sx={{ textDecoration: 'none' }}
onClick={onNavigate}
>
{featureName}
</Typography>
</Box> </Box>
<Box>{children}</Box> </Box>
</Card> <Box>{children}</Box>
); </Card>
}; );

View File

@ -89,10 +89,10 @@ export const ChangeRequestOverview: FC = () => {
<StyledAsideBox> <StyledAsideBox>
<ChangeRequestTimeline state={changeRequest.state} /> <ChangeRequestTimeline state={changeRequest.state} />
<ConditionallyRender <ConditionallyRender
condition={changeRequest.approvals.length > 0} condition={changeRequest.approvals?.length > 0}
show={ show={
<ChangeRequestReviewers> <ChangeRequestReviewers>
{changeRequest.approvals.map(approver => ( {changeRequest.approvals?.map(approver => (
<ChangeRequestReviewer <ChangeRequestReviewer
name={ name={
approver.createdBy.username || approver.createdBy.username ||

View File

@ -10,6 +10,7 @@ export interface IChangeRequest {
createdAt: Date; createdAt: Date;
features: IChangeRequestFeature[]; features: IChangeRequestFeature[];
approvals: IChangeRequestApproval[]; approvals: IChangeRequestApproval[];
conflict?: string;
} }
export interface IChangeRequestEnvironmentConfig { export interface IChangeRequestEnvironmentConfig {