1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +02:00

Add various fixes to the CR view (#10231)

Adds a number of small corrections and fixes to (mainly) CR-related
component. Discovered as part of adding the new JSON diff view. Refer to
the various inline comments for explanations.

---------

Co-authored-by: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com>
This commit is contained in:
Thomas Heartman 2025-06-27 12:16:11 +02:00 committed by GitHub
parent 55b8941306
commit f2766b6b3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 95 additions and 71 deletions

View File

@ -16,7 +16,7 @@ export const ArchiveFeatureChange: FC<IArchiveFeatureChange> = ({
actions, actions,
}) => ( }) => (
<ChangeItemWrapper> <ChangeItemWrapper>
<ArchiveBox>Archiving feature</ArchiveBox> <ArchiveBox>Archiving flag</ArchiveBox>
{actions} {actions}
</ChangeItemWrapper> </ChangeItemWrapper>
); );

View File

@ -26,6 +26,7 @@ import { FeatureStrategyForm } from '../../../../feature/FeatureStrategy/Feature
import { NewStrategyVariants } from 'component/feature/StrategyTypes/NewStrategyVariants'; import { NewStrategyVariants } from 'component/feature/StrategyTypes/NewStrategyVariants';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { constraintId } from 'constants/constraintId.ts'; import { constraintId } from 'constants/constraintId.ts';
import { apiPayloadConstraintReplacer } from 'utils/api-payload-constraint-replacer.ts';
interface IEditChangeProps { interface IEditChangeProps {
change: IChangeRequestAddStrategy | IChangeRequestUpdateStrategy; change: IChangeRequestAddStrategy | IChangeRequestUpdateStrategy;
@ -208,7 +209,7 @@ export const formatUpdateStrategyApiCode = (
} }
const url = `${unleashUrl}/api/admin/projects/${projectId}/change-requests/${changeRequestId}/changes/${changeId}`; const url = `${unleashUrl}/api/admin/projects/${projectId}/change-requests/${changeRequestId}/changes/${changeId}`;
const payload = JSON.stringify(strategy, undefined, 2); const payload = JSON.stringify(strategy, apiPayloadConstraintReplacer, 2);
return `curl --location --request PUT '${url}' \\ return `curl --location --request PUT '${url}' \\
--header 'Authorization: INSERT_API_KEY' \\ --header 'Authorization: INSERT_API_KEY' \\

View File

@ -6,6 +6,7 @@ import { Box, styled } from '@mui/material';
import { EnvironmentStrategyOrderDiff } from './EnvironmentStrategyOrderDiff.tsx'; import { EnvironmentStrategyOrderDiff } from './EnvironmentStrategyOrderDiff.tsx';
import { StrategyExecution } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution'; import { StrategyExecution } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution';
import { formatStrategyName } from '../../../../../../utils/strategyNames.tsx'; import { formatStrategyName } from '../../../../../../utils/strategyNames.tsx';
import type { IFeatureStrategy } from 'interfaces/strategy.ts';
const ChangeItemInfo = styled(Box)({ const ChangeItemInfo = styled(Box)({
display: 'flex', display: 'flex',
@ -74,14 +75,14 @@ export const EnvironmentStrategyExecutionOrder = ({
.map((strategy) => strategy.id) ?? [], .map((strategy) => strategy.id) ?? [],
}; };
const updatedStrategies = change.payload const updatedStrategies: IFeatureStrategy[] = change.payload
.map(({ id }) => { .map(({ id }) => {
return environmentStrategies.find((s) => s.id === id); return environmentStrategies.find((s) => s.id === id);
}) })
.filter(Boolean); .filter((strategy): strategy is IFeatureStrategy => Boolean(strategy));
const data = { const data = {
strategyIds: updatedStrategies.map((strategy) => strategy!.id), strategyIds: updatedStrategies.map((strategy) => strategy.id),
}; };
return ( return (
@ -105,7 +106,7 @@ export const EnvironmentStrategyExecutionOrder = ({
</StyledChangeHeader> </StyledChangeHeader>
<StyledStrategyExecutionWrapper> <StyledStrategyExecutionWrapper>
{updatedStrategies.map((strategy, index) => ( {updatedStrategies.map((strategy, index) => (
<StyledStrategyContainer> <StyledStrategyContainer key={strategy.id}>
{`${index + 1}: `} {`${index + 1}: `}
{formatStrategyName(strategy?.name || '')} {formatStrategyName(strategy?.name || '')}
{strategy?.title && ` - ${strategy.title}`} {strategy?.title && ` - ${strategy.title}`}

View File

@ -7,6 +7,7 @@ import type {
} from '../../../changeRequest.types'; } from '../../../changeRequest.types';
import { SegmentChangeDetails } from './SegmentChangeDetails.tsx'; import { SegmentChangeDetails } from './SegmentChangeDetails.tsx';
import { ConflictWarning } from './ConflictWarning.tsx'; import { ConflictWarning } from './ConflictWarning.tsx';
import { useSegment } from 'hooks/api/getters/useSegment/useSegment.ts';
interface ISegmentChangeProps { interface ISegmentChangeProps {
segmentChange: ISegmentChange; segmentChange: ISegmentChange;
@ -20,61 +21,65 @@ export const SegmentChange: FC<ISegmentChangeProps> = ({
onNavigate, onNavigate,
actions, actions,
changeRequestState, changeRequestState,
}) => ( }) => {
<Card const { segment } = useSegment(segmentChange.payload.id);
elevation={0}
sx={(theme) => ({ return (
marginTop: theme.spacing(2), <Card
marginBottom: theme.spacing(2), elevation={0}
overflow: 'hidden',
})}
>
<Box
sx={(theme) => ({ sx={(theme) => ({
backgroundColor: theme.palette.neutral.light, marginTop: theme.spacing(2),
borderRadius: (theme) => marginBottom: theme.spacing(2),
`${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px 0 0`,
border: '1px solid',
borderColor: (theme) =>
segmentChange.conflict
? theme.palette.warning.border
: theme.palette.divider,
borderBottom: 'none',
overflow: 'hidden', overflow: 'hidden',
})} })}
> >
<ConflictWarning conflict={segmentChange.conflict} />
<Box <Box
sx={{ sx={(theme) => ({
display: 'flex', backgroundColor: theme.palette.neutral.light,
pt: 2, borderRadius: `${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px 0 0`,
pb: 2, border: '1px solid',
px: 3, borderColor: segmentChange.conflict
}} ? theme.palette.warning.border
: theme.palette.divider,
borderBottom: 'none',
overflow: 'hidden',
})}
> >
<Typography>Segment name: </Typography> <ConflictWarning conflict={segmentChange.conflict} />
<Box
<Link
component={RouterLink}
to={`/segments/edit/${segmentChange.payload.id}`}
color='primary'
underline='hover'
sx={{ sx={{
marginLeft: 1, display: 'flex',
'& :hover': { pt: 2,
textDecoration: 'underline', pb: 2,
}, px: 3,
}} }}
onClick={onNavigate}
> >
<strong>{segmentChange.payload.name}</strong> <Typography>Segment name:</Typography>
</Link>
<Link
component={RouterLink}
to={`/segments/edit/${segmentChange.payload.id}`}
color='primary'
underline='hover'
sx={{
marginLeft: 1,
'& :hover': {
textDecoration: 'underline',
},
}}
onClick={onNavigate}
>
<strong>
{segmentChange.payload.name || segment?.name}
</strong>
</Link>
</Box>
</Box> </Box>
</Box> <SegmentChangeDetails
<SegmentChangeDetails change={segmentChange}
change={segmentChange} actions={actions}
actions={actions} changeRequestState={changeRequestState}
changeRequestState={changeRequestState} />
/> </Card>
</Card> );
); };

View File

@ -9,13 +9,14 @@ import {
formatStrategyName, formatStrategyName,
GetFeatureStrategyIcon, GetFeatureStrategyIcon,
} from 'utils/strategyNames'; } from 'utils/strategyNames';
import EventDiff from 'component/events/EventDiff/EventDiff'; import EventDiff, { NewEventDiff } from 'component/events/EventDiff/EventDiff';
import omit from 'lodash.omit'; import omit from 'lodash.omit';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink'; import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
import { Typography, styled } from '@mui/material'; import { Typography, styled } from '@mui/material';
import type { IFeatureStrategy } from 'interfaces/strategy'; import type { IFeatureStrategy } from 'interfaces/strategy';
import { textTruncated } from 'themes/themeStyles'; import { textTruncated } from 'themes/themeStyles';
import { NameWithChangeInfo } from '../NameWithChangeInfo/NameWithChangeInfo.tsx'; import { NameWithChangeInfo } from '../NameWithChangeInfo/NameWithChangeInfo.tsx';
import { useUiFlag } from 'hooks/useUiFlag.ts';
const StyledCodeSection = styled('div')(({ theme }) => ({ const StyledCodeSection = styled('div')(({ theme }) => ({
overflowX: 'auto', overflowX: 'auto',
@ -47,12 +48,22 @@ export const StrategyDiff: FC<{
| IChangeRequestDeleteStrategy; | IChangeRequestDeleteStrategy;
currentStrategy?: IFeatureStrategy; currentStrategy?: IFeatureStrategy;
}> = ({ change, currentStrategy }) => { }> = ({ change, currentStrategy }) => {
const useNewDiff = useUiFlag('improvedJsonDiff');
const changeRequestStrategy = const changeRequestStrategy =
change.action === 'deleteStrategy' ? undefined : change.payload; change.action === 'deleteStrategy' ? undefined : change.payload;
const sortedCurrentStrategy = sortSegments(currentStrategy); const sortedCurrentStrategy = sortSegments(currentStrategy);
const sortedChangeRequestStrategy = sortSegments(changeRequestStrategy); const sortedChangeRequestStrategy = sortSegments(changeRequestStrategy);
if (useNewDiff) {
return (
<NewEventDiff
entry={{
preData: omit(sortedCurrentStrategy, 'sortOrder'),
data: omit(sortedChangeRequestStrategy, 'snapshot'),
}}
/>
);
}
return ( return (
<StyledCodeSection> <StyledCodeSection>
<EventDiff <EventDiff

View File

@ -1,4 +1,4 @@
import { styled } from '@mui/material'; import { styled, type TypographyProps } from '@mui/material';
import { Box, Card, Paper, Typography } from '@mui/material'; import { Box, Card, Paper, Typography } from '@mui/material';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
@ -18,12 +18,14 @@ export const StyledInnerContainer = styled(Box)(({ theme }) => ({
alignItems: 'center', alignItems: 'center',
})); }));
export const StyledHeader = styled(Typography)(({ theme }) => ({ export const StyledHeader = styled(Typography)<TypographyProps>(
display: 'flex', ({ theme }) => ({
alignItems: 'center', display: 'flex',
marginRight: theme.spacing(1), alignItems: 'center',
fontSize: theme.fontSizes.mainHeader, marginRight: theme.spacing(1),
})); fontSize: theme.fontSizes.mainHeader,
}),
);
export const StyledCard = styled(Card)(({ theme }) => ({ export const StyledCard = styled(Card)(({ theme }) => ({
padding: theme.spacing(0.75, 1.5), padding: theme.spacing(0.75, 1.5),

View File

@ -26,7 +26,7 @@ export const ChangeRequestHeader: FC<{ changeRequest: ChangeRequestType }> = ({
title={title} title={title}
setTitle={setTitle} setTitle={setTitle}
> >
<StyledHeader variant='h1' sx={{ mr: 1.5 }}> <StyledHeader variant='h1' component='h2' sx={{ mr: 1.5 }}>
{title} {title}
</StyledHeader> </StyledHeader>
</ChangeRequestTitle> </ChangeRequestTitle>

View File

@ -70,7 +70,7 @@ test('Add single archive feature change to change request', async () => {
expect(screen.getByText('Archive feature flag')).toBeInTheDocument(); expect(screen.getByText('Archive feature flag')).toBeInTheDocument();
await screen.findByText( await screen.findByText(
'Archiving features with dependencies will also remove those dependencies.', 'Archiving flags with dependencies will also remove those dependencies.',
); );
const button = await screen.findByText('Add change to draft'); const button = await screen.findByText('Add change to draft');
@ -100,7 +100,7 @@ test('Add multiple archive feature changes to change request', async () => {
await screen.findByText('Archive feature flags'); await screen.findByText('Archive feature flags');
await screen.findByText( await screen.findByText(
'Archiving features with dependencies will also remove those dependencies.', 'Archiving flags with dependencies will also remove those dependencies.',
); );
const button = await screen.findByText('Add to change request'); const button = await screen.findByText('Add to change request');
@ -163,7 +163,7 @@ test('Show error message when multiple parents of orphaned children are archived
); );
expect( expect(
screen.queryByText( screen.queryByText(
'Archiving features with dependencies will also remove those dependencies.', 'Archiving flags with dependencies will also remove those dependencies.',
), ),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
@ -189,7 +189,7 @@ test('Show error message when 1 parent of orphaned children is archived', async
); );
expect( expect(
screen.queryByText( screen.queryByText(
'Archiving features with dependencies will also remove those dependencies.', 'Archiving flags with dependencies will also remove those dependencies.',
), ),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });

View File

@ -26,7 +26,7 @@ interface IFeatureArchiveDialogProps {
const RemovedDependenciesAlert = () => { const RemovedDependenciesAlert = () => {
return ( return (
<Alert severity='warning' sx={{ m: (theme) => theme.spacing(2, 0) }}> <Alert severity='warning' sx={{ m: (theme) => theme.spacing(2, 0) }}>
Archiving features with dependencies will also remove those Archiving flags with dependencies will also remove those
dependencies. dependencies.
</Alert> </Alert>
); );

View File

@ -4,6 +4,7 @@ import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashCon
import { ConstraintsList } from 'component/common/ConstraintsList/ConstraintsList'; import { ConstraintsList } from 'component/common/ConstraintsList/ConstraintsList';
import { ConstraintAccordionView } from 'component/common/NewConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView'; import { ConstraintAccordionView } from 'component/common/NewConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView';
import { constraintId } from 'constants/constraintId'; import { constraintId } from 'constants/constraintId';
import { objectId } from 'utils/objectId';
export interface IViewableConstraintsListProps { export interface IViewableConstraintsListProps {
constraints: IConstraint[]; constraints: IConstraint[];
@ -29,7 +30,7 @@ export const ViewableConstraintsList = ({
<ConstraintsList> <ConstraintsList>
{constraints.map((constraint) => ( {constraints.map((constraint) => (
<ConstraintAccordionView <ConstraintAccordionView
key={constraint[constraintId]} key={constraint[constraintId] || objectId(constraint)}
constraint={constraint} constraint={constraint}
/> />
))} ))}

View File

@ -78,7 +78,7 @@ const ButtonIcon = styled('span')(({ theme }) => ({
marginInlineEnd: theme.spacing(0.5), marginInlineEnd: theme.spacing(0.5),
})); }));
const NewEventDiff: FC<IEventDiffProps> = ({ entry, excludeKeys }) => { export const NewEventDiff: FC<IEventDiffProps> = ({ entry, excludeKeys }) => {
const changeType = entry.preData && entry.data ? 'edit' : 'replacement'; const changeType = entry.preData && entry.data ? 'edit' : 'replacement';
const showExpandButton = changeType === 'edit'; const showExpandButton = changeType === 'edit';
const [full, setFull] = useState(false); const [full, setFull] = useState(false);
@ -220,4 +220,7 @@ const EventDiff: FC<IEventDiffProps> = (props) => {
return <OldEventDiff {...props} />; return <OldEventDiff {...props} />;
}; };
/**
* @deprecated remove the default export with flag improvedJsonDiff. Switch imports in files that use this to the named import instead.
*/
export default EventDiff; export default EventDiff;