Display proxy container errors in the Web UI (#1130)

* Display proxy container errors in the Web UI

Add an error modal to display pod errors
Add icon to data tables to indicate errors are present
Display errors on the Service Mesh Overview Page and all the resource pages
This commit is contained in:
Risha Mars 2018-06-15 14:12:36 -07:00 committed by GitHub
parent 5c42e4e22b
commit 0ed40288e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 125 additions and 10 deletions

View File

@ -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;
}
}

View File

@ -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) => (
<div key={`error-${container}`}>
<p>Container: {container}</p>
<p>Image: {_.get(errors, [0, "image"])}</p>
<div className="error-text">
{
_.map(errors, (er, i) =>
<code key={`error-msg-${i}`}>{er.message}</code>
)
}
</div>
</div>
));
};
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 (
<div className="conduit-pod-error" key={err.pod}>
<h3>Pod: {err.pod}</h3>
{this.renderContainerErrors(err.byContainer)}
</div>
);
});
}
render() {
return (
<Icon type="warning" className="conduit-error-icon" onClick={this.showModal} />
);
}
}

View File

@ -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 <ConduitLink to={"/namespaces/" + row.name}>{row.name}</ConduitLink>;
nameContents = <ConduitLink to={"/namespaces/" + row.name}>{row.name}</ConduitLink>;
} else if (!row.added) {
return row.name;
nameContents = row.name;
} else {
return (
nameContents = (
<GrafanaLink
name={row.name}
namespace={row.namespace}
@ -73,6 +75,12 @@ const columnDefinitions = (resource, namespaces, onFilterClick, showNamespaceCol
ConduitLink={ConduitLink} />
);
}
return (
<React.Fragment>
{nameContents}
{ _.isEmpty(row.errors) ? null : <ErrorModal errors={row.errors} resourceName={row.name} resourceType={resource} /> }
</React.Fragment>
);
}
},
{

View File

@ -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 => <ConduitLink to={"/namespaces/" + d}>{d}</ConduitLink>
render: d => {
return (
<React.Fragment>
<ConduitLink to={"/namespaces/" + d.namespace}>{d.namespace}</ConduitLink>
{ _.isEmpty(d.errors) ? null :
<ErrorModal errors={d.errors} resourceName={d.namespace} resourceType="namespace" />
}
</React.Fragment>
);
}
},
{
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 (
<Tooltip
@ -165,7 +175,8 @@ class ServiceMesh extends React.Component {
meshedPercent: new Percentage(meshedPods, totalPods),
meshedPods,
totalPods,
failedPods
failedPods,
errors: ns.errorsByPod
};
});
return _.compact(dataPlaneNamepaces);

View File

@ -117,7 +117,8 @@ const processStatTable = table => {
successRate: getSuccessRate(row),
latency: getLatency(row),
tlsRequestPercent: getTlsRequestPercentage(row),
added: row.meshedPodCount === row.runningPodCount
added: row.meshedPodCount === row.runningPodCount,
errors: row.errorsByPod
};
})
.compact()

View File

@ -26,7 +26,8 @@ describe('MetricUtils', () => {
P95: 2,
P99: 7
},
added: true
added: true,
errors: {}
}
];
expect(result).to.have.length(1);

View File

@ -21,7 +21,8 @@
},
"timeWindow": "1m",
"runningPodCount": "1",
"failedPodCount": null
"failedPodCount": null,
"errorsByPod": {}
}
]
}