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:
parent
e20e7df10f
commit
7a32eacecb
@ -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,
|
||||
|
@ -19,6 +19,7 @@ const changeRequestWithDefaultChange = (
|
||||
username: 'author',
|
||||
imageUrl: '',
|
||||
},
|
||||
segments: [],
|
||||
features: [
|
||||
{
|
||||
name: 'Feature Toggle Name',
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
|
@ -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' ? (
|
@ -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>
|
||||
);
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
@ -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={
|
||||
|
@ -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();
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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'));
|
||||
|
Loading…
Reference in New Issue
Block a user