prometheus/web/ui/react-app/src/Panel.tsx
Julius Volz bca6e90ea6
Integrate beginning of React UI (#5694)
* Initial commit from Create React App

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Initial Prometheus expression browser code

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Grpahing, try out echarts

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Switch to flot

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Add metrics fetching and stuff

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Autosuggest and graph improvements

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Start implementing graph controls, add loading spinner

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* So many new features and fixes

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Fixed and built more features

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Make datetimepicker clear work

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Don't abort when executing empty expression

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Remove TabPaneAlert

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Split components into separate files

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Add table time input

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Move first files to TypeScript!

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* More TypeScript conversions

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* More TS conversions

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* More TS conversions

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* More TS conversions

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* More TS conversions

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* More TS fixes

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Convert Graph to TS

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Changes

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Resize detector, start building legend, axis font colors

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Make graph legend work

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Add URL params support and much more

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Put panel state into panel list, write URL options

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Change order of Graph and Table tabs

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Generalize time input naming more

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Work on history functionality

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* npm updates

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Move loading indicator into "Execute" button

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Fix typo

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Revert "Move loading indicator into "Execute" button"

This reverts commit ce7daee1f1af35da6c0d8b5517272839285ccfec.

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Improve error message when failing to fetch server time

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Move all code to Prometheus repo target dir

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Add react-app Makefile step and check in generated assets

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Add preliminary npm packages notice to NOTICE file

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Update React app's favicon and metadata

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Remove RP server refs, cleanups

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Use CircleCI image that includes NodeJS

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Add some missing React output assets

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Preserve CRLF in generated React files

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Switch from npm to yarn for React UI

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Save npm licenses and include them in release tarball

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Install npm on Travis

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Remove npm license tarball from source

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Remove React graph bundle from source

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Don't check in any compiled web assets

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Update README.md with node/yarn/React UI info

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Fix asset build step on CircleCI promu crossbuild

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Try to fix multi-arch go generate

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Remove check_assets from Travis CI build

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Prevent rebuilding of unchanged React app parts

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Fix npm license tarball path for promu

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Simplify Makefile

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Clarify build instructions in README.md

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Make minimal JS test pass

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Integrate React app tests into Makefile

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Separate react-app-tests target, but run it from CI

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Fix working directory for React app tests

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Remove local modifications to Makefile.common

This means that CircleCI will not run the React app tests, but at least
Travis still will...

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Depend on node_modules path for npm_licenses target

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Simplify tarball/docker/build Makefile targets

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Include React tests in "test" target

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Remove reference to removed "check_assets" target

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Do initial resize of expression input field

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Add React app proxying to local Prometheus in dev mode

Signed-off-by: Julius Volz <julius.volz@gmail.com>
2019-10-17 14:38:09 +02:00

298 lines
8.2 KiB
TypeScript

import React, { Component } from 'react';
import {
Alert,
Button,
Col,
Nav,
NavItem,
NavLink,
Row,
TabContent,
TabPane,
} from 'reactstrap';
import moment from 'moment-timezone';
import ExpressionInput from './ExpressionInput';
import GraphControls from './GraphControls';
import Graph from './Graph';
import DataTable from './DataTable';
import TimeInput from './TimeInput';
interface PanelProps {
options: PanelOptions;
onOptionsChanged: (opts: PanelOptions) => void;
metricNames: string[];
removePanel: () => void;
}
interface PanelState {
data: any; // TODO: Type data.
lastQueryParams: { // TODO: Share these with Graph.tsx in a file.
startTime: number,
endTime: number,
resolution: number,
} | null;
loading: boolean;
error: string | null;
stats: null; // TODO: Stats.
}
export interface PanelOptions {
expr: string;
type: PanelType;
range: number; // Range in seconds.
endTime: number | null; // Timestamp in milliseconds.
resolution: number | null; // Resolution in seconds.
stacked: boolean;
}
export enum PanelType {
Graph = 'graph',
Table = 'table',
}
export const PanelDefaultOptions: PanelOptions = {
type: PanelType.Table,
expr: '',
range: 3600,
endTime: null,
resolution: null,
stacked: false,
}
class Panel extends Component<PanelProps, PanelState> {
private abortInFlightFetch: (() => void) | null = null;
constructor(props: PanelProps) {
super(props);
this.state = {
data: null,
lastQueryParams: null,
loading: false,
error: null,
stats: null,
};
}
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) {
// 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.executeQuery(opts.expr);
}
}
componentDidMount() {
this.executeQuery(this.props.options.expr);
}
executeQuery = (expr: string): void => {
if (this.props.options.expr !== expr) {
this.setOptions({expr: expr});
}
if (expr === '') {
return;
}
if (this.abortInFlightFetch) {
this.abortInFlightFetch();
this.abortInFlightFetch = null;
}
const abortController = new AbortController();
this.abortInFlightFetch = () => abortController.abort();
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,
};
switch (this.props.options.type) {
case 'graph':
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'
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]))
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: startTime,
endTime: endTime,
resolution: resolution,
},
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};
this.props.onOptionsChanged(newOpts);
}
handleExpressionChange = (expr: string): void => {
this.setOptions({expr: expr});
}
handleChangeRange = (range: number): void => {
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});
}
handleChangeResolution = (resolution: number | null) => {
this.setOptions({resolution: resolution});
}
handleChangeStacking = (stacked: boolean) => {
this.setOptions({stacked: stacked});
}
render() {
return (
<div className="panel">
<Row>
<Col>
<ExpressionInput
value={this.props.options.expr}
executeQuery={this.executeQuery}
loading={this.state.loading}
metricNames={this.props.metricNames}
/>
</Col>
</Row>
<Row>
<Col>
{this.state.error && <Alert color="danger">{this.state.error}</Alert>}
</Col>
</Row>
<Row>
<Col>
<Nav tabs>
<NavItem>
<NavLink
className={this.props.options.type === 'table' ? 'active' : ''}
onClick={() => { this.setOptions({type: 'table'}); }}
>
Table
</NavLink>
</NavItem>
<NavItem>
<NavLink
className={this.props.options.type === 'graph' ? 'active' : ''}
onClick={() => { this.setOptions({type: 'graph'}); }}
>
Graph
</NavLink>
</NavItem>
</Nav>
<TabContent activeTab={this.props.options.type}>
<TabPane tabId="table">
{this.props.options.type === 'table' &&
<>
<div className="table-controls">
<TimeInput
time={this.props.options.endTime}
range={this.props.options.range}
placeholder="Evaluation time"
onChangeTime={this.handleChangeEndTime}
/>
</div>
<DataTable data={this.state.data} />
</>
}
</TabPane>
<TabPane tabId="graph">
{this.props.options.type === 'graph' &&
<>
<GraphControls
range={this.props.options.range}
endTime={this.props.options.endTime}
resolution={this.props.options.resolution}
stacked={this.props.options.stacked}
onChangeRange={this.handleChangeRange}
onChangeEndTime={this.handleChangeEndTime}
onChangeResolution={this.handleChangeResolution}
onChangeStacking={this.handleChangeStacking}
/>
<Graph data={this.state.data} stacked={this.props.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>
</Col>
</Row>
</div>
);
}
}
export default Panel;