mirror of https://github.com/linkerd/linkerd2.git
Update Top Routes in dashboard to use new API, add `to` query options (#2112)
The recent routes API changes caused the Top Routes tab to stop working, as it wasn't looking for the changed structure of the response. This PR updates that page to accept the new API response. This PR also adds to fields to the Top Routes query form, so that the equivalent of linkerd routes deploy --to deploy/authors will work in the dashboard.
This commit is contained in:
parent
28f662c9c6
commit
cce6183c4a
|
@ -4,7 +4,8 @@ import {
|
|||
emptyTapQuery,
|
||||
httpMethods,
|
||||
tapQueryPropType,
|
||||
tapQueryProps
|
||||
tapQueryProps,
|
||||
tapResourceTypes
|
||||
} from './util/TapUtils.jsx';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Card from '@material-ui/core/Card';
|
||||
|
@ -42,14 +43,6 @@ import _upperFirst from 'lodash/upperFirst';
|
|||
import _values from 'lodash/values';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
||||
// you can also tap resources to tap all pods in the resource
|
||||
const resourceTypes = [
|
||||
"deployment",
|
||||
"daemonset",
|
||||
"pod",
|
||||
"replicationcontroller",
|
||||
"statefulset"
|
||||
];
|
||||
|
||||
const getResourceList = (resourcesByNs, ns) => {
|
||||
return resourcesByNs[ns] || _uniq(_flatten(_values(resourcesByNs)));
|
||||
|
@ -241,7 +234,7 @@ class TapQueryForm extends React.Component {
|
|||
let nsEmpty = _isNil(selectedNs) || _isEmpty(selectedNs);
|
||||
let { classes } = this.props;
|
||||
|
||||
let resourceOptions = resourceTypes.concat(
|
||||
let resourceOptions = tapResourceTypes.concat(
|
||||
this.state.autocomplete[resourceKey] || [],
|
||||
nsEmpty ? [] : [`namespace/${selectedNs}`]
|
||||
).sort();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
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';
|
||||
|
@ -24,6 +25,7 @@ import _pick from 'lodash/pick';
|
|||
import _uniq from 'lodash/uniq';
|
||||
import _upperFirst from 'lodash/upperFirst';
|
||||
import { groupResourcesByNs } from './util/MetricUtils.jsx';
|
||||
import { tapResourceTypes } from './util/TapUtils.jsx';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
||||
|
@ -31,6 +33,9 @@ const topRoutesQueryProps = {
|
|||
resource_name: PropTypes.string,
|
||||
resource_type: PropTypes.string,
|
||||
namespace: PropTypes.string,
|
||||
to_name: PropTypes.string,
|
||||
to_type: PropTypes.string,
|
||||
to_namespace: PropTypes.string,
|
||||
};
|
||||
const topRoutesQueryPropType = PropTypes.shape(topRoutesQueryProps);
|
||||
|
||||
|
@ -38,6 +43,10 @@ const urlPropsQueryConfig = _mapValues(topRoutesQueryProps, () => {
|
|||
return { type: UrlQueryParamTypes.string };
|
||||
});
|
||||
|
||||
const toResourceName = (query, typeKey, nameKey) => {
|
||||
return `${query[typeKey] || ""}${!query[nameKey] ? "" : "/"}${query[nameKey] || ""}`;
|
||||
};
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
marginTop: 3 * theme.spacing.unit,
|
||||
|
@ -61,6 +70,9 @@ class TopRoutes extends React.Component {
|
|||
resource_name: '',
|
||||
resource_type: '',
|
||||
namespace: '',
|
||||
to_name: '',
|
||||
to_type: '',
|
||||
to_namespace: ''
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -151,26 +163,30 @@ 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);
|
||||
handleUrlUpdate = query => {
|
||||
for (let key in query) {
|
||||
this.props[`onChange${_upperFirst(key)}`](query[key]);
|
||||
}
|
||||
}
|
||||
|
||||
handleNamespaceSelect = e => {
|
||||
handleNamespaceSelect = nsKey => e => {
|
||||
let query = this.state.query;
|
||||
let formVal = _get(e, 'target.value');
|
||||
query.namespace = formVal;
|
||||
this.handleUrlUpdate("namespace", formVal);
|
||||
query[nsKey] = formVal;
|
||||
this.handleUrlUpdate(query);
|
||||
this.setState({ query });
|
||||
};
|
||||
|
||||
handleResourceSelect = e => {
|
||||
handleResourceSelect = (nameKey, typeKey) => e => {
|
||||
let query = this.state.query;
|
||||
let resource = _get(e, 'target.value');
|
||||
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);
|
||||
let [resourceType, resourceName] = resource.split("/");
|
||||
|
||||
resourceName = resourceName || "";
|
||||
query[nameKey] = resourceName;
|
||||
query[typeKey] = resourceType;
|
||||
|
||||
this.handleUrlUpdate(query);
|
||||
this.setState({ query });
|
||||
}
|
||||
|
||||
|
@ -186,7 +202,7 @@ class TopRoutes extends React.Component {
|
|||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
{ this.renderResourceDropdown() }
|
||||
{ this.renderResourceDropdown("Resource", "resource_name", "resource_type", "Resource to query") }
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
|
@ -209,6 +225,16 @@ class TopRoutes extends React.Component {
|
|||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid item container spacing={32} alignItems="center" justify="flex-start">
|
||||
<Grid item>
|
||||
{ this.renderNamespaceDropdown("To Namespace", "to_namespace", "Namespece of target resource") }
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
{ this.renderResourceDropdown("To Resource", "to_name", "to_type", "Target resource") }
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Divider light className={classes.root} />
|
||||
{this.props.singleNamespace === "true" ? null :
|
||||
|
@ -225,7 +251,7 @@ class TopRoutes extends React.Component {
|
|||
<InputLabel htmlFor={`${key}-dropdown`}>{title}</InputLabel>
|
||||
<Select
|
||||
value={this.state.query[key]}
|
||||
onChange={this.handleNamespaceSelect}
|
||||
onChange={this.handleNamespaceSelect(key)}
|
||||
inputProps={{
|
||||
name: key,
|
||||
id: `${key}-dropdown`,
|
||||
|
@ -241,19 +267,26 @@ class TopRoutes extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
renderResourceDropdown = () => {
|
||||
renderResourceDropdown = (title, nameKey, typeKey, helperText) => {
|
||||
const { classes } = this.props;
|
||||
let { query, services, resourcesByNs } = this.state;
|
||||
|
||||
let key = "resource_name";
|
||||
let servicesWithPrefix = services
|
||||
.filter(s => s.namespace === query.namespace)
|
||||
.map(svc => `service/${svc.name}`);
|
||||
let otherResources = resourcesByNs[query.namespace] || [];
|
||||
let nsFilterKey = "namespace";
|
||||
if (typeKey === "to_type") {
|
||||
nsFilterKey = "to_namespace";
|
||||
}
|
||||
|
||||
let dropdownOptions = servicesWithPrefix.concat(otherResources).sort();
|
||||
let dropdownVal = _isEmpty(query.resource_name) || _isEmpty(query.resource_type) ? "" :
|
||||
query.resource_type + "/" + query.resource_name;
|
||||
let servicesWithPrefix = services
|
||||
.filter(s => s[nsFilterKey] === query[nsFilterKey])
|
||||
.map(svc => `service/${svc.name}`);
|
||||
let otherResources = resourcesByNs[query[nsFilterKey]] || [];
|
||||
|
||||
let dropdownOptions = servicesWithPrefix
|
||||
.concat(otherResources)
|
||||
.concat(tapResourceTypes)
|
||||
.sort();
|
||||
|
||||
let dropdownVal = toResourceName(query, typeKey, nameKey);
|
||||
|
||||
if (_isEmpty(dropdownOptions) && !_isEmpty(dropdownVal)) {
|
||||
dropdownOptions = [dropdownVal]; // populate from url if autocomplete hasn't loaded
|
||||
|
@ -261,28 +294,31 @@ class TopRoutes extends React.Component {
|
|||
|
||||
return (
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor={`${key}-dropdown`}>Resource</InputLabel>
|
||||
<InputLabel htmlFor={`${nameKey}-dropdown`}>{title}</InputLabel>
|
||||
<Select
|
||||
value={dropdownVal}
|
||||
onChange={this.handleResourceSelect}
|
||||
onChange={this.handleResourceSelect(nameKey, typeKey)}
|
||||
disabled={_isEmpty(query.namespace)}
|
||||
inputProps={{
|
||||
name: key,
|
||||
id: `${key}-dropdown`,
|
||||
name: nameKey,
|
||||
id: `${nameKey}-dropdown`,
|
||||
}}
|
||||
name={key}>
|
||||
name={nameKey}>
|
||||
{
|
||||
dropdownOptions.map(resource => <MenuItem key={resource} value={resource}>{resource}</MenuItem>)
|
||||
}
|
||||
</Select>
|
||||
<FormHelperText>Resource to query</FormHelperText>
|
||||
<FormHelperText>{helperText}</FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
let query = this.state.query;
|
||||
let emptyQuery = _isEmpty(query.resource_name) || _isEmpty(query.resource_type);
|
||||
let emptyQuery = _isEmpty(query.resource_type);
|
||||
let cliQueryToDisplay = _merge({}, query, {toResource: toResourceName(query, "to_type", "to_name"), toNamespace: query.to_namespace});
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -292,8 +328,13 @@ class TopRoutes extends React.Component {
|
|||
}
|
||||
<Card>
|
||||
{ this.renderRoutesQueryForm() }
|
||||
{ emptyQuery ? null :
|
||||
<QueryToCliCmd cmdName="routes" query={query} resource={query.resource_type + "/" + query.resource_name} /> }
|
||||
{
|
||||
emptyQuery ? null :
|
||||
<QueryToCliCmd
|
||||
cmdName="routes"
|
||||
query={cliQueryToDisplay}
|
||||
resource={toResourceName(query, "resource_type", "resource_name")} />
|
||||
}
|
||||
{ !this.state.requestInProgress || !this._isMounted ? null : <TopRoutesModule query={this.state.query} /> }
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { DefaultRoute, processTopRoutesResults } from './util/MetricUtils.jsx';
|
||||
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import ConfigureProfilesMsg from './ConfigureProfilesMsg.jsx';
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Spinner from './util/Spinner.jsx';
|
||||
import TopRoutesTable from './TopRoutesTable.jsx';
|
||||
import _every from 'lodash/every';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import _get from 'lodash/get';
|
||||
import _isEmpty from 'lodash/isEmpty';
|
||||
import { apiErrorPropType } from './util/ApiHelpers.jsx';
|
||||
import { processTopRoutesResults } from './util/MetricUtils.jsx';
|
||||
import withREST from './util/withREST.jsx';
|
||||
|
||||
class TopRoutesBase extends React.Component {
|
||||
|
@ -43,16 +43,30 @@ 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 === DefaultRoute);
|
||||
let showCallToAction = !loading && (_isEmpty(metrics) || allRoutesUnknown);
|
||||
let results = _get(data, '[0].ok.routes', []);
|
||||
|
||||
let metricsByResource = results.map(r => {
|
||||
return {
|
||||
resource: r.resource,
|
||||
rows: processTopRoutesResults(r.rows)
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.loading()}
|
||||
{this.banner()}
|
||||
<TopRoutesTable rows={metrics} />
|
||||
{ showCallToAction ? <ConfigureProfilesMsg /> : null}
|
||||
{
|
||||
metricsByResource.map(metric => {
|
||||
return (
|
||||
<CardContent key={metric.resource}>
|
||||
<Typography variant="h5">{metric.resource}</Typography>
|
||||
<TopRoutesTable rows={metric.rows} />
|
||||
</CardContent>
|
||||
);
|
||||
})
|
||||
}
|
||||
{ !loading && _isEmpty(metricsByResource) ? <ConfigureProfilesMsg /> : null }
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ const routesColumns = [
|
|||
}
|
||||
},
|
||||
{
|
||||
title: "Authority",
|
||||
title: "Service",
|
||||
tooltip: "hostname:port used when communicating with this target",
|
||||
dataIndex: "authority",
|
||||
sorter: (a, b) => (a.authority).localeCompare(b.authority)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import _compact from 'lodash/compact';
|
||||
import _isNil from 'lodash/isNil';
|
||||
|
||||
const topRoutesDisplayOrder = [
|
||||
const topRoutesDisplayOrder = query => _compact([
|
||||
"namespace",
|
||||
];
|
||||
"toResource",
|
||||
_isNil(query.to_type) ? null : "toNamespace",
|
||||
]);
|
||||
|
||||
|
||||
const tapDisplayOrder = query => _compact([
|
||||
|
@ -22,7 +24,7 @@ export const displayOrder = (cmd, query) => {
|
|||
return tapDisplayOrder(query);
|
||||
}
|
||||
if (cmd === "routes") {
|
||||
return topRoutesDisplayOrder;
|
||||
return topRoutesDisplayOrder(query);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
|
|
@ -30,6 +30,15 @@ export const setMaxRps = query => {
|
|||
}
|
||||
};
|
||||
|
||||
// resources you can tap/top to tap all pods in the resource
|
||||
export const tapResourceTypes = [
|
||||
"deployment",
|
||||
"daemonset",
|
||||
"pod",
|
||||
"replicationcontroller",
|
||||
"statefulset"
|
||||
];
|
||||
|
||||
// use a generator to get this object, to prevent it from being overwritten
|
||||
export const emptyTapQuery = () => ({
|
||||
resource: "",
|
||||
|
|
|
@ -152,6 +152,9 @@ func (h *handler) handleAPITopRoutes(w http.ResponseWriter, req *http.Request, p
|
|||
ResourceType: req.FormValue("resource_type"),
|
||||
Namespace: req.FormValue("namespace"),
|
||||
},
|
||||
ToName: req.FormValue("to_name"),
|
||||
ToType: req.FormValue("to_type"),
|
||||
ToNamespace: req.FormValue("to_namespace"),
|
||||
}
|
||||
|
||||
topReq, err := util.BuildTopRoutesRequest(requestParams)
|
||||
|
|
Loading…
Reference in New Issue