Add a tabbed view to the resource detail page for Top and Routes (#1918)

Adds the top routes metrics to the resource detail pages.

* Add a tabbed view to the resource detail page
Add the ability to query top routes from the detail tabs

* Move ConfigureProfilesMsg to its own module
This commit is contained in:
Risha Mars 2018-12-05 13:55:31 -08:00 committed by GitHub
parent f80f3892a0
commit 5b26508f7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 202 additions and 54 deletions

View File

@ -7,6 +7,7 @@ import TableCell from '@material-ui/core/TableCell';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import TableSortLabel from '@material-ui/core/TableSortLabel';
import Tooltip from '@material-ui/core/Tooltip';
import _ from 'lodash';
import classNames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
@ -64,9 +65,10 @@ class BaseTable extends React.Component {
renderHeaderCell = (col, order, orderBy) => {
let active = orderBy === col.dataIndex;
const { classes, padding } = this.props;
let tableCell;
if (col.sorter) {
return (
tableCell = (
<TableCell
key={col.key || col.dataIndex}
numeric={col.isNumeric}
@ -82,7 +84,7 @@ class BaseTable extends React.Component {
</TableCell>
);
} else {
return (
tableCell = (
<TableCell
key={col.key || col.dataIndex}
numeric={col.isNumeric}
@ -91,6 +93,8 @@ class BaseTable extends React.Component {
</TableCell>
);
}
return _.isNil(col.tooltip) ? tableCell : <Tooltip key={col.key || col.dataIndex} placement="top" title={col.tooltip}>{tableCell}</Tooltip>;
}
render() {

View File

@ -0,0 +1,33 @@
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import PropTypes from 'prop-types';
import React from 'react';
import Typography from '@material-ui/core/Typography';
import { withStyles } from '@material-ui/core/styles';
const styles = theme => ({
root: {
marginTop: theme.spacing.unit * 3,
marginBottom: theme.spacing.unit * 3,
},
});
class ConfigureProfilesMsg extends React.Component {
render() {
const { classes } = this.props;
return (
<Card className={classes.root}>
<CardContent>
<Typography>
No traffic found. Does the service have a service profile? You can create one with the `linkerd profile` command.
</Typography>
</CardContent>
</Card>
);
}
}
ConfigureProfilesMsg.propTypes = {
classes: PropTypes.shape({}).isRequired
};
export default withStyles(styles, { withTheme: true })(ConfigureProfilesMsg);

View File

@ -10,7 +10,7 @@ import Octopus from './Octopus.jsx';
import PropTypes from 'prop-types';
import React from 'react';
import Spinner from './util/Spinner.jsx';
import TopModule from './TopModule.jsx';
import TopRoutesTabs from './TopRoutesTabs.jsx';
import Typography from '@material-ui/core/Typography';
import _ from 'lodash';
import { processNeighborData } from './util/TapUtils.jsx';
@ -195,9 +195,11 @@ export class ResourceDetailBase extends React.Component {
return <Spinner />;
}
let topQuery = {
resource: this.state.resourceType + "/" + this.state.resourceName,
namespace: this.state.namespace
let { resourceName, resourceType, namespace } = this.state;
let query = {
resourceName,
resourceType,
namespace
};
let unmeshed = _.chain(this.state.unmeshedSources)
@ -217,61 +219,54 @@ export class ResourceDetailBase extends React.Component {
<div>
{
this.state.resourceIsMeshed ? null :
<div className="page-section">
<React.Fragment>
<AddResources
resourceName={this.state.resourceName}
resourceType={this.state.resourceType} />
</div>
</React.Fragment>
}
<div className="page-section">
<Octopus
resource={this.state.resourceMetrics[0]}
neighbors={this.state.neighborMetrics}
unmeshedSources={_.values(this.state.unmeshedSources)}
api={this.api} />
</div>
<Octopus
resource={this.state.resourceMetrics[0]}
neighbors={this.state.neighborMetrics}
unmeshedSources={_.values(this.state.unmeshedSources)}
api={this.api} />
<TopRoutesTabs
query={query}
pathPrefix={this.props.pathPrefix}
updateNeighbors={this.updateNeighborsFromTapData}
disableTop={!this.state.resourceIsMeshed} />
{
!this.state.resourceIsMeshed ? null :
<div className="page-section">
<TopModule
pathPrefix={this.props.pathPrefix}
query={topQuery}
startTap={true}
updateNeighbors={this.updateNeighborsFromTapData}
maxRowsToDisplay={10} />
</div>
}
{ _.isEmpty(upstreams) ? null : (
<div className="page-section">
<React.Fragment>
<Typography variant="h5">Inbound</Typography>
<MetricsTable
resource={this.state.resource.type}
metrics={upstreams} />
</div>
</React.Fragment>
)
}
{ _.isEmpty(this.state.neighborMetrics.downstream) ? null : (
<div className="page-section">
<React.Fragment>
<Typography variant="h5">Outbound</Typography>
<MetricsTable
resource={this.state.resource.type}
metrics={this.state.neighborMetrics.downstream} />
</div>
</React.Fragment>
)
}
{
this.state.resource.type === "pod" ? null : (
<div className="page-section">
<React.Fragment>
<Typography variant="h5">Pods</Typography>
<MetricsTable
resource="pod"
metrics={this.state.podMetrics} />
</div>
</React.Fragment>
)
}
</div>
@ -279,7 +274,6 @@ export class ResourceDetailBase extends React.Component {
}
render() {
return (
<div className="page-content">
<div>

View File

@ -1,10 +1,9 @@
import Card from '@material-ui/core/Card';
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 Typography from '@material-ui/core/Typography';
import _ from 'lodash';
import { apiErrorPropType } from './util/ApiHelpers.jsx';
import { processTopRoutesResults } from './util/MetricUtils.jsx';
@ -18,6 +17,7 @@ class TopRoutesBase extends React.Component {
static propTypes = {
data: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
error: apiErrorPropType,
loading: PropTypes.bool.isRequired,
query: PropTypes.shape({}).isRequired,
}
@ -29,26 +29,24 @@ class TopRoutesBase extends React.Component {
return <ErrorBanner message={error} />;
}
configureProfileMsg() {
return (
<Card>
<CardContent>
<Typography>
No traffic found. Does the service have a service profile? You can create one with the `linkerd profile` command.
</Typography>
</CardContent>
</Card>
);
loading = () => {
const {loading} = this.props;
if (!loading) {
return;
}
return <Spinner />;
}
render() {
const {data} = this.props;
const {data, loading} = this.props;
let metrics = processTopRoutesResults(_.get(data, '[0].routes.rows', []));
return (
<React.Fragment>
{this.loading()}
{this.banner()}
{_.isEmpty(metrics) ? this.configureProfileMsg() : null}
{ !loading && _.isEmpty(metrics) ? <ConfigureProfilesMsg /> : null}
<TopRoutesTable rows={metrics} />
</React.Fragment>
);

View File

@ -10,6 +10,12 @@ const routesColumns = [
dataIndex: "route",
sorter: (a, b) => (a.route).localeCompare(b.route)
},
{
title: "Authority",
tooltip: "hostname:port used when communicating with this target",
dataIndex: "authority",
sorter: (a, b) => (a.authority).localeCompare(b.authority)
},
{
title: "Success Rate",
dataIndex: "successRate",
@ -57,14 +63,14 @@ export default class TopRoutesTable extends React.Component {
};
render() {
const { rows } = this.props;
const { rows } = this.props;
return (
<BaseTable
tableRows={rows}
tableColumns={routesColumns}
tableClassName="metric-table"
defaultOrderBy="route"
rowKey={r => r.route}
rowKey={r => r.route + r.authority}
padding="dense" />
);
}

View File

@ -0,0 +1,116 @@
import AppBar from '@material-ui/core/AppBar';
import ConfigureProfilesMsg from './ConfigureProfilesMsg.jsx';
import Paper from '@material-ui/core/Paper';
import PropTypes from 'prop-types';
import QueryToCliCmd from './QueryToCliCmd.jsx';
import React from 'react';
import Tab from '@material-ui/core/Tab';
import Tabs from '@material-ui/core/Tabs';
import TopModule from './TopModule.jsx';
import TopRoutesModule from './TopRoutesModule.jsx';
import _ from 'lodash';
import { withStyles } from '@material-ui/core/styles';
const styles = theme => ({
root: {
flexGrow: 1,
backgroundColor: theme.palette.background.paper,
marginBottom: theme.spacing.unit * 3,
},
});
class TopRoutesTabs extends React.Component {
state = {
value: 0,
};
handleChange = (_event, value) => {
this.setState({ value });
};
renderTopComponent() {
let { disableTop, query, pathPrefix, updateNeighborsFromTapData } = this.props;
if (disableTop) {
return null;
}
let topQuery = {
resource: query.resourceType + "/" + query.resourceName,
namespace: query.namespace
};
return (
<React.Fragment>
<TopModule
pathPrefix={pathPrefix}
query={topQuery}
startTap={true}
updateNeighbors={updateNeighborsFromTapData}
maxRowsToDisplay={10} />
<QueryToCliCmd cmdName="top" query={topQuery} resource={topQuery.resource} />
</React.Fragment>
);
}
renderRoutesComponent() {
const { query } = this.props;
if (_.isEmpty(query)) {
return <ConfigureProfilesMsg />;
}
let routesQuery = {
resource_name: query.resourceName,
resource_type: query.resourceType,
namespace: query.namespace
};
let resource = query.resourceType + "/" + query.resourceName;
return (
<React.Fragment>
<TopRoutesModule query={routesQuery} />
<QueryToCliCmd cmdName="routes" query={routesQuery} resource={resource} />
</React.Fragment>
);
}
render() {
const { classes } = this.props;
const { value } = this.state;
return (
<Paper className={classes.root}>
<AppBar position="static" className={classes.root}>
<Tabs
value={value}
onChange={this.handleChange}
indicatorColor="primary"
textColor="primary">
<Tab label="Live Calls" />
<Tab label="Route Metrics" />
</Tabs>
</AppBar>
{value === 0 && this.renderTopComponent()}
{value === 1 && this.renderRoutesComponent()}
</Paper>
);
}
}
TopRoutesTabs.propTypes = {
classes: PropTypes.shape({}).isRequired,
disableTop: PropTypes.bool,
pathPrefix: PropTypes.string.isRequired,
query: PropTypes.shape({}),
theme: PropTypes.shape({}).isRequired,
updateNeighborsFromTapData: PropTypes.func
};
TopRoutesTabs.defaultProps = {
disableTop: false,
query: {},
updateNeighborsFromTapData: _.noop
};
export default withStyles(styles, { withTheme: true })(TopRoutesTabs);

View File

@ -2,10 +2,6 @@ import _ from 'lodash';
const topRoutesDisplayOrder = [
"namespace",
"from",
"from_name",
"from_type",
"from_namespace"
];

View File

@ -159,6 +159,7 @@ const processStatTable = table => {
export const processTopRoutesResults = rows => {
return _.map(rows, row => ({
route: row.route || "UNKNOWN",
authority: row.authority,
totalRequests: getTotalRequests(row),
requestRate: getRequestRate(row),
successRate: getSuccessRate(row),