Add URL params support and much more

Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
Julius Volz 2019-02-17 01:23:51 +01:00
parent b2b4d3a18d
commit d9cc501dff
13 changed files with 367 additions and 107 deletions

View file

@ -11,7 +11,7 @@ body {
} }
.expression-input textarea { .expression-input textarea {
/* font-family: 'Courier New', Courier, monospace; */ font-family: Menlo,Monaco,Consolas,'Courier New',monospace;
resize: none; resize: none;
} }
@ -92,7 +92,7 @@ button.execute-btn {
} }
div.endtime-input { div.endtime-input {
width: 270px !important; width: 240px !important;
} }
.table-controls input { .table-controls input {
@ -128,6 +128,14 @@ div.endtime-input {
margin: 2px 8px 2px 0; margin: 2px 8px 2px 0;
} }
.legend-metric-name {
margin-right: 1px;
}
.legend-label-name {
font-weight: bold;
}
.graph { .graph {
margin: 0 5px 0 5px; margin: 0 5px 0 5px;
} }
@ -168,6 +176,5 @@ div.endtime-input {
} }
.add-panel-btn { .add-panel-btn {
margin-top: -20px;
margin-bottom: 20px; margin-bottom: 20px;
} }

View file

@ -2,8 +2,7 @@ import React, { PureComponent, ReactNode } from 'react';
import { Alert, Table } from 'reactstrap'; import { Alert, Table } from 'reactstrap';
import metricToSeriesName from './MetricFomat'; import SeriesName from './SeriesName';
import { ReactComponent } from '*.svg';
export interface QueryResult { export interface QueryResult {
data: null | { data: null | {
@ -48,6 +47,7 @@ class DataTable extends PureComponent<QueryResult> {
} }
render() { render() {
console.log("RENDER!");
const data = this.props.data; const data = this.props.data;
if (data === null) { if (data === null) {
@ -64,7 +64,7 @@ class DataTable extends PureComponent<QueryResult> {
case 'vector': case 'vector':
rows = (this.limitSeries(data.result) as InstantSample[]) rows = (this.limitSeries(data.result) as InstantSample[])
.map((s: InstantSample, index: number): ReactNode => { .map((s: InstantSample, index: number): ReactNode => {
return <tr key={index}><td>{metricToSeriesName(s.metric, false)}</td><td>{s.value[1]}</td></tr> return <tr key={index}><td><SeriesName labels={s.metric} format={false}/></td><td>{s.value[1]}</td></tr>;
}); });
limited = rows.length != data.result.length; limited = rows.length != data.result.length;
break; break;
@ -74,7 +74,7 @@ class DataTable extends PureComponent<QueryResult> {
const valueText = s.values.map((v) => { const valueText = s.values.map((v) => {
return [1] + ' @' + v[0]; return [1] + ' @' + v[0];
}).join('\n'); }).join('\n');
return <tr style={{whiteSpace: 'pre'}} key={index}><td>{metricToSeriesName(s.metric, false)}</td><td>{valueText}</td></tr> return <tr style={{whiteSpace: 'pre'}} key={index}><td><SeriesName labels={s.metric} format={false}/></td><td>{valueText}</td></tr>;
}); });
limited = rows.length != data.result.length; limited = rows.length != data.result.length;
break; break;

View file

@ -37,8 +37,15 @@ class ExpressionInput extends Component<ExpressionInputProps> {
} }
renderAutosuggest = (downshift: any) => { renderAutosuggest = (downshift: any) => {
if (!downshift.isOpen) {
return null;
}
if (this.prevNoMatchValue && downshift.inputValue.includes(this.prevNoMatchValue)) { if (this.prevNoMatchValue && downshift.inputValue.includes(this.prevNoMatchValue)) {
// TODO: Is this still correct with fuzzy? // TODO: Is this still correct with fuzzy?
if (downshift.isOpen) {
downshift.closeMenu();
}
return null; return null;
} }
@ -52,10 +59,6 @@ class ExpressionInput extends Component<ExpressionInputProps> {
return null; return null;
} }
if (!downshift.isOpen) {
return null; // TODO CHECK NEED FOR THIS
}
return ( return (
<ul className="autosuggest-dropdown" {...downshift.getMenuProps()}> <ul className="autosuggest-dropdown" {...downshift.getMenuProps()}>
{ {
@ -125,9 +128,20 @@ class ExpressionInput extends Component<ExpressionInputProps> {
// By default, Downshift otherwise jumps to the first/last suggestion item instead. // By default, Downshift otherwise jumps to the first/last suggestion item instead.
(event.nativeEvent as any).preventDownshiftDefault = true; (event.nativeEvent as any).preventDownshiftDefault = true;
break; break;
case 'ArrowUp':
case 'ArrowDown':
if (!downshift.isOpen) {
(event.nativeEvent as any).preventDownshiftDefault = true;
}
break;
case 'Enter': case 'Enter':
downshift.closeMenu(); downshift.closeMenu();
break; break;
case 'Escape':
if (!downshift.isOpen) {
this.exprInputRef.current!.blur();
}
break;
default: default:
} }
} }

View file

@ -58,7 +58,10 @@ class Graph extends PureComponent<GraphProps> {
return '<div class="labels">' + labelStrings.join('<br>') + '</div>'; return '<div class="labels">' + labelStrings.join('<br>') + '</div>';
}; };
formatValue = (y: number): string => { formatValue = (y: number | null): string => {
if (y === null) {
return 'null';
}
var abs_y = Math.abs(y); var abs_y = Math.abs(y);
if (abs_y >= 1e24) { if (abs_y >= 1e24) {
return (y / 1e24).toFixed(2) + "Y"; return (y / 1e24).toFixed(2) + "Y";
@ -151,6 +154,7 @@ class Graph extends PureComponent<GraphProps> {
}; };
} }
// This was adapted from Flot's color generation code.
getColors() { getColors() {
let colors = []; let colors = [];
const colorPool = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"]; const colorPool = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"];
@ -191,13 +195,17 @@ class Graph extends PureComponent<GraphProps> {
let data = []; let data = [];
let pos = 0; let pos = 0;
const params = this.props.queryParams!; const params = this.props.queryParams!;
for (let t = params.startTime; t <= params.endTime; t += params.resolution) { for (let t = params.startTime; t <= params.endTime; t += params.resolution) {
// Allow for floating point inaccuracy. // Allow for floating point inaccuracy.
if (ts.values.length > pos && ts.values[pos][0] < t + params.resolution / 100) { if (ts.values.length > pos && ts.values[pos][0] < t + params.resolution / 100) {
data.push([ts.values[pos][0] * 1000, this.parseValue(ts.values[pos][1])]); data.push([ts.values[pos][0] * 1000, this.parseValue(ts.values[pos][1])]);
pos++; pos++;
} else { } else {
data.push([t * 1000, null]); // TODO: Flot has problems displaying intermittent "null" values when stacked,
// resort to 0 now. In Grafana this works for some reason, figure out how they
// do it.
data.push([t * 1000, this.props.stacked ? 0 : null]);
} }
} }
@ -205,6 +213,7 @@ class Graph extends PureComponent<GraphProps> {
labels: ts.metric !== null ? ts.metric : {}, labels: ts.metric !== null ? ts.metric : {},
data: data, data: data,
color: colors[index], color: colors[index],
index: index,
}; };
}) })
} }
@ -214,7 +223,11 @@ class Graph extends PureComponent<GraphProps> {
if (isNaN(val)) { if (isNaN(val)) {
// "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). They // "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). They
// can't be graphed, so show them as gaps (null). // can't be graphed, so show them as gaps (null).
return null;
// TODO: Flot has problems displaying intermittent "null" values when stacked,
// resort to 0 now. In Grafana this works for some reason, figure out how they
// do it.
return this.props.stacked ? 0 : null;
} }
return val; return val;
}; };

