mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Fix/bugfixes (#279)
* fix: add try catch to copy * fix: show constraints on default strategy * fix: require name to submit context field * fix: require name and project id to be set in order to create a project * fix: change documentation icon * fix: only validate unique names on create * Update src/component/context/form-context-component.jsx Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai> Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>
This commit is contained in:
		
							parent
							
								
									0340573199
								
							
						
					
					
						commit
						f8e34d53ff
					
				| @ -3,5 +3,9 @@ import { makeStyles } from '@material-ui/styles'; | ||||
| export const useStyles = makeStyles({ | ||||
|     listItem: { | ||||
|         padding: 0, | ||||
|         ['& a']: { | ||||
|             textDecoration: 'none', | ||||
|             color: 'inherit', | ||||
|         }, | ||||
|     }, | ||||
| }); | ||||
|  | ||||
| @ -1,11 +1,20 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import { Component } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { Button, Chip, TextField, Switch, Icon, Typography } from '@material-ui/core'; | ||||
| import { | ||||
|     Button, | ||||
|     Chip, | ||||
|     TextField, | ||||
|     Switch, | ||||
|     Icon, | ||||
|     Typography, | ||||
| } from '@material-ui/core'; | ||||
| import styles from './Context.module.scss'; | ||||
| import classnames from 'classnames'; | ||||
| import { FormButtons, styles as commonStyles } from '../common'; | ||||
| import { trim } from '../common/util'; | ||||
| import PageContent from '../common/PageContent/PageContent'; | ||||
| import ConditionallyRender from '../common/ConditionallyRender'; | ||||
| import { Alert } from '@material-ui/lab'; | ||||
| 
 | ||||
| const sortIgnoreCase = (a, b) => { | ||||
|     a = a.toLowerCase(); | ||||
| @ -23,9 +32,26 @@ class AddContextComponent extends Component { | ||||
|             errors: {}, | ||||
|             currentLegalValue: '', | ||||
|             dirty: false, | ||||
|             focusedLegalValue: false, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     handleKeydown = e => { | ||||
|         if (e.key === 'Enter' && this.state.focusedLegalValue) { | ||||
|             this.addLegalValue(e); | ||||
|         } else if (e.key === 'Enter') { | ||||
|             this.onSubmit(e); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     componentDidMount() { | ||||
|         window.addEventListener('keydown', this.handleKeydown); | ||||
|     } | ||||
| 
 | ||||
|     componentWillUnmount() { | ||||
|         window.removeEventListener('keydown', this.handleKeydown); | ||||
|     } | ||||
| 
 | ||||
|     static getDerivedStateFromProps(props, state) { | ||||
|         if (state.contextField.initial && !props.contextField.initial) { | ||||
|             return { contextField: props.contextField }; | ||||
| @ -42,15 +68,19 @@ class AddContextComponent extends Component { | ||||
| 
 | ||||
|     validateContextName = async name => { | ||||
|         const { errors } = this.state; | ||||
|         const { validateName } = this.props; | ||||
|         const { validateName, editMode } = this.props; | ||||
| 
 | ||||
|         if (editMode) return true; | ||||
| 
 | ||||
|         try { | ||||
|             await validateName(name); | ||||
|             errors.name = undefined; | ||||
|         } catch (err) { | ||||
|             errors.name = err.message; | ||||
|         } | ||||
| 
 | ||||
|         this.setState({ errors }); | ||||
|         if (errors.name) return false; | ||||
|         return true; | ||||
|     }; | ||||
| 
 | ||||
|     onCancel = evt => { | ||||
| @ -58,10 +88,23 @@ class AddContextComponent extends Component { | ||||
|         this.props.history.push('/context'); | ||||
|     }; | ||||
| 
 | ||||
|     onSubmit = evt => { | ||||
|     onSubmit = async evt => { | ||||
|         evt.preventDefault(); | ||||
|         const { contextField } = this.state; | ||||
|         this.props.submit(contextField).then(() => this.props.history.push('/context')); | ||||
| 
 | ||||
|         const valid = await this.validateContextName(contextField.name); | ||||
| 
 | ||||
|         if (valid) { | ||||
|             this.props | ||||
|                 .submit(contextField) | ||||
|                 .then(() => this.props.history.push('/context')) | ||||
|                 .catch(e => | ||||
|                     this.setState(prev => ({ | ||||
|                         ...prev, | ||||
|                         errors: { api: e.toString() }, | ||||
|                     })) | ||||
|                 ); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     updateCurrentLegalValue = evt => { | ||||
| @ -82,7 +125,9 @@ class AddContextComponent extends Component { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const legalValues = contextField.legalValues.concat(trim(currentLegalValue)); | ||||
|         const legalValues = contextField.legalValues.concat( | ||||
|             trim(currentLegalValue) | ||||
|         ); | ||||
|         contextField.legalValues = legalValues.sort(sortIgnoreCase); | ||||
|         this.setState({ | ||||
|             contextField, | ||||
| @ -93,7 +138,9 @@ class AddContextComponent extends Component { | ||||
| 
 | ||||
|     removeLegalValue = index => { | ||||
|         const { contextField } = this.state; | ||||
|         const legalValues = contextField.legalValues.filter((_, i) => i !== index); | ||||
|         const legalValues = contextField.legalValues.filter( | ||||
|             (_, i) => i !== index | ||||
|         ); | ||||
|         contextField.legalValues = legalValues; | ||||
|         this.setState({ contextField }); | ||||
|     }; | ||||
| @ -115,11 +162,20 @@ class AddContextComponent extends Component { | ||||
|         return ( | ||||
|             <PageContent headerContent="Create context field"> | ||||
|                 <div className={styles.supporting}> | ||||
|                     Context fields are a basic building block used in Unleash to control roll-out. They can be used | ||||
|                     together with strategy constraints as part of the activation strategy evaluation. | ||||
|                     Context fields are a basic building block used in Unleash to | ||||
|                     control roll-out. They can be used together with strategy | ||||
|                     constraints as part of the activation strategy evaluation. | ||||
|                 </div> | ||||
|                 <form onSubmit={this.onSubmit}> | ||||
|                     <section className={styles.formContainer}> | ||||
|                         <ConditionallyRender | ||||
|                             condition={errors.api} | ||||
|                             show={ | ||||
|                                 <Alert severity="error"> | ||||
|                                     {this.state.errors.api} | ||||
|                                 </Alert> | ||||
|                             } | ||||
|                         /> | ||||
|                         <TextField | ||||
|                             className={commonStyles.fullwidth} | ||||
|                             label="Name" | ||||
| @ -130,8 +186,12 @@ class AddContextComponent extends Component { | ||||
|                             disabled={editMode} | ||||
|                             variant="outlined" | ||||
|                             size="small" | ||||
|                             onBlur={v => this.validateContextName(v.target.value)} | ||||
|                             onChange={v => this.setValue('name', trim(v.target.value))} | ||||
|                             onBlur={v => | ||||
|                                 this.validateContextName(v.target.value) | ||||
|                             } | ||||
|                             onChange={v => | ||||
|                                 this.setValue('name', trim(v.target.value)) | ||||
|                             } | ||||
|                         /> | ||||
|                         <TextField | ||||
|                             className={commonStyles.fullwidth} | ||||
| @ -142,7 +202,9 @@ class AddContextComponent extends Component { | ||||
|                             variant="outlined" | ||||
|                             size="small" | ||||
|                             defaultValue={contextField.description} | ||||
|                             onChange={v => this.setValue('description', v.target.value)} | ||||
|                             onChange={v => | ||||
|                                 this.setValue('description', v.target.value) | ||||
|                             } | ||||
|                         /> | ||||
|                         <br /> | ||||
|                         <br /> | ||||
| @ -150,8 +212,10 @@ class AddContextComponent extends Component { | ||||
|                     <section className={styles.inset}> | ||||
|                         <h6 className={styles.h6}>Legal values</h6> | ||||
|                         <p className={styles.alpha}> | ||||
|                             By defining the legal values the Unleash Admin UI will validate the user input. A concrete | ||||
|                             example would be that we know all values for our “environment” (local, development, stage, | ||||
|                             By defining the legal values the Unleash Admin UI | ||||
|                             will validate the user input. A concrete example | ||||
|                             would be that we know all values for our | ||||
|                             “environment” (local, development, stage, | ||||
|                             production). | ||||
|                         </p> | ||||
|                         <div> | ||||
| @ -159,6 +223,18 @@ class AddContextComponent extends Component { | ||||
|                                 label="Value" | ||||
|                                 name="value" | ||||
|                                 className={styles.valueField} | ||||
|                                 onFocus={() => | ||||
|                                     this.setState(prev => ({ | ||||
|                                         ...prev, | ||||
|                                         focusedLegalValue: true, | ||||
|                                     })) | ||||
|                                 } | ||||
|                                 onBlur={() => | ||||
|                                     this.setState(prev => ({ | ||||
|                                         ...prev, | ||||
|                                         focusedLegalValue: false, | ||||
|                                     })) | ||||
|                                 } | ||||
|                                 value={this.state.currentLegalValue} | ||||
|                                 error={!!errors.currentLegalValue} | ||||
|                                 helperText={errors.currentLegalValue} | ||||
| @ -176,15 +252,28 @@ class AddContextComponent extends Component { | ||||
|                                 Add | ||||
|                             </Button> | ||||
|                         </div> | ||||
|                         <div>{contextField.legalValues.map(this.renderLegalValue)}</div> | ||||
|                         <div> | ||||
|                             {contextField.legalValues.map( | ||||
|                                 this.renderLegalValue | ||||
|                             )} | ||||
|                         </div> | ||||
|                     </section> | ||||
|                     <br /> | ||||
|                     <section> | ||||
|                         <Typography variant="subtitle1">Custom stickiness (beta)</Typography> | ||||
|                         <p className={classnames(styles.alpha, styles.formContainer)}> | ||||
|                             By enabling stickiness on this context field you can use it together with the | ||||
|                             flexible-rollout strategy. This will guarantee a consistent behavior for specific values of | ||||
|                             this context field. PS! Not all client SDK's support this feature yet!{' '} | ||||
|                         <Typography variant="subtitle1"> | ||||
|                             Custom stickiness (beta) | ||||
|                         </Typography> | ||||
|                         <p | ||||
|                             className={classnames( | ||||
|                                 styles.alpha, | ||||
|                                 styles.formContainer | ||||
|                             )} | ||||
|                         > | ||||
|                             By enabling stickiness on this context field you can | ||||
|                             use it together with the flexible-rollout strategy. | ||||
|                             This will guarantee a consistent behavior for | ||||
|                             specific values of this context field. PS! Not all | ||||
|                             client SDK's support this feature yet!{' '} | ||||
|                             <a | ||||
|                                 href="https://unleash.github.io/docs/activation_strategy#flexiblerollout" | ||||
|                                 target="_blank" | ||||
| @ -193,14 +282,24 @@ class AddContextComponent extends Component { | ||||
|                                 Read more | ||||
|                             </a> | ||||
|                         </p> | ||||
|                         {console.log(contextField.stickiness)} | ||||
|                         <Switch | ||||
|                             label="Allow stickiness" | ||||
|                             checked={contextField.stickiness} | ||||
|                             value={contextField.stickiness} | ||||
|                             onChange={() => this.setValue('stickiness', !contextField.stickiness)} | ||||
|                             onChange={() => | ||||
|                                 this.setValue( | ||||
|                                     'stickiness', | ||||
|                                     !contextField.stickiness | ||||
|                                 ) | ||||
|                             } | ||||
|                         /> | ||||
|                     </section> | ||||
|                     <div className={styles.formButtons}> | ||||
|                         <FormButtons submitText={submitText} onCancel={this.onCancel} /> | ||||
|                         <FormButtons | ||||
|                             submitText={submitText} | ||||
|                             onCancel={this.onCancel} | ||||
|                         /> | ||||
|                     </div> | ||||
|                 </form> | ||||
|             </PageContent> | ||||
|  | ||||
| @ -12,22 +12,45 @@ const StrategyCardContent = ({ strategy, strategyDefinition }) => { | ||||
|     const resolveContent = () => { | ||||
|         switch (strategy.name) { | ||||
|             case 'default': | ||||
|                 return <StrategyCardContentDefault />; | ||||
|                 return <StrategyCardContentDefault strategy={strategy} />; | ||||
|             case 'flexibleRollout': | ||||
|                 return <StrategyCardContentFlexible strategy={strategy} />; | ||||
|             case 'userWithId': | ||||
|                 return <StrategyCardContentList parameter={'userIds'} valuesName={'userIds'} strategy={strategy} />; | ||||
|                 return ( | ||||
|                     <StrategyCardContentList | ||||
|                         parameter={'userIds'} | ||||
|                         valuesName={'userIds'} | ||||
|                         strategy={strategy} | ||||
|                     /> | ||||
|                 ); | ||||
|             case 'gradualRolloutRandom': | ||||
|                 return <StrategyCardContentGradRandom strategy={strategy} />; | ||||
|             case 'remoteAddress': | ||||
|                 return <StrategyCardContentList parameter={'IPs'} valuesName={'IPs'} strategy={strategy} />; | ||||
|                 return ( | ||||
|                     <StrategyCardContentList | ||||
|                         parameter={'IPs'} | ||||
|                         valuesName={'IPs'} | ||||
|                         strategy={strategy} | ||||
|                     /> | ||||
|                 ); | ||||
|             case 'applicationHostname': | ||||
|                 return <StrategyCardContentList parameter={'hostNames'} valuesName={'hostnames'} strategy={strategy} />; | ||||
|                 return ( | ||||
|                     <StrategyCardContentList | ||||
|                         parameter={'hostNames'} | ||||
|                         valuesName={'hostnames'} | ||||
|                         strategy={strategy} | ||||
|                     /> | ||||
|                 ); | ||||
|             case 'gradualRolloutUserId': | ||||
|             case 'gradualRolloutSessionId': | ||||
|                 return <StrategyCardContentRollout strategy={strategy} />; | ||||
|             default: | ||||
|                 return <StrategyCardContentCustom strategy={strategy} strategyDefinition={strategyDefinition} />; | ||||
|                 return ( | ||||
|                     <StrategyCardContentCustom | ||||
|                         strategy={strategy} | ||||
|                         strategyDefinition={strategyDefinition} | ||||
|                     /> | ||||
|                 ); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|  | ||||
| @ -3,14 +3,29 @@ import React from 'react'; | ||||
| import { Typography } from '@material-ui/core'; | ||||
| 
 | ||||
| import { useCommonStyles } from '../../../../../../common.styles'; | ||||
| import ConditionallyRender from '../../../../../common/ConditionallyRender'; | ||||
| import StrategyCardConstraints from '../common/StrategyCardConstraints/StrategyCardConstraints'; | ||||
| 
 | ||||
| const StrategyCardContentDefault = () => { | ||||
| const StrategyCardContentDefault = ({ strategy }) => { | ||||
|     const commonStyles = useCommonStyles(); | ||||
| 
 | ||||
|     const { constraints } = strategy; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <Typography className={commonStyles.textCenter}> | ||||
|                 The default strategy is either on or off for all users. | ||||
|             </Typography> | ||||
|             <ConditionallyRender | ||||
|                 condition={constraints && constraints.length > 0} | ||||
|                 show={ | ||||
|                     <> | ||||
|                         <div className={commonStyles.divider} /> | ||||
|                         <StrategyCardConstraints constraints={constraints} /> | ||||
|                     </> | ||||
|                 } | ||||
|             /> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
|  | ||||
| @ -15,8 +15,7 @@ import MenuIcon from '@material-ui/icons/Menu'; | ||||
| import Breadcrumb from '../breadcrumb'; | ||||
| import UserProfile from '../../user/UserProfile'; | ||||
| import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; | ||||
| import HelpIcon from '@material-ui/icons/Help'; | ||||
| 
 | ||||
| import MenuBookIcon from '@material-ui/icons/MenuBook'; | ||||
| import { useStyles } from './styles'; | ||||
| 
 | ||||
| const Header = ({ uiConfig, init }) => { | ||||
| @ -64,7 +63,7 @@ const Header = ({ uiConfig, init }) => { | ||||
|                                 rel="noopener noreferrer" | ||||
|                                 className={styles.docsLink} | ||||
|                             > | ||||
|                                 <HelpIcon className={styles.docsIcon} /> | ||||
|                                 <MenuBookIcon className={styles.docsIcon} /> | ||||
|                             </a> | ||||
|                         </Tooltip> | ||||
|                         <UserProfile /> | ||||
|  | ||||
| @ -3,5 +3,9 @@ import { makeStyles } from '@material-ui/styles'; | ||||
| export const useStyles = makeStyles({ | ||||
|     listItem: { | ||||
|         padding: 0, | ||||
|         ['& a']: { | ||||
|             textDecoration: 'none', | ||||
|             color: 'inherit', | ||||
|         }, | ||||
|     }, | ||||
| }); | ||||
|  | ||||
| @ -40,7 +40,9 @@ class AddContextComponent extends Component { | ||||
| 
 | ||||
|     validateId = async id => { | ||||
|         const { errors } = this.state; | ||||
|         const { validateId } = this.props; | ||||
|         const { validateId, editMode } = this.props; | ||||
|         if (editMode) return true; | ||||
| 
 | ||||
|         try { | ||||
|             await validateId(id); | ||||
|             errors.id = undefined; | ||||
| @ -49,6 +51,26 @@ class AddContextComponent extends Component { | ||||
|         } | ||||
| 
 | ||||
|         this.setState({ errors }); | ||||
|         if (errors.id) return false; | ||||
|         return true; | ||||
|     }; | ||||
| 
 | ||||
|     validateName = () => { | ||||
|         const { project } = this.state; | ||||
|         if (project.name.length === 0) { | ||||
|             this.setState(prev => ({ | ||||
|                 errors: { ...prev.errors, name: 'Name can not be empty.' }, | ||||
|             })); | ||||
|             return false; | ||||
|         } | ||||
|         return true; | ||||
|     }; | ||||
| 
 | ||||
|     validate = async id => { | ||||
|         const validId = await this.validateId(id); | ||||
|         const validName = this.validateName(); | ||||
| 
 | ||||
|         return validId && validName; | ||||
|     }; | ||||
| 
 | ||||
|     onCancel = evt => { | ||||
| @ -59,8 +81,13 @@ class AddContextComponent extends Component { | ||||
|     onSubmit = async evt => { | ||||
|         evt.preventDefault(); | ||||
|         const { project } = this.state; | ||||
| 
 | ||||
|         const valid = await this.validate(project.id); | ||||
| 
 | ||||
|         if (valid) { | ||||
|             await this.props.submit(project); | ||||
|             this.props.history.push('/projects'); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
| @ -71,12 +98,19 @@ class AddContextComponent extends Component { | ||||
| 
 | ||||
|         return ( | ||||
|             <PageContent headerContent={`${submitText} Project`}> | ||||
|                 <Typography variant="subtitle1" style={{ marginBottom: '0.5rem' }}> | ||||
|                     Projects allows you to group feature toggles together in the management UI. | ||||
|                 <Typography | ||||
|                     variant="subtitle1" | ||||
|                     style={{ marginBottom: '0.5rem' }} | ||||
|                 > | ||||
|                     Projects allows you to group feature toggles together in the | ||||
|                     management UI. | ||||
|                 </Typography> | ||||
|                 <form | ||||
|                     onSubmit={this.onSubmit} | ||||
|                     className={classnames(commonStyles.contentSpacing, styles.formContainer)} | ||||
|                     className={classnames( | ||||
|                         commonStyles.contentSpacing, | ||||
|                         styles.formContainer | ||||
|                     )} | ||||
|                 > | ||||
|                     <TextField | ||||
|                         label="Project Id" | ||||
| @ -89,7 +123,9 @@ class AddContextComponent extends Component { | ||||
|                         variant="outlined" | ||||
|                         size="small" | ||||
|                         onBlur={v => this.validateId(v.target.value)} | ||||
|                         onChange={v => this.setValue('id', trim(v.target.value))} | ||||
|                         onChange={v => | ||||
|                             this.setValue('id', trim(v.target.value)) | ||||
|                         } | ||||
|                     /> | ||||
|                     <br /> | ||||
|                     <TextField | ||||
| @ -114,14 +150,21 @@ class AddContextComponent extends Component { | ||||
|                         size="small" | ||||
|                         multiline | ||||
|                         value={project.description} | ||||
|                         onChange={v => this.setValue('description', v.target.value)} | ||||
|                         onChange={v => | ||||
|                             this.setValue('description', v.target.value) | ||||
|                         } | ||||
|                     /> | ||||
|                     <ConditionallyRender condition={hasAccess(CREATE_PROJECT)} show={ | ||||
|                     <ConditionallyRender | ||||
|                         condition={hasAccess(CREATE_PROJECT)} | ||||
|                         show={ | ||||
|                             <div className={styles.formButtons}> | ||||
|                             <FormButtons submitText={submitText} onCancel={this.onCancel} /> | ||||
|                                 <FormButtons | ||||
|                                     submitText={submitText} | ||||
|                                     onCancel={this.onCancel} | ||||
|                                 /> | ||||
|                             </div> | ||||
|                     } /> | ||||
|                      | ||||
|                         } | ||||
|                     /> | ||||
|                 </form> | ||||
|             </PageContent> | ||||
|         ); | ||||
|  | ||||
| @ -22,6 +22,7 @@ const UserInviteLink = ({ inviteLink }: IInviteLinkProps) => { | ||||
|     }); | ||||
| 
 | ||||
|     const handleCopy = () => { | ||||
|         try { | ||||
|             return navigator.clipboard | ||||
|                 .writeText(inviteLink) | ||||
|                 .then(() => { | ||||
| @ -32,13 +33,19 @@ const UserInviteLink = ({ inviteLink }: IInviteLinkProps) => { | ||||
|                     }); | ||||
|                 }) | ||||
|                 .catch(() => { | ||||
|                     setError(); | ||||
|                 }); | ||||
|         } catch (e) { | ||||
|             setError(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const setError = () => | ||||
|         setSnackbar({ | ||||
|             show: true, | ||||
|             type: 'error', | ||||
|             text: 'Could not copy invite link.', | ||||
|         }); | ||||
|             }); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <div | ||||
|  | ||||
| @ -39,7 +39,10 @@ export function createContextField(context) { | ||||
|         api | ||||
|             .create(context) | ||||
|             .then(() => dispatch(addContextField(context))) | ||||
|             .catch(dispatchError(dispatch, ERROR_ADD_CONTEXT_FIELD)); | ||||
|             .catch(e => { | ||||
|                 dispatchError(dispatch, ERROR_ADD_CONTEXT_FIELD); | ||||
|                 throw e; | ||||
|             }); | ||||
| } | ||||
| 
 | ||||
| export function updateContextField(context) { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user