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": {} } ] }