mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
Add URL params support and much more
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
b2b4d3a18d
commit
d9cc501dff
13
src/App.css
13
src/App.css
|
@ -11,7 +11,7 @@ body {
|
|||
}
|
||||
|
||||
.expression-input textarea {
|
||||
/* font-family: 'Courier New', Courier, monospace; */
|
||||
font-family: Menlo,Monaco,Consolas,'Courier New',monospace;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,7 @@ button.execute-btn {
|
|||
}
|
||||
|
||||
div.endtime-input {
|
||||
width: 270px !important;
|
||||
width: 240px !important;
|
||||
}
|
||||
|
||||
.table-controls input {
|
||||
|
@ -128,6 +128,14 @@ div.endtime-input {
|
|||
margin: 2px 8px 2px 0;
|
||||
}
|
||||
|
||||
.legend-metric-name {
|
||||
margin-right: 1px;
|
||||
}
|
||||
|
||||
.legend-label-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.graph {
|
||||
margin: 0 5px 0 5px;
|
||||
}
|
||||
|
@ -168,6 +176,5 @@ div.endtime-input {
|
|||
}
|
||||
|
||||
.add-panel-btn {
|
||||
margin-top: -20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
|
|
@ -2,8 +2,7 @@ import React, { PureComponent, ReactNode } from 'react';
|
|||
|
||||
import { Alert, Table } from 'reactstrap';
|
||||
|
||||
import metricToSeriesName from './MetricFomat';
|
||||
import { ReactComponent } from '*.svg';
|
||||
import SeriesName from './SeriesName';
|
||||
|
||||
export interface QueryResult {
|
||||
data: null | {
|
||||
|
@ -48,6 +47,7 @@ class DataTable extends PureComponent<QueryResult> {
|
|||
}
|
||||
|
||||
render() {
|
||||
console.log("RENDER!");
|
||||
const data = this.props.data;
|
||||
|
||||
if (data === null) {
|
||||
|
@ -64,7 +64,7 @@ class DataTable extends PureComponent<QueryResult> {
|
|||
case 'vector':
|
||||
rows = (this.limitSeries(data.result) as InstantSample[])
|
||||
.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;
|
||||
break;
|
||||
|
@ -74,7 +74,7 @@ class DataTable extends PureComponent<QueryResult> {
|
|||
const valueText = s.values.map((v) => {
|
||||
return [1] + ' @' + v[0];
|
||||
}).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;
|
||||
break;
|
||||
|
|
|
@ -37,8 +37,15 @@ class ExpressionInput extends Component<ExpressionInputProps> {
|
|||
}
|
||||
|
||||
renderAutosuggest = (downshift: any) => {
|
||||
if (!downshift.isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.prevNoMatchValue && downshift.inputValue.includes(this.prevNoMatchValue)) {
|
||||
// TODO: Is this still correct with fuzzy?
|
||||
if (downshift.isOpen) {
|
||||
downshift.closeMenu();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -52,10 +59,6 @@ class ExpressionInput extends Component<ExpressionInputProps> {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (!downshift.isOpen) {
|
||||
return null; // TODO CHECK NEED FOR THIS
|
||||
}
|
||||
|
||||
return (
|
||||
<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.
|
||||
(event.nativeEvent as any).preventDownshiftDefault = true;
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'ArrowDown':
|
||||
if (!downshift.isOpen) {
|
||||
(event.nativeEvent as any).preventDownshiftDefault = true;
|
||||
}
|
||||
break;
|
||||
case 'Enter':
|
||||
downshift.closeMenu();
|
||||
break;
|
||||
case 'Escape':
|
||||
if (!downshift.isOpen) {
|
||||
this.exprInputRef.current!.blur();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,10 @@ class Graph extends PureComponent<GraphProps> {
|
|||
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);
|
||||
if (abs_y >= 1e24) {
|
||||
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() {
|
||||
let colors = [];
|
||||
const colorPool = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"];
|
||||
|
@ -191,13 +195,17 @@ class Graph extends PureComponent<GraphProps> {
|
|||
let data = [];
|
||||
let pos = 0;
|
||||
const params = this.props.queryParams!;
|
||||
|
||||
for (let t = params.startTime; t <= params.endTime; t += params.resolution) {
|
||||
// Allow for floating point inaccuracy.
|
||||
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])]);
|
||||
pos++;
|
||||
} 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 : {},
|
||||
data: data,
|
||||
color: colors[index],
|
||||
index: index,
|
||||
};
|
||||
})
|
||||
}
|
||||
|
@ -214,7 +223,11 @@ class Graph extends PureComponent<GraphProps> {
|
|||
if (isNaN(val)) {
|
||||
// "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). They
|
||||
// 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;
|
||||
};
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import TimeInput from './TimeInput';
|
||||
import { parseRange, formatRange } from './utils/timeFormat';
|
||||
|
||||
library.add(
|
||||
faPlus,
|
||||
|
@ -42,15 +43,6 @@ class GraphControls extends Component<GraphControlsProps> {
|
|||
private rangeRef = 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 = [
|
||||
1,
|
||||
10,
|
||||
|
@ -72,28 +64,8 @@ class GraphControls extends Component<GraphControlsProps> {
|
|||
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 => {
|
||||
const range = this.parseRange(rangeText);
|
||||
const range = parseRange(rangeText);
|
||||
if (range === null) {
|
||||
this.changeRangeInput(this.props.range);
|
||||
} else {
|
||||
|
@ -102,7 +74,7 @@ class GraphControls extends Component<GraphControlsProps> {
|
|||
}
|
||||
|
||||
changeRangeInput = (range: number): void => {
|
||||
this.rangeRef.current!.value = this.formatRange(range);
|
||||
this.rangeRef.current!.value = formatRange(range);
|
||||
}
|
||||
|
||||
increaseRange = (): void => {
|
||||
|
@ -134,7 +106,7 @@ class GraphControls extends Component<GraphControlsProps> {
|
|||
</InputGroupAddon>
|
||||
|
||||
<Input
|
||||
defaultValue={this.formatRange(this.props.range)}
|
||||
defaultValue={formatRange(this.props.range)}
|
||||
innerRef={this.rangeRef}
|
||||
onBlur={() => this.onChangeRangeInput(this.rangeRef.current!.value)}
|
||||
/>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
|
||||
import metricToSeriesName from './MetricFomat';
|
||||
import SeriesName from './SeriesName';
|
||||
|
||||
interface LegendProps {
|
||||
series: any; // TODO: Type this.
|
||||
|
@ -8,13 +8,14 @@ interface LegendProps {
|
|||
|
||||
class Legend extends PureComponent<LegendProps> {
|
||||
renderLegendItem(s: any) {
|
||||
const seriesName = metricToSeriesName(s.labels, false);
|
||||
return (
|
||||
<tr key={seriesName} className="legend-item">
|
||||
<tr key={s.index} className="legend-item">
|
||||
<td>
|
||||
<div className="legend-swatch" style={{backgroundColor: s.color}}></div>
|
||||
</td>
|
||||
<td>{seriesName}</td>
|
||||
<td>
|
||||
<SeriesName labels={s.labels} format={true} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
function metricToSeriesName(labels: {[key: string]: string}, formatHTML: boolean): string {
|
||||
function metricToSeriesName(labels: {[key: string]: string}): string {
|
||||
if (labels === null) {
|
||||
return 'scalar';
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ function metricToSeriesName(labels: {[key: string]: string}, formatHTML: boolean
|
|||
let labelStrings: string[] = [];
|
||||
for (let label in labels) {
|
||||
if (label !== '__name__') {
|
||||
labelStrings.push((formatHTML ? '<b>' : '') + label + (formatHTML ? '</b>' : '') + '="' + labels[label] + '"');
|
||||
labelStrings.push(label + '="' + labels[label] + '"');
|
||||
}
|
||||
}
|
||||
tsName += labelStrings.join(', ') + '}';
|
||||
|
|
110
src/Panel.tsx
110
src/Panel.tsx
|
@ -22,18 +22,14 @@ import TimeInput from './TimeInput';
|
|||
|
||||
interface PanelProps {
|
||||
metricNames: string[];
|
||||
initialOptions?: PanelOptions | undefined;
|
||||
removePanel: () => void;
|
||||
// TODO Put initial panel values here.
|
||||
}
|
||||
|
||||
interface PanelState {
|
||||
expr: string;
|
||||
type: 'graph' | 'table';
|
||||
range: number;
|
||||
endTime: number | null;
|
||||
resolution: number | null;
|
||||
stacked: boolean;
|
||||
data: any; // TODO: Define data.
|
||||
options: PanelOptions;
|
||||
data: any; // TODO: Type data.
|
||||
lastQueryParams: { // TODO: Share these with Graph.tsx in a file.
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
|
@ -44,6 +40,29 @@ interface PanelState {
|
|||
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> {
|
||||
private abortInFlightFetch: (() => void) | null = null;
|
||||
|
||||
|
@ -51,12 +70,14 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
expr: 'rate(node_cpu_seconds_total[1m])',
|
||||
type: 'graph',
|
||||
options: this.props.initialOptions ? this.props.initialOptions : {
|
||||
expr: '',
|
||||
type: PanelType.Table,
|
||||
range: 3600,
|
||||
endTime: null, // This is in milliseconds.
|
||||
resolution: null,
|
||||
stacked: false,
|
||||
},
|
||||
data: null,
|
||||
lastQueryParams: null,
|
||||
loading: false,
|
||||
|
@ -66,12 +87,14 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps: PanelProps, prevState: PanelState) {
|
||||
if (prevState.type !== this.state.type ||
|
||||
prevState.range !== this.state.range ||
|
||||
prevState.endTime !== this.state.endTime ||
|
||||
prevState.resolution !== this.state.resolution) {
|
||||
const prevOpts = prevState.options;
|
||||
const opts = this.state.options;
|
||||
if (prevOpts.type !== opts.type ||
|
||||
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
|
||||
// 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.
|
||||
|
@ -86,7 +109,7 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
}
|
||||
|
||||
executeQuery = (): void => {
|
||||
if (this.state.expr === '') {
|
||||
if (this.state.options.expr === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -100,15 +123,15 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
this.setState({loading: true});
|
||||
|
||||
const endTime = this.getEndTime().valueOf() / 1000; // TODO: shouldn'T valueof only work when it's a moment?
|
||||
const startTime = endTime - this.state.range;
|
||||
const resolution = this.state.resolution || Math.max(Math.floor(this.state.range / 250), 1);
|
||||
const startTime = endTime - this.state.options.range;
|
||||
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 params: {[key: string]: string} = {
|
||||
'query': this.state.expr,
|
||||
'query': this.state.options.expr,
|
||||
};
|
||||
|
||||
switch (this.state.type) {
|
||||
switch (this.state.options.type) {
|
||||
case 'graph':
|
||||
url.pathname = '/api/v1/query_range'
|
||||
Object.assign(params, {
|
||||
|
@ -125,7 +148,7 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
})
|
||||
break;
|
||||
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]))
|
||||
|
||||
|
@ -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 => {
|
||||
this.setState({expr: expr});
|
||||
this.setOptions({expr: expr});
|
||||
}
|
||||
|
||||
handleChangeRange = (range: number): void => {
|
||||
this.setState({range: range});
|
||||
this.setOptions({range: range});
|
||||
}
|
||||
|
||||
getEndTime = (): number | moment.Moment => {
|
||||
if (this.state.endTime === null) {
|
||||
if (this.state.options.endTime === null) {
|
||||
return moment();
|
||||
}
|
||||
return this.state.endTime;
|
||||
return this.state.options.endTime;
|
||||
}
|
||||
|
||||
handleChangeEndTime = (endTime: number | null) => {
|
||||
this.setState({endTime: endTime});
|
||||
this.setOptions({endTime: endTime});
|
||||
}
|
||||
|
||||
handleChangeResolution = (resolution: number) => {
|
||||
// TODO: Where should we validate domain model constraints? In the parent's
|
||||
// change handler like here, or in the calling component?
|
||||
if (resolution > 0) {
|
||||
this.setState({resolution: resolution});
|
||||
this.setOptions({resolution: resolution});
|
||||
}
|
||||
}
|
||||
|
||||
handleChangeStacking = (stacked: boolean) => {
|
||||
this.setState({stacked: stacked});
|
||||
this.setOptions({stacked: stacked});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -197,7 +225,7 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
<Row>
|
||||
<Col>
|
||||
<ExpressionInput
|
||||
value={this.state.expr}
|
||||
value={this.state.options.expr}
|
||||
onChange={this.handleExpressionChange}
|
||||
executeQuery={this.executeQuery}
|
||||
loading={this.state.loading}
|
||||
|
@ -215,45 +243,45 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
<Nav tabs>
|
||||
<NavItem>
|
||||
<NavLink
|
||||
className={this.state.type === 'graph' ? 'active' : ''}
|
||||
onClick={() => { this.setState({type: 'graph'}); }}
|
||||
className={this.state.options.type === 'graph' ? 'active' : ''}
|
||||
onClick={() => { this.setOptions({type: 'graph'}); }}
|
||||
>
|
||||
Graph
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink
|
||||
className={this.state.type === 'table' ? 'active' : ''}
|
||||
onClick={() => { this.setState({type: 'table'}); }}
|
||||
className={this.state.options.type === 'table' ? 'active' : ''}
|
||||
onClick={() => { this.setOptions({type: 'table'}); }}
|
||||
>
|
||||
Table
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
<TabContent activeTab={this.state.type}>
|
||||
<TabContent activeTab={this.state.options.type}>
|
||||
<TabPane tabId="graph">
|
||||
{this.state.type === 'graph' &&
|
||||
{this.state.options.type === 'graph' &&
|
||||
<>
|
||||
<GraphControls
|
||||
range={this.state.range}
|
||||
endTime={this.state.endTime}
|
||||
resolution={this.state.resolution}
|
||||
stacked={this.state.stacked}
|
||||
range={this.state.options.range}
|
||||
endTime={this.state.options.endTime}
|
||||
resolution={this.state.options.resolution}
|
||||
stacked={this.state.options.stacked}
|
||||
|
||||
onChangeRange={this.handleChangeRange}
|
||||
onChangeEndTime={this.handleChangeEndTime}
|
||||
onChangeResolution={this.handleChangeResolution}
|
||||
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 tabId="table">
|
||||
{this.state.type === 'table' &&
|
||||
{this.state.options.type === 'table' &&
|
||||
<>
|
||||
<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>
|
||||
<DataTable data={this.state.data} />
|
||||
</>
|
||||
|
|
|
@ -2,15 +2,17 @@ import React, { Component } from 'react';
|
|||
|
||||
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 {
|
||||
panels: {
|
||||
key: string,
|
||||
key: string;
|
||||
initialOptions?: PanelOptions;
|
||||
}[],
|
||||
metricNames: string[],
|
||||
fetchMetricsError: string | null,
|
||||
timeDriftError: string | null,
|
||||
metricNames: string[];
|
||||
fetchMetricsError: string | null;
|
||||
timeDriftError: string | null;
|
||||
}
|
||||
|
||||
class PanelList extends Component<any, PanelListState> {
|
||||
|
@ -19,8 +21,20 @@ class PanelList extends Component<any, PanelListState> {
|
|||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
const urlPanels = getPanelOptionsFromQueryString(window.location.search).map((opts: PanelOptions) => {
|
||||
return {
|
||||
key: this.getKey(),
|
||||
initialOptions: opts,
|
||||
};
|
||||
});
|
||||
|
||||
this.state = {
|
||||
panels: [],
|
||||
panels: urlPanels.length !== 0 ? urlPanels : [
|
||||
{
|
||||
key: this.getKey(),
|
||||
initialOptions: PanelDefaultOptions,
|
||||
},
|
||||
],
|
||||
metricNames: [],
|
||||
fetchMetricsError: null,
|
||||
timeDriftError: null,
|
||||
|
@ -30,8 +44,6 @@ class PanelList extends Component<any, PanelListState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.addPanel();
|
||||
|
||||
fetch("http://demo.robustperception.io:9090/api/v1/label/__name__/values", {cache: "no-store"})
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
|
@ -95,7 +107,12 @@ class PanelList extends Component<any, PanelListState> {
|
|||
</Col>
|
||||
</Row>
|
||||
{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>
|
||||
</>
|
||||
|
|
70
src/SeriesName.tsx
Normal file
70
src/SeriesName.tsx
Normal 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;
|
|
@ -76,7 +76,7 @@ class TimeInput extends Component<TimeInputProps> {
|
|||
showToday: true,
|
||||
},
|
||||
sideBySide: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
format: 'YYYY-MM-DD HH:MM',
|
||||
locale: 'en',
|
||||
timeZone: 'UTC',
|
||||
defaultDate: this.props.endTime,
|
||||
|
|
38
src/utils/timeFormat.ts
Normal file
38
src/utils/timeFormat.ts
Normal 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
100
src/utils/urlParams.ts
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue