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:
Risha Mars 2018-12-10 10:40:24 -08:00 committed by GitHub
parent 0f8bcc9159
commit 6214c9a15d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 70 additions and 21 deletions

View File

@ -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>

View File

@ -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)));

View File

@ -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 (

View File

@ -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",

View File

@ -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),