WIP add kubernetes dashboard

pull/549/head
Andrew Watkins 2016-11-14 14:30:27 -08:00
parent b66526b483
commit 01ae8b1b82
8 changed files with 275 additions and 0 deletions

View File

@ -7,6 +7,7 @@ import App from 'src/App';
import AlertsApp from 'src/alerts';
import CheckSources from 'src/CheckSources';
import {HostsPage, HostPage} from 'src/hosts';
import {KubernetesPage} from 'src/kubernetes';
import {KapacitorPage, KapacitorRulePage, KapacitorRulesPage, KapacitorTasksPage} from 'src/kapacitor';
import TasksPage from 'src/tasks';
import DataExplorer from 'src/chronograf';
@ -100,6 +101,7 @@ const Root = React.createClass({
<Route path="chronograf/data-explorer/:base64ExplorerID" component={DataExplorer} />
<Route path="hosts" component={HostsPage} />
<Route path="hosts/:hostID" component={HostPage} />
<Route path="kubernetes" component={KubernetesPage} />
<Route path="kapacitor-config" component={KapacitorPage} />
<Route path="kapacitor-tasks" component={KapacitorTasksPage} />
<Route path="alerts" component={AlertsApp} />

View File

@ -0,0 +1,41 @@
import {proxy} from 'utils/queryUrlGenerator';
import AJAX from 'utils/ajax';
import _ from 'lodash';
export function getAppsForHosts(proxyLink, hosts, appMappings) {
const measurements = appMappings.map((m) => `^${m.measurement}$`).join('|');
const measurementsToApps = _.zipObject(appMappings.map(m => m.measurement), appMappings.map(m => m.name));
return proxy({
source: proxyLink,
query: `show series from /${measurements}/`,
db: 'telegraf',
}).then((resp) => {
const newHosts = Object.assign({}, hosts);
const allSeries = _.get(resp, ['data', 'results', '0', 'series', '0', 'values'], []);
allSeries.forEach(([series]) => {
const matches = series.match(/(\w*).*,host=([^,]*)/);
if (!matches || matches.length !== 3) { // eslint-disable-line no-magic-numbers
return;
}
const measurement = matches[1];
const host = matches[2];
if (!newHosts[host]) {
return;
}
if (!newHosts[host].apps) {
newHosts[host].apps = [];
}
newHosts[host].apps = _.uniq(newHosts[host].apps.concat(measurementsToApps[measurement]));
});
return newHosts;
});
}
export function fetchLayouts() {
return AJAX({
url: `/chronograf/v1/layouts`,
method: 'GET',
});
}

View File

@ -0,0 +1,85 @@
import React, {PropTypes} from 'react';
import LayoutRenderer from '../components/LayoutRenderer';
import TimeRangeDropdown from '../../shared/components/TimeRangeDropdown';
import timeRanges from 'hson!../../shared/data/timeRanges.hson';
export const KubernetesPage = React.createClass({
propTypes: {
source: PropTypes.shape({
links: PropTypes.shape({
proxy: PropTypes.string.isRequired,
}).isRequired,
}),
layouts: PropTypes.arrayOf(PropTypes.shape().isRequired).isRequired,
},
getInitialState() {
const fifteenMinutesIndex = 1;
return {
timeRange: timeRanges[fifteenMinutesIndex],
};
},
renderLayouts(layouts) {
const autoRefreshMs = 15000;
const {timeRange} = this.state;
const source = this.props.source.links.proxy;
let layoutCells = [];
layouts.forEach((layout) => {
layoutCells = layoutCells.concat(layout.cells);
});
layoutCells.forEach((cell, i) => {
cell.queries.forEach((q) => {
q.text = q.query;
q.database = q.db;
});
cell.x = (i * 4 % 12); // eslint-disable-line no-magic-numbers
cell.y = 0;
});
return (
<LayoutRenderer
timeRange={timeRange}
cells={layoutCells}
autoRefreshMs={autoRefreshMs}
source={source}
/>
);
},
handleChooseTimeRange({lower}) {
const timeRange = timeRanges.find((range) => range.queryValue === lower);
this.setState({timeRange});
},
render() {
const {layouts} = this.props;
const {timeRange} = this.state;
return (
<div className="host-dashboard hosts-page">
<div className="enterprise-header hosts-dashboard-header">
<div className="enterprise-header__container">
<div className="enterprise-header__left">
<h2>Kubernetes Dashboard</h2>
</div>
<div className="enterprise-header__right">
<h1>Range:</h1>
<TimeRangeDropdown onChooseTimeRange={this.handleChooseTimeRange} selected={timeRange.inputValue} />
</div>
</div>
</div>
<div className="hosts-page-scroll-container">
<div className="container-fluid hosts-dashboard">
<div className="row">
{ (layouts.length > 0) ? this.renderLayouts(layouts) : '' }
</div>
</div>
</div>
</div>
);
},
});
export default KubernetesPage;

View File

@ -0,0 +1,109 @@
import React, {PropTypes} from 'react';
import AutoRefresh from 'shared/components/AutoRefresh';
import LineGraph from 'shared/components/LineGraph';
import ReactGridLayout from 'react-grid-layout';
import _ from 'lodash';
const RefreshingLineGraph = AutoRefresh(LineGraph);
export const LayoutRenderer = React.createClass({
propTypes: {
timeRange: PropTypes.shape({
defaultGroupBy: PropTypes.string.isRequired,
queryValue: PropTypes.string.isRequired,
}).isRequired,
cells: PropTypes.arrayOf(
PropTypes.shape({
queries: PropTypes.arrayOf(
PropTypes.shape({
rp: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
database: PropTypes.string.isRequired,
groupbys: PropTypes.arrayOf(PropTypes.string),
wheres: PropTypes.arrayOf(PropTypes.string),
}).isRequired
).isRequired,
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
w: PropTypes.number.isRequired,
h: PropTypes.number.isRequired,
i: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
}).isRequired
),
autoRefreshMs: PropTypes.number.isRequired,
host: PropTypes.string,
source: PropTypes.string,
},
getInitialState() {
return ({
layout: _.without(this.props.cells, ['queries']),
});
},
buildQuery(q) {
const {timeRange, host} = this.props;
const {wheres, groupbys} = q;
let text = q.text;
text += ` where time > ${timeRange.queryValue}`
if (host) {
text += ` and \"host\" = '${host}'`;
}
if (wheres && wheres.length > 0) {
text += ` and ${wheres.join(' and ')}`;
}
if (groupbys) {
if (groupbys.find((g) => g.includes("time"))) {
text += ` group by ${groupbys.join(',')}`;
} else if (groupbys.length > 0) {
text += ` group by time(${timeRange.defaultGroupBy}),${groupbys.join(',')}`;
} else {
text += ` group by time(${timeRange.defaultGroupBy})`;
}
} else {
text += ` group by time(${timeRange.defaultGroupBy})`;
}
return text;
},
generateGraphs() {
const {autoRefreshMs, source} = this.props;
return this.props.cells.map((cell) => {
const qs = cell.queries.map((q) => {
return Object.assign({}, q, {
host: source,
text: this.buildQuery(q),
});
});
return (
<div key={cell.i}>
<h2 className="hosts-graph-heading">{cell.name}</h2>
<div className="hosts-graph graph-panel__graph-container">
<RefreshingLineGraph
queries={qs}
autoRefresh={autoRefreshMs}
/>
</div>
</div>
);
});
},
render() {
return (
<ReactGridLayout layout={this.state.layout} isDraggable={false} isResizable={false} cols={12} rowHeight={90} width={1200}>
{this.generateGraphs()}
</ReactGridLayout>
);
},
});
export default LayoutRenderer;

View File

@ -0,0 +1,34 @@
import React, {PropTypes} from 'react';
import {fetchLayouts} from '../apis';
import KubernetesDashboard from 'src/kubernetes/components/KubernetesDashboard';
export const KubernetesPage = React.createClass({
propTypes: {
source: PropTypes.shape({
links: PropTypes.shape({
proxy: PropTypes.string.isRequired,
}).isRequired,
}),
},
getInitialState() {
return {
layouts: [],
};
},
componentDidMount() {
fetchLayouts().then(({data: {layouts}}) => {
const kubernetesLayouts = layouts.filter((l) => l.app === 'kubernetes');
this.setState({layouts: kubernetesLayouts});
});
},
render() {
return (
<KubernetesDashboard layouts={this.state.layouts} source={this.props.source} />
);
},
});
export default KubernetesPage;

View File

@ -0,0 +1,2 @@
import KubernetesPage from './containers/KubernetesPage';
export {KubernetesPage};

View File

@ -1,4 +1,5 @@
import AJAX from 'utils/ajax';
import {proxy} from 'utils/queryUrlGenerator';
export function getSources() {
return AJAX({

View File

@ -23,6 +23,7 @@ const SideNav = React.createClass({
<NavBlock icon="cpu" link={`${sourcePrefix}/hosts`}>
<NavHeader link={`${sourcePrefix}/hosts`} title="Infrastructure" />
<NavListItem link={`${sourcePrefix}/hosts`}>Host List</NavListItem>
<NavListItem link={`${sourcePrefix}/kubernetes`}>Kubernetes Dashboard</NavListItem>
</NavBlock>
<NavBlock icon="graphline" link={dataExplorerLink}>
<NavHeader link={dataExplorerLink} title={'Data'} />