mirror of https://github.com/linkerd/linkerd2.git
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:
parent
3ff971fd59
commit
692c4ca75b
|
@ -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));
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue