mirror of https://github.com/linkerd/linkerd2.git
Consolidate api calling from the UI; Start surfacing API errors (#65)
* Adds an ApiHelpers module that wraps our api calls to the server * Adds ability to display error messages from the server
This commit is contained in:
parent
78d7b22e1c
commit
8cf1bdbee3
|
@ -25,7 +25,7 @@
|
|||
border: 2px solid var(--green);
|
||||
}
|
||||
&.health-bad {
|
||||
border: 2px solid #EB5757;
|
||||
border: 2px solid var(--bad-red);
|
||||
}
|
||||
&.health-neutral {
|
||||
border: 2px solid #108ee9;
|
||||
|
@ -44,7 +44,7 @@
|
|||
}
|
||||
&.health-bad {
|
||||
& .ant-progress-bg {
|
||||
background: #EB5757;
|
||||
background: var(--bad-red);
|
||||
}
|
||||
}
|
||||
&.health-neutral {
|
||||
|
|
|
@ -42,7 +42,7 @@ td .status-dot {
|
|||
background-color: var(--green);
|
||||
}
|
||||
&.status-dot-bad {
|
||||
background-color: #EB5757;
|
||||
background-color: var(--bad-red);
|
||||
}
|
||||
&.status-dot-neutral {
|
||||
background-color: #E0E0E0;
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
width: 100%;
|
||||
float: right;
|
||||
min-height: 50vh;
|
||||
/* background-color: #ff6600;*/
|
||||
/*background-image: url(./../img/sidebar-bg.png);*/
|
||||
background-size: auto 100%;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
|
||||
|
||||
:root {
|
||||
--white: #fff;
|
||||
--buttonblue: #4076c4;
|
||||
--messageblue: #2F80ED;
|
||||
--darkblue: #071E3C;
|
||||
--warmgrey: #f6f6f6;
|
||||
--coldgrey: #c9c9c9;
|
||||
--latency-p99: #2F80ED;
|
||||
--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;
|
||||
--font-weight-bold: 700;
|
||||
--font-weight-extra-bold: 900;
|
||||
--base-width: 8px;
|
||||
--white: #fff;
|
||||
--buttonblue: #4076c4;
|
||||
--messageblue: #2F80ED;
|
||||
--darkblue: #071E3C;
|
||||
--warmgrey: #f6f6f6;
|
||||
--coldgrey: #c9c9c9;
|
||||
--latency-p99: #2F80ED;
|
||||
--latency-p95: #2D9CDB;
|
||||
--latency-p50: #56CCF2;
|
||||
--subheader-grey: #828282;
|
||||
--status-gray: #BDBDBD;
|
||||
--green: #27AE60;
|
||||
--bad-red: #EB5757;
|
||||
--font-stack: 'Lato', helvetica, arial, sans-serif;
|
||||
--font-weight-light: 300;
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-bold: 700;
|
||||
--font-weight-extra-bold: 900;
|
||||
--base-width: 8px;
|
||||
}
|
||||
|
||||
.hide {
|
||||
|
@ -148,7 +149,6 @@ tr th.numeric, .numeric {
|
|||
/* fancy borders */
|
||||
.border-container {
|
||||
padding: var(--base-width);
|
||||
/* height: 190px; */
|
||||
background-repeat: space;
|
||||
margin-bottom: calc(var(--base-width)*2);
|
||||
}
|
||||
|
@ -156,7 +156,6 @@ tr th.numeric, .numeric {
|
|||
overflow: hidden;
|
||||
padding: var(--base-width);
|
||||
background-color: white;
|
||||
/* height: 174px; */
|
||||
}
|
||||
.border-container.border-bad {
|
||||
background-image: url(./../img/red_check.png);
|
||||
|
@ -166,4 +165,23 @@ tr th.numeric, .numeric {
|
|||
}
|
||||
.border-container.border-neutral {
|
||||
background-image: url(./../img/grey_check.png);
|
||||
}
|
||||
}
|
||||
|
||||
.error-message-container {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
padding: var(--base-width);
|
||||
margin-bottom: calc(3 * var(--base-width));
|
||||
border-radius: 5px;
|
||||
background: var(--bad-red);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
|
||||
& .dismiss {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import _ from 'lodash';
|
||||
import { ApiHelpers } from './util/ApiHelpers.js';
|
||||
import BarChart from './BarChart.jsx';
|
||||
import ConduitSpinner from "./ConduitSpinner.jsx";
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import HealthPane from './HealthPane.jsx';
|
||||
import { incompleteMeshMessage } from './util/CopyUtils.jsx';
|
||||
import Metric from './Metric.jsx';
|
||||
|
@ -17,6 +19,8 @@ import 'whatwg-fetch';
|
|||
export default class Deployment extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.api = ApiHelpers(this.props.pathPrefix);
|
||||
this.handleApiError = this.handleApiError.bind(this);
|
||||
this.loadFromServer = this.loadFromServer.bind(this);
|
||||
this.state = this.initialState(this.props.location);
|
||||
}
|
||||
|
@ -53,7 +57,8 @@ export default class Deployment extends React.Component {
|
|||
downstreamMetrics: [],
|
||||
downstreamTsByDeploy: {},
|
||||
pendingRequests: false,
|
||||
loaded: false
|
||||
loaded: false,
|
||||
error: ''
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -71,18 +76,18 @@ 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());
|
||||
let podTsFetch = fetch(podTimeseriesUrl).then(r => r.json());
|
||||
let upstreamFetch = fetch(upstreamRollupUrl).then(r => r.json());
|
||||
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());
|
||||
let deployFetch = this.api.fetch(deployMetricsUrl);
|
||||
let podListFetch = this.api.fetchPods();
|
||||
let podRollupFetch = this.api.fetch(podRollupUrl);
|
||||
let podTsFetch = this.api.fetch(podTimeseriesUrl);
|
||||
let upstreamFetch = this.api.fetch(upstreamRollupUrl);
|
||||
let upstreamTsFetch = this.api.fetch(upstreamTimeseriesUrl);
|
||||
let downstreamFetch = this.api.fetch(downstreamRollupUrl);
|
||||
let downstreamTsFetch = this.api.fetch(downstreamTimeseriesUrl);
|
||||
|
||||
Promise.all([deployFetch, podFetch, podTsFetch, upstreamFetch, upstreamTsFetch, downstreamFetch, downstreamTsFetch, podListFetch])
|
||||
// expose serverPromise for testing
|
||||
this.serverPromise = Promise.all([deployFetch, podRollupFetch, 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");
|
||||
|
@ -109,11 +114,17 @@ export default class Deployment extends React.Component {
|
|||
downstreamTsByDeploy: downstreamTsByDeploy,
|
||||
lastUpdated: Date.now(),
|
||||
pendingRequests: false,
|
||||
loaded: true
|
||||
loaded: true,
|
||||
error: ''
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setState({ pendingRequests: false });
|
||||
});
|
||||
}).catch(this.handleApiError);
|
||||
}
|
||||
|
||||
handleApiError(e) {
|
||||
this.setState({
|
||||
pendingRequests: false,
|
||||
error: `Error getting data from server: ${e.message}`
|
||||
});
|
||||
}
|
||||
|
||||
numUpstreams() {
|
||||
|
@ -223,18 +234,19 @@ export default class Deployment extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.loaded) {
|
||||
return <ConduitSpinner />;
|
||||
} else {
|
||||
return (
|
||||
<div className="page-content deployment-detail">
|
||||
<div className="page-header">
|
||||
<div className="subsection-header">Deployment detail</div>
|
||||
{this.renderDeploymentTitle()}
|
||||
return (
|
||||
<div className="page-content deployment-detail">
|
||||
{ !this.state.error ? null : <ErrorBanner message={this.state.error} /> }
|
||||
{ !this.state.loaded ? <ConduitSpinner /> :
|
||||
<div>
|
||||
<div className="page-header">
|
||||
<div className="subsection-header">Deployment detail</div>
|
||||
{this.renderDeploymentTitle()}
|
||||
</div>
|
||||
{this.renderSections()}
|
||||
</div>
|
||||
{this.renderSections()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import _ from 'lodash';
|
||||
import { ApiHelpers } from './util/ApiHelpers.js';
|
||||
import CallToAction from './CallToAction.jsx';
|
||||
import ConduitSpinner from "./ConduitSpinner.jsx";
|
||||
import DeploymentSummary from './DeploymentSummary.jsx';
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import React from 'react';
|
||||
import { rowGutter } from './util/Utils.js';
|
||||
import TabbedMetricsTable from './TabbedMetricsTable.jsx';
|
||||
|
@ -14,8 +16,10 @@ const maxTsToFetch = 15; // Beyond this, stop showing sparklines in table
|
|||
export default class Deployments extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.api = ApiHelpers(this.props.pathPrefix);
|
||||
this.loadFromServer = this.loadFromServer.bind(this);
|
||||
this.loadTimeseriesFromServer = this.loadTimeseriesFromServer.bind(this);
|
||||
this.handleApiError = this.handleApiError.bind(this);
|
||||
|
||||
this.state = {
|
||||
metricsWindow: "10m",
|
||||
|
@ -25,7 +29,8 @@ export default class Deployments extends React.Component {
|
|||
lastUpdated: 0,
|
||||
limitSparklineData: false,
|
||||
pendingRequests: false,
|
||||
loaded: false
|
||||
loaded: false,
|
||||
error: ''
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -56,13 +61,12 @@ export default class Deployments extends React.Component {
|
|||
this.setState({ pendingRequests: true });
|
||||
|
||||
let rollupPath = `${this.props.pathPrefix}/api/metrics?window=${this.state.metricsWindow}`;
|
||||
let podPath = `${this.props.pathPrefix}/api/pods`;
|
||||
let rollupRequest = fetch(rollupPath).then(r => r.json());
|
||||
|
||||
let podRequest = fetch(podPath).then(r => r.json());
|
||||
let rollupRequest = this.api.fetch(rollupPath);
|
||||
let podsRequest = this.api.fetchPods(this.props.pathPrefix);
|
||||
|
||||
// expose serverPromise for testing
|
||||
this.serverPromise = Promise.all([rollupRequest, podRequest])
|
||||
this.serverPromise = Promise.all([rollupRequest, podsRequest])
|
||||
.then(([rollup, p]) => {
|
||||
let poByDeploy = getPodsByDeployment(p.pods);
|
||||
let meshDeploys = processRollupMetrics(rollup.metrics, "targetDeploy");
|
||||
|
@ -70,14 +74,11 @@ export default class Deployments extends React.Component {
|
|||
|
||||
this.loadTimeseriesFromServer(meshDeploys, combinedMetrics);
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({ pendingRequests: false });
|
||||
});
|
||||
.catch(this.handleApiError);
|
||||
}
|
||||
|
||||
loadTimeseriesFromServer(meshDeployMetrics, combinedMetrics) {
|
||||
let limitSparklineData = _.size(meshDeployMetrics) > maxTsToFetch;
|
||||
|
||||
let rollupPath = `${this.props.pathPrefix}/api/metrics?window=${this.state.metricsWindow}`;
|
||||
let timeseriesPath = `${rollupPath}×eries=true`;
|
||||
|
||||
|
@ -85,7 +86,8 @@ export default class Deployments extends React.Component {
|
|||
metrics: combinedMetrics,
|
||||
limitSparklineData: limitSparklineData,
|
||||
loaded: true,
|
||||
pendingRequests: false
|
||||
pendingRequests: false,
|
||||
error: ''
|
||||
};
|
||||
|
||||
if (limitSparklineData) {
|
||||
|
@ -94,7 +96,7 @@ export default class Deployments extends React.Component {
|
|||
|
||||
let tsPromises = _.map(leastHealthyDeployments, dep => {
|
||||
let tsPathForDeploy = `${timeseriesPath}&target_deploy=${dep.name}`;
|
||||
return fetch(tsPathForDeploy).then(r => r.json());
|
||||
return this.api.fetch(tsPathForDeploy);
|
||||
});
|
||||
Promise.all(tsPromises)
|
||||
.then(tsMetrics => {
|
||||
|
@ -107,25 +109,27 @@ export default class Deployments extends React.Component {
|
|||
timeseriesByDeploy: tsByDeploy,
|
||||
lastUpdated: Date.now(),
|
||||
}));
|
||||
}).catch(() => {
|
||||
this.setState({ pendingRequests: false });
|
||||
});
|
||||
}).catch(this.handleApiError);
|
||||
} else {
|
||||
// fetch timeseries for all deploys
|
||||
fetch(timeseriesPath)
|
||||
.then(r => r.json())
|
||||
this.api.fetch(timeseriesPath)
|
||||
.then(ts => {
|
||||
let tsByDeploy = processTimeseriesMetrics(ts.metrics, "targetDeploy");
|
||||
this.setState(_.merge({}, updatedState, {
|
||||
timeseriesByDeploy: tsByDeploy,
|
||||
lastUpdated: Date.now()
|
||||
}));
|
||||
}).catch(() => {
|
||||
this.setState({ pendingRequests: false });
|
||||
});
|
||||
}).catch(this.handleApiError);
|
||||
}
|
||||
}
|
||||
|
||||
handleApiError(e) {
|
||||
this.setState({
|
||||
pendingRequests: false,
|
||||
error: `Error getting data from server: ${e.message}`
|
||||
});
|
||||
}
|
||||
|
||||
getLeastHealthyDeployments(deployMetrics, limit = 3) {
|
||||
return _(deployMetrics)
|
||||
.filter('added')
|
||||
|
@ -169,19 +173,20 @@ export default class Deployments extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.loaded) {
|
||||
return <ConduitSpinner />;
|
||||
} else return (
|
||||
return (
|
||||
<div className="page-content">
|
||||
<div className="page-header">
|
||||
<h1>All deployments</h1>
|
||||
</div>
|
||||
{
|
||||
_.isEmpty(this.state.metrics) ?
|
||||
<CallToAction numDeployments={_.size(this.state.metrics)} /> :
|
||||
this.renderPageContents()
|
||||
{ !this.state.error ? null : <ErrorBanner message={this.state.error} /> }
|
||||
{ !this.state.loaded ? <ConduitSpinner /> :
|
||||
<div>
|
||||
<div className="page-header">
|
||||
<h1>All deployments</h1>
|
||||
</div>
|
||||
{ _.isEmpty(this.state.metrics) ?
|
||||
<CallToAction numDeployments={_.size(this.state.metrics)} /> :
|
||||
this.renderPageContents()
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
export default class Docs extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="page-content">
|
||||
<div className="page-header">
|
||||
<h1>Docs</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { Col, Row } from 'antd';
|
||||
|
||||
const defaultErrorMsg = "An error has occurred";
|
||||
|
||||
export default class ErrorMessage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.hideMessage = this.hideMessage.bind(this);
|
||||
this.state = {
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (!_.isEmpty(newProps.message)) {
|
||||
this.setState({ visible :true });
|
||||
}
|
||||
}
|
||||
|
||||
hideMessage() {
|
||||
this.setState({
|
||||
visible: false
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return !this.state.visible ? null : (
|
||||
<Row gutter={0}>
|
||||
<div className="error-message-container">
|
||||
<Col span={20}>
|
||||
{this.props.message || defaultErrorMsg}
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<div className="dismiss" onClick={this.hideMessage}>Dismiss X</div>
|
||||
</Col>
|
||||
</div>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
import _ from 'lodash';
|
||||
import { ApiHelpers } from './util/ApiHelpers.js';
|
||||
import ConduitSpinner from "./ConduitSpinner.jsx";
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import HealthPane from './HealthPane.jsx';
|
||||
import React from 'react';
|
||||
import StatPane from './StatPane.jsx';
|
||||
|
@ -10,6 +12,7 @@ import 'whatwg-fetch';
|
|||
export default class PodDetail extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.api = ApiHelpers(this.props.pathPrefix);
|
||||
this.loadFromServer = this.loadFromServer.bind(this);
|
||||
this.state = this.initialState(this.props.location);
|
||||
}
|
||||
|
@ -44,7 +47,8 @@ export default class PodDetail extends React.Component {
|
|||
downstreamTsByPod: {},
|
||||
podTs: {},
|
||||
pendingRequests: false,
|
||||
loaded: false
|
||||
loaded: false,
|
||||
error: ''
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -62,11 +66,11 @@ export default class PodDetail extends React.Component {
|
|||
let downstreamRollupUrl = `${metricsUrl}&aggregation=target_pod&source_pod=${this.state.pod}`;
|
||||
let downstreamTimeseriesUrl = `${downstreamRollupUrl}×eries=true`;
|
||||
|
||||
let podFetch = fetch(podMetricsUrl).then(r => r.json());
|
||||
let upstreamFetch = fetch(upstreamRollupUrl).then(r => r.json());
|
||||
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 podFetch = this.api.fetch(podMetricsUrl);
|
||||
let upstreamFetch = this.api.fetch(upstreamRollupUrl);
|
||||
let upstreamTsFetch = this.api.fetch(upstreamTimeseriesUrl);
|
||||
let downstreamFetch = this.api.fetch(downstreamRollupUrl);
|
||||
let downstreamTsFetch = this.api.fetch(downstreamTimeseriesUrl);
|
||||
|
||||
Promise.all([podFetch, upstreamFetch, upstreamTsFetch, downstreamFetch, downstreamTsFetch])
|
||||
.then(([podMetrics, upstreamRollup, upstreamTimeseries, downstreamRollup, downstreamTimeseries]) => {
|
||||
|
@ -86,11 +90,17 @@ export default class PodDetail extends React.Component {
|
|||
upstreamTsByPod: upstreamTsByPod,
|
||||
downstreamMetrics: downstreamMetrics,
|
||||
downstreamTsByPod: downstreamTsByPod,
|
||||
loaded: true
|
||||
loaded: true,
|
||||
error: ''
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setState({ pendingRequests: false });
|
||||
});
|
||||
}).catch(this.handleApiError);
|
||||
}
|
||||
|
||||
handleApiError(e) {
|
||||
this.setState({
|
||||
pendingRequests: false,
|
||||
error: `Error getting data from server: ${e.message}`
|
||||
});
|
||||
}
|
||||
|
||||
renderSections() {
|
||||
|
@ -122,15 +132,18 @@ export default class PodDetail extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.loaded) {
|
||||
return <ConduitSpinner />;
|
||||
} else return (
|
||||
return (
|
||||
<div className="page-content pod-detail">
|
||||
<div className="page-header">
|
||||
<div className="subsection-header">Pod detail</div>
|
||||
<h1>{this.state.pod}</h1>
|
||||
</div>
|
||||
{this.renderSections()}
|
||||
{ !this.state.error ? null : <ErrorBanner message={this.state.error} /> }
|
||||
{ !this.state.loaded ? <ConduitSpinner /> :
|
||||
<div>
|
||||
<div className="page-header">
|
||||
<div className="subsection-header">Pod detail</div>
|
||||
<h1>{this.state.pod}</h1>
|
||||
</div>
|
||||
{this.renderSections()}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import _ from 'lodash';
|
||||
import { ApiHelpers } from './util/ApiHelpers.js';
|
||||
import CallToAction from './CallToAction.jsx';
|
||||
import ConduitSpinner from "./ConduitSpinner.jsx";
|
||||
import DeploymentSummary from './DeploymentSummary.jsx';
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import { incompleteMeshMessage } from './util/CopyUtils.jsx';
|
||||
import Metric from './Metric.jsx';
|
||||
import React from 'react';
|
||||
|
@ -10,7 +12,6 @@ import StatusTable from './StatusTable.jsx';
|
|||
import { Col, Row, Table } from 'antd';
|
||||
import { processRollupMetrics, processTimeseriesMetrics } from './util/MetricUtils.js';
|
||||
import './../../css/service-mesh.css';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
const serviceMeshDetailsColumns = [
|
||||
{
|
||||
|
@ -52,6 +53,8 @@ export default class ServiceMesh extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this.loadFromServer = this.loadFromServer.bind(this);
|
||||
this.handleApiError = this.handleApiError.bind(this);
|
||||
this.api = ApiHelpers(this.props.pathPrefix);
|
||||
|
||||
this.state = {
|
||||
pollingInterval: 2000,
|
||||
|
@ -61,7 +64,8 @@ export default class ServiceMesh extends React.Component {
|
|||
components: [],
|
||||
lastUpdated: 0,
|
||||
pendingRequests: false,
|
||||
loaded: false
|
||||
loaded: false,
|
||||
error: ''
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -82,10 +86,10 @@ export default class ServiceMesh extends React.Component {
|
|||
|
||||
let rollupPath = `${this.props.pathPrefix}/api/metrics?window=${this.state.metricsWindow}&aggregation=mesh`;
|
||||
let timeseriesPath = `${rollupPath}×eries=true`;
|
||||
let podsPath = `${this.props.pathPrefix}/api/pods`;
|
||||
let rollupRequest = fetch(rollupPath).then(r => r.json());
|
||||
let timeseriesRequest = fetch(timeseriesPath).then(r => r.json());
|
||||
let podsRequest = fetch(podsPath).then(r => r.json());
|
||||
|
||||
let rollupRequest = this.api.fetch(rollupPath);
|
||||
let timeseriesRequest = this.api.fetch(timeseriesPath);
|
||||
let podsRequest = this.api.fetchPods(this.props.pathPrefix);
|
||||
|
||||
this.serverPromise = Promise.all([rollupRequest, timeseriesRequest, podsRequest])
|
||||
.then(([metrics, ts, pods]) => {
|
||||
|
@ -101,11 +105,17 @@ export default class ServiceMesh extends React.Component {
|
|||
components: c,
|
||||
lastUpdated: Date.now(),
|
||||
pendingRequests: false,
|
||||
loaded: true
|
||||
loaded: true,
|
||||
error: ''
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setState({ pendingRequests: false });
|
||||
});
|
||||
}).catch(this.handleApiError);
|
||||
}
|
||||
|
||||
handleApiError(e) {
|
||||
this.setState({
|
||||
pendingRequests: false,
|
||||
error: `Error getting data from server: ${e.message}`
|
||||
});
|
||||
}
|
||||
|
||||
addedDeploymentCount() {
|
||||
|
@ -312,16 +322,19 @@ export default class ServiceMesh extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.loaded) {
|
||||
return <ConduitSpinner />;
|
||||
} else return (
|
||||
return (
|
||||
<div className="page-content">
|
||||
<div className="page-header">
|
||||
<h1>Service mesh overview</h1>
|
||||
{this.renderOverview()}
|
||||
{this.renderControlPlane()}
|
||||
{this.renderDataPlane()}
|
||||
</div>
|
||||
{ !this.state.error ? null : <ErrorBanner message={this.state.error} /> }
|
||||
{ !this.state.loaded ? <ConduitSpinner /> :
|
||||
<div>
|
||||
<div className="page-header">
|
||||
<h1>Service mesh overview</h1>
|
||||
</div>
|
||||
{this.renderOverview()}
|
||||
{this.renderControlPlane()}
|
||||
{this.renderDataPlane()}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import 'whatwg-fetch';
|
||||
|
||||
export const ApiHelpers = pathPrefix => {
|
||||
const podsPath = `${pathPrefix}/api/pods`;
|
||||
|
||||
const apiFetch = path => {
|
||||
return fetch(path).then(handleFetchErr).then(r => r.json());
|
||||
};
|
||||
|
||||
const fetchPods = () => {
|
||||
return apiFetch(podsPath);
|
||||
};
|
||||
|
||||
const handleFetchErr = resp => {
|
||||
if (!resp.ok) {
|
||||
throw Error(resp.statusText);
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
|
||||
return {
|
||||
fetch: apiFetch,
|
||||
fetchPods
|
||||
};
|
||||
};
|
|
@ -1,6 +1,5 @@
|
|||
import Deployment from './components/Deployment.jsx';
|
||||
import Deployments from './components/Deployments.jsx';
|
||||
import Docs from './components/Docs.jsx';
|
||||
import enUS from 'antd/lib/locale-provider/en_US'; // configure ant locale globally
|
||||
import NoMatch from './components/NoMatch.jsx';
|
||||
import PodDetail from './components/PodDetail.jsx';
|
||||
|
@ -38,7 +37,6 @@ ReactDOM.render((
|
|||
<Route path={`${pathPrefix}/deployment`} render={props => <Deployment pathPrefix={pathPrefix} location={props.location} />} />
|
||||
<Route path={`${pathPrefix}/pod`} render={props => <PodDetail pathPrefix={pathPrefix} location={props.location} />} />
|
||||
<Route path={`${pathPrefix}/routes`} render={() => <Routes pathPrefix={pathPrefix} />} />
|
||||
<Route path={`${pathPrefix}/docs`} render={() => <Docs pathPrefix={pathPrefix} />} />
|
||||
<Route component={NoMatch} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
|
|
@ -10,6 +10,10 @@ sinonStubPromise(sinon);
|
|||
describe('Deployment', () => {
|
||||
let component, fetchStub;
|
||||
|
||||
function withPromise(fn) {
|
||||
return component.find("Deployment").get(0).serverPromise.then(fn);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fetchStub = sinon.stub(window, 'fetch');
|
||||
});
|
||||
|
@ -20,9 +24,13 @@ describe('Deployment', () => {
|
|||
|
||||
it('renders the spinner before metrics are loaded', () => {
|
||||
fetchStub.returnsPromise().resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ metrics: [] })
|
||||
});
|
||||
component = mount(routerWrap(Deployment));
|
||||
expect(component.find("ConduitSpinner")).to.have.length(1);
|
||||
|
||||
return withPromise(() => {
|
||||
expect(component.find("ConduitSpinner")).to.have.length(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,18 +24,19 @@ describe('Deployments', () => {
|
|||
});
|
||||
|
||||
it('renders the spinner before metrics are loaded', () => {
|
||||
fetchStub.returnsPromise().resolves({
|
||||
json: () => Promise.resolve({ metrics: [] })
|
||||
});
|
||||
fetchStub.returnsPromise().resolves({ ok: true });
|
||||
component = mount(routerWrap(Deployments));
|
||||
|
||||
expect(component.find("Deployments")).to.have.length(1);
|
||||
expect(component.find("ConduitSpinner")).to.have.length(1);
|
||||
expect(component.find("CallToAction")).to.have.length(0);
|
||||
return withPromise(() => {
|
||||
expect(component.find("Deployments")).to.have.length(1);
|
||||
expect(component.find("ConduitSpinner")).to.have.length(1);
|
||||
expect(component.find("CallToAction")).to.have.length(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a call to action if no metrics are received', () => {
|
||||
fetchStub.returnsPromise().resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ metrics: [] })
|
||||
});
|
||||
component = mount(routerWrap(Deployments));
|
||||
|
@ -49,6 +50,7 @@ describe('Deployments', () => {
|
|||
|
||||
it('renders the deployments page if pod data is received', () => {
|
||||
fetchStub.returnsPromise().resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ metrics: [], pods: podFixtures.pods })
|
||||
});
|
||||
component = mount(routerWrap(Deployments));
|
||||
|
|
|
@ -24,25 +24,41 @@ describe('ServiceMesh', () => {
|
|||
window.fetch.restore();
|
||||
});
|
||||
|
||||
it("displays an error if the api call didn't go well", () => {
|
||||
let errorMsg = "Something went wrong!";
|
||||
|
||||
fetchStub.returnsPromise().resolves({
|
||||
ok: false,
|
||||
statusText: errorMsg
|
||||
});
|
||||
component = mount(routerWrap(ServiceMesh));
|
||||
|
||||
return withPromise(() => {
|
||||
expect(component.html()).to.include(errorMsg);
|
||||
});
|
||||
});
|
||||
|
||||
it("displays message for no deployments detetcted", () => {
|
||||
fetchStub.returnsPromise().resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ pods: []})
|
||||
});
|
||||
component = mount(routerWrap(ServiceMesh));
|
||||
|
||||
return withPromise(() => {
|
||||
expect(component.html()).includes("No deployments detected.");
|
||||
expect(component.html()).to.include("No deployments detected.");
|
||||
});
|
||||
});
|
||||
|
||||
it("displays message for more than one deployment added to servicemesh", () => {
|
||||
fetchStub.returnsPromise().resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ pods: podFixtures.pods})
|
||||
});
|
||||
component = mount(routerWrap(ServiceMesh));
|
||||
|
||||
return withPromise(() => {
|
||||
expect(component.html()).includes("deployments have not been added to the service mesh.");
|
||||
expect(component.html()).to.include("deployments have not been added to the service mesh.");
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -51,12 +67,13 @@ describe('ServiceMesh', () => {
|
|||
_.set(addedPods[0], "added", true);
|
||||
|
||||
fetchStub.returnsPromise().resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ pods: addedPods})
|
||||
});
|
||||
component = mount(routerWrap(ServiceMesh));
|
||||
|
||||
return withPromise(() => {
|
||||
expect(component.html()).includes("1 deployment has not been added to the service mesh.");
|
||||
expect(component.html()).to.include("1 deployment has not been added to the service mesh.");
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -67,12 +84,13 @@ describe('ServiceMesh', () => {
|
|||
});
|
||||
|
||||
fetchStub.returnsPromise().resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ pods: addedPods})
|
||||
});
|
||||
component = mount(routerWrap(ServiceMesh));
|
||||
|
||||
return withPromise(() => {
|
||||
expect(component.html()).includes("All deployments have been added to the service mesh.");
|
||||
expect(component.html()).to.include("All deployments have been added to the service mesh.");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@ module.exports = {
|
|||
publicPath: 'dist/',
|
||||
filename: 'index_bundle.js'
|
||||
},
|
||||
// devtool: 'inline-cheap-source-map', // uncomment for nicer logging, makes dev slower
|
||||
// devtool: 'source-map', // uncomment for nicer logging, makes dev slower
|
||||
externals: {
|
||||
cheerio: 'window',
|
||||
'react/addons': 'react',
|
||||
|
|
|
@ -95,7 +95,6 @@ func NewServer(addr, templateDir, staticDir, uuid, webpackDevServer string, relo
|
|||
server.router.GET("/deployments", handler.handleIndex)
|
||||
server.router.GET("/servicemesh", handler.handleIndex)
|
||||
server.router.GET("/routes", handler.handleIndex)
|
||||
server.router.GET("/docs", handler.handleIndex)
|
||||
server.router.ServeFiles(
|
||||
"/dist/*filepath", // add catch-all parameter to match all files in dir
|
||||
filesonly.FileSystem(server.staticDir))
|
||||
|
|
Loading…
Reference in New Issue