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:
Risha Mars 2017-12-21 15:18:35 -08:00 committed by GitHub
parent 78d7b22e1c
commit 8cf1bdbee3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 288 additions and 151 deletions

View File

@ -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 {

View File

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

View File

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

View File

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

View File

@ -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}&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());
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>
);
}
}

View File

@ -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}&timeseries=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>);
}
}

View File

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

View File

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

View File

@ -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}&timeseries=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>
);
}

View File

@ -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}&timeseries=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>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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.");
});
});
});
});

View File

@ -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',

View File

@ -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))