Add links to each deployment name in the Conduit dashboard (#44)

* add links for each deployment name and add a message for unadded deployments
This commit is contained in:
Dennis Adjei-Baah 2017-12-19 15:40:24 -08:00 committed by GitHub
parent 3591936de3
commit 42d942c0bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 140 additions and 54 deletions

View File

@ -14,5 +14,17 @@ indent_size = 4
[*.go]
indent_style = tab
[*.jsx]
indent_size = 2
indent_style = space
[*.js]
indent_size = 2
indent_style = space
[*.css]
indent_size = 2
indent_style = space
[*.yml]
indent_size = 2

View File

@ -14,7 +14,7 @@
}
& .summary {
color: #27AE60;
color: var(--green);
}
@ -56,8 +56,8 @@
}
& .complete {
color: #27AE60;
background-color: #27AE60;
color: var(--green);
background-color: var(--green);
}
& .incomplete {
color: #828282;

View File

@ -5,6 +5,32 @@
margin-bottom: 30px;
}
& .deployment-title {
margin: 6px 0;
& h1 {
display: inline-block;
}
& .unadded-message {
display: inline;
}
}
& .status-badge {
display: inline-block;
border-radius: 50px;
padding: 4px 10px;
margin: 6px 12px;
font-size: small;
&.unadded {
background: var(--status-gray);
color: white;
vertical-align: top;
}
}
& .deployment-details {
&.border-container {
height: 276px; /* override height */

View File

@ -22,7 +22,7 @@
padding: 0 var(--base-width);
&.health-good {
border: 2px solid #27AE60;
border: 2px solid var(--green);
}
&.health-bad {
border: 2px solid #EB5757;
@ -30,13 +30,16 @@
&.health-neutral {
border: 2px solid #108ee9;
}
&.health-unknown {
border: 2px solid var(--status-gray);
}
}
& .adjacent-health {
/* override ant progress bar styles */
&.health-good {
& .ant-progress-bg {
background: #27AE60;
background: var(--green);
}
}
&.health-bad {
@ -49,6 +52,11 @@
background: #108ee9;
}
}
&.health-unknown {
& .ant-progress-bg {
background: var(--status-gray);
}
}
}
& .entity-count {

View File

@ -39,7 +39,7 @@ td .status-dot {
}
&.status-dot-good {
background-color: #27AE60;
background-color: var(--green);
}
&.status-dot-bad {
background-color: #EB5757;

View File

@ -11,6 +11,8 @@
--latency-p95: #2D9CDB;
--latency-p50: #56CCF2;
--subheader-grey: #828282;
--status-gray: #BDBDBD;
--green: #27AE60;
--font-stack: 'Lato', helvetica, arial, sans-serif;
--font-weight-light: 300;
--font-weight-regular: 400;
@ -115,7 +117,7 @@ h2, h3, h4, h5, h6 {
text-transform: uppercase;
font-weight: bold;
border-bottom: 1px solid;
border-color: #BDBDBD;
border-color: var(--status-gray);
word-break: keep-all;
}

View File

@ -9,7 +9,7 @@ import StatPane from './StatPane.jsx';
import TabbedMetricsTable from './TabbedMetricsTable.jsx';
import UpstreamDownstream from './UpstreamDownstream.jsx';
import { Col, Row } from 'antd';
import { processRollupMetrics, processTimeseriesMetrics } from './util/MetricUtils.js';
import { getPodsByDeployment, processRollupMetrics, processTimeseriesMetrics } from './util/MetricUtils.js';
import './../../css/deployment.css';
import 'whatwg-fetch';
@ -69,6 +69,7 @@ export default class Deployment extends React.Component {
let upstreamTimeseriesUrl = `${upstreamRollupUrl}&timeseries=true`;
let downstreamRollupUrl = `${metricsUrl}&aggregation=target_deploy&source_deploy=${this.state.deploy}`;
let downstreamTimeseriesUrl = `${downstreamRollupUrl}&timeseries=true`;
let podListUrl = `${this.props.pathPrefix}/api/pods`;
let deployFetch = fetch(deployMetricsUrl).then(r => r.json());
let podFetch = fetch(podRollupUrl).then(r => r.json());
@ -77,9 +78,10 @@ export default class Deployment extends React.Component {
let upstreamTsFetch = fetch(upstreamTimeseriesUrl).then(r => r.json());
let downstreamFetch = fetch(downstreamRollupUrl).then(r => r.json());
let downstreamTsFetch = fetch(downstreamTimeseriesUrl).then(r => r.json());
let podListFetch = fetch(podListUrl).then(r => r.json());
Promise.all([deployFetch, podFetch, podTsFetch, upstreamFetch, upstreamTsFetch, downstreamFetch, downstreamTsFetch])
.then(([deployMetrics, podRollup, podTimeseries, upstreamRollup, upstreamTimeseries, downstreamRollup, downstreamTimeseries]) => {
Promise.all([deployFetch, podFetch, podTsFetch, upstreamFetch, upstreamTsFetch, downstreamFetch, downstreamTsFetch, podListFetch])
.then(([deployMetrics, podRollup, podTimeseries, upstreamRollup, upstreamTimeseries, downstreamRollup, downstreamTimeseries, podList]) => {
let tsByDeploy = processTimeseriesMetrics(deployMetrics.metrics, "targetDeploy");
let podMetrics = processRollupMetrics(podRollup.metrics, "targetPod");
let podTs = processTimeseriesMetrics(podTimeseries.metrics, "targetPod");
@ -89,12 +91,14 @@ export default class Deployment extends React.Component {
let downstreamMetrics = processRollupMetrics(downstreamRollup.metrics, "targetDeploy");
let downstreamTsByDeploy = processTimeseriesMetrics(downstreamTimeseries.metrics, "targetDeploy");
let deploy = _.find(getPodsByDeployment(podList.pods), ["name", this.state.deploy]);
let totalRequestRate = _.sumBy(podMetrics, "requestRate");
_.each(podMetrics, datum => datum.totalRequests = totalRequestRate);
this.setState({
metrics: podMetrics,
timeseriesByPod: podTs,
added: deploy.added,
deployTs: _.get(tsByDeploy, this.state.deploy, {}),
upstreamMetrics: upstreamMetrics,
upstreamTsByDeploy: upstreamTsByDeploy,
@ -128,7 +132,8 @@ export default class Deployment extends React.Component {
entityType="deployment"
currentSr={currentSuccessRate}
upstreamMetrics={this.state.upstreamMetrics}
downstreamMetrics={this.state.downstreamMetrics} />,
downstreamMetrics={this.state.downstreamMetrics}
deploymentAdded={this.state.added} />,
<StatPane
key="stat-pane"
lastUpdated={this.state.lastUpdated}
@ -188,12 +193,23 @@ export default class Deployment extends React.Component {
);
}
renderContent() {
if (_.isEmpty(this.state.metrics)) {
return <div>No data</div>;
} else {
return this.renderSections();
}
renderDeploymentTitle() {
return (
<div className="deployment-title">
<h1>{this.state.deploy}</h1>
{
!this.state.added ? (
<div className="unadded-message">
<div className="status-badge unadded"><p>UNADDED</p></div>
<div className="call-to-action">
<div className="action">Add {this.state.deploy} to the deployment.yml file</div>
<div className="action">Then run <code>kubectl inject deployment.yml | kubectl apply -f -</code> to add the deploys to the service mesh</div>
</div>
</div>
) : null
}
</div>
);
}
render() {
@ -203,9 +219,9 @@ export default class Deployment extends React.Component {
<div className="page-content deployment-detail">
<div className="page-header">
<div className="subsection-header">Deployment detail</div>
<h1>{this.state.deploy}</h1>
{this.renderDeploymentTitle()}
</div>
{this.renderContent()}
{this.renderSections()}
</div>
);
}

View File

@ -6,7 +6,7 @@ import React from 'react';
import { rowGutter } from './util/Utils.js';
import TabbedMetricsTable from './TabbedMetricsTable.jsx';
import { Col, Row } from 'antd';
import { emptyMetric, processRollupMetrics, processTimeseriesMetrics } from './util/MetricUtils.js';
import { emptyMetric, getPodsByDeployment, processRollupMetrics, processTimeseriesMetrics } from './util/MetricUtils.js';
import './../../css/deployments.css';
import 'whatwg-fetch';
@ -38,17 +38,6 @@ export default class Deployments extends React.Component {
window.clearInterval(this.timerId);
}
getDeploymentList(pods) {
return _(pods)
.reject(p => _.isEmpty(p.deployment) || p.controlPlane)
.groupBy('deployment')
.map((componentPods, name) => {
return { name: name, added: _.every(componentPods, 'added') };
})
.sortBy('name')
.value();
}
addDeploysWithNoMetrics(deploys, metrics) {
// also display deployments which have not been added to the service mesh
// (and therefore have no associated metrics)
@ -75,7 +64,7 @@ export default class Deployments extends React.Component {
// expose serverPromise for testing
this.serverPromise = Promise.all([rollupRequest, podRequest])
.then(([rollup, p]) => {
let poByDeploy = this.getDeploymentList(p.pods);
let poByDeploy = getPodsByDeployment(p.pods);
let meshDeploys = processRollupMetrics(rollup.metrics, "targetDeploy");
let combinedMetrics = this.addDeploysWithNoMetrics(poByDeploy, meshDeploys);

View File

@ -5,17 +5,16 @@ import React from 'react';
import { Col, Progress, Row } from 'antd';
import './../../css/health-pane.css';
const TrafficIndicator = () => {
const TrafficIndicator = ({healthStat}) => {
return (
<Progress
percent={100}
status="active"
status={healthStat === "health-unknown" ? "success" : "active"}
strokeWidth={8}
format={() => null} />
);
};
const neutralSr = 0.5;
export default class HealthPane extends React.Component {
getRequestRate(metrics) {
@ -27,6 +26,9 @@ export default class HealthPane extends React.Component {
}
getHealthClassName(successRate) {
if (_.isNil(successRate) || _.isNaN(successRate)) {
return "health-unknown";
}
if (successRate < 0.4) {
return "health-bad";
}
@ -39,7 +41,7 @@ export default class HealthPane extends React.Component {
getHealthStats() {
let inboundSr = this.getAvgSuccessRate(this.props.upstreamMetrics);
let outboundSr = this.getAvgSuccessRate(this.props.downstreamMetrics);
let sr = _.isUndefined(this.props.currentSr) ? neutralSr : this.props.currentSr;
let sr = this.props.currentSr;
return {
inbound: {
@ -76,7 +78,7 @@ export default class HealthPane extends React.Component {
<Col span={8}>
<div className="entity-count">&laquo; {_.size(this.props.upstreamMetrics)} {this.props.entityType}s</div>
<div className={`adjacent-health ${stats.inbound.health}`}>
<TrafficIndicator />
<TrafficIndicator healthStat={stats.inbound.health} />
</div>
</Col>
<Col span={8}>
@ -86,7 +88,7 @@ export default class HealthPane extends React.Component {
<Col span={8}>
<div className="entity-count float-right">{_.size(this.props.downstreamMetrics)} {this.props.entityType}s &raquo;</div>
<div className={`adjacent-health ${stats.outbound.health}`}>
<TrafficIndicator />
<TrafficIndicator healthStat={stats.outbound.health} />
</div>
</Col>
</Row>

View File

@ -211,6 +211,7 @@ export default class ServiceMesh extends React.Component {
<StatusTable
data={this.state.components}
shouldLink={false}
statusColumnTitle="Pod Status" />
</div>
);

View File

@ -41,13 +41,7 @@ const columns = {
title: "Deployment",
dataIndex: "name",
key: "name",
render: (name, record) => {
if (shouldLink && _.some(record.statuses, ["value", "good"])) {
return <Link to={`${pathPrefix}/deployment?deploy=${name}`}>{name}</Link>;
} else {
return name;
}
}
render: name => shouldLink ? <Link to={`${pathPrefix}/deployment?deploy=${name}`}>{name}</Link> : name
};
},
pods: {

View File

@ -26,10 +26,7 @@ const columns = {
title: resource.title,
dataIndex: "name",
key: "name",
render: (_text, deploy) => {
return deploy.added ? <Link to={`${pathPrefix}${resource.url}${deploy.name}`}>{deploy.name}</Link> :
deploy.name;
}
render: name => <Link to={`${pathPrefix}${resource.url}${name}`}>{name}</Link>
};
},
successRate: {

View File

@ -26,6 +26,17 @@ const convertLatencyTs = rawTs => {
return _.groupBy(latencies, 'label');
};
export const getPodsByDeployment = pods => {
return _(pods)
.reject(p => _.isEmpty(p.deployment) || p.controlPlane)
.groupBy('deployment')
.map((componentPods, name) => {
return { name: name, added: _.every(componentPods, 'added') };
})
.sortBy('name')
.value();
};
export const processTimeseriesMetrics = (rawTs, targetEntity) => {
let tsbyEntity = _.groupBy(rawTs, "metadata." + targetEntity);
return _.reduce(tsbyEntity, (mem, metrics, entity) => {

View File

@ -0,0 +1,28 @@
import Deployment from '../js/components/Deployment.jsx';
import { expect } from 'chai';
import { mount } from 'enzyme';
import { routerWrap } from "./testHelpers.jsx";
import sinon from 'sinon';
import sinonStubPromise from 'sinon-stub-promise';
sinonStubPromise(sinon);
describe('Deployment', () => {
let component, fetchStub;
beforeEach(() => {
fetchStub = sinon.stub(window, 'fetch');
});
afterEach(() => {
window.fetch.restore();
});
it('renders the spinner before metrics are loaded', () => {
fetchStub.returnsPromise().resolves({
json: () => Promise.resolve({ metrics: [] })
});
component = mount(routerWrap(Deployment));
expect(component.find("ConduitSpinner")).to.have.length(1);
});
});

View File

@ -2,7 +2,7 @@ import Deployments from '../js/components/Deployments.jsx';
import { expect } from 'chai';
import { mount } from 'enzyme';
import podFixtures from './fixtures/pods.json';
import React from 'react';
import { routerWrap } from "./testHelpers.jsx";
import sinon from 'sinon';
import sinonStubPromise from 'sinon-stub-promise';
@ -12,7 +12,7 @@ describe('Deployments', () => {
let component, fetchStub;
function withPromise(fn) {
return component.get(0).serverPromise.then(fn);
return component.find("Deployments").get(0).serverPromise.then(fn);
}
beforeEach(() => {
@ -27,7 +27,7 @@ describe('Deployments', () => {
fetchStub.returnsPromise().resolves({
json: () => Promise.resolve({ metrics: [] })
});
component = mount(<Deployments />);
component = mount(routerWrap(Deployments));
expect(component.find("Deployments")).to.have.length(1);
expect(component.find("ConduitSpinner")).to.have.length(1);
@ -38,7 +38,7 @@ describe('Deployments', () => {
fetchStub.returnsPromise().resolves({
json: () => Promise.resolve({ metrics: [] })
});
component = mount(<Deployments />);
component = mount(routerWrap(Deployments));
return withPromise(() => {
expect(component.find("Deployments")).to.have.length(1);
@ -51,7 +51,7 @@ describe('Deployments', () => {
fetchStub.returnsPromise().resolves({
json: () => Promise.resolve({ metrics: [], pods: podFixtures.pods })
});
component = mount(<Deployments />);
component = mount(routerWrap(Deployments));
return withPromise(() => {
expect(component.find("Deployments")).to.have.length(1);