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:
CSTDev 2019-10-28 14:02:42 +00:00 committed by Julius Volz
parent e8027ba515
commit 3b39f6ae45
29 changed files with 690 additions and 474 deletions

View file

@ -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

View 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"
]
}

View file

@ -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"
}

View file

@ -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() {

View file

@ -13,7 +13,7 @@ const Checkbox: FC<CheckboxProps> = ({ children, wrapperStyles, id, ...rest }) =
{children}
</Label>
</FormGroup>
)
}
);
};
export default memo(Checkbox);

View file

@ -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>
</>
);

View file

@ -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({
height: 'auto',
value: this.exprInputRef.current!.value
}, this.setHeight);
}
this.setState(
{
height: 'auto',
value: this.exprInputRef.current!.value,
},
this.setHeight
);
};
handleDropdownSelection = (value: string) => {
this.setState({ value, height: 'auto' }, this.setHeight);
@ -67,50 +70,54 @@ 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 matches = this.getSearchMatches(inputValue!, items);
return !matches.length ? acc : [
...acc,
<Card tag={ListGroup} key={title}>
<CardHeader style={{ fontSize: 13 }}>{title}</CardHeader>
{
matches
.slice(0, 100) // Limit DOM rendering to 100 results, as DOM rendering is sloooow.
.map(({ original, string }) => {
const itemProps = downshift.getItemProps({
key: original,
index,
item: original,
style: {
backgroundColor: highlightedIndex === index++ ? 'lightgray' : 'white',
},
})
return (
<SanitizeHTML tag={ListGroupItem} {...itemProps} allowedTags={['strong']}>
{string}
</SanitizeHTML>
);
})
}
</Card>
]
}, [] as JSX.Element[]) : []
const sections = inputValue!.length
? Object.entries(autocompleteSections).reduce(
(acc, [title, items]) => {
const matches = this.getSearchMatches(inputValue!, items);
return !matches.length
? acc
: [
...acc,
<Card tag={ListGroup} key={title}>
<CardHeader style={{ fontSize: 13 }}>{title}</CardHeader>
{matches
.slice(0, 100) // Limit DOM rendering to 100 results, as DOM rendering is sloooow.
.map(({ original, string }) => {
const itemProps = downshift.getItemProps({
key: original,
index,
item: original,
style: {
backgroundColor: highlightedIndex === index++ ? 'lightgray' : 'white',
},
});
return (
<SanitizeHTML tag={ListGroupItem} {...itemProps} allowedTags={['strong']}>
{string}
</SanitizeHTML>
);
})}
</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>

View file

@ -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 } = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;'
'/': '&#x2F;',
};
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>
);
}

View file

@ -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>
);

View file

@ -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>
);

View file

@ -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;

View file

@ -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;

View file

@ -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 ||
prevOpts.range !== opts.range ||
prevOpts.endTime !== opts.endTime ||
prevOpts.resolution !== opts.resolution) {
if (
prevOpts.type !== opts.type ||
prevOpts.range !== opts.range ||
prevOpts.endTime !== opts.endTime ||
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,114 +111,114 @@ 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})
.then(resp => resp.json())
.then(json => {
if (json.status !== 'success') {
throw new Error(json.error || 'invalid response JSON');
}
let resultSeries = 0;
if (json.data) {
const { resultType, result } = json.data;
if (resultType === "scalar") {
resultSeries = 1;
} else if (result && result.length > 0) {
resultSeries = result.length;
fetch(url.toString(), { cache: 'no-store', signal: abortController.signal })
.then(resp => resp.json())
.then(json => {
if (json.status !== 'success') {
throw new Error(json.error || 'invalid response JSON');
}
}
this.setState({
error: null,
data: json.data,
lastQueryParams: {
startTime,
endTime,
resolution,
},
stats: {
loadTime: Date.now() - queryStart,
resolution,
resultSeries
},
loading: false,
});
this.abortInFlightFetch = null;
})
.catch(error => {
if (error.name === 'AbortError') {
// Aborts are expected, don't show an error for them.
return
}
this.setState({
error: 'Error executing query: ' + error.message,
loading: false,
let resultSeries = 0;
if (json.data) {
const { resultType, result } = json.data;
if (resultType === 'scalar') {
resultSeries = 1;
} else if (result && result.length > 0) {
resultSeries = result.length;
}
}
this.setState({
error: null,
data: json.data,
lastQueryParams: {
startTime,
endTime,
resolution,
},
stats: {
loadTime: Date.now() - queryStart,
resolution,
resultSeries,
},
loading: false,
});
this.abortInFlightFetch = null;
})
});
}
.catch(error => {
if (error.name === 'AbortError') {
// Aborts are expected, don't show an error for them.
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>

View file

@ -2,19 +2,21 @@ import React, { FC } from 'react';
import './QueryStatsView.css';
export interface QueryStats {
loadTime: number;
resolution: number;
resultSeries: number;
loadTime: number;
resolution: number;
resultSeries: number;
}
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 &ensp; Resolution: {resolution}s &ensp; Result series: {resultSeries}</span>
</div>
);
}
return (
<div className="query-stats">
<span className="float-right">
Load time: {loadTime}ms &ensp; Resolution: {resolution}s &ensp; Result series: {resultSeries}
</span>
</div>
);
};
export default QueryStatsView;

View file

@ -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>
);
@ -33,7 +32,7 @@ class SeriesName extends PureComponent<SeriesNameProps> {
<>
<span className="legend-metric-name">{labels.__name__ || ''}</span>
<span className="legend-label-brace">{'{'}</span>
{labelNodes}
{labelNodes}
<span className="legend-label-brace">{'}'}</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] + '"');
}

