mirror of https://github.com/linkerd/linkerd2.git
Add the basis for an octopus graph to resource detail page (#1494)
Add a basic top graph depicting the current resource's stats and it's upstreams and downstreams. Also add upstreams and downstreams tables for this resource This will be styled more later, but just getting the basic components and data onto the page.
This commit is contained in:
parent
5fc63cde10
commit
cc98b5e784
|
@ -0,0 +1,40 @@
|
|||
@import 'styles.css';
|
||||
|
||||
.octopus-container {
|
||||
padding: 80px;
|
||||
}
|
||||
|
||||
.octopus-graph {
|
||||
background-color: #ffffff;
|
||||
height: 100%;
|
||||
width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 24px;
|
||||
box-shadow: 2px 2px 2px var(--neutralgrey);
|
||||
|
||||
& .octopus-title, & .octopus-metric {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& .octopus-upstreams .neighbor, & .octopus-downstreams .neighbor {
|
||||
clear: both;
|
||||
|
||||
& .status-dot {
|
||||
margin: 4px 4px 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
& .octopus-upstreams .neighbor > div div {
|
||||
float: left;
|
||||
}
|
||||
|
||||
& .octopus-downstreams .neighbor > div div {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.octopus-metric-lg {
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
}
|
|
@ -62,28 +62,14 @@
|
|||
}
|
||||
|
||||
/* styles for the StatusTable */
|
||||
td .status-dot {
|
||||
td div.status-dot {
|
||||
float: left;
|
||||
width: calc(2 * var(--base-width));
|
||||
height: calc(2 * var(--base-width));
|
||||
min-width: calc(2 * var(--base-width));
|
||||
border-radius: 50%;
|
||||
margin-right: var(--base-width);
|
||||
|
||||
&.dot-multiline {
|
||||
margin-top: calc(0.5 * var(--base-width));
|
||||
margin-bottom: calc(0.5 * var(--base-width));
|
||||
}
|
||||
|
||||
&.status-dot-good {
|
||||
background-color: var(--green);
|
||||
}
|
||||
&.status-dot-poor {
|
||||
background-color: var(--siennared);
|
||||
}
|
||||
&.status-dot-neutral {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -239,3 +239,24 @@ a.button.primary:active {
|
|||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
/* Colored dot for indicating statuses */
|
||||
div.status-dot {
|
||||
width: calc(2 * var(--base-width));
|
||||
height: calc(2 * var(--base-width));
|
||||
min-width: calc(2 * var(--base-width));
|
||||
border-radius: 50%;
|
||||
|
||||
&.status-dot-good {
|
||||
background-color: var(--green);
|
||||
}
|
||||
&.status-dot-poor {
|
||||
background-color: var(--siennared);
|
||||
}
|
||||
&.status-dot-neutral {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
&.status-dot-ok {
|
||||
background-color: #ffd54f;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Col, Popover, Row } from 'antd';
|
||||
import { metricToFormatter, toShortResourceName } from './util/Utils.js';
|
||||
import './../../css/octopus.css';
|
||||
|
||||
const displayName = resource => `${toShortResourceName(resource.type)}/${resource.name}`;
|
||||
|
||||
const getDotClassification = sr => {
|
||||
if (sr < 0.9) {
|
||||
return "status-dot-poor";
|
||||
} else if (sr < 0.95) {
|
||||
return "status-dot-ok";
|
||||
} else {return "status-dot-good";}
|
||||
};
|
||||
|
||||
const Neighbor = ({neighbor, direction}) => {
|
||||
return (
|
||||
<div className="neighbor">
|
||||
<Popover
|
||||
title={displayName(neighbor)}
|
||||
content={<MetricSummaryRow resource={neighbor} metricClass="metric-sm" />}
|
||||
placement={direction ==="in" ? "left" : "right"}>
|
||||
<div className="neighbor-row">
|
||||
<div>{direction === "in" ? "<" : ">"}</div>
|
||||
<div className={`status-dot ${getDotClassification(neighbor.successRate)}`} />
|
||||
<div>{displayName(neighbor)}</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Neighbor.propTypes = {
|
||||
direction: PropTypes.string.isRequired,
|
||||
neighbor: PropTypes.shape({}).isRequired
|
||||
};
|
||||
|
||||
const Metric = ({title, value, metricClass}) => {
|
||||
return (
|
||||
<Row type="flex" justify="center" className={`octopus-${metricClass}`}>
|
||||
<Col span={12} className="octopus-metric-title"><div>{title}</div></Col>
|
||||
<Col span={12} className="octopus-metric-value"><div>{value}</div></Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
Metric.propTypes = {
|
||||
metricClass: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
const MetricSummaryRow = ({resource, metricClass}) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Metric title="Success Rate" value={metricToFormatter["SUCCESS_RATE"](resource.successRate)} metricClass={metricClass} />
|
||||
<Metric title="Request Rate" value={metricToFormatter["REQUEST_RATE"](resource.requestRate)} metricClass={metricClass} />
|
||||
<Metric title="P99 Latency" value={metricToFormatter["LATENCY"](_.get(resource, "latency.P99"))} metricClass={metricClass} />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
MetricSummaryRow.propTypes = {
|
||||
metricClass: PropTypes.string.isRequired,
|
||||
resource: PropTypes.shape({}).isRequired
|
||||
};
|
||||
|
||||
export default class Octopus extends React.Component {
|
||||
static defaultProps = {
|
||||
metrics: {},
|
||||
neighbors: {}
|
||||
}
|
||||
static propTypes = {
|
||||
metrics: PropTypes.shape({}),
|
||||
neighbors: PropTypes.shape({}),
|
||||
resource: PropTypes.shape({}).isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
let { resource, metrics, neighbors } = this.props;
|
||||
|
||||
return (
|
||||
<div className="octopus-container">
|
||||
<div className="octopus-graph">
|
||||
<h1 className="octopus-title">{displayName(resource)}</h1>
|
||||
<MetricSummaryRow resource={metrics} metricClass="metric-lg" />
|
||||
<hr />
|
||||
<Row type="flex" justify="center">
|
||||
<Col span={12} className="octopus-upstreams">
|
||||
{_.map(neighbors.upstream, n => <Neighbor neighbor={n} direction="in" key={n.namespace + "-" + n.name} />)}
|
||||
</Col>
|
||||
<Col span={12} className="octopus-downstreams">
|
||||
{_.map(neighbors.downstream, n => <Neighbor neighbor={n} direction="out" key={n.namespace + "-" + n.name} />)}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import _ from 'lodash';
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import MetricsTable from './MetricsTable.jsx';
|
||||
import Octopus from './Octopus.jsx';
|
||||
import PageHeader from './PageHeader.jsx';
|
||||
import { processSingleResourceRollup } from './util/MetricUtils.js';
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -94,12 +95,22 @@ export class ResourceDetailBase extends React.Component {
|
|||
this.api.fetchMetrics(
|
||||
`${this.api.urlsForResource("pod", resource.namespace)}`
|
||||
),
|
||||
// upstream resources of this resource (meshed traffic only)
|
||||
this.api.fetchMetrics(
|
||||
`${this.api.urlsForResource(resource.type)}&to_name=${resource.name}&to_type=${resource.type}&to_namespace=${resource.namespace}`
|
||||
),
|
||||
// downstream resources of this resource (meshed traffic only)
|
||||
this.api.fetchMetrics(
|
||||
`${this.api.urlsForResource(resource.type)}&from_name=${resource.name}&from_type=${resource.type}&from_namespace=${resource.namespace}`
|
||||
)
|
||||
]);
|
||||
|
||||
Promise.all(this.api.getCurrentPromises())
|
||||
.then(([resourceRsp, podListRsp, podRsp]) => {
|
||||
.then(([resourceRsp, podListRsp, podMetricsRsp, upstreamRsp, downstreamRsp]) => {
|
||||
let resourceMetrics = processSingleResourceRollup(resourceRsp);
|
||||
let podMetrics = processSingleResourceRollup(podRsp);
|
||||
let podMetrics = processSingleResourceRollup(podMetricsRsp);
|
||||
let upstreamMetrics = processSingleResourceRollup(upstreamRsp);
|
||||
let downstreamMetrics = processSingleResourceRollup(downstreamRsp);
|
||||
|
||||
// INEFFICIENT: get metrics for all the pods belonging to this resource.
|
||||
// Do this by querying for metrics for all pods in this namespace and then filtering
|
||||
|
@ -118,6 +129,10 @@ export class ResourceDetailBase extends React.Component {
|
|||
this.setState({
|
||||
resourceMetrics,
|
||||
podMetrics: podMetricsForResource,
|
||||
neighborMetrics: {
|
||||
upstream: upstreamMetrics,
|
||||
downstream: downstreamMetrics
|
||||
},
|
||||
loaded: true,
|
||||
pendingRequests: false,
|
||||
error: null
|
||||
|
@ -154,11 +169,32 @@ export class ResourceDetailBase extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
<div className="page-section">
|
||||
<MetricsTable
|
||||
resource={this.state.resource.type}
|
||||
metrics={this.state.resourceMetrics} />
|
||||
<Octopus
|
||||
resource={this.state.resource}
|
||||
metrics={this.state.resourceMetrics[0]}
|
||||
neighbors={this.state.neighborMetrics} />
|
||||
</div>
|
||||
|
||||
{ _.isEmpty(this.state.neighborMetrics.upstream) ? null : (
|
||||
<div className="page-section">
|
||||
<h2 className="subsection-header">Upstreams</h2>
|
||||
<MetricsTable
|
||||
resource={this.state.resource.type}
|
||||
metrics={this.state.neighborMetrics.upstream} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{ _.isEmpty(this.state.neighborMetrics.downstream) ? null : (
|
||||
<div className="page-section">
|
||||
<h2 className="subsection-header">Downstreams</h2>
|
||||
<MetricsTable
|
||||
resource={this.state.resource.type}
|
||||
metrics={this.state.neighborMetrics.downstream} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
this.state.resource.type === "pod" ? null : (
|
||||
<div className="page-section">
|
||||
|
@ -167,7 +203,7 @@ export class ResourceDetailBase extends React.Component {
|
|||
resource="pod"
|
||||
metrics={this.state.podMetrics} />
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -112,6 +112,7 @@ const processStatTable = table => {
|
|||
return {
|
||||
name: row.resource.name,
|
||||
namespace: row.resource.namespace,
|
||||
type: row.resource.type,
|
||||
totalRequests: getTotalRequests(row),
|
||||
requestRate: getRequestRate(row),
|
||||
successRate: getSuccessRate(row),
|
||||
|
|
|
@ -143,10 +143,25 @@ const camelCaseLookUp = {
|
|||
"daemonset": "daemonSet"
|
||||
};
|
||||
|
||||
export const resourceTypeToCamelCase = resource => {
|
||||
return camelCaseLookUp[resource] || resource;
|
||||
export const resourceTypeToCamelCase = resource => camelCaseLookUp[resource] || resource;
|
||||
|
||||
/*
|
||||
A simplified version of ShortNameFromCanonicalResourceName
|
||||
*/
|
||||
const shortNameLookup = {
|
||||
"deployment": "deploy",
|
||||
"daemonset": "ds",
|
||||
"namespace": "ns",
|
||||
"pod": "po",
|
||||
"replicationcontroller": "rc",
|
||||
"replicaset": "rs",
|
||||
"service": "svc",
|
||||
"statefulset": "sts",
|
||||
"authority": "au"
|
||||
};
|
||||
|
||||
export const toShortResourceName = name => shortNameLookup[name] || name;
|
||||
|
||||
/*
|
||||
produce octets given an ip address
|
||||
*/
|
||||
|
|
|
@ -17,6 +17,7 @@ describe('MetricUtils', () => {
|
|||
{
|
||||
name: 'voting',
|
||||
namespace: 'emojivoto',
|
||||
type: 'deployment',
|
||||
requestRate: 2.5,
|
||||
successRate: 0.9,
|
||||
totalRequests: 150,
|
||||
|
|
Loading…
Reference in New Issue