mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Feat: (VariantCustomization) Allow user to customize variant weights (#216)
Co-authored-by: Jaynish Buddhdev <bjaynish1@gmail.com>
This commit is contained in:
		
							parent
							
								
									a12c0e32b2
								
							
						
					
					
						commit
						d7ae641274
					
				| @ -1,3 +1,5 @@ | ||||
| import { weightTypes } from '../feature/variant/enums'; | ||||
| 
 | ||||
| const dateTimeOptions = { | ||||
|     day: '2-digit', | ||||
|     month: '2-digit', | ||||
| @ -22,11 +24,37 @@ export const trim = value => { | ||||
| }; | ||||
| 
 | ||||
| export function updateWeight(variants, totalWeight) { | ||||
|     const size = variants.length; | ||||
|     const percentage = parseInt((1 / size) * totalWeight); | ||||
|     const variantMetadata = variants.reduce( | ||||
|         ({ remainingPercentage, variableVariantCount }, variant) => { | ||||
|             if (variant.weight && variant.weightType === weightTypes.FIX) { | ||||
|                 remainingPercentage -= Number(variant.weight); | ||||
|             } else { | ||||
|                 variableVariantCount += 1; | ||||
|             } | ||||
|             return { | ||||
|                 remainingPercentage, | ||||
|                 variableVariantCount, | ||||
|             }; | ||||
|         }, | ||||
|         { remainingPercentage: totalWeight, variableVariantCount: 0 } | ||||
|     ); | ||||
| 
 | ||||
|     variants.forEach(v => { | ||||
|         v.weight = percentage; | ||||
|     const { remainingPercentage, variableVariantCount } = variantMetadata; | ||||
| 
 | ||||
|     if (remainingPercentage < 0) { | ||||
|         throw new Error('The traffic distribution total must equal 100%'); | ||||
|     } | ||||
| 
 | ||||
|     if (!variableVariantCount) { | ||||
|         throw new Error('There must be atleast one variable variant'); | ||||
|     } | ||||
| 
 | ||||
|     const percentage = parseInt(remainingPercentage / variableVariantCount); | ||||
| 
 | ||||
|     return variants.map(variant => { | ||||
|         if (variant.weightType !== weightTypes.FIX) { | ||||
|             variant.weight = percentage; | ||||
|         } | ||||
|         return variant; | ||||
|     }); | ||||
|     return variants; | ||||
| } | ||||
|  | ||||
| @ -37,6 +37,9 @@ exports[`renders correctly with with variants 1`] = ` | ||||
|         <th> | ||||
|           Weight | ||||
|         </th> | ||||
|         <th> | ||||
|           Weight Type | ||||
|         </th> | ||||
|         <th | ||||
|           className="actions" | ||||
|         /> | ||||
| @ -67,6 +70,9 @@ exports[`renders correctly with with variants 1`] = ` | ||||
|           3.4 | ||||
|            % | ||||
|         </td> | ||||
|         <td> | ||||
|           Variable | ||||
|         </td> | ||||
|         <td | ||||
|           className="actions" | ||||
|         > | ||||
| @ -95,6 +101,9 @@ exports[`renders correctly with with variants 1`] = ` | ||||
|           3.3 | ||||
|            % | ||||
|         </td> | ||||
|         <td> | ||||
|           Variable | ||||
|         </td> | ||||
|         <td | ||||
|           className="actions" | ||||
|         > | ||||
| @ -126,6 +135,9 @@ exports[`renders correctly with with variants 1`] = ` | ||||
|           3.3 | ||||
|            % | ||||
|         </td> | ||||
|         <td> | ||||
|           Fix | ||||
|         </td> | ||||
|         <td | ||||
|           className="actions" | ||||
|         > | ||||
|  | ||||
| @ -4,6 +4,7 @@ import { MemoryRouter } from 'react-router-dom'; | ||||
| import UpdateVariant from './../update-variant-component'; | ||||
| import renderer from 'react-test-renderer'; | ||||
| import { UPDATE_FEATURE } from '../../../../permissions'; | ||||
| import { weightTypes } from '../enums'; | ||||
| 
 | ||||
| jest.mock('react-mdl'); | ||||
| 
 | ||||
| @ -72,6 +73,7 @@ test('renders correctly with with variants', () => { | ||||
|             { | ||||
|                 name: 'orange', | ||||
|                 weight: 33, | ||||
|                 weightType: weightTypes.FIX, | ||||
|                 payload: { | ||||
|                     type: 'string', | ||||
|                     value: '{"color": "blue", "animated": false}', | ||||
|  | ||||
| @ -1,9 +1,11 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import Modal from 'react-modal'; | ||||
| import { Button, Textfield, DialogActions, Grid, Cell, Icon } from 'react-mdl'; | ||||
| import { Button, Textfield, DialogActions, Grid, Cell, Icon, Switch } from 'react-mdl'; | ||||
| import styles from './variant.scss'; | ||||
| import MySelect from '../../common/select'; | ||||
| import { trim } from '../form/util'; | ||||
| import { weightTypes } from './enums'; | ||||
| import OverrideConfig from './override-config'; | ||||
| 
 | ||||
| Modal.setAppElement('#app'); | ||||
| @ -46,7 +48,11 @@ function AddVariant({ showDialog, closeDialog, save, validateName, editVariant, | ||||
| 
 | ||||
|     const clear = () => { | ||||
|         if (editVariant) { | ||||
|             setData({ name: editVariant.name }); | ||||
|             setData({ | ||||
|                 name: editVariant.name, | ||||
|                 weight: editVariant.weight / 10, | ||||
|                 weightType: editVariant.weightType || weightTypes.VARIABLE, | ||||
|             }); | ||||
|             if (editVariant.payload) { | ||||
|                 setPayload(editVariant.payload); | ||||
|             } | ||||
| @ -67,11 +73,20 @@ function AddVariant({ showDialog, closeDialog, save, validateName, editVariant, | ||||
|         clear(); | ||||
|     }, [editVariant]); | ||||
| 
 | ||||
|     const setName = e => { | ||||
|         e.preventDefault(); | ||||
|     const setVariantValue = e => { | ||||
|         const { name, value } = e.target; | ||||
|         setData({ | ||||
|             ...data, | ||||
|             [e.target.name]: trim(e.target.value), | ||||
|             [name]: trim(value), | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     const setVariantWeightType = e => { | ||||
|         const { checked, name } = e.target; | ||||
|         const weightType = checked ? weightTypes.FIX : weightTypes.VARIABLE; | ||||
|         setData({ | ||||
|             ...data, | ||||
|             [name]: weightType, | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
| @ -88,6 +103,8 @@ function AddVariant({ showDialog, closeDialog, save, validateName, editVariant, | ||||
|         try { | ||||
|             const variant = { | ||||
|                 name: data.name, | ||||
|                 weight: data.weight * 10, | ||||
|                 weightType: data.weightType, | ||||
|                 payload: payload.value ? payload : undefined, | ||||
|                 overrides: overrides | ||||
|                     .map(o => ({ | ||||
| @ -100,7 +117,7 @@ function AddVariant({ showDialog, closeDialog, save, validateName, editVariant, | ||||
|             clear(); | ||||
|             closeDialog(); | ||||
|         } catch (error) { | ||||
|             const msg = error.error || 'Could not add variant'; | ||||
|             const msg = error.message || 'Could not add variant'; | ||||
|             setError({ general: msg }); | ||||
|         } | ||||
|     }; | ||||
| @ -152,6 +169,8 @@ function AddVariant({ showDialog, closeDialog, save, validateName, editVariant, | ||||
|         setOverrides([...overrides, ...[{ contextName: 'userId', values: [] }]]); | ||||
|     }; | ||||
| 
 | ||||
|     const isFixWeight = data.weightType === weightTypes.FIX; | ||||
| 
 | ||||
|     return ( | ||||
|         <Modal isOpen={showDialog} contentLabel="Example Modal" style={customStyles} onRequestClose={onCancel}> | ||||
|             <h3>{title}</h3> | ||||
| @ -167,10 +186,33 @@ function AddVariant({ showDialog, closeDialog, save, validateName, editVariant, | ||||
|                     value={data.name} | ||||
|                     error={error.name} | ||||
|                     type="name" | ||||
|                     onChange={setName} | ||||
|                     onChange={setVariantValue} | ||||
|                 /> | ||||
|                 <br /> | ||||
|                 <br /> | ||||
|                 <Grid noSpacing className={styles.flex}> | ||||
|                     <Cell col={3} className={styles.flex}> | ||||
|                         <Textfield | ||||
|                             id="weight" | ||||
|                             floatingLabel | ||||
|                             label="Weight" | ||||
|                             name="weight" | ||||
|                             placeholder="" | ||||
|                             style={{ width: '40px', marginRight: '5px' }} | ||||
|                             inputClassName={styles.inputWeight} | ||||
|                             value={data.weight} | ||||
|                             error={error.weight} | ||||
|                             type="number" | ||||
|                             disabled={!isFixWeight} | ||||
|                             onChange={setVariantValue} | ||||
|                         /> | ||||
|                         <span>%</span> | ||||
|                     </Cell> | ||||
|                     <Cell col={9} className={[styles.flexCenter, styles.marginL10]}> | ||||
|                         <Switch name="weightType" checked={isFixWeight} onChange={setVariantWeightType}> | ||||
|                             Custom percentage | ||||
|                         </Switch> | ||||
|                     </Cell> | ||||
|                 </Grid> | ||||
|                 <p style={{ marginBottom: '0' }}> | ||||
|                     <strong>Payload </strong> | ||||
|                     <Icon name="info" title="Passed to the variant object. Can be anything (json, value, csv)" /> | ||||
| @ -198,13 +240,11 @@ function AddVariant({ showDialog, closeDialog, save, validateName, editVariant, | ||||
|                         /> | ||||
|                     </Cell> | ||||
|                 </Grid> | ||||
|                 {overrides.length > 0 ? ( | ||||
|                 {overrides.length > 0 && ( | ||||
|                     <p style={{ marginBottom: '0' }}> | ||||
|                         <strong>Overrides </strong> | ||||
|                         <Icon name="info" title="Here you can specify which users that should get this variant." /> | ||||
|                     </p> | ||||
|                 ) : ( | ||||
|                     undefined | ||||
|                 )} | ||||
| 
 | ||||
|                 <OverrideConfig | ||||
|  | ||||
							
								
								
									
										4
									
								
								frontend/src/component/feature/variant/enums.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								frontend/src/component/feature/variant/enums.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| export const weightTypes = { | ||||
|     FIX: 'fix', | ||||
|     VARIABLE: 'variable', | ||||
| }; | ||||
| @ -6,7 +6,11 @@ import styles from './variant.scss'; | ||||
| import { UPDATE_FEATURE } from '../../../permissions'; | ||||
| import AddVariant from './add-variant'; | ||||
| 
 | ||||
| const initalState = { showDialog: false, editVariant: undefined, editIndex: -1 }; | ||||
| const initalState = { | ||||
|     showDialog: false, | ||||
|     editVariant: undefined, | ||||
|     editIndex: -1, | ||||
| }; | ||||
| 
 | ||||
| class UpdateVariantComponent extends Component { | ||||
|     constructor(props) { | ||||
| @ -68,6 +72,7 @@ class UpdateVariantComponent extends Component { | ||||
|                     <th>Variant name</th> | ||||
|                     <th className={styles.labels} /> | ||||
|                     <th>Weight</th> | ||||
|                     <th>Weight Type</th> | ||||
|                     <th className={styles.actions} /> | ||||
|                 </tr> | ||||
|             </thead> | ||||
|  | ||||
| @ -1,11 +1,12 @@ | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| 
 | ||||
| import { IconButton, Chip } from 'react-mdl'; | ||||
| import styles from './variant.scss'; | ||||
| import { UPDATE_FEATURE } from '../../../permissions'; | ||||
| import { weightTypes } from './enums'; | ||||
| 
 | ||||
| function VariantViewComponent({ variant, editVariant, removeVariant, hasPermission }) { | ||||
|     const { FIX } = weightTypes; | ||||
|     return ( | ||||
|         <tr> | ||||
|             <td onClick={editVariant}>{variant.name}</td> | ||||
| @ -18,6 +19,7 @@ function VariantViewComponent({ variant, editVariant, removeVariant, hasPermissi | ||||
|                 )} | ||||
|             </td> | ||||
|             <td>{variant.weight / 10.0} %</td> | ||||
|             <td>{variant.weightType === FIX ? 'Fix' : 'Variable'}</td> | ||||
|             {hasPermission(UPDATE_FEATURE) ? ( | ||||
|                 <td className={styles.actions}> | ||||
|                     <IconButton name="edit" onClick={editVariant} /> | ||||
|  | ||||
| @ -2,11 +2,13 @@ | ||||
|     width: 100%; | ||||
|     max-width: 700px; | ||||
| 
 | ||||
|     th, td { | ||||
|     th, | ||||
|     td { | ||||
|         text-align: center; | ||||
|         width: 100px; | ||||
|     } | ||||
|     th:first-of-type, td:first-of-type { | ||||
|     th:first-of-type, | ||||
|     td:first-of-type { | ||||
|         text-align: left; | ||||
|         width: 100%; | ||||
|     } | ||||
| @ -16,10 +18,10 @@ | ||||
| } | ||||
| 
 | ||||
| @media (max-width: 600px) { | ||||
|     th.labels  { | ||||
|     th.labels { | ||||
|         display: none; | ||||
|     } | ||||
|     td.labels  { | ||||
|     td.labels { | ||||
|         display: none; | ||||
|     } | ||||
| } | ||||
| @ -58,4 +60,23 @@ td.actions { | ||||
|     i { | ||||
|         font-size: 18px; | ||||
|     } | ||||
| } | ||||
| } | ||||
| 
 | ||||
| .inputWeight { | ||||
|     text-align: right; | ||||
| } | ||||
| 
 | ||||
| .flexCenter { | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
| } | ||||
| 
 | ||||
| .flex { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
| } | ||||
| 
 | ||||
| .marginL10 { | ||||
|     margin-left: 10px; | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user