Re-add sortable column headers to tables in web UI (#1814)

* Re-add sortable column headers to tables in web UI
* Display sort icons on all sortable columns
* Disable src/dst popover in top table

Signed-off-by: Kevin Lingerfelt <kl@buoyant.io>
This commit is contained in:
Kevin Lingerfelt 2018-10-26 14:58:01 -07:00 committed by GitHub
parent d2f847a484
commit cf7a532e15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 196 additions and 106 deletions

View File

@ -6,6 +6,7 @@ import TableBody from '@material-ui/core/TableBody';
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 _ from 'lodash';
import { withStyles } from '@material-ui/core/styles';
@ -17,61 +18,131 @@ const styles = theme => ({
overflowX: 'auto',
},
table: {},
activeSortIcon: {
opacity: 1,
},
inactiveSortIcon: {
opacity: 0.4,
},
});
function BaseTable(props) {
const { classes, tableRows, tableColumns, tableClassName, rowKey, padding} = props;
class BaseTable extends React.Component {
constructor(props) {
super(props);
this.state = {
order: this.props.defaultOrder || "asc",
orderBy: this.props.defaultOrderBy
};
}
return (
<Paper className={classes.root}>
<Table className={`${classes.table} ${tableClassName}`} padding={padding}>
<TableHead>
<TableRow>
{ _.map(tableColumns, c => (
<TableCell
key={c.key}
numeric={c.isNumeric}>{c.title}
</TableCell>
))
}
</TableRow>
</TableHead>
<TableBody>
{
_.map(tableRows, d => {
let key = !rowKey ? d.key : rowKey(d);
return (
<TableRow key={key}>
{ _.map(tableColumns, c => (
<TableCell
key={`table-${key}-${c.key}`}
numeric={c.isNumeric}>{c.render(d)}
</TableCell>
))
}
</TableRow>
);
})}
</TableBody>
</Table>
</Paper>
);
createSortHandler = col => () => {
let orderBy = col.dataIndex;
let order = col.defaultSortOrder || 'asc';
if (this.state.orderBy === orderBy && this.state.order === order) {
order = order === 'asc' ? 'desc' : 'asc';
}
this.setState({ order, orderBy });
};
sortRows = (tableRows, tableColumns, order, orderBy) => {
if (!orderBy) {
return tableRows;
}
let col = _.find(tableColumns, ['dataIndex', orderBy]);
let sorted = tableRows.sort(col.sorter);
return order === 'desc' ? _.reverse(sorted) : sorted;
}
renderHeaderCell = (col, order, orderBy, classes) => {
let active = orderBy === col.dataIndex;
if (col.sorter) {
return (
<TableCell
key={col.key || col.dataIndex}
numeric={col.isNumeric}
sortDirection={orderBy === col.dataIndex ? order : false}>
<TableSortLabel
active={active}
direction={active ? order : col.defaultSortOrder || 'asc'}
classes={{icon: active ? classes.activeSortIcon : classes.inactiveSortIcon}}
onClick={this.createSortHandler(col)}>
{col.title}
</TableSortLabel>
</TableCell>
);
} else {
return (
<TableCell
key={col.key || col.dataIndex}
numeric={col.isNumeric}>
{col.title}
</TableCell>
);
}
}
render() {
const { classes, tableRows, tableColumns, tableClassName, rowKey, padding} = this.props;
const {order, orderBy} = this.state;
const sortedTableRows = this.sortRows(tableRows, tableColumns, order, orderBy);
return (
<Paper className={classes.root}>
<Table className={`${classes.table} ${tableClassName}`} padding={padding}>
<TableHead>
<TableRow>
{ _.map(tableColumns, c => (
this.renderHeaderCell(c, order, orderBy, classes)
))}
</TableRow>
</TableHead>
<TableBody>
{
_.map(sortedTableRows, d => {
let key = !rowKey ? d.key : rowKey(d);
return (
<TableRow key={key}>
{ _.map(tableColumns, c => (
<TableCell
key={`table-${key}-${c.key || c.dataIndex}`}
numeric={c.isNumeric}>
{c.render ? c.render(d) : _.get(d, c.dataIndex)}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
</Paper>
);
}
}
BaseTable.propTypes = {
classes: PropTypes.shape({}).isRequired,
defaultOrder: PropTypes.string,
defaultOrderBy: PropTypes.string,
padding: PropTypes.string,
rowKey: PropTypes.func,
tableClassName: PropTypes.string,
tableColumns: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string,
dataIndex: PropTypes.string,
defaultSortOrder: PropTypes.string,
isNumeric: PropTypes.bool,
render: PropTypes.func
render: PropTypes.func,
sorter: PropTypes.func,
title: PropTypes.string
})).isRequired,
tableRows: PropTypes.arrayOf(PropTypes.shape({}))
};
BaseTable.defaultProps = {
defaultOrder: "asc",
defaultOrderBy: null,
padding: "default",
rowKey: null,
tableClassName: "",

View File

@ -21,7 +21,7 @@ const getClassification = (meshedPodCount, failedPodCount) => {
const namespacesColumns = PrefixedLink => [
{
title: "Namespace",
key: "namespace",
dataIndex: "namespace",
render: d => {
return (
<React.Fragment>
@ -37,9 +37,8 @@ const namespacesColumns = PrefixedLink => [
},
{
title: "Meshed pods",
key: "meshedPodsStr",
isNumeric: true,
render: d => d.meshedPodsStr
dataIndex: "meshedPodsStr",
isNumeric: true
},
{
title: "Meshed Status",

View File

@ -1,4 +1,4 @@
import { friendlyTitle, metricToFormatter } from './util/Utils.js';
import { friendlyTitle, metricToFormatter, numericSort } from './util/Utils.js';
import BaseTable from './BaseTable.jsx';
import ErrorModal from './ErrorModal.jsx';
@ -17,23 +17,25 @@ const columnDefinitions = (resource, showNamespaceColumn, PrefixedLink) => {
let nsColumn = [
{
title: "Namespace",
key: "namespace",
dataIndex: "namespace",
isNumeric: false,
render: d => !d.namespace ? "---" : <PrefixedLink to={"/namespaces/" + d.namespace}>{d.namespace}</PrefixedLink>
render: d => !d.namespace ? "---" : <PrefixedLink to={"/namespaces/" + d.namespace}>{d.namespace}</PrefixedLink>,
sorter: (a, b) => (a.namespace || "").localeCompare(b.namespace)
}
];
let meshedColumn = {
title: "Meshed",
key: "meshed",
dataIndex: "pods.totalPods",
isNumeric: true,
render: d => !d.pods ? null : d.pods.meshedPods + "/" + d.pods.totalPods
render: d => !d.pods ? null : d.pods.meshedPods + "/" + d.pods.totalPods,
sorter: (a, b) => numericSort(a.pods.totalPods, b.pods.totalPods)
};
let columns = [
{
title: friendlyTitle(resource).singular,
key: "resource-title",
dataIndex: "name",
isNumeric: false,
render: d => {
let nameContents;
@ -55,43 +57,52 @@ const columnDefinitions = (resource, showNamespaceColumn, PrefixedLink) => {
<Grid item><ErrorModal errors={d.errors} resourceName={d.name} resourceType={resource} /></Grid>}
</Grid>
);
}
},
sorter: (a, b) => (a.name || "").localeCompare(b.name)
},
{
title: "Success Rate",
key: "success-rate",
dataIndex: "successRate",
isNumeric: true,
render: d => <SuccessRateMiniChart sr={d.successRate} />
render: d => <SuccessRateMiniChart sr={d.successRate} />,
sorter: (a, b) => numericSort(a.successRate, b.successRate)
},
{
title: "Request Rate",
key: "request-rate",
dataIndex: "requestRate",
isNumeric: true,
render: d => metricToFormatter["NO_UNIT"](d.requestRate)
render: d => metricToFormatter["NO_UNIT"](d.requestRate),
sorter: (a, b) => numericSort(a.requestRate, b.requestRate)
},
{
title: "P50 Latency",
key: "p50_latency",
dataIndex: "P50",
isNumeric: true,
render: d => metricToFormatter["LATENCY"](d.P50)
render: d => metricToFormatter["LATENCY"](d.P50),
sorter: (a, b) => numericSort(a.P50, b.P50)
},
{
title: "P95 Latency",
key: "p95_latency",
dataIndex: "P95",
isNumeric: true,
render: d => metricToFormatter["LATENCY"](d.P95)
render: d => metricToFormatter["LATENCY"](d.P95),
sorter: (a, b) => numericSort(a.P95, b.P95)
},
{
title: "P99 Latency",
key: "p99_latency",
dataIndex: "P99",
isNumeric: true,
render: d => metricToFormatter["LATENCY"](d.P99)
render: d => metricToFormatter["LATENCY"](d.P99),
sorter: (a, b) => numericSort(a.P99, b.P99)
},
{
title: "TLS",
key: "has_tls",
dataIndex: "tlsRequestPercent",
isNumeric: true,
render: d => _.isNil(d.tlsRequestPercent) || d.tlsRequestPercent.get() === -1 ? "---" : d.tlsRequestPercent.prettyRate()
render: d => _.isNil(d.tlsRequestPercent) || d.tlsRequestPercent.get() === -1 ? "---" : d.tlsRequestPercent.prettyRate(),
sorter: (a, b) => numericSort(
a.tlsRequestPercent ? a.tlsRequestPercent.get() : -1,
b.tlsRequestPercent ? b.tlsRequestPercent.get() : -1)
},
{
title: "Grafana Dashboard",
@ -165,6 +176,7 @@ class MetricsTable extends React.Component {
tableRows={rows}
tableColumns={columns}
tableClassName="metric-table"
defaultOrderBy="name"
padding="dense" />
);
}

View File

@ -19,14 +19,12 @@ import { withContext } from './util/AppContext.jsx';
const serviceMeshDetailsColumns = [
{
title: "Name",
key: "name",
render: d => d.name
dataIndex: "name"
},
{
title: "Value",
key: "value",
isNumeric: true,
render: d => d.value
dataIndex: "value",
isNumeric: true
}
];

View File

@ -67,14 +67,12 @@ StatusDot.propTypes = {
const columns = {
resourceName: {
title: "Deployment",
key: "name",
render: d => d.name
dataIndex: "name"
},
pods: {
title: "Pods",
key: "pods",
isNumeric: true,
render: d => d.numEntities
dataIndex: "numEntities",
isNumeric: true
},
status: (name, classes) => {
return {

View File

@ -1,17 +1,17 @@
import { directionColumn, srcDstColumn, tapLink } from './util/TapUtils.jsx';
import { formatLatencySec, numericSort } from './util/Utils.js';
import BaseTable from './BaseTable.jsx';
import PropTypes from 'prop-types';
import React from 'react';
import SuccessRateMiniChart from './util/SuccessRateMiniChart.jsx';
import _ from 'lodash';
import { formatLatencySec } from './util/Utils.js';
import { withContext } from './util/AppContext.jsx';
const topColumns = (resourceType, ResourceLink, PrefixedLink) => [
{
title: " ",
key: "direction",
dataIndex: "direction",
render: d => directionColumn(d.direction)
},
{
@ -21,44 +21,50 @@ const topColumns = (resourceType, ResourceLink, PrefixedLink) => [
},
{
title: "Method",
key: "httpMethod",
render: d => d.httpMethod,
dataIndex: "httpMethod",
sorter: (a, b) => a.httpMethod.localeCompare(b.httpMethod)
},
{
title: "Path",
key: "path",
render: d => d.path
dataIndex: "path",
sorter: (a, b) => a.path.localeCompare(b.path)
},
{
title: "Count",
key: "count",
dataIndex: "count",
isNumeric: true,
render: d => d.count
defaultSortOrder: "desc",
sorter: (a, b) => numericSort(a.count, b.count)
},
{
title: "Best",
key: "best",
dataIndex: "best",
isNumeric: true,
render: d => formatLatencySec(d.best)
render: d => formatLatencySec(d.best),
sorter: (a, b) => numericSort(a.best, b.best)
},
{
title: "Worst",
key: "worst",
dataIndex: "worst",
isNumeric: true,
render: d => formatLatencySec(d.worst)
defaultSortOrder: "desc",
render: d => formatLatencySec(d.worst),
sorter: (a, b) => numericSort(a.worst, b.worst)
},
{
title: "Last",
key: "last",
dataIndex: "last",
isNumeric: true,
render: d => formatLatencySec(d.last)
render: d => formatLatencySec(d.last),
sorter: (a, b) => numericSort(a.last, b.last)
},
{
title: "Success Rate",
key: "successRate",
dataIndex: "successRate",
isNumeric: true,
render: d => _.isNil(d) || _.isNil(d.successRate) ? "---" :
<SuccessRateMiniChart sr={d.successRate.get()} />
<SuccessRateMiniChart sr={d.successRate.get()} />,
sorter: (a, b) => numericSort(a.successRate.get(), b.successRate.get()),
},
{
title: "Tap",
@ -76,15 +82,22 @@ class TopEventTable extends React.Component {
resourceType: PropTypes.string.isRequired,
tableRows: PropTypes.arrayOf(PropTypes.shape({}))
};
static defaultProps = {
tableRows: []
};
static defaultProps = {
tableRows: []
};
render() {
const { tableRows, resourceType, api } = this.props;
let columns = topColumns(resourceType, api.ResourceLink, api.PrefixedLink);
return <BaseTable tableRows={tableRows} tableColumns={columns} tableClassName="metric-table" />;
}
render() {
const { tableRows, resourceType, api } = this.props;
let columns = topColumns(resourceType, api.ResourceLink, api.PrefixedLink);
return (
<BaseTable
tableRows={tableRows}
tableColumns={columns}
tableClassName="metric-table"
defaultOrderBy="count"
defaultOrder="desc" />
);
}
}
export default withContext(TopEventTable);

View File

@ -1,7 +1,6 @@
import { podOwnerLookup, toShortResourceName } from './Utils.js';
import BaseTable from '../BaseTable.jsx';
import Popover from '../Popover.jsx';
import PropTypes from 'prop-types';
import React from 'react';
import TapLink from '../TapLink.jsx';
@ -195,9 +194,9 @@ const resourceShortLink = (resourceType, labels, ResourceLink) => (
const displayLimit = 3; // how many upstreams/downstreams to display in the popover table
const popoverSrcDstColumns = [
{ title: "Source", key: "source", render: d => d.source },
{ title: "Source", dataIndex: "source" },
{ title: "", key: "arrow", render: () => <i className="fas fa-long-arrow-alt-right" /> },
{ title: "Destination", key: "destination", render: d => d.destination }
{ title: "Destination", dataIndex: "destination" }
];
const getPodOwner = (labels, ResourceLink) => {
@ -249,7 +248,7 @@ const getIpList = (endpoint, display) => {
return <div className="popover-td">{ipList}</div>;
};
const popoverResourceTable = (d, ResourceLink) => {
const popoverResourceTable = (d, ResourceLink) => { // eslint-disable-line no-unused-vars
let tableData = [
{
source: getPodOwner(d.sourceLabels, ResourceLink),
@ -304,17 +303,11 @@ export const srcDstColumn = (d, resourceType, ResourceLink) => {
labels = d.destinationLabels;
}
let baseContent = (
return (
<div className="src-dst-name">
{ !_.isEmpty(labels[resourceType]) ? resourceShortLink(resourceType, labels, ResourceLink) : display.str }
</div>
);
return (
<Popover
popoverContent={popoverResourceTable(d, ResourceLink)}
baseContent={baseContent} />
);
};
export const tapLink = (d, resourceType, PrefixedLink) => {

View File

@ -1,4 +1,5 @@
import * as d3 from 'd3';
import _ from 'lodash';
/*
@ -111,6 +112,11 @@ export const toClassName = name => {
return _.lowerCase(name).replace(/[^a-zA-Z0-9]/g, "_");
};
/*
Definition of sort, for numeric column sorting
*/
export const numericSort = (a, b) => (_.isNil(a) ? -1 : a) - (_.isNil(b) ? -1 : b);
/*
Nicely readable names for the stat resources
*/