View file

@ -18,6 +18,7 @@ import {
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import TimeInput from './TimeInput'; import TimeInput from './TimeInput';
import { parseRange, formatRange } from './utils/timeFormat';
library.add( library.add(
faPlus, faPlus,
@ -42,15 +43,6 @@ class GraphControls extends Component<GraphControlsProps> {
private rangeRef = React.createRef<HTMLInputElement>(); private rangeRef = React.createRef<HTMLInputElement>();
private resolutionRef = React.createRef<HTMLInputElement>(); private resolutionRef = React.createRef<HTMLInputElement>();
rangeUnits: {[unit: string]: number} = {
'y': 60 * 60 * 24 * 365,
'w': 60 * 60 * 24 * 7,
'd': 60 * 60 * 24,
'h': 60 * 60,
'm': 60,
's': 1
}
rangeSteps = [ rangeSteps = [
1, 1,
10, 10,
@ -72,28 +64,8 @@ class GraphControls extends Component<GraphControlsProps> {
730*24*60*60, 730*24*60*60,
] ]
parseRange(rangeText: string): number | null {
const rangeRE = new RegExp('^([0-9]+)([ywdhms]+)$');
const matches = rangeText.match(rangeRE);
if (!matches || matches.length !== 3) {
return null;
}
const value = parseInt(matches[1]);
const unit = matches[2];
return value * this.rangeUnits[unit];
}
formatRange(range: number): string {
for (let unit of Object.keys(this.rangeUnits)) {
if (range % this.rangeUnits[unit] === 0) {
return (range / this.rangeUnits[unit]) + unit;
}
}
return range + 's';
}
onChangeRangeInput = (rangeText: string): void => { onChangeRangeInput = (rangeText: string): void => {
const range = this.parseRange(rangeText); const range = parseRange(rangeText);
if (range === null) { if (range === null) {
this.changeRangeInput(this.props.range); this.changeRangeInput(this.props.range);
} else { } else {
@ -102,7 +74,7 @@ class GraphControls extends Component<GraphControlsProps> {
} }
changeRangeInput = (range: number): void => { changeRangeInput = (range: number): void => {
this.rangeRef.current!.value = this.formatRange(range); this.rangeRef.current!.value = formatRange(range);
} }
increaseRange = (): void => { increaseRange = (): void => {
@ -134,7 +106,7 @@ class GraphControls extends Component<GraphControlsProps> {
</InputGroupAddon> </InputGroupAddon>
<Input <Input
defaultValue={this.formatRange(this.props.range)} defaultValue={formatRange(this.props.range)}
innerRef={this.rangeRef} innerRef={this.rangeRef}
onBlur={() => this.onChangeRangeInput(this.rangeRef.current!.value)} onBlur={() => this.onChangeRangeInput(this.rangeRef.current!.value)}
/> />

View file

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import metricToSeriesName from './MetricFomat'; import SeriesName from './SeriesName';
interface LegendProps { interface LegendProps {
series: any; // TODO: Type this. series: any; // TODO: Type this.
@ -8,13 +8,14 @@ interface LegendProps {
class Legend extends PureComponent<LegendProps> { class Legend extends PureComponent<LegendProps> {
renderLegendItem(s: any) { renderLegendItem(s: any) {
const seriesName = metricToSeriesName(s.labels, false);
return ( return (
<tr key={seriesName} className="legend-item"> <tr key={s.index} className="legend-item">
<td> <td>
<div className="legend-swatch" style={{backgroundColor: s.color}}></div> <div className="legend-swatch" style={{backgroundColor: s.color}}></div>
</td> </td>
<td>{seriesName}</td> <td>
<SeriesName labels={s.labels} format={true} />
</td>
</tr> </tr>
); );
} }

View file

@ -1,4 +1,4 @@
function metricToSeriesName(labels: {[key: string]: string}, formatHTML: boolean): string { function metricToSeriesName(labels: {[key: string]: string}): string {
if (labels === null) { if (labels === null) {
return 'scalar'; return 'scalar';
} }
@ -6,7 +6,7 @@ function metricToSeriesName(labels: {[key: string]: string}, formatHTML: boolean
let labelStrings: string[] = []; let labelStrings: string[] = [];
for (let label in labels) { for (let label in labels) {
if (label !== '__name__') { if (label !== '__name__') {
labelStrings.push((formatHTML ? '<b>' : '') + label + (formatHTML ? '</b>' : '') + '="' + labels[label] + '"'); labelStrings.push(label + '="' + labels[label] + '"');
} }
} }
tsName += labelStrings.join(', ') + '}'; tsName += labelStrings.join(', ') + '}';

View file

@ -22,18 +22,14 @@ import TimeInput from './TimeInput';
interface PanelProps { interface PanelProps {
metricNames: string[]; metricNames: string[];
initialOptions?: PanelOptions | undefined;
removePanel: () => void; removePanel: () => void;
// TODO Put initial panel values here. // TODO Put initial panel values here.
} }
interface PanelState { interface PanelState {
expr: string; options: PanelOptions;
type: 'graph' | 'table'; data: any; // TODO: Type data.
range: number;
endTime: number | null;
resolution: number | null;
stacked: boolean;
data: any; // TODO: Define data.
lastQueryParams: { // TODO: Share these with Graph.tsx in a file. lastQueryParams: { // TODO: Share these with Graph.tsx in a file.
startTime: number, startTime: number,
endTime: number, endTime: number,
@ -44,6 +40,29 @@ interface PanelState {
stats: null; // TODO: Stats. stats: null; // TODO: Stats.
} }
export interface PanelOptions {
expr: string;
type: PanelType;
range: number; // Range in seconds.
endTime: number | null; // Timestamp in milliseconds.
resolution: number | null; // Resolution in seconds.
stacked: boolean;
}
export enum PanelType {
Graph = 'graph',
Table = 'table',
}
export const PanelDefaultOptions: PanelOptions = {
type: PanelType.Table,
expr: 'rate(node_cpu_seconds_total[5m])',
range: 3600,
endTime: null,
resolution: null,
stacked: false,
}
class Panel extends Component<PanelProps, PanelState> { class Panel extends Component<PanelProps, PanelState> {
private abortInFlightFetch: (() => void) | null = null; private abortInFlightFetch: (() => void) | null = null;
@ -51,12 +70,14 @@ class Panel extends Component<PanelProps, PanelState> {
super(props); super(props);
this.state = { this.state = {
expr: 'rate(node_cpu_seconds_total[1m])', options: this.props.initialOptions ? this.props.initialOptions : {
type: 'graph', expr: '',
range: 3600, type: PanelType.Table,
endTime: null, // This is in milliseconds. range: 3600,
resolution: null, endTime: null, // This is in milliseconds.
stacked: false, resolution: null,
stacked: false,
},
data: null, data: null,
lastQueryParams: null, lastQueryParams: null,
loading: false, loading: false,
@ -66,12 +87,14 @@ class Panel extends Component<PanelProps, PanelState> {
} }
componentDidUpdate(prevProps: PanelProps, prevState: PanelState) { componentDidUpdate(prevProps: PanelProps, prevState: PanelState) {
if (prevState.type !== this.state.type || const prevOpts = prevState.options;
prevState.range !== this.state.range || const opts = this.state.options;
prevState.endTime !== this.state.endTime || if (prevOpts.type !== opts.type ||
prevState.resolution !== this.state.resolution) { prevOpts.range !== opts.range ||
prevOpts.endTime !== opts.endTime ||
prevOpts.resolution !== opts.resolution) {
if (prevState.type !== this.state.type) { if (prevOpts.type !== opts.type) {
// If the other options change, we still want to show the old data until the new // If the other options change, we still want to show the old data until the new
// query completes, but this is not a good idea when we actually change between // query completes, but this is not a good idea when we actually change between
// table and graph view, since not all queries work well in both. // table and graph view, since not all queries work well in both.
@ -86,7 +109,7 @@ class Panel extends Component<PanelProps, PanelState> {
} }
executeQuery = (): void => { executeQuery = (): void => {
if (this.state.expr === '') { if (this.state.options.expr === '') {
return; return;
} }
@ -100,15 +123,15 @@ class Panel extends Component<PanelProps, PanelState> {
this.setState({loading: true}); this.setState({loading: true});
const endTime = this.getEndTime().valueOf() / 1000; // TODO: shouldn'T valueof only work when it's a moment? const endTime = this.getEndTime().valueOf() / 1000; // TODO: shouldn'T valueof only work when it's a moment?
const startTime = endTime - this.state.range; const startTime = endTime - this.state.options.range;
const resolution = this.state.resolution || Math.max(Math.floor(this.state.range / 250), 1); const resolution = this.state.options.resolution || Math.max(Math.floor(this.state.options.range / 250), 1);
const url = new URL('http://demo.robustperception.io:9090/');//window.location.href); const url = new URL('http://demo.robustperception.io:9090/');//window.location.href);
const params: {[key: string]: string} = { const params: {[key: string]: string} = {
'query': this.state.expr, 'query': this.state.options.expr,
}; };
switch (this.state.type) { switch (this.state.options.type) {
case 'graph': case 'graph':
url.pathname = '/api/v1/query_range' url.pathname = '/api/v1/query_range'
Object.assign(params, { Object.assign(params, {
@ -125,7 +148,7 @@ class Panel extends Component<PanelProps, PanelState> {
}) })
break; break;
default: default:
throw new Error('Invalid panel type "' + this.state.type + '"'); throw new Error('Invalid panel type "' + this.state.options.type + '"');
} }
Object.keys(params).forEach(key => url.searchParams.append(key, params[key])) Object.keys(params).forEach(key => url.searchParams.append(key, params[key]))
@ -160,35 +183,40 @@ class Panel extends Component<PanelProps, PanelState> {
}); });
} }
setOptions(opts: object): void {
const newOpts = Object.assign({}, this.state.options);
this.setState({options: Object.assign(newOpts, opts)});
}
handleExpressionChange = (expr: string): void => { handleExpressionChange = (expr: string): void => {
this.setState({expr: expr}); this.setOptions({expr: expr});
} }
handleChangeRange = (range: number): void => { handleChangeRange = (range: number): void => {
this.setState({range: range}); this.setOptions({range: range});
} }
getEndTime = (): number | moment.Moment => { getEndTime = (): number | moment.Moment => {
if (this.state.endTime === null) { if (this.state.options.endTime === null) {
return moment(); return moment();
} }
return this.state.endTime; return this.state.options.endTime;
} }
handleChangeEndTime = (endTime: number | null) => { handleChangeEndTime = (endTime: number | null) => {
this.setState({endTime: endTime}); this.setOptions({endTime: endTime});
} }
handleChangeResolution = (resolution: number) => { handleChangeResolution = (resolution: number) => {
// TODO: Where should we validate domain model constraints? In the parent's // TODO: Where should we validate domain model constraints? In the parent's
// change handler like here, or in the calling component? // change handler like here, or in the calling component?
if (resolution > 0) { if (resolution > 0) {
this.setState({resolution: resolution}); this.setOptions({resolution: resolution});
} }
} }
handleChangeStacking = (stacked: boolean) => { handleChangeStacking = (stacked: boolean) => {
this.setState({stacked: stacked}); this.setOptions({stacked: stacked});
} }
render() { render() {
@ -197,7 +225,7 @@ class Panel extends Component<PanelProps, PanelState> {
<Row> <Row>
<Col> <Col>
<ExpressionInput <ExpressionInput
value={this.state.expr} value={this.state.options.expr}
onChange={this.handleExpressionChange} onChange={this.handleExpressionChange}
executeQuery={this.executeQuery} executeQuery={this.executeQuery}
loading={this.state.loading} loading={this.state.loading}
@ -215,45 +243,45 @@ class Panel extends Component<PanelProps, PanelState> {
<Nav tabs> <Nav tabs>
<NavItem> <NavItem>
<NavLink <NavLink
className={this.state.type === 'graph' ? 'active' : ''} className={this.state.options.type === 'graph' ? 'active' : ''}
onClick={() => { this.setState({type: 'graph'}); }} onClick={() => { this.setOptions({type: 'graph'}); }}
> >
Graph Graph
</NavLink> </NavLink>
</NavItem> </NavItem>
<NavItem> <NavItem>
<NavLink <NavLink
className={this.state.type === 'table' ? 'active' : ''} className={this.state.options.type === 'table' ? 'active' : ''}
onClick={() => { this.setState({type: 'table'}); }} onClick={() => { this.setOptions({type: 'table'}); }}
> >
Table Table
</NavLink> </NavLink>
</NavItem> </NavItem>
</Nav> </Nav>
<TabContent activeTab={this.state.type}> <TabContent activeTab={this.state.options.type}>
<TabPane tabId="graph"> <TabPane tabId="graph">
{this.state.type === 'graph' && {this.state.options.type === 'graph' &&
<> <>
<GraphControls <GraphControls
range={this.state.range} range={this.state.options.range}
endTime={this.state.endTime} endTime={this.state.options.endTime}
resolution={this.state.resolution} resolution={this.state.options.resolution}
stacked={this.state.stacked} stacked={this.state.options.stacked}
onChangeRange={this.handleChangeRange} onChangeRange={this.handleChangeRange}
onChangeEndTime={this.handleChangeEndTime} onChangeEndTime={this.handleChangeEndTime}
onChangeResolution={this.handleChangeResolution} onChangeResolution={this.handleChangeResolution}
onChangeStacking={this.handleChangeStacking} onChangeStacking={this.handleChangeStacking}
/> />
<Graph data={this.state.data} stacked={this.state.stacked} queryParams={this.state.lastQueryParams} /> <Graph data={this.state.data} stacked={this.state.options.stacked} queryParams={this.state.lastQueryParams} />
</> </>
} }
</TabPane> </TabPane>
<TabPane tabId="table"> <TabPane tabId="table">
{this.state.type === 'table' && {this.state.options.type === 'table' &&
<> <>
<div className="table-controls"> <div className="table-controls">
<TimeInput endTime={this.state.endTime} range={this.state.range} onChangeEndTime={this.handleChangeEndTime} /> <TimeInput endTime={this.state.options.endTime} range={this.state.options.range} onChangeEndTime={this.handleChangeEndTime} />
</div> </div>
<DataTable data={this.state.data} /> <DataTable data={this.state.data} />
</> </>

View file

@ -2,15 +2,17 @@ import React, { Component } from 'react';
import { Alert, Button, Col, Row } from 'reactstrap'; import { Alert, Button, Col, Row } from 'reactstrap';
import Panel from './Panel'; import Panel, { PanelOptions, PanelType, PanelDefaultOptions } from './Panel';
import { getPanelOptionsFromQueryString } from './utils/urlParams';
interface PanelListState { interface PanelListState {
panels: { panels: {
key: string, key: string;
initialOptions?: PanelOptions;
}[], }[],
metricNames: string[], metricNames: string[];
fetchMetricsError: string | null, fetchMetricsError: string | null;
timeDriftError: string | null, timeDriftError: string | null;
} }
class PanelList extends Component<any, PanelListState> { class PanelList extends Component<any, PanelListState> {
@ -19,8 +21,20 @@ class PanelList extends Component<any, PanelListState> {
constructor(props: any) { constructor(props: any) {
super(props); super(props);
const urlPanels = getPanelOptionsFromQueryString(window.location.search).map((opts: PanelOptions) => {
return {
key: this.getKey(),
initialOptions: opts,
};
});
this.state = { this.state = {
panels: [], panels: urlPanels.length !== 0 ? urlPanels : [
{
key: this.getKey(),
initialOptions: PanelDefaultOptions,
},
],
metricNames: [], metricNames: [],
fetchMetricsError: null, fetchMetricsError: null,
timeDriftError: null, timeDriftError: null,
@ -30,8 +44,6 @@ class PanelList extends Component<any, PanelListState> {
} }
componentDidMount() { componentDidMount() {
this.addPanel();
fetch("http://demo.robustperception.io:9090/api/v1/label/__name__/values", {cache: "no-store"}) fetch("http://demo.robustperception.io:9090/api/v1/label/__name__/values", {cache: "no-store"})
.then(resp => { .then(resp => {
if (resp.ok) { if (resp.ok) {
@ -95,7 +107,12 @@ class PanelList extends Component<any, PanelListState> {
</Col> </Col>
</Row> </Row>
{this.state.panels.map(p => {this.state.panels.map(p =>
<Panel key={p.key} removePanel={() => this.removePanel(p.key)} metricNames={this.state.metricNames}/> <Panel
key={p.key}
initialOptions={p.initialOptions}
removePanel={() => this.removePanel(p.key)}
metricNames={this.state.metricNames}
/>
)} )}
<Button color="primary" className="add-panel-btn" onClick={this.addPanel}>Add Panel</Button> <Button color="primary" className="add-panel-btn" onClick={this.addPanel}>Add Panel</Button>
</> </>

70
src/SeriesName.tsx Normal file
View file

@ -0,0 +1,70 @@
import React, { PureComponent } from "react";
interface SeriesNameProps {
labels: {[key: string]: string} | null;
format: boolean;
}
class SeriesName extends PureComponent<SeriesNameProps> {
renderFormatted(): React.ReactNode {
const labels = this.props.labels!;
let labelNodes: React.ReactNode[] = [];
let first = true;
for (let label in labels) {
if (label === '__name__') {
continue;
}
labelNodes.push(
<span key={label}>
{!first && ', '}
<span className="legend-label-name">{label}</span>=
<span className="legend-label-value">"{labels[label]}"</span>
</span>
);
if (first) {
first = false;
}
}
return (
<>
<span className="legend-metric-name">{labels.__name__ || ''}</span>
<span className="legend-label-brace">{'{'}</span>
{labelNodes}
<span className="legend-label-brace">{'}'}</span>
</>
);
}
renderPlain() {
const labels = this.props.labels!;
let tsName = (labels.__name__ || '') + '{';
let labelStrings: string[] = [];
for (let label in labels) {
if (label !== '__name__') {
labelStrings.push(label + '="' + labels[label] + '"');
}
}
tsName += labelStrings.join(', ') + '}';
return tsName;
}
render() {
if (this.props.labels === null) {
return 'scalar';
}
if (this.props.format) {
return this.renderFormatted();
}
// Return a simple text node. This is much faster to scroll through
// for longer lists (hundreds of items).
return this.renderPlain();
}
}
export default SeriesName;

View file

@ -76,7 +76,7 @@ class TimeInput extends Component<TimeInputProps> {
showToday: true, showToday: true,
}, },
sideBySide: true, sideBySide: true,
format: 'YYYY-MM-DD HH:mm:ss', format: 'YYYY-MM-DD HH:MM',
locale: 'en', locale: 'en',
timeZone: 'UTC', timeZone: 'UTC',
defaultDate: this.props.endTime, defaultDate: this.props.endTime,

38
src/utils/timeFormat.ts Normal file
View file

@ -0,0 +1,38 @@
import moment from 'moment-timezone';
const rangeUnits: {[unit: string]: number} = {
'y': 60 * 60 * 24 * 365,
'w': 60 * 60 * 24 * 7,
'd': 60 * 60 * 24,
'h': 60 * 60,
'm': 60,
's': 1
}
export function parseRange(rangeText: string): number | null {
const rangeRE = new RegExp('^([0-9]+)([ywdhms]+)$');
const matches = rangeText.match(rangeRE);
if (!matches || matches.length !== 3) {
return null;
}
const value = parseInt(matches[1]);
const unit = matches[2];
return value * rangeUnits[unit];
}
export function formatRange(range: number): string {
for (let unit of Object.keys(rangeUnits)) {
if (range % rangeUnits[unit] === 0) {
return (range / rangeUnits[unit]) + unit;
}
}
return range + 's';
}
export function parseTime(timeText: string): number {
return moment.utc(timeText).valueOf();
}
export function formatTime(time: number): string {
return moment.utc(time).format('YYYY-MM-DD HH:mm');
}

100
src/utils/urlParams.ts Normal file
View file

@ -0,0 +1,100 @@
import { parseRange, parseTime } from './timeFormat';
import { PanelOptions, PanelType, PanelDefaultOptions } from '../Panel';
export function getPanelOptionsFromQueryString(query: string): PanelOptions[] {
if (query === '') {
return [];
}
const params = query.substring(1).split('&');
return parseParams(params);
}
const paramFormat = /^g\d+\..+=.+$/;
interface IncompletePanelOptions {
expr?: string;
type?: PanelType;
range?: number;
endTime?: number | null;
resolution?: number | null;
stacked?: boolean;
}
function parseParams(params: string[]): PanelOptions[] {
const sortedParams = params.filter((p) => {
return paramFormat.test(p);
}).sort();
let panelOpts: PanelOptions[] = [];
let key = 0;
let options: IncompletePanelOptions = {};
for (const p of sortedParams) {
const prefix = 'g' + key + '.';
if (!p.startsWith(prefix)) {
panelOpts.push({
...PanelDefaultOptions,
...options,
});
options = {};
key++;
}
addParam(options, p.substring(prefix.length));
}
panelOpts.push({
...PanelDefaultOptions,
...options,
});
return panelOpts;
}
function addParam(opts: IncompletePanelOptions, param: string): void {
let [ opt, val ] = param.split('=');
val = decodeURIComponent(val.replace(/\+/g, ' '));
console.log(val);
switch(opt) {
case 'expr':
console.log(val);
opts.expr = val;
break;
case 'tab':
if (val === '0') {
opts.type = PanelType.Graph;
} else {
opts.type = PanelType.Table;
}
break;
case 'stacked':
opts.stacked = val === '1';
break;
case 'range_input':
const range = parseRange(val);
if (range !== null) {
opts.range = range;
}
break;
case 'end_input':
opts.endTime = parseTime(val);
break;
case 'step_input':
const res = parseInt(val)
if (res > 0) {
opts.resolution = res;
}
break;
case 'moment_input':
opts.endTime = parseTime(val);
break;
}
}