diff --git a/frontend/src/component/changeRequest/ChangeRequest.test.tsx b/frontend/src/component/changeRequest/ChangeRequest.test.tsx index 19e7048253..e90fcdf9ac 100644 --- a/frontend/src/component/changeRequest/ChangeRequest.test.tsx +++ b/frontend/src/component/changeRequest/ChangeRequest.test.tsx @@ -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, diff --git a/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.test.tsx b/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.test.tsx index a1488de42f..a46c70e2f0 100644 --- a/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.test.tsx +++ b/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.test.tsx @@ -19,6 +19,7 @@ const changeRequestWithDefaultChange = ( username: 'author', imageUrl: '', }, + segments: [], features: [ { name: 'Feature Toggle Name', diff --git a/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.tsx b/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.tsx index 340345ca7b..fec8f21e1e 100644 --- a/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.tsx +++ b/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.tsx @@ -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 = ({ }) => { return ( + 0} + show={ + + You request changes for these segments: + + } + /> + + {changeRequest.segments?.map(segment => ( + + ))} + 0} + show={ + + You request changes for these feature toggles: + + } + /> {changeRequest.features?.map(feature => ( = ({ conflict={feature.conflict} > {feature.changes.map((change, index) => ( - = ({ /> ))} {feature.defaultChange ? ( - { +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); diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/Change.tsx b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/FeatureChange.tsx similarity index 98% rename from frontend/src/component/changeRequest/ChangeRequest/Changes/Change/Change.tsx rename to frontend/src/component/changeRequest/ChangeRequest/Changes/Change/FeatureChange.tsx index 3b9f8b4365..4d0a90b433 100644 --- a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/Change.tsx +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/FeatureChange.tsx @@ -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<{ } /> + ({ padding: theme.spacing(3) })}> {change.action === 'updateEnabled' && ( )} + {change.action === 'addStrategy' || change.action === 'deleteStrategy' || change.action === 'updateStrategy' ? ( diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/SegmentChange.tsx b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/SegmentChange.tsx new file mode 100644 index 0000000000..eaec26055a --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/SegmentChange.tsx @@ -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 = ({ + segmentChange, + onNavigate, +}) => ( + ({ + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + overflow: 'hidden', + })} + > + ({ + 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', + })} + > + + Segment name: + + + {segmentChange.payload.name} + + + + + +); diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/SegmentChangeDetails.tsx b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/SegmentChangeDetails.tsx new file mode 100644 index 0000000000..41dbc6fde5 --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/SegmentChangeDetails.tsx @@ -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 ( + + {change.action === 'deleteSegment' && ( + + + ({ + color: theme.palette.error.main, + })} + > + - Deleting segment: + + + + + +
{discard}
+
+ )} + {change.action === 'updateSegment' && ( + <> + + + Editing segment: + + + + +
{discard}
+
+ + )} +
+ ); +}; diff --git a/frontend/src/component/changeRequest/ChangeRequest/SegmentTooltipLink.tsx b/frontend/src/component/changeRequest/ChangeRequest/SegmentTooltipLink.tsx new file mode 100644 index 0000000000..f3997706a3 --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequest/SegmentTooltipLink.tsx @@ -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 ( + + + + ); +}; +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 = ({ + change, + children, +}) => ( + + + + + {formatStrategyName(change.payload.name)} + + + + +); diff --git a/frontend/src/component/changeRequest/ChangeRequestSidebar/EnvironmentChangeRequest/EnvironmentChangeRequest.tsx b/frontend/src/component/changeRequest/ChangeRequestSidebar/EnvironmentChangeRequest/EnvironmentChangeRequest.tsx index c13d3e50b5..c5831b0a71 100644 --- a/frontend/src/component/changeRequest/ChangeRequestSidebar/EnvironmentChangeRequest/EnvironmentChangeRequest.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestSidebar/EnvironmentChangeRequest/EnvironmentChangeRequest.tsx @@ -112,11 +112,7 @@ export const EnvironmentChangeRequest: FC<{ - - You request changes for these feature toggles: - {children} - ; 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'; diff --git a/frontend/src/component/changeRequest/changesCount.ts b/frontend/src/component/changeRequest/changesCount.ts index 92c9504fc8..9c66dc9707 100644 --- a/frontend/src/component/changeRequest/changesCount.ts +++ b/frontend/src/component/changeRequest/changesCount.ts @@ -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; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx index c98e7dedc0..fb66319f12 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx @@ -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'));