mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	refactor: fix small issues around custom strategies (#1181)
* refactor: validate strategy name on blur * refactor: remove strategy parameter type text in favor of docs * refactor: improve pie chart rendering * refactor: show icons for all feature strategies * refactor: fix list parameter add button style
This commit is contained in:
		
							parent
							
								
									79aa2d4ebe
								
							
						
					
					
						commit
						710ebe08b3
					
				| @ -1,51 +1,45 @@ | |||||||
| import { useTheme } from '@mui/material'; | import { useTheme } from '@mui/material'; | ||||||
|  | import { CSSProperties } from 'react'; | ||||||
| 
 | 
 | ||||||
| interface IPercentageCircleProps { | interface IPercentageCircleProps { | ||||||
|     styles?: object; |  | ||||||
|     percentage: number; |     percentage: number; | ||||||
|     secondaryPieColor?: string; |     size?: `${number}rem`; | ||||||
|     className?: string; |  | ||||||
|     hideNumber?: boolean; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const PercentageCircle = ({ | const PercentageCircle = ({ | ||||||
|     styles, |  | ||||||
|     percentage, |     percentage, | ||||||
|     secondaryPieColor, |     size = '4rem', | ||||||
|     hideNumber, |  | ||||||
|     ...rest |  | ||||||
| }: IPercentageCircleProps) => { | }: IPercentageCircleProps) => { | ||||||
|     const theme = useTheme(); |     const theme = useTheme(); | ||||||
| 
 | 
 | ||||||
|     let circle = { |     const style: CSSProperties = { | ||||||
|         height: '65px', |         display: 'block', | ||||||
|         width: '65px', |         borderRadius: '100%', | ||||||
|         borderRadius: '50%', |         transform: 'rotate(-90deg)', | ||||||
|         color: '#fff', |         height: size, | ||||||
|         backgroundColor: theme.palette.grey[200], |         width: size, | ||||||
|         backgroundImage: `conic-gradient(${ |         background: theme.palette.grey[200], | ||||||
|             theme.palette.primary.light |  | ||||||
|         } ${percentage}%, ${secondaryPieColor || theme.palette.grey[200]} 1%)`,
 |  | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     if (percentage === 100) { |     // The percentage circle used to be drawn by CSS with a conic-gradient,
 | ||||||
|         return ( |     // but the result was either jagged or blurry. SVG seems to look better.
 | ||||||
|             <div |     // See https://stackoverflow.com/a/70659532.
 | ||||||
|                 style={{ |     const r = 100 / (2 * Math.PI); | ||||||
|                     ...circle, |     const d = 2 * r; | ||||||
|                     ...styles, |  | ||||||
|                     display: 'flex', |  | ||||||
|                     justifyContent: 'center', |  | ||||||
|                     alignItems: 'center', |  | ||||||
|                     fontSize: '12px', |  | ||||||
|                 }} |  | ||||||
|             > |  | ||||||
|                 {hideNumber ? null : '100%'} |  | ||||||
|             </div> |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     return <div style={{ ...circle, ...styles }} {...rest} />; |     return ( | ||||||
|  |         <svg viewBox={`0 0 ${d} ${d}`} style={style} aria-hidden> | ||||||
|  |             <circle | ||||||
|  |                 r={r} | ||||||
|  |                 cx={r} | ||||||
|  |                 cy={r} | ||||||
|  |                 fill="none" | ||||||
|  |                 stroke={theme.palette.primary.light} | ||||||
|  |                 strokeWidth={d} | ||||||
|  |                 strokeDasharray={`${percentage} 100`} | ||||||
|  |             /> | ||||||
|  |         </svg> | ||||||
|  |     ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default PercentageCircle; | export default PercentageCircle; | ||||||
|  | |||||||
| @ -13,24 +13,17 @@ export const FeatureStrategyIcons = ({ | |||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const strategyNames = strategies.map(strategy => strategy.name); |  | ||||||
|     const uniqueStrategyNames = uniqueValues(strategyNames); |  | ||||||
| 
 |  | ||||||
|     return ( |     return ( | ||||||
|         <StyledList aria-label="Feature strategies"> |         <StyledList aria-label="Feature strategies"> | ||||||
|             {uniqueStrategyNames.map(strategyName => ( |             {strategies.map(strategy => ( | ||||||
|                 <StyledListItem key={strategyName}> |                 <StyledListItem key={strategy.name}> | ||||||
|                     <FeatureStrategyIcon strategyName={strategyName} /> |                     <FeatureStrategyIcon strategyName={strategy.name} /> | ||||||
|                 </StyledListItem> |                 </StyledListItem> | ||||||
|             ))} |             ))} | ||||||
|         </StyledList> |         </StyledList> | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const uniqueValues = <T,>(values: T[]): T[] => { |  | ||||||
|     return [...new Set(values)]; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const StyledList = styled('ul')(() => ({ | const StyledList = styled('ul')(() => ({ | ||||||
|     all: 'unset', |     all: 'unset', | ||||||
|     display: 'flex', |     display: 'flex', | ||||||
|  | |||||||
| @ -43,29 +43,25 @@ export const StrategyExecution = ({ strategy }: IStrategyExecutionProps) => { | |||||||
|             switch (key) { |             switch (key) { | ||||||
|                 case 'rollout': |                 case 'rollout': | ||||||
|                 case 'Rollout': |                 case 'Rollout': | ||||||
|  |                     const percentage = parseParameterNumber(parameters[key]); | ||||||
|                     return ( |                     return ( | ||||||
|                         <Box |                         <Box | ||||||
|                             className={styles.summary} |                             className={styles.summary} | ||||||
|                             key={key} |                             key={key} | ||||||
|                             sx={{ display: 'flex', alignItems: 'center' }} |                             sx={{ display: 'flex', alignItems: 'center' }} | ||||||
|                         > |                         > | ||||||
|  |                             <Box sx={{ mr: '1rem' }}> | ||||||
|                                 <PercentageCircle |                                 <PercentageCircle | ||||||
|                                 hideNumber |                                     percentage={percentage} | ||||||
|                                 percentage={parseParameterNumber( |                                     size="2rem" | ||||||
|                                     parameters[key] |  | ||||||
|                                 )} |  | ||||||
|                                 styles={{ |  | ||||||
|                                     width: '2rem', |  | ||||||
|                                     height: '2rem', |  | ||||||
|                                     marginRight: '1rem', |  | ||||||
|                                 }} |  | ||||||
|                                 /> |                                 /> | ||||||
|  |                             </Box> | ||||||
|                             <div> |                             <div> | ||||||
|                                 <Chip |                                 <Chip | ||||||
|                                     color="success" |                                     color="success" | ||||||
|                                     variant="outlined" |                                     variant="outlined" | ||||||
|                                     size="small" |                                     size="small" | ||||||
|                                     label={`${parameters[key]}%`} |                                     label={`${percentage}%`} | ||||||
|                                 />{' '} |                                 />{' '} | ||||||
|                                 of your base{' '} |                                 of your base{' '} | ||||||
|                                 {constraints.length > 0 |                                 {constraints.length > 0 | ||||||
|  | |||||||
| @ -23,6 +23,7 @@ export const useStyles = makeStyles()(theme => ({ | |||||||
|         maxWidth: '270px', |         maxWidth: '270px', | ||||||
|         marginTop: '0.25rem', |         marginTop: '0.25rem', | ||||||
|         fontSize: theme.fontSizes.smallBody, |         fontSize: theme.fontSizes.smallBody, | ||||||
|  |         textAlign: 'right', | ||||||
|         [theme.breakpoints.down(700)]: { |         [theme.breakpoints.down(700)]: { | ||||||
|             display: 'none', |             display: 'none', | ||||||
|         }, |         }, | ||||||
| @ -33,7 +34,7 @@ export const useStyles = makeStyles()(theme => ({ | |||||||
|         fontSize: theme.fontSizes.subHeader, |         fontSize: theme.fontSizes.subHeader, | ||||||
|     }, |     }, | ||||||
|     percentageCircle: { |     percentageCircle: { | ||||||
|         transform: 'scale(0.85)', |         margin: '0 1rem', | ||||||
|         [theme.breakpoints.down(500)]: { |         [theme.breakpoints.down(500)]: { | ||||||
|             display: 'none', |             display: 'none', | ||||||
|         }, |         }, | ||||||
|  | |||||||
| @ -73,11 +73,9 @@ const FeatureOverviewEnvironmentMetrics = ({ | |||||||
|                     hour |                     hour | ||||||
|                 </p> |                 </p> | ||||||
|             </div> |             </div> | ||||||
|             <PercentageCircle |             <div className={styles.percentageCircle} data-loading> | ||||||
|                 className={styles.percentageCircle} |                 <PercentageCircle percentage={percentage} size="3rem" /> | ||||||
|                 percentage={percentage} |             </div> | ||||||
|                 data-loading |  | ||||||
|             /> |  | ||||||
|         </div> |         </div> | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -97,7 +97,13 @@ const StrategyInputList = ({ | |||||||
|             <ConditionallyRender |             <ConditionallyRender | ||||||
|                 condition={!disabled} |                 condition={!disabled} | ||||||
|                 show={ |                 show={ | ||||||
|                     <div style={{ display: 'flex', alignItems: 'center' }}> |                     <div | ||||||
|  |                         style={{ | ||||||
|  |                             display: 'flex', | ||||||
|  |                             alignItems: 'center', | ||||||
|  |                             gap: '1rem', | ||||||
|  |                         }} | ||||||
|  |                     > | ||||||
|                         <TextField |                         <TextField | ||||||
|                             name={`input_field`} |                             name={`input_field`} | ||||||
|                             variant="outlined" |                             variant="outlined" | ||||||
| @ -116,6 +122,7 @@ const StrategyInputList = ({ | |||||||
|                         <Button |                         <Button | ||||||
|                             onClick={setValue} |                             onClick={setValue} | ||||||
|                             data-testid={ADD_TO_STRATEGY_INPUT_LIST} |                             data-testid={ADD_TO_STRATEGY_INPUT_LIST} | ||||||
|  |                             variant="outlined" | ||||||
|                             color="secondary" |                             color="secondary" | ||||||
|                             startIcon={<Add />} |                             startIcon={<Add />} | ||||||
|                         > |                         > | ||||||
|  | |||||||
| @ -83,6 +83,7 @@ export const CreateStrategy = () => { | |||||||
|                 handleCancel={handleCancel} |                 handleCancel={handleCancel} | ||||||
|                 strategyName={strategyName} |                 strategyName={strategyName} | ||||||
|                 setStrategyName={setStrategyName} |                 setStrategyName={setStrategyName} | ||||||
|  |                 validateStrategyName={validateStrategyName} | ||||||
|                 strategyDesc={strategyDesc} |                 strategyDesc={strategyDesc} | ||||||
|                 setStrategyDesc={setStrategyDesc} |                 setStrategyDesc={setStrategyDesc} | ||||||
|                 params={params} |                 params={params} | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ interface IStrategyFormProps { | |||||||
|     strategyDesc: string; |     strategyDesc: string; | ||||||
|     params: IStrategyParameter[]; |     params: IStrategyParameter[]; | ||||||
|     setStrategyName: React.Dispatch<React.SetStateAction<string>>; |     setStrategyName: React.Dispatch<React.SetStateAction<string>>; | ||||||
|  |     validateStrategyName?: () => void; | ||||||
|     setStrategyDesc: React.Dispatch<React.SetStateAction<string>>; |     setStrategyDesc: React.Dispatch<React.SetStateAction<string>>; | ||||||
|     setParams: React.Dispatch<React.SetStateAction<IStrategyParameter[]>>; |     setParams: React.Dispatch<React.SetStateAction<IStrategyParameter[]>>; | ||||||
|     handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void; |     handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void; | ||||||
| @ -31,6 +32,7 @@ export const StrategyForm: React.FC<IStrategyFormProps> = ({ | |||||||
|     params, |     params, | ||||||
|     setParams, |     setParams, | ||||||
|     setStrategyName, |     setStrategyName, | ||||||
|  |     validateStrategyName, | ||||||
|     setStrategyDesc, |     setStrategyDesc, | ||||||
|     errors, |     errors, | ||||||
|     mode, |     mode, | ||||||
| @ -65,7 +67,8 @@ export const StrategyForm: React.FC<IStrategyFormProps> = ({ | |||||||
|                     onChange={e => setStrategyName(trim(e.target.value))} |                     onChange={e => setStrategyName(trim(e.target.value))} | ||||||
|                     error={Boolean(errors.name)} |                     error={Boolean(errors.name)} | ||||||
|                     errorText={errors.name} |                     errorText={errors.name} | ||||||
|                     onFocus={() => clearErrors()} |                     onFocus={clearErrors} | ||||||
|  |                     onBlur={validateStrategyName} | ||||||
|                 /> |                 /> | ||||||
|                 <p className={styles.inputDescription}> |                 <p className={styles.inputDescription}> | ||||||
|                     What is your strategy description? |                     What is your strategy description? | ||||||
|  | |||||||
| @ -29,12 +29,6 @@ export const useStyles = makeStyles()(theme => ({ | |||||||
|     inputDescription: { |     inputDescription: { | ||||||
|         marginBottom: '0.5rem', |         marginBottom: '0.5rem', | ||||||
|     }, |     }, | ||||||
|     typeDescription: { |  | ||||||
|         fontSize: theme.fontSizes.smallBody, |  | ||||||
|         color: theme.palette.grey[600], |  | ||||||
|         top: '-13px', |  | ||||||
|         position: 'relative', |  | ||||||
|     }, |  | ||||||
|     errorMessage: { |     errorMessage: { | ||||||
|         fontSize: theme.fontSizes.smallBody, |         fontSize: theme.fontSizes.smallBody, | ||||||
|         color: theme.palette.error.main, |         color: theme.palette.error.main, | ||||||
|  | |||||||
| @ -17,31 +17,22 @@ const paramTypesOptions = [ | |||||||
|     { |     { | ||||||
|         key: 'string', |         key: 'string', | ||||||
|         label: 'string', |         label: 'string', | ||||||
|         description: 'A string is a collection of characters', |  | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|         key: 'percentage', |         key: 'percentage', | ||||||
|         label: 'percentage', |         label: 'percentage', | ||||||
|         description: |  | ||||||
|             'Percentage is used when you want to make your feature visible to a process part of your customers', |  | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|         key: 'list', |         key: 'list', | ||||||
|         label: 'list', |         label: 'list', | ||||||
|         description: |  | ||||||
|             'A list is used when you want to define several parameters that must be met before your feature becomes visible to your customers', |  | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|         key: 'number', |         key: 'number', | ||||||
|         label: 'number', |         label: 'number', | ||||||
|         description: |  | ||||||
|             'Number is used when you have one or more digits that must be met for your feature to be visible to your customers', |  | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|         key: 'boolean', |         key: 'boolean', | ||||||
|         label: 'boolean', |         label: 'boolean', | ||||||
|         description: |  | ||||||
|             'A boolean value represents a truth value, which is either true or false', |  | ||||||
|     }, |     }, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| @ -68,11 +59,6 @@ export const StrategyParameter = ({ | |||||||
|         set({ type }); |         set({ type }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const renderParamTypeDescription = () => { |  | ||||||
|         return paramTypesOptions.find(param => param.key === input.type) |  | ||||||
|             ?.description; |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     return ( |     return ( | ||||||
|         <div className={styles.paramsContainer}> |         <div className={styles.paramsContainer}> | ||||||
|             <Divider className={styles.divider} /> |             <Divider className={styles.divider} /> | ||||||
| @ -80,7 +66,16 @@ export const StrategyParameter = ({ | |||||||
|                 condition={index === 0} |                 condition={index === 0} | ||||||
|                 show={ |                 show={ | ||||||
|                     <p className={styles.input}> |                     <p className={styles.input}> | ||||||
|                         The parameters define what the strategy will look like. |                         Parameters let you provide arguments to your strategy | ||||||
|  |                         that it can access for evaluation. Read more in the{' '} | ||||||
|  |                         <a | ||||||
|  |                             href="https://docs.getunleash.io/advanced/custom_activation_strategy#parameter-types" | ||||||
|  |                             target="_blank" | ||||||
|  |                             rel="noreferrer" | ||||||
|  |                         > | ||||||
|  |                             parameter types documentation | ||||||
|  |                         </a> | ||||||
|  |                         . | ||||||
|                     </p> |                     </p> | ||||||
|                 } |                 } | ||||||
|             /> |             /> | ||||||
| @ -114,9 +109,6 @@ export const StrategyParameter = ({ | |||||||
|                 id={`prop-type-${index}-select`} |                 id={`prop-type-${index}-select`} | ||||||
|                 className={styles.input} |                 className={styles.input} | ||||||
|             /> |             /> | ||||||
|             <p className={styles.typeDescription}> |  | ||||||
|                 {renderParamTypeDescription()} |  | ||||||
|             </p> |  | ||||||
|             <Input |             <Input | ||||||
|                 rows={2} |                 rows={2} | ||||||
|                 multiline |                 multiline | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user