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

feat: Strategy variants stickiness (#4250)

This commit is contained in:
Mateusz Kwasniewski 2023-07-17 13:58:54 +02:00 committed by GitHub
parent fb6e4906a7
commit 56d5579b89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 162 additions and 63 deletions

View File

@ -248,7 +248,11 @@ export const FeatureStrategyForm = ({
/> />
<StyledHr /> <StyledHr />
<ConditionallyRender <ConditionallyRender
condition={Boolean(uiConfig?.flags?.strategyVariant)} condition={
Boolean(uiConfig?.flags?.strategyVariant) &&
strategy.parameters != null &&
'stickiness' in strategy.parameters
}
show={ show={
<StrategyVariants <StrategyVariants
strategy={strategy} strategy={strategy}

View File

@ -150,6 +150,7 @@ interface IVariantFormProps {
updateVariant: (updatedVariant: IFeatureVariantEdit) => void; updateVariant: (updatedVariant: IFeatureVariantEdit) => void;
removeVariant: (variantId: string) => void; removeVariant: (variantId: string) => void;
error?: string; error?: string;
disableOverrides?: boolean;
} }
export const VariantForm = ({ export const VariantForm = ({
@ -158,6 +159,7 @@ export const VariantForm = ({
updateVariant, updateVariant,
removeVariant, removeVariant,
error, error,
disableOverrides = false,
}: IVariantFormProps) => { }: IVariantFormProps) => {
const [name, setName] = useState(variant.name); const [name, setName] = useState(variant.name);
const [customPercentage, setCustomPercentage] = useState( const [customPercentage, setCustomPercentage] = useState(
@ -168,8 +170,9 @@ export const VariantForm = ({
variant.payload || EMPTY_PAYLOAD variant.payload || EMPTY_PAYLOAD
); );
const [overrides, overridesDispatch] = useOverrides( const [overrides, overridesDispatch] = useOverrides(
variant.overrides || [] 'overrides' in variant ? variant.overrides || [] : []
); );
const { context } = useUnleashContext(); const { context } = useUnleashContext();
const [errors, setErrors] = useState<IVariantFormErrors>({}); const [errors, setErrors] = useState<IVariantFormErrors>({});
@ -269,7 +272,7 @@ export const VariantForm = ({
}; };
useEffect(() => { useEffect(() => {
updateVariant({ const newVariant: IFeatureVariantEdit = {
...variant, ...variant,
name, name,
weight: Number(customPercentage ? percentage : 100) * 10, weight: Number(customPercentage ? percentage : 100) * 10,
@ -277,19 +280,22 @@ export const VariantForm = ({
stickiness: stickiness:
variants?.length > 0 ? variants[0].stickiness : 'default', variants?.length > 0 ? variants[0].stickiness : 'default',
payload: payload.value ? payload : undefined, payload: payload.value ? payload : undefined,
overrides: overrides
.map(o => ({
contextName: o.contextName,
values: o.values,
}))
.filter(o => o.values && o.values.length > 0),
isValid: isValid:
isNameNotEmpty(name) && isNameNotEmpty(name) &&
isNameUnique(name, variant.id) && isNameUnique(name, variant.id) &&
isValidPercentage(percentage) && isValidPercentage(percentage) &&
isValidPayload(payload) && isValidPayload(payload) &&
!error, !error,
}); };
if (!disableOverrides) {
newVariant['overrides'] = overrides
.map(o => ({
contextName: o.contextName,
values: o.values,
}))
.filter(o => o.values && o.values.length > 0);
}
updateVariant(newVariant);
}, [name, customPercentage, percentage, payload, overrides]); }, [name, customPercentage, percentage, payload, overrides]);
useEffect(() => { useEffect(() => {
@ -423,24 +429,28 @@ export const VariantForm = ({
/> />
</StyledFieldColumn> </StyledFieldColumn>
</StyledRow> </StyledRow>
<StyledMarginLabel> {!disableOverrides ? (
Overrides <>
<HelpIcon tooltip="Here you can specify which users should get this variant." /> <StyledMarginLabel>
</StyledMarginLabel> Overrides
<OverrideConfig <HelpIcon tooltip="Here you can specify which users should get this variant." />
overrides={overrides} </StyledMarginLabel>
overridesDispatch={overridesDispatch} <OverrideConfig
/> overrides={overrides}
<div> overridesDispatch={overridesDispatch}
<StyledAddOverrideButton />
onClick={onAddOverride} <div>
variant="text" <StyledAddOverrideButton
color="primary" onClick={onAddOverride}
data-testid="VARIANT_ADD_OVERRIDE_BUTTON" variant="text"
> color="primary"
Add override data-testid="VARIANT_ADD_OVERRIDE_BUTTON"
</StyledAddOverrideButton> >
</div> Add override
</StyledAddOverrideButton>
</div>
</>
) : null}
</StyledVariantForm> </StyledVariantForm>
); );
}; };

View File

@ -0,0 +1,96 @@
import { screen, waitFor } from '@testing-library/react';
import { render } from 'utils/testRenderer';
import { StrategyVariants } from './StrategyVariants';
import { Route, Routes } from 'react-router-dom';
import { UPDATE_FEATURE_ENVIRONMENT_VARIANTS } from '../../providers/AccessProvider/permissions';
import { IFeatureStrategy } from '../../../interfaces/strategy';
import React, { useState } from 'react';
test('should render variants', async () => {
let currentStrategy: Partial<IFeatureStrategy> = {};
const initialStrategy = {
name: '',
constraints: [],
parameters: { stickiness: 'clientId' },
variants: [
{
name: 'variantName',
stickiness: 'default',
weight: 1000,
weightType: 'variable',
payload: {
type: 'string',
value: 'variantValue',
},
},
],
};
const Parent = () => {
const [strategy, setStrategy] =
useState<Partial<IFeatureStrategy>>(initialStrategy);
currentStrategy = strategy;
return (
<StrategyVariants strategy={strategy} setStrategy={setStrategy} />
);
};
render(
<Routes>
<Route
path={'projects/:projectId/features/:featureId/strategies/edit'}
element={<Parent />}
/>
</Routes>,
{
route: 'projects/default/features/colors/strategies/edit?environmentId=development&strategyId=2e4f0555-518b-45b3-b0cd-a32cca388a92',
permissions: [
{
permission: UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
project: 'default',
environment: 'development',
},
],
}
);
// static form info
await screen.findByText('Variants');
const button = await screen.findByText('Add variant');
// variant form populated
const variantInput = screen.getByDisplayValue('variantValue');
expect(variantInput).toBeInTheDocument();
// overrides disabled
expect(screen.queryByText('Overrides')).not.toBeInTheDocument();
// add second variant
button.click();
// UI allows to adjust percentages
const matchedElements = screen.getAllByText('Custom percentage');
expect(matchedElements.length).toBe(2);
// correct variants set on the parent
await waitFor(() => {
expect(currentStrategy).toMatchObject({
...initialStrategy,
variants: [
{
name: 'variantName',
payload: { type: 'string', value: 'variantValue' },
stickiness: 'clientId',
weight: 500,
weightType: 'variable',
},
{
name: '',
stickiness: 'clientId',
weight: 500,
weightType: 'variable',
},
],
});
});
});

View File

@ -7,8 +7,7 @@ import { UPDATE_FEATURE_ENVIRONMENT_VARIANTS } from '../../providers/AccessProvi
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { WeightType } from '../../../constants/variantTypes'; import { WeightType } from '../../../constants/variantTypes';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useDefaultProjectSettings } from 'hooks/useDefaultProjectSettings'; import { styled, Typography } from '@mui/material';
import { styled } from '@mui/material';
import { useRequiredQueryParam } from 'hooks/useRequiredQueryParam'; import { useRequiredQueryParam } from 'hooks/useRequiredQueryParam';
import { IFeatureStrategy } from 'interfaces/strategy'; import { IFeatureStrategy } from 'interfaces/strategy';
@ -26,8 +25,10 @@ export const StrategyVariants: FC<{
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const environment = useRequiredQueryParam('environmentId'); const environment = useRequiredQueryParam('environmentId');
const [variantsEdit, setVariantsEdit] = useState<IFeatureVariantEdit[]>([]); const [variantsEdit, setVariantsEdit] = useState<IFeatureVariantEdit[]>([]);
const [newVariant, setNewVariant] = useState<string>(); const stickiness =
const { defaultStickiness, loading } = useDefaultProjectSettings(projectId); strategy?.parameters && 'stickiness' in strategy?.parameters
? String(strategy.parameters.stickiness)
: 'default';
useEffect(() => { useEffect(() => {
setVariantsEdit( setVariantsEdit(
@ -41,19 +42,6 @@ export const StrategyVariants: FC<{
); );
}, []); }, []);
useEffect(() => {
setStrategy(prev => ({
...prev,
variants: variantsEdit.map(variant => ({
name: variant.name,
weight: variant.weight,
stickiness: variant.stickiness,
payload: variant.payload,
weightType: variant.weightType,
})),
}));
}, [JSON.stringify(variantsEdit)]);
const updateVariant = (updatedVariant: IFeatureVariantEdit, id: string) => { const updateVariant = (updatedVariant: IFeatureVariantEdit, id: string) => {
setVariantsEdit(prevVariants => setVariantsEdit(prevVariants =>
updateWeightEdit( updateWeightEdit(
@ -63,6 +51,16 @@ export const StrategyVariants: FC<{
1000 1000
) )
); );
setStrategy(prev => ({
...prev,
variants: variantsEdit.map(variant => ({
name: variant.name,
weight: variant.weight,
stickiness,
payload: variant.payload,
weightType: variant.weightType,
})),
}));
}; };
const addVariant = () => { const addVariant = () => {
@ -73,24 +71,23 @@ export const StrategyVariants: FC<{
name: '', name: '',
weightType: WeightType.VARIABLE, weightType: WeightType.VARIABLE,
weight: 0, weight: 0,
overrides: [], stickiness,
stickiness:
variantsEdit?.length > 0
? variantsEdit[0].stickiness
: defaultStickiness,
new: true, new: true,
isValid: false, isValid: false,
id, id,
}, },
]); ]);
setNewVariant(id);
}; };
return ( return (
<> <>
<Typography component="h3" sx={{ m: 0 }} variant="h3">
Variants
</Typography>
<StyledVariantForms> <StyledVariantForms>
{variantsEdit.map(variant => ( {variantsEdit.map(variant => (
<VariantForm <VariantForm
disableOverrides={true}
key={variant.id} key={variant.id}
variant={variant} variant={variant}
variants={variantsEdit} variants={variantsEdit}

View File

@ -53,15 +53,7 @@ export interface IFeatureVariant {
stickiness: string; stickiness: string;
weight: number; weight: number;
weightType: string; weightType: string;
overrides: IOverride[]; overrides?: IOverride[];
payload?: IPayload;
}
export interface IFeatureStrategyVariant {
name: string;
stickiness: string;
weight: number;
weightType: string;
payload?: IPayload; payload?: IPayload;
} }

View File

@ -1,5 +1,5 @@
import { Operator } from 'constants/operators'; import { Operator } from 'constants/operators';
import { IFeatureStrategyVariant } from './featureToggle'; import { IFeatureVariant } from './featureToggle';
export interface IFeatureStrategy { export interface IFeatureStrategy {
id: string; id: string;
@ -8,7 +8,7 @@ export interface IFeatureStrategy {
title?: string; title?: string;
constraints: IConstraint[]; constraints: IConstraint[];
parameters: IFeatureStrategyParameters; parameters: IFeatureStrategyParameters;
variants?: IFeatureStrategyVariant[]; variants?: IFeatureVariant[];
featureName?: string; featureName?: string;
projectId?: string; projectId?: string;
environment?: string; environment?: string;
@ -26,7 +26,7 @@ export interface IFeatureStrategyPayload {
title?: string; title?: string;
constraints: IConstraint[]; constraints: IConstraint[];
parameters: IFeatureStrategyParameters; parameters: IFeatureStrategyParameters;
variants?: IFeatureStrategyVariant[]; variants?: IFeatureVariant[];
segments?: number[]; segments?: number[];
disabled?: boolean; disabled?: boolean;
} }