mirror of https://github.com/linkerd/linkerd2.git
Show Top Routes in sidebar, change forms to query all resources (#1937)
Now that #1921 has merged, we can query for top routes for any resource, not just services. This PR adds a dropdown for all resources to the Top Routes query form. It also adds a link to the Top Routes page in the sidebar.
This commit is contained in:
parent
bef9479f57
commit
7ea867843b
|
@ -234,6 +234,7 @@ class NavigationBase extends React.Component {
|
|||
{ this.menuItem("/overview", "Overview", <HomeIcon />) }
|
||||
{ this.menuItem("/tap", "Tap", <Icon className={classNames("fas fa-microscope", classes.shrinkIcon)} />) }
|
||||
{ this.menuItem("/top", "Top", <Icon className={classNames("fas fa-stream", classes.shrinkIcon)} />) }
|
||||
{ this.menuItem("/routes", "Top Routes", <Icon className={classNames("fas fa-random", classes.shrinkIcon)} />) }
|
||||
{ this.menuItem("/servicemesh", "Service Mesh", <CloudQueueIcon className={classes.shrinkIcon} />) }
|
||||
<NavigationResources />
|
||||
</MenuList>
|
||||
|
|
|
@ -7,6 +7,7 @@ import React from 'react';
|
|||
import TapEventTable from './TapEventTable.jsx';
|
||||
import TapQueryForm from './TapQueryForm.jsx';
|
||||
import _ from 'lodash';
|
||||
import { groupResourcesByNs } from './util/MetricUtils.jsx';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
|
||||
const urlPropsQueryConfig = {
|
||||
|
@ -116,40 +117,6 @@ class Tap extends React.Component {
|
|||
this.stopTapStreaming();
|
||||
}
|
||||
|
||||
getResourcesByNs(rsp) {
|
||||
let statTables = _.get(rsp, [0, "ok", "statTables"]);
|
||||
let authoritiesByNs = {};
|
||||
let resourcesByNs = _.reduce(statTables, (mem, table) => {
|
||||
_.each(table.podGroup.rows, row => {
|
||||
// filter out resources that aren't meshed. note that authorities don't
|
||||
// have pod counts and therefore can't be filtered out here
|
||||
if (row.meshedPodCount === "0" && row.resource.type !== "authority") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mem[row.resource.namespace]) {
|
||||
mem[row.resource.namespace] = [];
|
||||
authoritiesByNs[row.resource.namespace] = [];
|
||||
}
|
||||
|
||||
switch (row.resource.type.toLowerCase()) {
|
||||
case "service":
|
||||
break;
|
||||
case "authority":
|
||||
authoritiesByNs[row.resource.namespace].push(row.resource.name);
|
||||
break;
|
||||
default:
|
||||
mem[row.resource.namespace].push(`${row.resource.type}/${row.resource.name}`);
|
||||
}
|
||||
});
|
||||
return mem;
|
||||
}, {});
|
||||
return {
|
||||
authoritiesByNs,
|
||||
resourcesByNs
|
||||
};
|
||||
}
|
||||
|
||||
indexTapResult = data => {
|
||||
// keep an index of tap request rows by id. this allows us to collate
|
||||
// requestInit/responseInit/responseEnd into one single table row,
|
||||
|
@ -264,8 +231,8 @@ class Tap extends React.Component {
|
|||
let url = this.api.urlsForResource("all");
|
||||
this.api.setCurrentRequests([this.api.fetchMetrics(url)]);
|
||||
this.serverPromise = Promise.all(this.api.getCurrentPromises())
|
||||
.then(rsp => {
|
||||
let { resourcesByNs, authoritiesByNs } = this.getResourcesByNs(rsp);
|
||||
.then(([rsp]) => {
|
||||
let { resourcesByNs, authoritiesByNs } = groupResourcesByNs(rsp);
|
||||
|
||||
this.setState({
|
||||
resourcesByNs,
|
||||
|
|
|
@ -11,9 +11,9 @@ import PropTypes from 'prop-types';
|
|||
import QueryToCliCmd from './QueryToCliCmd.jsx';
|
||||
import React from 'react';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import TopRoutesModule from './TopRoutesModule.jsx';
|
||||
import _ from 'lodash';
|
||||
import { groupResourcesByNs } from './util/MetricUtils.jsx';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
|
||||
class TopRoutes extends React.Component {
|
||||
|
@ -38,6 +38,7 @@ class TopRoutes extends React.Component {
|
|||
error: null,
|
||||
services: [],
|
||||
namespaces: [],
|
||||
resourcesByNs: {},
|
||||
pollingInterval: 5000,
|
||||
pendingRequests: false,
|
||||
requestInProgress: false
|
||||
|
@ -58,16 +59,22 @@ class TopRoutes extends React.Component {
|
|||
}
|
||||
this.setState({ pendingRequests: true });
|
||||
|
||||
this.api.setCurrentRequests([this.api.fetchServices()]);
|
||||
let allMetricsUrl = this.api.urlsForResource("all");
|
||||
this.api.setCurrentRequests([
|
||||
this.api.fetchServices(),
|
||||
this.api.fetchMetrics(allMetricsUrl)
|
||||
]);
|
||||
|
||||
this.serverPromise = Promise.all(this.api.getCurrentPromises())
|
||||
.then(([svcList]) => {
|
||||
.then(([svcList, allMetrics]) => {
|
||||
let services = _.get(svcList, 'services', []);
|
||||
let namespaces = _.uniq(_.map(services, 'namespace'));
|
||||
let { resourcesByNs } = groupResourcesByNs(allMetrics);
|
||||
|
||||
this.setState({
|
||||
services,
|
||||
namespaces,
|
||||
resourcesByNs,
|
||||
pendingRequests: false,
|
||||
error: null
|
||||
});
|
||||
|
@ -102,21 +109,28 @@ class TopRoutes extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
handleFormEvent = key => {
|
||||
return e => {
|
||||
let query = this.state.query;
|
||||
query[key] = _.get(e, 'target.value');
|
||||
this.setState({ query });
|
||||
};
|
||||
handleNamespaceSelect = e => {
|
||||
let query = this.state.query;
|
||||
query.namespace = _.get(e, 'target.value');
|
||||
this.setState({ query });
|
||||
};
|
||||
|
||||
handleResourceSelect = 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.setState({ query });
|
||||
}
|
||||
|
||||
renderRoutesQueryForm = () => {
|
||||
return (
|
||||
<CardContent>
|
||||
<Grid container direction="column">
|
||||
<Grid container direction="column" spacing={16}>
|
||||
<Grid item container spacing={8} alignItems="center">
|
||||
<Grid item xs={6} md={3}>
|
||||
{ this.renderNamespaceDropdown("Namespace", "namespace", "Namespace of the configured service") }
|
||||
{ this.renderNamespaceDropdown("Namespace", "namespace", "Namespace to query") }
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6} md={3}>
|
||||
|
@ -124,18 +138,6 @@ class TopRoutes extends React.Component {
|
|||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid item container spacing={8} alignItems="center">
|
||||
<Grid item xs={6} md={3}>
|
||||
{ this.renderTextInput("From", "from_name", "Resource name") }
|
||||
</Grid>
|
||||
<Grid item xs={6} md={3}>
|
||||
{ this.renderTextInput("From type", "from_type", "Resource type") }
|
||||
</Grid>
|
||||
<Grid item xs={6} md={3}>
|
||||
{ this.renderTextInput("From namespace", "from_namespace", "Resource namespace") }
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid item container spacing={8} alignItems="center">
|
||||
<Grid item>
|
||||
<Button
|
||||
|
@ -167,7 +169,7 @@ class TopRoutes extends React.Component {
|
|||
<InputLabel htmlFor={`${key}-dropdown`}>{title}</InputLabel>
|
||||
<Select
|
||||
value={this.state.query[key]}
|
||||
onChange={this.handleFormEvent(key)}
|
||||
onChange={this.handleNamespaceSelect}
|
||||
inputProps={{
|
||||
name: key,
|
||||
id: `${key}-dropdown`,
|
||||
|
@ -187,15 +189,21 @@ class TopRoutes extends React.Component {
|
|||
let key = "resource_name";
|
||||
let services = _.chain(this.state.services)
|
||||
.filter(['namespace', this.state.query.namespace])
|
||||
.map('name').sortBy().value();
|
||||
.map(svc => `service/${svc.name}`).value();
|
||||
let otherResources = this.state.resourcesByNs[this.state.query.namespace] || [];
|
||||
|
||||
let { query } = this.state;
|
||||
let dropdownOptions = _.sortBy(_.concat(services, otherResources));
|
||||
let dropdownVal = _.isEmpty(query.resource_name) || _.isEmpty(query.resource_type) ? "" :
|
||||
query.resource_type + "/" + query.resource_name;
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<InputLabel htmlFor={`${key}-dropdown`}>Service</InputLabel>
|
||||
<InputLabel htmlFor={`${key}-dropdown`}>Resource</InputLabel>
|
||||
<Select
|
||||
value={this.state.query[key]}
|
||||
onChange={this.handleFormEvent(key)}
|
||||
disabled={_.isEmpty(this.state.query.namespace)}
|
||||
value={dropdownVal}
|
||||
onChange={this.handleResourceSelect}
|
||||
disabled={_.isEmpty(query.namespace)}
|
||||
autoWidth
|
||||
inputProps={{
|
||||
name: key,
|
||||
|
@ -203,26 +211,14 @@ class TopRoutes extends React.Component {
|
|||
}}
|
||||
name={key}>
|
||||
{
|
||||
_.map(services, svc => <MenuItem key={`service-${svc}`} value={svc}>{svc}</MenuItem>)
|
||||
_.map(dropdownOptions, resource => <MenuItem key={resource} value={resource}>{resource}</MenuItem>)
|
||||
}
|
||||
</Select>
|
||||
<FormHelperText>The configured service</FormHelperText>
|
||||
<FormHelperText>Resource to query</FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
|
||||
renderTextInput = (title, key, helperText) => {
|
||||
return (
|
||||
<TextField
|
||||
id={key}
|
||||
label={title}
|
||||
value={this.state.query[key]}
|
||||
onChange={this.handleFormEvent(key)}
|
||||
helperText={helperText}
|
||||
margin="normal" />
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let query = this.state.query;
|
||||
let from = '';
|
||||
|
@ -241,9 +237,10 @@ class TopRoutes extends React.Component {
|
|||
}
|
||||
<Card>
|
||||
{ this.renderRoutesQueryForm() }
|
||||
<QueryToCliCmd cmdName="routes" query={query} resource={this.state.query.resource_name} />
|
||||
{ _.isEmpty(query.resource_name) || _.isEmpty(query.resource_type) ? null :
|
||||
<QueryToCliCmd cmdName="routes" query={query} resource={query.resource_type + "/" + query.resource_name} /> }
|
||||
{ !this.state.requestInProgress ? null : <TopRoutesModule query={this.state.query} /> }
|
||||
</Card>
|
||||
{ !this.state.requestInProgress ? null : <TopRoutesModule query={this.state.query} /> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -199,6 +199,40 @@ export const processMultiResourceRollup = rawMetrics => {
|
|||
return metricsByResource;
|
||||
};
|
||||
|
||||
export const groupResourcesByNs = apiRsp => {
|
||||
let statTables = _.get(apiRsp, ["ok", "statTables"]);
|
||||
let authoritiesByNs = {};
|
||||
let resourcesByNs = _.reduce(statTables, (mem, table) => {
|
||||
_.each(table.podGroup.rows, row => {
|
||||
// filter out resources that aren't meshed. note that authorities don't
|
||||
// have pod counts and therefore can't be filtered out here
|
||||
if (row.meshedPodCount === "0" && row.resource.type !== "authority") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mem[row.resource.namespace]) {
|
||||
mem[row.resource.namespace] = [];
|
||||
authoritiesByNs[row.resource.namespace] = [];
|
||||
}
|
||||
|
||||
switch (row.resource.type.toLowerCase()) {
|
||||
case "service":
|
||||
break;
|
||||
case "authority":
|
||||
authoritiesByNs[row.resource.namespace].push(row.resource.name);
|
||||
break;
|
||||
default:
|
||||
mem[row.resource.namespace].push(`${row.resource.type}/${row.resource.name}`);
|
||||
}
|
||||
});
|
||||
return mem;
|
||||
}, {});
|
||||
return {
|
||||
authoritiesByNs,
|
||||
resourcesByNs
|
||||
};
|
||||
};
|
||||
|
||||
export const excludeResourcesFromRollup = (rollupMetrics, resourcesToExclude) => {
|
||||
_.each(resourcesToExclude, resource => {
|
||||
delete rollupMetrics[resource];
|
||||
|
|
Loading…
Reference in New Issue