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:
parent
e728ecba69
commit
c7fe4a5a01
@ -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 can’t 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
|
||||
can’t 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>
|
||||
))}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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),
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user