1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-31 00:16:47 +01:00

feat: segments in pending CR screen (#4420)

This commit is contained in:
Jaanus Sellin 2023-08-04 15:34:03 +03:00 committed by GitHub
parent e20e7df10f
commit 7a32eacecb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 329 additions and 28 deletions

View File

@ -29,6 +29,7 @@ const pendingChangeRequest = (featureName: string) =>
'https://gravatar.com/avatar/21232f297a57a5a743894a0e4a801fc3?size=42&default=retro',
},
createdAt: '2022-12-02T09:19:12.242Z',
segments: [],
features: [
{
name: featureName,

View File

@ -19,6 +19,7 @@ const changeRequestWithDefaultChange = (
username: 'author',
imageUrl: '',
},
segments: [],
features: [
{
name: 'Feature Toggle Name',

View File

@ -2,8 +2,10 @@ import React, { VFC } from 'react';
import { Box, Typography } from '@mui/material';
import type { IChangeRequest } from '../changeRequest.types';
import { FeatureToggleChanges } from './Changes/FeatureToggleChanges';
import { Change } from './Changes/Change/Change';
import { FeatureChange } from './Changes/Change/FeatureChange';
import { ChangeActions } from './Changes/Change/ChangeActions';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { SegmentChange } from './Changes/Change/SegmentChange';
interface IChangeRequestProps {
changeRequest: IChangeRequest;
@ -18,6 +20,30 @@ export const ChangeRequest: VFC<IChangeRequestProps> = ({
}) => {
return (
<Box>
<ConditionallyRender
condition={changeRequest.segments.length > 0}
show={
<Typography variant="body2" color="text.secondary">
You request changes for these segments:
</Typography>
}
/>
{changeRequest.segments?.map(segment => (
<SegmentChange
key={segment.payload.id}
segmentChange={segment}
onNavigate={onNavigate}
/>
))}
<ConditionallyRender
condition={changeRequest.features.length > 0}
show={
<Typography variant="body2" color="text.secondary">
You request changes for these feature toggles:
</Typography>
}
/>
{changeRequest.features?.map(feature => (
<FeatureToggleChanges
key={feature.name}
@ -27,7 +53,7 @@ export const ChangeRequest: VFC<IChangeRequestProps> = ({
conflict={feature.conflict}
>
{feature.changes.map((change, index) => (
<Change
<FeatureChange
key={index}
discard={
<ChangeActions
@ -44,7 +70,7 @@ export const ChangeRequest: VFC<IChangeRequestProps> = ({
/>
))}
{feature.defaultChange ? (
<Change
<FeatureChange
discard={
<Typography
variant="body2"

View File

@ -1,6 +1,6 @@
import React, { FC, useState } from 'react';
import {
IChange,
IFeatureChange,
IChangeRequest,
IChangeRequestAddStrategy,
IChangeRequestUpdateStrategy,
@ -26,7 +26,10 @@ import {
import { Delete, Edit, MoreVert } from '@mui/icons-material';
import { EditChange } from './EditChange';
const useShowActions = (changeRequest: IChangeRequest, change: IChange) => {
const useShowActions = (
changeRequest: IChangeRequest,
change: IFeatureChange
) => {
const { isChangeRequestConfigured } = useChangeRequestsEnabled(
changeRequest.project
);
@ -57,7 +60,7 @@ const StyledPopover = styled(Popover)(({ theme }) => ({
export const ChangeActions: FC<{
changeRequest: IChangeRequest;
feature: string;
change: IChange;
change: IFeatureChange;
onRefetch?: () => void;
}> = ({ changeRequest, feature, change, onRefetch }) => {
const { showDiscard, showEdit } = useShowActions(changeRequest, change);

View File

@ -1,6 +1,6 @@
import { FC, ReactNode } from 'react';
import {
IChange,
IFeatureChange,
IChangeRequest,
IChangeRequestFeature,
} from '../../../changeRequest.types';
@ -54,11 +54,11 @@ const StyledAlert = styled(Alert)(({ theme }) => ({
},
}));
export const Change: FC<{
export const FeatureChange: FC<{
discard: ReactNode;
index: number;
changeRequest: IChangeRequest;
change: IChange;
change: IFeatureChange;
feature: IChangeRequestFeature;
}> = ({ index, change, feature, changeRequest, discard }) => {
const lastIndex = feature.defaultChange
@ -82,6 +82,7 @@ export const Change: FC<{
</StyledAlert>
}
/>
<Box sx={theme => ({ padding: theme.spacing(3) })}>
{change.action === 'updateEnabled' && (
<ToggleStatusChange
@ -89,6 +90,7 @@ export const Change: FC<{
discard={discard}
/>
)}
{change.action === 'addStrategy' ||
change.action === 'deleteStrategy' ||
change.action === 'updateStrategy' ? (

View File

@ -0,0 +1,64 @@
import { FC } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { Box, Card, Typography, Link } from '@mui/material';
import { ISegmentChange } from '../../../changeRequest.types';
import { SegmentChangeDetails } from './SegmentChangeDetails';
interface ISegmentChangeProps {
segmentChange: ISegmentChange;
onNavigate?: () => void;
}
export const SegmentChange: FC<ISegmentChangeProps> = ({
segmentChange,
onNavigate,
}) => (
<Card
elevation={0}
sx={theme => ({
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
overflow: 'hidden',
})}
>
<Box
sx={theme => ({
backgroundColor: theme.palette.neutral.light,
borderRadius: theme =>
`${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px 0 0`,
border: '1px solid',
borderColor: theme => theme.palette.divider,
borderBottom: 'none',
overflow: 'hidden',
})}
>
<Box
sx={{
display: 'flex',
pt: 2,
pb: 2,
px: 3,
}}
>
<Typography>Segment name: </Typography>
<Link
component={RouterLink}
to={`/segments/${segmentChange.payload.id}`}
color="primary"
underline="hover"
sx={{
marginLeft: 1,
'& :hover': {
textDecoration: 'underline',
},
}}
onClick={onNavigate}
>
<strong>{segmentChange.payload.name}</strong>
</Link>
</Box>
</Box>
<SegmentChangeDetails change={segmentChange} />
</Card>
);

View File

@ -0,0 +1,89 @@
import { VFC, FC, ReactNode } from 'react';
import { Box, styled, Typography } from '@mui/material';
import {
IChangeRequestDeleteSegment,
IChangeRequestUpdateSegment,
} from 'component/changeRequest/changeRequest.types';
import { useSegment } from 'hooks/api/getters/useSegment/useSegment';
import { SegmentDiff, SegmentTooltipLink } from '../../SegmentTooltipLink';
const ChangeItemCreateEditWrapper = styled(Box)(({ theme }) => ({
display: 'grid',
gridTemplateColumns: 'auto 40px',
gap: theme.spacing(1),
alignItems: 'center',
width: '100%',
}));
export const ChangeItemWrapper = styled(Box)({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
});
const ChangeItemInfo: FC = styled(Box)(({ theme }) => ({
display: 'grid',
gridTemplateColumns: '150px auto',
gridAutoFlow: 'column',
alignItems: 'center',
flexGrow: 1,
gap: theme.spacing(1),
}));
const SegmentContainer = styled(Box)(({ theme }) => ({
borderLeft: '1px solid',
borderRight: '1px solid',
borderTop: '1px solid',
borderBottom: '1px solid',
borderColor: theme.palette.divider,
borderTopColor: theme.palette.divider,
padding: theme.spacing(3),
}));
export const SegmentChangeDetails: VFC<{
discard?: ReactNode;
change: IChangeRequestUpdateSegment | IChangeRequestDeleteSegment;
}> = ({ discard, change }) => {
const { segment: currentSegment } = useSegment(change.payload.id);
return (
<SegmentContainer>
{change.action === 'deleteSegment' && (
<ChangeItemWrapper>
<ChangeItemInfo>
<Typography
sx={theme => ({
color: theme.palette.error.main,
})}
>
- Deleting segment:
</Typography>
<SegmentTooltipLink change={change}>
<SegmentDiff
change={change}
currentSegment={currentSegment}
/>
</SegmentTooltipLink>
</ChangeItemInfo>
<div>{discard}</div>
</ChangeItemWrapper>
)}
{change.action === 'updateSegment' && (
<>
<ChangeItemCreateEditWrapper>
<ChangeItemInfo>
<Typography>Editing segment:</Typography>
<SegmentTooltipLink change={change}>
<SegmentDiff
change={change}
currentSegment={currentSegment}
/>
</SegmentTooltipLink>
</ChangeItemInfo>
<div>{discard}</div>
</ChangeItemCreateEditWrapper>
</>
)}
</SegmentContainer>
);
};

View File

@ -0,0 +1,79 @@
import {
IChangeRequestDeleteSegment,
IChangeRequestUpdateSegment,
} from 'component/changeRequest/changeRequest.types';
import { FC } from 'react';
import { formatStrategyName } from 'utils/strategyNames';
import EventDiff from 'component/events/EventDiff/EventDiff';
import omit from 'lodash.omit';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
import { Typography, styled } from '@mui/material';
import { textTruncated } from 'themes/themeStyles';
import { ISegment } from 'interfaces/segment';
const StyledCodeSection = styled('div')(({ theme }) => ({
overflowX: 'auto',
'& code': {
wordWrap: 'break-word',
whiteSpace: 'pre-wrap',
fontFamily: 'monospace',
lineHeight: 1.5,
fontSize: theme.fontSizes.smallBody,
},
}));
export const SegmentDiff: FC<{
change: IChangeRequestUpdateSegment | IChangeRequestDeleteSegment;
currentSegment?: ISegment;
}> = ({ change, currentSegment }) => {
const changeRequestStrategy =
change.action === 'deleteSegment' ? undefined : change.payload;
return (
<StyledCodeSection>
<EventDiff
entry={{
preData: omit(currentSegment, 'sortOrder'),
data: changeRequestStrategy,
}}
/>
</StyledCodeSection>
);
};
interface IStrategyTooltipLinkProps {
change: IChangeRequestUpdateSegment | IChangeRequestDeleteSegment;
}
const StyledContainer: FC = styled('div')(({ theme }) => ({
display: 'grid',
gridAutoFlow: 'column',
gridTemplateColumns: 'auto 1fr',
gap: theme.spacing(1),
alignItems: 'center',
}));
const Truncated = styled('div')(() => ({
...textTruncated,
maxWidth: 500,
}));
export const SegmentTooltipLink: FC<IStrategyTooltipLinkProps> = ({
change,
children,
}) => (
<StyledContainer>
<Truncated>
<TooltipLink
tooltip={children}
tooltipProps={{
maxWidth: 500,
maxHeight: 600,
}}
>
<Typography component="span">
{formatStrategyName(change.payload.name)}
</Typography>
</TooltipLink>
</Truncated>
</StyledContainer>
);

View File

@ -112,11 +112,7 @@ export const EnvironmentChangeRequest: FC<{
</ChangeRequestTitle>
</ChangeRequestHeader>
<ChangeRequestContent>
<Typography variant="body2" color="text.secondary">
You request changes for these feature toggles:
</Typography>
{children}
<ConditionallyRender
condition={environmentChangeRequest?.state === 'Draft'}
show={

View File

@ -1,11 +1,11 @@
import React, { FC, useState } from 'react';
import { screen } from '@testing-library/react';
import { ChangeRequestTitle } from './ChangeRequestTitle';
import { ChangeRequestState } from '../../changeRequest.types';
import { ChangeRequestState } from 'component/changeRequest/changeRequest.types';
import userEvent from '@testing-library/user-event';
import { testServerRoute, testServerSetup } from 'utils/testServer';
import { render } from 'utils/testRenderer';
import { UIProviderContainer } from '../../../providers/UIProvider/UIProviderContainer';
import { UIProviderContainer } from 'component/providers/UIProvider/UIProviderContainer';
const changeRequest = {
id: 3,
@ -19,6 +19,7 @@ const changeRequest = {
features: [],
approvals: [],
comments: [],
segments: [],
};
const server = testServerSetup();

View File

@ -13,6 +13,7 @@ export interface IChangeRequest {
createdBy: Pick<IUser, 'id' | 'username' | 'imageUrl'>;
createdAt: Date;
features: IChangeRequestFeature[];
segments: ISegmentChange[];
approvals: IChangeRequestApproval[];
comments: IChangeRequestComment[];
conflict?: string;
@ -28,7 +29,14 @@ export interface IChangeRequestEnvironmentConfig {
export interface IChangeRequestFeature {
name: string;
conflict?: string;
changes: IChange[];
changes: IFeatureChange[];
defaultChange?: IChangeRequestAddStrategy | IChangeRequestEnabled;
}
export interface IChangeRequestSegment {
name: string;
conflict?: string;
changes: IFeatureChange[];
defaultChange?: IChangeRequestAddStrategy | IChangeRequestEnabled;
}
@ -44,7 +52,7 @@ export interface IChangeRequestComment {
id: string;
}
export interface IChangeRequestBase {
export interface IChangeRequestChangeBase {
id: number;
action: ChangeRequestAction;
payload: ChangeRequestPayload;
@ -66,39 +74,63 @@ type ChangeRequestPayload =
| ChangeRequestEditStrategy
| ChangeRequestDeleteStrategy
| ChangeRequestVariantPatch
| IChangeRequestUpdateSegment
| IChangeRequestDeleteSegment
| SetStrategySortOrderSchema;
export interface IChangeRequestAddStrategy extends IChangeRequestBase {
export interface IChangeRequestAddStrategy extends IChangeRequestChangeBase {
action: 'addStrategy';
payload: ChangeRequestAddStrategy;
}
export interface IChangeRequestDeleteStrategy extends IChangeRequestBase {
export interface IChangeRequestDeleteStrategy extends IChangeRequestChangeBase {
action: 'deleteStrategy';
payload: ChangeRequestDeleteStrategy;
}
export interface IChangeRequestUpdateStrategy extends IChangeRequestBase {
export interface IChangeRequestUpdateStrategy extends IChangeRequestChangeBase {
action: 'updateStrategy';
payload: ChangeRequestEditStrategy;
}
export interface IChangeRequestEnabled extends IChangeRequestBase {
export interface IChangeRequestEnabled extends IChangeRequestChangeBase {
action: 'updateEnabled';
payload: ChangeRequestEnabled;
}
export interface IChangeRequestPatchVariant extends IChangeRequestBase {
export interface IChangeRequestPatchVariant extends IChangeRequestChangeBase {
action: 'patchVariant';
payload: ChangeRequestVariantPatch;
}
export interface IChangeRequestReorderStrategy extends IChangeRequestBase {
export interface IChangeRequestReorderStrategy
extends IChangeRequestChangeBase {
action: 'reorderStrategy';
payload: SetStrategySortOrderSchema;
}
export type IChange =
export interface IChangeRequestUpdateSegment {
action: 'updateSegment';
payload: {
id: number;
name: string;
description?: string;
project?: string;
constraints: IFeatureStrategy['constraints'];
};
}
export interface IChangeRequestDeleteSegment {
action: 'deleteSegment';
payload: {
id: number;
name: string;
};
}
export type IChange = IFeatureChange | ISegmentChange;
export type IFeatureChange =
| IChangeRequestAddStrategy
| IChangeRequestDeleteStrategy
| IChangeRequestUpdateStrategy
@ -106,6 +138,10 @@ export type IChange =
| IChangeRequestPatchVariant
| IChangeRequestReorderStrategy;
export type ISegmentChange =
| IChangeRequestUpdateSegment
| IChangeRequestDeleteSegment;
type ChangeRequestVariantPatch = {
variants: IFeatureVariant[];
};
@ -132,4 +168,6 @@ export type ChangeRequestAction =
| 'updateStrategy'
| 'deleteStrategy'
| 'patchVariant'
| 'reorderStrategy';
| 'reorderStrategy'
| 'updateSegment'
| 'deleteSegment';

View File

@ -1,4 +1,5 @@
import { IChangeRequest } from './changeRequest.types';
export const changesCount = (changeRequest: IChangeRequest) =>
changeRequest.features.flatMap(feature => feature.changes).length;
changeRequest.features.flatMap(feature => feature.changes).length +
changeRequest.segments.length;

View File

@ -7,7 +7,7 @@ import { IFeatureStrategy } from 'interfaces/strategy';
import { StrategyItem } from './StrategyItem/StrategyItem';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { Badge } from 'component/common/Badge/Badge';
import { IChange } from 'component/changeRequest/changeRequest.types';
import { IFeatureChange } from 'component/changeRequest/changeRequest.types';
import { useStrategyChangeFromRequest } from './StrategyItem/useStrategyChangeFromRequest';
interface IStrategyDraggableItemProps {
@ -74,7 +74,7 @@ export const StrategyDraggableItem = ({
const ChangeRequestStatusBadge = ({
change,
}: {
change: IChange | undefined;
change: IFeatureChange | undefined;
}) => {
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));