mirror of https://github.com/linkerd/linkerd2.git
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:
parent
d2f847a484
commit
cf7a532e15
|
@ -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: "",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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" />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue