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 {
|
.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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(', ') + '}';
|
||||||
|
|
110
src/Panel.tsx
110
src/Panel.tsx
|
@ -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: '',
|
||||||
|
type: PanelType.Table,
|
||||||
range: 3600,
|
range: 3600,
|
||||||
endTime: null, // This is in milliseconds.
|
endTime: null, // This is in milliseconds.
|
||||||
resolution: null,
|
resolution: null,
|
||||||
stacked: false,
|
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} />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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
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,
|
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
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