From c7fe4a5a015c07c6ae6c90869c5da97d08433c3e Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Thu, 1 Dec 2022 14:44:33 +0100 Subject: [PATCH] visual changes representation (#2583) --- .../ChangeRequest/ChangeRequest.tsx | 314 ++++++++---------- .../CodeSnippetPopover/CodeSnippetPopover.tsx | 120 +++++++ .../component/events/EventCard/EventCard.tsx | 2 +- .../StrategyExecution/StrategyExecution.tsx | 17 +- .../FeatureOverviewSegment.tsx | 8 +- 5 files changed, 267 insertions(+), 194 deletions(-) create mode 100644 frontend/src/component/changeRequest/ChangeRequest/CodeSnippetPopover/CodeSnippetPopover.tsx diff --git a/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.tsx b/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.tsx index 100dd329e84..a571beb130f 100644 --- a/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.tsx +++ b/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.tsx @@ -1,5 +1,5 @@ import React, { FC, VFC } from 'react'; -import { Alert, Box, Popover, styled, Typography } from '@mui/material'; +import { Alert, Box, styled } from '@mui/material'; import { ChangeRequestFeatureToggleChange } from '../ChangeRequestOverview/ChangeRequestFeatureToggleChange/ChangeRequestFeatureToggleChange'; import { objectId } from 'utils/objectId'; import { ToggleStatusChange } from '../ChangeRequestOverview/ChangeRequestFeatureToggleChange/ToggleStatusChange'; @@ -7,27 +7,24 @@ import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useCh import { formatUnknownError } from 'utils/formatUnknownError'; import useToast from 'hooks/useToast'; import type { + IChange, IChangeRequest, - IChangeRequestDeleteStrategy, - IChangeRequestUpdateStrategy, + IChangeRequestFeature, } from '../changeRequest.types'; +import { hasNameField } from '../changeRequest.types'; import { Discard, StrategyAddedChange, StrategyDeletedChange, StrategyEditedChange, } from '../ChangeRequestOverview/ChangeRequestFeatureToggleChange/StrategyChange'; -import { - formatStrategyName, - GetFeatureStrategyIcon, -} from 'utils/strategyNames'; -import { - hasNameField, - IChangeRequestAddStrategy, -} from '../changeRequest.types'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; -import EventDiff from '../../events/EventDiff/EventDiff'; +import { StrategyExecution } from '../../feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution'; +import { + CodeSnippetPopover, + PopoverDiff, +} from './CodeSnippetPopover/CodeSnippetPopover'; interface IChangeRequestProps { changeRequest: IChangeRequest; @@ -77,61 +74,121 @@ const StyledAlert = styled(Alert)(({ theme }) => ({ }, })); -const CodeSnippetPopover: FC<{ - change: - | IChangeRequestAddStrategy - | IChangeRequestUpdateStrategy - | IChangeRequestDeleteStrategy; -}> = ({ change }) => { - const [anchorEl, setAnchorEl] = React.useState(null); - - const handlePopoverOpen = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const handlePopoverClose = () => { - setAnchorEl(null); - }; +const Change: FC<{ + onDiscard: () => Promise; + index: number; + changeRequest: IChangeRequest; + change: IChange; + feature: IChangeRequestFeature; +}> = ({ index, change, feature, changeRequest, onDiscard }) => { + const { isChangeRequestConfigured } = useChangeRequestsEnabled( + changeRequest.project + ); + const allowChangeRequestActions = isChangeRequestConfigured( + changeRequest.environment + ); - const open = Boolean(anchorEl); + const showDiscard = + allowChangeRequestActions && + !['Cancelled', 'Applied'].includes(changeRequest.state) && + changeRequest.features.flatMap(feature => feature.changes).length > 1; return ( - <> - - - - {formatStrategyName(change.payload.name)} - - - - + + Conflict! This change can’t be applied.{' '} + {change.conflict}. + + } + /> + + {change.action === 'updateEnabled' && ( + } + /> + } /> - - - + )} + {change.action === 'addStrategy' && ( + <> + } + /> + } + > + + + + + + + )} + {change.action === 'deleteStrategy' && ( + } + /> + } + > + {hasNameField(change.payload) && ( + + + + )} + + )} + {change.action === 'updateStrategy' && ( + <> + } + /> + } + > + + + + + + + )} + + ); }; @@ -154,132 +211,25 @@ export const ChangeRequest: VFC = ({ setToastApiError(formatUnknownError(error)); } }; - const { isChangeRequestConfigured } = useChangeRequestsEnabled( - changeRequest.project - ); - const allowChangeRequestActions = isChangeRequestConfigured( - changeRequest.environment - ); - - const showDiscard = - allowChangeRequestActions && - !['Cancelled', 'Applied'].includes(changeRequest.state) && - changeRequest.features.flatMap(feature => feature.changes).length > 1; return ( - {changeRequest.features?.map(featureToggleChange => ( + {changeRequest.features?.map(feature => ( - {featureToggleChange.changes.map((change, index) => ( - - - Conflict! This change - can’t be applied. {change.conflict}. - - } - /> - - {change.action === 'updateEnabled' && ( - - } - /> - } - /> - )} - {change.action === 'addStrategy' && ( - - } - /> - } - > - - - )} - {change.action === 'deleteStrategy' && ( - - } - /> - } - > - {hasNameField(change.payload) && ( - - )} - - )} - {change.action === 'updateStrategy' && ( - - } - /> - } - > - - - )} - - + {feature.changes.map((change, index) => ( + ))} ))} diff --git a/frontend/src/component/changeRequest/ChangeRequest/CodeSnippetPopover/CodeSnippetPopover.tsx b/frontend/src/component/changeRequest/ChangeRequest/CodeSnippetPopover/CodeSnippetPopover.tsx new file mode 100644 index 00000000000..ce6548a186c --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequest/CodeSnippetPopover/CodeSnippetPopover.tsx @@ -0,0 +1,120 @@ +import { + IChangeRequestAddStrategy, + IChangeRequestDeleteStrategy, + IChangeRequestUpdateStrategy, +} from '../../changeRequest.types'; +import React, { FC } from 'react'; +import { + formatStrategyName, + GetFeatureStrategyIcon, +} from '../../../../utils/strategyNames'; +import { Popover, Typography } from '@mui/material'; +import { useFeature } from '../../../../hooks/api/getters/useFeature/useFeature'; +import { StyledCodeSection } from '../../../events/EventCard/EventCard'; +import EventDiff from '../../../events/EventDiff/EventDiff'; + +const useCurrentStrategy = ( + change: + | IChangeRequestAddStrategy + | IChangeRequestUpdateStrategy + | IChangeRequestDeleteStrategy, + project: string, + feature: string, + environmentName: string +) => { + const currentFeature = useFeature(project, feature); + const currentStrategy = currentFeature.feature?.environments + .find(environment => environment.name === environmentName) + ?.strategies.find( + strategy => + 'id' in change.payload && strategy.id === change.payload.id + ); + return currentStrategy; +}; + +export const PopoverDiff: FC<{ + change: + | IChangeRequestAddStrategy + | IChangeRequestUpdateStrategy + | IChangeRequestDeleteStrategy; + project: string; + feature: string; + environmentName: string; +}> = ({ change, project, feature, environmentName }) => { + const currentStrategy = useCurrentStrategy( + change, + project, + feature, + environmentName + ); + const changeRequestStrategy = + change.action === 'deleteStrategy' ? undefined : change.payload; + + return ( + + + + ); +}; +interface ICodeSnippetPopoverProps { + change: + | IChangeRequestAddStrategy + | IChangeRequestUpdateStrategy + | IChangeRequestDeleteStrategy; +} + +// based on: https://mui.com/material-ui/react-popover/#mouse-over-interaction +export const CodeSnippetPopover: FC = ({ + change, + children, +}) => { + const [anchorEl, setAnchorEl] = React.useState(null); + + const handlePopoverOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handlePopoverClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + + return ( + <> + + + + {formatStrategyName(change.payload.name)} + + + {children} + + + ); +}; diff --git a/frontend/src/component/events/EventCard/EventCard.tsx b/frontend/src/component/events/EventCard/EventCard.tsx index d5d2b0233ed..91235d07447 100644 --- a/frontend/src/component/events/EventCard/EventCard.tsx +++ b/frontend/src/component/events/EventCard/EventCard.tsx @@ -40,7 +40,7 @@ const StyledContainerListItem = styled('li')(({ theme }) => ({ }, })); -const StyledCodeSection = styled('div')(({ theme }) => ({ +export const StyledCodeSection = styled('div')(({ theme }) => ({ backgroundColor: 'white', overflowX: 'auto', padding: theme.spacing(2), diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx index f47ab515e5a..62de42ff6fb 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx @@ -1,6 +1,6 @@ import { Fragment, useMemo, VFC } from 'react'; import { Box, Chip } from '@mui/material'; -import { IFeatureStrategy } from 'interfaces/strategy'; +import { IFeatureStrategy, IFeatureStrategyPayload } from 'interfaces/strategy'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle'; import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; @@ -19,7 +19,7 @@ import { import StringTruncator from 'component/common/StringTruncator/StringTruncator'; interface IStrategyExecutionProps { - strategy: IFeatureStrategy; + strategy: IFeatureStrategyPayload; } const NoItems: VFC = () => ( @@ -35,7 +35,10 @@ export const StrategyExecution: VFC = ({ const { classes: styles } = useStyles(); const { strategies } = useStrategies(); const { uiConfig } = useUiConfig(); - const { segments } = useSegments(strategy.id); + const { segments } = useSegments(); + const strategySegments = segments?.filter(segment => { + return strategy.segments?.includes(segment.id); + }); const definition = strategies.find(strategyDefinition => { return strategyDefinition.name === strategy.name; @@ -243,9 +246,11 @@ export const StrategyExecution: VFC = ({ } const listItems = [ - Boolean(uiConfig.flags.SE) && segments && segments.length > 0 && ( - - ), + Boolean(uiConfig.flags.SE) && + strategySegments && + strategySegments.length > 0 && ( + + ), constraints.length > 0 && ( { - const { segments } = useSegments(strategyId); - if (!segments || segments.length === 0) { return null; }