mirror of https://github.com/linkerd/linkerd2.git
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:
parent
3591936de3
commit
42d942c0bf
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -39,7 +39,7 @@ td .status-dot {
|
|||
}
|
||||
|
||||
&.status-dot-good {
|
||||
background-color: #27AE60;
|
||||
background-color: var(--green);
|
||||
}
|
||||
&.status-dot-bad {
|
||||
background-color: #EB5757;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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}×eries=true`;
|
||||
let downstreamRollupUrl = `${metricsUrl}&aggregation=target_deploy&source_deploy=${this.state.deploy}`;
|
||||
let downstreamTimeseriesUrl = `${downstreamRollupUrl}×eries=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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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">« {_.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 »</div>
|
||||
<div className={`adjacent-health ${stats.outbound.health}`}>
|
||||
<TrafficIndicator />
|
||||
<TrafficIndicator healthStat={stats.outbound.health} />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -211,6 +211,7 @@ export default class ServiceMesh extends React.Component {
|
|||
|
||||
<StatusTable
|
||||
data={this.state.components}
|
||||
shouldLink={false}
|
||||
statusColumnTitle="Pod Status" />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue