From cc98b5e78489e51675ea52248b3e7820adce4de1 Mon Sep 17 00:00:00 2001 From: Risha Mars Date: Tue, 21 Aug 2018 15:01:34 -0700 Subject: [PATCH] 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. --- web/app/css/octopus.css | 40 +++++++++ web/app/css/service-mesh.css | 16 +--- web/app/css/styles.css | 21 +++++ web/app/js/components/Octopus.jsx | 99 +++++++++++++++++++++++ web/app/js/components/ResourceDetail.jsx | 48 +++++++++-- web/app/js/components/util/MetricUtils.js | 1 + web/app/js/components/util/Utils.js | 19 ++++- web/app/test/MetricUtilsTest.js | 1 + 8 files changed, 222 insertions(+), 23 deletions(-) create mode 100644 web/app/css/octopus.css create mode 100644 web/app/js/components/Octopus.jsx diff --git a/web/app/css/octopus.css b/web/app/css/octopus.css new file mode 100644 index 000000000..321270f2c --- /dev/null +++ b/web/app/css/octopus.css @@ -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; +} diff --git a/web/app/css/service-mesh.css b/web/app/css/service-mesh.css index f3a859cd0..9b15200a6 100644 --- a/web/app/css/service-mesh.css +++ b/web/app/css/service-mesh.css @@ -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; - } } diff --git a/web/app/css/styles.css b/web/app/css/styles.css index d363d7db1..038fa471d 100644 --- a/web/app/css/styles.css +++ b/web/app/css/styles.css @@ -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; + } +} diff --git a/web/app/js/components/Octopus.jsx b/web/app/js/components/Octopus.jsx new file mode 100644 index 000000000..161f45f26 --- /dev/null +++ b/web/app/js/components/Octopus.jsx @@ -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 ( +
+ } + placement={direction ==="in" ? "left" : "right"}> +
+
{direction === "in" ? "<" : ">"}
+
+
{displayName(neighbor)}
+
+ +
+ ); +}; +Neighbor.propTypes = { + direction: PropTypes.string.isRequired, + neighbor: PropTypes.shape({}).isRequired +}; + +const Metric = ({title, value, metricClass}) => { + return ( + +
{title}
+
{value}
+
+ ); +}; +Metric.propTypes = { + metricClass: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + value: PropTypes.string.isRequired +}; + +const MetricSummaryRow = ({resource, metricClass}) => { + return ( + + + + + + ); +}; +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 ( +
+
+

{displayName(resource)}

+ +
+ + + {_.map(neighbors.upstream, n => )} + + + {_.map(neighbors.downstream, n => )} + + +
+
+ ); + } +} diff --git a/web/app/js/components/ResourceDetail.jsx b/web/app/js/components/ResourceDetail.jsx index 71ddc0905..fc49bb4ef 100644 --- a/web/app/js/components/ResourceDetail.jsx +++ b/web/app/js/components/ResourceDetail.jsx @@ -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 (
- +
+ { _.isEmpty(this.state.neighborMetrics.upstream) ? null : ( +
+

Upstreams

+ +
+ ) + } + + { _.isEmpty(this.state.neighborMetrics.downstream) ? null : ( +
+

Downstreams

+ +
+ ) + } + { this.state.resource.type === "pod" ? null : (
@@ -167,7 +203,7 @@ export class ResourceDetailBase extends React.Component { resource="pod" metrics={this.state.podMetrics} />
- ) + ) }
); diff --git a/web/app/js/components/util/MetricUtils.js b/web/app/js/components/util/MetricUtils.js index 157c80bab..04a9de531 100644 --- a/web/app/js/components/util/MetricUtils.js +++ b/web/app/js/components/util/MetricUtils.js @@ -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), diff --git a/web/app/js/components/util/Utils.js b/web/app/js/components/util/Utils.js index 6db69be03..a8c35ce02 100644 --- a/web/app/js/components/util/Utils.js +++ b/web/app/js/components/util/Utils.js @@ -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 */ diff --git a/web/app/test/MetricUtilsTest.js b/web/app/test/MetricUtilsTest.js index 3a19c92d9..ca791229f 100644 --- a/web/app/test/MetricUtilsTest.js +++ b/web/app/test/MetricUtilsTest.js @@ -17,6 +17,7 @@ describe('MetricUtils', () => { { name: 'voting', namespace: 'emojivoto', + type: 'deployment', requestRate: 2.5, successRate: 0.9, totalRequests: 150,