diff --git a/web/app/js/components/BaseTable.jsx b/web/app/js/components/BaseTable.jsx
index 581b27bf8f..adb413e5e7 100644
--- a/web/app/js/components/BaseTable.jsx
+++ b/web/app/js/components/BaseTable.jsx
@@ -94,7 +94,8 @@ class BaseTable extends React.Component {
);
}
- return _.isNil(col.tooltip) ? tableCell : {tableCell};
+ return _.isNil(col.tooltip) ? tableCell :
+ {tableCell};
}
render() {
@@ -116,7 +117,7 @@ class BaseTable extends React.Component {
{
_.map(sortedTableRows, d => {
let key = !rowKey ? d.key : rowKey(d);
- return (
+ let tableRow = (
{ _.map(tableColumns, c => (
{c.render ? c.render(d) : _.get(d, c.dataIndex)}
- ))}
+ )
+ )}
);
- })}
+ return _.isNil(d.tooltip) ? tableRow :
+ {tableRow};
+ }
+ )}
diff --git a/web/app/js/components/TopRoutes.jsx b/web/app/js/components/TopRoutes.jsx
index 5cc1073d74..08a7b2d595 100644
--- a/web/app/js/components/TopRoutes.jsx
+++ b/web/app/js/components/TopRoutes.jsx
@@ -1,3 +1,4 @@
+import { UrlQueryParamTypes, addUrlProps } from 'react-url-query';
import Button from '@material-ui/core/Button';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
@@ -20,6 +21,16 @@ import { groupResourcesByNs } from './util/MetricUtils.jsx';
import { withContext } from './util/AppContext.jsx';
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 => ({
root: {
@@ -35,24 +46,28 @@ class TopRoutes extends React.Component {
api: PropTypes.shape({
PrefixedLink: PropTypes.func.isRequired,
}).isRequired,
- classes: PropTypes.shape({}).isRequired
+ classes: PropTypes.shape({}).isRequired,
+ query: topRoutesQueryPropType,
+ }
+ static defaultProps = {
+ query: {
+ resource_name: '',
+ resource_type: '',
+ namespace: '',
+ },
}
constructor(props) {
super(props);
this.api = this.props.api;
+ let query = _.merge({}, props.query, _.pick(this.props, _.keys(topRoutesQueryProps)));
+
this.state = {
- query: {
- resource_name: '',
- namespace: '',
- from_name: '',
- from_type: '',
- from_namespace: ''
- },
+ query: query,
error: null,
services: [],
- namespaces: [],
+ namespaces: ["default"],
resourcesByNs: {},
pollingInterval: 5000,
pendingRequests: false,
@@ -61,10 +76,12 @@ class TopRoutes extends React.Component {
}
componentDidMount() {
+ this._isMounted = true; // https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
this.startServerPolling();
}
componentWillUnmount() {
+ this._isMounted = false;
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 => {
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 });
};
@@ -136,6 +162,8 @@ class TopRoutes extends React.Component {
let [resource_type, resource_name] = resource.split("/");
query.resource_name = resource_name;
query.resource_type = resource_type;
+ this.handleUrlUpdate("resource_name", resource_name);
+ this.handleUrlUpdate("resource_type", resource_type);
this.setState({ query });
}
@@ -215,11 +243,14 @@ class TopRoutes extends React.Component {
.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;
+ if (_.isEmpty(dropdownOptions) && !_.isEmpty(dropdownVal)) {
+ dropdownOptions = [dropdownVal]; // populate from url if autocomplete hasn't loaded
+ }
+
return (
Resource
@@ -255,11 +286,11 @@ class TopRoutes extends React.Component {
{ this.renderRoutesQueryForm() }
{ emptyQuery ? null :
}
- { !this.state.requestInProgress ? null : }
+ { !this.state.requestInProgress || !this._isMounted ? null : }
);
}
}
-export default withContext(withStyles(styles, { withTheme: true })(TopRoutes));
+export default addUrlProps({ urlPropsQueryConfig })(withContext(withStyles(styles, { withTheme: true })(TopRoutes)));
diff --git a/web/app/js/components/TopRoutesModule.jsx b/web/app/js/components/TopRoutesModule.jsx
index 571d771b27..8a3eb50b48 100644
--- a/web/app/js/components/TopRoutesModule.jsx
+++ b/web/app/js/components/TopRoutesModule.jsx
@@ -1,3 +1,5 @@
+import { DefaultRoute, processTopRoutesResults } from './util/MetricUtils.jsx';
+
import ConfigureProfilesMsg from './ConfigureProfilesMsg.jsx';
import ErrorBanner from './ErrorBanner.jsx';
import PropTypes from 'prop-types';
@@ -6,7 +8,6 @@ import Spinner from './util/Spinner.jsx';
import TopRoutesTable from './TopRoutesTable.jsx';
import _ from 'lodash';
import { apiErrorPropType } from './util/ApiHelpers.jsx';
-import { processTopRoutesResults } from './util/MetricUtils.jsx';
import withREST from './util/withREST.jsx';
class TopRoutesBase extends React.Component {
@@ -41,7 +42,7 @@ 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 allRoutesUnknown = _.every(metrics, m => m.route === DefaultRoute);
let showCallToAction = !loading && (_.isEmpty(metrics) || allRoutesUnknown);
return (
diff --git a/web/app/js/components/TopRoutesTable.jsx b/web/app/js/components/TopRoutesTable.jsx
index df4e7ef061..d7418d814f 100644
--- a/web/app/js/components/TopRoutesTable.jsx
+++ b/web/app/js/components/TopRoutesTable.jsx
@@ -1,5 +1,6 @@
import { metricToFormatter, numericSort } from './util/Utils.js';
import BaseTable from './BaseTable.jsx';
+import { DefaultRoute } from './util/MetricUtils.jsx';
import PropTypes from 'prop-types';
import React from 'react';
import SuccessRateMiniChart from './util/SuccessRateMiniChart.jsx';
@@ -8,7 +9,16 @@ const routesColumns = [
{
title: "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",
diff --git a/web/app/js/components/util/MetricUtils.jsx b/web/app/js/components/util/MetricUtils.jsx
index 0d1bee9718..5a1fb60d2d 100644
--- a/web/app/js/components/util/MetricUtils.jsx
+++ b/web/app/js/components/util/MetricUtils.jsx
@@ -156,9 +156,11 @@ const processStatTable = table => {
.value();
};
+export const DefaultRoute = "[default]";
export const processTopRoutesResults = rows => {
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,
totalRequests: getTotalRequests(row),
requestRate: getRequestRate(row),