Skip to content

Commit

Permalink
Replace plotly line chart with pf-react area chart
Browse files Browse the repository at this point in the history
  • Loading branch information
TheRealJon committed Apr 29, 2019
1 parent b53117a commit e4c1f98
Show file tree
Hide file tree
Showing 20 changed files with 766 additions and 46 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
},
"dependencies": {
"@patternfly/patternfly": "1.0.219",
"@patternfly/react-charts": "^3.1.1",
"@patternfly/react-core": "2.4.1",
"brace": "0.11.x",
"classnames": "2.x",
Expand Down
6 changes: 6 additions & 0 deletions frontend/public/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ class App extends React.PureComponent {
}

_onNavToggle() {

// Some components, like svg charts, need to reflow when nav is toggled
setTimeout(() => {
window.dispatchEvent(new Event('nav_toggle'));
}, 100);

this.setState(prevState => {
return {
isNavOpen: !prevState.isNavOpen,
Expand Down
8 changes: 4 additions & 4 deletions frontend/public/components/build.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { K8sResourceKindReference, referenceFor, K8sResourceKind } from '../modu
import { cloneBuild, formatBuildDuration, BuildPhase, getBuildNumber } from '../module/k8s/builds';
import { ColHead, DetailsPage, List, ListHeader, ListPage } from './factory';
import { errorModal } from './modals';
import { BuildHooks, BuildStrategy, Kebab, SectionHeading, history, navFactory, ResourceKebab, ResourceLink, resourceObjPath, ResourceSummary, Timestamp, AsyncComponent, resourcePath, StatusIcon } from './utils';
import { BuildHooks, BuildStrategy, Kebab, SectionHeading, history, navFactory, ResourceKebab, ResourceLink, resourceObjPath, ResourceSummary, Timestamp, AsyncComponent, resourcePath, StatusIcon, humanizeDecimalBytes, humanizeCpuCores } from './utils';
import { BuildPipeline, BuildPipelineLogLink } from './build-pipeline';
import { breadcrumbsForOwnerRefs } from './utils/breadcrumbs';
import { fromNow } from './utils/datetime';
Expand Down Expand Up @@ -92,13 +92,13 @@ const BuildGraphs = requirePrometheus(({build}) => {
return <React.Fragment>
<div className="row">
<div className="col-md-4">
<Line title="Memory Usage" namespace={namespace} query={`pod_name:container_memory_usage_bytes:sum{pod_name='${podName}',container_name='',namespace='${namespace}'}`} />
<Line title="Memory Usage" humanize={humanizeDecimalBytes} namespace={namespace} query={`pod_name:container_memory_usage_bytes:sum{pod_name='${podName}',container_name='',namespace='${namespace}'}`} />
</div>
<div className="col-md-4">
<Line title="CPU Usage" namespace={namespace} query={`pod_name:container_cpu_usage:sum{pod_name='${podName}',container_name='',namespace='${namespace}'}`} />
<Line title="CPU Usage" humanize={humanizeCpuCores} namespace={namespace} query={`pod_name:container_cpu_usage:sum{pod_name='${podName}',container_name='',namespace='${namespace}'}`} />
</div>
<div className="col-md-4">
<Line title="Filesystem" namespace={namespace} query={`pod_name:container_fs_usage_bytes:sum{pod_name='${podName}',container_name='',namespace='${namespace}'}`} />
<Line title="Filesystem" humanize={humanizeDecimalBytes} namespace={namespace} query={`pod_name:container_fs_usage_bytes:sum{pod_name='${podName}',container_name='',namespace='${namespace}'}`} />
</div>
</div>

Expand Down
26 changes: 14 additions & 12 deletions frontend/public/components/cluster-health.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as React from 'react';
import { Helmet } from 'react-helmet';

import { humanizeMem, humanizeNumber, PageHeading } from './utils';
import { humanizeBinaryBytes, humanizeNumber, PageHeading, humanizeCpuCores, humanizeDecimalBytes } from './utils';
import { Bar, Gauge, Line, Scalar } from './graphs';

const LINE_CHART_HEIGHT = 200;

const multiLoadQueries = [
{
name: '1m',
Expand Down Expand Up @@ -62,7 +64,7 @@ export const ClusterHealth = () => <div>
</div>
</div>
<div className="col-lg-6 col-md-6">
<Line title="Cluster Load Average" query={multiLoadQueries} />
<Line title="Cluster Load Average" humanize={humanizeCpuCores} height={LINE_CHART_HEIGHT} query={multiLoadQueries} />
</div>
<div className="col-lg-3 col-md-6">
<Bar title="CPU Usage by Namespace" query={'sort(topk(10, sum by (namespace) (namespace:container_cpu_usage:sum)))'} humanize={humanizeNumber} metric="namespace" />
Expand All @@ -81,10 +83,10 @@ export const ClusterHealth = () => <div>
</div>
</div>
<div className="col-lg-6 col-md-6">
<Line title="Memory" query={memoryQueries} />
<Line title="Memory" humanize={humanizeDecimalBytes} height={LINE_CHART_HEIGHT} query={memoryQueries} />
</div>
<div className="col-lg-3 col-md-6">
<Bar title="Mem. Usage by Namespace" query={'sort(topk(10, sum by (namespace) (namespace:container_memory_usage_bytes:sum)))'} humanize={humanizeMem} metric="namespace" />
<Bar title="Mem. Usage by Namespace" query={'sort(topk(10, sum by (namespace) (namespace:container_memory_usage_bytes:sum)))'} humanize={humanizeBinaryBytes} metric="namespace" />
</div>
</div>

Expand All @@ -102,30 +104,30 @@ export const ClusterHealth = () => <div>

</div>
<div className="col-lg-6 col-md-6">
<Line title="Disk" query={diskQueries} />
<Line title="Disk" humanize={humanizeDecimalBytes} height={LINE_CHART_HEIGHT} query={diskQueries} />
</div>
</div>

<div className="row">
<div className="col-lg-9">
<Line title="Network Received (bytes/s)" query={'sum(rate(container_network_receive_bytes_total{interface="eth0"}[1m]))'} />
<Line humanize={humanizeDecimalBytes} title="Network Received (bytes/s)" height={LINE_CHART_HEIGHT} query={'sum(rate(container_network_receive_bytes_total{interface="eth0"}[1m]))'} />
</div>
<div className="col-lg-3 col-md-6">
<Bar title="Network Receive (Top 10 Namespaces)" query={'sort(topk(10, sum by (namespace) (rate(container_network_receive_bytes_total{interface="eth0"}[1m]))))'} humanize={humanizeMem} metric="namespace" />
<Bar title="Network Receive (Top 10 Namespaces)" query={'sort(topk(10, sum by (namespace) (rate(container_network_receive_bytes_total{interface="eth0"}[1m]))))'} humanize={humanizeBinaryBytes} metric="namespace" />
</div>
</div>

<div className="row">
<div className="col-lg-9">
<Line title="Network Transmitted (bytes/s)" query={'sum(rate(container_network_transmit_bytes_total{interface="eth0"}[1m]))'} />
<Line title="Network Transmitted (bytes/s)" humanize={humanizeDecimalBytes} height={LINE_CHART_HEIGHT} query={'sum(rate(container_network_transmit_bytes_total{interface="eth0"}[1m]))'} />
</div>
<div className="col-lg-3 col-md-6">
<Bar title="Network Transmit (Top 10 Namespaces)" query={'sort(topk(10, sum by (namespace) (rate(container_network_transmit_bytes_total{interface="eth0"}[1m]))))'} humanize={humanizeMem} metric="namespace" />
<Bar title="Network Transmit (Top 10 Namespaces)" query={'sort(topk(10, sum by (namespace) (rate(container_network_transmit_bytes_total{interface="eth0"}[1m]))))'} humanize={humanizeBinaryBytes} metric="namespace" />
</div>
</div>
<div className="row">
<div className="col-lg-9">
<Line title="API Error Rate (5m)" query={'sum(rate(apiserver_request_count{code=~"5.."}[1m]))'} />
<Line title="API Error Rate (5m)" height={LINE_CHART_HEIGHT} query={'sum(rate(apiserver_request_count{code=~"5.."}[1m]))'} />
</div>
</div>
<div className="row">
Expand All @@ -136,10 +138,10 @@ export const ClusterHealth = () => <div>
<Bar title="API Reads Req/sec (Top 10 Clients)" query={'sort(topk(10, sum by (client)(rate(apiserver_request_count{verb!="POST", verb!="PUT", verb!="PATCH"}[5m]))))'} humanize={humanizeNumber} metric="client" />
</div>
<div className="col-lg-9">
<Bar title="Top 10 Pod Memory" query={'sort(topk(10, sum by (pod_name) (container_memory_usage_bytes{pod_name!=""})))'} humanize={humanizeMem} metric="pod_name" />
<Bar title="Top 10 Pod Memory" query={'sort(topk(10, sum by (pod_name) (container_memory_usage_bytes{pod_name!=""})))'} humanize={humanizeBinaryBytes} metric="pod_name" />
</div>
<div className="col-lg-9">
<Bar title="Top 10 Pod Memory (non-system)" query={'sort(topk(10, sum by (pod_name) (container_memory_usage_bytes{pod_name!="", namespace!="kube-system", namespace!="tectonic-system"})))'} humanize={humanizeMem} metric="pod_name" />
<Bar title="Top 10 Pod Memory (non-system)" query={'sort(topk(10, sum by (pod_name) (container_memory_usage_bytes{pod_name!="", namespace!="kube-system", namespace!="tectonic-system"})))'} humanize={humanizeBinaryBytes} metric="pod_name" />
</div>
</div>
</div>
Expand Down
9 changes: 6 additions & 3 deletions frontend/public/components/graphs/_graphs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
padding-top: 15px; // So graph title doesn't abut border
margin-top: 0;
margin-bottom: 16px;
overflow: hidden;
overflow: visible; // Tooltips may overflow
}

.graph-wrapper svg {
overflow: visible !important; // Tooltips may overflow
}

.graph-title {
margin: 0;
text-align: center;
margin: 0 0 10px 0;
color: black;
overflow: hidden;
text-overflow: ellipsis;
Expand Down
1 change: 1 addition & 0 deletions frontend/public/components/graphs/graph-loader.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { Bar } from './bar';
export { Gauge } from './gauge';
export { Line } from './line';
export { PlotlyLine } from './plotly-line';
export { QueryBrowser } from './query-browser';
export { Scalar } from './scalar';
export { Donut } from './donut';
7 changes: 4 additions & 3 deletions frontend/public/components/graphs/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import { AsyncComponent } from '../utils/async';
import { FLAGS, connectToFlags } from '../../features';
export { Status, errorStatus } from './status';

export const prometheusBasePath = window.SERVER_FLAGS.prometheusBaseURL;
export const prometheusTenancyBasePath = window.SERVER_FLAGS.prometheusTenancyBaseURL;
export const alertManagerBasePath = window.SERVER_FLAGS.alertManagerBaseURL;
export const prometheusBasePath = 'https://prometheus-k8s-openshift-monitoring.apps.jonjacks.devcluster.openshift.com';
export const prometheusTenancyBasePath = 'https://prometheus-k8s-openshift-monitoring.apps.jonjacks.devcluster.openshift.com';
export const alertManagerBasePath = 'https://prometheus-k8s-openshift-monitoring.apps.jonjacks.devcluster.openshift.com';

export const QueryBrowser = props => <AsyncComponent loader={() => import('./graph-loader').then(c => c.QueryBrowser)} {...props} />;
export const Bar = props => <AsyncComponent loader={() => import('./graph-loader').then(c => c.Bar)} {...props} />;
export const Gauge = props => <AsyncComponent loader={() => import('./graph-loader').then(c => c.Gauge)} {...props} />;
export const Line = props => <AsyncComponent loader={() => import('./graph-loader').then(c => c.Line)} {...props} />;
export const PlotlyLine = props => <AsyncComponent loader={() => import('./graph-loader').then(c => c.PlotlyLine)} {...props} />;
export const Scalar = props => <AsyncComponent loader={() => import('./graph-loader').then(c => c.Scalar)} {...props} />;
export const Donut = props => <AsyncComponent loader={() => import('./graph-loader').then(c => c.Donut)} {...props} />;

Expand Down
40 changes: 40 additions & 0 deletions frontend/public/components/graphs/line.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as _ from 'lodash-es';
import * as React from 'react';
import { Chart, ChartArea, ChartVoronoiContainer, ChartAxis, ChartTheme, ChartGroup } from '@patternfly/react-charts';

import { humanizeNumber } from '../utils';
import { withPrometheusWatch } from './prometheus-watch';
import { resizable } from './resizable';
import { twentyFourHourTime } from '../utils/datetime';
import { areaStyles, cartesianChartStyles } from './themes';



const LineChart = ({
data,
humanize = humanizeNumber,
height = 90,
tickCount = 3,
theme = ChartTheme.light.multi,
width
}) => {
// Override theme. Once PF React Charts is released and we update, there should be much less we need to override here.
const _theme = _.merge(theme, cartesianChartStyles, areaStyles);
const getLabel = v => (data.length > 1) ? `${v.name}: ${humanize(v.y)}` : humanize(v.y);
const container = <ChartVoronoiContainer voronoiDimension="x" labels={getLabel} />;
return <Chart
containerComponent={container}
domainPadding={{y: 20}}
height={height}
width={width}
theme={_theme}
>
<ChartAxis scale="time" tickCount={tickCount} tickFormat={tick => twentyFourHourTime(tick)} />
<ChartAxis dependentAxis tickCount={tickCount} tickFormat={tick => humanize(tick)} />
<ChartGroup>
{ _.map(data, (values, i) => <ChartArea key={i} data={values} />) }
</ChartGroup>
</Chart>;
}

export const Line = _.flow(withPrometheusWatch,resizable)(LineChart);
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const baseData = {
type: 'scatter',
};

export class Line_ extends BaseGraph {
export class PlotlyLine_ extends BaseGraph {
constructor(props) {
super(props);

Expand Down Expand Up @@ -126,8 +126,8 @@ export class Line_ extends BaseGraph {
});
}
}
export const Line = connectToURLs(MonitoringRoutes.Prometheus)(Line_);
export const PlotlyLine = connectToURLs(MonitoringRoutes.Prometheus)(PlotlyLine_);

Line_.contextTypes = {
PlotlyLine_.contextTypes = {
urls: PropTypes.object,
};
140 changes: 140 additions & 0 deletions frontend/public/components/graphs/prometheus-watch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/* eslint-disable no-undef, no-unused-vars */
import * as _ from 'lodash-es';
import * as React from 'react';
import * as classNames from 'classnames';

import { coFetchJSON } from '../../co-fetch';
import { connectToURLs, MonitoringRoutes } from '../../monitoring';
import { SafetyFirst } from '../safety-first';
import { prometheusTenancyBasePath, prometheusBasePath } from '.';

export const withPrometheusWatch = (Component) => connectToURLs(MonitoringRoutes.Prometheus)(
class extends SafetyFirst<PrometheusWatchProps, PrometheusWatchState> {
private interval;
private timeSpan;
private start;
private end;
constructor(props) {
super(props);
this.interval = null;
this.timeSpan = props.timeSpan || 60 * 60 * 1000; // 1 hour
this.start = null;
this.end = null;
this.state = {
error: null,
data: [],
};
}

fetch(enablePolling = true) {
const { query, basePath, namespace, numSamples, timeout, title = ''} = this.props;
const timeSpan = this.end - this.start || this.timeSpan;
const end = this.end || Date.now();
const start = this.start || (end - timeSpan);
const baseUrl = basePath || (namespace ? prometheusTenancyBasePath : prometheusBasePath);
const pollInterval = timeSpan ? Math.max(timeSpan / 120, 3000) : 15000;
const stepSize = (timeSpan && numSamples ? timeSpan / numSamples : pollInterval) / 1000;
const timeoutParam = timeout ? `&timeout=${encodeURIComponent(timeout)}` : '';
const queries = !_.isArray(query) ? [{query, name: title}] : query;
const promises = queries.map(q => {
const nsParam = namespace ? `&namespace=${encodeURIComponent(namespace)}` : '';
const url = this.timeSpan
? `${baseUrl}/api/v1/query_range?query=${encodeURIComponent(q.query)}&start=${start / 1000}&end=${end / 1000}&step=${stepSize}${nsParam}${timeoutParam}`
: `${baseUrl}/api/v1/query?query=${encodeURIComponent(q.query)}${nsParam}${timeoutParam}`;
return coFetchJSON(url).then(result => {
const values = _.get(result, 'data.result[0].values');
return _.map(values, v => ({
name: q.name,
x: new Date(v[0] * 1000),
y: parseFloat(v[1]),
}));
});
});
Promise.all(promises)
.then(data => this.setState({data}))
.catch(error => this.setState({error}))
.then(() => {
if (enablePolling) {
this.interval = setTimeout(() => {
if (this.isMounted_) {
this.fetch();
}
}, pollInterval);
}
});
}

componentWillMount() {
if (this.props.query) {
this.fetch();
}
}

componentWillUnmount() {
super.componentWillUnmount();
clearInterval(this.interval);
}

prometheusURL() {
const { urls, query } = this.props;
const base = urls && urls[MonitoringRoutes.Prometheus];
if (!base) {
return null;
}

const queries = _.isArray(query) ? query : [{query}];
const params = new URLSearchParams();
_.each(queries, (q, i) => {
params.set(`g${i}.range_input`, '1h');
params.set(`g${i}.expr`, q.query);
params.set(`g${i}.tab`, '0');
});

return `${base}/graph?${params.toString()}`;
}

render() {
const { title, className, ...rest } = this.props;
const { data } = this.state;
const url = this.props.query ? this.prometheusURL() : null;

return <div className={classNames('graph-wrapper', className)}>
{ title && <h5 className="graph-title">{title}</h5> }
{
url
? <a href={url} target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none' }}>
<Component {...rest} data={data} />
</a>
: <Component {...rest} data={data} />
}
</div>;
}
}
);

type PrometheusQuery = {
name: string;
query: string;
};

type CartesianData = {
x: Date;
y: number;
};

type PrometheusWatchProps = {
basePath?: string;
className?: string;
humanize?: (v: number) => string;
namespace?: string;
numSamples?: number;
query: PrometheusQuery[] | string;
timeout?: string;
title?: string;
urls?: string[];
};

type PrometheusWatchState = {
data: CartesianData[][];
error?: string;
};
4 changes: 2 additions & 2 deletions frontend/public/components/graphs/query-browser.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { addTraces, deleteTraces, relayout, restyle } from 'plotly.js/lib/core';
import { connectToURLs, MonitoringRoutes } from '../../monitoring';
import { Dropdown, ExternalLink, LoadingInline } from '../utils';
import { formatPrometheusDuration, parsePrometheusDuration } from '../utils/datetime';
import { Line_ } from './line';
import { PlotlyLine_ } from './plotly-line';

const spans = ['5m', '15m', '30m', '1h', '2h', '6h', '12h', '1d', '2d', '1w', '2w'];
const dropdownItems = _.zipObject(spans, spans);

class QueryBrowser_ extends Line_ {
class QueryBrowser_ extends PlotlyLine_ {
constructor(props) {
super(props);

Expand Down
Loading

0 comments on commit e4c1f98

Please sign in to comment.