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