mirror of
https://github.com/prometheus/prometheus.git
synced 2024-12-25 05:34:05 -08:00
WIP: React UI Linting rules (#6206)
* Initial react-ui linting rules Signed-off-by: cstdev <pietomb00@hotmail.com> * Add react linting to build process Move eslint config to its own file to keep package.json clearer. Signed-off-by: cstdev <pietomb00@hotmail.com> * Linting changes from master Signed-off-by: cstdev <pietomb00@hotmail.com> * Move CI linting to makefile and travis Also add trailing comma to multiline imports. Signed-off-by: cstdev <pietomb00@hotmail.com> * Add lint fix target to makefile Signed-off-by: cstdev <pietomb00@hotmail.com> * Lint latest master Signed-off-by: cstdev <pietomb00@hotmail.com>
This commit is contained in:
parent
e8027ba515
commit
3b39f6ae45
12
Makefile
12
Makefile
|
@ -47,8 +47,18 @@ assets: $(REACT_APP_OUTPUT_DIR)
|
|||
cd web/ui && GO111MODULE=$(GO111MODULE) GOOS= GOARCH= $(GO) generate -x -v $(GOOPTS)
|
||||
@$(GOFMT) -w ./web/ui
|
||||
|
||||
.PHONY: react-app-lint
|
||||
react-app-lint:
|
||||
@echo ">> running React app linting"
|
||||
cd $(REACT_APP_PATH) && yarn lint:ci
|
||||
|
||||
.PHONY: react-app-lint-fix
|
||||
react-app-lint-fix:
|
||||
@echo ">> running React app linting and fixing errors where possibe"
|
||||
cd $(REACT_APP_PATH) && yarn lint
|
||||
|
||||
.PHONY: react-app-test
|
||||
react-app-test: $(REACT_APP_NODE_MODULES_PATH)
|
||||
react-app-test: | $(REACT_APP_NODE_MODULES_PATH) react-app-lint
|
||||
@echo ">> running React app tests"
|
||||
cd $(REACT_APP_PATH) && yarn test --no-watch
|
||||
|
||||
|
|
31
web/ui/react-app/.eslintrc.json
Normal file
31
web/ui/react-app/.eslintrc.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": [
|
||||
"react-app",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/camelcase": "warn",
|
||||
"eol-last": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"prefer-const": "warn",
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
{
|
||||
"arrays": "always-multiline",
|
||||
"objects": "always-multiline",
|
||||
"imports": "always-multiline"
|
||||
}
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
"prettier"
|
||||
]
|
||||
}
|
|
@ -42,10 +42,14 @@
|
|||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"eject": "react-scripts eject",
|
||||
"lint:ci": "eslint --quiet \"src/**/*.{ts,tsx}\"",
|
||||
"lint": "eslint --fix \"src/**/*.{ts,tsx}\""
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
"prettier": {
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 125
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
|
@ -56,7 +60,20 @@
|
|||
"devDependencies": {
|
||||
"@types/flot": "0.0.31",
|
||||
"@types/moment-timezone": "^0.5.10",
|
||||
"@types/reactstrap": "^8.0.5"
|
||||
"@types/reactstrap": "^8.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "2.x",
|
||||
"@typescript-eslint/parser": "2.x",
|
||||
"babel-eslint": "10.x",
|
||||
"eslint": "6.x",
|
||||
"eslint-config-prettier": "^6.4.0",
|
||||
"eslint-config-react-app": "^5.0.2",
|
||||
"eslint-plugin-flowtype": "3.x",
|
||||
"eslint-plugin-import": "2.x",
|
||||
"eslint-plugin-jsx-a11y": "6.x",
|
||||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"eslint-plugin-react": "7.x",
|
||||
"eslint-plugin-react-hooks": "1.x",
|
||||
"prettier": "^1.18.2"
|
||||
},
|
||||
"proxy": "http://localhost:9090"
|
||||
}
|
||||
|
|
|
@ -1,19 +1,10 @@
|
|||
import React, { Component } from 'react';
|
||||
import Navigation from "./Navbar";
|
||||
import Navigation from './Navbar';
|
||||
import { Container } from 'reactstrap';
|
||||
|
||||
import './App.css';
|
||||
import { Router } from '@reach/router';
|
||||
import {
|
||||
Alerts,
|
||||
Config,
|
||||
Flags,
|
||||
Rules,
|
||||
Services,
|
||||
Status,
|
||||
Targets,
|
||||
PanelList,
|
||||
} from './pages';
|
||||
import { Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList } from './pages';
|
||||
|
||||
class App extends Component {
|
||||
render() {
|
||||
|
|
|
@ -13,7 +13,7 @@ const Checkbox: FC<CheckboxProps> = ({ children, wrapperStyles, id, ...rest }) =
|
|||
{children}
|
||||
</Label>
|
||||
</FormGroup>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Checkbox);
|
||||
|
|
|
@ -5,33 +5,38 @@ import { Alert, Table } from 'reactstrap';
|
|||
import SeriesName from './SeriesName';
|
||||
|
||||
export interface QueryResult {
|
||||
data: null | {
|
||||
resultType: 'vector',
|
||||
result: InstantSample[],
|
||||
} | {
|
||||
resultType: 'matrix',
|
||||
result: RangeSamples[],
|
||||
} | {
|
||||
resultType: 'scalar',
|
||||
result: SampleValue,
|
||||
} | {
|
||||
resultType: 'string',
|
||||
result: string,
|
||||
},
|
||||
};
|
||||
data:
|
||||
| null
|
||||
| {
|
||||
resultType: 'vector';
|
||||
result: InstantSample[];
|
||||
}
|
||||
| {
|
||||
resultType: 'matrix';
|
||||
result: RangeSamples[];
|
||||
}
|
||||
| {
|
||||
resultType: 'scalar';
|
||||
result: SampleValue;
|
||||
}
|
||||
| {
|
||||
resultType: 'string';
|
||||
result: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface InstantSample {
|
||||
metric: Metric,
|
||||
value: SampleValue,
|
||||
metric: Metric;
|
||||
value: SampleValue;
|
||||
}
|
||||
|
||||
interface RangeSamples {
|
||||
metric: Metric,
|
||||
values: SampleValue[],
|
||||
metric: Metric;
|
||||
values: SampleValue[];
|
||||
}
|
||||
|
||||
interface Metric {
|
||||
[key: string]: string,
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
type SampleValue = [number, string];
|
||||
|
@ -59,29 +64,55 @@ class DataTable extends PureComponent<QueryResult> {
|
|||
|
||||
let rows: ReactNode[] = [];
|
||||
let limited = false;
|
||||
switch(data.resultType) {
|
||||
switch (data.resultType) {
|
||||
case 'vector':
|
||||
rows = (this.limitSeries(data.result) as InstantSample[])
|
||||
.map((s: InstantSample, index: number): ReactNode => {
|
||||
return <tr key={index}><td><SeriesName labels={s.metric} format={false}/></td><td>{s.value[1]}</td></tr>;
|
||||
});
|
||||
rows = (this.limitSeries(data.result) as InstantSample[]).map(
|
||||
(s: InstantSample, index: number): ReactNode => {
|
||||
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;
|
||||
case 'matrix':
|
||||
rows = (this.limitSeries(data.result) as RangeSamples[])
|
||||
.map((s, index) => {
|
||||
const valueText = s.values.map((v) => {
|
||||
rows = (this.limitSeries(data.result) as RangeSamples[]).map((s, index) => {
|
||||
const valueText = s.values
|
||||
.map(v => {
|
||||
return [1] + ' @' + v[0];
|
||||
}).join('\n');
|
||||
return <tr style={{whiteSpace: 'pre'}} key={index}><td><SeriesName labels={s.metric} format={false}/></td><td>{valueText}</td></tr>;
|
||||
})
|
||||
.join('\n');
|
||||
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;
|
||||
case 'scalar':
|
||||
rows.push(<tr><td>scalar</td><td>{data.result[1]}</td></tr>);
|
||||
rows.push(
|
||||
<tr>
|
||||
<td>scalar</td>
|
||||
<td>{data.result[1]}</td>
|
||||
</tr>
|
||||
);
|
||||
break;
|
||||
case 'string':
|
||||
rows.push(<tr><td>scalar</td><td>{data.result[1]}</td></tr>);
|
||||
rows.push(
|
||||
<tr>
|
||||
<td>scalar</td>
|
||||
<td>{data.result[1]}</td>
|
||||
</tr>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return <Alert color="danger">Unsupported result value type</Alert>;
|
||||
|
@ -89,15 +120,13 @@ class DataTable extends PureComponent<QueryResult> {
|
|||
|
||||
return (
|
||||
<>
|
||||
{limited &&
|
||||
{limited && (
|
||||
<Alert color="danger">
|
||||
<strong>Warning:</strong> Fetched {data.result.length} metrics, only displaying first {rows.length}.
|
||||
</Alert>
|
||||
}
|
||||
)}
|
||||
<Table hover size="sm" className="data-table">
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
<tbody>{rows}</tbody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -37,8 +37,8 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
|||
super(props);
|
||||
this.state = {
|
||||
value: props.value,
|
||||
height: 'auto'
|
||||
}
|
||||
height: 'auto',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -49,14 +49,17 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
|||
const { offsetHeight, clientHeight, scrollHeight } = this.exprInputRef.current!;
|
||||
const offset = offsetHeight - clientHeight; // Needed in order for the height to be more accurate.
|
||||
this.setState({ height: scrollHeight + offset });
|
||||
}
|
||||
};
|
||||
|
||||
handleInput = () => {
|
||||
this.setState({
|
||||
this.setState(
|
||||
{
|
||||
height: 'auto',
|
||||
value: this.exprInputRef.current!.value
|
||||
}, this.setHeight);
|
||||
}
|
||||
value: this.exprInputRef.current!.value,
|
||||
},
|
||||
this.setHeight
|
||||
);
|
||||
};
|
||||
|
||||
handleDropdownSelection = (value: string) => {
|
||||
this.setState({ value, height: 'auto' }, this.setHeight);
|
||||
|
@ -67,30 +70,32 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
|||
this.props.executeQuery(this.exprInputRef.current!.value);
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
executeQuery = () => this.props.executeQuery(this.exprInputRef.current!.value);
|
||||
|
||||
getSearchMatches = (input: string, expressions: string[]) => {
|
||||
return fuzzy.filter(input.replace(/ /g, ''), expressions, {
|
||||
pre: "<strong>",
|
||||
post: "</strong>",
|
||||
pre: '<strong>',
|
||||
post: '</strong>',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
createAutocompleteSection = (downshift: ControllerStateAndHelpers<any>) => {
|
||||
const { inputValue = '', closeMenu, highlightedIndex } = downshift;
|
||||
const { autocompleteSections } = this.props;
|
||||
let index = 0;
|
||||
const sections = inputValue!.length ?
|
||||
Object.entries(autocompleteSections).reduce((acc, [title, items]) => {
|
||||
const sections = inputValue!.length
|
||||
? Object.entries(autocompleteSections).reduce(
|
||||
(acc, [title, items]) => {
|
||||
const matches = this.getSearchMatches(inputValue!, items);
|
||||
return !matches.length ? acc : [
|
||||
return !matches.length
|
||||
? acc
|
||||
: [
|
||||
...acc,
|
||||
<Card tag={ListGroup} key={title}>
|
||||
<CardHeader style={{ fontSize: 13 }}>{title}</CardHeader>
|
||||
{
|
||||
matches
|
||||
{matches
|
||||
.slice(0, 100) // Limit DOM rendering to 100 results, as DOM rendering is sloooow.
|
||||
.map(({ original, string }) => {
|
||||
const itemProps = downshift.getItemProps({
|
||||
|
@ -100,17 +105,19 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
|||
style: {
|
||||
backgroundColor: highlightedIndex === index++ ? 'lightgray' : 'white',
|
||||
},
|
||||
})
|
||||
});
|
||||
return (
|
||||
<SanitizeHTML tag={ListGroupItem} {...itemProps} allowedTags={['strong']}>
|
||||
{string}
|
||||
</SanitizeHTML>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Card>
|
||||
]
|
||||
}, [] as JSX.Element[]) : []
|
||||
})}
|
||||
</Card>,
|
||||
];
|
||||
},
|
||||
[] as JSX.Element[]
|
||||
)
|
||||
: [];
|
||||
|
||||
if (!sections.length) {
|
||||
// This is ugly but is needed in order to sync state updates.
|
||||
|
@ -121,19 +128,16 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
|||
|
||||
return (
|
||||
<div {...downshift.getMenuProps()} className="autosuggest-dropdown">
|
||||
{ sections }
|
||||
{sections}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value, height } = this.state;
|
||||
return (
|
||||
<Downshift
|
||||
inputValue={value}
|
||||
onSelect={this.handleDropdownSelection}
|
||||
>
|
||||
{(downshift) => (
|
||||
<Downshift inputValue={value} onSelect={this.handleDropdownSelection}>
|
||||
{downshift => (
|
||||
<div>
|
||||
<InputGroup className="expression-input">
|
||||
<InputGroupAddon addonType="prepend">
|
||||
|
@ -175,15 +179,11 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
|||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
},
|
||||
} as any)}
|
||||
/>
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button
|
||||
className="execute-btn"
|
||||
color="primary"
|
||||
onClick={this.executeQuery}
|
||||
>
|
||||
<Button className="execute-btn" color="primary" onClick={this.executeQuery}>
|
||||
Execute
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
|
|
|
@ -12,7 +12,7 @@ require('flot/source/jquery.flot.time');
|
|||
require('flot/source/jquery.canvaswrapper');
|
||||
require('jquery.flot.tooltip');
|
||||
|
||||
var graphID = 0;
|
||||
let graphID = 0;
|
||||
function getGraphID() {
|
||||
// TODO: This is ugly.
|
||||
return graphID++;
|
||||
|
@ -22,9 +22,9 @@ interface GraphProps {
|
|||
data: any; // TODO: Type this.
|
||||
stacked: boolean;
|
||||
queryParams: {
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
resolution: number,
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
resolution: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
@ -33,76 +33,76 @@ class Graph extends PureComponent<GraphProps> {
|
|||
private chartRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
escapeHTML(str: string) {
|
||||
var entityMap: {[key: string]: string} = {
|
||||
const entityMap: { [key: string]: string } = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/'
|
||||
'/': '/',
|
||||
};
|
||||
|
||||
return String(str).replace(/[&<>"'/]/g, function (s) {
|
||||
return String(str).replace(/[&<>"'/]/g, function(s) {
|
||||
return entityMap[s];
|
||||
});
|
||||
}
|
||||
|
||||
renderLabels(labels: {[key: string]: string}) {
|
||||
let labelStrings: string[] = [];
|
||||
for (let label in labels) {
|
||||
renderLabels(labels: { [key: string]: string }) {
|
||||
const labelStrings: string[] = [];
|
||||
for (const label in labels) {
|
||||
if (label !== '__name__') {
|
||||
labelStrings.push('<strong>' + label + '</strong>: ' + this.escapeHTML(labels[label]));
|
||||
}
|
||||
}
|
||||
return '<div class="labels">' + labelStrings.join('<br>') + '</div>';
|
||||
};
|
||||
}
|
||||
|
||||
formatValue = (y: number | null): string => {
|
||||
if (y === null) {
|
||||
return 'null';
|
||||
}
|
||||
var abs_y = Math.abs(y);
|
||||
const abs_y = Math.abs(y);
|
||||
if (abs_y >= 1e24) {
|
||||
return (y / 1e24).toFixed(2) + "Y";
|
||||
return (y / 1e24).toFixed(2) + 'Y';
|
||||
} else if (abs_y >= 1e21) {
|
||||
return (y / 1e21).toFixed(2) + "Z";
|
||||
return (y / 1e21).toFixed(2) + 'Z';
|
||||
} else if (abs_y >= 1e18) {
|
||||
return (y / 1e18).toFixed(2) + "E";
|
||||
return (y / 1e18).toFixed(2) + 'E';
|
||||
} else if (abs_y >= 1e15) {
|
||||
return (y / 1e15).toFixed(2) + "P";
|
||||
return (y / 1e15).toFixed(2) + 'P';
|
||||
} else if (abs_y >= 1e12) {
|
||||
return (y / 1e12).toFixed(2) + "T";
|
||||
return (y / 1e12).toFixed(2) + 'T';
|
||||
} else if (abs_y >= 1e9) {
|
||||
return (y / 1e9).toFixed(2) + "G";
|
||||
return (y / 1e9).toFixed(2) + 'G';
|
||||
} else if (abs_y >= 1e6) {
|
||||
return (y / 1e6).toFixed(2) + "M";
|
||||
return (y / 1e6).toFixed(2) + 'M';
|
||||
} else if (abs_y >= 1e3) {
|
||||
return (y / 1e3).toFixed(2) + "k";
|
||||
return (y / 1e3).toFixed(2) + 'k';
|
||||
} else if (abs_y >= 1) {
|
||||
return y.toFixed(2)
|
||||
return y.toFixed(2);
|
||||
} else if (abs_y === 0) {
|
||||
return y.toFixed(2)
|
||||
return y.toFixed(2);
|
||||
} else if (abs_y <= 1e-24) {
|
||||
return (y / 1e-24).toFixed(2) + "y";
|
||||
return (y / 1e-24).toFixed(2) + 'y';
|
||||
} else if (abs_y <= 1e-21) {
|
||||
return (y / 1e-21).toFixed(2) + "z";
|
||||
return (y / 1e-21).toFixed(2) + 'z';
|
||||
} else if (abs_y <= 1e-18) {
|
||||
return (y / 1e-18).toFixed(2) + "a";
|
||||
return (y / 1e-18).toFixed(2) + 'a';
|
||||
} else if (abs_y <= 1e-15) {
|
||||
return (y / 1e-15).toFixed(2) + "f";
|
||||
return (y / 1e-15).toFixed(2) + 'f';
|
||||
} else if (abs_y <= 1e-12) {
|
||||
return (y / 1e-12).toFixed(2) + "p";
|
||||
return (y / 1e-12).toFixed(2) + 'p';
|
||||
} else if (abs_y <= 1e-9) {
|
||||
return (y / 1e-9).toFixed(2) + "n";
|
||||
return (y / 1e-9).toFixed(2) + 'n';
|
||||
} else if (abs_y <= 1e-6) {
|
||||
return (y / 1e-6).toFixed(2) + "µ";
|
||||
} else if (abs_y <=1e-3) {
|
||||
return (y / 1e-3).toFixed(2) + "m";
|
||||
return (y / 1e-6).toFixed(2) + 'µ';
|
||||
} else if (abs_y <= 1e-3) {
|
||||
return (y / 1e-3).toFixed(2) + 'm';
|
||||
} else if (abs_y <= 1) {
|
||||
return y.toFixed(2)
|
||||
return y.toFixed(2);
|
||||
}
|
||||
throw Error("couldn't format a value, this is a bug");
|
||||
}
|
||||
};
|
||||
|
||||
getOptions(): any {
|
||||
return {
|
||||
|
@ -133,9 +133,9 @@ class Graph extends PureComponent<GraphProps> {
|
|||
cssClass: 'graph-tooltip',
|
||||
content: (label: string, xval: number, yval: number, flotItem: any) => {
|
||||
const series = flotItem.series; // TODO: type this.
|
||||
var date = '<span class="date">' + new Date(xval).toUTCString() + '</span>';
|
||||
var swatch = '<span class="detail-swatch" style="background-color: ' + series.color + '"></span>';
|
||||
var content = swatch + (series.labels.__name__ || 'value') + ": <strong>" + yval + '</strong>';
|
||||
const date = '<span class="date">' + new Date(xval).toUTCString() + '</span>';
|
||||
const swatch = '<span class="detail-swatch" style="background-color: ' + series.color + '"></span>';
|
||||
const content = swatch + (series.labels.__name__ || 'value') + ': <strong>' + yval + '</strong>';
|
||||
return date + '<br>' + content + '<br>' + this.renderLabels(series.labels);
|
||||
},
|
||||
defaultTheme: false,
|
||||
|
@ -149,20 +149,20 @@ class Graph extends PureComponent<GraphProps> {
|
|||
fill: this.props.stacked,
|
||||
},
|
||||
shadowSize: 0,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// This was adapted from Flot's color generation code.
|
||||
getColors() {
|
||||
let colors = [];
|
||||
const colorPool = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"];
|
||||
const colors = [];
|
||||
const colorPool = ['#edc240', '#afd8f8', '#cb4b4b', '#4da74d', '#9440ed'];
|
||||
const colorPoolSize = colorPool.length;
|
||||
let variation = 0;
|
||||
const neededColors = this.props.data.result.length;
|
||||
|
||||
for (let i = 0; i < neededColors; i++) {
|
||||
const c = ($ as any).color.parse(colorPool[i % colorPoolSize] || "#666");
|
||||
const c = ($ as any).color.parse(colorPool[i % colorPoolSize] || '#666');
|
||||
|
||||
// Each time we exhaust the colors in the pool we adjust
|
||||
// a scaling factor used to produce more variations on
|
||||
|
@ -191,7 +191,7 @@ class Graph extends PureComponent<GraphProps> {
|
|||
|
||||
return this.props.data.result.map((ts: any /* TODO: Type this*/, index: number) => {
|
||||
// Insert nulls for all missing steps.
|
||||
let data = [];
|
||||
const data = [];
|
||||
let pos = 0;
|
||||
const params = this.props.queryParams!;
|
||||
|
||||
|
@ -214,11 +214,11 @@ class Graph extends PureComponent<GraphProps> {
|
|||
color: colors[index],
|
||||
index: index,
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
parseValue(value: string) {
|
||||
var val = parseFloat(value);
|
||||
const val = parseFloat(value);
|
||||
if (isNaN(val)) {
|
||||
// "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). They
|
||||
// can't be graphed, so show them as gaps (null).
|
||||
|
@ -229,7 +229,7 @@ class Graph extends PureComponent<GraphProps> {
|
|||
return this.props.stacked ? 0 : null;
|
||||
}
|
||||
return val;
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.plot();
|
||||
|
@ -264,7 +264,11 @@ class Graph extends PureComponent<GraphProps> {
|
|||
}
|
||||
|
||||
if (this.props.data.resultType !== 'matrix') {
|
||||
return <Alert color="danger">Query result is of wrong type '{this.props.data.resultType}', should be 'matrix' (range vector).</Alert>;
|
||||
return (
|
||||
<Alert color="danger">
|
||||
Query result is of wrong type '{this.props.data.resultType}', should be 'matrix' (range vector).
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.data.result.length === 0) {
|
||||
|
@ -275,7 +279,7 @@ class Graph extends PureComponent<GraphProps> {
|
|||
<div className="graph">
|
||||
<ReactResizeDetector handleWidth onResize={() => this.plot()} />
|
||||
<div className="graph-chart" ref={this.chartRef} />
|
||||
<Legend series={this.getData()}/>
|
||||
<Legend series={this.getData()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,20 +1,8 @@
|
|||
import React, { Component } from 'react';
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Form,
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
Input,
|
||||
} from 'reactstrap';
|
||||
import { Button, ButtonGroup, Form, InputGroup, InputGroupAddon, Input } from 'reactstrap';
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faPlus,
|
||||
faMinus,
|
||||
faChartArea,
|
||||
faChartLine,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { faPlus, faMinus, faChartArea, faChartLine } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import TimeInput from './TimeInput';
|
||||
import { parseRange, formatRange } from './utils/timeFormat';
|
||||
|
@ -39,22 +27,22 @@ class GraphControls extends Component<GraphControlsProps> {
|
|||
1,
|
||||
10,
|
||||
60,
|
||||
5*60,
|
||||
15*60,
|
||||
30*60,
|
||||
60*60,
|
||||
2*60*60,
|
||||
6*60*60,
|
||||
12*60*60,
|
||||
24*60*60,
|
||||
48*60*60,
|
||||
7*24*60*60,
|
||||
14*24*60*60,
|
||||
28*24*60*60,
|
||||
56*24*60*60,
|
||||
365*24*60*60,
|
||||
730*24*60*60,
|
||||
]
|
||||
5 * 60,
|
||||
15 * 60,
|
||||
30 * 60,
|
||||
60 * 60,
|
||||
2 * 60 * 60,
|
||||
6 * 60 * 60,
|
||||
12 * 60 * 60,
|
||||
24 * 60 * 60,
|
||||
48 * 60 * 60,
|
||||
7 * 24 * 60 * 60,
|
||||
14 * 24 * 60 * 60,
|
||||
28 * 24 * 60 * 60,
|
||||
56 * 24 * 60 * 60,
|
||||
365 * 24 * 60 * 60,
|
||||
730 * 24 * 60 * 60,
|
||||
];
|
||||
|
||||
onChangeRangeInput = (rangeText: string): void => {
|
||||
const range = parseRange(rangeText);
|
||||
|
@ -63,31 +51,31 @@ class GraphControls extends Component<GraphControlsProps> {
|
|||
} else {
|
||||
this.props.onChangeRange(range);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
changeRangeInput = (range: number): void => {
|
||||
this.rangeRef.current!.value = formatRange(range);
|
||||
}
|
||||
};
|
||||
|
||||
increaseRange = (): void => {
|
||||
for (let range of this.rangeSteps) {
|
||||
for (const range of this.rangeSteps) {
|
||||
if (this.props.range < range) {
|
||||
this.changeRangeInput(range);
|
||||
this.props.onChangeRange(range);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
decreaseRange = (): void => {
|
||||
for (let range of this.rangeSteps.slice().reverse()) {
|
||||
for (const range of this.rangeSteps.slice().reverse()) {
|
||||
if (this.props.range > range) {
|
||||
this.changeRangeInput(range);
|
||||
this.props.onChangeRange(range);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps: GraphControlsProps) {
|
||||
if (prevProps.range !== this.props.range) {
|
||||
|
@ -103,7 +91,9 @@ class GraphControls extends Component<GraphControlsProps> {
|
|||
<Form inline className="graph-controls" onSubmit={e => e.preventDefault()}>
|
||||
<InputGroup className="range-input" size="sm">
|
||||
<InputGroupAddon addonType="prepend">
|
||||
<Button title="Decrease range" onClick={this.decreaseRange}><FontAwesomeIcon icon={faMinus} fixedWidth/></Button>
|
||||
<Button title="Decrease range" onClick={this.decreaseRange}>
|
||||
<FontAwesomeIcon icon={faMinus} fixedWidth />
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
|
||||
<Input
|
||||
|
@ -113,7 +103,9 @@ class GraphControls extends Component<GraphControlsProps> {
|
|||
/>
|
||||
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button title="Increase range" onClick={this.increaseRange}><FontAwesomeIcon icon={faPlus} fixedWidth/></Button>
|
||||
<Button title="Increase range" onClick={this.increaseRange}>
|
||||
<FontAwesomeIcon icon={faPlus} fixedWidth />
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
|
@ -137,8 +129,16 @@ class GraphControls extends Component<GraphControlsProps> {
|
|||
/>
|
||||
|
||||
<ButtonGroup className="stacked-input" size="sm">
|
||||
<Button title="Show unstacked line graph" onClick={() => this.props.onChangeStacking(false)} active={!this.props.stacked}><FontAwesomeIcon icon={faChartLine} fixedWidth/></Button>
|
||||
<Button title="Show stacked graph" onClick={() => this.props.onChangeStacking(true)} active={this.props.stacked}><FontAwesomeIcon icon={faChartArea} fixedWidth/></Button>
|
||||
<Button
|
||||
title="Show unstacked line graph"
|
||||
onClick={() => this.props.onChangeStacking(false)}
|
||||
active={!this.props.stacked}
|
||||
>
|
||||
<FontAwesomeIcon icon={faChartLine} fixedWidth />
|
||||
</Button>
|
||||
<Button title="Show stacked graph" onClick={() => this.props.onChangeStacking(true)} active={this.props.stacked}>
|
||||
<FontAwesomeIcon icon={faChartArea} fixedWidth />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Form>
|
||||
);
|
||||
|
|
|
@ -11,7 +11,7 @@ class Legend extends PureComponent<LegendProps> {
|
|||
return (
|
||||
<tr key={s.index} className="legend-item">
|
||||
<td>
|
||||
<div className="legend-swatch" style={{backgroundColor: s.color}}></div>
|
||||
<div className="legend-swatch" style={{ backgroundColor: s.color }}></div>
|
||||
</td>
|
||||
<td>
|
||||
<SeriesName labels={s.labels} format={true} />
|
||||
|
@ -24,7 +24,9 @@ class Legend extends PureComponent<LegendProps> {
|
|||
return (
|
||||
<table className="graph-legend">
|
||||
<tbody>
|
||||
{this.props.series.map((s: any) => {return this.renderLegendItem(s)})}
|
||||
{this.props.series.map((s: any) => {
|
||||
return this.renderLegendItem(s);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
function metricToSeriesName(labels: {[key: string]: string}): string {
|
||||
function metricToSeriesName(labels: { [key: string]: string }): string {
|
||||
if (labels === null) {
|
||||
return 'scalar';
|
||||
}
|
||||
let tsName = (labels.__name__ || '') + '{';
|
||||
let labelStrings: string[] = [];
|
||||
for (let label in labels) {
|
||||
const labelStrings: string[] = [];
|
||||
for (const label in labels) {
|
||||
if (label !== '__name__') {
|
||||
labelStrings.push(label + '="' + labels[label] + '"');
|
||||
}
|
||||
}
|
||||
tsName += labelStrings.join(', ') + '}';
|
||||
return tsName;
|
||||
};
|
||||
}
|
||||
|
||||
export default metricToSeriesName;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Link } from "@reach/router";
|
||||
import { Link } from '@reach/router';
|
||||
import {
|
||||
Collapse,
|
||||
Navbar,
|
||||
|
@ -18,37 +18,59 @@ const Navigation = () => {
|
|||
const toggle = () => setIsOpen(!isOpen);
|
||||
return (
|
||||
<Navbar className="mb-3" dark color="dark" expand="md">
|
||||
<NavbarToggler onClick={toggle}/>
|
||||
<Link className="pt-0 navbar-brand" to="/new/graph">Prometheus</Link>
|
||||
<NavbarToggler onClick={toggle} />
|
||||
<Link className="pt-0 navbar-brand" to="/new/graph">
|
||||
Prometheus
|
||||
</Link>
|
||||
<Collapse isOpen={isOpen} navbar style={{ justifyContent: 'space-between' }}>
|
||||
<Nav className="ml-0" navbar>
|
||||
<NavItem>
|
||||
<NavLink tag={Link} to="/new/alerts">Alerts</NavLink>
|
||||
<NavLink tag={Link} to="/new/alerts">
|
||||
Alerts
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink tag={Link} to="/new/graph">Graph</NavLink>
|
||||
<NavLink tag={Link} to="/new/graph">
|
||||
Graph
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<UncontrolledDropdown nav inNavbar>
|
||||
<DropdownToggle nav caret>Status</DropdownToggle>
|
||||
<DropdownToggle nav caret>
|
||||
Status
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
<DropdownItem tag={Link} to="/new/status">Runtime & Build Information</DropdownItem>
|
||||
<DropdownItem tag={Link} to="/new/flags">Command-Line Flags</DropdownItem>
|
||||
<DropdownItem tag={Link} to="/new/config">Configuration</DropdownItem>
|
||||
<DropdownItem tag={Link} to="/new/rules">Rules</DropdownItem>
|
||||
<DropdownItem tag={Link} to="/new/targets">Targets</DropdownItem>
|
||||
<DropdownItem tag={Link} to="/new/service-discovery">Service Discovery</DropdownItem>
|
||||
<DropdownItem tag={Link} to="/new/status">
|
||||
Runtime & Build Information
|
||||
</DropdownItem>
|
||||
<DropdownItem tag={Link} to="/new/flags">
|
||||
Command-Line Flags
|
||||
</DropdownItem>
|
||||
<DropdownItem tag={Link} to="/new/config">
|
||||
Configuration
|
||||
</DropdownItem>
|
||||
<DropdownItem tag={Link} to="/new/rules">
|
||||
Rules
|
||||
</DropdownItem>
|
||||
<DropdownItem tag={Link} to="/new/targets">
|
||||
Targets
|
||||
</DropdownItem>
|
||||
<DropdownItem tag={Link} to="/new/service-discovery">
|
||||
Service Discovery
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</UncontrolledDropdown>
|
||||
<NavItem>
|
||||
<NavLink href="https://prometheus.io/docs/prometheus/latest/getting_started/">Help</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink tag={Link} to="../../graph">Classic UI</NavLink>
|
||||
<NavLink tag={Link} to="../../graph">
|
||||
Classic UI
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
</Collapse>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Navigation;
|
||||
|
|
|
@ -1,16 +1,6 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Col,
|
||||
Nav,
|
||||
NavItem,
|
||||
NavLink,
|
||||
Row,
|
||||
TabContent,
|
||||
TabPane,
|
||||
} from 'reactstrap';
|
||||
import { Alert, Button, Col, Nav, NavItem, NavLink, Row, TabContent, TabPane } from 'reactstrap';
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
|
@ -32,14 +22,15 @@ interface PanelProps {
|
|||
|
||||
interface PanelState {
|
||||
data: any; // TODO: Type data.
|
||||
lastQueryParams: { // TODO: Share these with Graph.tsx in a file.
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
resolution: number,
|
||||
lastQueryParams: {
|
||||
// TODO: Share these with Graph.tsx in a file.
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
resolution: number;
|
||||
} | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
stats: QueryStats | null,
|
||||
stats: QueryStats | null;
|
||||
}
|
||||
|
||||
export interface PanelOptions {
|
||||
|
@ -63,7 +54,7 @@ export const PanelDefaultOptions: PanelOptions = {
|
|||
endTime: null,
|
||||
resolution: null,
|
||||
stacked: false,
|
||||
}
|
||||
};
|
||||
|
||||
class Panel extends Component<PanelProps, PanelState> {
|
||||
private abortInFlightFetch: (() => void) | null = null;
|
||||
|
@ -83,16 +74,17 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
componentDidUpdate(prevProps: PanelProps, prevState: PanelState) {
|
||||
const prevOpts = prevProps.options;
|
||||
const opts = this.props.options;
|
||||
if (prevOpts.type !== opts.type ||
|
||||
if (
|
||||
prevOpts.type !== opts.type ||
|
||||
prevOpts.range !== opts.range ||
|
||||
prevOpts.endTime !== opts.endTime ||
|
||||
prevOpts.resolution !== opts.resolution) {
|
||||
|
||||
prevOpts.resolution !== opts.resolution
|
||||
) {
|
||||
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.
|
||||
this.setState({data: null});
|
||||
this.setState({ data: null });
|
||||
}
|
||||
this.executeQuery(opts.expr);
|
||||
}
|
||||
|
@ -104,9 +96,9 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
|
||||
executeQuery = (expr: string): void => {
|
||||
const queryStart = Date.now();
|
||||
this.props.onExecuteQuery(expr)
|
||||
this.props.onExecuteQuery(expr);
|
||||
if (this.props.options.expr !== expr) {
|
||||
this.setOptions({expr: expr});
|
||||
this.setOptions({ expr: expr });
|
||||
}
|
||||
if (expr === '') {
|
||||
return;
|
||||
|
@ -119,38 +111,38 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
|
||||
const abortController = new AbortController();
|
||||
this.abortInFlightFetch = () => abortController.abort();
|
||||
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 startTime = endTime - this.props.options.range;
|
||||
const resolution = this.props.options.resolution || Math.max(Math.floor(this.props.options.range / 250), 1);
|
||||
const url = new URL(window.location.href);
|
||||
const params: {[key: string]: string} = {
|
||||
'query': expr,
|
||||
const params: { [key: string]: string } = {
|
||||
query: expr,
|
||||
};
|
||||
|
||||
switch (this.props.options.type) {
|
||||
case 'graph':
|
||||
url.pathname = '../../api/v1/query_range'
|
||||
url.pathname = '../../api/v1/query_range';
|
||||
Object.assign(params, {
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
step: resolution,
|
||||
})
|
||||
});
|
||||
// TODO path prefix here and elsewhere.
|
||||
break;
|
||||
case 'table':
|
||||
url.pathname = '../../api/v1/query'
|
||||
url.pathname = '../../api/v1/query';
|
||||
Object.assign(params, {
|
||||
time: endTime,
|
||||
})
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid panel type "' + this.props.options.type + '"');
|
||||
}
|
||||
Object.keys(params).forEach(key => url.searchParams.append(key, params[key]))
|
||||
Object.keys(params).forEach(key => url.searchParams.append(key, params[key]));
|
||||
|
||||
fetch(url.toString(), {cache: 'no-store', signal: abortController.signal})
|
||||
fetch(url.toString(), { cache: 'no-store', signal: abortController.signal })
|
||||
.then(resp => resp.json())
|
||||
.then(json => {
|
||||
if (json.status !== 'success') {
|
||||
|
@ -160,7 +152,7 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
let resultSeries = 0;
|
||||
if (json.data) {
|
||||
const { resultType, result } = json.data;
|
||||
if (resultType === "scalar") {
|
||||
if (resultType === 'scalar') {
|
||||
resultSeries = 1;
|
||||
} else if (result && result.length > 0) {
|
||||
resultSeries = result.length;
|
||||
|
@ -178,7 +170,7 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
stats: {
|
||||
loadTime: Date.now() - queryStart,
|
||||
resolution,
|
||||
resultSeries
|
||||
resultSeries,
|
||||
},
|
||||
loading: false,
|
||||
});
|
||||
|
@ -187,46 +179,46 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
.catch(error => {
|
||||
if (error.name === 'AbortError') {
|
||||
// Aborts are expected, don't show an error for them.
|
||||
return
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
error: 'Error executing query: ' + error.message,
|
||||
loading: false,
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setOptions(opts: object): void {
|
||||
const newOpts = {...this.props.options, ...opts};
|
||||
const newOpts = { ...this.props.options, ...opts };
|
||||
this.props.onOptionsChanged(newOpts);
|
||||
}
|
||||
|
||||
handleExpressionChange = (expr: string): void => {
|
||||
this.setOptions({expr: expr});
|
||||
}
|
||||
this.setOptions({ expr: expr });
|
||||
};
|
||||
|
||||
handleChangeRange = (range: number): void => {
|
||||
this.setOptions({range: range});
|
||||
}
|
||||
this.setOptions({ range: range });
|
||||
};
|
||||
|
||||
getEndTime = (): number | moment.Moment => {
|
||||
if (this.props.options.endTime === null) {
|
||||
return moment();
|
||||
}
|
||||
return this.props.options.endTime;
|
||||
}
|
||||
};
|
||||
|
||||
handleChangeEndTime = (endTime: number | null) => {
|
||||
this.setOptions({endTime: endTime});
|
||||
}
|
||||
this.setOptions({ endTime: endTime });
|
||||
};
|
||||
|
||||
handleChangeResolution = (resolution: number | null) => {
|
||||
this.setOptions({resolution: resolution});
|
||||
}
|
||||
this.setOptions({ resolution: resolution });
|
||||
};
|
||||
|
||||
handleChangeStacking = (stacked: boolean) => {
|
||||
this.setOptions({stacked: stacked});
|
||||
}
|
||||
this.setOptions({ stacked: stacked });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { pastQueries, metricNames, options } = this.props;
|
||||
|
@ -246,9 +238,7 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
{this.state.error && <Alert color="danger">{this.state.error}</Alert>}
|
||||
</Col>
|
||||
<Col>{this.state.error && <Alert color="danger">{this.state.error}</Alert>}</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
|
@ -256,7 +246,9 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
<NavItem>
|
||||
<NavLink
|
||||
className={options.type === 'table' ? 'active' : ''}
|
||||
onClick={() => { this.setOptions({type: 'table'}); }}
|
||||
onClick={() => {
|
||||
this.setOptions({ type: 'table' });
|
||||
}}
|
||||
>
|
||||
Table
|
||||
</NavLink>
|
||||
|
@ -264,19 +256,18 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
<NavItem>
|
||||
<NavLink
|
||||
className={options.type === 'graph' ? 'active' : ''}
|
||||
onClick={() => { this.setOptions({type: 'graph'}); }}
|
||||
onClick={() => {
|
||||
this.setOptions({ type: 'graph' });
|
||||
}}
|
||||
>
|
||||
Graph
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
{
|
||||
(!this.state.loading && !this.state.error && this.state.stats) &&
|
||||
<QueryStatsView {...this.state.stats} />
|
||||
}
|
||||
{!this.state.loading && !this.state.error && this.state.stats && <QueryStatsView {...this.state.stats} />}
|
||||
</Nav>
|
||||
<TabContent activeTab={options.type}>
|
||||
<TabPane tabId="table">
|
||||
{options.type === 'table' &&
|
||||
{options.type === 'table' && (
|
||||
<>
|
||||
<div className="table-controls">
|
||||
<TimeInput
|
||||
|
@ -288,17 +279,16 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
</div>
|
||||
<DataTable data={this.state.data} />
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</TabPane>
|
||||
<TabPane tabId="graph">
|
||||
{this.props.options.type === 'graph' &&
|
||||
{this.props.options.type === 'graph' && (
|
||||
<>
|
||||
<GraphControls
|
||||
range={options.range}
|
||||
endTime={options.endTime}
|
||||
resolution={options.resolution}
|
||||
stacked={options.stacked}
|
||||
|
||||
onChangeRange={this.handleChangeRange}
|
||||
onChangeEndTime={this.handleChangeEndTime}
|
||||
onChangeResolution={this.handleChangeResolution}
|
||||
|
@ -306,14 +296,16 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
/>
|
||||
<Graph data={this.state.data} stacked={options.stacked} queryParams={this.state.lastQueryParams} />
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<Button className="float-right" color="link" onClick={this.props.removePanel} size="sm">Remove Panel</Button>
|
||||
<Button className="float-right" color="link" onClick={this.props.removePanel} size="sm">
|
||||
Remove Panel
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
|
|
@ -8,13 +8,15 @@ export interface QueryStats {
|
|||
}
|
||||
|
||||
const QueryStatsView: FC<QueryStats> = props => {
|
||||
const {loadTime, resolution, resultSeries} = props;
|
||||
const { loadTime, resolution, resultSeries } = props;
|
||||
|
||||
return (
|
||||
<div className="query-stats">
|
||||
<span className="float-right">Load time: {loadTime}ms   Resolution: {resolution}s   Result series: {resultSeries}</span>
|
||||
<span className="float-right">
|
||||
Load time: {loadTime}ms   Resolution: {resolution}s   Result series: {resultSeries}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default QueryStatsView;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { PureComponent } from "react";
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
interface SeriesNameProps {
|
||||
labels: {[key: string]: string} | null;
|
||||
labels: { [key: string]: string } | null;
|
||||
format: boolean;
|
||||
}
|
||||
|
||||
|
@ -9,9 +9,9 @@ class SeriesName extends PureComponent<SeriesNameProps> {
|
|||
renderFormatted(): React.ReactNode {
|
||||
const labels = this.props.labels!;
|
||||
|
||||
let labelNodes: React.ReactNode[] = [];
|
||||
const labelNodes: React.ReactNode[] = [];
|
||||
let first = true;
|
||||
for (let label in labels) {
|
||||
for (const label in labels) {
|
||||
if (label === '__name__') {
|
||||
continue;
|
||||
}
|
||||
|
@ -19,8 +19,7 @@ class SeriesName extends PureComponent<SeriesNameProps> {
|
|||
labelNodes.push(
|
||||
<span key={label}>
|
||||
{!first && ', '}
|
||||
<span className="legend-label-name">{label}</span>=
|
||||
<span className="legend-label-value">"{labels[label]}"</span>
|
||||
<span className="legend-label-name">{label}</span>=<span className="legend-label-value">"{labels[label]}"</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
|
@ -43,8 +42,8 @@ class SeriesName extends PureComponent<SeriesNameProps> {
|
|||
const labels = this.props.labels!;
|
||||
|
||||
let tsName = (labels.__name__ || '') + '{';
|
||||
let labelStrings: string[] = [];
|
||||
for (let label in labels) {
|
||||
const labelStrings: string[] = [];
|
||||
for (const label in labels) {
|
||||
if (label !== '__name__') {
|
||||
labelStrings.push(label + '="' + labels[label] + '"');
|
||||
}
|
||||
|
|
|
@ -19,12 +19,7 @@ import {
|
|||
faTimes,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
library.add(
|
||||
faCalendarCheck,
|
||||
faArrowUp,
|
||||
faArrowDown,
|
||||
faTimes,
|
||||
);
|
||||
library.add(faCalendarCheck, faArrowUp, faArrowDown, faTimes);
|
||||
// Sadly needed to also replace <i> within the date picker, since it's not a React component.
|
||||
dom.watch();
|
||||
|
||||
|
@ -42,21 +37,21 @@ class TimeInput extends Component<TimeInputProps> {
|
|||
|
||||
getBaseTime = (): number => {
|
||||
return this.props.time || moment().valueOf();
|
||||
}
|
||||
};
|
||||
|
||||
increaseTime = (): void => {
|
||||
const time = this.getBaseTime() + this.props.range*1000/2;
|
||||
const time = this.getBaseTime() + (this.props.range * 1000) / 2;
|
||||
this.props.onChangeTime(time);
|
||||
}
|
||||
};
|
||||
|
||||
decreaseTime = (): void => {
|
||||
const time = this.getBaseTime() - this.props.range*1000/2;
|
||||
const time = this.getBaseTime() - (this.props.range * 1000) / 2;
|
||||
this.props.onChangeTime(time);
|
||||
}
|
||||
};
|
||||
|
||||
clearTime = (): void => {
|
||||
this.props.onChangeTime(null);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.$time = $(this.timeInputRef.current!);
|
||||
|
@ -96,7 +91,9 @@ class TimeInput extends Component<TimeInputProps> {
|
|||
return (
|
||||
<InputGroup className="time-input" size="sm">
|
||||
<InputGroupAddon addonType="prepend">
|
||||
<Button title="Decrease time" onClick={this.decreaseTime}><FontAwesomeIcon icon={faChevronLeft} fixedWidth /></Button>
|
||||
<Button title="Decrease time" onClick={this.decreaseTime}>
|
||||
<FontAwesomeIcon icon={faChevronLeft} fixedWidth />
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
|
||||
<Input
|
||||
|
@ -104,19 +101,23 @@ class TimeInput extends Component<TimeInputProps> {
|
|||
innerRef={this.timeInputRef}
|
||||
onFocus={() => this.$time.datetimepicker('show')}
|
||||
onBlur={() => this.$time.datetimepicker('hide')}
|
||||
onKeyDown={(e) => ['Escape', 'Enter'].includes(e.key) && this.$time.datetimepicker('hide')}
|
||||
onKeyDown={e => ['Escape', 'Enter'].includes(e.key) && this.$time.datetimepicker('hide')}
|
||||
/>
|
||||
|
||||
{/* CAUTION: While the datetimepicker also has an option to show a 'clear' button,
|
||||
that functionality is broken, so we create an external solution instead. */}
|
||||
{this.props.time &&
|
||||
{this.props.time && (
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button className="clear-time-btn" title="Clear time" onClick={this.clearTime}><FontAwesomeIcon icon={faTimes} fixedWidth /></Button>
|
||||
<Button className="clear-time-btn" title="Clear time" onClick={this.clearTime}>
|
||||
<FontAwesomeIcon icon={faTimes} fixedWidth />
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
}
|
||||
)}
|
||||
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button title="Increase time" onClick={this.increaseTime}><FontAwesomeIcon icon={faChevronRight} fixedWidth /></Button>
|
||||
<Button title="Increase time" onClick={this.increaseTime}>
|
||||
<FontAwesomeIcon icon={faChevronRight} fixedWidth />
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC } from 'react';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
|
||||
const Alerts: FC<RouteComponentProps> = props => <div>Alerts page</div>
|
||||
const Alerts: FC<RouteComponentProps> = props => <div>Alerts page</div>;
|
||||
|
||||
export default Alerts;
|
||||
|
|
|
@ -9,7 +9,7 @@ import './Config.css';
|
|||
|
||||
const Config: FC<RouteComponentProps> = () => {
|
||||
const [config, setConfig] = useState(null);
|
||||
const [error, setError] = useState("");
|
||||
const [error, setError] = useState('');
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -25,7 +25,10 @@ const Config: FC<RouteComponentProps> = () => {
|
|||
Configuration
|
||||
<CopyToClipboard
|
||||
text={config ? config! : ''}
|
||||
onCopy={(text, result) => { setCopied(result); setTimeout(setCopied, 1500);}}
|
||||
onCopy={(text, result) => {
|
||||
setCopied(result);
|
||||
setTimeout(setCopied, 1500);
|
||||
}}
|
||||
>
|
||||
<Button color="light" disabled={!config}>
|
||||
{copied ? 'Copied' : 'Copy to clipboard'}
|
||||
|
@ -33,14 +36,17 @@ const Config: FC<RouteComponentProps> = () => {
|
|||
</CopyToClipboard>
|
||||
</h2>
|
||||
|
||||
{error
|
||||
? <Alert color="danger"><strong>Error:</strong> Error fetching configuration: {error}</Alert>
|
||||
: config
|
||||
? <pre className="config-yaml">{config}</pre>
|
||||
: <FontAwesomeIcon icon={faSpinner} spin />
|
||||
}
|
||||
{error ? (
|
||||
<Alert color="danger">
|
||||
<strong>Error:</strong> Error fetching configuration: {error}
|
||||
</Alert>
|
||||
) : config ? (
|
||||
<pre className="config-yaml">{config}</pre>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faSpinner} spin />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Config;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC } from 'react';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
|
||||
const Flags : FC<RouteComponentProps> = () => <div>Flags page</div>
|
||||
const Flags: FC<RouteComponentProps> = () => <div>Flags page</div>;
|
||||
|
||||
export default Flags;
|
||||
|
|
|
@ -20,14 +20,17 @@ interface PanelListState {
|
|||
}
|
||||
|
||||
class PanelList extends Component<any, PanelListState> {
|
||||
private key: number = 0;
|
||||
private key = 0;
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
const urlPanels = decodePanelOptionsFromQueryString(window.location.search);
|
||||
|
||||
this.state = {
|
||||
panels: urlPanels.length !== 0 ? urlPanels : [
|
||||
panels:
|
||||
urlPanels.length !== 0
|
||||
? urlPanels
|
||||
: [
|
||||
{
|
||||
key: this.getKey(),
|
||||
options: PanelDefaultOptions,
|
||||
|
@ -41,7 +44,7 @@ class PanelList extends Component<any, PanelListState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetch("../../api/v1/label/__name__/values", {cache: "no-store"})
|
||||
fetch('../../api/v1/label/__name__/values', { cache: 'no-store' })
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json();
|
||||
|
@ -50,12 +53,12 @@ class PanelList extends Component<any, PanelListState> {
|
|||
}
|
||||
})
|
||||
.then(json => {
|
||||
this.setState({metricNames: json.data});
|
||||
this.setState({ metricNames: json.data });
|
||||
})
|
||||
.catch(error => this.setState({ fetchMetricsError: error.message }));
|
||||
|
||||
const browserTime = new Date().getTime() / 1000;
|
||||
fetch("../../api/v1/query?query=time()", {cache: "no-store"})
|
||||
fetch('../../api/v1/query?query=time()', { cache: 'no-store' })
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json();
|
||||
|
@ -68,7 +71,11 @@ class PanelList extends Component<any, PanelListState> {
|
|||
const delta = Math.abs(browserTime - serverTime);
|
||||
|
||||
if (delta >= 30) {
|
||||
throw new Error('Detected ' + delta + ' seconds time difference between your browser and the server. Prometheus relies on accurate time and time drift might cause unexpected query results.');
|
||||
throw new Error(
|
||||
'Detected ' +
|
||||
delta +
|
||||
' seconds time difference between your browser and the server. Prometheus relies on accurate time and time drift might cause unexpected query results.'
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(error => this.setState({ timeDriftError: error.message }));
|
||||
|
@ -76,9 +83,9 @@ class PanelList extends Component<any, PanelListState> {
|
|||
window.onpopstate = () => {
|
||||
const panels = decodePanelOptionsFromQueryString(window.location.search);
|
||||
if (panels.length !== 0) {
|
||||
this.setState({panels: panels});
|
||||
}
|
||||
this.setState({ panels: panels });
|
||||
}
|
||||
};
|
||||
|
||||
this.updatePastQueries();
|
||||
}
|
||||
|
@ -90,13 +97,13 @@ class PanelList extends Component<any, PanelListState> {
|
|||
toggleQueryHistory = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
localStorage.setItem('enable-query-history', `${e.target.checked}`);
|
||||
this.updatePastQueries();
|
||||
}
|
||||
};
|
||||
|
||||
updatePastQueries = () => {
|
||||
this.setState({
|
||||
pastQueries: this.isHistoryEnabled() ? this.getHistoryItems() : []
|
||||
pastQueries: this.isHistoryEnabled() ? this.getHistoryItems() : [],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleQueryHistory = (query: string) => {
|
||||
const isSimpleMetric = this.state.metricNames.indexOf(query) !== -1;
|
||||
|
@ -104,12 +111,15 @@ class PanelList extends Component<any, PanelListState> {
|
|||
return;
|
||||
}
|
||||
const historyItems = this.getHistoryItems();
|
||||
const extendedItems = historyItems.reduce((acc, metric) => {
|
||||
const extendedItems = historyItems.reduce(
|
||||
(acc, metric) => {
|
||||
return metric === query ? acc : [...acc, metric]; // Prevent adding query twice.
|
||||
}, [query]);
|
||||
},
|
||||
[query]
|
||||
);
|
||||
localStorage.setItem('history', JSON.stringify(extendedItems.slice(0, 50)));
|
||||
this.updatePastQueries();
|
||||
}
|
||||
};
|
||||
|
||||
getKey(): string {
|
||||
return (this.key++).toString();
|
||||
|
@ -121,11 +131,11 @@ class PanelList extends Component<any, PanelListState> {
|
|||
return {
|
||||
key: key,
|
||||
options: opts,
|
||||
}
|
||||
};
|
||||
}
|
||||
return p;
|
||||
});
|
||||
this.setState({panels: newPanels}, this.updateURL)
|
||||
this.setState({ panels: newPanels }, this.updateURL);
|
||||
}
|
||||
|
||||
updateURL(): void {
|
||||
|
@ -139,15 +149,15 @@ class PanelList extends Component<any, PanelListState> {
|
|||
key: this.getKey(),
|
||||
options: PanelDefaultOptions,
|
||||
});
|
||||
this.setState({panels: panels}, this.updateURL);
|
||||
}
|
||||
this.setState({ panels: panels }, this.updateURL);
|
||||
};
|
||||
|
||||
removePanel = (key: string): void => {
|
||||
const panels = this.state.panels.filter(panel => {
|
||||
return panel.key !== key;
|
||||
});
|
||||
this.setState({panels: panels}, this.updateURL);
|
||||
}
|
||||
this.setState({ panels: panels }, this.updateURL);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { metricNames, pastQueries, timeDriftError, fetchMetricsError } = this.state;
|
||||
|
@ -158,21 +168,30 @@ class PanelList extends Component<any, PanelListState> {
|
|||
id="query-history-checkbox"
|
||||
wrapperStyles={{ margin: '0 0 0 15px', alignSelf: 'center' }}
|
||||
onChange={this.toggleQueryHistory}
|
||||
defaultChecked={this.isHistoryEnabled()}>
|
||||
defaultChecked={this.isHistoryEnabled()}
|
||||
>
|
||||
Enable query history
|
||||
</Checkbox>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
{timeDriftError && <Alert color="danger"><strong>Warning:</strong> Error fetching server time: {this.state.timeDriftError}</Alert>}
|
||||
{timeDriftError && (
|
||||
<Alert color="danger">
|
||||
<strong>Warning:</strong> Error fetching server time: {this.state.timeDriftError}
|
||||
</Alert>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
{fetchMetricsError && <Alert color="danger"><strong>Warning:</strong> Error fetching metrics list: {this.state.fetchMetricsError}</Alert>}
|
||||
{fetchMetricsError && (
|
||||
<Alert color="danger">
|
||||
<strong>Warning:</strong> Error fetching metrics list: {this.state.fetchMetricsError}
|
||||
</Alert>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
{this.state.panels.map(p =>
|
||||
{this.state.panels.map(p => (
|
||||
<Panel
|
||||
onExecuteQuery={this.handleQueryHistory}
|
||||
key={p.key}
|
||||
|
@ -182,8 +201,10 @@ class PanelList extends Component<any, PanelListState> {
|
|||
metricNames={metricNames}
|
||||
pastQueries={pastQueries}
|
||||
/>
|
||||
)}
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC } from 'react';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
|
||||
const Rules: FC<RouteComponentProps> = () => <div>Rules page</div>
|
||||
const Rules: FC<RouteComponentProps> = () => <div>Rules page</div>;
|
||||
|
||||
export default Rules;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC } from 'react';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
|
||||
const Services: FC<RouteComponentProps> = () => <div>Services page</div>
|
||||
const Services: FC<RouteComponentProps> = () => <div>Services page</div>;
|
||||
|
||||
export default Services;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC } from 'react';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
|
||||
const Status: FC<RouteComponentProps> = () => <div>Status page</div>
|
||||
const Status: FC<RouteComponentProps> = () => <div>Status page</div>;
|
||||
|
||||
export default Status;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC } from 'react';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
|
||||
const Targets: FC<RouteComponentProps> = () => <div>Targets page</div>
|
||||
const Targets: FC<RouteComponentProps> = () => <div>Targets page</div>;
|
||||
|
||||
export default Targets;
|
||||
|
|
|
@ -7,13 +7,4 @@ import Status from './Status';
|
|||
import Targets from './Targets';
|
||||
import PanelList from './PanelList';
|
||||
|
||||
export {
|
||||
Alerts,
|
||||
Config,
|
||||
Flags,
|
||||
Rules,
|
||||
Services,
|
||||
Status,
|
||||
Targets,
|
||||
PanelList,
|
||||
}
|
||||
export { Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList };
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
export const uuidGen = () => '_' + Math.random().toString(36).substr(2, 9);
|
||||
export const uuidGen = () =>
|
||||
'_' +
|
||||
Math.random()
|
||||
.toString(36)
|
||||
.substr(2, 9);
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
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
|
||||
}
|
||||
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]+)$');
|
||||
|
@ -21,9 +21,9 @@ export function parseRange(rangeText: string): number | null {
|
|||
}
|
||||
|
||||
export function formatRange(range: number): string {
|
||||
for (let unit of Object.keys(rangeUnits)) {
|
||||
for (const unit of Object.keys(rangeUnits)) {
|
||||
if (range % rangeUnits[unit] === 0) {
|
||||
return (range / rangeUnits[unit]) + unit;
|
||||
return range / rangeUnits[unit] + unit;
|
||||
}
|
||||
}
|
||||
return range + 's';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { parseRange, parseTime, formatRange, formatTime } from './timeFormat';
|
||||
import { PanelOptions, PanelType, PanelDefaultOptions } from '../Panel';
|
||||
|
||||
export function decodePanelOptionsFromQueryString(query: string): {key: string, options: PanelOptions}[] {
|
||||
export function decodePanelOptionsFromQueryString(query: string): { key: string; options: PanelOptions }[] {
|
||||
if (query === '') {
|
||||
return [];
|
||||
}
|
||||
|
@ -21,12 +21,14 @@ interface IncompletePanelOptions {
|
|||
stacked?: boolean;
|
||||
}
|
||||
|
||||
function parseParams(params: string[]): {key: string, options: PanelOptions}[] {
|
||||
const sortedParams = params.filter((p) => {
|
||||
function parseParams(params: string[]): { key: string; options: PanelOptions }[] {
|
||||
const sortedParams = params
|
||||
.filter(p => {
|
||||
return paramFormat.test(p);
|
||||
}).sort();
|
||||
})
|
||||
.sort();
|
||||
|
||||
let panelOpts: {key: string, options: PanelOptions}[] = [];
|
||||
const panelOpts: { key: string; options: PanelOptions }[] = [];
|
||||
|
||||
let key = 0;
|
||||
let options: IncompletePanelOptions = {};
|
||||
|
@ -36,7 +38,7 @@ function parseParams(params: string[]): {key: string, options: PanelOptions}[] {
|
|||
if (!p.startsWith(prefix)) {
|
||||
panelOpts.push({
|
||||
key: key.toString(),
|
||||
options: {...PanelDefaultOptions, ...options},
|
||||
options: { ...PanelDefaultOptions, ...options },
|
||||
});
|
||||
options = {};
|
||||
key++;
|
||||
|
@ -46,17 +48,17 @@ function parseParams(params: string[]): {key: string, options: PanelOptions}[] {
|
|||
}
|
||||
panelOpts.push({
|
||||
key: key.toString(),
|
||||
options: {...PanelDefaultOptions, ...options},
|
||||
options: { ...PanelDefaultOptions, ...options },
|
||||
});
|
||||
|
||||
return panelOpts;
|
||||
}
|
||||
|
||||
function addParam(opts: IncompletePanelOptions, param: string): void {
|
||||
let [ opt, val ] = param.split('=');
|
||||
let [opt, val] = param.split('=');
|
||||
val = decodeURIComponent(val.replace(/\+/g, ' '));
|
||||
|
||||
switch(opt) {
|
||||
switch (opt) {
|
||||
case 'expr':
|
||||
opts.expr = val;
|
||||
break;
|
||||
|
@ -97,29 +99,29 @@ function addParam(opts: IncompletePanelOptions, param: string): void {
|
|||
}
|
||||
}
|
||||
|
||||
export function encodePanelOptionsToQueryString(panels: {key: string, options: PanelOptions}[]): string {
|
||||
export function encodePanelOptionsToQueryString(panels: { key: string; options: PanelOptions }[]): string {
|
||||
const queryParams: string[] = [];
|
||||
|
||||
panels.forEach(p => {
|
||||
const prefix = 'g' + p.key + '.';
|
||||
const o = p.options;
|
||||
const panelParams: {[key: string]: string | undefined} = {
|
||||
'expr': o.expr,
|
||||
'tab': o.type === PanelType.Graph ? '0' : '1',
|
||||
'stacked': o.stacked ? '1' : '0',
|
||||
'range_input': formatRange(o.range),
|
||||
'end_input': o.endTime !== null ? formatTime(o.endTime) : undefined,
|
||||
'moment_input': o.endTime !== null ? formatTime(o.endTime) : undefined,
|
||||
'step_input': o.resolution !== null ? o.resolution.toString() : undefined,
|
||||
const panelParams: { [key: string]: string | undefined } = {
|
||||
expr: o.expr,
|
||||
tab: o.type === PanelType.Graph ? '0' : '1',
|
||||
stacked: o.stacked ? '1' : '0',
|
||||
range_input: formatRange(o.range),
|
||||
end_input: o.endTime !== null ? formatTime(o.endTime) : undefined,
|
||||
moment_input: o.endTime !== null ? formatTime(o.endTime) : undefined,
|
||||
step_input: o.resolution !== null ? o.resolution.toString() : undefined,
|
||||
};
|
||||
|
||||
for (let o in panelParams) {
|
||||
for (const o in panelParams) {
|
||||
const pp = panelParams[o];
|
||||
if (pp !== undefined) {
|
||||
queryParams.push(prefix + o + '=' + encodeURIComponent(pp));
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return '?' + queryParams.join('&');
|
||||
}
|
||||
|
|
|
@ -1464,6 +1464,17 @@
|
|||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@2.x":
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.5.0.tgz#101d96743ce3365b3223df73d641078c9b775903"
|
||||
integrity sha512-ddrJZxp5ns1Lh5ofZQYk3P8RyvKfyz/VcRR4ZiJLHO/ljnQAO8YvTfj268+WJOOadn99mvDiqJA65+HAKoeSPA==
|
||||
dependencies:
|
||||
"@typescript-eslint/experimental-utils" "2.5.0"
|
||||
eslint-utils "^1.4.2"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
regexpp "^2.0.1"
|
||||
tsutils "^3.17.1"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^2.2.0":
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.4.0.tgz#aaf6b542ff75b78f4191a8bf1c519184817caa24"
|
||||
|
@ -1484,6 +1495,25 @@
|
|||
"@typescript-eslint/typescript-estree" "2.4.0"
|
||||
eslint-scope "^5.0.0"
|
||||
|
||||
"@typescript-eslint/experimental-utils@2.5.0":
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.5.0.tgz#383a97ded9a7940e5053449f6d73995e782b8fb1"
|
||||
integrity sha512-UgcQGE0GKJVChyRuN1CWqDW8Pnu7+mVst0aWrhiyuUD1J9c+h8woBdT4XddCvhcXDodTDVIfE3DzGHVjp7tUeQ==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.3"
|
||||
"@typescript-eslint/typescript-estree" "2.5.0"
|
||||
eslint-scope "^5.0.0"
|
||||
|
||||
"@typescript-eslint/parser@2.x":
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.5.0.tgz#858030ddd808fbbe88e03f42e5971efaccb8218a"
|
||||
integrity sha512-9UBMiAwIDWSl79UyogaBdj3hidzv6exjKUx60OuZuFnJf56tq/UMpdPcX09YmGqE8f4AnAueYtBxV8IcAT3jdQ==
|
||||
dependencies:
|
||||
"@types/eslint-visitor-keys" "^1.0.0"
|
||||
"@typescript-eslint/experimental-utils" "2.5.0"
|
||||
"@typescript-eslint/typescript-estree" "2.5.0"
|
||||
eslint-visitor-keys "^1.1.0"
|
||||
|
||||
"@typescript-eslint/parser@^2.2.0":
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.4.0.tgz#fe43ed5fec14af03d3594fce2c3b7ec4c8df0243"
|
||||
|
@ -1505,6 +1535,17 @@
|
|||
lodash.unescape "4.0.1"
|
||||
semver "^6.3.0"
|
||||
|
||||
"@typescript-eslint/typescript-estree@2.5.0":
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.5.0.tgz#40ada624d6217ef092a3a79ed30d947ad4f212ce"
|
||||
integrity sha512-AXURyF8NcA3IsnbjNX1v9qbwa0dDoY9YPcKYR2utvMHoUcu3636zrz0gRWtVAyxbPCkhyKuGg6WZIyi2Fc79CA==
|
||||
dependencies:
|
||||
debug "^4.1.1"
|
||||
glob "^7.1.4"
|
||||
is-glob "^4.0.1"
|
||||
lodash.unescape "4.0.1"
|
||||
semver "^6.3.0"
|
||||
|
||||
"@webassemblyjs/ast@1.8.5":
|
||||
version "1.8.5"
|
||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359"
|
||||
|
@ -2033,7 +2074,7 @@ babel-code-frame@^6.22.0:
|
|||
esutils "^2.0.2"
|
||||
js-tokens "^3.0.2"
|
||||
|
||||
babel-eslint@10.0.3:
|
||||
babel-eslint@10.0.3, babel-eslint@10.x:
|
||||
version "10.0.3"
|
||||
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a"
|
||||
integrity sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA==
|
||||
|
@ -3842,6 +3883,13 @@ escodegen@^1.11.0, escodegen@^1.11.1, escodegen@^1.9.1:
|
|||
optionalDependencies:
|
||||
source-map "~0.6.1"
|
||||
|
||||
eslint-config-prettier@^6.4.0:
|
||||
version "6.4.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.4.0.tgz#0a04f147e31d33c6c161b2dd0971418ac52d0477"
|
||||
integrity sha512-YrKucoFdc7SEko5Sxe4r6ixqXPDP1tunGw91POeZTTRKItf/AMFYt/YLEQtZMkR2LVpAVhcAcZgcWpm1oGPW7w==
|
||||
dependencies:
|
||||
get-stdin "^6.0.0"
|
||||
|
||||
eslint-config-react-app@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-5.0.2.tgz#df40d73a1402986030680c040bbee520db5a32a4"
|
||||
|
@ -3876,14 +3924,14 @@ eslint-module-utils@^2.4.0:
|
|||
debug "^2.6.8"
|
||||
pkg-dir "^2.0.0"
|
||||
|
||||
eslint-plugin-flowtype@3.13.0:
|
||||
eslint-plugin-flowtype@3.13.0, eslint-plugin-flowtype@3.x:
|
||||
version "3.13.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-3.13.0.tgz#e241ebd39c0ce519345a3f074ec1ebde4cf80f2c"
|
||||
integrity sha512-bhewp36P+t7cEV0b6OdmoRWJCBYRiHFlqPZAG1oS3SF+Y0LQkeDvFSM4oxoxvczD1OdONCXMlJfQFiWLcV9urw==
|
||||
dependencies:
|
||||
lodash "^4.17.15"
|
||||
|
||||
eslint-plugin-import@2.18.2:
|
||||
eslint-plugin-import@2.18.2, eslint-plugin-import@2.x:
|
||||
version "2.18.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz#02f1180b90b077b33d447a17a2326ceb400aceb6"
|
||||
integrity sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ==
|
||||
|
@ -3900,7 +3948,7 @@ eslint-plugin-import@2.18.2:
|
|||
read-pkg-up "^2.0.0"
|
||||
resolve "^1.11.0"
|
||||
|
||||
eslint-plugin-jsx-a11y@6.2.3:
|
||||
eslint-plugin-jsx-a11y@6.2.3, eslint-plugin-jsx-a11y@6.x:
|
||||
version "6.2.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz#b872a09d5de51af70a97db1eea7dc933043708aa"
|
||||
integrity sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==
|
||||
|
@ -3915,7 +3963,14 @@ eslint-plugin-jsx-a11y@6.2.3:
|
|||
has "^1.0.3"
|
||||
jsx-ast-utils "^2.2.1"
|
||||
|
||||
eslint-plugin-react-hooks@^1.6.1:
|
||||
eslint-plugin-prettier@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.1.tgz#507b8562410d02a03f0ddc949c616f877852f2ba"
|
||||
integrity sha512-A+TZuHZ0KU0cnn56/9mfR7/KjUJ9QNVXUhwvRFSR7PGPe0zQR6PTkmyqg1AtUUEOzTqeRsUwyKFh0oVZKVCrtA==
|
||||
dependencies:
|
||||
prettier-linter-helpers "^1.0.0"
|
||||
|
||||
eslint-plugin-react-hooks@1.x, eslint-plugin-react-hooks@^1.6.1:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz#6210b6d5a37205f0b92858f895a4e827020a7d04"
|
||||
integrity sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==
|
||||
|
@ -3935,6 +3990,21 @@ eslint-plugin-react@7.14.3:
|
|||
prop-types "^15.7.2"
|
||||
resolve "^1.10.1"
|
||||
|
||||
eslint-plugin-react@7.x:
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.16.0.tgz#9928e4f3e2122ed3ba6a5b56d0303ba3e41d8c09"
|
||||
integrity sha512-GacBAATewhhptbK3/vTP09CbFrgUJmBSaaRcWdbQLFvUZy9yVcQxigBNHGPU/KE2AyHpzj3AWXpxoMTsIDiHug==
|
||||
dependencies:
|
||||
array-includes "^3.0.3"
|
||||
doctrine "^2.1.0"
|
||||
has "^1.0.3"
|
||||
jsx-ast-utils "^2.2.1"
|
||||
object.entries "^1.1.0"
|
||||
object.fromentries "^2.0.0"
|
||||
object.values "^1.1.0"
|
||||
prop-types "^15.7.2"
|
||||
resolve "^1.12.0"
|
||||
|
||||
eslint-scope@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
|
||||
|
@ -3963,7 +4033,7 @@ eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"
|
||||
integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==
|
||||
|
||||
eslint@^6.1.0:
|
||||
eslint@6.x, eslint@^6.1.0:
|
||||
version "6.5.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.5.1.tgz#828e4c469697d43bb586144be152198b91e96ed6"
|
||||
integrity sha512-32h99BoLYStT1iq1v2P9uwpyznQ4M2jRiFB6acitKz52Gqn+vPaMDUTB1bYi1WN4Nquj2w+t+bimYUG83DC55A==
|
||||
|
@ -4221,6 +4291,11 @@ fast-deep-equal@^2.0.1:
|
|||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
|
||||
integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
|
||||
|
||||
fast-diff@^1.1.2:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
|
||||
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
|
||||
|
||||
fast-glob@^2.0.2:
|
||||
version "2.2.7"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
|
||||
|
@ -4599,6 +4674,11 @@ get-own-enumerable-property-symbols@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.1.tgz#6f7764f88ea11e0b514bd9bd860a132259992ca4"
|
||||
integrity sha512-09/VS4iek66Dh2bctjRkowueRJbY1JDGR1L/zRxO1Qk8Uxs6PnqaNSqalpizPT+CDjre3hnEsuzvhgomz9qYrA==
|
||||
|
||||
get-stdin@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b"
|
||||
integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==
|
||||
|
||||
get-stream@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
|
||||
|
@ -8240,6 +8320,18 @@ prepend-http@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
||||
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
|
||||
|
||||
prettier-linter-helpers@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
|
||||
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
|
||||
dependencies:
|
||||
fast-diff "^1.1.2"
|
||||
|
||||
prettier@^1.18.2:
|
||||
version "1.18.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea"
|
||||
integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==
|
||||
|
||||
pretty-bytes@^5.1.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2"
|
||||
|
|
Loading…
Reference in a new issue