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,