mirror of https://github.com/linkerd/linkerd2.git
Add small success rate chart to table, misc web tweaks (#1628)
A bunch of web UI tweaks: - Add a small success rate chart to the metrics tables - Improve latency formatting for seconds latencies - Rename upstream/downstream to inbound/outbound - Make Top table look consistent with rest of tables on page - Fix widths of metrics column columns so that tables align
This commit is contained in:
parent
01be78e455
commit
b49ccce5f0
|
|
@ -36,15 +36,6 @@
|
|||
& .ant-progress-text {
|
||||
color: black;
|
||||
}
|
||||
& .status-good .ant-progress-circle-path {
|
||||
stroke: var(--green);
|
||||
}
|
||||
& .status-ok .ant-progress-circle-path {
|
||||
stroke: var(--orange)
|
||||
}
|
||||
& .status-poor .ant-progress-circle-path {
|
||||
stroke: var(--red);
|
||||
}
|
||||
}
|
||||
|
||||
& .octopus-metric {
|
||||
|
|
@ -57,7 +48,7 @@
|
|||
color: var(--red);
|
||||
}
|
||||
&.status-neutral {
|
||||
color: #E0E0E0;
|
||||
color: var(--graphgrey);
|
||||
}
|
||||
&.status-ok {
|
||||
color: var(--orange);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
--warmgrey: #f6f6f6;
|
||||
--coldgrey: #c9c9c9;
|
||||
--neutralgrey: #828282;
|
||||
--graphgrey: #E0E0E0;
|
||||
--silver: #BDBDBD;
|
||||
--green: #26E99D;
|
||||
--red: #FF4D2B;
|
||||
|
|
@ -243,6 +244,30 @@ a.button.primary:active {
|
|||
& tr .numeric {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
& .metric-table-sr {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
& .metric-table-sr-chart.ant-progress-circle {
|
||||
/* override ant progress circle height */
|
||||
& div.ant-progress-inner {
|
||||
max-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ant Progress bar color orverrides */
|
||||
.success-rate-arc {
|
||||
&.status-good .ant-progress-circle-path {
|
||||
stroke: var(--green);
|
||||
}
|
||||
&.status-ok .ant-progress-circle-path {
|
||||
stroke: var(--orange)
|
||||
}
|
||||
&.status-poor .ant-progress-circle-path {
|
||||
stroke: var(--red);
|
||||
}
|
||||
}
|
||||
|
||||
/* Colored dot for indicating statuses */
|
||||
|
|
@ -262,7 +287,7 @@ div.status-dot {
|
|||
background-color: #E0E0E0;
|
||||
}
|
||||
&.status-dot-ok {
|
||||
background-color: #ffd54f;
|
||||
background-color: var(--orange);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import _ from 'lodash';
|
|||
import BaseTable from './BaseTable.jsx';
|
||||
import ErrorModal from './ErrorModal.jsx';
|
||||
import GrafanaLink from './GrafanaLink.jsx';
|
||||
import { processedMetricsPropType } from './util/MetricUtils.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
|
|
@ -12,11 +11,13 @@ import {
|
|||
metricToFormatter,
|
||||
numericSort
|
||||
} from './util/Utils.js';
|
||||
import { processedMetricsPropType, successRateWithMiniChart } from './util/MetricUtils.jsx';
|
||||
|
||||
/*
|
||||
Table to display Success Rate, Requests and Latency in tabs.
|
||||
Expects rollup and timeseries data.
|
||||
*/
|
||||
const smMetricColWidth = "70px";
|
||||
|
||||
const withTooltip = (d, metricName) => {
|
||||
return (
|
||||
|
|
@ -75,6 +76,7 @@ const columnDefinitions = (resource, namespaces, onFilterClick, showNamespaceCol
|
|||
title: formatTitle("Dash", "Grafana Dashboard"),
|
||||
key: "grafanaDashboard",
|
||||
className: "numeric",
|
||||
width: smMetricColWidth,
|
||||
render: row => !row.added || _.get(row, "pods.totalPods") === "0" ? null : (
|
||||
<GrafanaLink
|
||||
name={row.name}
|
||||
|
|
@ -117,14 +119,16 @@ const columnDefinitions = (resource, namespaces, onFilterClick, showNamespaceCol
|
|||
dataIndex: "successRate",
|
||||
key: "successRateRollup",
|
||||
className: "numeric",
|
||||
width: "120px",
|
||||
sorter: (a, b) => numericSort(a.successRate, b.successRate),
|
||||
render: d => metricToFormatter["SUCCESS_RATE"](d)
|
||||
render: successRateWithMiniChart
|
||||
},
|
||||
{
|
||||
title: formatTitle("RPS", "Request Rate"),
|
||||
dataIndex: "requestRate",
|
||||
key: "requestRateRollup",
|
||||
className: "numeric",
|
||||
width: smMetricColWidth,
|
||||
sorter: (a, b) => numericSort(a.requestRate, b.requestRate),
|
||||
render: d => withTooltip(d, "REQUEST_RATE")
|
||||
},
|
||||
|
|
@ -133,6 +137,7 @@ const columnDefinitions = (resource, namespaces, onFilterClick, showNamespaceCol
|
|||
dataIndex: "P50",
|
||||
key: "p50LatencyRollup",
|
||||
className: "numeric",
|
||||
width: smMetricColWidth,
|
||||
sorter: (a, b) => numericSort(a.P50, b.P50),
|
||||
render: metricToFormatter["LATENCY"]
|
||||
},
|
||||
|
|
@ -141,6 +146,7 @@ const columnDefinitions = (resource, namespaces, onFilterClick, showNamespaceCol
|
|||
dataIndex: "P95",
|
||||
key: "p95LatencyRollup",
|
||||
className: "numeric",
|
||||
width: smMetricColWidth,
|
||||
sorter: (a, b) => numericSort(a.P95, b.P95),
|
||||
render: metricToFormatter["LATENCY"]
|
||||
},
|
||||
|
|
@ -149,6 +155,7 @@ const columnDefinitions = (resource, namespaces, onFilterClick, showNamespaceCol
|
|||
dataIndex: "P99",
|
||||
key: "p99LatencyRollup",
|
||||
className: "numeric",
|
||||
width: smMetricColWidth,
|
||||
sorter: (a, b) => numericSort(a.P99, b.P99),
|
||||
render: metricToFormatter["LATENCY"]
|
||||
},
|
||||
|
|
@ -157,6 +164,7 @@ const columnDefinitions = (resource, namespaces, onFilterClick, showNamespaceCol
|
|||
key: "tlsTraffic",
|
||||
dataIndex: "tlsRequestPercent",
|
||||
className: "numeric",
|
||||
width: smMetricColWidth,
|
||||
sorter: (a, b) => numericSort(a.tlsRequestPercent.get(), b.tlsRequestPercent.get()),
|
||||
render: d => _.isNil(d) || d.get() === -1 ? "---" : d.prettyRate()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import ErrorBanner from './ErrorBanner.jsx';
|
|||
import { friendlyTitle } from './util/Utils.js';
|
||||
import MetricsTable from './MetricsTable.jsx';
|
||||
import NetworkGraph from './NetworkGraph.jsx';
|
||||
import { processMultiResourceRollup } from './util/MetricUtils.js';
|
||||
import { processMultiResourceRollup } from './util/MetricUtils.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Spin } from 'antd';
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { Collapse, Icon, Spin, Tooltip } from 'antd';
|
||||
import { processMultiResourceRollup, processSingleResourceRollup } from './util/MetricUtils.js';
|
||||
import { processMultiResourceRollup, processSingleResourceRollup } from './util/MetricUtils.jsx';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
const isMeshedTooltip = (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import _ from 'lodash';
|
||||
import { metricsPropType } from './util/MetricUtils.js';
|
||||
import { metricsPropType } from './util/MetricUtils.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
|
|
|
|||
|
|
@ -2,19 +2,12 @@ import _ from 'lodash';
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Col, Icon, Progress, Row } from 'antd';
|
||||
import { getSuccessRateClassification, srArcClassLabels } from './util/MetricUtils.jsx' ;
|
||||
import { metricToFormatter, toShortResourceName } from './util/Utils.js';
|
||||
import './../../css/octopus.css';
|
||||
|
||||
const displayName = resource => `${toShortResourceName(resource.type)}/${resource.name}`;
|
||||
|
||||
const getSrClassification = sr => {
|
||||
if (sr < 0.9) {
|
||||
return "status-poor";
|
||||
} else if (sr < 0.95) {
|
||||
return "status-ok";
|
||||
} else {return "status-good";}
|
||||
};
|
||||
|
||||
const Metric = ({title, value, className}) => {
|
||||
return (
|
||||
<Row type="flex" justify="center" className={`octopus-metric ${className}`}>
|
||||
|
|
@ -60,7 +53,7 @@ export default class Octopus extends React.Component {
|
|||
<div>
|
||||
<div className="octopus-sr-gauge">
|
||||
<Progress
|
||||
className={getSrClassification(resource.successRate)}
|
||||
className={`success-rate-arc ${getSuccessRateClassification(resource.successRate, srArcClassLabels)}`}
|
||||
type="dashboard"
|
||||
format={() => metricToFormatter["SUCCESS_RATE"](resource.successRate)}
|
||||
width={type === "main" ? 132 : 64}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import ErrorBanner from './ErrorBanner.jsx';
|
|||
import MetricsTable from './MetricsTable.jsx';
|
||||
import Octopus from './Octopus.jsx';
|
||||
import { processNeighborData } from './util/TapUtils.jsx';
|
||||
import { processSingleResourceRollup } from './util/MetricUtils.js';
|
||||
import { processSingleResourceRollup } from './util/MetricUtils.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Spin } from 'antd';
|
||||
|
|
@ -226,7 +226,7 @@ export class ResourceDetailBase extends React.Component {
|
|||
|
||||
{ _.isEmpty(this.state.neighborMetrics.upstream) ? null : (
|
||||
<div className="page-section">
|
||||
<h2 className="subsection-header">Upstreams</h2>
|
||||
<h2 className="subsection-header">Inbound</h2>
|
||||
<MetricsTable
|
||||
resource={this.state.resource.type}
|
||||
metrics={this.state.neighborMetrics.upstream} />
|
||||
|
|
@ -236,7 +236,7 @@ export class ResourceDetailBase extends React.Component {
|
|||
|
||||
{ _.isEmpty(this.state.neighborMetrics.downstream) ? null : (
|
||||
<div className="page-section">
|
||||
<h2 className="subsection-header">Downstreams</h2>
|
||||
<h2 className="subsection-header">Outbound</h2>
|
||||
<MetricsTable
|
||||
resource={this.state.resource.type}
|
||||
metrics={this.state.neighborMetrics.downstream} />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import { Spin } from 'antd';
|
||||
import withREST from './util/withREST.jsx';
|
||||
import { metricsPropType, processSingleResourceRollup } from './util/MetricUtils.js';
|
||||
import { metricsPropType, processSingleResourceRollup } from './util/MetricUtils.jsx';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
export class ResourceListBase extends React.Component {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
getSuccessRateClassification,
|
||||
processMultiResourceRollup,
|
||||
processSingleResourceRollup
|
||||
} from './util/MetricUtils.js';
|
||||
} from './util/MetricUtils.jsx';
|
||||
import {linkerdLogoOnly, linkerdWordLogo} from './util/SvgWrappers.jsx';
|
||||
import './../../css/sidebar.css';
|
||||
import 'whatwg-fetch';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { successRateWithMiniChart } from './util/MetricUtils.jsx';
|
||||
import { Table } from 'antd';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { directionColumn, srcDstColumn } from './util/TapUtils.jsx';
|
||||
|
|
@ -26,32 +27,38 @@ const topColumns = (resourceType, ResourceLink) => [
|
|||
{
|
||||
title: "Count",
|
||||
dataIndex: "count",
|
||||
className: "numeric",
|
||||
defaultSortOrder: "descend",
|
||||
sorter: (a, b) => numericSort(a.count, b.count),
|
||||
},
|
||||
{
|
||||
title: "Best",
|
||||
dataIndex: "best",
|
||||
className: "numeric",
|
||||
sorter: (a, b) => numericSort(a.best, b.best),
|
||||
render: formatLatencySec
|
||||
},
|
||||
{
|
||||
title: "Worst",
|
||||
dataIndex: "worst",
|
||||
className: "numeric",
|
||||
sorter: (a, b) => numericSort(a.worst, b.worst),
|
||||
render: formatLatencySec
|
||||
},
|
||||
{
|
||||
title: "Last",
|
||||
dataIndex: "last",
|
||||
className: "numeric",
|
||||
sorter: (a, b) => numericSort(a.last, b.last),
|
||||
render: formatLatencySec
|
||||
},
|
||||
{
|
||||
title: "Success Rate",
|
||||
dataIndex: "successRate",
|
||||
className: "numeric",
|
||||
width: "128px",
|
||||
sorter: (a, b) => numericSort(a.successRate.get(), b.successRate.get()),
|
||||
render: d => !d ? "---" : d.prettyRate()
|
||||
render: d => successRateWithMiniChart(d.get())
|
||||
}
|
||||
];
|
||||
|
||||
|
|
@ -75,7 +82,7 @@ class TopEventTable extends React.Component {
|
|||
columns={topColumns(this.props.resourceType, this.props.api.ResourceLink)}
|
||||
rowKey="key"
|
||||
pagination={false}
|
||||
className="top-event-table"
|
||||
className="top-event-table metric-table"
|
||||
size="middle" />
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import _ from 'lodash';
|
||||
import { metricToFormatter } from './Utils.js';
|
||||
import Percentage from './Percentage.js';
|
||||
import { Progress } from 'antd';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
const getPodCategorization = pod => {
|
||||
if (pod.added && pod.status === "Running") {
|
||||
|
|
@ -27,6 +30,27 @@ export const getSuccessRateClassification = (rate, successRateLabels) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const srArcClassLabels = {
|
||||
good: "status-good",
|
||||
neutral: "status-ok",
|
||||
bad: "status-poor",
|
||||
default: "status-ok"
|
||||
};
|
||||
|
||||
export const successRateWithMiniChart = sr => (
|
||||
<div>
|
||||
<span className="metric-table-sr">{metricToFormatter["SUCCESS_RATE"](sr)}</span>
|
||||
<Progress
|
||||
className={`success-rate-arc ${getSuccessRateClassification(sr, srArcClassLabels)} metric-table-sr-chart`}
|
||||
type="dashboard"
|
||||
showInfo={false}
|
||||
width={32}
|
||||
strokeWidth={12}
|
||||
percent={sr * 100}
|
||||
gapDegree={180} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const getTotalRequests = row => {
|
||||
let success = parseInt(_.get(row, ["stats", "successCount"], 0), 10);
|
||||
let failure = parseInt(_.get(row, ["stats", "failureCount"], 0), 10);
|
||||
|
|
@ -12,6 +12,7 @@ export const rowGutter = 3 * baseWidth;
|
|||
*/
|
||||
const successRateFormatter = d3.format(".2%");
|
||||
const commaFormatter = d3.format(",");
|
||||
const secondsFormatter = d3.format(",.3s");
|
||||
|
||||
export const formatWithComma = m => {
|
||||
if (_.isNil(m)) {
|
||||
|
|
@ -42,7 +43,7 @@ export const formatLatencySec = latency => {
|
|||
} else if (s < 1.0) {
|
||||
return `${niceLatency(s * 1000)} ms`;
|
||||
} else {
|
||||
return `${niceLatency(s)} s`;
|
||||
return `${secondsFormatter(s)} s`;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -196,3 +197,11 @@ export const publicAddressToString = (ipv4, port) => {
|
|||
let octets = decodeIPToOctets(ipv4);
|
||||
return octets.join(".") + ":" + port;
|
||||
};
|
||||
|
||||
export const getSrClassification = sr => {
|
||||
if (sr < 0.9) {
|
||||
return "status-poor";
|
||||
} else if (sr < 0.95) {
|
||||
return "status-ok";
|
||||
} else {return "status-good";}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import Percentage from '../js/components/util/Percentage';
|
|||
import {
|
||||
processMultiResourceRollup,
|
||||
processSingleResourceRollup
|
||||
} from '../js/components/util/MetricUtils.js';
|
||||
} from '../js/components/util/MetricUtils.jsx';
|
||||
|
||||
describe('MetricUtils', () => {
|
||||
describe('processSingleResourceRollup', () => {
|
||||
|
|
|
|||
|
|
@ -79,8 +79,8 @@ describe('Utils', () => {
|
|||
});
|
||||
|
||||
it('formats latency greater than 1s as s', () => {
|
||||
expect(metricToFormatter["LATENCY"](1000)).to.equal('1 s');
|
||||
expect(metricToFormatter["LATENCY"](9999)).to.equal('10 s');
|
||||
expect(metricToFormatter["LATENCY"](1000)).to.equal('1.00 s');
|
||||
expect(metricToFormatter["LATENCY"](9999)).to.equal('10.0 s');
|
||||
expect(metricToFormatter["LATENCY"](99999)).to.equal('100 s');
|
||||
});
|
||||
|
||||
|
|
@ -99,7 +99,8 @@ describe('Utils', () => {
|
|||
expect(formatLatencySec("0.000231910")).to.equal("232 µs");
|
||||
expect(formatLatencySec("0.000988600")).to.equal("989 µs");
|
||||
expect(formatLatencySec("0.005598200")).to.equal("6 ms");
|
||||
expect(formatLatencySec("3.029409200")).to.equal("3 s");
|
||||
expect(formatLatencySec("3.029409200")).to.equal("3.03 s");
|
||||
expect(formatLatencySec("34.395600")).to.equal("34.4 s");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue