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) cd web/ui && GO111MODULE=$(GO111MODULE) GOOS= GOARCH= $(GO) generate -x -v $(GOOPTS)
@$(GOFMT) -w ./web/ui @$(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 .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" @echo ">> running React app tests"
cd $(REACT_APP_PATH) && yarn test --no-watch 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", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "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": { "prettier": {
"extends": "react-app" "singleQuote": true,
"trailingComma": "es5",
"printWidth": 125
}, },
"browserslist": [ "browserslist": [
">0.2%", ">0.2%",
@ -56,7 +60,20 @@
"devDependencies": { "devDependencies": {
"@types/flot": "0.0.31", "@types/flot": "0.0.31",
"@types/moment-timezone": "^0.5.10", "@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" "proxy": "http://localhost:9090"
} }

View file

@ -1,19 +1,10 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import Navigation from "./Navbar"; import Navigation from './Navbar';
import { Container } from 'reactstrap'; import { Container } from 'reactstrap';
import './App.css'; import './App.css';
import { Router } from '@reach/router'; import { Router } from '@reach/router';
import { import { Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList } from './pages';
Alerts,
Config,
Flags,
Rules,
Services,
Status,
Targets,
PanelList,
} from './pages';
class App extends Component { class App extends Component {
render() { render() {

View file

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

View file

@ -5,33 +5,38 @@ import { Alert, Table } from 'reactstrap';
import SeriesName from './SeriesName'; import SeriesName from './SeriesName';
export interface QueryResult { export interface QueryResult {
data: null | { data:
resultType: 'vector', | null
result: InstantSample[], | {
} | { resultType: 'vector';
resultType: 'matrix', result: InstantSample[];
result: RangeSamples[], }
} | { | {
resultType: 'scalar', resultType: 'matrix';
result: SampleValue, result: RangeSamples[];
} | { }
resultType: 'string', | {
result: string, resultType: 'scalar';
}, result: SampleValue;
}; }
| {
resultType: 'string';
result: string;
};
}
interface InstantSample { interface InstantSample {
metric: Metric, metric: Metric;
value: SampleValue, value: SampleValue;
} }
interface RangeSamples { interface RangeSamples {
metric: Metric, metric: Metric;
values: SampleValue[], values: SampleValue[];
} }
interface Metric { interface Metric {
[key: string]: string, [key: string]: string;
} }
type SampleValue = [number, string]; type SampleValue = [number, string];
@ -59,29 +64,55 @@ class DataTable extends PureComponent<QueryResult> {
let rows: ReactNode[] = []; let rows: ReactNode[] = [];
let limited = false; let limited = false;
switch(data.resultType) { switch (data.resultType) {
case 'vector': case 'vector':
rows = (this.limitSeries(data.result) as InstantSample[]) rows = (this.limitSeries(data.result) as InstantSample[]).map(
.map((s: InstantSample, index: number): ReactNode => { (s: InstantSample, index: number): ReactNode => {
return <tr key={index}><td><SeriesName labels={s.metric} format={false}/></td><td>{s.value[1]}</td></tr>; return (
}); <tr key={index}>
<td>
<SeriesName labels={s.metric} format={false} />
</td>
<td>{s.value[1]}</td>
</tr>
);
}
);
limited = rows.length !== data.result.length; limited = rows.length !== data.result.length;
break; break;
case 'matrix': case 'matrix':
rows = (this.limitSeries(data.result) as RangeSamples[]) rows = (this.limitSeries(data.result) as RangeSamples[]).map((s, index) => {
.map((s, index) => { const valueText = s.values
const valueText = s.values.map((v) => { .map(v => {
return [1] + ' @' + v[0]; 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; limited = rows.length !== data.result.length;
break; break;
case 'scalar': 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; break;
case 'string': 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; break;
default: default:
return <Alert color="danger">Unsupported result value type</Alert>; return <Alert color="danger">Unsupported result value type</Alert>;
@ -89,15 +120,13 @@ class DataTable extends PureComponent<QueryResult> {
return ( return (
<> <>
{limited && {limited && (
<Alert color="danger"> <Alert color="danger">
<strong>Warning:</strong> Fetched {data.result.length} metrics, only displaying first {rows.length}. <strong>Warning:</strong> Fetched {data.result.length} metrics, only displaying first {rows.length}.
</Alert> </Alert>
} )}
<Table hover size="sm" className="data-table"> <Table hover size="sm" className="data-table">
<tbody> <tbody>{rows}</tbody>
{rows}
</tbody>
</Table> </Table>
</> </>
); );

View file

@ -37,8 +37,8 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
super(props); super(props);
this.state = { this.state = {
value: props.value, value: props.value,
height: 'auto' height: 'auto',
} };
} }
componentDidMount() { componentDidMount() {
@ -49,14 +49,17 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
const { offsetHeight, clientHeight, scrollHeight } = this.exprInputRef.current!; const { offsetHeight, clientHeight, scrollHeight } = this.exprInputRef.current!;
const offset = offsetHeight - clientHeight; // Needed in order for the height to be more accurate. const offset = offsetHeight - clientHeight; // Needed in order for the height to be more accurate.
this.setState({ height: scrollHeight + offset }); this.setState({ height: scrollHeight + offset });
} };
handleInput = () => { handleInput = () => {
this.setState({ this.setState(
height: 'auto', {
value: this.exprInputRef.current!.value height: 'auto',
}, this.setHeight); value: this.exprInputRef.current!.value,
} },
this.setHeight
);
};
handleDropdownSelection = (value: string) => { handleDropdownSelection = (value: string) => {
this.setState({ value, height: 'auto' }, this.setHeight); this.setState({ value, height: 'auto' }, this.setHeight);
@ -67,50 +70,54 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
this.props.executeQuery(this.exprInputRef.current!.value); this.props.executeQuery(this.exprInputRef.current!.value);
event.preventDefault(); event.preventDefault();
} }
} };
executeQuery = () => this.props.executeQuery(this.exprInputRef.current!.value); executeQuery = () => this.props.executeQuery(this.exprInputRef.current!.value);
getSearchMatches = (input: string, expressions: string[]) => { getSearchMatches = (input: string, expressions: string[]) => {
return fuzzy.filter(input.replace(/ /g, ''), expressions, { return fuzzy.filter(input.replace(/ /g, ''), expressions, {
pre: "<strong>", pre: '<strong>',
post: "</strong>", post: '</strong>',
}); });
} };
createAutocompleteSection = (downshift: ControllerStateAndHelpers<any>) => { createAutocompleteSection = (downshift: ControllerStateAndHelpers<any>) => {
const { inputValue = '', closeMenu, highlightedIndex } = downshift; const { inputValue = '', closeMenu, highlightedIndex } = downshift;
const { autocompleteSections } = this.props; const { autocompleteSections } = this.props;
let index = 0; let index = 0;
const sections = inputValue!.length ? const sections = inputValue!.length
Object.entries(autocompleteSections).reduce((acc, [title, items]) => { ? Object.entries(autocompleteSections).reduce(
const matches = this.getSearchMatches(inputValue!, items); (acc, [title, items]) => {
return !matches.length ? acc : [ const matches = this.getSearchMatches(inputValue!, items);
...acc, return !matches.length
<Card tag={ListGroup} key={title}> ? acc
<CardHeader style={{ fontSize: 13 }}>{title}</CardHeader> : [
{ ...acc,
matches <Card tag={ListGroup} key={title}>
.slice(0, 100) // Limit DOM rendering to 100 results, as DOM rendering is sloooow. <CardHeader style={{ fontSize: 13 }}>{title}</CardHeader>
.map(({ original, string }) => { {matches
const itemProps = downshift.getItemProps({ .slice(0, 100) // Limit DOM rendering to 100 results, as DOM rendering is sloooow.
key: original, .map(({ original, string }) => {
index, const itemProps = downshift.getItemProps({
item: original, key: original,
style: { index,
backgroundColor: highlightedIndex === index++ ? 'lightgray' : 'white', item: original,
}, style: {
}) backgroundColor: highlightedIndex === index++ ? 'lightgray' : 'white',
return ( },
<SanitizeHTML tag={ListGroupItem} {...itemProps} allowedTags={['strong']}> });
{string} return (
</SanitizeHTML> <SanitizeHTML tag={ListGroupItem} {...itemProps} allowedTags={['strong']}>
); {string}
}) </SanitizeHTML>
} );
</Card> })}
] </Card>,
}, [] as JSX.Element[]) : [] ];
},
[] as JSX.Element[]
)
: [];
if (!sections.length) { if (!sections.length) {
// This is ugly but is needed in order to sync state updates. // This is ugly but is needed in order to sync state updates.
@ -121,19 +128,16 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
return ( return (
<div {...downshift.getMenuProps()} className="autosuggest-dropdown"> <div {...downshift.getMenuProps()} className="autosuggest-dropdown">
{ sections } {sections}
</div> </div>
); );
} };
render() { render() {
const { value, height } = this.state; const { value, height } = this.state;
return ( return (
<Downshift <Downshift inputValue={value} onSelect={this.handleDropdownSelection}>
inputValue={value} {downshift => (
onSelect={this.handleDropdownSelection}
>
{(downshift) => (
<div> <div>
<InputGroup className="expression-input"> <InputGroup className="expression-input">
<InputGroupAddon addonType="prepend"> <InputGroupAddon addonType="prepend">
@ -175,15 +179,11 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
break; break;
default: default:
} }
} },
} as any)} } as any)}
/> />
<InputGroupAddon addonType="append"> <InputGroupAddon addonType="append">
<Button <Button className="execute-btn" color="primary" onClick={this.executeQuery}>
className="execute-btn"
color="primary"
onClick={this.executeQuery}
>
Execute Execute
</Button> </Button>
</InputGroupAddon> </InputGroupAddon>

View file

@ -12,7 +12,7 @@ require('flot/source/jquery.flot.time');
require('flot/source/jquery.canvaswrapper'); require('flot/source/jquery.canvaswrapper');
require('jquery.flot.tooltip'); require('jquery.flot.tooltip');
var graphID = 0; let graphID = 0;
function getGraphID() { function getGraphID() {
// TODO: This is ugly. // TODO: This is ugly.
return graphID++; return graphID++;
@ -22,9 +22,9 @@ interface GraphProps {
data: any; // TODO: Type this. data: any; // TODO: Type this.
stacked: boolean; stacked: boolean;
queryParams: { queryParams: {
startTime: number, startTime: number;
endTime: number, endTime: number;
resolution: number, resolution: number;
} | null; } | null;
} }
@ -33,76 +33,76 @@ class Graph extends PureComponent<GraphProps> {
private chartRef = React.createRef<HTMLDivElement>(); private chartRef = React.createRef<HTMLDivElement>();
escapeHTML(str: string) { escapeHTML(str: string) {
var entityMap: {[key: string]: string} = { const entityMap: { [key: string]: string } = {
'&': '&amp;', '&': '&amp;',
'<': '&lt;', '<': '&lt;',
'>': '&gt;', '>': '&gt;',
'"': '&quot;', '"': '&quot;',
"'": '&#39;', "'": '&#39;',
'/': '&#x2F;' '/': '&#x2F;',
}; };
return String(str).replace(/[&<>"'/]/g, function (s) { return String(str).replace(/[&<>"'/]/g, function(s) {
return entityMap[s]; return entityMap[s];
}); });
} }
renderLabels(labels: {[key: string]: string}) { renderLabels(labels: { [key: string]: string }) {
let labelStrings: string[] = []; const labelStrings: string[] = [];
for (let label in labels) { for (const label in labels) {
if (label !== '__name__') { if (label !== '__name__') {
labelStrings.push('<strong>' + label + '</strong>: ' + this.escapeHTML(labels[label])); labelStrings.push('<strong>' + label + '</strong>: ' + this.escapeHTML(labels[label]));
} }
} }
return '<div class="labels">' + labelStrings.join('<br>') + '</div>'; return '<div class="labels">' + labelStrings.join('<br>') + '</div>';
}; }
formatValue = (y: number | null): string => { formatValue = (y: number | null): string => {
if (y === null) { if (y === null) {
return 'null'; return 'null';
} }
var abs_y = Math.abs(y); const abs_y = Math.abs(y);
if (abs_y >= 1e24) { if (abs_y >= 1e24) {
return (y / 1e24).toFixed(2) + "Y"; return (y / 1e24).toFixed(2) + 'Y';
} else if (abs_y >= 1e21) { } else if (abs_y >= 1e21) {
return (y / 1e21).toFixed(2) + "Z"; return (y / 1e21).toFixed(2) + 'Z';
} else if (abs_y >= 1e18) { } else if (abs_y >= 1e18) {
return (y / 1e18).toFixed(2) + "E"; return (y / 1e18).toFixed(2) + 'E';
} else if (abs_y >= 1e15) { } else if (abs_y >= 1e15) {
return (y / 1e15).toFixed(2) + "P"; return (y / 1e15).toFixed(2) + 'P';
} else if (abs_y >= 1e12) { } else if (abs_y >= 1e12) {
return (y / 1e12).toFixed(2) + "T"; return (y / 1e12).toFixed(2) + 'T';
} else if (abs_y >= 1e9) { } else if (abs_y >= 1e9) {
return (y / 1e9).toFixed(2) + "G"; return (y / 1e9).toFixed(2) + 'G';
} else if (abs_y >= 1e6) { } else if (abs_y >= 1e6) {
return (y / 1e6).toFixed(2) + "M"; return (y / 1e6).toFixed(2) + 'M';
} else if (abs_y >= 1e3) { } else if (abs_y >= 1e3) {
return (y / 1e3).toFixed(2) + "k"; return (y / 1e3).toFixed(2) + 'k';
} else if (abs_y >= 1) { } else if (abs_y >= 1) {
return y.toFixed(2) return y.toFixed(2);
} else if (abs_y === 0) { } else if (abs_y === 0) {
return y.toFixed(2) return y.toFixed(2);
} else if (abs_y <= 1e-24) { } 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) { } 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) { } 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) { } 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) { } 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) { } 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) { } else if (abs_y <= 1e-6) {
return (y / 1e-6).toFixed(2) + "µ"; return (y / 1e-6).toFixed(2) + 'µ';
} else if (abs_y <=1e-3) { } else if (abs_y <= 1e-3) {
return (y / 1e-3).toFixed(2) + "m"; return (y / 1e-3).toFixed(2) + 'm';
} else if (abs_y <= 1) { } else if (abs_y <= 1) {
return y.toFixed(2) return y.toFixed(2);
} }
throw Error("couldn't format a value, this is a bug"); throw Error("couldn't format a value, this is a bug");
} };
getOptions(): any { getOptions(): any {
return { return {
@ -133,9 +133,9 @@ class Graph extends PureComponent<GraphProps> {
cssClass: 'graph-tooltip', cssClass: 'graph-tooltip',
content: (label: string, xval: number, yval: number, flotItem: any) => { content: (label: string, xval: number, yval: number, flotItem: any) => {
const series = flotItem.series; // TODO: type this. const series = flotItem.series; // TODO: type this.
var date = '<span class="date">' + new Date(xval).toUTCString() + '</span>'; const date = '<span class="date">' + new Date(xval).toUTCString() + '</span>';
var swatch = '<span class="detail-swatch" style="background-color: ' + series.color + '"></span>'; const swatch = '<span class="detail-swatch" style="background-color: ' + series.color + '"></span>';
var content = swatch + (series.labels.__name__ || 'value') + ": <strong>" + yval + '</strong>'; const content = swatch + (series.labels.__name__ || 'value') + ': <strong>' + yval + '</strong>';
return date + '<br>' + content + '<br>' + this.renderLabels(series.labels); return date + '<br>' + content + '<br>' + this.renderLabels(series.labels);
}, },
defaultTheme: false, defaultTheme: false,
@ -149,20 +149,20 @@ class Graph extends PureComponent<GraphProps> {
fill: this.props.stacked, fill: this.props.stacked,
}, },
shadowSize: 0, shadowSize: 0,
} },
}; };
} }
// This was adapted from Flot's color generation code. // This was adapted from Flot's color generation code.
getColors() { getColors() {
let colors = []; const colors = [];
const colorPool = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"]; const colorPool = ['#edc240', '#afd8f8', '#cb4b4b', '#4da74d', '#9440ed'];
const colorPoolSize = colorPool.length; const colorPoolSize = colorPool.length;
let variation = 0; let variation = 0;
const neededColors = this.props.data.result.length; const neededColors = this.props.data.result.length;
for (let i = 0; i < neededColors; i++) { 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 // Each time we exhaust the colors in the pool we adjust
// a scaling factor used to produce more variations on // 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) => { return this.props.data.result.map((ts: any /* TODO: Type this*/, index: number) => {
// Insert nulls for all missing steps. // Insert nulls for all missing steps.
let data = []; const data = [];
let pos = 0; let pos = 0;
const params = this.props.queryParams!; const params = this.props.queryParams!;
@ -214,11 +214,11 @@ class Graph extends PureComponent<GraphProps> {
color: colors[index], color: colors[index],
index: index, index: index,
}; };
}) });
} }
parseValue(value: string) { parseValue(value: string) {
var val = parseFloat(value); const val = parseFloat(value);
if (isNaN(val)) { if (isNaN(val)) {
// "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). They // "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). They
// can't be graphed, so show them as gaps (null). // can't be graphed, so show them as gaps (null).
@ -229,7 +229,7 @@ class Graph extends PureComponent<GraphProps> {
return this.props.stacked ? 0 : null; return this.props.stacked ? 0 : null;
} }
return val; return val;
}; }
componentDidMount() { componentDidMount() {
this.plot(); this.plot();
@ -264,7 +264,11 @@ class Graph extends PureComponent<GraphProps> {
} }
if (this.props.data.resultType !== 'matrix') { 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) { if (this.props.data.result.length === 0) {
@ -275,7 +279,7 @@ class Graph extends PureComponent<GraphProps> {
<div className="graph"> <div className="graph">
<ReactResizeDetector handleWidth onResize={() => this.plot()} /> <ReactResizeDetector handleWidth onResize={() => this.plot()} />
<div className="graph-chart" ref={this.chartRef} /> <div className="graph-chart" ref={this.chartRef} />
<Legend series={this.getData()}/> <Legend series={this.getData()} />
</div> </div>
); );
} }

View file

@ -1,20 +1,8 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { import { Button, ButtonGroup, Form, InputGroup, InputGroupAddon, Input } from 'reactstrap';
Button,
ButtonGroup,
Form,
InputGroup,
InputGroupAddon,
Input,
} from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { import { faPlus, faMinus, faChartArea, faChartLine } from '@fortawesome/free-solid-svg-icons';
faPlus,
faMinus,
faChartArea,
faChartLine,
} from '@fortawesome/free-solid-svg-icons';
import TimeInput from './TimeInput'; import TimeInput from './TimeInput';
import { parseRange, formatRange } from './utils/timeFormat'; import { parseRange, formatRange } from './utils/timeFormat';
@ -39,22 +27,22 @@ class GraphControls extends Component<GraphControlsProps> {
1, 1,
10, 10,
60, 60,
5*60, 5 * 60,
15*60, 15 * 60,
30*60, 30 * 60,
60*60, 60 * 60,
2*60*60, 2 * 60 * 60,
6*60*60, 6 * 60 * 60,
12*60*60, 12 * 60 * 60,
24*60*60, 24 * 60 * 60,
48*60*60, 48 * 60 * 60,
7*24*60*60, 7 * 24 * 60 * 60,
14*24*60*60, 14 * 24 * 60 * 60,
28*24*60*60, 28 * 24 * 60 * 60,
56*24*60*60, 56 * 24 * 60 * 60,
365*24*60*60, 365 * 24 * 60 * 60,
730*24*60*60, 730 * 24 * 60 * 60,
] ];
onChangeRangeInput = (rangeText: string): void => { onChangeRangeInput = (rangeText: string): void => {
const range = parseRange(rangeText); const range = parseRange(rangeText);
@ -63,31 +51,31 @@ class GraphControls extends Component<GraphControlsProps> {
} else { } else {
this.props.onChangeRange(range); this.props.onChangeRange(range);
} }
} };
changeRangeInput = (range: number): void => { changeRangeInput = (range: number): void => {
this.rangeRef.current!.value = formatRange(range); this.rangeRef.current!.value = formatRange(range);
} };
increaseRange = (): void => { increaseRange = (): void => {
for (let range of this.rangeSteps) { for (const range of this.rangeSteps) {
if (this.props.range < range) { if (this.props.range < range) {
this.changeRangeInput(range); this.changeRangeInput(range);
this.props.onChangeRange(range); this.props.onChangeRange(range);
return; return;
} }
} }
} };
decreaseRange = (): void => { decreaseRange = (): void => {
for (let range of this.rangeSteps.slice().reverse()) { for (const range of this.rangeSteps.slice().reverse()) {
if (this.props.range > range) { if (this.props.range > range) {
this.changeRangeInput(range); this.changeRangeInput(range);
this.props.onChangeRange(range); this.props.onChangeRange(range);
return; return;
} }
} }
} };
componentDidUpdate(prevProps: GraphControlsProps) { componentDidUpdate(prevProps: GraphControlsProps) {
if (prevProps.range !== this.props.range) { if (prevProps.range !== this.props.range) {
@ -103,7 +91,9 @@ class GraphControls extends Component<GraphControlsProps> {
<Form inline className="graph-controls" onSubmit={e => e.preventDefault()}> <Form inline className="graph-controls" onSubmit={e => e.preventDefault()}>
<InputGroup className="range-input" size="sm"> <InputGroup className="range-input" size="sm">
<InputGroupAddon addonType="prepend"> <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> </InputGroupAddon>
<Input <Input
@ -113,7 +103,9 @@ class GraphControls extends Component<GraphControlsProps> {
/> />
<InputGroupAddon addonType="append"> <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> </InputGroupAddon>
</InputGroup> </InputGroup>
@ -137,8 +129,16 @@ class GraphControls extends Component<GraphControlsProps> {
/> />
<ButtonGroup className="stacked-input" size="sm"> <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
<Button title="Show stacked graph" onClick={() => this.props.onChangeStacking(true)} active={this.props.stacked}><FontAwesomeIcon icon={faChartArea} fixedWidth/></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> </ButtonGroup>
</Form> </Form>
); );

View file

@ -11,7 +11,7 @@ class Legend extends PureComponent<LegendProps> {
return ( return (
<tr key={s.index} className="legend-item"> <tr key={s.index} className="legend-item">
<td> <td>
<div className="legend-swatch" style={{backgroundColor: s.color}}></div> <div className="legend-swatch" style={{ backgroundColor: s.color }}></div>
</td> </td>
<td> <td>
<SeriesName labels={s.labels} format={true} /> <SeriesName labels={s.labels} format={true} />
@ -24,7 +24,9 @@ class Legend extends PureComponent<LegendProps> {
return ( return (
<table className="graph-legend"> <table className="graph-legend">
<tbody> <tbody>
{this.props.series.map((s: any) => {return this.renderLegendItem(s)})} {this.props.series.map((s: any) => {
return this.renderLegendItem(s);
})}
</tbody> </tbody>
</table> </table>
); );

View file

@ -1,16 +1,16 @@
function metricToSeriesName(labels: {[key: string]: string}): string { function metricToSeriesName(labels: { [key: string]: string }): string {
if (labels === null) { if (labels === null) {
return 'scalar'; return 'scalar';
} }
let tsName = (labels.__name__ || '') + '{'; let tsName = (labels.__name__ || '') + '{';
let labelStrings: string[] = []; const labelStrings: string[] = [];
for (let label in labels) { for (const label in labels) {
if (label !== '__name__') { if (label !== '__name__') {
labelStrings.push(label + '="' + labels[label] + '"'); labelStrings.push(label + '="' + labels[label] + '"');
} }
} }
tsName += labelStrings.join(', ') + '}'; tsName += labelStrings.join(', ') + '}';
return tsName; return tsName;
}; }
export default metricToSeriesName; export default metricToSeriesName;

View file

@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Link } from "@reach/router"; import { Link } from '@reach/router';
import { import {
Collapse, Collapse,
Navbar, Navbar,
@ -18,37 +18,59 @@ const Navigation = () => {
const toggle = () => setIsOpen(!isOpen); const toggle = () => setIsOpen(!isOpen);
return ( return (
<Navbar className="mb-3" dark color="dark" expand="md"> <Navbar className="mb-3" dark color="dark" expand="md">
<NavbarToggler onClick={toggle}/> <NavbarToggler onClick={toggle} />
<Link className="pt-0 navbar-brand" to="/new/graph">Prometheus</Link> <Link className="pt-0 navbar-brand" to="/new/graph">
Prometheus
</Link>
<Collapse isOpen={isOpen} navbar style={{ justifyContent: 'space-between' }}> <Collapse isOpen={isOpen} navbar style={{ justifyContent: 'space-between' }}>
<Nav className="ml-0" navbar> <Nav className="ml-0" navbar>
<NavItem> <NavItem>
<NavLink tag={Link} to="/new/alerts">Alerts</NavLink> <NavLink tag={Link} to="/new/alerts">
Alerts
</NavLink>
</NavItem> </NavItem>
<NavItem> <NavItem>
<NavLink tag={Link} to="/new/graph">Graph</NavLink> <NavLink tag={Link} to="/new/graph">
Graph
</NavLink>
</NavItem> </NavItem>
<UncontrolledDropdown nav inNavbar> <UncontrolledDropdown nav inNavbar>
<DropdownToggle nav caret>Status</DropdownToggle> <DropdownToggle nav caret>
Status
</DropdownToggle>
<DropdownMenu> <DropdownMenu>
<DropdownItem tag={Link} to="/new/status">Runtime & Build Information</DropdownItem> <DropdownItem tag={Link} to="/new/status">
<DropdownItem tag={Link} to="/new/flags">Command-Line Flags</DropdownItem> Runtime & Build Information
<DropdownItem tag={Link} to="/new/config">Configuration</DropdownItem> </DropdownItem>
<DropdownItem tag={Link} to="/new/rules">Rules</DropdownItem> <DropdownItem tag={Link} to="/new/flags">
<DropdownItem tag={Link} to="/new/targets">Targets</DropdownItem> Command-Line Flags
<DropdownItem tag={Link} to="/new/service-discovery">Service Discovery</DropdownItem> </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> </DropdownMenu>
</UncontrolledDropdown> </UncontrolledDropdown>
<NavItem> <NavItem>
<NavLink href="https://prometheus.io/docs/prometheus/latest/getting_started/">Help</NavLink> <NavLink href="https://prometheus.io/docs/prometheus/latest/getting_started/">Help</NavLink>
</NavItem> </NavItem>
<NavItem> <NavItem>
<NavLink tag={Link} to="../../graph">Classic UI</NavLink> <NavLink tag={Link} to="../../graph">
Classic UI
</NavLink>
</NavItem> </NavItem>
</Nav> </Nav>
</Collapse> </Collapse>
</Navbar> </Navbar>
); );
} };
export default Navigation; export default Navigation;

View file

@ -1,16 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { import { Alert, Button, Col, Nav, NavItem, NavLink, Row, TabContent, TabPane } from 'reactstrap';
Alert,
Button,
Col,
Nav,
NavItem,
NavLink,
Row,
TabContent,
TabPane,
} from 'reactstrap';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
@ -32,14 +22,15 @@ interface PanelProps {
interface PanelState { interface PanelState {
data: any; // TODO: Type data. data: any; // TODO: Type data.
lastQueryParams: { // TODO: Share these with Graph.tsx in a file. lastQueryParams: {
startTime: number, // TODO: Share these with Graph.tsx in a file.
endTime: number, startTime: number;
resolution: number, endTime: number;
resolution: number;
} | null; } | null;
loading: boolean; loading: boolean;
error: string | null; error: string | null;
stats: QueryStats | null, stats: QueryStats | null;
} }
export interface PanelOptions { export interface PanelOptions {
@ -63,7 +54,7 @@ export const PanelDefaultOptions: PanelOptions = {
endTime: null, endTime: null,
resolution: null, resolution: null,
stacked: false, stacked: false,
} };
class Panel extends Component<PanelProps, PanelState> { class Panel extends Component<PanelProps, PanelState> {
private abortInFlightFetch: (() => void) | null = null; private abortInFlightFetch: (() => void) | null = null;
@ -83,16 +74,17 @@ class Panel extends Component<PanelProps, PanelState> {
componentDidUpdate(prevProps: PanelProps, prevState: PanelState) { componentDidUpdate(prevProps: PanelProps, prevState: PanelState) {
const prevOpts = prevProps.options; const prevOpts = prevProps.options;
const opts = this.props.options; const opts = this.props.options;
if (prevOpts.type !== opts.type || if (
prevOpts.range !== opts.range || prevOpts.type !== opts.type ||
prevOpts.endTime !== opts.endTime || prevOpts.range !== opts.range ||
prevOpts.resolution !== opts.resolution) { prevOpts.endTime !== opts.endTime ||
prevOpts.resolution !== opts.resolution
) {
if (prevOpts.type !== opts.type) { if (prevOpts.type !== opts.type) {
// If the other options change, we still want to show the old data until the new // If the other options change, we still want to show the old data until the new
// query completes, but this is not a good idea when we actually change between // query completes, but this is not a good idea when we actually change between
// table and graph view, since not all queries work well in both. // table and graph view, since not all queries work well in both.
this.setState({data: null}); this.setState({ data: null });
} }
this.executeQuery(opts.expr); this.executeQuery(opts.expr);
} }
@ -104,9 +96,9 @@ class Panel extends Component<PanelProps, PanelState> {
executeQuery = (expr: string): void => { executeQuery = (expr: string): void => {
const queryStart = Date.now(); const queryStart = Date.now();
this.props.onExecuteQuery(expr) this.props.onExecuteQuery(expr);
if (this.props.options.expr !== expr) { if (this.props.options.expr !== expr) {
this.setOptions({expr: expr}); this.setOptions({ expr: expr });
} }
if (expr === '') { if (expr === '') {
return; return;
@ -119,114 +111,114 @@ class Panel extends Component<PanelProps, PanelState> {
const abortController = new AbortController(); const abortController = new AbortController();
this.abortInFlightFetch = () => abortController.abort(); 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 endTime = this.getEndTime().valueOf() / 1000; // TODO: shouldn't valueof only work when it's a moment?
const startTime = endTime - this.props.options.range; const startTime = endTime - this.props.options.range;
const resolution = this.props.options.resolution || Math.max(Math.floor(this.props.options.range / 250), 1); const resolution = this.props.options.resolution || Math.max(Math.floor(this.props.options.range / 250), 1);
const url = new URL(window.location.href); const url = new URL(window.location.href);
const params: {[key: string]: string} = { const params: { [key: string]: string } = {
'query': expr, query: expr,
}; };
switch (this.props.options.type) { switch (this.props.options.type) {
case 'graph': case 'graph':
url.pathname = '../../api/v1/query_range' url.pathname = '../../api/v1/query_range';
Object.assign(params, { Object.assign(params, {
start: startTime, start: startTime,
end: endTime, end: endTime,
step: resolution, step: resolution,
}) });
// TODO path prefix here and elsewhere. // TODO path prefix here and elsewhere.
break; break;
case 'table': case 'table':
url.pathname = '../../api/v1/query' url.pathname = '../../api/v1/query';
Object.assign(params, { Object.assign(params, {
time: endTime, time: endTime,
}) });
break; break;
default: default:
throw new Error('Invalid panel type "' + this.props.options.type + '"'); 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(resp => resp.json())
.then(json => { .then(json => {
if (json.status !== 'success') { if (json.status !== 'success') {
throw new Error(json.error || 'invalid response JSON'); 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;
} }
}
this.setState({ let resultSeries = 0;
error: null, if (json.data) {
data: json.data, const { resultType, result } = json.data;
lastQueryParams: { if (resultType === 'scalar') {
startTime, resultSeries = 1;
endTime, } else if (result && result.length > 0) {
resolution, resultSeries = result.length;
}, }
stats: { }
loadTime: Date.now() - queryStart,
resolution, this.setState({
resultSeries error: null,
}, data: json.data,
loading: false, lastQueryParams: {
}); startTime,
this.abortInFlightFetch = null; endTime,
}) resolution,
.catch(error => { },
if (error.name === 'AbortError') { stats: {
// Aborts are expected, don't show an error for them. loadTime: Date.now() - queryStart,
return resolution,
} resultSeries,
this.setState({ },
error: 'Error executing query: ' + error.message, loading: false,
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 { setOptions(opts: object): void {
const newOpts = {...this.props.options, ...opts}; const newOpts = { ...this.props.options, ...opts };
this.props.onOptionsChanged(newOpts); this.props.onOptionsChanged(newOpts);
} }
handleExpressionChange = (expr: string): void => { handleExpressionChange = (expr: string): void => {
this.setOptions({expr: expr}); this.setOptions({ expr: expr });
} };
handleChangeRange = (range: number): void => { handleChangeRange = (range: number): void => {
this.setOptions({range: range}); this.setOptions({ range: range });
} };
getEndTime = (): number | moment.Moment => { getEndTime = (): number | moment.Moment => {
if (this.props.options.endTime === null) { if (this.props.options.endTime === null) {
return moment(); return moment();
} }
return this.props.options.endTime; return this.props.options.endTime;
} };
handleChangeEndTime = (endTime: number | null) => { handleChangeEndTime = (endTime: number | null) => {
this.setOptions({endTime: endTime}); this.setOptions({ endTime: endTime });
} };
handleChangeResolution = (resolution: number | null) => { handleChangeResolution = (resolution: number | null) => {
this.setOptions({resolution: resolution}); this.setOptions({ resolution: resolution });
} };
handleChangeStacking = (stacked: boolean) => { handleChangeStacking = (stacked: boolean) => {
this.setOptions({stacked: stacked}); this.setOptions({ stacked: stacked });
} };
render() { render() {
const { pastQueries, metricNames, options } = this.props; const { pastQueries, metricNames, options } = this.props;
@ -246,9 +238,7 @@ class Panel extends Component<PanelProps, PanelState> {
</Col> </Col>
</Row> </Row>
<Row> <Row>
<Col> <Col>{this.state.error && <Alert color="danger">{this.state.error}</Alert>}</Col>
{this.state.error && <Alert color="danger">{this.state.error}</Alert>}
</Col>
</Row> </Row>
<Row> <Row>
<Col> <Col>
@ -256,7 +246,9 @@ class Panel extends Component<PanelProps, PanelState> {
<NavItem> <NavItem>
<NavLink <NavLink
className={options.type === 'table' ? 'active' : ''} className={options.type === 'table' ? 'active' : ''}
onClick={() => { this.setOptions({type: 'table'}); }} onClick={() => {
this.setOptions({ type: 'table' });
}}
> >
Table Table
</NavLink> </NavLink>
@ -264,19 +256,18 @@ class Panel extends Component<PanelProps, PanelState> {
<NavItem> <NavItem>
<NavLink <NavLink
className={options.type === 'graph' ? 'active' : ''} className={options.type === 'graph' ? 'active' : ''}
onClick={() => { this.setOptions({type: 'graph'}); }} onClick={() => {
this.setOptions({ type: 'graph' });
}}
> >
Graph Graph
</NavLink> </NavLink>
</NavItem> </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> </Nav>
<TabContent activeTab={options.type}> <TabContent activeTab={options.type}>
<TabPane tabId="table"> <TabPane tabId="table">
{options.type === 'table' && {options.type === 'table' && (
<> <>
<div className="table-controls"> <div className="table-controls">
<TimeInput <TimeInput
@ -288,17 +279,16 @@ class Panel extends Component<PanelProps, PanelState> {
</div> </div>
<DataTable data={this.state.data} /> <DataTable data={this.state.data} />
</> </>
} )}
</TabPane> </TabPane>
<TabPane tabId="graph"> <TabPane tabId="graph">
{this.props.options.type === 'graph' && {this.props.options.type === 'graph' && (
<> <>
<GraphControls <GraphControls
range={options.range} range={options.range}
endTime={options.endTime} endTime={options.endTime}
resolution={options.resolution} resolution={options.resolution}
stacked={options.stacked} stacked={options.stacked}
onChangeRange={this.handleChangeRange} onChangeRange={this.handleChangeRange}
onChangeEndTime={this.handleChangeEndTime} onChangeEndTime={this.handleChangeEndTime}
onChangeResolution={this.handleChangeResolution} 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} /> <Graph data={this.state.data} stacked={options.stacked} queryParams={this.state.lastQueryParams} />
</> </>
} )}
</TabPane> </TabPane>
</TabContent> </TabContent>
</Col> </Col>
</Row> </Row>
<Row> <Row>
<Col> <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> </Col>
</Row> </Row>
</div> </div>

View file

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

View file

@ -1,7 +1,7 @@
import React, { PureComponent } from "react"; import React, { PureComponent } from 'react';
interface SeriesNameProps { interface SeriesNameProps {
labels: {[key: string]: string} | null; labels: { [key: string]: string } | null;
format: boolean; format: boolean;
} }
@ -9,9 +9,9 @@ class SeriesName extends PureComponent<SeriesNameProps> {
renderFormatted(): React.ReactNode { renderFormatted(): React.ReactNode {
const labels = this.props.labels!; const labels = this.props.labels!;
let labelNodes: React.ReactNode[] = []; const labelNodes: React.ReactNode[] = [];
let first = true; let first = true;
for (let label in labels) { for (const label in labels) {
if (label === '__name__') { if (label === '__name__') {
continue; continue;
} }
@ -19,8 +19,7 @@ class SeriesName extends PureComponent<SeriesNameProps> {
labelNodes.push( labelNodes.push(
<span key={label}> <span key={label}>
{!first && ', '} {!first && ', '}
<span className="legend-label-name">{label}</span>= <span className="legend-label-name">{label}</span>=<span className="legend-label-value">"{labels[label]}"</span>
<span className="legend-label-value">"{labels[label]}"</span>
</span> </span>
); );
@ -33,7 +32,7 @@ class SeriesName extends PureComponent<SeriesNameProps> {
<> <>
<span className="legend-metric-name">{labels.__name__ || ''}</span> <span className="legend-metric-name">{labels.__name__ || ''}</span>
<span className="legend-label-brace">{'{'}</span> <span className="legend-label-brace">{'{'}</span>
{labelNodes} {labelNodes}
<span className="legend-label-brace">{'}'}</span> <span className="legend-label-brace">{'}'}</span>
</> </>
); );
@ -43,8 +42,8 @@ class SeriesName extends PureComponent<SeriesNameProps> {
const labels = this.props.labels!; const labels = this.props.labels!;
let tsName = (labels.__name__ || '') + '{'; let tsName = (labels.__name__ || '') + '{';
let labelStrings: string[] = []; const labelStrings: string[] = [];
for (let label in labels) { for (const label in labels) {
if (label !== '__name__') { if (label !== '__name__') {
labelStrings.push(label + '="' + labels[label] + '"'); labelStrings.push(label + '="' + labels[label] + '"');
} }

View file

@ -19,12 +19,7 @@ import {
faTimes, faTimes,
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
library.add( library.add(faCalendarCheck, faArrowUp, faArrowDown, faTimes);
faCalendarCheck,
faArrowUp,
faArrowDown,
faTimes,
);
// Sadly needed to also replace <i> within the date picker, since it's not a React component. // Sadly needed to also replace <i> within the date picker, since it's not a React component.
dom.watch(); dom.watch();
@ -42,21 +37,21 @@ class TimeInput extends Component<TimeInputProps> {
getBaseTime = (): number => { getBaseTime = (): number => {
return this.props.time || moment().valueOf(); return this.props.time || moment().valueOf();
} };
increaseTime = (): void => { increaseTime = (): void => {
const time = this.getBaseTime() + this.props.range*1000/2; const time = this.getBaseTime() + (this.props.range * 1000) / 2;
this.props.onChangeTime(time); this.props.onChangeTime(time);
} };
decreaseTime = (): void => { decreaseTime = (): void => {
const time = this.getBaseTime() - this.props.range*1000/2; const time = this.getBaseTime() - (this.props.range * 1000) / 2;
this.props.onChangeTime(time); this.props.onChangeTime(time);
} };
clearTime = (): void => { clearTime = (): void => {
this.props.onChangeTime(null); this.props.onChangeTime(null);
} };
componentDidMount() { componentDidMount() {
this.$time = $(this.timeInputRef.current!); this.$time = $(this.timeInputRef.current!);
@ -96,7 +91,9 @@ class TimeInput extends Component<TimeInputProps> {
return ( return (
<InputGroup className="time-input" size="sm"> <InputGroup className="time-input" size="sm">
<InputGroupAddon addonType="prepend"> <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> </InputGroupAddon>
<Input <Input
@ -104,19 +101,23 @@ class TimeInput extends Component<TimeInputProps> {
innerRef={this.timeInputRef} innerRef={this.timeInputRef}
onFocus={() => this.$time.datetimepicker('show')} onFocus={() => this.$time.datetimepicker('show')}
onBlur={() => this.$time.datetimepicker('hide')} 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, {/* CAUTION: While the datetimepicker also has an option to show a 'clear' button,
that functionality is broken, so we create an external solution instead. */} that functionality is broken, so we create an external solution instead. */}
{this.props.time && {this.props.time && (
<InputGroupAddon addonType="append"> <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>
} )}
<InputGroupAddon addonType="append"> <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> </InputGroupAddon>
</InputGroup> </InputGroup>
); );

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { RouteComponentProps } from '@reach/router'; 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; export default Alerts;

View file

@ -9,7 +9,7 @@ import './Config.css';
const Config: FC<RouteComponentProps> = () => { const Config: FC<RouteComponentProps> = () => {
const [config, setConfig] = useState(null); const [config, setConfig] = useState(null);
const [error, setError] = useState(""); const [error, setError] = useState('');
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
useEffect(() => { useEffect(() => {
@ -25,7 +25,10 @@ const Config: FC<RouteComponentProps> = () => {
Configuration&nbsp; Configuration&nbsp;
<CopyToClipboard <CopyToClipboard
text={config ? config! : ''} text={config ? config! : ''}
onCopy={(text, result) => { setCopied(result); setTimeout(setCopied, 1500);}} onCopy={(text, result) => {
setCopied(result);
setTimeout(setCopied, 1500);
}}
> >
<Button color="light" disabled={!config}> <Button color="light" disabled={!config}>
{copied ? 'Copied' : 'Copy to clipboard'} {copied ? 'Copied' : 'Copy to clipboard'}
@ -33,14 +36,17 @@ const Config: FC<RouteComponentProps> = () => {
</CopyToClipboard> </CopyToClipboard>
</h2> </h2>
{error {error ? (
? <Alert color="danger"><strong>Error:</strong> Error fetching configuration: {error}</Alert> <Alert color="danger">
: config <strong>Error:</strong> Error fetching configuration: {error}
? <pre className="config-yaml">{config}</pre> </Alert>
: <FontAwesomeIcon icon={faSpinner} spin /> ) : config ? (
} <pre className="config-yaml">{config}</pre>
) : (
<FontAwesomeIcon icon={faSpinner} spin />
)}
</> </>
) );
} };
export default Config; export default Config;

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { RouteComponentProps } from '@reach/router'; import { RouteComponentProps } from '@reach/router';
const Flags : FC<RouteComponentProps> = () => <div>Flags page</div> const Flags: FC<RouteComponentProps> = () => <div>Flags page</div>;
export default Flags; export default Flags;

View file

@ -20,19 +20,22 @@ interface PanelListState {
} }
class PanelList extends Component<any, PanelListState> { class PanelList extends Component<any, PanelListState> {
private key: number = 0; private key = 0;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
const urlPanels = decodePanelOptionsFromQueryString(window.location.search); const urlPanels = decodePanelOptionsFromQueryString(window.location.search);
this.state = { this.state = {
panels: urlPanels.length !== 0 ? urlPanels : [ panels:
{ urlPanels.length !== 0
key: this.getKey(), ? urlPanels
options: PanelDefaultOptions, : [
}, {
], key: this.getKey(),
options: PanelDefaultOptions,
},
],
pastQueries: [], pastQueries: [],
metricNames: [], metricNames: [],
fetchMetricsError: null, fetchMetricsError: null,
@ -41,44 +44,48 @@ class PanelList extends Component<any, PanelListState> {
} }
componentDidMount() { componentDidMount() {
fetch("../../api/v1/label/__name__/values", {cache: "no-store"}) fetch('../../api/v1/label/__name__/values', { cache: 'no-store' })
.then(resp => { .then(resp => {
if (resp.ok) { if (resp.ok) {
return resp.json(); return resp.json();
} else { } else {
throw new Error('Unexpected response status when fetching metric names: ' + resp.statusText); // TODO extract error throw new Error('Unexpected response status when fetching metric names: ' + resp.statusText); // TODO extract error
} }
}) })
.then(json => { .then(json => {
this.setState({metricNames: json.data}); this.setState({ metricNames: json.data });
}) })
.catch(error => this.setState({ fetchMetricsError: error.message })); .catch(error => this.setState({ fetchMetricsError: error.message }));
const browserTime = new Date().getTime() / 1000; 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 => { .then(resp => {
if (resp.ok) { if (resp.ok) {
return resp.json(); return resp.json();
} else { } else {
throw new Error('Unexpected response status when fetching metric names: ' + resp.statusText); // TODO extract error throw new Error('Unexpected response status when fetching metric names: ' + resp.statusText); // TODO extract error
} }
}) })
.then(json => { .then(json => {
const serverTime = json.data.result[0]; const serverTime = json.data.result[0];
const delta = Math.abs(browserTime - serverTime); const delta = Math.abs(browserTime - serverTime);
if (delta >= 30) { 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 +
.catch(error => this.setState({ timeDriftError: error.message })); ' 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 = () => { window.onpopstate = () => {
const panels = decodePanelOptionsFromQueryString(window.location.search); const panels = decodePanelOptionsFromQueryString(window.location.search);
if (panels.length !== 0) { if (panels.length !== 0) {
this.setState({panels: panels}); this.setState({ panels: panels });
} }
} };
this.updatePastQueries(); this.updatePastQueries();
} }
@ -90,13 +97,13 @@ class PanelList extends Component<any, PanelListState> {
toggleQueryHistory = (e: ChangeEvent<HTMLInputElement>) => { toggleQueryHistory = (e: ChangeEvent<HTMLInputElement>) => {
localStorage.setItem('enable-query-history', `${e.target.checked}`); localStorage.setItem('enable-query-history', `${e.target.checked}`);
this.updatePastQueries(); this.updatePastQueries();
} };
updatePastQueries = () => { updatePastQueries = () => {
this.setState({ this.setState({
pastQueries: this.isHistoryEnabled() ? this.getHistoryItems() : [] pastQueries: this.isHistoryEnabled() ? this.getHistoryItems() : [],
}); });
} };
handleQueryHistory = (query: string) => { handleQueryHistory = (query: string) => {
const isSimpleMetric = this.state.metricNames.indexOf(query) !== -1; const isSimpleMetric = this.state.metricNames.indexOf(query) !== -1;
@ -104,12 +111,15 @@ class PanelList extends Component<any, PanelListState> {
return; return;
} }
const historyItems = this.getHistoryItems(); const historyItems = this.getHistoryItems();
const extendedItems = historyItems.reduce((acc, metric) => { const extendedItems = historyItems.reduce(
return metric === query ? acc : [...acc, metric]; // Prevent adding query twice. (acc, metric) => {
}, [query]); return metric === query ? acc : [...acc, metric]; // Prevent adding query twice.
},
[query]
);
localStorage.setItem('history', JSON.stringify(extendedItems.slice(0, 50))); localStorage.setItem('history', JSON.stringify(extendedItems.slice(0, 50)));
this.updatePastQueries(); this.updatePastQueries();
} };
getKey(): string { getKey(): string {
return (this.key++).toString(); return (this.key++).toString();
@ -121,11 +131,11 @@ class PanelList extends Component<any, PanelListState> {
return { return {
key: key, key: key,
options: opts, options: opts,
} };
} }
return p; return p;
}); });
this.setState({panels: newPanels}, this.updateURL) this.setState({ panels: newPanels }, this.updateURL);
} }
updateURL(): void { updateURL(): void {
@ -139,15 +149,15 @@ class PanelList extends Component<any, PanelListState> {
key: this.getKey(), key: this.getKey(),
options: PanelDefaultOptions, options: PanelDefaultOptions,
}); });
this.setState({panels: panels}, this.updateURL); this.setState({ panels: panels }, this.updateURL);
} };
removePanel = (key: string): void => { removePanel = (key: string): void => {
const panels = this.state.panels.filter(panel => { const panels = this.state.panels.filter(panel => {
return panel.key !== key; return panel.key !== key;
}); });
this.setState({panels: panels}, this.updateURL); this.setState({ panels: panels }, this.updateURL);
} };
render() { render() {
const { metricNames, pastQueries, timeDriftError, fetchMetricsError } = this.state; const { metricNames, pastQueries, timeDriftError, fetchMetricsError } = this.state;
@ -158,21 +168,30 @@ class PanelList extends Component<any, PanelListState> {
id="query-history-checkbox" id="query-history-checkbox"
wrapperStyles={{ margin: '0 0 0 15px', alignSelf: 'center' }} wrapperStyles={{ margin: '0 0 0 15px', alignSelf: 'center' }}
onChange={this.toggleQueryHistory} onChange={this.toggleQueryHistory}
defaultChecked={this.isHistoryEnabled()}> defaultChecked={this.isHistoryEnabled()}
>
Enable query history Enable query history
</Checkbox> </Checkbox>
</Row> </Row>
<Row> <Row>
<Col> <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> </Col>
</Row> </Row>
<Row> <Row>
<Col> <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> </Col>
</Row> </Row>
{this.state.panels.map(p => {this.state.panels.map(p => (
<Panel <Panel
onExecuteQuery={this.handleQueryHistory} onExecuteQuery={this.handleQueryHistory}
key={p.key} key={p.key}
@ -182,8 +201,10 @@ class PanelList extends Component<any, PanelListState> {
metricNames={metricNames} metricNames={metricNames}
pastQueries={pastQueries} 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 React, { FC } from 'react';
import { RouteComponentProps } from '@reach/router'; import { RouteComponentProps } from '@reach/router';
const Rules: FC<RouteComponentProps> = () => <div>Rules page</div> const Rules: FC<RouteComponentProps> = () => <div>Rules page</div>;
export default Rules; export default Rules;

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { RouteComponentProps } from '@reach/router'; import { RouteComponentProps } from '@reach/router';
const Services: FC<RouteComponentProps> = () => <div>Services page</div> const Services: FC<RouteComponentProps> = () => <div>Services page</div>;
export default Services; export default Services;

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { RouteComponentProps } from '@reach/router'; import { RouteComponentProps } from '@reach/router';
const Status: FC<RouteComponentProps> = () => <div>Status page</div> const Status: FC<RouteComponentProps> = () => <div>Status page</div>;
export default Status; export default Status;

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { RouteComponentProps } from '@reach/router'; import { RouteComponentProps } from '@reach/router';
const Targets: FC<RouteComponentProps> = () => <div>Targets page</div> const Targets: FC<RouteComponentProps> = () => <div>Targets page</div>;
export default Targets; export default Targets;

View file

@ -7,13 +7,4 @@ import Status from './Status';
import Targets from './Targets'; import Targets from './Targets';
import PanelList from './PanelList'; import PanelList from './PanelList';
export { export { Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList };
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'; import moment from 'moment-timezone';
const rangeUnits: {[unit: string]: number} = { const rangeUnits: { [unit: string]: number } = {
'y': 60 * 60 * 24 * 365, y: 60 * 60 * 24 * 365,
'w': 60 * 60 * 24 * 7, w: 60 * 60 * 24 * 7,
'd': 60 * 60 * 24, d: 60 * 60 * 24,
'h': 60 * 60, h: 60 * 60,
'm': 60, m: 60,
's': 1 s: 1,
} };
export function parseRange(rangeText: string): number | null { export function parseRange(rangeText: string): number | null {
const rangeRE = new RegExp('^([0-9]+)([ywdhms]+)$'); const rangeRE = new RegExp('^([0-9]+)([ywdhms]+)$');
@ -21,9 +21,9 @@ export function parseRange(rangeText: string): number | null {
} }
export function formatRange(range: number): string { 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) { if (range % rangeUnits[unit] === 0) {
return (range / rangeUnits[unit]) + unit; return range / rangeUnits[unit] + unit;
} }
} }
return range + 's'; return range + 's';

View file

@ -1,7 +1,7 @@
import { parseRange, parseTime, formatRange, formatTime } from './timeFormat'; import { parseRange, parseTime, formatRange, formatTime } from './timeFormat';
import { PanelOptions, PanelType, PanelDefaultOptions } from '../Panel'; 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 === '') { if (query === '') {
return []; return [];
} }
@ -21,12 +21,14 @@ interface IncompletePanelOptions {
stacked?: boolean; stacked?: boolean;
} }
function parseParams(params: string[]): {key: string, options: PanelOptions}[] { function parseParams(params: string[]): { key: string; options: PanelOptions }[] {
const sortedParams = params.filter((p) => { const sortedParams = params
return paramFormat.test(p); .filter(p => {
}).sort(); return paramFormat.test(p);
})
.sort();
let panelOpts: {key: string, options: PanelOptions}[] = []; const panelOpts: { key: string; options: PanelOptions }[] = [];
let key = 0; let key = 0;
let options: IncompletePanelOptions = {}; let options: IncompletePanelOptions = {};
@ -36,7 +38,7 @@ function parseParams(params: string[]): {key: string, options: PanelOptions}[] {
if (!p.startsWith(prefix)) { if (!p.startsWith(prefix)) {
panelOpts.push({ panelOpts.push({
key: key.toString(), key: key.toString(),
options: {...PanelDefaultOptions, ...options}, options: { ...PanelDefaultOptions, ...options },
}); });
options = {}; options = {};
key++; key++;
@ -46,17 +48,17 @@ function parseParams(params: string[]): {key: string, options: PanelOptions}[] {
} }
panelOpts.push({ panelOpts.push({
key: key.toString(), key: key.toString(),
options: {...PanelDefaultOptions, ...options}, options: { ...PanelDefaultOptions, ...options },
}); });
return panelOpts; return panelOpts;
} }
function addParam(opts: IncompletePanelOptions, param: string): void { function addParam(opts: IncompletePanelOptions, param: string): void {
let [ opt, val ] = param.split('='); let [opt, val] = param.split('=');
val = decodeURIComponent(val.replace(/\+/g, ' ')); val = decodeURIComponent(val.replace(/\+/g, ' '));
switch(opt) { switch (opt) {
case 'expr': case 'expr':
opts.expr = val; opts.expr = val;
break; 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[] = []; const queryParams: string[] = [];
panels.forEach(p => { panels.forEach(p => {
const prefix = 'g' + p.key + '.'; const prefix = 'g' + p.key + '.';
const o = p.options; const o = p.options;
const panelParams: {[key: string]: string | undefined} = { const panelParams: { [key: string]: string | undefined } = {
'expr': o.expr, expr: o.expr,
'tab': o.type === PanelType.Graph ? '0' : '1', tab: o.type === PanelType.Graph ? '0' : '1',
'stacked': o.stacked ? '1' : '0', stacked: o.stacked ? '1' : '0',
'range_input': formatRange(o.range), range_input: formatRange(o.range),
'end_input': o.endTime !== null ? formatTime(o.endTime) : undefined, end_input: o.endTime !== null ? formatTime(o.endTime) : undefined,
'moment_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, step_input: o.resolution !== null ? o.resolution.toString() : undefined,
}; };
for (let o in panelParams) { for (const o in panelParams) {
const pp = panelParams[o]; const pp = panelParams[o];
if (pp !== undefined) { if (pp !== undefined) {
queryParams.push(prefix + o + '=' + encodeURIComponent(pp)); queryParams.push(prefix + o + '=' + encodeURIComponent(pp));
} }
} }
}) });
return '?' + queryParams.join('&'); return '?' + queryParams.join('&');
} }

View file

@ -1464,6 +1464,17 @@
dependencies: dependencies:
"@types/yargs-parser" "*" "@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": "@typescript-eslint/eslint-plugin@^2.2.0":
version "2.4.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.4.0.tgz#aaf6b542ff75b78f4191a8bf1c519184817caa24" 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" "@typescript-eslint/typescript-estree" "2.4.0"
eslint-scope "^5.0.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": "@typescript-eslint/parser@^2.2.0":
version "2.4.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.4.0.tgz#fe43ed5fec14af03d3594fce2c3b7ec4c8df0243" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.4.0.tgz#fe43ed5fec14af03d3594fce2c3b7ec4c8df0243"
@ -1505,6 +1535,17 @@
lodash.unescape "4.0.1" lodash.unescape "4.0.1"
semver "^6.3.0" 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": "@webassemblyjs/ast@1.8.5":
version "1.8.5" version "1.8.5"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" 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" esutils "^2.0.2"
js-tokens "^3.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" version "10.0.3"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a" resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a"
integrity sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA== integrity sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA==
@ -3842,6 +3883,13 @@ escodegen@^1.11.0, escodegen@^1.11.1, escodegen@^1.9.1:
optionalDependencies: optionalDependencies:
source-map "~0.6.1" 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: eslint-config-react-app@^5.0.2:
version "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" 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" debug "^2.6.8"
pkg-dir "^2.0.0" 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" version "3.13.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-3.13.0.tgz#e241ebd39c0ce519345a3f074ec1ebde4cf80f2c" resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-3.13.0.tgz#e241ebd39c0ce519345a3f074ec1ebde4cf80f2c"
integrity sha512-bhewp36P+t7cEV0b6OdmoRWJCBYRiHFlqPZAG1oS3SF+Y0LQkeDvFSM4oxoxvczD1OdONCXMlJfQFiWLcV9urw== integrity sha512-bhewp36P+t7cEV0b6OdmoRWJCBYRiHFlqPZAG1oS3SF+Y0LQkeDvFSM4oxoxvczD1OdONCXMlJfQFiWLcV9urw==
dependencies: dependencies:
lodash "^4.17.15" 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" version "2.18.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz#02f1180b90b077b33d447a17a2326ceb400aceb6" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz#02f1180b90b077b33d447a17a2326ceb400aceb6"
integrity sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ== integrity sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ==
@ -3900,7 +3948,7 @@ eslint-plugin-import@2.18.2:
read-pkg-up "^2.0.0" read-pkg-up "^2.0.0"
resolve "^1.11.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" version "6.2.3"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz#b872a09d5de51af70a97db1eea7dc933043708aa" resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz#b872a09d5de51af70a97db1eea7dc933043708aa"
integrity sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg== integrity sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==
@ -3915,7 +3963,14 @@ eslint-plugin-jsx-a11y@6.2.3:
has "^1.0.3" has "^1.0.3"
jsx-ast-utils "^2.2.1" 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" version "1.7.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz#6210b6d5a37205f0b92858f895a4e827020a7d04" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz#6210b6d5a37205f0b92858f895a4e827020a7d04"
integrity sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA== integrity sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==
@ -3935,6 +3990,21 @@ eslint-plugin-react@7.14.3:
prop-types "^15.7.2" prop-types "^15.7.2"
resolve "^1.10.1" 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: eslint-scope@^4.0.3:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" 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" 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== 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" version "6.5.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.5.1.tgz#828e4c469697d43bb586144be152198b91e96ed6" resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.5.1.tgz#828e4c469697d43bb586144be152198b91e96ed6"
integrity sha512-32h99BoLYStT1iq1v2P9uwpyznQ4M2jRiFB6acitKz52Gqn+vPaMDUTB1bYi1WN4Nquj2w+t+bimYUG83DC55A== 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" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= 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: fast-glob@^2.0.2:
version "2.2.7" version "2.2.7"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" 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" 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== 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: get-stream@^4.0.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" 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" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= 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: pretty-bytes@^5.1.0:
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2"