1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-27 01:19:00 +02:00
unleash.unleash/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/DiffableChange.tsx
2025-06-24 18:05:11 +02:00

403 lines
13 KiB
TypeScript

import type React from 'react';
import type { FC, ReactNode } from 'react';
import { Box, Button, styled, Tooltip, Typography } from '@mui/material';
import { Tab, Tabs, TabsList, TabPanel } from '@mui/base';
import BlockIcon from '@mui/icons-material/Block';
import TrackChangesIcon from '@mui/icons-material/TrackChanges';
import {
StrategyDiff,
StrategyTooltipLink,
} from '../../StrategyTooltipLink/StrategyTooltipLink.tsx';
import { StrategyExecution } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution';
import type {
ChangeRequestState,
IChangeRequestAddStrategy,
IChangeRequestDeleteStrategy,
IChangeRequestUpdateStrategy,
} from 'component/changeRequest/changeRequest.types';
import { useCurrentStrategy } from './hooks/useCurrentStrategy.ts';
import { Badge } from 'component/common/Badge/Badge';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { flexRow } from 'themes/themeStyles';
import { EnvironmentVariantsTable } from 'component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsTable/EnvironmentVariantsTable';
import { ChangeOverwriteWarning } from './ChangeOverwriteWarning/ChangeOverwriteWarning.tsx';
import type { IFeatureStrategy } from 'interfaces/strategy';
export const ChangeItemWrapper = styled(Box)({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
});
const ChangeItemCreateEditDeleteWrapper = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
gap: theme.spacing(1),
alignItems: 'center',
width: '100%',
}));
const ChangeItemInfo: FC<{ children?: React.ReactNode }> = styled(Box)(
({ theme }) => ({
display: 'grid',
gridTemplateColumns: '150px auto',
gridAutoFlow: 'column',
alignItems: 'center',
flexGrow: 1,
gap: theme.spacing(1),
}),
);
const StyledBox: FC<{ children?: React.ReactNode }> = styled(Box)(
({ theme }) => ({
marginTop: theme.spacing(2),
}),
);
const StyledTypography: FC<{ children?: React.ReactNode }> = styled(Typography)(
({ theme }) => ({
margin: `${theme.spacing(1)} 0`,
}),
);
const DisabledEnabledState: FC<{ show?: boolean; disabled: boolean }> = ({
show = true,
disabled,
}) => {
if (!show) {
return null;
}
if (disabled) {
return (
<Tooltip
title='This strategy will not be taken into account when evaluating feature flag.'
arrow
sx={{ cursor: 'pointer' }}
>
<Badge color='disabled' icon={<BlockIcon />}>
Disabled
</Badge>
</Tooltip>
);
}
return (
<Tooltip
title='This was disabled before and with this change it will be taken into account when evaluating feature flag.'
arrow
sx={{ cursor: 'pointer' }}
>
<Badge color='success' icon={<TrackChangesIcon />}>
Enabled
</Badge>
</Tooltip>
);
};
const EditHeader: FC<{
wasDisabled?: boolean;
willBeDisabled?: boolean;
}> = ({ wasDisabled = false, willBeDisabled = false }) => {
if (wasDisabled && willBeDisabled) {
return (
<Typography color='action.disabled'>Editing strategy:</Typography>
);
}
if (!wasDisabled && willBeDisabled) {
return <Typography color='error.dark'>Editing strategy:</Typography>;
}
if (wasDisabled && !willBeDisabled) {
return <Typography color='success.dark'>Editing strategy:</Typography>;
}
return <Typography>Editing strategy:</Typography>;
};
const hasDiff = (object: unknown, objectToCompare: unknown) =>
JSON.stringify(object) !== JSON.stringify(objectToCompare);
const DeleteStrategy: FC<{
change: IChangeRequestDeleteStrategy;
changeRequestState: ChangeRequestState;
currentStrategy: IFeatureStrategy | undefined;
actions?: ReactNode;
}> = ({ change, changeRequestState, currentStrategy, actions }) => {
const name =
changeRequestState === 'Applied'
? change.payload?.snapshot?.name
: currentStrategy?.name;
const title =
changeRequestState === 'Applied'
? change.payload?.snapshot?.title
: currentStrategy?.title;
const referenceStrategy =
changeRequestState === 'Applied'
? change.payload.snapshot
: currentStrategy;
return (
<>
<ChangeItemCreateEditDeleteWrapper className='delete-strategy-information-wrapper'>
<ChangeItemInfo>
<Typography
sx={(theme) => ({
color: theme.palette.error.main,
})}
>
- Deleting strategy:
</Typography>
<StrategyTooltipLink name={name || ''} title={title}>
<StrategyDiff
change={change}
currentStrategy={referenceStrategy}
/>
</StrategyTooltipLink>
</ChangeItemInfo>
<div>{actions}</div>
</ChangeItemCreateEditDeleteWrapper>
{referenceStrategy && (
<StrategyExecution strategy={referenceStrategy} />
)}
</>
);
};
const RightHandSide = styled('div')(({ theme }) => ({
display: 'flex',
gap: theme.spacing(1),
alignItems: 'center',
}));
const StyledTabList = styled(TabsList)(({ theme }) => ({
display: 'inline-flex',
flexDirection: 'row',
gap: theme.spacing(0.5),
}));
const StyledButton = styled(Button)(({ theme }) => ({
whiteSpace: 'nowrap',
color: theme.palette.text.secondary,
fontWeight: 'normal',
'&[aria-selected="true"]': {
fontWeight: 'bold',
color: theme.palette.primary.main,
background: theme.palette.background.elevation1,
},
}));
const UpdateStrategy: FC<{
change: IChangeRequestUpdateStrategy;
changeRequestState: ChangeRequestState;
currentStrategy: IFeatureStrategy | undefined;
actions?: ReactNode;
}> = ({ change, changeRequestState, currentStrategy, actions }) => {
const previousTitle =
changeRequestState === 'Applied'
? change.payload.snapshot?.title
: currentStrategy?.title;
const referenceStrategy =
changeRequestState === 'Applied'
? change.payload.snapshot
: currentStrategy;
const hasVariantDiff = hasDiff(
referenceStrategy?.variants || [],
change.payload.variants || [],
);
return (
<Tabs
aria-label='View rendered change or JSON diff'
selectionFollowsFocus
>
<ChangeOverwriteWarning
data={{
current: currentStrategy,
change,
changeType: 'strategy',
}}
changeRequestState={changeRequestState}
/>
<ChangeItemCreateEditDeleteWrapper>
<ChangeItemInfo>
<EditHeader
wasDisabled={currentStrategy?.disabled}
willBeDisabled={change.payload?.disabled}
/>
<StrategyTooltipLink
name={change.payload.name}
title={change.payload.title}
previousTitle={previousTitle}
>
<StrategyDiff
change={change}
currentStrategy={referenceStrategy}
/>
</StrategyTooltipLink>
</ChangeItemInfo>
<RightHandSide>
<StyledTabList>
<Tab<typeof StyledButton>
value={0}
slots={{ root: StyledButton }}
>
Change
</Tab>
<Tab<typeof StyledButton>
value={1}
slots={{ root: StyledButton }}
>
View diff
</Tab>
</StyledTabList>
{actions}
</RightHandSide>
</ChangeItemCreateEditDeleteWrapper>
<ConditionallyRender
condition={
change.payload?.disabled !== currentStrategy?.disabled
}
show={
<Typography
sx={{
marginTop: (theme) => theme.spacing(2),
marginBottom: (theme) => theme.spacing(2),
...flexRow,
gap: (theme) => theme.spacing(1),
}}
>
This strategy will be{' '}
<DisabledEnabledState
disabled={change.payload?.disabled || false}
/>
</Typography>
}
/>
<TabPanel value={0}>
<StrategyExecution strategy={change.payload} />
{hasVariantDiff ? (
<StyledBox>
{change.payload.variants?.length ? (
<>
<StyledTypography>
{currentStrategy?.variants?.length
? 'Updating strategy variants to:'
: 'Adding strategy variants:'}
</StyledTypography>
<EnvironmentVariantsTable
variants={change.payload.variants || []}
/>
</>
) : (
<StyledTypography>
Removed all strategy variants.
</StyledTypography>
)}
</StyledBox>
) : null}
</TabPanel>
<TabPanel value={1}>
<StrategyDiff
change={change}
currentStrategy={referenceStrategy}
/>
</TabPanel>
</Tabs>
);
};
const AddStrategy: FC<{
change: IChangeRequestAddStrategy;
actions?: ReactNode;
}> = ({ change, actions }) => (
<>
<ChangeItemCreateEditDeleteWrapper>
<ChangeItemInfo>
<Typography
color={
change.payload?.disabled
? 'action.disabled'
: 'success.dark'
}
>
+ Adding strategy:
</Typography>
<StrategyTooltipLink
name={change.payload.name}
title={change.payload.title}
/>
<div>
<DisabledEnabledState
disabled
show={change.payload?.disabled === true}
/>
</div>
</ChangeItemInfo>
<StrategyDiff change={change} currentStrategy={undefined} />
<div>{actions}</div>
</ChangeItemCreateEditDeleteWrapper>
<StrategyExecution strategy={change.payload} />
{change.payload.variants?.length ? (
<StyledBox>
<StyledTypography>Adding strategy variants:</StyledTypography>
<EnvironmentVariantsTable
variants={change.payload.variants || []}
/>
</StyledBox>
) : null}
</>
);
export const DiffableChange: FC<{
actions?: ReactNode;
change:
| IChangeRequestAddStrategy
| IChangeRequestDeleteStrategy
| IChangeRequestUpdateStrategy;
environmentName: string;
featureName: string;
projectId: string;
changeRequestState: ChangeRequestState;
}> = ({
actions,
change,
featureName,
environmentName,
projectId,
changeRequestState,
}) => {
const currentStrategy = useCurrentStrategy(
change,
projectId,
featureName,
environmentName,
);
return (
<>
{change.action === 'addStrategy' && (
<AddStrategy change={change} actions={actions} />
)}
{change.action === 'deleteStrategy' && (
<DeleteStrategy
change={change}
changeRequestState={changeRequestState}
currentStrategy={currentStrategy}
actions={actions}
/>
)}
{change.action === 'updateStrategy' && (
<UpdateStrategy
change={change}
changeRequestState={changeRequestState}
currentStrategy={currentStrategy}
actions={actions}
/>
)}
</>
);
};