View file

@ -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>
);

View file

@ -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;

View file

@ -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&nbsp;
<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;

View file

@ -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;

View file

@ -20,19 +20,22 @@ 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 : [
{
key: this.getKey(),
options: PanelDefaultOptions,
},
],
panels:
urlPanels.length !== 0
? urlPanels
: [
{
key: this.getKey(),
options: PanelDefaultOptions,
},
],
pastQueries: [],
metricNames: [],
fetchMetricsError: null,
@ -41,44 +44,48 @@ class PanelList extends Component<any, PanelListState> {
}
componentDidMount() {
fetch("../../api/v1/label/__name__/values", {cache: "no-store"})
.then(resp => {
if (resp.ok) {
return resp.json();
} else {
throw new Error('Unexpected response status when fetching metric names: ' + resp.statusText); // TODO extract error
}
})
.then(json => {
this.setState({metricNames: json.data});
})
.catch(error => this.setState({ fetchMetricsError: error.message }));
fetch('../../api/v1/label/__name__/values', { cache: 'no-store' })
.then(resp => {
if (resp.ok) {
return resp.json();
} else {
throw new Error('Unexpected response status when fetching metric names: ' + resp.statusText); // TODO extract error
}
})
.then(json => {
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"})
.then(resp => {
if (resp.ok) {
return resp.json();
} else {
throw new Error('Unexpected response status when fetching metric names: ' + resp.statusText); // TODO extract error
}
})
.then(json => {
const serverTime = json.data.result[0];
const delta = Math.abs(browserTime - serverTime);
fetch('../../api/v1/query?query=time()', { cache: 'no-store' })
.then(resp => {
if (resp.ok) {
return resp.json();
} else {
throw new Error('Unexpected response status when fetching metric names: ' + resp.statusText); // TODO extract error
}
})
.then(json => {
const serverTime = json.data.result[0];
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.');
}
})
.catch(error => this.setState({ timeDriftError: error.message }));
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.'
);
}
})
.catch(error => this.setState({ timeDriftError: error.message }));
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) => {
return metric === query ? acc : [...acc, metric]; // Prevent adding query twice.
}, [query]);
const extendedItems = historyItems.reduce(
(acc, metric) => {
return metric === query ? acc : [...acc, metric]; // Prevent adding query twice.
},
[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>
</>
);
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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 };

View file

@ -1 +1,5 @@
export const uuidGen = () => '_' + Math.random().toString(36).substr(2, 9);
export const uuidGen = () =>
'_' +
Math.random()
.toString(36)
.substr(2, 9);

View file

@ -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';

View file

@ -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) => {
return paramFormat.test(p);
}).sort();
function parseParams(params: string[]): { key: string; options: PanelOptions }[] {
const sortedParams = params
.filter(p => {
return paramFormat.test(p);
})
.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('&');
}

View file

@ -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"