mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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