mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-31 13:47:02 +02:00
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
This commit is contained in:
parent
5ad3178590
commit
863788d7b3
@ -274,7 +274,7 @@ describe('Strategy change conflict detection', () => {
|
||||
name: 'variant1',
|
||||
weight: 1000,
|
||||
payload: {
|
||||
type: 'string',
|
||||
type: 'string' as const,
|
||||
value: 'beaty',
|
||||
},
|
||||
stickiness: 'userId',
|
||||
|
@ -12,11 +12,19 @@ const StyledContainer = styled('div')(({ theme }) => ({
|
||||
gap: theme.spacing(1),
|
||||
alignItems: 'center',
|
||||
fontSize: theme.typography.body2.fontSize,
|
||||
padding: theme.spacing(2, 3),
|
||||
margin: theme.spacing(2, 3),
|
||||
}));
|
||||
|
||||
const StyledContent = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
const StyledType = styled('span')(({ theme }) => ({
|
||||
display: 'block',
|
||||
flexShrink: 0,
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
color: theme.palette.text.secondary,
|
||||
@ -46,13 +54,15 @@ export const StrategyEvaluationItem: FC<StrategyItemProps> = ({
|
||||
}) => (
|
||||
<StyledContainer>
|
||||
<StyledType>{type}</StyledType>
|
||||
{children}
|
||||
{values && values?.length > 0 ? (
|
||||
<StyledValuesGroup>
|
||||
{values?.map((value, index) => (
|
||||
<StyledValue key={`${value}#${index}`} label={value} />
|
||||
))}
|
||||
</StyledValuesGroup>
|
||||
) : null}
|
||||
<StyledContent>
|
||||
{children}
|
||||
{values && values?.length > 0 ? (
|
||||
<StyledValuesGroup>
|
||||
{values?.map((value, index) => (
|
||||
<StyledValue key={`${value}#${index}`} label={value} />
|
||||
))}
|
||||
</StyledValuesGroup>
|
||||
) : null}
|
||||
</StyledContent>
|
||||
</StyledContainer>
|
||||
);
|
||||
|
@ -15,7 +15,7 @@ interface ISidebarModalProps {
|
||||
|
||||
const TRANSITION_DURATION = 250;
|
||||
|
||||
const ModalContentWrapper = styled('div')({
|
||||
const ModalContentWrapper = styled('div')(({ theme }) => ({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
@ -24,7 +24,8 @@ const ModalContentWrapper = styled('div')({
|
||||
maxWidth: '98vw',
|
||||
overflow: 'auto',
|
||||
boxShadow: '0 0 1rem rgba(0, 0, 0, 0.25)',
|
||||
});
|
||||
background: theme.palette.background.paper,
|
||||
}));
|
||||
|
||||
const FixedWidthContentWrapper = styled(ModalContentWrapper)({
|
||||
width: 1300,
|
||||
|
@ -0,0 +1,46 @@
|
||||
import { StrategyEvaluationChip } from 'component/common/ConstraintsList/StrategyEvaluationChip/StrategyEvaluationChip';
|
||||
import { StrategyEvaluationItem } from 'component/common/ConstraintsList/StrategyEvaluationItem/StrategyEvaluationItem';
|
||||
import type { ParametersSchema, StrategyVariantSchema } from 'openapi';
|
||||
import type { FC } from 'react';
|
||||
import { parseParameterNumber } from 'utils/parseParameter';
|
||||
import { RolloutVariants } from './RolloutVariants/RolloutVariants';
|
||||
|
||||
export const RolloutParameter: FC<{
|
||||
value: string;
|
||||
parameters?: ParametersSchema;
|
||||
hasConstraints?: boolean;
|
||||
variants?: StrategyVariantSchema[];
|
||||
displayGroupId?: boolean;
|
||||
}> = ({ value, parameters, hasConstraints, variants, displayGroupId }) => {
|
||||
const percentage = parseParameterNumber(value);
|
||||
|
||||
const explainStickiness =
|
||||
typeof parameters?.stickiness === 'string' &&
|
||||
parameters?.stickiness !== 'default';
|
||||
const stickiness = explainStickiness ? (
|
||||
<>
|
||||
with <strong>{parameters.stickiness}</strong>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StrategyEvaluationItem type='Rollout %'>
|
||||
<StrategyEvaluationChip label={`${percentage}%`} /> of your base{' '}
|
||||
{stickiness}
|
||||
<span>
|
||||
{hasConstraints ? 'who match constraints ' : ' '}
|
||||
is included.
|
||||
</span>
|
||||
{displayGroupId && parameters?.groupId ? (
|
||||
<StrategyEvaluationChip
|
||||
label={`groupId: ${parameters?.groupId}`}
|
||||
/>
|
||||
) : null}
|
||||
</StrategyEvaluationItem>
|
||||
<RolloutVariants variants={variants} />
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,71 @@
|
||||
import { styled } from '@mui/material';
|
||||
import { StrategyEvaluationChip } from 'component/common/ConstraintsList/StrategyEvaluationChip/StrategyEvaluationChip';
|
||||
import { StrategyEvaluationItem } from 'component/common/ConstraintsList/StrategyEvaluationItem/StrategyEvaluationItem';
|
||||
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||
import type { StrategyVariantSchema } from 'openapi';
|
||||
import type { FC } from 'react';
|
||||
|
||||
const StyledVariantChip = styled(StrategyEvaluationChip)<{ order: number }>(
|
||||
({ theme, order }) => {
|
||||
const variantColor =
|
||||
theme.palette.variants[order % theme.palette.variants.length];
|
||||
|
||||
return {
|
||||
borderRadius: theme.shape.borderRadiusExtraLarge,
|
||||
border: 'none',
|
||||
color: theme.palette.text.primary,
|
||||
background:
|
||||
// TODO: adjust theme.palette.variants
|
||||
theme.mode === 'dark'
|
||||
? `hsl(from ${variantColor} h calc(s - 30) calc(l - 45))`
|
||||
: `hsl(from ${variantColor} h s calc(l + 5))`,
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const StyledPayloadHeader = styled('div')(({ theme }) => ({
|
||||
fontSize: theme.typography.body2.fontSize,
|
||||
marginBottom: theme.spacing(1),
|
||||
}));
|
||||
|
||||
export const RolloutVariants: FC<{
|
||||
variants?: StrategyVariantSchema[];
|
||||
}> = ({ variants }) => {
|
||||
if (!variants?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StrategyEvaluationItem type={`Variants (${variants.length})`}>
|
||||
{variants.map((variant, i) => (
|
||||
<HtmlTooltip
|
||||
arrow
|
||||
title={
|
||||
variant.payload?.value ? (
|
||||
<div>
|
||||
<StyledPayloadHeader>
|
||||
Payload:
|
||||
</StyledPayloadHeader>
|
||||
<code>{variant.payload?.value}</code>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
key={variant.name}
|
||||
>
|
||||
<StyledVariantChip
|
||||
key={variant.name}
|
||||
order={i}
|
||||
label={
|
||||
<>
|
||||
<span>
|
||||
{variant.weight / 10}% – {variant.name}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</HtmlTooltip>
|
||||
))}
|
||||
</StrategyEvaluationItem>
|
||||
);
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import type { FC } from 'react';
|
||||
import { styled } from '@mui/material';
|
||||
import type { CreateFeatureStrategySchema } from 'openapi';
|
||||
import type { FeatureStrategySchema } from 'openapi';
|
||||
import type { IFeatureStrategyPayload } from 'interfaces/strategy';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { StrategyExecution as LegacyStrategyExecution } from './LegacyStrategyExecution';
|
||||
@ -20,7 +20,7 @@ const FilterContainer = styled('div', {
|
||||
);
|
||||
|
||||
type StrategyExecutionProps = {
|
||||
strategy: IFeatureStrategyPayload | CreateFeatureStrategySchema;
|
||||
strategy: IFeatureStrategyPayload | FeatureStrategySchema;
|
||||
displayGroupId?: boolean;
|
||||
};
|
||||
|
||||
@ -32,7 +32,10 @@ export const StrategyExecution: FC<StrategyExecutionProps> = ({
|
||||
const { segments } = useSegments();
|
||||
const { isCustomStrategy, customStrategyParameters: customStrategyItems } =
|
||||
useCustomStrategyParameters(strategy, strategies);
|
||||
const strategyParameters = useStrategyParameters(strategy, displayGroupId);
|
||||
const strategyParameters = useStrategyParameters(
|
||||
strategy as FeatureStrategySchema,
|
||||
displayGroupId,
|
||||
);
|
||||
const { constraints } = strategy;
|
||||
const strategySegments = segments?.filter((segment) =>
|
||||
strategy.segments?.includes(segment.id),
|
||||
@ -52,7 +55,7 @@ export const StrategyExecution: FC<StrategyExecutionProps> = ({
|
||||
<FilterContainer grayscale={strategy.disabled === true}>
|
||||
<ConstraintsList>
|
||||
{strategySegments?.map((segment) => (
|
||||
<SegmentItem segment={segment} />
|
||||
<SegmentItem segment={segment} key={segment.id} />
|
||||
))}
|
||||
{constraints?.map((constraint, index) => (
|
||||
<ConstraintItem
|
||||
|
@ -1,71 +1,30 @@
|
||||
import { type FC, useMemo } from 'react';
|
||||
import { StrategyEvaluationChip } from 'component/common/ConstraintsList/StrategyEvaluationChip/StrategyEvaluationChip';
|
||||
import {
|
||||
parseParameterNumber,
|
||||
parseParameterStrings,
|
||||
} from 'utils/parseParameter';
|
||||
import { useMemo } from 'react';
|
||||
import { parseParameterStrings } from 'utils/parseParameter';
|
||||
import { StrategyEvaluationItem } from 'component/common/ConstraintsList/StrategyEvaluationItem/StrategyEvaluationItem';
|
||||
import type { IFeatureStrategyPayload } from 'interfaces/strategy';
|
||||
import type { CreateFeatureStrategySchema } from 'openapi';
|
||||
|
||||
const RolloutParameter: FC<{
|
||||
value?: string | number;
|
||||
parameters?: (
|
||||
| IFeatureStrategyPayload
|
||||
| CreateFeatureStrategySchema
|
||||
)['parameters'];
|
||||
hasConstraints?: boolean;
|
||||
displayGroupId?: boolean;
|
||||
}> = ({ value, parameters, hasConstraints, displayGroupId }) => {
|
||||
const percentage = parseParameterNumber(value);
|
||||
|
||||
const explainStickiness =
|
||||
typeof parameters?.stickiness === 'string' &&
|
||||
parameters?.stickiness !== 'default';
|
||||
const stickiness = explainStickiness ? (
|
||||
<>
|
||||
with <strong>{parameters.stickiness}</strong>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
|
||||
return (
|
||||
<StrategyEvaluationItem type='Rollout %'>
|
||||
<StrategyEvaluationChip label={`${percentage}%`} /> of your base{' '}
|
||||
{stickiness}
|
||||
<span>
|
||||
{hasConstraints ? 'who match constraints ' : ' '}
|
||||
is included.
|
||||
</span>
|
||||
{displayGroupId && parameters?.groupId ? (
|
||||
<StrategyEvaluationChip
|
||||
label={`groupId: ${parameters?.groupId}`}
|
||||
/>
|
||||
) : null}
|
||||
</StrategyEvaluationItem>
|
||||
);
|
||||
};
|
||||
import type { FeatureStrategySchema } from 'openapi';
|
||||
import { RolloutParameter } from '../RolloutParameter/RolloutParameter';
|
||||
|
||||
export const useStrategyParameters = (
|
||||
strategy: IFeatureStrategyPayload | CreateFeatureStrategySchema,
|
||||
strategy: Partial<FeatureStrategySchema>,
|
||||
displayGroupId?: boolean,
|
||||
) => {
|
||||
const { constraints } = strategy;
|
||||
const { constraints, variants } = strategy;
|
||||
const { parameters } = strategy;
|
||||
const hasConstraints = Boolean(constraints?.length);
|
||||
const parameterKeys = parameters ? Object.keys(parameters) : [];
|
||||
const mapPredefinedStrategies = (key: string) => {
|
||||
const type = key.toLocaleLowerCase();
|
||||
const value = parameters?.[key] || '';
|
||||
|
||||
if (type === 'rollout') {
|
||||
return (
|
||||
<RolloutParameter
|
||||
key={key}
|
||||
value={parameters?.[key]}
|
||||
value={value}
|
||||
parameters={parameters}
|
||||
hasConstraints={hasConstraints}
|
||||
displayGroupId={displayGroupId}
|
||||
variants={variants}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -75,7 +34,7 @@ export const useStrategyParameters = (
|
||||
<StrategyEvaluationItem
|
||||
key={key}
|
||||
type={key}
|
||||
values={parseParameterStrings(parameters?.[key])}
|
||||
values={parseParameterStrings(value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -88,7 +47,7 @@ export const useStrategyParameters = (
|
||||
[
|
||||
...parameterKeys.map(mapPredefinedStrategies),
|
||||
strategy.name === 'default' ? (
|
||||
<RolloutParameter value={100} />
|
||||
<RolloutParameter value='100' />
|
||||
) : null,
|
||||
].filter(Boolean),
|
||||
[parameters, hasConstraints, displayGroupId],
|
||||
|
@ -1,8 +1,6 @@
|
||||
import type { DragEventHandler, FC, ReactNode } from 'react';
|
||||
import type { IFeatureStrategy } from 'interfaces/strategy';
|
||||
import { StrategyExecution } from './StrategyExecution/StrategyExecution';
|
||||
import SplitPreviewSlider from 'component/feature/StrategyTypes/SplitPreviewSlider/SplitPreviewSlider';
|
||||
import { Box } from '@mui/material';
|
||||
import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer';
|
||||
|
||||
type StrategyItemProps = {
|
||||
@ -29,16 +27,6 @@ export const StrategyItem: FC<StrategyItemProps> = ({
|
||||
headerItemsRight={headerItemsRight}
|
||||
>
|
||||
<StrategyExecution strategy={strategy} />
|
||||
|
||||
{strategy.variants &&
|
||||
strategy.variants.length > 0 &&
|
||||
(strategy.disabled ? (
|
||||
<Box sx={{ opacity: '0.5' }}>
|
||||
<SplitPreviewSlider variants={strategy.variants} />
|
||||
</Box>
|
||||
) : (
|
||||
<SplitPreviewSlider variants={strategy.variants} />
|
||||
))}
|
||||
</StrategyItemContainer>
|
||||
);
|
||||
};
|
||||
|
@ -149,7 +149,7 @@ const payloadOptions = [
|
||||
{ key: 'number', label: 'number' },
|
||||
];
|
||||
|
||||
const EMPTY_PAYLOAD = { type: 'string', value: '' };
|
||||
const EMPTY_PAYLOAD = { type: 'string' as const, value: '' };
|
||||
|
||||
enum ErrorField {
|
||||
NAME = 'name',
|
||||
@ -438,7 +438,7 @@ export const VariantForm = ({
|
||||
clearError(ErrorField.PAYLOAD);
|
||||
setPayload((payload) => ({
|
||||
...payload,
|
||||
type: e.target.value,
|
||||
type: e.target.value as typeof payload.type,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
|
@ -48,6 +48,7 @@ const StyledVariantBoxContainer = styled(Box)(() => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginLeft: 'auto',
|
||||
flexWrap: 'wrap',
|
||||
}));
|
||||
|
||||
const StyledVariantBox = styled(Box, {
|
||||
|
@ -19,7 +19,7 @@ test('should render variants', async () => {
|
||||
weight: 1000,
|
||||
weightType: 'variable' as const,
|
||||
payload: {
|
||||
type: 'string',
|
||||
type: 'string' as const,
|
||||
value: 'variantValue',
|
||||
},
|
||||
},
|
||||
|
@ -99,6 +99,9 @@ export interface IFeatureEnvironmentWithCrEnabled extends IFeatureEnvironment {
|
||||
crEnabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `StrategyVariantSchema` from openapi
|
||||
*/
|
||||
export interface IFeatureVariant {
|
||||
name: string;
|
||||
stickiness: string;
|
||||
@ -114,7 +117,7 @@ export interface IOverride {
|
||||
}
|
||||
|
||||
export interface IPayload {
|
||||
type: string;
|
||||
type: 'string' | 'number' | 'json' | 'csv';
|
||||
value: string;
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,9 @@ export interface IFeatureStrategyParameters {
|
||||
[key: string]: string | number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `FeatureStrategySchema` from openapi
|
||||
*/
|
||||
export interface IFeatureStrategyPayload {
|
||||
id?: string;
|
||||
name?: string;
|
||||
|
Loading…
Reference in New Issue
Block a user