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 FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||||
import type { IReleasePlanMilestoneStrategy } from 'interfaces/releasePlans';
|
import type { IReleasePlanMilestoneStrategy } from 'interfaces/releasePlans';
|
||||||
import type { ISegment } from 'interfaces/segment';
|
import type { ISegment } from 'interfaces/segment';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { BuiltInStrategies, formatStrategyName } from 'utils/strategyNames';
|
import { BuiltInStrategies, formatStrategyName } from 'utils/strategyNames';
|
||||||
import { MilestoneStrategyTitle } from './MilestoneStrategyTitle';
|
import { MilestoneStrategyTitle } from './MilestoneStrategyTitle';
|
||||||
import { MilestoneStrategyType } from './MilestoneStrategyType';
|
import { MilestoneStrategyType } from './MilestoneStrategyType';
|
||||||
@ -22,6 +22,7 @@ import { useFormErrors } from 'hooks/useFormErrors';
|
|||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import { MilestoneStrategySegment } from './MilestoneStrategySegment';
|
import { MilestoneStrategySegment } from './MilestoneStrategySegment';
|
||||||
import { MilestoneStrategyConstraints } from './MilestoneStrategyConstraints';
|
import { MilestoneStrategyConstraints } from './MilestoneStrategyConstraints';
|
||||||
|
import { MilestoneStrategyVariants } from './MilestoneStrategyVariants';
|
||||||
import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValidation/useConstraintsValidation';
|
import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValidation/useConstraintsValidation';
|
||||||
|
|
||||||
const StyledCancelButton = styled(Button)(({ theme }) => ({
|
const StyledCancelButton = styled(Button)(({ theme }) => ({
|
||||||
@ -138,6 +139,28 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
const { strategyDefinition } = useStrategy(strategy?.name);
|
const { strategyDefinition } = useStrategy(strategy?.name);
|
||||||
const hasValidConstraints = useConstraintsValidation(strategy?.constraints);
|
const hasValidConstraints = useConstraintsValidation(strategy?.constraints);
|
||||||
const errors = useFormErrors();
|
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) {
|
if (!strategy || !addStrategy || !strategyDefinition) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -153,6 +176,7 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
return constraintCount + segmentCount;
|
return constraintCount + segmentCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateParameter = (key: string, value: string) => true;
|
||||||
const updateParameter = (name: string, value: string) => {
|
const updateParameter = (name: string, value: string) => {
|
||||||
setAddStrategy(
|
setAddStrategy(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
@ -165,6 +189,7 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
}
|
}
|
||||||
draft.parameters = draft.parameters ?? {};
|
draft.parameters = draft.parameters ?? {};
|
||||||
draft.parameters[name] = value;
|
draft.parameters[name] = value;
|
||||||
|
validateParameter(name, value);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -220,6 +245,18 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{showVariants && (
|
||||||
|
<Tab
|
||||||
|
label={
|
||||||
|
<Typography>
|
||||||
|
Variants
|
||||||
|
<StyledBadge>
|
||||||
|
{addStrategy?.variants?.length || 0}
|
||||||
|
</StyledBadge>
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</StyledTabs>
|
</StyledTabs>
|
||||||
<StyledContentDiv>
|
<StyledContentDiv>
|
||||||
{activeTab === 0 && (
|
{activeTab === 0 && (
|
||||||
@ -262,6 +299,12 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
</StyledTargetingHeader>
|
</StyledTargetingHeader>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{activeTab === 2 && showVariants && (
|
||||||
|
<MilestoneStrategyVariants
|
||||||
|
strategy={addStrategy}
|
||||||
|
setStrategy={setAddStrategy}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</StyledContentDiv>
|
</StyledContentDiv>
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>
|
||||||
<Button
|
<Button
|
||||||
|
Loading…
Reference in New Issue
Block a user