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,