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:
Risha Mars 2018-08-21 15:01:34 -07:00 committed by GitHub
parent 5fc63cde10
commit cc98b5e784
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 222 additions and 23 deletions

40
web/app/css/octopus.css Normal file
View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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>
);
}
}

View File

@ -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>
);

View File

@ -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),

View File

@ -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
*/

View File

@ -17,6 +17,7 @@ describe('MetricUtils', () => {
{
name: 'voting',
namespace: 'emojivoto',
type: 'deployment',
requestRate: 2.5,
successRate: 0.9,
totalRequests: 150,