Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Monitoring dashboards: Initial commit #3771

Merged
merged 1 commit into from Dec 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions frontend/packages/console-shared/src/components/alerts/error.tsx
@@ -0,0 +1,15 @@
import * as React from 'react';
import { Alert } from '@patternfly/react-core';

const ErrorAlert: React.FC<Props> = ({ message, title = 'An error occurred' }) => (
<Alert isInline className="co-alert co-alert--scrollable" title={title} variant="danger">
{message}
</Alert>
);

type Props = {
message: string;
title?: string;
};

export default ErrorAlert;
2 changes: 2 additions & 0 deletions frontend/public/components/monitoring.tsx
Expand Up @@ -31,6 +31,7 @@ import {
import store from '../redux';
import { Table, TableData, TableRow, TextFilter } from './factory';
import { confirmModal } from './modals';
import MonitoringDashboardsPage from './monitoring/dashboards';
import { graphStateToProps, QueryBrowserPage, ToggleGraph } from './monitoring/metrics';
import { Labels, QueryBrowser, QueryObj } from './monitoring/query-browser';
import { CheckBoxes } from './row-filter';
Expand Down Expand Up @@ -1558,6 +1559,7 @@ const PollerPages = () => {
export const MonitoringUI = () => (
<Switch>
<Redirect from="/monitoring" exact to="/monitoring/alerts" />
<Route path="/monitoring/dashboards" exact component={MonitoringDashboardsPage} />
<Route path="/monitoring/query-browser" exact component={QueryBrowserPage} />
<Route path="/monitoring/silences/~new" exact component={CreateSilence} />
<Route component={PollerPages} />
Expand Down
30 changes: 30 additions & 0 deletions frontend/public/components/monitoring/_monitoring.scss
@@ -1,3 +1,33 @@
.co-m-pane__heading--monitoring-dashboards {
justify-content: flex-start;
}

.monitoring-dashboards__dropdown {
width: 120px;
}

.monitoring-dashboards__dropdown-wrap {
margin-bottom: 10px;
margin-left: 20px;
}

.monitoring-dashboards__options {
display: flex;
justify-content: space-between;
}

.monitoring-dashboards__options-group {
display: flex;
}

.monitoring-dashboards__panel {
margin: 0 -5px 20px -5px;
}

.monitoring-dashboards__single-stat {
font-size: var(--pf-global--FontSize--3xl);
}

.query-browser__toggle-graph {
margin-bottom: 5px;
float: right;
Expand Down
@@ -0,0 +1,7 @@
import * as React from 'react';

import { Bar } from '../../graphs';

const BarChart: React.FC<{ query: string }> = ({ query }) => <Bar query={query} />;

export default BarChart;
21 changes: 21 additions & 0 deletions frontend/public/components/monitoring/dashboards/graph.tsx
@@ -0,0 +1,21 @@
import * as React from 'react';

import { QueryBrowser } from '../query-browser';

const Graph: React.FC<Props> = ({ pollInterval, queries, timespan }) => (
<QueryBrowser
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to see we can reuse this 👍

defaultSamples={30}
defaultTimespan={timespan}
hideControls
pollInterval={pollInterval}
queries={queries}
/>
);

type Props = {
pollInterval: number;
queries: string[];
timespan: number;
};

export default Graph;
237 changes: 237 additions & 0 deletions frontend/public/components/monitoring/dashboards/index.tsx
@@ -0,0 +1,237 @@
import * as _ from 'lodash-es';
import * as React from 'react';
import { Helmet } from 'react-helmet';

import ErrorAlert from '@console/shared/src/components/alerts/error';
import Dashboard from '@console/shared/src/components/dashboard/Dashboard';
import DashboardCard from '@console/shared/src/components/dashboard/dashboard-card/DashboardCard';
import DashboardCardBody from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardBody';
import DashboardCardHeader from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardHeader';
import DashboardCardTitle from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardTitle';

import { k8sBasePath } from '../../../module/k8s';
import { Dropdown, LoadingInline, useSafeFetch } from '../../utils';
import { parsePrometheusDuration } from '../../utils/datetime';
import { withFallback } from '../../utils/error-boundary';
import BarChart from './bar-chart';
import Graph from './graph';
import SingleStat from './single-stat';
import Table from './table';
import { Panel } from './types';

const evaluateTemplate = (s: string) => {
// TODO: Variable options will be created based on the dashboard `templating` section
const variables = {
cluster: '',
datasource: 'prometheus',
interval: '4h',
namespace: 'openshift-kube-apiserver',
node: '',
resolution: '5m',
};

return _.reduce(
variables,
(result: string, v: string, k: string): string => {
return result.replace(new RegExp(`\\$${k}`, 'g'), v);
},
s,
);
};

// TODO: Just a stub for now
const VariableDropdowns = () => null;

const timespanOptions = ['5m', '15m', '30m', '1h', '2h', '6h', '12h', '1d', '2d', '1w', '2w'];
const defaultTimespan = '30m';

const pollIntervalOptions = ['5s', '15s', '30s', '1m', '2m', '5m', '15m', '30m'];
const defaultPollInterval = '30s';

// TODO: Dynamically load the list of dashboards
const boards = [
'k8s-resources-cluster',
'k8s-resources-namespace',
'cluster-total',
'node-cluster-rsrc-use',
];
const DashboardDropdown: React.FC<{ setBoard: (string) => void }> = ({ setBoard }) => {
const items = _.zipObject(boards, boards);
return <Dropdown items={items} onChange={(v) => setBoard(v)} selectedKey={boards[0]} />;
};

const DurationDropdown: React.FC<DurationDropdownProps> = ({ items, onChange, selected }) => (
<Dropdown
buttonClassName="monitoring-dashboards__dropdown"
items={_.zipObject(items, items)}
onChange={(v: string) => onChange(parsePrometheusDuration(v))}
selectedKey={selected}
/>
);

const Card: React.FC<PanelProps> = ({ panel, pollInterval, timespan }) => {
const queries = _.map(panel.targets, 'expr').map(evaluateTemplate);
if (!queries.length) {
return null;
}

// If panel doesn't specify a span, try to get it from the gridPos or default to full width
let colSpan = panel.span;
if (!_.isNumber(colSpan)) {
colSpan = _.has(panel, 'gridPos.w') ? panel.gridPos.w / 2 : 12;
}

return (
<div className={`col-xs-${colSpan}`}>
<DashboardCard className="monitoring-dashboards__panel">
<DashboardCardHeader>
<DashboardCardTitle>{panel.title}</DashboardCardTitle>
</DashboardCardHeader>
<DashboardCardBody>
{panel.type === 'grafana-piechart-panel' && <BarChart query={queries[0]} />}
{panel.type === 'graph' && (
<Graph pollInterval={pollInterval} queries={queries} timespan={timespan} />
)}
{panel.type === 'row' && !_.isEmpty(panel.panels) && (
<div className="row">
{_.map(panel.panels, (p) => (
<Card key={p.id} panel={p} pollInterval={pollInterval} timespan={timespan} />
))}
</div>
)}
{panel.type === 'singlestat' && (
<SingleStat
decimals={panel.decimals}
format={panel.format}
pollInterval={pollInterval}
postfix={panel.postfix}
prefix={panel.prefix}
query={queries[0]}
units={panel.units}
/>
)}
{panel.type === 'table' && (
<Table panel={panel} pollInterval={pollInterval} queries={queries} />
)}
</DashboardCardBody>
</DashboardCard>
</div>
);
};

const Board: React.FC<{ board: string; pollInterval: number; timespan: number }> = ({
board,
pollInterval,
timespan,
}) => {
const [data, setData] = React.useState();
const [error, setError] = React.useState<string>();

const safeFetch = React.useCallback(useSafeFetch(), []);

React.useEffect(() => {
const path = `${k8sBasePath}/api/v1/namespaces/openshift-monitoring/configmaps/grafana-dashboard-${board}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok great, can take a look at the configmaps from here.

This is where we need to have the discussion about ConfigMap vs CRD and the RBAC ramifications, as users may not be able to access these resources.

safeFetch(path)
.then((response) => {
const json = _.get(response, ['data', `${board}.json`]);
if (!json) {
setData(undefined);
setError('Dashboard definition JSON not found');
} else {
setData(JSON.parse(json));
setError(undefined);
}
})
.catch((err) => {
if (err.name !== 'AbortError') {
spadgett marked this conversation as resolved.
Show resolved Hide resolved
setData(undefined);
setError(_.get(err, 'json.error', err.message));
}
});
}, [board, safeFetch]);

if (error) {
return <ErrorAlert message={error} />;
}
if (!data) {
return <LoadingInline />;
}

const rows = _.isEmpty(data.rows) ? [{ panels: data.panels }] : data.rows;

return (
<>
{_.map(rows, (row, i) => (
<div className="row" key={i}>
{_.map(row.panels, (panel, j) => (
<Card key={j} panel={panel} pollInterval={pollInterval} timespan={timespan} />
))}
</div>
))}
</>
);
};

const MonitoringDashboardsPage: React.FC<{}> = () => {
const [pollInterval, setPollInterval] = React.useState(
parsePrometheusDuration(defaultPollInterval),
);
const [timespan, setTimespan] = React.useState(parsePrometheusDuration(defaultTimespan));
const [board, setBoard] = React.useState(boards[0]);

return (
<>
<Helmet>
<title>Metrics Dashboards</title>
</Helmet>
<div className="co-m-nav-title co-m-nav-title--detail">
<h1 className="co-m-pane__heading co-m-pane__heading--monitoring-dashboards">
Metrics Dashboards
<div className="monitoring-dashboards__dropdown-wrap">
<DashboardDropdown setBoard={setBoard} />
</div>
</h1>
<div className="monitoring-dashboards__options">
<div className="monitoring-dashboards__options-group">
<VariableDropdowns />
</div>
<div className="monitoring-dashboards__options-group">
<div className="monitoring-dashboards__dropdown-wrap">
<h4>Time Range</h4>
<DurationDropdown
items={timespanOptions}
onChange={setTimespan}
selected={defaultTimespan}
/>
</div>
<div className="monitoring-dashboards__dropdown-wrap">
<h4>Refresh Interval</h4>
<DurationDropdown
items={pollIntervalOptions}
onChange={setPollInterval}
selected={defaultPollInterval}
/>
</div>
</div>
</div>
</div>
<Dashboard>
{board && <Board board={board} timespan={timespan} pollInterval={pollInterval} />}
</Dashboard>
</>
);
};

type DurationDropdownProps = {
items: string[];
onChange: (string) => void;
selected: string;
};

type PanelProps = {
panel: Panel;
pollInterval: number;
timespan: number;
};

export default withFallback(MonitoringDashboardsPage);