diff --git a/web/app/css/service-mesh.css b/web/app/css/service-mesh.css
index 1bab79ac6..48ab78e79 100644
--- a/web/app/css/service-mesh.css
+++ b/web/app/css/service-mesh.css
@@ -85,3 +85,28 @@ td .status-dot {
background-color: #E0E0E0;
}
}
+
+
+/* error indicator and modal */
+.conduit-error-icon {
+ cursor: pointer;
+ margin-left: 5px;
+ color: var(--siennared);
+}
+
+.conduit-pod-error {
+ margin-top: calc(2 * var(--base-width));
+ margin-bottom: calc(3 * var(--base-width));
+
+ & p {
+ line-height: 8px;
+ }
+
+ & .error-text {
+ padding: var(--base-width);
+ border-radius: calc(0.5 * var(--base-width));
+ font-size: 12px;
+ color: white;
+ background-color: #696969;
+ }
+}
diff --git a/web/app/js/components/ErrorModal.jsx b/web/app/js/components/ErrorModal.jsx
new file mode 100644
index 000000000..bd4593831
--- /dev/null
+++ b/web/app/js/components/ErrorModal.jsx
@@ -0,0 +1,68 @@
+import _ from 'lodash';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Icon, Modal } from 'antd';
+
+export default class ErrorModal extends React.Component {
+ static propTypes = {
+ errors: PropTypes.shape({}).isRequired,
+ resourceName: PropTypes.string.isRequired,
+ resourceType: PropTypes.string.isRequired
+ }
+
+ showModal = () => {
+ Modal.error({
+ title: `Errors in ${this.props.resourceType} ${this.props.resourceName}`,
+ width: 800,
+ maskClosable: true,
+ content: this.renderPodErrors(this.props.errors),
+ onOk() {},
+ });
+ }
+
+ renderContainerErrors = errorsByContainer => {
+ return _.map(errorsByContainer, (errors, container) => (
+
+
Container: {container}
+
Image: {_.get(errors, [0, "image"])}
+
+ {
+ _.map(errors, (er, i) =>
+ {er.message}
+ )
+ }
+
+
+ ));
+ };
+
+ renderPodErrors = podErrors => {
+ let errorsByPodAndContainer = _(podErrors)
+ .keys()
+ .sortBy()
+ .map(pod => {
+ return {
+ pod: pod,
+ byContainer: _(podErrors[pod].errors)
+ .groupBy( "container.container")
+ .mapValues(v => _.map(v, "container"))
+ .value()
+ };
+ }).value();
+
+ return _.map(errorsByPodAndContainer, err => {
+ return (
+
+
Pod: {err.pod}
+ {this.renderContainerErrors(err.byContainer)}
+
+ );
+ });
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/web/app/js/components/MetricsTable.jsx b/web/app/js/components/MetricsTable.jsx
index ad3a6883e..b96facf15 100644
--- a/web/app/js/components/MetricsTable.jsx
+++ b/web/app/js/components/MetricsTable.jsx
@@ -1,5 +1,6 @@
import _ from 'lodash';
import BaseTable from './BaseTable.jsx';
+import ErrorModal from './ErrorModal.jsx';
import GrafanaLink from './GrafanaLink.jsx';
import { processedMetricsPropType } from './util/MetricUtils.js';
import PropTypes from 'prop-types';
@@ -60,12 +61,13 @@ const columnDefinitions = (resource, namespaces, onFilterClick, showNamespaceCol
defaultSortOrder: 'ascend',
sorter: (a, b) => (a.name || "").localeCompare(b.name),
render: row => {
+ let nameContents;
if (resource.toLowerCase() === "namespace") {
- return {row.name};
+ nameContents = {row.name};
} else if (!row.added) {
- return row.name;
+ nameContents = row.name;
} else {
- return (
+ nameContents = (
);
}
+ return (
+
+ {nameContents}
+ { _.isEmpty(row.errors) ? null : }
+
+ );
}
},
{
diff --git a/web/app/js/components/ServiceMesh.jsx b/web/app/js/components/ServiceMesh.jsx
index 96132867b..c97e6e06f 100644
--- a/web/app/js/components/ServiceMesh.jsx
+++ b/web/app/js/components/ServiceMesh.jsx
@@ -2,6 +2,7 @@ import _ from 'lodash';
import CallToAction from './CallToAction.jsx';
import ConduitSpinner from "./ConduitSpinner.jsx";
import ErrorBanner from './ErrorBanner.jsx';
+import ErrorModal from './ErrorModal.jsx';
import { incompleteMeshMessage } from './util/CopyUtils.jsx';
import Metric from './Metric.jsx';
import { numericSort } from './util/Utils.js';
@@ -41,11 +42,19 @@ const getClassification = (meshedPodCount, failedPodCount) => {
const namespacesColumns = ConduitLink => [
{
title: "Namespace",
- dataIndex: "namespace",
key: "namespace",
defaultSortOrder: "ascend",
sorter: (a, b) => (a.namespace || "").localeCompare(b.namespace),
- render: d => {d}
+ render: d => {
+ return (
+
+ {d.namespace}
+ { _.isEmpty(d.errors) ? null :
+
+ }
+
+ );
+ }
},
{
title: "Meshed pods",
@@ -62,7 +71,8 @@ const namespacesColumns = ConduitLink => [
let containerWidth = 132;
let percent = row.meshedPercent.get();
let barWidth = percent < 0 ? 0 : Math.round(percent * containerWidth);
- let barType = getClassification(row.meshedPods, row.failedPods);
+ let barType = _.isEmpty(row.errors) ?
+ getClassification(row.meshedPods, row.failedPods) : "poor";
return (
{
successRate: getSuccessRate(row),
latency: getLatency(row),
tlsRequestPercent: getTlsRequestPercentage(row),
- added: row.meshedPodCount === row.runningPodCount
+ added: row.meshedPodCount === row.runningPodCount,
+ errors: row.errorsByPod
};
})
.compact()
diff --git a/web/app/test/MetricUtilsTest.js b/web/app/test/MetricUtilsTest.js
index e6e860f75..494705d50 100644
--- a/web/app/test/MetricUtilsTest.js
+++ b/web/app/test/MetricUtilsTest.js
@@ -26,7 +26,8 @@ describe('MetricUtils', () => {
P95: 2,
P99: 7
},
- added: true
+ added: true,
+ errors: {}
}
];
expect(result).to.have.length(1);
diff --git a/web/app/test/fixtures/deployRollup.json b/web/app/test/fixtures/deployRollup.json
index 52ce52c82..756b7cfd2 100644
--- a/web/app/test/fixtures/deployRollup.json
+++ b/web/app/test/fixtures/deployRollup.json
@@ -21,7 +21,8 @@
},
"timeWindow": "1m",
"runningPodCount": "1",
- "failedPodCount": null
+ "failedPodCount": null,
+ "errorsByPod": {}
}
]
}