mirror of https://github.com/linkerd/linkerd2.git
Add ability to change the time window for metrics fetching throughout the app (#237)
* Control metricsWindow from root of app - Add buttons [currently hidden] on metrics pages to control window of metrics requests - Consolidate metricsWindow usage (stop passing it around) - Add a ConduitLink component so we can stop passing around pathPrefix - Add tests for ApiHelpers * Hide the time window buttons; fix bug in absolute links * Add a note explaining why metricWindow buttons are disabled * Convert ConduitLink in to a component that wraps another
This commit is contained in:
parent
b691c2e25b
commit
9887f10749
|
@ -70,6 +70,10 @@ h2, h3, h4, h5, h6 {
|
|||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.time-window-btns {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.subsection-header {
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
|
|
|
@ -4,13 +4,13 @@ import ConduitSpinner from "./ConduitSpinner.jsx";
|
|||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import { incompleteMeshMessage } from './util/CopyUtils.jsx';
|
||||
import Metric from './Metric.jsx';
|
||||
import PageHeader from './PageHeader.jsx';
|
||||
import React from 'react';
|
||||
import ResourceHealthOverview from './ResourceHealthOverview.jsx';
|
||||
import ResourceMetricsOverview from './ResourceMetricsOverview.jsx';
|
||||
import { rowGutter } from './util/Utils.js';
|
||||
import TabbedMetricsTable from './TabbedMetricsTable.jsx';
|
||||
import UpstreamDownstream from './UpstreamDownstream.jsx';
|
||||
import { ApiHelpers, urlsForResource } from './util/ApiHelpers.js';
|
||||
import { Col, Row } from 'antd';
|
||||
import { emptyMetric, getPodsByDeployment, processRollupMetrics, processTimeseriesMetrics } from './util/MetricUtils.js';
|
||||
import './../../css/deployment.css';
|
||||
|
@ -19,7 +19,7 @@ import 'whatwg-fetch';
|
|||
export default class DeploymentDetail extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.api = ApiHelpers(this.props.pathPrefix);
|
||||
this.api = this.props.api;
|
||||
this.handleApiError = this.handleApiError.bind(this);
|
||||
this.loadFromServer = this.loadFromServer.bind(this);
|
||||
this.state = this.initialState(this.props.location);
|
||||
|
@ -47,7 +47,6 @@ export default class DeploymentDetail extends React.Component {
|
|||
return {
|
||||
lastUpdated: 0,
|
||||
pollingInterval: 10000,
|
||||
metricsWindow: "10m",
|
||||
deploy: deployment,
|
||||
metrics: [],
|
||||
pods: [],
|
||||
|
@ -66,7 +65,7 @@ export default class DeploymentDetail extends React.Component {
|
|||
}
|
||||
this.setState({ pendingRequests: true });
|
||||
|
||||
let urls = urlsForResource(this.props.pathPrefix, this.state.metricsWindow);
|
||||
let urls = this.api.urlsForResource;
|
||||
|
||||
let deployMetricsUrl = urls["deployment"].url(this.state.deploy).ts;
|
||||
let podRollupUrl = urls["pod"].url(this.state.deploy).rollup;
|
||||
|
@ -74,12 +73,12 @@ export default class DeploymentDetail extends React.Component {
|
|||
let downstreamRollupUrl = urls["downstream_deployment"].url(this.state.deploy).rollup;
|
||||
let pathMetricsUrl = urls["path"].url(this.state.deploy).rollup;
|
||||
|
||||
let deployFetch = this.api.fetch(deployMetricsUrl);
|
||||
let deployFetch = this.api.fetchMetrics(deployMetricsUrl);
|
||||
let podListFetch = this.api.fetchPods();
|
||||
let podRollupFetch = this.api.fetch(podRollupUrl);
|
||||
let upstreamFetch = this.api.fetch(upstreamRollupUrl);
|
||||
let downstreamFetch = this.api.fetch(downstreamRollupUrl);
|
||||
let pathsFetch = this.api.fetch(pathMetricsUrl);
|
||||
let podRollupFetch = this.api.fetchMetrics(podRollupUrl);
|
||||
let upstreamFetch = this.api.fetchMetrics(upstreamRollupUrl);
|
||||
let downstreamFetch = this.api.fetchMetrics(downstreamRollupUrl);
|
||||
let pathsFetch = this.api.fetchMetrics(pathMetricsUrl);
|
||||
|
||||
// expose serverPromise for testing
|
||||
this.serverPromise = Promise.all([deployFetch, podRollupFetch, upstreamFetch, downstreamFetch, podListFetch, pathsFetch])
|
||||
|
@ -142,7 +141,8 @@ export default class DeploymentDetail extends React.Component {
|
|||
<ResourceMetricsOverview
|
||||
key="stat-pane"
|
||||
lastUpdated={this.state.lastUpdated}
|
||||
timeseries={this.state.deployTs} />,
|
||||
timeseries={this.state.deployTs}
|
||||
window={this.api.getMetricsWindow()} />,
|
||||
this.renderMidsection(),
|
||||
<UpstreamDownstream
|
||||
key="deploy-upstream-downstream"
|
||||
|
@ -151,8 +151,7 @@ export default class DeploymentDetail extends React.Component {
|
|||
lastUpdated={this.state.lastUpdated}
|
||||
upstreamMetrics={this.state.upstreamMetrics}
|
||||
downstreamMetrics={this.state.downstreamMetrics}
|
||||
metricsWindow={this.state.metricsWindow}
|
||||
pathPrefix={this.props.pathPrefix} />,
|
||||
api={this.api} />,
|
||||
this.renderPaths()
|
||||
];
|
||||
}
|
||||
|
@ -188,8 +187,7 @@ export default class DeploymentDetail extends React.Component {
|
|||
resourceName={this.state.deploy}
|
||||
metrics={podTableData}
|
||||
lastUpdated={this.state.lastUpdated}
|
||||
pathPrefix={this.props.pathPrefix}
|
||||
metricsWindow={this.state.metricsWindow} />
|
||||
api={this.api} />
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
|
@ -220,8 +218,7 @@ export default class DeploymentDetail extends React.Component {
|
|||
metrics={this.state.pathMetrics}
|
||||
hideSparklines={true}
|
||||
lastUpdated={this.props.lastUpdated}
|
||||
metricsWindow={this.props.metricsWindow}
|
||||
pathPrefix={this.props.pathPrefix} />
|
||||
api={this.api} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -231,12 +228,7 @@ export default class DeploymentDetail extends React.Component {
|
|||
<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">
|
||||
{incompleteMeshMessage(this.state.deploy)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="status-badge unadded">UNADDED</p>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
|
@ -249,10 +241,12 @@ export default class DeploymentDetail extends React.Component {
|
|||
{ !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>
|
||||
<PageHeader
|
||||
subHeaderTitle="Deployment detail"
|
||||
subHeader={this.renderDeploymentTitle()}
|
||||
subMessage={!this.state.added ? incompleteMeshMessage(this.state.deploy) : null}
|
||||
api={this.api} />
|
||||
|
||||
{this.renderSections()}
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import LineGraph from './LineGraph.jsx';
|
||||
import { Link } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import { metricToFormatter, toClassName } from './util/Utils.js';
|
||||
|
||||
|
@ -8,7 +7,9 @@ export default class DeploymentSummary extends React.Component {
|
|||
if (this.props.noLink) {
|
||||
return this.props.data.name;
|
||||
} else {
|
||||
return <Link to={`${this.props.pathPrefix}/deployment?deploy=${this.props.data.name}`}>{this.props.data.name}</Link>;
|
||||
return (<this.props.api.ConduitLink
|
||||
to={`/deployment?deploy=${this.props.data.name}`}
|
||||
name={this.props.data.name} />);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,7 +18,7 @@ export default class DeploymentSummary extends React.Component {
|
|||
<div className={`border-container border-neutral`}>
|
||||
<div className="border-container-content">
|
||||
<div className="summary-title">{this.title()}</div>
|
||||
<div className="summary-info">Last 10 minutes RPS</div>
|
||||
<div className="summary-info">RPS (last {this.props.api.getMetricsWindowDisplayText()})</div>
|
||||
|
||||
<LineGraph
|
||||
data={this.props.requestTs}
|
||||
|
|
|
@ -3,10 +3,10 @@ import CallToAction from './CallToAction.jsx';
|
|||
import ConduitSpinner from "./ConduitSpinner.jsx";
|
||||
import DeploymentSummary from './DeploymentSummary.jsx';
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import PageHeader from './PageHeader.jsx';
|
||||
import React from 'react';
|
||||
import ScatterPlot from './ScatterPlot.jsx';
|
||||
import TabbedMetricsTable from './TabbedMetricsTable.jsx';
|
||||
import { ApiHelpers, urlsForResource } from './util/ApiHelpers.js';
|
||||
import { Col, Row } from 'antd';
|
||||
import { emptyMetric, getPodsByDeployment, processRollupMetrics, processTimeseriesMetrics } from './util/MetricUtils.js';
|
||||
import { metricToFormatter, rowGutter } from './util/Utils.js';
|
||||
|
@ -26,13 +26,12 @@ let nodeStats = (description, node) => (
|
|||
export default class DeploymentsList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.api = ApiHelpers(this.props.pathPrefix);
|
||||
this.api = this.props.api;
|
||||
this.handleApiError = this.handleApiError.bind(this);
|
||||
this.loadFromServer = this.loadFromServer.bind(this);
|
||||
this.loadTimeseriesFromServer = this.loadTimeseriesFromServer.bind(this);
|
||||
|
||||
this.state = {
|
||||
metricsWindow: "10m",
|
||||
pollingInterval: 10000, // TODO: poll based on metricsWindow size
|
||||
metrics: [],
|
||||
timeseriesByDeploy: {},
|
||||
|
@ -70,9 +69,7 @@ export default class DeploymentsList extends React.Component {
|
|||
}
|
||||
this.setState({ pendingRequests: true });
|
||||
|
||||
let rollupPath = `${this.props.pathPrefix}/api/metrics?window=${this.state.metricsWindow}`;
|
||||
|
||||
let rollupRequest = this.api.fetch(rollupPath);
|
||||
let rollupRequest = this.api.fetchMetrics(this.api.urlsForResource["deployment"].url().rollup);
|
||||
let podsRequest = this.api.fetchPods();
|
||||
|
||||
// expose serverPromise for testing
|
||||
|
@ -91,12 +88,12 @@ export default class DeploymentsList extends React.Component {
|
|||
// fetch only the timeseries for the 3 deployments we display at the top of the page
|
||||
let limitSparklineData = _.size(meshDeployMetrics) > maxTsToFetch;
|
||||
|
||||
let resourceInfo = urlsForResource(this.props.pathPrefix, this.state.metricsWindow)["deployment"];
|
||||
let resourceInfo = this.api.urlsForResource["deployment"];
|
||||
let leastHealthyDeployments = this.getLeastHealthyDeployments(meshDeployMetrics);
|
||||
|
||||
let tsPromises = _.map(leastHealthyDeployments, dep => {
|
||||
let tsPathForDeploy = resourceInfo.url(dep.name).ts;
|
||||
return this.api.fetch(tsPathForDeploy);
|
||||
return this.api.fetchMetrics(tsPathForDeploy);
|
||||
});
|
||||
|
||||
Promise.all(tsPromises)
|
||||
|
@ -147,8 +144,8 @@ export default class DeploymentsList extends React.Component {
|
|||
key={deployment.name}
|
||||
lastUpdated={this.state.lastUpdated}
|
||||
data={deployment}
|
||||
requestTs={_.get(this.state.timeseriesByDeploy, [deployment.name, "REQUEST_RATE"], [])}
|
||||
pathPrefix={this.props.pathPrefix} />
|
||||
api = {this.api}
|
||||
requestTs={_.get(this.state.timeseriesByDeploy, [deployment.name, "REQUEST_RATE"], [])} />
|
||||
</Col>);
|
||||
})
|
||||
}
|
||||
|
@ -162,8 +159,7 @@ export default class DeploymentsList extends React.Component {
|
|||
lastUpdated={this.state.lastUpdated}
|
||||
metrics={this.state.metrics}
|
||||
hideSparklines={this.state.limitSparklineData}
|
||||
metricsWindow={this.state.metricsWindow}
|
||||
pathPrefix={this.props.pathPrefix} />
|
||||
api={this.api} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -213,9 +209,7 @@ export default class DeploymentsList extends React.Component {
|
|||
{ !this.state.error ? null : <ErrorBanner message={this.state.error} /> }
|
||||
{ !this.state.loaded ? <ConduitSpinner /> :
|
||||
<div>
|
||||
<div className="page-header">
|
||||
<h1>Deployments</h1>
|
||||
</div>
|
||||
<PageHeader header="Deployments" api={this.api} />
|
||||
{ _.isEmpty(this.state.metrics) ?
|
||||
<CallToAction numDeployments={_.size(this.state.metrics)} /> :
|
||||
this.renderPageContents()
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { Col, Radio, Row } from 'antd';
|
||||
|
||||
export default class PageHeader extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onTimeWindowClick = this.onTimeWindowClick.bind(this);
|
||||
this.state = {
|
||||
selectedWindow: this.props.api.getMetricsWindow()
|
||||
};
|
||||
}
|
||||
|
||||
onTimeWindowClick(e) {
|
||||
let window = e.target.value;
|
||||
this.props.api.setMetricsWindow(window);
|
||||
this.setState({selectedWindow: window});
|
||||
}
|
||||
|
||||
// don't use time window changing until the results of Telemetry Scalability are in
|
||||
// https://github.com/runconduit/conduit/milestone/4
|
||||
renderMetricWindowButtons() {
|
||||
if (this.props.hideButtons) {
|
||||
return null;
|
||||
} else {
|
||||
return (<Col span={6}>
|
||||
<Radio.Group
|
||||
className="time-window-btns"
|
||||
value={this.state.selectedWindow}
|
||||
onChange={this.onTimeWindowClick} >
|
||||
{
|
||||
_.map(this.props.api.getValidMetricsWindows(), (w, i) => {
|
||||
return <Radio.Button key={`metrics-window-btn-${i}`} value={w}>{w}</Radio.Button>;
|
||||
})
|
||||
}
|
||||
</Radio.Group>
|
||||
</Col>);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="page-header">
|
||||
<Row>
|
||||
<Col span={18}>
|
||||
{!this.props.header ? null : <h1>{this.props.header}</h1>}
|
||||
{!this.props.subHeaderTitle ? null : <div className="subsection-header">{this.props.subHeaderTitle}</div>}
|
||||
{!this.props.subHeader ? null : <h1>{this.props.subHeader}</h1>}
|
||||
</Col>
|
||||
{/* {this.renderMetricWindowButtons()} */}
|
||||
</Row>
|
||||
|
||||
{!this.props.subMessage ? null : <div>{this.props.subMessage}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,15 +1,15 @@
|
|||
import ConduitSpinner from "./ConduitSpinner.jsx";
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import PageHeader from './PageHeader.jsx';
|
||||
import { processRollupMetrics } from './util/MetricUtils.js';
|
||||
import React from 'react';
|
||||
import TabbedMetricsTable from './TabbedMetricsTable.jsx';
|
||||
import { ApiHelpers, urlsForResource } from './util/ApiHelpers.js';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
export default class Paths extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.api = ApiHelpers(this.props.pathPrefix);
|
||||
this.api = this.props.api;
|
||||
this.handleApiError = this.handleApiError.bind(this);
|
||||
this.loadFromServer = this.loadFromServer.bind(this);
|
||||
this.state = this.initialState();
|
||||
|
@ -28,7 +28,6 @@ export default class Paths extends React.Component {
|
|||
return {
|
||||
lastUpdated: 0,
|
||||
pollingInterval: 10000,
|
||||
metricsWindow: "1m",
|
||||
metrics: [],
|
||||
pendingRequests: false,
|
||||
loaded: false,
|
||||
|
@ -42,9 +41,9 @@ export default class Paths extends React.Component {
|
|||
}
|
||||
this.setState({ pendingRequests: true });
|
||||
|
||||
let urls = urlsForResource(this.props.pathPrefix, this.state.metricsWindow);
|
||||
let urls = this.api.urlsForResource;
|
||||
|
||||
this.api.fetch(urls["path"].url().rollup).then(r => {
|
||||
this.api.fetchMetrics(urls["path"].url().rollup).then(r => {
|
||||
let metrics = processRollupMetrics(r.metrics, "path");
|
||||
|
||||
this.setState({
|
||||
|
@ -70,16 +69,13 @@ export default class Paths extends React.Component {
|
|||
{ !this.state.error ? null : <ErrorBanner message={this.state.error} /> }
|
||||
{ !this.state.loaded ? <ConduitSpinner /> :
|
||||
<div>
|
||||
<div className="page-header">
|
||||
<h1>Paths</h1>
|
||||
</div>
|
||||
<PageHeader header="Paths" api={this.api} />
|
||||
|
||||
<TabbedMetricsTable
|
||||
resource="path"
|
||||
metrics={this.state.metrics}
|
||||
lastUpdated={this.state.lastUpdated}
|
||||
pathPrefix={this.props.pathPrefix}
|
||||
metricsWindow={this.state.metricsWindow}
|
||||
api={this.api}
|
||||
sortable={true}
|
||||
hideSparklines={true} />
|
||||
</div>
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import _ from 'lodash';
|
||||
import ConduitSpinner from "./ConduitSpinner.jsx";
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import PageHeader from './PageHeader.jsx';
|
||||
import React from 'react';
|
||||
import ResourceHealthOverview from './ResourceHealthOverview.jsx';
|
||||
import ResourceMetricsOverview from './ResourceMetricsOverview.jsx';
|
||||
import UpstreamDownstream from './UpstreamDownstream.jsx';
|
||||
import { ApiHelpers, urlsForResource } from './util/ApiHelpers.js';
|
||||
import { processRollupMetrics, processTimeseriesMetrics } from './util/MetricUtils.js';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
export default class PodDetail extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.api = ApiHelpers(this.props.pathPrefix);
|
||||
this.api = this.props.api;
|
||||
this.handleApiError = this.handleApiError.bind(this);
|
||||
this.loadFromServer = this.loadFromServer.bind(this);
|
||||
this.state = this.initialState(this.props.location);
|
||||
|
@ -40,7 +40,6 @@ export default class PodDetail extends React.Component {
|
|||
return {
|
||||
lastUpdated: 0,
|
||||
pollingInterval: 10000,
|
||||
metricsWindow: "10m",
|
||||
pod: pod,
|
||||
upstreamMetrics: [],
|
||||
downstreamMetrics: [],
|
||||
|
@ -57,16 +56,16 @@ export default class PodDetail extends React.Component {
|
|||
}
|
||||
this.setState({ pendingRequests: true });
|
||||
|
||||
let urls = urlsForResource(this.props.pathPrefix, this.state.metricsWindow);
|
||||
let urls = this.api.urlsForResource;
|
||||
|
||||
let metricsUrl = urls["deployment"].url().rollup;
|
||||
let podMetricsUrl = `${metricsUrl}×eries=true&target_pod=${this.state.pod}`;
|
||||
let upstreamRollupUrl = urls["upstream_pod"].url(this.state.pod).rollup;
|
||||
let downstreamRollupUrl = urls["downstream_pod"].url(this.state.pod).rollup;
|
||||
|
||||
let podFetch = this.api.fetch(podMetricsUrl);
|
||||
let upstreamFetch = this.api.fetch(upstreamRollupUrl);
|
||||
let downstreamFetch = this.api.fetch(downstreamRollupUrl);
|
||||
let podFetch = this.api.fetchMetrics(podMetricsUrl);
|
||||
let upstreamFetch = this.api.fetchMetrics(upstreamRollupUrl);
|
||||
let downstreamFetch = this.api.fetchMetrics(downstreamRollupUrl);
|
||||
|
||||
Promise.all([podFetch, upstreamFetch, downstreamFetch])
|
||||
.then(([podMetrics, upstreamRollup, downstreamRollup]) => {
|
||||
|
@ -110,7 +109,8 @@ export default class PodDetail extends React.Component {
|
|||
<ResourceMetricsOverview
|
||||
key="pod-stat-pane"
|
||||
lastUpdated={this.state.lastUpdated}
|
||||
timeseries={this.state.podTs} />,
|
||||
timeseries={this.state.podTs}
|
||||
window={this.api.getMetricsWindow()} />,
|
||||
<UpstreamDownstream
|
||||
key="pod-upstream-downstream"
|
||||
resourceType="pod"
|
||||
|
@ -118,8 +118,7 @@ export default class PodDetail extends React.Component {
|
|||
lastUpdated={this.state.lastUpdated}
|
||||
upstreamMetrics={this.state.upstreamMetrics}
|
||||
downstreamMetrics={this.state.downstreamMetrics}
|
||||
metricsWindow={this.state.metricsWindow}
|
||||
pathPrefix={this.props.pathPrefix} />
|
||||
api={this.api} />
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -129,10 +128,10 @@ export default class PodDetail extends React.Component {
|
|||
{ !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>
|
||||
<PageHeader
|
||||
subHeaderTitle="Pod detail"
|
||||
subHeader={this.state.pod}
|
||||
api={this.api} />
|
||||
{this.renderSections()}
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -15,12 +15,14 @@ export default class ResourceMetricsOverview extends React.Component {
|
|||
name="Request rate"
|
||||
metric="REQUEST_RATE"
|
||||
lastUpdated={this.props.lastUpdated}
|
||||
window={this.props.window}
|
||||
timeseries={_.get(this.props.timeseries, "REQUEST_RATE", [])} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<ResourceOverviewMetric
|
||||
name="Success rate"
|
||||
metric="SUCCESS_RATE"
|
||||
window={this.props.window}
|
||||
lastUpdated={this.props.lastUpdated}
|
||||
timeseries={_.get(this.props.timeseries, "SUCCESS_RATE", [])} />
|
||||
</Col>
|
||||
|
|
|
@ -3,7 +3,7 @@ import LineGraph from './LineGraph.jsx';
|
|||
import React from 'react';
|
||||
import { metricToFormatter, toClassName } from './util/Utils.js';
|
||||
|
||||
export default class EntityOverviewMetric extends React.Component {
|
||||
export default class ResourceOverviewMetric extends React.Component {
|
||||
render() {
|
||||
let lastDatapoint = _.last(this.props.timeseries) || {};
|
||||
let metric = _.get(lastDatapoint, "value");
|
||||
|
@ -15,7 +15,7 @@ export default class EntityOverviewMetric extends React.Component {
|
|||
<div className="summary-container clearfix">
|
||||
<div className="metric-info">
|
||||
<div className="summary-title">{this.props.name}</div>
|
||||
<div className="summary-info">Last 10 minutes performance</div>
|
||||
<div className="summary-info">last {this.props.window}</div>
|
||||
</div>
|
||||
<div className="metric-value">{displayMetric}</div>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
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 PageHeader from './PageHeader.jsx';
|
||||
import React from 'react';
|
||||
import { rowGutter } from './util/Utils.js';
|
||||
import StatusTable from './StatusTable.jsx';
|
||||
|
@ -54,11 +54,10 @@ export default class ServiceMesh extends React.Component {
|
|||
super(props);
|
||||
this.loadFromServer = this.loadFromServer.bind(this);
|
||||
this.handleApiError = this.handleApiError.bind(this);
|
||||
this.api = ApiHelpers(this.props.pathPrefix);
|
||||
this.api = this.props.api;
|
||||
|
||||
this.state = {
|
||||
pollingInterval: 2000,
|
||||
metricsWindow: "10m",
|
||||
metrics: [],
|
||||
deploys: [],
|
||||
components: [],
|
||||
|
@ -84,11 +83,11 @@ export default class ServiceMesh extends React.Component {
|
|||
}
|
||||
this.setState({ pendingRequests: true });
|
||||
|
||||
let rollupPath = `${this.props.pathPrefix}/api/metrics?window=${this.state.metricsWindow}&aggregation=mesh`;
|
||||
let rollupPath = `/api/metrics?aggregation=mesh`;
|
||||
let timeseriesPath = `${rollupPath}×eries=true`;
|
||||
|
||||
let rollupRequest = this.api.fetch(rollupPath);
|
||||
let timeseriesRequest = this.api.fetch(timeseriesPath);
|
||||
let rollupRequest = this.api.fetchMetrics(rollupPath);
|
||||
let timeseriesRequest = this.api.fetchMetrics(timeseriesPath);
|
||||
let podsRequest = this.api.fetchPods();
|
||||
|
||||
this.serverPromise = Promise.all([rollupRequest, timeseriesRequest, podsRequest])
|
||||
|
@ -199,6 +198,7 @@ export default class ServiceMesh extends React.Component {
|
|||
data.name = componentNames[meshComponent];
|
||||
return (<Col span={8} key={`col-${data.id}`}>
|
||||
<DeploymentSummary
|
||||
api={this.api}
|
||||
key={data.id}
|
||||
lastUpdated={this.state.lastUpdated}
|
||||
data={data}
|
||||
|
@ -222,8 +222,9 @@ export default class ServiceMesh extends React.Component {
|
|||
|
||||
<StatusTable
|
||||
data={this.state.components}
|
||||
statusColumnTitle="Pod Status"
|
||||
shouldLink={false}
|
||||
statusColumnTitle="Pod Status" />
|
||||
api={this.api} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -241,7 +242,7 @@ export default class ServiceMesh extends React.Component {
|
|||
data={this.state.deploys}
|
||||
statusColumnTitle="Proxy Status"
|
||||
shouldLink={true}
|
||||
pathPrefix={this.props.pathPrefix} />
|
||||
api={this.api} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -327,9 +328,10 @@ export default class ServiceMesh extends React.Component {
|
|||
{ !this.state.error ? null : <ErrorBanner message={this.state.error} /> }
|
||||
{ !this.state.loaded ? <ConduitSpinner /> :
|
||||
<div>
|
||||
<div className="page-header">
|
||||
<h1>Service mesh overview</h1>
|
||||
</div>
|
||||
<PageHeader
|
||||
header="Service mesh overview"
|
||||
hideButtons={this.proxyCount() === 0}
|
||||
api={this.api} />
|
||||
{this.renderOverview()}
|
||||
{this.renderControlPlane()}
|
||||
{this.renderDataPlane()}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import _ from 'lodash';
|
||||
import { ApiHelpers } from './util/ApiHelpers.js';
|
||||
import { getPodsByDeployment } from './util/MetricUtils.js';
|
||||
import { Link } from 'react-router-dom';
|
||||
import logo from './../../img/reversed_logo.png';
|
||||
import React from 'react';
|
||||
import Version from './Version.jsx';
|
||||
|
@ -13,7 +11,7 @@ const searchBarWidth = 240;
|
|||
export default class Sidebar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.api = ApiHelpers(this.props.pathPrefix);
|
||||
this.api = this.props.api;
|
||||
this.filterDeployments = this.filterDeployments.bind(this);
|
||||
this.onAutocompleteSelect = this.onAutocompleteSelect.bind(this);
|
||||
this.loadFromServer();
|
||||
|
@ -60,12 +58,12 @@ export default class Sidebar extends React.Component {
|
|||
|
||||
render() {
|
||||
let normalizedPath = this.props.location.pathname.replace(this.props.pathPrefix, "");
|
||||
|
||||
let ConduitLink = this.api.ConduitLink;
|
||||
return (
|
||||
<div className="sidebar">
|
||||
<div className="list-container">
|
||||
<div className="sidebar-headers">
|
||||
<Link to={`${this.props.pathPrefix}/servicemesh`}><img src={logo} /></Link>
|
||||
<ConduitLink to="/servicemesh"><img src={logo} /></ConduitLink>
|
||||
</div>
|
||||
|
||||
<AutoComplete
|
||||
|
@ -79,16 +77,16 @@ export default class Sidebar extends React.Component {
|
|||
|
||||
<Menu className="sidebar-menu" theme="dark" selectedKeys={[normalizedPath]}>
|
||||
<Menu.Item className="sidebar-menu-item" key="/servicemesh">
|
||||
<Link to={`${this.props.pathPrefix}/servicemesh`}>Service mesh</Link>
|
||||
<ConduitLink to="/servicemesh">Service mesh</ConduitLink>
|
||||
</Menu.Item>
|
||||
<Menu.Item className="sidebar-menu-item" key="/deployments">
|
||||
<Link to={`${this.props.pathPrefix}/deployments`}>Deployments</Link>
|
||||
<ConduitLink to="/deployments">Deployments</ConduitLink>
|
||||
</Menu.Item>
|
||||
<Menu.Item className="sidebar-menu-item" key="/routes">
|
||||
<Link to={`${this.props.pathPrefix}/routes`}>Routes</Link>
|
||||
<ConduitLink to="/routes">Routes</ConduitLink>
|
||||
</Menu.Item>
|
||||
<Menu.Item className="sidebar-menu-item" key="/docs">
|
||||
<Link to="https://conduit.io/docs/" target="_blank">Documentation</Link>
|
||||
<ConduitLink to="https://conduit.io/docs/" absolute="true">Documentation</ConduitLink>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import _ from 'lodash';
|
||||
import { Link } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import { Table, Tooltip } from 'antd';
|
||||
|
||||
|
@ -37,12 +36,12 @@ const StatusDot = ({status, multilineDots, columnName}) => (
|
|||
);
|
||||
|
||||
const columns = {
|
||||
resourceName: (shouldLink, pathPrefix) => {
|
||||
resourceName: (shouldLink, ConduitLink) => {
|
||||
return {
|
||||
title: "Deployment",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
render: name => shouldLink ? <Link to={`${pathPrefix}/deployment?deploy=${name}`}>{name}</Link> : name
|
||||
render: name => shouldLink ? <ConduitLink to={`/deployment?deploy=${name}`}>{name}</ConduitLink> : name
|
||||
};
|
||||
},
|
||||
pods: {
|
||||
|
@ -85,7 +84,7 @@ export default class StatusTable extends React.Component {
|
|||
|
||||
render() {
|
||||
let tableCols = [
|
||||
columns.resourceName(this.props.shouldLink, this.props.pathPrefix),
|
||||
columns.resourceName(this.props.shouldLink, this.props.api.ConduitLink),
|
||||
columns.pods,
|
||||
columns.status(this.props.statusColumnTitle)
|
||||
];
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import _ from 'lodash';
|
||||
import LineGraph from './LineGraph.jsx';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Percentage from './util/Percentage.js';
|
||||
import { processTimeseriesMetrics } from './util/MetricUtils.js';
|
||||
import React from 'react';
|
||||
import { ApiHelpers, urlsForResource } from './util/ApiHelpers.js';
|
||||
import { metricToFormatter, toClassName } from './util/Utils.js';
|
||||
import { Table, Tabs } from 'antd';
|
||||
|
||||
|
@ -23,16 +21,16 @@ const resourceInfo = {
|
|||
"path": { title: "path", url: null }
|
||||
};
|
||||
|
||||
const generateColumns = sortable => {
|
||||
const generateColumns = (sortable, ConduitLink) => {
|
||||
return {
|
||||
resourceName: (resource, pathPrefix) => {
|
||||
resourceName: resource => {
|
||||
return {
|
||||
title: resource.title,
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
sorter: sortable ? (a, b) => (a.name || "").localeCompare(b.name) : false,
|
||||
render: name => !resource.url ? name :
|
||||
<Link to={`${pathPrefix}${resource.url}${name}`}>{name}</Link>
|
||||
<ConduitLink to={`${resource.url}${name}`}>{name}</ConduitLink>
|
||||
};
|
||||
},
|
||||
successRate: {
|
||||
|
@ -91,14 +89,14 @@ const numericSort = (a, b) => (_.isNil(a) ? -1 : a) - (_.isNil(b) ? -1 : b);
|
|||
|
||||
const metricToColumns = baseCols => {
|
||||
return {
|
||||
requestRate: (resource, pathPrefix) => [
|
||||
baseCols.resourceName(resource, pathPrefix),
|
||||
requestRate: resource => [
|
||||
baseCols.resourceName(resource),
|
||||
baseCols.requests,
|
||||
resource.title === "deployment" ? null : baseCols.requestDistribution
|
||||
],
|
||||
successRate: (resource, pathPrefix) => [baseCols.resourceName(resource, pathPrefix), baseCols.successRate],
|
||||
latency: (resource, pathPrefix) => [
|
||||
baseCols.resourceName(resource, pathPrefix),
|
||||
successRate: resource => [baseCols.resourceName(resource), baseCols.successRate],
|
||||
latency: resource => [
|
||||
baseCols.resourceName(resource),
|
||||
baseCols.latencyP50,
|
||||
baseCols.latencyP95,
|
||||
baseCols.latencyP99
|
||||
|
@ -115,11 +113,11 @@ const nameToDataKey = {
|
|||
export default class TabbedMetricsTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.api = ApiHelpers(this.props.pathPrefix);
|
||||
this.api = this.props.api;
|
||||
this.handleApiError = this.handleApiError.bind(this);
|
||||
this.loadFromServer = this.loadFromServer.bind(this);
|
||||
|
||||
let tsHelper = urlsForResource(this.props.pathPrefix, this.props.metricsWindow)[this.props.resource];
|
||||
let tsHelper = this.api.urlsForResource[this.props.resource];
|
||||
|
||||
this.state = {
|
||||
timeseries: {},
|
||||
|
@ -128,7 +126,6 @@ export default class TabbedMetricsTable extends React.Component {
|
|||
metricsUrl: tsHelper.url(this.props.resourceName),
|
||||
error: '',
|
||||
lastUpdated: this.props.lastUpdated,
|
||||
metricsWindow: "10s",
|
||||
pollingInterval: 10000,
|
||||
pendingRequests: false
|
||||
};
|
||||
|
@ -167,7 +164,7 @@ export default class TabbedMetricsTable extends React.Component {
|
|||
}
|
||||
this.setState({ pendingRequests: true });
|
||||
|
||||
this.api.fetch(this.state.metricsUrl.ts)
|
||||
this.api.fetchMetrics(this.state.metricsUrl.ts)
|
||||
.then(tsResp => {
|
||||
let tsByEntity = processTimeseriesMetrics(tsResp.metrics, this.state.groupBy);
|
||||
this.setState({
|
||||
|
@ -188,7 +185,7 @@ export default class TabbedMetricsTable extends React.Component {
|
|||
|
||||
getSparklineColumn(metricName) {
|
||||
return {
|
||||
title: "10 minute history",
|
||||
title: `History (last ${this.api.getMetricsWindow()})`,
|
||||
key: metricName,
|
||||
className: "numeric",
|
||||
render: d => {
|
||||
|
@ -212,8 +209,8 @@ export default class TabbedMetricsTable extends React.Component {
|
|||
|
||||
renderTable(metric) {
|
||||
let resource = resourceInfo[this.props.resource];
|
||||
let columnDefinitions = metricToColumns(generateColumns(this.props.sortable));
|
||||
let columns = _.compact(columnDefinitions[metric](resource, this.props.pathPrefix));
|
||||
let columnDefinitions = metricToColumns(generateColumns(this.props.sortable, this.props.api.ConduitLink));
|
||||
let columns = _.compact(columnDefinitions[metric](resource));
|
||||
if (!this.props.hideSparklines) {
|
||||
columns.push(this.getSparklineColumn(metric));
|
||||
}
|
||||
|
|
|
@ -26,8 +26,7 @@ export default class UpstreamDownstreamTables extends React.Component {
|
|||
hideSparklines={numUpstreams > maxTsToFetch}
|
||||
lastUpdated={this.props.lastUpdated}
|
||||
metrics={this.props.upstreamMetrics}
|
||||
metricsWindow={this.props.metricsWindow}
|
||||
pathPrefix={this.props.pathPrefix} />
|
||||
api={this.props.api} />
|
||||
</div>
|
||||
}
|
||||
{
|
||||
|
@ -44,8 +43,7 @@ export default class UpstreamDownstreamTables extends React.Component {
|
|||
hideSparklines={numDownstreams > maxTsToFetch}
|
||||
lastUpdated={this.props.lastUpdated}
|
||||
metrics={this.props.downstreamMetrics}
|
||||
metricsWindow={this.props.metricsWindow}
|
||||
pathPrefix={this.props.pathPrefix} />
|
||||
api={this.props.api} />
|
||||
</div>
|
||||
}
|
||||
</Col>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ApiHelpers } from './util/ApiHelpers.js';
|
||||
import { ApiHelpers } from './util/ApiHelpers.jsx';
|
||||
import { Link } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import './../../css/version.css';
|
||||
|
|
|
@ -1,12 +1,37 @@
|
|||
import _ from 'lodash';
|
||||
import { Link } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
export const ApiHelpers = pathPrefix => {
|
||||
const podsPath = `${pathPrefix}/api/pods`;
|
||||
export const ApiHelpers = (pathPrefix, defaultMetricsWindow = '10m') => {
|
||||
let metricsWindow = defaultMetricsWindow;
|
||||
const podsPath = `/api/pods`;
|
||||
|
||||
const validMetricsWindows = {
|
||||
"10s": "10 minutes",
|
||||
"1m": "1 minute",
|
||||
"10m": "10 minutes",
|
||||
"1h": "1 hour"
|
||||
};
|
||||
|
||||
const apiFetch = path => {
|
||||
if (!_.isEmpty(pathPrefix)) {
|
||||
path = `${pathPrefix}${path}`;
|
||||
}
|
||||
return fetch(path).then(handleFetchErr).then(r => r.json());
|
||||
};
|
||||
|
||||
const fetchMetrics = path => {
|
||||
if (path.indexOf("window") === -1) {
|
||||
if (path.indexOf("?") === -1) {
|
||||
path = `${path}?window=${getMetricsWindow()}`;
|
||||
} else {
|
||||
path = `${path}&window=${getMetricsWindow()}`;
|
||||
}
|
||||
}
|
||||
return apiFetch(path);
|
||||
};
|
||||
|
||||
const fetchPods = () => {
|
||||
return apiFetch(podsPath);
|
||||
};
|
||||
|
@ -18,20 +43,16 @@ export const ApiHelpers = pathPrefix => {
|
|||
return resp;
|
||||
};
|
||||
|
||||
return {
|
||||
fetch: apiFetch,
|
||||
fetchPods
|
||||
const getMetricsWindow = () => metricsWindow;
|
||||
const getMetricsWindowDisplayText = () => validMetricsWindows[metricsWindow];
|
||||
|
||||
const setMetricsWindow = window => {
|
||||
if (!validMetricsWindows[window]) return;
|
||||
metricsWindow = window;
|
||||
};
|
||||
};
|
||||
|
||||
export const urlsForResource = (pathPrefix, metricsWindow) => {
|
||||
/*
|
||||
Timeseries fetches used in the TabbedMetricsTable
|
||||
Rollup fetches used throughout app
|
||||
*/
|
||||
let metricsUrl = `${pathPrefix}/api/metrics?window=${metricsWindow}`;
|
||||
|
||||
return {
|
||||
const metricsUrl = `/api/metrics?`;
|
||||
const urlsForResource = {
|
||||
// all deploys (default), or a given deploy if specified
|
||||
"deployment": {
|
||||
groupBy: "targetDeploy",
|
||||
|
@ -119,4 +140,28 @@ export const urlsForResource = (pathPrefix, metricsWindow) => {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// prefix all links in the app with `pathPrefix`
|
||||
const ConduitLink = props => {
|
||||
let {to, absolute} = props;
|
||||
|
||||
if (absolute) {
|
||||
return <Link to={to} target="_blank">{props.children}</Link>;
|
||||
} else {
|
||||
return <Link to={`${pathPrefix}${to}`}>{props.children}</Link>;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
fetch: apiFetch,
|
||||
fetchMetrics,
|
||||
fetchPods,
|
||||
getMetricsWindow,
|
||||
setMetricsWindow,
|
||||
getValidMetricsWindows: () => _.keys(validMetricsWindows),
|
||||
getMetricsWindowDisplayText,
|
||||
urlsForResource: urlsForResource,
|
||||
ConduitLink
|
||||
};
|
||||
};
|
|
@ -1,3 +1,4 @@
|
|||
import { ApiHelpers } from './components/util/ApiHelpers.jsx';
|
||||
import DeploymentDetail from './components/DeploymentDetail.jsx';
|
||||
import DeploymentsList from './components/DeploymentsList.jsx';
|
||||
import { Layout } from 'antd';
|
||||
|
@ -13,7 +14,7 @@ import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom';
|
|||
import './../css/styles.css';
|
||||
|
||||
let appMain = document.getElementById('main');
|
||||
let appData = appMain.dataset;
|
||||
let appData = !appMain ? {} : appMain.dataset;
|
||||
|
||||
let pathPrefix = "";
|
||||
let proxyPathMatch = window.location.pathname.match(/\/api\/v1\/namespaces\/.*\/proxy/g);
|
||||
|
@ -21,23 +22,25 @@ if (proxyPathMatch) {
|
|||
pathPrefix = proxyPathMatch[0];
|
||||
}
|
||||
|
||||
let api = ApiHelpers(pathPrefix);
|
||||
|
||||
ReactDOM.render((
|
||||
<BrowserRouter>
|
||||
<Layout>
|
||||
<Layout.Sider width="310">
|
||||
<Route render={routeProps => <Sidebar {...routeProps} goVersion={appData.goVersion} releaseVersion={appData.releaseVersion} pathPrefix={pathPrefix} uuid={appData.uuid} />} />
|
||||
<Route render={routeProps => <Sidebar {...routeProps} goVersion={appData.goVersion} releaseVersion={appData.releaseVersion} api={api} pathPrefix={pathPrefix} uuid={appData.uuid} />} />
|
||||
</Layout.Sider>
|
||||
<Layout>
|
||||
<Layout.Content style={{ margin: '0 0', padding: 0, background: '#fff' }}>
|
||||
<div className="main-content">
|
||||
<Switch>
|
||||
<Redirect exact from={`${pathPrefix}/`} to={`${pathPrefix}/servicemesh`} />
|
||||
<Route path={`${pathPrefix}/servicemesh`} render={() => <ServiceMesh pathPrefix={pathPrefix} releaseVersion={appData.releaseVersion} />} />
|
||||
<Route path={`${pathPrefix}/deployments`} render={() => <DeploymentsList pathPrefix={pathPrefix} />} />
|
||||
<Route path={`${pathPrefix}/deployment`} render={props => <DeploymentDetail pathPrefix={pathPrefix} location={props.location} />} />
|
||||
<Route path={`${pathPrefix}/paths`} render={props => <Paths 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}/servicemesh`} render={() => <ServiceMesh api={api} releaseVersion={appData.releaseVersion} />} />
|
||||
<Route path={`${pathPrefix}/deployments`} render={() => <DeploymentsList api={api} />} />
|
||||
<Route path={`${pathPrefix}/deployment`} render={props => <DeploymentDetail api={api} location={props.location} />} />
|
||||
<Route path={`${pathPrefix}/paths`} render={props => <Paths api={api} location={props.location} />} />
|
||||
<Route path={`${pathPrefix}/pod`} render={props => <PodDetail api={api} location={props.location} />} />
|
||||
<Route path={`${pathPrefix}/routes`} render={() => <Routes />} />
|
||||
<Route component={NoMatch} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
/* eslint-disable */
|
||||
import 'raf/polyfill'; // the polyfill import must be first
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
import { ApiHelpers } from '../js/components/util/ApiHelpers.jsx';
|
||||
import Enzyme from 'enzyme';
|
||||
import { expect } from 'chai';
|
||||
import { mount } from 'enzyme';
|
||||
import { routerWrap } from './testHelpers.jsx';
|
||||
import sinon from 'sinon';
|
||||
import sinonStubPromise from 'sinon-stub-promise';
|
||||
/* eslint-enable */
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
sinonStubPromise(sinon);
|
||||
|
||||
describe('ApiHelpers', () => {
|
||||
let api, fetchStub;
|
||||
|
||||
beforeEach(() => {
|
||||
fetchStub = sinon.stub(window, 'fetch');
|
||||
fetchStub.returnsPromise().resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ metrics: [] })
|
||||
});
|
||||
api = ApiHelpers("");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
api = null;
|
||||
window.fetch.restore();
|
||||
});
|
||||
|
||||
describe('getMetricsWindow/setMetricsWindow', () => {
|
||||
it('sets a default metricsWindow', () => {
|
||||
expect(api.getMetricsWindow()).to.equal('10m');
|
||||
});
|
||||
|
||||
it('changes the metricsWindow on valid window input', () => {
|
||||
expect(api.getMetricsWindow()).to.equal('10m');
|
||||
|
||||
api.setMetricsWindow('10s');
|
||||
expect(api.getMetricsWindow()).to.equal('10s');
|
||||
|
||||
api.setMetricsWindow('1m');
|
||||
expect(api.getMetricsWindow()).to.equal('1m');
|
||||
|
||||
api.setMetricsWindow('10m');
|
||||
expect(api.getMetricsWindow()).to.equal('10m');
|
||||
});
|
||||
|
||||
it('does not change metricsWindow on invalid window size', () => {
|
||||
expect(api.getMetricsWindow()).to.equal('10m');
|
||||
|
||||
api.setMetricsWindow('10h');
|
||||
expect(api.getMetricsWindow()).to.equal('10m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConduitLink', () => {
|
||||
it('wraps a relative link with the pathPrefix', () => {
|
||||
api = ApiHelpers('/my/path/prefix');
|
||||
let linkProps = { to: "/myrelpath", children: ["Informative Link Title"] };
|
||||
let conduitLink = mount(routerWrap(api.ConduitLink, linkProps));
|
||||
|
||||
expect(conduitLink.find("Link")).to.have.length(1);
|
||||
expect(conduitLink.html()).to.contain('href="/my/path/prefix/myrelpath"');
|
||||
expect(conduitLink.html()).to.contain(linkProps.children[0]);
|
||||
});
|
||||
|
||||
it('wraps a relative link with no pathPrefix', () => {
|
||||
api = ApiHelpers('');
|
||||
let linkProps = { to: "/myrelpath", children: ["Informative Link Title"] };
|
||||
let conduitLink = mount(routerWrap(api.ConduitLink, linkProps));
|
||||
|
||||
expect(conduitLink.find("Link")).to.have.length(1);
|
||||
expect(conduitLink.html()).to.contain('href="/myrelpath"');
|
||||
expect(conduitLink.html()).to.contain(linkProps.children[0]);
|
||||
});
|
||||
|
||||
it('leaves an absolute link unchanged', () => {
|
||||
api = ApiHelpers('/my/path/prefix');
|
||||
let linkProps = { absolute: "true", to: "http://xkcd.com", children: ["Best Webcomic"] };
|
||||
let conduitLink = mount(routerWrap(api.ConduitLink, linkProps));
|
||||
|
||||
expect(conduitLink.find("Link")).to.have.length(1);
|
||||
expect(conduitLink.html()).to.contain('href="http://xkcd.com"');
|
||||
expect(conduitLink.html()).to.contain(linkProps.children[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetch', () => {
|
||||
it('adds pathPrefix to a metrics request', () => {
|
||||
api = ApiHelpers('/the/path/prefix');
|
||||
api.fetch('/resource/foo');
|
||||
|
||||
expect(fetchStub.calledOnce).to.be.true;
|
||||
expect(fetchStub.args[0][0]).to.equal('/the/path/prefix/resource/foo');
|
||||
});
|
||||
|
||||
it('requests from / when there is no path prefix', () => {
|
||||
api = ApiHelpers('');
|
||||
api.fetch('/resource/foo');
|
||||
|
||||
expect(fetchStub.calledOnce).to.be.true;
|
||||
expect(fetchStub.args[0][0]).to.equal('/resource/foo');
|
||||
});
|
||||
|
||||
it('throws an error if response status is not "ok"', () => {
|
||||
let errorMessage = "do or do not. there is no try.";
|
||||
fetchStub.returnsPromise().resolves({
|
||||
ok: false,
|
||||
statusText: errorMessage
|
||||
});
|
||||
|
||||
api = ApiHelpers('');
|
||||
let errorHandler = sinon.spy();
|
||||
|
||||
api.fetch('/resource/foo')
|
||||
.catch(errorHandler);
|
||||
|
||||
expect(errorHandler.args[0][0].message).to.equal(errorMessage);
|
||||
expect(errorHandler.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchMetrics', () => {
|
||||
it('adds pathPrefix and metricsWindow to a metrics request', () => {
|
||||
api = ApiHelpers('/the/prefix');
|
||||
api.fetchMetrics('/my/path');
|
||||
|
||||
expect(fetchStub.calledOnce).to.be.true;
|
||||
expect(fetchStub.args[0][0]).to.equal('/the/prefix/my/path?window=10m');
|
||||
});
|
||||
|
||||
it('adds a ?window= if metricsWindow is the only param', () => {
|
||||
api.fetchMetrics('/metrics');
|
||||
|
||||
expect(fetchStub.calledOnce).to.be.true;
|
||||
expect(fetchStub.args[0][0]).to.equal('/metrics?window=10m');
|
||||
});
|
||||
|
||||
it('adds &window= if metricsWindow is not the only param', () => {
|
||||
api.fetchMetrics('/metrics?foo=3&bar="me"');
|
||||
|
||||
expect(fetchStub.calledOnce).to.be.true;
|
||||
expect(fetchStub.args[0][0]).to.equal('/metrics?foo=3&bar="me"&window=10m');
|
||||
});
|
||||
|
||||
it('does not add another &window= if there is already a window param', () => {
|
||||
api.fetchMetrics('/metrics?foo=3&window=24h&bar="me"');
|
||||
|
||||
expect(fetchStub.calledOnce).to.be.true;
|
||||
expect(fetchStub.args[0][0]).to.equal('/metrics?foo=3&window=24h&bar="me"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchPods', () => {
|
||||
it('fetches the pods from the api', () => {
|
||||
api = ApiHelpers("/random/prefix");
|
||||
api.fetchPods();
|
||||
|
||||
expect(fetchStub.calledOnce).to.be.true;
|
||||
expect(fetchStub.args[0][0]).to.equal('/random/prefix/api/pods');
|
||||
});
|
||||
});
|
||||
|
||||
describe('urlsForResource', () => {
|
||||
it('returns the correct timeseries and metric rollup urls for deployment overviews', () => {
|
||||
api = ApiHelpers('/go/my/own/way');
|
||||
let deploymentUrls = api.urlsForResource["deployment"].url("myDeploy");
|
||||
|
||||
expect(deploymentUrls.ts).to.equal('/api/metrics?×eries=true&target_deploy=myDeploy');
|
||||
expect(deploymentUrls.rollup).to.equal('/api/metrics?&target_deploy=myDeploy');
|
||||
});
|
||||
|
||||
it('returns the correct timeseries and metric rollup urls for upstream deployments', () => {
|
||||
let deploymentUrls = api.urlsForResource["upstream_deployment"].url("farUp");
|
||||
|
||||
expect(deploymentUrls.ts).to.equal('/api/metrics?&aggregation=source_deploy&target_deploy=farUp×eries=true');
|
||||
expect(deploymentUrls.rollup).to.equal('/api/metrics?&aggregation=source_deploy&target_deploy=farUp');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-disable */
|
||||
import 'raf/polyfill';
|
||||
import 'raf/polyfill'; // the polyfill import must be first
|
||||
import { ApiHelpers } from '../js/components/util/ApiHelpers.jsx';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
import DeploymentDetail from '../js/components/DeploymentDetail.jsx';
|
||||
import Enzyme from 'enzyme';
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import _ from 'lodash';
|
||||
import { ApiHelpers } from '../js/components/util/ApiHelpers.jsx';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import React from 'react';
|
||||
import { Route, Router } from 'react-router';
|
||||
|
||||
const componentDefaultProps = { api: ApiHelpers("") };
|
||||
|
||||
export function routerWrap(Component, extraProps={}, route="/", currentLoc="/") {
|
||||
const createElement = (ComponentToWrap, props) => <ComponentToWrap {...(_.merge({}, componentDefaultProps, props, extraProps))} />;
|
||||
return (
|
||||
<Router history={createMemoryHistory(currentLoc)} createElement={(Component, props) => <Component {...(_.merge({}, props, extraProps))} />}>
|
||||
<Route path={route} component={Component} />
|
||||
<Router history={createMemoryHistory(currentLoc)} createElement={createElement}>
|
||||
<Route path={route} render={props => createElement(Component, props)} />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue