mirror of https://github.com/linkerd/linkerd2.git
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:
parent
f80f3892a0
commit
5b26508f7c
|
@ -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() {
|
||||
|
|
|
@ -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);
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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" />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -2,10 +2,6 @@ import _ from 'lodash';
|
|||
|
||||
const topRoutesDisplayOrder = [
|
||||
"namespace",
|
||||
"from",
|
||||
"from_name",
|
||||
"from_type",
|
||||
"from_namespace"
|
||||
];
|
||||
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in New Issue