Skip to content

Commit 863788d

Browse files
authored
Feat(UI): new strategy variant chips (#9507)
- new way of showing strategy variants - fixed wrapping issue in strategy editing, for a lot of variants defined (`SplitPreviewSlider.tsx` change) - aligned difference between API and manually added types
1 parent 5ad3178 commit 863788d

File tree

13 files changed

+169
-84
lines changed

13 files changed

+169
-84
lines changed

frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ChangeOverwriteWarning/strategy-change-diff-calculation.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ describe('Strategy change conflict detection', () => {
274274
name: 'variant1',
275275
weight: 1000,
276276
payload: {
277-
type: 'string',
277+
type: 'string' as const,
278278
value: 'beaty',
279279
},
280280
stickiness: 'userId',

frontend/src/component/common/ConstraintsList/StrategyEvaluationItem/StrategyEvaluationItem.tsx

+19-9
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,19 @@ const StyledContainer = styled('div')(({ theme }) => ({
1212
gap: theme.spacing(1),
1313
alignItems: 'center',
1414
fontSize: theme.typography.body2.fontSize,
15-
padding: theme.spacing(2, 3),
15+
margin: theme.spacing(2, 3),
16+
}));
17+
18+
const StyledContent = styled('div')(({ theme }) => ({
19+
display: 'flex',
20+
gap: theme.spacing(1),
21+
flexWrap: 'wrap',
22+
alignItems: 'center',
1623
}));
1724

1825
const StyledType = styled('span')(({ theme }) => ({
1926
display: 'block',
27+
flexShrink: 0,
2028
fontSize: theme.fontSizes.smallerBody,
2129
fontWeight: theme.typography.fontWeightBold,
2230
color: theme.palette.text.secondary,
@@ -46,13 +54,15 @@ export const StrategyEvaluationItem: FC<StrategyItemProps> = ({
4654
}) => (
4755
<StyledContainer>
4856
<StyledType>{type}</StyledType>
49-
{children}
50-
{values && values?.length > 0 ? (
51-
<StyledValuesGroup>
52-
{values?.map((value, index) => (
53-
<StyledValue key={`${value}#${index}`} label={value} />
54-
))}
55-
</StyledValuesGroup>
56-
) : null}
57+
<StyledContent>
58+
{children}
59+
{values && values?.length > 0 ? (
60+
<StyledValuesGroup>
61+
{values?.map((value, index) => (
62+
<StyledValue key={`${value}#${index}`} label={value} />
63+
))}
64+
</StyledValuesGroup>
65+
) : null}
66+
</StyledContent>
5767
</StyledContainer>
5868
);

frontend/src/component/common/SidebarModal/SidebarModal.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ interface ISidebarModalProps {
1515

1616
const TRANSITION_DURATION = 250;
1717

18-
const ModalContentWrapper = styled('div')({
18+
const ModalContentWrapper = styled('div')(({ theme }) => ({
1919
position: 'absolute',
2020
top: 0,
2121
right: 0,
@@ -24,7 +24,8 @@ const ModalContentWrapper = styled('div')({
2424
maxWidth: '98vw',
2525
overflow: 'auto',
2626
boxShadow: '0 0 1rem rgba(0, 0, 0, 0.25)',
27-
});
27+
background: theme.palette.background.paper,
28+
}));
2829

2930
const FixedWidthContentWrapper = styled(ModalContentWrapper)({
3031
width: 1300,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { StrategyEvaluationChip } from 'component/common/ConstraintsList/StrategyEvaluationChip/StrategyEvaluationChip';
2+
import { StrategyEvaluationItem } from 'component/common/ConstraintsList/StrategyEvaluationItem/StrategyEvaluationItem';
3+
import type { ParametersSchema, StrategyVariantSchema } from 'openapi';
4+
import type { FC } from 'react';
5+
import { parseParameterNumber } from 'utils/parseParameter';
6+
import { RolloutVariants } from './RolloutVariants/RolloutVariants';
7+
8+
export const RolloutParameter: FC<{
9+
value: string;
10+
parameters?: ParametersSchema;
11+
hasConstraints?: boolean;
12+
variants?: StrategyVariantSchema[];
13+
displayGroupId?: boolean;
14+
}> = ({ value, parameters, hasConstraints, variants, displayGroupId }) => {
15+
const percentage = parseParameterNumber(value);
16+
17+
const explainStickiness =
18+
typeof parameters?.stickiness === 'string' &&
19+
parameters?.stickiness !== 'default';
20+
const stickiness = explainStickiness ? (
21+
<>
22+
with <strong>{parameters.stickiness}</strong>
23+
</>
24+
) : (
25+
''
26+
);
27+
28+
return (
29+
<>
30+
<StrategyEvaluationItem type='Rollout %'>
31+
<StrategyEvaluationChip label={`${percentage}%`} /> of your base{' '}
32+
{stickiness}
33+
<span>
34+
{hasConstraints ? 'who match constraints ' : ' '}
35+
is included.
36+
</span>
37+
{displayGroupId && parameters?.groupId ? (
38+
<StrategyEvaluationChip
39+
label={`groupId: ${parameters?.groupId}`}
40+
/>
41+
) : null}
42+
</StrategyEvaluationItem>
43+
<RolloutVariants variants={variants} />
44+
</>
45+
);
46+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { styled } from '@mui/material';
2+
import { StrategyEvaluationChip } from 'component/common/ConstraintsList/StrategyEvaluationChip/StrategyEvaluationChip';
3+
import { StrategyEvaluationItem } from 'component/common/ConstraintsList/StrategyEvaluationItem/StrategyEvaluationItem';
4+
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
5+
import type { StrategyVariantSchema } from 'openapi';
6+
import type { FC } from 'react';
7+
8+
const StyledVariantChip = styled(StrategyEvaluationChip)<{ order: number }>(
9+
({ theme, order }) => {
10+
const variantColor =
11+
theme.palette.variants[order % theme.palette.variants.length];
12+
13+
return {
14+
borderRadius: theme.shape.borderRadiusExtraLarge,
15+
border: 'none',
16+
color: theme.palette.text.primary,
17+
background:
18+
// TODO: adjust theme.palette.variants
19+
theme.mode === 'dark'
20+
? `hsl(from ${variantColor} h calc(s - 30) calc(l - 45))`
21+
: `hsl(from ${variantColor} h s calc(l + 5))`,
22+
fontWeight: theme.typography.fontWeightRegular,
23+
};
24+
},
25+
);
26+
27+
const StyledPayloadHeader = styled('div')(({ theme }) => ({
28+
fontSize: theme.typography.body2.fontSize,
29+
marginBottom: theme.spacing(1),
30+
}));
31+
32+
export const RolloutVariants: FC<{
33+
variants?: StrategyVariantSchema[];
34+
}> = ({ variants }) => {
35+
if (!variants?.length) {
36+
return null;
37+
}
38+
39+
return (
40+
<StrategyEvaluationItem type={`Variants (${variants.length})`}>
41+
{variants.map((variant, i) => (
42+
<HtmlTooltip
43+
arrow
44+
title={
45+
variant.payload?.value ? (
46+
<div>
47+
<StyledPayloadHeader>
48+
Payload:
49+
</StyledPayloadHeader>
50+
<code>{variant.payload?.value}</code>
51+
</div>
52+
) : null
53+
}
54+
key={variant.name}
55+
>
56+
<StyledVariantChip
57+
key={variant.name}
58+
order={i}
59+
label={
60+
<>
61+
<span>
62+
{variant.weight / 10}% – {variant.name}
63+
</span>
64+
</>
65+
}
66+
/>
67+
</HtmlTooltip>
68+
))}
69+
</StrategyEvaluationItem>
70+
);
71+
};

frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { FC } from 'react';
22
import { styled } from '@mui/material';
3-
import type { CreateFeatureStrategySchema } from 'openapi';
3+
import type { FeatureStrategySchema } from 'openapi';
44
import type { IFeatureStrategyPayload } from 'interfaces/strategy';
55
import { useUiFlag } from 'hooks/useUiFlag';
66
import { StrategyExecution as LegacyStrategyExecution } from './LegacyStrategyExecution';
@@ -20,7 +20,7 @@ const FilterContainer = styled('div', {
2020
);
2121

2222
type StrategyExecutionProps = {
23-
strategy: IFeatureStrategyPayload | CreateFeatureStrategySchema;
23+
strategy: IFeatureStrategyPayload | FeatureStrategySchema;
2424
displayGroupId?: boolean;
2525
};
2626

@@ -32,7 +32,10 @@ export const StrategyExecution: FC<StrategyExecutionProps> = ({
3232
const { segments } = useSegments();
3333
const { isCustomStrategy, customStrategyParameters: customStrategyItems } =
3434
useCustomStrategyParameters(strategy, strategies);
35-
const strategyParameters = useStrategyParameters(strategy, displayGroupId);
35+
const strategyParameters = useStrategyParameters(
36+
strategy as FeatureStrategySchema,
37+
displayGroupId,
38+
);
3639
const { constraints } = strategy;
3740
const strategySegments = segments?.filter((segment) =>
3841
strategy.segments?.includes(segment.id),
@@ -52,7 +55,7 @@ export const StrategyExecution: FC<StrategyExecutionProps> = ({
5255
<FilterContainer grayscale={strategy.disabled === true}>
5356
<ConstraintsList>
5457
{strategySegments?.map((segment) => (
55-
<SegmentItem segment={segment} />
58+
<SegmentItem segment={segment} key={segment.id} />
5659
))}
5760
{constraints?.map((constraint, index) => (
5861
<ConstraintItem

frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/hooks/useStrategyParameters.tsx

+11-52
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,30 @@
1-
import { type FC, useMemo } from 'react';
2-
import { StrategyEvaluationChip } from 'component/common/ConstraintsList/StrategyEvaluationChip/StrategyEvaluationChip';
3-
import {
4-
parseParameterNumber,
5-
parseParameterStrings,
6-
} from 'utils/parseParameter';
1+
import { useMemo } from 'react';
2+
import { parseParameterStrings } from 'utils/parseParameter';
73
import { StrategyEvaluationItem } from 'component/common/ConstraintsList/StrategyEvaluationItem/StrategyEvaluationItem';
8-
import type { IFeatureStrategyPayload } from 'interfaces/strategy';
9-
import type { CreateFeatureStrategySchema } from 'openapi';
10-
11-
const RolloutParameter: FC<{
12-
value?: string | number;
13-
parameters?: (
14-
| IFeatureStrategyPayload
15-
| CreateFeatureStrategySchema
16-
)['parameters'];
17-
hasConstraints?: boolean;
18-
displayGroupId?: boolean;
19-
}> = ({ value, parameters, hasConstraints, displayGroupId }) => {
20-
const percentage = parseParameterNumber(value);
21-
22-
const explainStickiness =
23-
typeof parameters?.stickiness === 'string' &&
24-
parameters?.stickiness !== 'default';
25-
const stickiness = explainStickiness ? (
26-
<>
27-
with <strong>{parameters.stickiness}</strong>
28-
</>
29-
) : (
30-
''
31-
);
32-
33-
return (
34-
<StrategyEvaluationItem type='Rollout %'>
35-
<StrategyEvaluationChip label={`${percentage}%`} /> of your base{' '}
36-
{stickiness}
37-
<span>
38-
{hasConstraints ? 'who match constraints ' : ' '}
39-
is included.
40-
</span>
41-
{displayGroupId && parameters?.groupId ? (
42-
<StrategyEvaluationChip
43-
label={`groupId: ${parameters?.groupId}`}
44-
/>
45-
) : null}
46-
</StrategyEvaluationItem>
47-
);
48-
};
4+
import type { FeatureStrategySchema } from 'openapi';
5+
import { RolloutParameter } from '../RolloutParameter/RolloutParameter';
496

507
export const useStrategyParameters = (
51-
strategy: IFeatureStrategyPayload | CreateFeatureStrategySchema,
8+
strategy: Partial<FeatureStrategySchema>,
529
displayGroupId?: boolean,
5310
) => {
54-
const { constraints } = strategy;
11+
const { constraints, variants } = strategy;
5512
const { parameters } = strategy;
5613
const hasConstraints = Boolean(constraints?.length);
5714
const parameterKeys = parameters ? Object.keys(parameters) : [];
5815
const mapPredefinedStrategies = (key: string) => {
5916
const type = key.toLocaleLowerCase();
17+
const value = parameters?.[key] || '';
6018

6119
if (type === 'rollout') {
6220
return (
6321
<RolloutParameter
6422
key={key}
65-
value={parameters?.[key]}
23+
value={value}
6624
parameters={parameters}
6725
hasConstraints={hasConstraints}
6826
displayGroupId={displayGroupId}
27+
variants={variants}
6928
/>
7029
);
7130
}
@@ -75,7 +34,7 @@ export const useStrategyParameters = (
7534
<StrategyEvaluationItem
7635
key={key}
7736
type={key}
78-
values={parseParameterStrings(parameters?.[key])}
37+
values={parseParameterStrings(value)}
7938
/>
8039
);
8140
}
@@ -88,7 +47,7 @@ export const useStrategyParameters = (
8847
[
8948
...parameterKeys.map(mapPredefinedStrategies),
9049
strategy.name === 'default' ? (
91-
<RolloutParameter value={100} />
50+
<RolloutParameter value='100' />
9251
) : null,
9352
].filter(Boolean),
9453
[parameters, hasConstraints, displayGroupId],

frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx

-12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import type { DragEventHandler, FC, ReactNode } from 'react';
22
import type { IFeatureStrategy } from 'interfaces/strategy';
33
import { StrategyExecution } from './StrategyExecution/StrategyExecution';
4-
import SplitPreviewSlider from 'component/feature/StrategyTypes/SplitPreviewSlider/SplitPreviewSlider';
5-
import { Box } from '@mui/material';
64
import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer';
75

86
type StrategyItemProps = {
@@ -29,16 +27,6 @@ export const StrategyItem: FC<StrategyItemProps> = ({
2927
headerItemsRight={headerItemsRight}
3028
>
3129
<StrategyExecution strategy={strategy} />
32-
33-
{strategy.variants &&
34-
strategy.variants.length > 0 &&
35-
(strategy.disabled ? (
36-
<Box sx={{ opacity: '0.5' }}>
37-
<SplitPreviewSlider variants={strategy.variants} />
38-
</Box>
39-
) : (
40-
<SplitPreviewSlider variants={strategy.variants} />
41-
))}
4230
</StrategyItemContainer>
4331
);
4432
};

frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/VariantForm/VariantForm.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ const payloadOptions = [
149149
{ key: 'number', label: 'number' },
150150
];
151151

152-
const EMPTY_PAYLOAD = { type: 'string', value: '' };
152+
const EMPTY_PAYLOAD = { type: 'string' as const, value: '' };
153153

154154
enum ErrorField {
155155
NAME = 'name',
@@ -438,7 +438,7 @@ export const VariantForm = ({
438438
clearError(ErrorField.PAYLOAD);
439439
setPayload((payload) => ({
440440
...payload,
441-
type: e.target.value,
441+
type: e.target.value as typeof payload.type,
442442
}));
443443
}}
444444
/>

frontend/src/component/feature/StrategyTypes/SplitPreviewSlider/SplitPreviewSlider.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const StyledVariantBoxContainer = styled(Box)(() => ({
4848
display: 'flex',
4949
alignItems: 'center',
5050
marginLeft: 'auto',
51+
flexWrap: 'wrap',
5152
}));
5253

5354
const StyledVariantBox = styled(Box, {

frontend/src/component/feature/StrategyTypes/StrategyVariants.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ test('should render variants', async () => {
1919
weight: 1000,
2020
weightType: 'variable' as const,
2121
payload: {
22-
type: 'string',
22+
type: 'string' as const,
2323
value: 'variantValue',
2424
},
2525
},

0 commit comments

Comments
 (0)