mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +01:00
feat: add variants to release plan template strategies (#8870)
This commit is contained in:
parent
f629773fef
commit
9044d4c537
@ -0,0 +1,189 @@
|
||||
import type { IReleasePlanMilestoneStrategy } from 'interfaces/releasePlans';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Box, styled, Typography, Button } from '@mui/material';
|
||||
import { HelpIcon } from '../../common/HelpIcon/HelpIcon';
|
||||
import { StrategyVariantsUpgradeAlert } from 'component/common/StrategyVariantsUpgradeAlert/StrategyVariantsUpgradeAlert';
|
||||
import { VariantForm } from 'component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/VariantForm/VariantForm';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { IFeatureVariantEdit } from 'component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/EnvironmentVariantsModal';
|
||||
import { updateWeightEdit } from 'component/common/util';
|
||||
import { WeightType } from 'constants/variantTypes';
|
||||
import { useTheme } from '@mui/material';
|
||||
import Add from '@mui/icons-material/Add';
|
||||
import SplitPreviewSlider from 'component/feature/StrategyTypes/SplitPreviewSlider/SplitPreviewSlider';
|
||||
|
||||
const StyledVariantForms = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
const StyledHelpIconBox = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledVariantsHeader = styled('div')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
marginTop: theme.spacing(1.5),
|
||||
}));
|
||||
|
||||
interface IMilestoneStrategyVariantsProps {
|
||||
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>;
|
||||
setStrategy: React.Dispatch<
|
||||
React.SetStateAction<Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>>
|
||||
>;
|
||||
}
|
||||
|
||||
export const MilestoneStrategyVariants = ({
|
||||
strategy,
|
||||
setStrategy,
|
||||
}: IMilestoneStrategyVariantsProps) => {
|
||||
const initialVariants = (strategy.variants || []).map((variant) => ({
|
||||
...variant,
|
||||
new: true,
|
||||
isValid: true,
|
||||
id: uuidv4(),
|
||||
overrides: [],
|
||||
}));
|
||||
const [variantsEdit, setVariantsEdit] =
|
||||
useState<IFeatureVariantEdit[]>(initialVariants);
|
||||
|
||||
const stickiness =
|
||||
strategy?.parameters && 'stickiness' in strategy?.parameters
|
||||
? String(strategy.parameters.stickiness)
|
||||
: 'default';
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setStrategy((prev) => ({
|
||||
...prev,
|
||||
variants: variantsEdit.filter((variant) =>
|
||||
Boolean(variant.name),
|
||||
),
|
||||
}));
|
||||
};
|
||||
}, [JSON.stringify(variantsEdit)]);
|
||||
|
||||
useEffect(() => {
|
||||
setStrategy((prev) => ({
|
||||
...prev,
|
||||
variants: variantsEdit.map((variant) => ({
|
||||
stickiness,
|
||||
name: variant.name,
|
||||
weight: variant.weight,
|
||||
payload: variant.payload,
|
||||
weightType: variant.weightType,
|
||||
})),
|
||||
}));
|
||||
}, [stickiness, JSON.stringify(variantsEdit)]);
|
||||
|
||||
const updateVariant = (updatedVariant: IFeatureVariantEdit, id: string) => {
|
||||
setVariantsEdit((prevVariants) =>
|
||||
updateWeightEdit(
|
||||
prevVariants.map((prevVariant) =>
|
||||
prevVariant.id === id ? updatedVariant : prevVariant,
|
||||
),
|
||||
1000,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const addVariant = () => {
|
||||
const id = uuidv4();
|
||||
setVariantsEdit((variantsEdit) => [
|
||||
...variantsEdit,
|
||||
{
|
||||
name: '',
|
||||
weightType: WeightType.VARIABLE,
|
||||
weight: 0,
|
||||
stickiness,
|
||||
new: true,
|
||||
isValid: false,
|
||||
id,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const variantWeightsError =
|
||||
variantsEdit.reduce(
|
||||
(acc, variant) => acc + (variant.weight || 0),
|
||||
0,
|
||||
) !== 1000;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledVariantsHeader>
|
||||
Variants enhance a feature flag by providing a version of the
|
||||
feature to be enabled
|
||||
</StyledVariantsHeader>
|
||||
<StyledHelpIconBox>
|
||||
<Typography>Variants</Typography>
|
||||
<HelpIcon
|
||||
htmlTooltip
|
||||
tooltip={
|
||||
<Box>
|
||||
<Typography variant='body2'>
|
||||
Variants in feature toggling allow you to serve
|
||||
different versions of a feature to different
|
||||
users. This can be used for A/B testing, gradual
|
||||
rollouts, and canary releases. Variants provide
|
||||
a way to control the user experience at a
|
||||
granular level, enabling you to test and
|
||||
optimize different aspects of your features.
|
||||
Read more about variants{' '}
|
||||
<a
|
||||
href='https://docs.getunleash.io/reference/strategy-variants'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</StyledHelpIconBox>
|
||||
<StyledVariantForms>
|
||||
{variantsEdit.length > 0 && <StrategyVariantsUpgradeAlert />}
|
||||
|
||||
{variantsEdit.map((variant, i) => (
|
||||
<VariantForm
|
||||
disableOverrides={true}
|
||||
key={variant.id}
|
||||
variant={variant}
|
||||
variants={variantsEdit}
|
||||
updateVariant={(updatedVariant) =>
|
||||
updateVariant(updatedVariant, variant.id)
|
||||
}
|
||||
removeVariant={() =>
|
||||
setVariantsEdit((variantsEdit) =>
|
||||
updateWeightEdit(
|
||||
variantsEdit.filter(
|
||||
(v) => v.id !== variant.id,
|
||||
),
|
||||
1000,
|
||||
),
|
||||
)
|
||||
}
|
||||
decorationColor={
|
||||
theme.palette.variants[
|
||||
i % theme.palette.variants.length
|
||||
]
|
||||
}
|
||||
weightsError={variantWeightsError}
|
||||
/>
|
||||
))}
|
||||
</StyledVariantForms>
|
||||
<Button onClick={addVariant} variant='outlined' startIcon={<Add />}>
|
||||
Add variant
|
||||
</Button>
|
||||
<SplitPreviewSlider
|
||||
variants={variantsEdit}
|
||||
weightsError={variantWeightsError}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -13,7 +13,7 @@ import { Badge } from 'component/common/Badge/Badge';
|
||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||
import type { IReleasePlanMilestoneStrategy } from 'interfaces/releasePlans';
|
||||
import type { ISegment } from 'interfaces/segment';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BuiltInStrategies, formatStrategyName } from 'utils/strategyNames';
|
||||
import { MilestoneStrategyTitle } from './MilestoneStrategyTitle';
|
||||
import { MilestoneStrategyType } from './MilestoneStrategyType';
|
||||
@ -22,6 +22,7 @@ import { useFormErrors } from 'hooks/useFormErrors';
|
||||
import produce from 'immer';
|
||||
import { MilestoneStrategySegment } from './MilestoneStrategySegment';
|
||||
import { MilestoneStrategyConstraints } from './MilestoneStrategyConstraints';
|
||||
import { MilestoneStrategyVariants } from './MilestoneStrategyVariants';
|
||||
import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValidation/useConstraintsValidation';
|
||||
|
||||
const StyledCancelButton = styled(Button)(({ theme }) => ({
|
||||
@ -138,6 +139,28 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
||||
const { strategyDefinition } = useStrategy(strategy?.name);
|
||||
const hasValidConstraints = useConstraintsValidation(strategy?.constraints);
|
||||
const errors = useFormErrors();
|
||||
const showVariants = Boolean(
|
||||
addStrategy?.parameters && 'stickiness' in addStrategy?.parameters,
|
||||
);
|
||||
|
||||
const stickiness =
|
||||
addStrategy?.parameters && 'stickiness' in addStrategy?.parameters
|
||||
? String(addStrategy.parameters.stickiness)
|
||||
: 'default';
|
||||
|
||||
useEffect(() => {
|
||||
setAddStrategy((prev) => ({
|
||||
...prev,
|
||||
variants: (addStrategy.variants || []).map((variant) => ({
|
||||
stickiness,
|
||||
name: variant.name,
|
||||
weight: variant.weight,
|
||||
payload: variant.payload,
|
||||
weightType: variant.weightType,
|
||||
})),
|
||||
}));
|
||||
}, [stickiness, JSON.stringify(addStrategy.variants)]);
|
||||
|
||||
if (!strategy || !addStrategy || !strategyDefinition) {
|
||||
return null;
|
||||
}
|
||||
@ -153,6 +176,7 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
||||
return constraintCount + segmentCount;
|
||||
};
|
||||
|
||||
const validateParameter = (key: string, value: string) => true;
|
||||
const updateParameter = (name: string, value: string) => {
|
||||
setAddStrategy(
|
||||
produce((draft) => {
|
||||
@ -165,6 +189,7 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
||||
}
|
||||
draft.parameters = draft.parameters ?? {};
|
||||
draft.parameters[name] = value;
|
||||
validateParameter(name, value);
|
||||
}),
|
||||
);
|
||||
};
|
||||
@ -220,6 +245,18 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
{showVariants && (
|
||||
<Tab
|
||||
label={
|
||||
<Typography>
|
||||
Variants
|
||||
<StyledBadge>
|
||||
{addStrategy?.variants?.length || 0}
|
||||
</StyledBadge>
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</StyledTabs>
|
||||
<StyledContentDiv>
|
||||
{activeTab === 0 && (
|
||||
@ -262,6 +299,12 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
||||
</StyledTargetingHeader>
|
||||
</>
|
||||
)}
|
||||
{activeTab === 2 && showVariants && (
|
||||
<MilestoneStrategyVariants
|
||||
strategy={addStrategy}
|
||||
setStrategy={setAddStrategy}
|
||||
/>
|
||||
)}
|
||||
</StyledContentDiv>
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
|
Loading…
Reference in New Issue
Block a user