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:
Risha Mars 2018-12-05 15:15:40 -08:00 committed by GitHub
parent bef9479f57
commit 7ea867843b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 80 additions and 81 deletions

View File

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

View File

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

View File

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

View File

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