mirror of https://github.com/linkerd/linkerd2.git
Miscellaneous improvements to the Top Routes UX (#1963)
* Renames UNKNOWN in the tables to (default) which is less scary (#1946) * adds a tooltip explaining what (default) is * adds url props to the Top Routes page, so that they query can be populated by a url * fixes a js error that occurs when switching pages
This commit is contained in:
parent
0f8bcc9159
commit
6214c9a15d
|
@ -94,7 +94,8 @@ class BaseTable extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _.isNil(col.tooltip) ? tableCell : <Tooltip key={col.key || col.dataIndex} placement="top" title={col.tooltip}>{tableCell}</Tooltip>;
|
return _.isNil(col.tooltip) ? tableCell :
|
||||||
|
<Tooltip key={col.key || col.dataIndex} placement="top" title={col.tooltip}>{tableCell}</Tooltip>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -116,7 +117,7 @@ class BaseTable extends React.Component {
|
||||||
{
|
{
|
||||||
_.map(sortedTableRows, d => {
|
_.map(sortedTableRows, d => {
|
||||||
let key = !rowKey ? d.key : rowKey(d);
|
let key = !rowKey ? d.key : rowKey(d);
|
||||||
return (
|
let tableRow = (
|
||||||
<TableRow key={key}>
|
<TableRow key={key}>
|
||||||
{ _.map(tableColumns, c => (
|
{ _.map(tableColumns, c => (
|
||||||
<TableCell
|
<TableCell
|
||||||
|
@ -125,10 +126,14 @@ class BaseTable extends React.Component {
|
||||||
numeric={c.isNumeric}>
|
numeric={c.isNumeric}>
|
||||||
{c.render ? c.render(d) : _.get(d, c.dataIndex)}
|
{c.render ? c.render(d) : _.get(d, c.dataIndex)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
})}
|
return _.isNil(d.tooltip) ? tableRow :
|
||||||
|
<Tooltip key={`table-row-${key}`} placement="left" title={d.tooltip}>{tableRow}</Tooltip>;
|
||||||
|
}
|
||||||
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { UrlQueryParamTypes, addUrlProps } from 'react-url-query';
|
||||||
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';
|
||||||
|
@ -20,6 +21,16 @@ 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';
|
import { withStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
const topRoutesQueryProps = {
|
||||||
|
resource_name: PropTypes.string,
|
||||||
|
resource_type: PropTypes.string,
|
||||||
|
namespace: PropTypes.string,
|
||||||
|
};
|
||||||
|
const topRoutesQueryPropType = PropTypes.shape(topRoutesQueryProps);
|
||||||
|
|
||||||
|
const urlPropsQueryConfig = _.mapValues(topRoutesQueryProps, () => {
|
||||||
|
return { type: UrlQueryParamTypes.string };
|
||||||
|
});
|
||||||
|
|
||||||
const styles = theme => ({
|
const styles = theme => ({
|
||||||
root: {
|
root: {
|
||||||
|
@ -35,24 +46,28 @@ class TopRoutes extends React.Component {
|
||||||
api: PropTypes.shape({
|
api: PropTypes.shape({
|
||||||
PrefixedLink: PropTypes.func.isRequired,
|
PrefixedLink: PropTypes.func.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
classes: PropTypes.shape({}).isRequired
|
classes: PropTypes.shape({}).isRequired,
|
||||||
|
query: topRoutesQueryPropType,
|
||||||
|
}
|
||||||
|
static defaultProps = {
|
||||||
|
query: {
|
||||||
|
resource_name: '',
|
||||||
|
resource_type: '',
|
||||||
|
namespace: '',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.api = this.props.api;
|
this.api = this.props.api;
|
||||||
|
|
||||||
|
let query = _.merge({}, props.query, _.pick(this.props, _.keys(topRoutesQueryProps)));
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
query: {
|
query: query,
|
||||||
resource_name: '',
|
|
||||||
namespace: '',
|
|
||||||
from_name: '',
|
|
||||||
from_type: '',
|
|
||||||
from_namespace: ''
|
|
||||||
},
|
|
||||||
error: null,
|
error: null,
|
||||||
services: [],
|
services: [],
|
||||||
namespaces: [],
|
namespaces: ["default"],
|
||||||
resourcesByNs: {},
|
resourcesByNs: {},
|
||||||
pollingInterval: 5000,
|
pollingInterval: 5000,
|
||||||
pendingRequests: false,
|
pendingRequests: false,
|
||||||
|
@ -61,10 +76,12 @@ class TopRoutes extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this._isMounted = true; // https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
|
||||||
this.startServerPolling();
|
this.startServerPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
this._isMounted = false;
|
||||||
this.stopServerPolling();
|
this.stopServerPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,9 +141,18 @@ class TopRoutes extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Each time state.query is updated, this method calls the equivalent
|
||||||
|
// onChange method to reflect the update in url query params. These onChange
|
||||||
|
// methods are automatically added to props by react-url-query.
|
||||||
|
handleUrlUpdate = (name, formVal) => {
|
||||||
|
this.props[`onChange${_.upperFirst(name)}`](formVal);
|
||||||
|
}
|
||||||
|
|
||||||
handleNamespaceSelect = e => {
|
handleNamespaceSelect = e => {
|
||||||
let query = this.state.query;
|
let query = this.state.query;
|
||||||
query.namespace = _.get(e, 'target.value');
|
let formVal = _.get(e, 'target.value');
|
||||||
|
query.namespace = formVal;
|
||||||
|
this.handleUrlUpdate("namespace", formVal);
|
||||||
this.setState({ query });
|
this.setState({ query });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -136,6 +162,8 @@ class TopRoutes extends React.Component {
|
||||||
let [resource_type, resource_name] = resource.split("/");
|
let [resource_type, resource_name] = resource.split("/");
|
||||||
query.resource_name = resource_name;
|
query.resource_name = resource_name;
|
||||||
query.resource_type = resource_type;
|
query.resource_type = resource_type;
|
||||||
|
this.handleUrlUpdate("resource_name", resource_name);
|
||||||
|
this.handleUrlUpdate("resource_type", resource_type);
|
||||||
this.setState({ query });
|
this.setState({ query });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,11 +243,14 @@ class TopRoutes extends React.Component {
|
||||||
.map(svc => `service/${svc.name}`).value();
|
.map(svc => `service/${svc.name}`).value();
|
||||||
let otherResources = resourcesByNs[query.namespace] || [];
|
let otherResources = resourcesByNs[query.namespace] || [];
|
||||||
|
|
||||||
|
|
||||||
let dropdownOptions = _.sortBy(_.concat(servicesWithPrefix, otherResources));
|
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;
|
||||||
|
|
||||||
|
if (_.isEmpty(dropdownOptions) && !_.isEmpty(dropdownVal)) {
|
||||||
|
dropdownOptions = [dropdownVal]; // populate from url if autocomplete hasn't loaded
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl className={classes.formControl}>
|
<FormControl className={classes.formControl}>
|
||||||
<InputLabel htmlFor={`${key}-dropdown`}>Resource</InputLabel>
|
<InputLabel htmlFor={`${key}-dropdown`}>Resource</InputLabel>
|
||||||
|
@ -255,11 +286,11 @@ class TopRoutes extends React.Component {
|
||||||
{ this.renderRoutesQueryForm() }
|
{ this.renderRoutesQueryForm() }
|
||||||
{ emptyQuery ? 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 || !this._isMounted ? null : <TopRoutesModule query={this.state.query} /> }
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withContext(withStyles(styles, { withTheme: true })(TopRoutes));
|
export default addUrlProps({ urlPropsQueryConfig })(withContext(withStyles(styles, { withTheme: true })(TopRoutes)));
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { DefaultRoute, processTopRoutesResults } from './util/MetricUtils.jsx';
|
||||||
|
|
||||||
import ConfigureProfilesMsg from './ConfigureProfilesMsg.jsx';
|
import ConfigureProfilesMsg from './ConfigureProfilesMsg.jsx';
|
||||||
import ErrorBanner from './ErrorBanner.jsx';
|
import ErrorBanner from './ErrorBanner.jsx';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
@ -6,7 +8,6 @@ import Spinner from './util/Spinner.jsx';
|
||||||
import TopRoutesTable from './TopRoutesTable.jsx';
|
import TopRoutesTable from './TopRoutesTable.jsx';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { apiErrorPropType } from './util/ApiHelpers.jsx';
|
import { apiErrorPropType } from './util/ApiHelpers.jsx';
|
||||||
import { processTopRoutesResults } from './util/MetricUtils.jsx';
|
|
||||||
import withREST from './util/withREST.jsx';
|
import withREST from './util/withREST.jsx';
|
||||||
|
|
||||||
class TopRoutesBase extends React.Component {
|
class TopRoutesBase extends React.Component {
|
||||||
|
@ -41,7 +42,7 @@ 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 allRoutesUnknown = _.every(metrics, m => m.route === DefaultRoute);
|
||||||
let showCallToAction = !loading && (_.isEmpty(metrics) || allRoutesUnknown);
|
let showCallToAction = !loading && (_.isEmpty(metrics) || allRoutesUnknown);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { metricToFormatter, numericSort } from './util/Utils.js';
|
import { metricToFormatter, numericSort } from './util/Utils.js';
|
||||||
import BaseTable from './BaseTable.jsx';
|
import BaseTable from './BaseTable.jsx';
|
||||||
|
import { DefaultRoute } from './util/MetricUtils.jsx';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import SuccessRateMiniChart from './util/SuccessRateMiniChart.jsx';
|
import SuccessRateMiniChart from './util/SuccessRateMiniChart.jsx';
|
||||||
|
@ -8,7 +9,16 @@ const routesColumns = [
|
||||||
{
|
{
|
||||||
title: "Route",
|
title: "Route",
|
||||||
dataIndex: "route",
|
dataIndex: "route",
|
||||||
sorter: (a, b) => (a.route).localeCompare(b.route)
|
sorter: (a, b) => {
|
||||||
|
if (a.route === DefaultRoute) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
if (b.route === DefaultRoute) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (a.route).localeCompare(b.route);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Authority",
|
title: "Authority",
|
||||||
|
|
|
@ -156,9 +156,11 @@ const processStatTable = table => {
|
||||||
.value();
|
.value();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DefaultRoute = "[default]";
|
||||||
export const processTopRoutesResults = rows => {
|
export const processTopRoutesResults = rows => {
|
||||||
return _.map(rows, row => ({
|
return _.map(rows, row => ({
|
||||||
route: row.route || "UNKNOWN",
|
route: row.route || DefaultRoute,
|
||||||
|
tooltip: !_.isEmpty(row.route) ? null : "Traffic does not match any configured routes",
|
||||||
authority: row.authority,
|
authority: row.authority,
|
||||||
totalRequests: getTotalRequests(row),
|
totalRequests: getTotalRequests(row),
|
||||||
requestRate: getRequestRate(row),
|
requestRate: getRequestRate(row),
|
||||||
|
|
Loading…
Reference in New Issue