Add the create new service profile button by default at /routes (#1941)

* Show the call to action if all metric rows are UNKNOWN
* Also enable creating of a new service profile by default on the Top Routes page
* Fix bug in passing down props.classes from the Navigation component
* Adjust form appearance
This commit is contained in:
Risha Mars 2018-12-06 17:26:21 -08:00 committed by GitHub
parent 3ff971fd59
commit 692c4ca75b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 87 additions and 40 deletions

View File

@ -1,11 +1,12 @@
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent'; import CardContent from '@material-ui/core/CardContent';
import Dialog from '@material-ui/core/Dialog'; import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions'; import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent'; import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText'; import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle'; import DialogTitle from '@material-ui/core/DialogTitle';
import IconButton from '@material-ui/core/IconButton';
import NoteAddIcon from '@material-ui/icons/NoteAdd';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import TextField from '@material-ui/core/TextField'; import TextField from '@material-ui/core/TextField';
@ -18,6 +19,9 @@ const styles = theme => ({
button: { button: {
margin: theme.spacing.unit, margin: theme.spacing.unit,
}, },
margin: {
marginRight: theme.spacing.unit,
},
container: { container: {
display: 'flex', display: 'flex',
flexWrap: 'wrap', flexWrap: 'wrap',
@ -62,19 +66,36 @@ class ConfigureProfilesMsg extends React.Component {
} }
renderDownloadProfileForm = () => { renderDownloadProfileForm = () => {
const { api, classes } = this.props; const { api, classes, showAsIcon } = this.props;
let { query } = this.state; let { query } = this.state;
let downloadUrl = api.prefixedUrl(`/profiles/new?service=${query.service}&namespace=${query.namespace}`); let downloadUrl = api.prefixedUrl(`/profiles/new?service=${query.service}&namespace=${query.namespace}`);
let button;
return ( if (showAsIcon) {
<React.Fragment> button = (
<IconButton
onClick={this.handleClickOpen}
aria-label="Add"
className={classes.margin}
variant="outlined">
<NoteAddIcon fontSize="small" />
</IconButton>
);
} else {
button = (
<Button <Button
className={classes.button} className={classes.button}
variant="outlined" variant="outlined"
color="primary" color="primary"
size="small"
onClick={this.handleClickOpen}>Create Service Profile onClick={this.handleClickOpen}>Create Service Profile
</Button> </Button>
);
}
return (
<React.Fragment>
{button}
<Dialog <Dialog
open={this.state.open} open={this.state.open}
@ -117,26 +138,34 @@ class ConfigureProfilesMsg extends React.Component {
} }
render() { render() {
const { classes } = this.props; const { showAsIcon } = this.props;
return ( if (showAsIcon) {
<Card className={classes.root}> return this.renderDownloadProfileForm();
} else {
return (
<CardContent> <CardContent>
<Typography component="div"> <Typography component="div">
No traffic found. 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()} {this.renderDownloadProfileForm()}
</Typography> </Typography>
</CardContent> </CardContent>
</Card> );
); }
} }
} }
ConfigureProfilesMsg.propTypes = { ConfigureProfilesMsg.propTypes = {
api: PropTypes.shape({ api: PropTypes.shape({
prefixedFetch: PropTypes.func.isRequired, prefixedUrl: PropTypes.func.isRequired,
}).isRequired, }).isRequired,
classes: PropTypes.shape({}).isRequired classes: PropTypes.shape({}).isRequired,
showAsIcon: PropTypes.bool
};
ConfigureProfilesMsg.defaultProps = {
showAsIcon: false
}; };
export default withContext(withStyles(styles, { withTheme: true })(ConfigureProfilesMsg)); export default withContext(withStyles(styles, { withTheme: true })(ConfigureProfilesMsg));

View File

@ -199,7 +199,7 @@ class NavigationBase extends React.Component {
); );
} }
render() { render() {
const { classes, ChildComponent } = this.props; const { classes, ChildComponent, ...otherProps } = this.props;
return ( return (
<div className={classes.root}> <div className={classes.root}>
@ -287,7 +287,7 @@ class NavigationBase extends React.Component {
<main className={classNames(classes.content, {[classes.contentDrawerClose]: !this.state.drawerOpen})}> <main className={classNames(classes.content, {[classes.contentDrawerClose]: !this.state.drawerOpen})}>
<div className={classes.toolbar} /> <div className={classes.toolbar} />
<div className="main-content"><ChildComponent {...this.props} /></div> <div className="main-content"><ChildComponent {...otherProps} /></div>
</main> </main>
</div> </div>
); );

View File

@ -1,6 +1,8 @@
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Card from '@material-ui/core/Card'; import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent'; import CardContent from '@material-ui/core/CardContent';
import ConfigureProfilesMsg from './ConfigureProfilesMsg.jsx';
import Divider from '@material-ui/core/Divider';
import ErrorBanner from './ErrorBanner.jsx'; import ErrorBanner from './ErrorBanner.jsx';
import FormControl from '@material-ui/core/FormControl'; import FormControl from '@material-ui/core/FormControl';
import FormHelperText from '@material-ui/core/FormHelperText'; import FormHelperText from '@material-ui/core/FormHelperText';
@ -12,15 +14,28 @@ import QueryToCliCmd from './QueryToCliCmd.jsx';
import React from 'react'; import React from 'react';
import Select from '@material-ui/core/Select'; import Select from '@material-ui/core/Select';
import TopRoutesModule from './TopRoutesModule.jsx'; import TopRoutesModule from './TopRoutesModule.jsx';
import Typography from '@material-ui/core/Typography';
import _ from 'lodash'; import _ from 'lodash';
import { groupResourcesByNs } from './util/MetricUtils.jsx'; import { groupResourcesByNs } from './util/MetricUtils.jsx';
import { withContext } from './util/AppContext.jsx'; import { withContext } from './util/AppContext.jsx';
import { withStyles } from '@material-ui/core/styles';
const styles = theme => ({
root: {
marginTop: 3 * theme.spacing.unit,
marginBottom:theme.spacing.unit,
},
formControl: {
minWidth: 200,
},
});
class TopRoutes extends React.Component { class TopRoutes extends React.Component {
static propTypes = { static propTypes = {
api: PropTypes.shape({ api: PropTypes.shape({
PrefixedLink: PropTypes.func.isRequired, PrefixedLink: PropTypes.func.isRequired,
}).isRequired }).isRequired,
classes: PropTypes.shape({}).isRequired
} }
constructor(props) { constructor(props) {
@ -125,20 +140,20 @@ class TopRoutes extends React.Component {
} }
renderRoutesQueryForm = () => { renderRoutesQueryForm = () => {
const { classes } = this.props;
return ( return (
<CardContent> <CardContent>
<Grid container direction="column" spacing={16}> <Grid container direction="column" spacing={16}>
<Grid item container spacing={8} alignItems="center"> <Grid item container spacing={32} alignItems="center" justify="flex-start">
<Grid item xs={6} md={3}> <Grid item>
{ this.renderNamespaceDropdown("Namespace", "namespace", "Namespace to query") } { this.renderNamespaceDropdown("Namespace", "namespace", "Namespace to query") }
</Grid> </Grid>
<Grid item xs={6} md={3}> <Grid item>
{ this.renderServiceDropdown() } { this.renderServiceDropdown() }
</Grid> </Grid>
</Grid>
<Grid item container spacing={8} alignItems="center">
<Grid item> <Grid item>
<Button <Button
color="primary" color="primary"
@ -148,6 +163,7 @@ class TopRoutes extends React.Component {
Start Start
</Button> </Button>
</Grid> </Grid>
<Grid item> <Grid item>
<Button <Button
color="default" color="default"
@ -159,13 +175,17 @@ class TopRoutes extends React.Component {
</Grid> </Grid>
</Grid> </Grid>
</Grid> </Grid>
<Divider light className={classes.root} />
<Typography variant="caption">You can also create a new profile <ConfigureProfilesMsg showAsIcon={true} /></Typography>
</CardContent> </CardContent>
); );
} }
renderNamespaceDropdown = (title, key, helperText) => { renderNamespaceDropdown = (title, key, helperText) => {
const { classes } = this.props;
return ( return (
<FormControl> <FormControl className={classes.formControl}>
<InputLabel htmlFor={`${key}-dropdown`}>{title}</InputLabel> <InputLabel htmlFor={`${key}-dropdown`}>{title}</InputLabel>
<Select <Select
value={this.state.query[key]} value={this.state.query[key]}
@ -186,25 +206,27 @@ class TopRoutes extends React.Component {
} }
renderServiceDropdown = () => { renderServiceDropdown = () => {
let key = "resource_name"; const { classes } = this.props;
let services = _.chain(this.state.services) let { query, services, resourcesByNs } = this.state;
.filter(['namespace', this.state.query.namespace])
.map(svc => `service/${svc.name}`).value();
let otherResources = this.state.resourcesByNs[this.state.query.namespace] || [];
let { query } = this.state; let key = "resource_name";
let dropdownOptions = _.sortBy(_.concat(services, otherResources)); let servicesWithPrefix = _.chain(services)
.filter(['namespace', query.namespace])
.map(svc => `service/${svc.name}`).value();
let otherResources = resourcesByNs[query.namespace] || [];
let dropdownOptions = _.sortBy(_.concat(servicesWithPrefix, otherResources));
let dropdownVal = _.isEmpty(query.resource_name) || _.isEmpty(query.resource_type) ? "" : let dropdownVal = _.isEmpty(query.resource_name) || _.isEmpty(query.resource_type) ? "" :
query.resource_type + "/" + query.resource_name; query.resource_type + "/" + query.resource_name;
return ( return (
<FormControl> <FormControl className={classes.formControl}>
<InputLabel htmlFor={`${key}-dropdown`}>Resource</InputLabel> <InputLabel htmlFor={`${key}-dropdown`}>Resource</InputLabel>
<Select <Select
value={dropdownVal} value={dropdownVal}
onChange={this.handleResourceSelect} onChange={this.handleResourceSelect}
disabled={_.isEmpty(query.namespace)} disabled={_.isEmpty(query.namespace)}
autoWidth
inputProps={{ inputProps={{
name: key, name: key,
id: `${key}-dropdown`, id: `${key}-dropdown`,
@ -221,13 +243,7 @@ class TopRoutes extends React.Component {
render() { render() {
let query = this.state.query; let query = this.state.query;
let from = ''; let emptyQuery = _.isEmpty(query.resource_name) || _.isEmpty(query.resource_type);
if (_.isEmpty(query.from_type)) {
from = query.from_name;
} else {
from = `${query.from_type}${_.isEmpty(query.from_name) ? "" : "/"}${query.from_name}`;
}
query.from = from;
return ( return (
<div> <div>
@ -237,7 +253,7 @@ class TopRoutes extends React.Component {
} }
<Card> <Card>
{ this.renderRoutesQueryForm() } { this.renderRoutesQueryForm() }
{ _.isEmpty(query.resource_name) || _.isEmpty(query.resource_type) ? null : { emptyQuery ? null :
<QueryToCliCmd cmdName="routes" query={query} resource={query.resource_type + "/" + query.resource_name} /> } <QueryToCliCmd cmdName="routes" query={query} resource={query.resource_type + "/" + query.resource_name} /> }
{ !this.state.requestInProgress ? null : <TopRoutesModule query={this.state.query} /> } { !this.state.requestInProgress ? null : <TopRoutesModule query={this.state.query} /> }
</Card> </Card>
@ -246,4 +262,4 @@ class TopRoutes extends React.Component {
} }
} }
export default withContext(TopRoutes); export default withContext(withStyles(styles, { withTheme: true })(TopRoutes));

View File

@ -41,13 +41,15 @@ class TopRoutesBase extends React.Component {
render() { render() {
const {data, loading} = this.props; const {data, loading} = this.props;
let metrics = processTopRoutesResults(_.get(data, '[0].routes.rows', [])); let metrics = processTopRoutesResults(_.get(data, '[0].routes.rows', []));
let allRoutesUnknown = _.every(metrics, m => m.route === "UNKNOWN");
let showCallToAction = !loading && (_.isEmpty(metrics) || allRoutesUnknown);
return ( return (
<React.Fragment> <React.Fragment>
{this.loading()} {this.loading()}
{this.banner()} {this.banner()}
{ !loading && _.isEmpty(metrics) ? <ConfigureProfilesMsg /> : null}
<TopRoutesTable rows={metrics} /> <TopRoutesTable rows={metrics} />
{ showCallToAction ? <ConfigureProfilesMsg /> : null}
</React.Fragment> </React.Fragment>
); );
} }