1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +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:
Fredrik Strand Oseberg 2021-04-28 14:27:25 +02:00 committed by GitHub
parent 0340573199
commit f8e34d53ff
9 changed files with 262 additions and 65 deletions

View File

@ -3,5 +3,9 @@ import { makeStyles } from '@material-ui/styles';
export const useStyles = makeStyles({ export const useStyles = makeStyles({
listItem: { listItem: {
padding: 0, padding: 0,
['& a']: {
textDecoration: 'none',
color: 'inherit',
},
}, },
}); });

View File

@ -1,11 +1,20 @@
import React, { Component } from 'react'; import { Component } from 'react';
import PropTypes from 'prop-types'; 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 styles from './Context.module.scss';
import classnames from 'classnames'; import classnames from 'classnames';
import { FormButtons, styles as commonStyles } from '../common'; import { FormButtons, styles as commonStyles } from '../common';
import { trim } from '../common/util'; import { trim } from '../common/util';
import PageContent from '../common/PageContent/PageContent'; import PageContent from '../common/PageContent/PageContent';
import ConditionallyRender from '../common/ConditionallyRender';
import { Alert } from '@material-ui/lab';
const sortIgnoreCase = (a, b) => { const sortIgnoreCase = (a, b) => {
a = a.toLowerCase(); a = a.toLowerCase();
@ -23,9 +32,26 @@ class AddContextComponent extends Component {
errors: {}, errors: {},
currentLegalValue: '', currentLegalValue: '',
dirty: false, 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) { static getDerivedStateFromProps(props, state) {
if (state.contextField.initial && !props.contextField.initial) { if (state.contextField.initial && !props.contextField.initial) {
return { contextField: props.contextField }; return { contextField: props.contextField };
@ -42,15 +68,19 @@ class AddContextComponent extends Component {
validateContextName = async name => { validateContextName = async name => {
const { errors } = this.state; const { errors } = this.state;
const { validateName } = this.props; const { validateName, editMode } = this.props;
if (editMode) return true;
try { try {
await validateName(name); await validateName(name);
errors.name = undefined; errors.name = undefined;
} catch (err) { } catch (err) {
errors.name = err.message; errors.name = err.message;
} }
this.setState({ errors }); this.setState({ errors });
if (errors.name) return false;
return true;
}; };
onCancel = evt => { onCancel = evt => {
@ -58,10 +88,23 @@ class AddContextComponent extends Component {
this.props.history.push('/context'); this.props.history.push('/context');
}; };
onSubmit = evt => { onSubmit = async evt => {
evt.preventDefault(); evt.preventDefault();
const { contextField } = this.state; 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 => { updateCurrentLegalValue = evt => {
@ -82,7 +125,9 @@ class AddContextComponent extends Component {
return; return;
} }
const legalValues = contextField.legalValues.concat(trim(currentLegalValue)); const legalValues = contextField.legalValues.concat(
trim(currentLegalValue)
);
contextField.legalValues = legalValues.sort(sortIgnoreCase); contextField.legalValues = legalValues.sort(sortIgnoreCase);
this.setState({ this.setState({
contextField, contextField,
@ -93,7 +138,9 @@ class AddContextComponent extends Component {
removeLegalValue = index => { removeLegalValue = index => {
const { contextField } = this.state; const { contextField } = this.state;
const legalValues = contextField.legalValues.filter((_, i) => i !== index); const legalValues = contextField.legalValues.filter(
(_, i) => i !== index
);
contextField.legalValues = legalValues; contextField.legalValues = legalValues;
this.setState({ contextField }); this.setState({ contextField });
}; };
@ -115,11 +162,20 @@ class AddContextComponent extends Component {
return ( return (
<PageContent headerContent="Create context field"> <PageContent headerContent="Create context field">
<div className={styles.supporting}> <div className={styles.supporting}>
Context fields are a basic building block used in Unleash to control roll-out. They can be used Context fields are a basic building block used in Unleash to
together with strategy constraints as part of the activation strategy evaluation. control roll-out. They can be used together with strategy
constraints as part of the activation strategy evaluation.
</div> </div>
<form onSubmit={this.onSubmit}> <form onSubmit={this.onSubmit}>
<section className={styles.formContainer}> <section className={styles.formContainer}>
<ConditionallyRender
condition={errors.api}
show={
<Alert severity="error">
{this.state.errors.api}
</Alert>
}
/>
<TextField <TextField
className={commonStyles.fullwidth} className={commonStyles.fullwidth}
label="Name" label="Name"
@ -130,8 +186,12 @@ class AddContextComponent extends Component {
disabled={editMode} disabled={editMode}
variant="outlined" variant="outlined"
size="small" size="small"
onBlur={v => this.validateContextName(v.target.value)} onBlur={v =>
onChange={v => this.setValue('name', trim(v.target.value))} this.validateContextName(v.target.value)
}
onChange={v =>
this.setValue('name', trim(v.target.value))
}
/> />
<TextField <TextField
className={commonStyles.fullwidth} className={commonStyles.fullwidth}
@ -142,7 +202,9 @@ class AddContextComponent extends Component {
variant="outlined" variant="outlined"
size="small" size="small"
defaultValue={contextField.description} defaultValue={contextField.description}
onChange={v => this.setValue('description', v.target.value)} onChange={v =>
this.setValue('description', v.target.value)
}
/> />
<br /> <br />
<br /> <br />
@ -150,8 +212,10 @@ class AddContextComponent extends Component {
<section className={styles.inset}> <section className={styles.inset}>
<h6 className={styles.h6}>Legal values</h6> <h6 className={styles.h6}>Legal values</h6>
<p className={styles.alpha}> <p className={styles.alpha}>
By defining the legal values the Unleash Admin UI will validate the user input. A concrete By defining the legal values the Unleash Admin UI
example would be that we know all values for our environment (local, development, stage, will validate the user input. A concrete example
would be that we know all values for our
environment (local, development, stage,
production). production).
</p> </p>
<div> <div>
@ -159,6 +223,18 @@ class AddContextComponent extends Component {
label="Value" label="Value"
name="value" name="value"
className={styles.valueField} className={styles.valueField}
onFocus={() =>
this.setState(prev => ({
...prev,
focusedLegalValue: true,
}))
}
onBlur={() =>
this.setState(prev => ({
...prev,
focusedLegalValue: false,
}))
}
value={this.state.currentLegalValue} value={this.state.currentLegalValue}
error={!!errors.currentLegalValue} error={!!errors.currentLegalValue}
helperText={errors.currentLegalValue} helperText={errors.currentLegalValue}
@ -176,15 +252,28 @@ class AddContextComponent extends Component {
Add Add
</Button> </Button>
</div> </div>
<div>{contextField.legalValues.map(this.renderLegalValue)}</div> <div>
{contextField.legalValues.map(
this.renderLegalValue
)}
</div>
</section> </section>
<br /> <br />
<section> <section>
<Typography variant="subtitle1">Custom stickiness (beta)</Typography> <Typography variant="subtitle1">
<p className={classnames(styles.alpha, styles.formContainer)}> Custom stickiness (beta)
By enabling stickiness on this context field you can use it together with the </Typography>
flexible-rollout strategy. This will guarantee a consistent behavior for specific values of <p
this context field. PS! Not all client SDK's support this feature yet!{' '} 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 <a
href="https://unleash.github.io/docs/activation_strategy#flexiblerollout" href="https://unleash.github.io/docs/activation_strategy#flexiblerollout"
target="_blank" target="_blank"
@ -193,14 +282,24 @@ class AddContextComponent extends Component {
Read more Read more
</a> </a>
</p> </p>
{console.log(contextField.stickiness)}
<Switch <Switch
label="Allow stickiness" label="Allow stickiness"
checked={contextField.stickiness}
value={contextField.stickiness} value={contextField.stickiness}
onChange={() => this.setValue('stickiness', !contextField.stickiness)} onChange={() =>
this.setValue(
'stickiness',
!contextField.stickiness
)
}
/> />
</section> </section>
<div className={styles.formButtons}> <div className={styles.formButtons}>
<FormButtons submitText={submitText} onCancel={this.onCancel} /> <FormButtons
submitText={submitText}
onCancel={this.onCancel}
/>
</div> </div>
</form> </form>
</PageContent> </PageContent>

View File

@ -12,22 +12,45 @@ const StrategyCardContent = ({ strategy, strategyDefinition }) => {
const resolveContent = () => { const resolveContent = () => {
switch (strategy.name) { switch (strategy.name) {
case 'default': case 'default':
return <StrategyCardContentDefault />; return <StrategyCardContentDefault strategy={strategy} />;
case 'flexibleRollout': case 'flexibleRollout':
return <StrategyCardContentFlexible strategy={strategy} />; return <StrategyCardContentFlexible strategy={strategy} />;
case 'userWithId': case 'userWithId':
return <StrategyCardContentList parameter={'userIds'} valuesName={'userIds'} strategy={strategy} />; return (
<StrategyCardContentList
parameter={'userIds'}
valuesName={'userIds'}
strategy={strategy}
/>
);
case 'gradualRolloutRandom': case 'gradualRolloutRandom':
return <StrategyCardContentGradRandom strategy={strategy} />; return <StrategyCardContentGradRandom strategy={strategy} />;
case 'remoteAddress': case 'remoteAddress':
return <StrategyCardContentList parameter={'IPs'} valuesName={'IPs'} strategy={strategy} />; return (
<StrategyCardContentList
parameter={'IPs'}
valuesName={'IPs'}
strategy={strategy}
/>
);
case 'applicationHostname': case 'applicationHostname':
return <StrategyCardContentList parameter={'hostNames'} valuesName={'hostnames'} strategy={strategy} />; return (
<StrategyCardContentList
parameter={'hostNames'}
valuesName={'hostnames'}
strategy={strategy}
/>
);
case 'gradualRolloutUserId': case 'gradualRolloutUserId':
case 'gradualRolloutSessionId': case 'gradualRolloutSessionId':
return <StrategyCardContentRollout strategy={strategy} />; return <StrategyCardContentRollout strategy={strategy} />;
default: default:
return <StrategyCardContentCustom strategy={strategy} strategyDefinition={strategyDefinition} />; return (
<StrategyCardContentCustom
strategy={strategy}
strategyDefinition={strategyDefinition}
/>
);
} }
}; };

View File

@ -3,14 +3,29 @@ import React from 'react';
import { Typography } from '@material-ui/core'; import { Typography } from '@material-ui/core';
import { useCommonStyles } from '../../../../../../common.styles'; 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 commonStyles = useCommonStyles();
const { constraints } = strategy;
return ( return (
<Typography className={commonStyles.textCenter}> <>
The default strategy is either on or off for all users. <Typography className={commonStyles.textCenter}>
</Typography> 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} />
</>
}
/>
</>
); );
}; };

View File

@ -15,8 +15,7 @@ import MenuIcon from '@material-ui/icons/Menu';
import Breadcrumb from '../breadcrumb'; import Breadcrumb from '../breadcrumb';
import UserProfile from '../../user/UserProfile'; import UserProfile from '../../user/UserProfile';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import HelpIcon from '@material-ui/icons/Help'; import MenuBookIcon from '@material-ui/icons/MenuBook';
import { useStyles } from './styles'; import { useStyles } from './styles';
const Header = ({ uiConfig, init }) => { const Header = ({ uiConfig, init }) => {
@ -64,7 +63,7 @@ const Header = ({ uiConfig, init }) => {
rel="noopener noreferrer" rel="noopener noreferrer"
className={styles.docsLink} className={styles.docsLink}
> >
<HelpIcon className={styles.docsIcon} /> <MenuBookIcon className={styles.docsIcon} />
</a> </a>
</Tooltip> </Tooltip>
<UserProfile /> <UserProfile />

View File

@ -3,5 +3,9 @@ import { makeStyles } from '@material-ui/styles';
export const useStyles = makeStyles({ export const useStyles = makeStyles({
listItem: { listItem: {
padding: 0, padding: 0,
['& a']: {
textDecoration: 'none',
color: 'inherit',
},
}, },
}); });

View File

@ -40,7 +40,9 @@ class AddContextComponent extends Component {
validateId = async id => { validateId = async id => {
const { errors } = this.state; const { errors } = this.state;
const { validateId } = this.props; const { validateId, editMode } = this.props;
if (editMode) return true;
try { try {
await validateId(id); await validateId(id);
errors.id = undefined; errors.id = undefined;
@ -49,6 +51,26 @@ class AddContextComponent extends Component {
} }
this.setState({ errors }); 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 => { onCancel = evt => {
@ -59,8 +81,13 @@ class AddContextComponent extends Component {
onSubmit = async evt => { onSubmit = async evt => {
evt.preventDefault(); evt.preventDefault();
const { project } = this.state; const { project } = this.state;
await this.props.submit(project);
this.props.history.push('/projects'); const valid = await this.validate(project.id);
if (valid) {
await this.props.submit(project);
this.props.history.push('/projects');
}
}; };
render() { render() {
@ -71,12 +98,19 @@ class AddContextComponent extends Component {
return ( return (
<PageContent headerContent={`${submitText} Project`}> <PageContent headerContent={`${submitText} Project`}>
<Typography variant="subtitle1" style={{ marginBottom: '0.5rem' }}> <Typography
Projects allows you to group feature toggles together in the management UI. variant="subtitle1"
style={{ marginBottom: '0.5rem' }}
>
Projects allows you to group feature toggles together in the
management UI.
</Typography> </Typography>
<form <form
onSubmit={this.onSubmit} onSubmit={this.onSubmit}
className={classnames(commonStyles.contentSpacing, styles.formContainer)} className={classnames(
commonStyles.contentSpacing,
styles.formContainer
)}
> >
<TextField <TextField
label="Project Id" label="Project Id"
@ -89,7 +123,9 @@ class AddContextComponent extends Component {
variant="outlined" variant="outlined"
size="small" size="small"
onBlur={v => this.validateId(v.target.value)} 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 /> <br />
<TextField <TextField
@ -114,14 +150,21 @@ class AddContextComponent extends Component {
size="small" size="small"
multiline multiline
value={project.description} 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={
<div className={styles.formButtons}>
<FormButtons
submitText={submitText}
onCancel={this.onCancel}
/>
</div>
}
/> />
<ConditionallyRender condition={hasAccess(CREATE_PROJECT)} show={
<div className={styles.formButtons}>
<FormButtons submitText={submitText} onCancel={this.onCancel} />
</div>
} />
</form> </form>
</PageContent> </PageContent>
); );

View File

@ -22,24 +22,31 @@ const UserInviteLink = ({ inviteLink }: IInviteLinkProps) => {
}); });
const handleCopy = () => { const handleCopy = () => {
return navigator.clipboard try {
.writeText(inviteLink) return navigator.clipboard
.then(() => { .writeText(inviteLink)
setSnackbar({ .then(() => {
show: true, setSnackbar({
type: 'success', show: true,
text: 'Successfully copied invite link.', type: 'success',
text: 'Successfully copied invite link.',
});
})
.catch(() => {
setError();
}); });
}) } catch (e) {
.catch(() => { setError();
setSnackbar({ }
show: true,
type: 'error',
text: 'Could not copy invite link.',
});
});
}; };
const setError = () =>
setSnackbar({
show: true,
type: 'error',
text: 'Could not copy invite link.',
});
return ( return (
<div <div
style={{ style={{

View File

@ -39,7 +39,10 @@ export function createContextField(context) {
api api
.create(context) .create(context)
.then(() => dispatch(addContextField(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) { export function updateContextField(context) {