1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00

visual changes representation (#2583)

This commit is contained in:
Mateusz Kwasniewski 2022-12-01 14:44:33 +01:00 committed by GitHub
parent e728ecba69
commit c7fe4a5a01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 267 additions and 194 deletions

View File

@ -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<HTMLElement | null>(null);
const Change: FC<{
onDiscard: () => Promise<void>;
index: number;
changeRequest: IChangeRequest;
change: IChange;
feature: IChangeRequestFeature;
}> = ({ index, change, feature, changeRequest, onDiscard }) => {
const { isChangeRequestConfigured } = useChangeRequestsEnabled(
changeRequest.project
);
const allowChangeRequestActions = isChangeRequestConfigured(
changeRequest.environment
);
const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handlePopoverClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const showDiscard =
allowChangeRequestActions &&
!['Cancelled', 'Applied'].includes(changeRequest.state) &&
changeRequest.features.flatMap(feature => feature.changes).length > 1;
return (
<>
<GetFeatureStrategyIcon strategyName={change.payload.name} />
<Typography
onMouseEnter={handlePopoverOpen}
onMouseLeave={handlePopoverClose}
>
{formatStrategyName(change.payload.name)}
</Typography>
<Popover
id={String(change.id)}
sx={{
pointerEvents: 'none',
}}
open={open}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
onClose={handlePopoverClose}
disableRestoreFocus
>
<Box sx={{ paddingLeft: 3, paddingRight: 3 }}>
<EventDiff
entry={{
data: change.payload,
}}
<StyledSingleChangeBox
key={objectId(change)}
$hasConflict={Boolean(change.conflict)}
$isInConflictFeature={Boolean(feature.conflict)}
$isAfterWarning={Boolean(feature.changes[index - 1]?.conflict)}
$isLast={index + 1 === feature.changes.length}
>
<ConditionallyRender
condition={Boolean(change.conflict) && !feature.conflict}
show={
<StyledAlert severity="warning">
<strong>Conflict!</strong> This change cant be applied.{' '}
{change.conflict}.
</StyledAlert>
}
/>
<Box sx={{ p: 2 }}>
{change.action === 'updateEnabled' && (
<ToggleStatusChange
enabled={change.payload.enabled}
discard={
<ConditionallyRender
condition={showDiscard}
show={<Discard onDiscard={onDiscard} />}
/>
}
/>
</Box>
</Popover>
</>
)}
{change.action === 'addStrategy' && (
<>
<StrategyAddedChange
discard={
<ConditionallyRender
condition={showDiscard}
show={<Discard onDiscard={onDiscard} />}
/>
}
>
<CodeSnippetPopover change={change}>
<PopoverDiff
change={change}
feature={feature.name}
environmentName={changeRequest.environment}
project={changeRequest.project}
/>
</CodeSnippetPopover>
</StrategyAddedChange>
<StrategyExecution strategy={change.payload} />
</>
)}
{change.action === 'deleteStrategy' && (
<StrategyDeletedChange
discard={
<ConditionallyRender
condition={showDiscard}
show={<Discard onDiscard={onDiscard} />}
/>
}
>
{hasNameField(change.payload) && (
<CodeSnippetPopover change={change}>
<PopoverDiff
change={change}
feature={feature.name}
environmentName={changeRequest.environment}
project={changeRequest.project}
/>
</CodeSnippetPopover>
)}
</StrategyDeletedChange>
)}
{change.action === 'updateStrategy' && (
<>
<StrategyEditedChange
discard={
<ConditionallyRender
condition={showDiscard}
show={<Discard onDiscard={onDiscard} />}
/>
}
>
<CodeSnippetPopover change={change}>
<PopoverDiff
change={change}
feature={feature.name}
environmentName={changeRequest.environment}
project={changeRequest.project}
/>
</CodeSnippetPopover>
</StrategyEditedChange>
<StrategyExecution strategy={change.payload} />
</>
)}
</Box>
</StyledSingleChangeBox>
);
};
@ -154,132 +211,25 @@ export const ChangeRequest: VFC<IChangeRequestProps> = ({
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 (
<Box>
{changeRequest.features?.map(featureToggleChange => (
{changeRequest.features?.map(feature => (
<ChangeRequestFeatureToggleChange
key={featureToggleChange.name}
featureName={featureToggleChange.name}
key={feature.name}
featureName={feature.name}
projectId={changeRequest.project}
onNavigate={onNavigate}
conflict={featureToggleChange.conflict}
conflict={feature.conflict}
>
{featureToggleChange.changes.map((change, index) => (
<StyledSingleChangeBox
key={objectId(change)}
$hasConflict={Boolean(change.conflict)}
$isInConflictFeature={Boolean(
featureToggleChange.conflict
)}
$isAfterWarning={Boolean(
featureToggleChange.changes[index - 1]?.conflict
)}
$isLast={
index + 1 === featureToggleChange.changes.length
}
>
<ConditionallyRender
condition={
Boolean(change.conflict) &&
!featureToggleChange.conflict
}
show={
<StyledAlert severity="warning">
<strong>Conflict!</strong> This change
cant be applied. {change.conflict}.
</StyledAlert>
}
/>
<Box sx={{ p: 2 }}>
{change.action === 'updateEnabled' && (
<ToggleStatusChange
enabled={change.payload.enabled}
discard={
<ConditionallyRender
condition={showDiscard}
show={
<Discard
onDiscard={onDiscard(
change.id
)}
/>
}
/>
}
/>
)}
{change.action === 'addStrategy' && (
<StrategyAddedChange
discard={
<ConditionallyRender
condition={showDiscard}
show={
<Discard
onDiscard={onDiscard(
change.id
)}
/>
}
/>
}
>
<CodeSnippetPopover change={change} />
</StrategyAddedChange>
)}
{change.action === 'deleteStrategy' && (
<StrategyDeletedChange
discard={
<ConditionallyRender
condition={showDiscard}
show={
<Discard
onDiscard={onDiscard(
change.id
)}
/>
}
/>
}
>
{hasNameField(change.payload) && (
<CodeSnippetPopover
change={change}
/>
)}
</StrategyDeletedChange>
)}
{change.action === 'updateStrategy' && (
<StrategyEditedChange
discard={
<ConditionallyRender
condition={showDiscard}
show={
<Discard
onDiscard={onDiscard(
change.id
)}
/>
}
/>
}
>
<CodeSnippetPopover change={change} />
</StrategyEditedChange>
)}
</Box>
</StyledSingleChangeBox>
{feature.changes.map((change, index) => (
<Change
onDiscard={onDiscard(change.id)}
index={index}
changeRequest={changeRequest}
change={change}
feature={feature}
/>
))}
</ChangeRequestFeatureToggleChange>
))}

View File

@ -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 (
<StyledCodeSection>
<EventDiff
entry={{
preData: currentStrategy,
data: changeRequestStrategy,
}}
/>
</StyledCodeSection>
);
};
interface ICodeSnippetPopoverProps {
change:
| IChangeRequestAddStrategy
| IChangeRequestUpdateStrategy
| IChangeRequestDeleteStrategy;
}
// based on: https://mui.com/material-ui/react-popover/#mouse-over-interaction
export const CodeSnippetPopover: FC<ICodeSnippetPopoverProps> = ({
change,
children,
}) => {
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handlePopoverClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
return (
<>
<GetFeatureStrategyIcon strategyName={change.payload.name} />
<Typography
onMouseEnter={handlePopoverOpen}
onMouseLeave={handlePopoverClose}
>
{formatStrategyName(change.payload.name)}
</Typography>
<Popover
id={String(change.id)}
sx={{
pointerEvents: 'none',
}}
open={open}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
onClose={handlePopoverClose}
disableRestoreFocus
>
{children}
</Popover>
</>
);
};

View File

@ -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),

View File

@ -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<IStrategyExecutionProps> = ({
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<IStrategyExecutionProps> = ({
}
const listItems = [
Boolean(uiConfig.flags.SE) && segments && segments.length > 0 && (
<FeatureOverviewSegment strategyId={strategy.id} />
),
Boolean(uiConfig.flags.SE) &&
strategySegments &&
strategySegments.length > 0 && (
<FeatureOverviewSegment segments={strategySegments} />
),
constraints.length > 0 && (
<ConstraintAccordionList
constraints={constraints}

View File

@ -1,18 +1,16 @@
import { Fragment } from 'react';
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { SegmentItem } from '../../../../common/SegmentItem/SegmentItem';
import { ISegment } from 'interfaces/segment';
interface IFeatureOverviewSegmentProps {
strategyId: string;
segments?: ISegment[];
}
export const FeatureOverviewSegment = ({
strategyId,
segments,
}: IFeatureOverviewSegmentProps) => {
const { segments } = useSegments(strategyId);
if (!segments || segments.length === 0) {
return null;
}