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 Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
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 IconButton from '@material-ui/core/IconButton';
|
||||
import NoteAddIcon from '@material-ui/icons/NoteAdd';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
|
@ -18,6 +19,9 @@ const styles = theme => ({
|
|||
button: {
|
||||
margin: theme.spacing.unit,
|
||||
},
|
||||
margin: {
|
||||
marginRight: theme.spacing.unit,
|
||||
},
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
|
@ -62,19 +66,36 @@ class ConfigureProfilesMsg extends React.Component {
|
|||
}
|
||||
|
||||
renderDownloadProfileForm = () => {
|
||||
const { api, classes } = this.props;
|
||||
const { api, classes, showAsIcon } = this.props;
|
||||
let { query } = this.state;
|
||||
|
||||
let downloadUrl = api.prefixedUrl(`/profiles/new?service=${query.service}&namespace=${query.namespace}`);
|
||||
let button;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
if (showAsIcon) {
|
||||
button = (
|
||||
<IconButton
|
||||
onClick={this.handleClickOpen}
|
||||
aria-label="Add"
|
||||
className={classes.margin}
|
||||
variant="outlined">
|
||||
<NoteAddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
);
|
||||
} else {
|
||||
button = (
|
||||
<Button
|
||||
className={classes.button}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={this.handleClickOpen}>Create Service Profile
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
{button}
|
||||
|
||||
<Dialog
|
||||
open={this.state.open}
|
||||
|
@ -117,26 +138,34 @@ class ConfigureProfilesMsg extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
const { showAsIcon } = this.props;
|
||||
|
||||
return (
|
||||
<Card className={classes.root}>
|
||||
if (showAsIcon) {
|
||||
return this.renderDownloadProfileForm();
|
||||
} else {
|
||||
return (
|
||||
<CardContent>
|
||||
<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()}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConfigureProfilesMsg.propTypes = {
|
||||
api: PropTypes.shape({
|
||||
prefixedFetch: PropTypes.func.isRequired,
|
||||
prefixedUrl: PropTypes.func.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));
|
||||
|
|
|
@ -199,7 +199,7 @@ class NavigationBase extends React.Component {
|
|||
);
|
||||
}
|
||||
render() {
|
||||
const { classes, ChildComponent } = this.props;
|
||||
const { classes, ChildComponent, ...otherProps } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
|
@ -287,7 +287,7 @@ class NavigationBase extends React.Component {
|
|||
|
||||
<main className={classNames(classes.content, {[classes.contentDrawerClose]: !this.state.drawerOpen})}>
|
||||
<div className={classes.toolbar} />
|
||||
<div className="main-content"><ChildComponent {...this.props} /></div>
|
||||
<div className="main-content"><ChildComponent {...otherProps} /></div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import Button from '@material-ui/core/Button';
|
||||
import Card from '@material-ui/core/Card';
|
||||
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 FormControl from '@material-ui/core/FormControl';
|
||||
import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
|
@ -12,15 +14,28 @@ import QueryToCliCmd from './QueryToCliCmd.jsx';
|
|||
import React from 'react';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import TopRoutesModule from './TopRoutesModule.jsx';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import _ from 'lodash';
|
||||
import { groupResourcesByNs } from './util/MetricUtils.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 {
|
||||
static propTypes = {
|
||||
api: PropTypes.shape({
|
||||
PrefixedLink: PropTypes.func.isRequired,
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
classes: PropTypes.shape({}).isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -125,20 +140,20 @@ class TopRoutes extends React.Component {
|
|||
}
|
||||
|
||||
renderRoutesQueryForm = () => {
|
||||
const { classes } = this.props;
|
||||
|
||||
return (
|
||||
<CardContent>
|
||||
<Grid container direction="column" spacing={16}>
|
||||
<Grid item container spacing={8} alignItems="center">
|
||||
<Grid item xs={6} md={3}>
|
||||
<Grid item container spacing={32} alignItems="center" justify="flex-start">
|
||||
<Grid item>
|
||||
{ this.renderNamespaceDropdown("Namespace", "namespace", "Namespace to query") }
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6} md={3}>
|
||||
<Grid item>
|
||||
{ this.renderServiceDropdown() }
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid item container spacing={8} alignItems="center">
|
||||
<Grid item>
|
||||
<Button
|
||||
color="primary"
|
||||
|
@ -148,6 +163,7 @@ class TopRoutes extends React.Component {
|
|||
Start
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Button
|
||||
color="default"
|
||||
|
@ -159,13 +175,17 @@ class TopRoutes extends React.Component {
|
|||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Divider light className={classes.root} />
|
||||
<Typography variant="caption">You can also create a new profile <ConfigureProfilesMsg showAsIcon={true} /></Typography>
|
||||
</CardContent>
|
||||
);
|
||||
}
|
||||
|
||||
renderNamespaceDropdown = (title, key, helperText) => {
|
||||
const { classes } = this.props;
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor={`${key}-dropdown`}>{title}</InputLabel>
|
||||
<Select
|
||||
value={this.state.query[key]}
|
||||
|
@ -186,25 +206,27 @@ class TopRoutes extends React.Component {
|
|||
}
|
||||
|
||||
renderServiceDropdown = () => {
|
||||
let key = "resource_name";
|
||||
let services = _.chain(this.state.services)
|
||||
.filter(['namespace', this.state.query.namespace])
|
||||
.map(svc => `service/${svc.name}`).value();
|
||||
let otherResources = this.state.resourcesByNs[this.state.query.namespace] || [];
|
||||
const { classes } = this.props;
|
||||
let { query, services, resourcesByNs } = this.state;
|
||||
|
||||
let { query } = this.state;
|
||||
let dropdownOptions = _.sortBy(_.concat(services, otherResources));
|
||||
let key = "resource_name";
|
||||
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) ? "" :
|
||||
query.resource_type + "/" + query.resource_name;
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor={`${key}-dropdown`}>Resource</InputLabel>
|
||||
<Select
|
||||
value={dropdownVal}
|
||||
onChange={this.handleResourceSelect}
|
||||
disabled={_.isEmpty(query.namespace)}
|
||||
autoWidth
|
||||
inputProps={{
|
||||
name: key,
|
||||
id: `${key}-dropdown`,
|
||||
|
@ -221,13 +243,7 @@ class TopRoutes extends React.Component {
|
|||
|
||||
render() {
|
||||
let query = this.state.query;
|
||||
let from = '';
|
||||
if (_.isEmpty(query.from_type)) {
|
||||
from = query.from_name;
|
||||
} else {
|
||||
from = `${query.from_type}${_.isEmpty(query.from_name) ? "" : "/"}${query.from_name}`;
|
||||
}
|
||||
query.from = from;
|
||||
let emptyQuery = _.isEmpty(query.resource_name) || _.isEmpty(query.resource_type);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -237,7 +253,7 @@ class TopRoutes extends React.Component {
|
|||
}
|
||||
<Card>
|
||||
{ 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} /> }
|
||||
{ !this.state.requestInProgress ? null : <TopRoutesModule query={this.state.query} /> }
|
||||
</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() {
|
||||
const {data, loading} = this.props;
|
||||
let metrics = processTopRoutesResults(_.get(data, '[0].routes.rows', []));
|
||||
let allRoutesUnknown = _.every(metrics, m => m.route === "UNKNOWN");
|
||||
let showCallToAction = !loading && (_.isEmpty(metrics) || allRoutesUnknown);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.loading()}
|
||||
{this.banner()}
|
||||
{ !loading && _.isEmpty(metrics) ? <ConfigureProfilesMsg /> : null}
|
||||
<TopRoutesTable rows={metrics} />
|
||||
{ showCallToAction ? <ConfigureProfilesMsg /> : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue