Add validation to the Service Profile form (#2490) (#2514)

Fixes #2490

The following PR adds validation to the New Service Profile popup form in the linkerd dashboard for the service and namespace user input.

* Add validation to the Service Profile form (#2490)

Signed-off-by: Gaurav Kumar <gaurav.kumar9825@gmail.com>

* Change RegExp to handle all cases

Signed-off-by: Gaurav Kumar <gaurav.kumar9825@gmail.com>

* Fix link and error msg issue

Signed-off-by: Gaurav Kumar <gaurav.kumar9825@gmail.com>

* Refactor code

Signed-off-by: Gaurav Kumar <gaurav.kumar9825@gmail.com>

* Revert to onBlur and remove warning on field clear

Signed-off-by: Gaurav Kumar <gaurav.kumar9825@gmail.com>

* Refactor code to make it more readable

Signed-off-by: Gaurav Kumar <gaurav.kumar9825@gmail.com>

* Reset state to initialstate on form close

Signed-off-by: Gaurav Kumar <gaurav.kumar9825@gmail.com>
This commit is contained in:
Gaurav Kumar 2019-03-30 03:27:52 +05:30 committed by Risha Mars
parent 9eab0e28a6
commit 23454d248d
1 changed files with 109 additions and 32 deletions

View File

@ -5,11 +5,14 @@ import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import FormControl from '@material-ui/core/FormControl';
import FormHelperText from '@material-ui/core/FormHelperText';
import IconButton from '@material-ui/core/IconButton';
import Input from '@material-ui/core/Input';
import InputLabel from '@material-ui/core/InputLabel';
import NoteAddIcon from '@material-ui/icons/NoteAdd';
import PropTypes from 'prop-types';
import React from 'react';
import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';
import _isEmpty from 'lodash/isEmpty';
import { withContext } from './util/AppContext.jsx';
@ -29,6 +32,7 @@ const styles = theme => ({
textField: {
marginLeft: theme.spacing.unit,
marginRight: theme.spacing.unit,
marginTop: 2 * theme.spacing.unit,
width: 200,
},
root: {
@ -36,14 +40,24 @@ const styles = theme => ({
marginBottom: theme.spacing.unit * 3,
},
});
const dns1035ServiceFmt = '^[a-z]([-a-z0-9]*[a-z0-9])?$';
const dns1123NamespaceFmt = '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$';
const serviceNameRegexp = RegExp(dns1035ServiceFmt);
const namespaceNameRegexp = RegExp(dns1123NamespaceFmt);
class ConfigureProfilesMsg extends React.Component {
constructor(props) {
super(props);
this.state = {
open: false,
error: {
service: false,
namespace: false
},
query: {
service: "",
namespace: ""
service: '',
namespace: ''
}
};
}
@ -53,7 +67,17 @@ class ConfigureProfilesMsg extends React.Component {
};
handleClose = () => {
this.setState({ open: false });
this.setState({
open: false,
error: {
service: false,
namespace: false
},
query: {
service: '',
namespace: ''
}
});
};
handleChange = name => {
@ -61,13 +85,30 @@ class ConfigureProfilesMsg extends React.Component {
return e => {
state.query[name] = e.target.value;
state.error[name] = false;
this.setState(state);
};
}
};
validateFields = (type, name) => {
let error = this.state.error;
if (_isEmpty(name)) {
error[type] = false;
} else {
let match = type === 'service' ?
serviceNameRegexp.test(name) :
namespaceNameRegexp.test(name);
error[type] = !match;
}
this.setState({ error });
};
renderDownloadProfileForm = () => {
const { api, classes, showAsIcon } = this.props;
let { query } = this.state;
let { query, error } = this.state;
let downloadUrl = api.prefixedUrl(`/profiles/new?service=${query.service}&namespace=${query.namespace}`);
let button;
@ -89,14 +130,27 @@ class ConfigureProfilesMsg extends React.Component {
variant="outlined"
color="primary"
size="small"
onClick={this.handleClickOpen}>Create Service Profile
onClick={this.handleClickOpen}>
Create Service Profile
</Button>
);
}
let disableDownloadButton = _isEmpty(query.service) || _isEmpty(query.namespace) ||
error.service || error.namespace;
let downloadButton = (
<Button
disabled={disableDownloadButton}
onClick={() => this.handleClose(downloadUrl)}
color="primary">
Download
</Button>
);
return (
<React.Fragment>
{button}
<Dialog
open={this.state.open}
onClose={this.handleClose}
@ -104,33 +158,55 @@ class ConfigureProfilesMsg extends React.Component {
<DialogTitle id="form-dialog-title">New service profile</DialogTitle>
<DialogContent>
<DialogContentText>
To create a service profile, download a profile and then apply it with `kubectl apply`.
To create a service profile, download a profile and then apply it
with `kubectl apply`.
</DialogContentText>
<TextField
id="service"
label="Service"
<FormControl
className={classes.textField}
value={this.state.service}
onChange={this.handleChange('service')}
margin="normal" />
<TextField
id="namespace"
label="Namespace"
onBlur={() => this.validateFields('service', query.service)}
error={error.service}>
<InputLabel htmlFor="component-error">Service</InputLabel>
<Input
id="component-error"
value={this.state.service}
onChange={this.handleChange('service')}
aria-describedby="component-error-text" />
{error.service && (
<FormHelperText id="component-error-text">
Service name must consist of lower case alphanumeric characters or &#45;
start with an alphabetic character, and end with an alphanumeric character
</FormHelperText>
)}
</FormControl>
<FormControl
className={classes.textField}
value={this.state.name}
onChange={this.handleChange('namespace')}
margin="normal" />
onBlur={() => this.validateFields('namespace', query.namespace)}
error={error.namespace}>
<InputLabel htmlFor="component-error">Namespace</InputLabel>
<Input
id="component-error"
value={this.state.name}
onChange={this.handleChange('namespace')}
aria-describedby="component-error-text" />
{error.namespace && (
<FormHelperText id="component-error-text">
Namespace must consist of lower case alphanumeric characters or &#45;
and must start and end with an alphanumeric character
</FormHelperText>
)}
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={this.handleClose} color="primary">Cancel</Button>
<a href={downloadUrl} style={{ textDecoration: 'none' }}>
<Button
disabled={_isEmpty(query.service) || _isEmpty(query.namespace)}
onClick={this.handleClose}
color="primary">Download
</Button>
</a>
<Button onClick={this.handleClose} color="primary">
Cancel
</Button>
{disableDownloadButton ?
downloadButton :
<a
href={disableDownloadButton ? '' : downloadUrl}
style={{ textDecoration: 'none' }}>{downloadButton}
</a>
}
</DialogActions>
</Dialog>
</React.Fragment>
@ -145,8 +221,9 @@ class ConfigureProfilesMsg extends React.Component {
return (
<CardContent>
<Typography component="div">
No named route traffic found. This could be because the service is not receiving any traffic,
or because there is no service profile configured. Does the service have a service profile?
No named route traffic found. This could be because the service is
not receiving any traffic, or because there is no service profile
configured. Does the service have a service profile?
{this.renderDownloadProfileForm()}
</Typography>
</CardContent>