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 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