Browse Source

migrate query history (#6193)

* migrate query history

Signed-off-by: blalov <boyko.lalov@tick42.com>

* update lock file

Signed-off-by: blalov <boyko.lalov@tick42.com>

* set expression input height when item is selected

Signed-off-by: blalov <boyko.lalov@tick42.com>

* pr review changes

Signed-off-by: blalov <boyko.lalov@tick42.com>
pull/6207/head
Boyko 5 years ago committed by Julius Volz
parent
commit
e235af9c47
  1. 4
      web/ui/react-app/package.json
  2. 10
      web/ui/react-app/src/App.css
  3. 17
      web/ui/react-app/src/App.tsx
  4. 22
      web/ui/react-app/src/Checkbox.tsx
  5. 17
      web/ui/react-app/src/ExpressionInput.tsx
  6. 16
      web/ui/react-app/src/GraphControls.tsx
  7. 2
      web/ui/react-app/src/Panel.tsx
  8. 62
      web/ui/react-app/src/PanelList.tsx
  9. 9
      web/ui/react-app/src/TimeInput.tsx
  10. 1
      web/ui/react-app/src/utils/func.ts

4
web/ui/react-app/package.json

@ -11,8 +11,8 @@
"@types/node": "^12.11.1",
"@types/react": "^16.8.2",
"@types/react-dom": "^16.8.0",
"@types/sanitize-html": "^1.20.2",
"@types/react-resize-detector": "^4.0.2",
"@types/sanitize-html": "^1.20.2",
"bootstrap": "^4.2.1",
"downshift": "^3.2.2",
"flot": "^3.2.13",
@ -26,10 +26,10 @@
"popper.js": "^1.14.3",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"sanitize-html": "^1.20.1",
"react-resize-detector": "^4.2.1",
"react-scripts": "^3.2.0",
"reactstrap": "^8.0.1",
"sanitize-html": "^1.20.1",
"tempusdominus-bootstrap-4": "^5.1.2",
"tempusdominus-core": "^5.0.3",
"typescript": "^3.3.3"

10
web/ui/react-app/src/App.css

@ -10,6 +10,16 @@ button.classic-ui-btn {
margin-bottom: 20px;
}
input[type='checkbox']:checked + label {
color: #286090;
}
.custom-control-label {
cursor: pointer;
font-size: .875rem;
line-height: 1.8;
}
.expression-input {
margin-bottom: 10px;
}

17
web/ui/react-app/src/App.tsx

@ -1,31 +1,16 @@
import React, { Component } from 'react';
import {
Button,
Container,
Col,
Row,
} from 'reactstrap';
import PanelList from './PanelList';
import './App.css';
import PanelList from './PanelList';
class App extends Component {
render() {
return (
<Container fluid={true}>
<Row>
<Col>
<Button
className="float-right classic-ui-btn"
color="link"
onClick={() => {window.location.pathname = "../../graph"}}
size="sm">
Return to classic UI
</Button>
</Col>
</Row>
<PanelList />
</Container>
);

22
web/ui/react-app/src/Checkbox.tsx

@ -0,0 +1,22 @@
import React, { FC, HTMLProps, memo } from 'react';
import { FormGroup, Label, Input } from 'reactstrap';
import { uuidGen } from './utils/func';
const Checkbox: FC<HTMLProps<HTMLSpanElement>> = ({ children, onChange, style }) => {
const id = uuidGen();
return (
<FormGroup className="custom-control custom-checkbox" style={style}>
<Input
onChange={onChange}
type="checkbox"
className="custom-control-input"
id={`checkbox_${id}`}
placeholder="password placeholder" />
<Label style={{ userSelect: 'none' }} className="custom-control-label" for={`checkbox_${id}`}>
{children}
</Label>
</FormGroup>
)
}
export default memo(Checkbox);

17
web/ui/react-app/src/ExpressionInput.tsx

@ -11,12 +11,9 @@ import Downshift, { ControllerStateAndHelpers } from 'downshift';
import fuzzy from 'fuzzy';
import SanitizeHTML from './components/SanitizeHTML';
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons';
library.add(faSearch, faSpinner);
interface ExpressionInputProps {
value: string;
metricNames: string[];
@ -59,8 +56,10 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
}, this.setHeight);
}
handleDropdownSelection = (value: string) => this.setState({ value });
handleDropdownSelection = (value: string) => {
this.setState({ value, height: 'auto' }, this.setHeight)
};
handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
this.props.executeQuery(this.exprInputRef.current!.value);
@ -73,7 +72,9 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
renderAutosuggest = (downshift: ControllerStateAndHelpers<any>) => {
const { inputValue } = downshift
if (!inputValue || (this.prevNoMatchValue && inputValue.includes(this.prevNoMatchValue))) {
downshift.closeMenu();
// This is ugly but is needed in order to sync state updates.
// This way we force downshift to wait React render call to complete before closeMenu to be triggered.
setTimeout(downshift.closeMenu);
return null;
}
@ -84,7 +85,7 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
if (matches.length === 0) {
this.prevNoMatchValue = inputValue;
downshift.closeMenu();
setTimeout(downshift.closeMenu);
return null;
}
@ -128,7 +129,7 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
<InputGroup className="expression-input">
<InputGroupAddon addonType="prepend">
<InputGroupText>
{this.props.loading ? <FontAwesomeIcon icon="spinner" spin/> : <FontAwesomeIcon icon="search"/>}
{this.props.loading ? <FontAwesomeIcon icon={faSpinner} spin /> : <FontAwesomeIcon icon={faSearch} />}
</InputGroupText>
</InputGroupAddon>
<Input

16
web/ui/react-app/src/GraphControls.tsx

@ -8,7 +8,6 @@ import {
Input,
} from 'reactstrap';
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faPlus,
@ -20,13 +19,6 @@ import {
import TimeInput from './TimeInput';
import { parseRange, formatRange } from './utils/timeFormat';
library.add(
faPlus,
faMinus,
faChartArea,
faChartLine,
);
interface GraphControlsProps {
range: number;
endTime: number | null;
@ -111,7 +103,7 @@ class GraphControls extends Component<GraphControlsProps> {
<Form inline className="graph-controls" onSubmit={e => e.preventDefault()}>
<InputGroup className="range-input" size="sm">
<InputGroupAddon addonType="prepend">
<Button title="Decrease range" onClick={this.decreaseRange}><FontAwesomeIcon icon="minus" fixedWidth/></Button>
<Button title="Decrease range" onClick={this.decreaseRange}><FontAwesomeIcon icon={faMinus} fixedWidth/></Button>
</InputGroupAddon>
<Input
@ -121,7 +113,7 @@ class GraphControls extends Component<GraphControlsProps> {
/>
<InputGroupAddon addonType="append">
<Button title="Increase range" onClick={this.increaseRange}><FontAwesomeIcon icon="plus" fixedWidth/></Button>
<Button title="Increase range" onClick={this.increaseRange}><FontAwesomeIcon icon={faPlus} fixedWidth/></Button>
</InputGroupAddon>
</InputGroup>
@ -145,8 +137,8 @@ class GraphControls extends Component<GraphControlsProps> {
/>
<ButtonGroup className="stacked-input" size="sm">
<Button title="Show unstacked line graph" onClick={() => this.props.onChangeStacking(false)} active={!this.props.stacked}><FontAwesomeIcon icon="chart-line" fixedWidth/></Button>
<Button title="Show stacked graph" onClick={() => this.props.onChangeStacking(true)} active={this.props.stacked}><FontAwesomeIcon icon="chart-area" fixedWidth/></Button>
<Button title="Show unstacked line graph" onClick={() => this.props.onChangeStacking(false)} active={!this.props.stacked}><FontAwesomeIcon icon={faChartLine} fixedWidth/></Button>
<Button title="Show stacked graph" onClick={() => this.props.onChangeStacking(true)} active={this.props.stacked}><FontAwesomeIcon icon={faChartArea} fixedWidth/></Button>
</ButtonGroup>
</Form>
);

2
web/ui/react-app/src/Panel.tsx

@ -26,6 +26,7 @@ interface PanelProps {
onOptionsChanged: (opts: PanelOptions) => void;
metricNames: string[];
removePanel: () => void;
onExecuteQuery: (query: string) => void;
}
interface PanelState {
@ -102,6 +103,7 @@ class Panel extends Component<PanelProps, PanelState> {
executeQuery = (expr: string): void => {
const queryStart = Date.now();
this.props.onExecuteQuery(expr)
if (this.props.options.expr !== expr) {
this.setOptions({expr: expr});
}

62
web/ui/react-app/src/PanelList.tsx

@ -1,9 +1,10 @@
import React, { Component } from 'react';
import React, { Component, ChangeEvent } from 'react';
import { Alert, Button, Col, Row } from 'reactstrap';
import Panel, { PanelOptions, PanelDefaultOptions } from './Panel';
import { decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString } from './utils/urlParams';
import Checkbox from './Checkbox';
interface PanelListState {
panels: {
@ -17,7 +18,7 @@ interface PanelListState {
class PanelList extends Component<any, PanelListState> {
private key: number = 0;
private initialMetricNames: string[] = [];
constructor(props: any) {
super(props);
@ -45,7 +46,10 @@ class PanelList extends Component<any, PanelListState> {
throw new Error('Unexpected response status when fetching metric names: ' + resp.statusText); // TODO extract error
}
})
.then(json => this.setState({ metricNames: json.data }))
.then(json => {
this.initialMetricNames = json.data;
this.setMetrics();
})
.catch(error => this.setState({ fetchMetricsError: error.message }));
const browserTime = new Date().getTime() / 1000;
@ -75,6 +79,40 @@ class PanelList extends Component<any, PanelListState> {
}
}
isHistoryEnabled = () => JSON.parse(localStorage.getItem('enable-query-history') || 'false') as boolean;
getHistoryItems = () => JSON.parse(localStorage.getItem('history') || '[]') as string[];
toggleQueryHistory = (e: ChangeEvent<HTMLInputElement>) => {
localStorage.setItem('enable-query-history', `${e.target.checked}`);
this.setMetrics();
}
setMetrics = () => {
if (this.isHistoryEnabled()) {
const historyItems = this.getHistoryItems();
const { length } = historyItems;
this.setState({
metricNames: [...historyItems.slice(length - 50, length), ...this.initialMetricNames],
});
} else {
this.setState({ metricNames: this.initialMetricNames });
}
}
handleQueryHistory = (query: string) => {
const isSimpleMetric = this.initialMetricNames.indexOf(query) !== -1;
if (isSimpleMetric || !query.length) {
return;
}
const historyItems = this.getHistoryItems();
const extendedItems = historyItems.reduce((acc, metric) => {
return metric === query ? acc : [...acc, metric]; // Prevent adding query twice.
}, [query]);
localStorage.setItem('history', JSON.stringify(extendedItems));
this.setMetrics();
}
getKey(): string {
return (this.key++).toString();
}
@ -116,6 +154,23 @@ class PanelList extends Component<any, PanelListState> {
render() {
return (
<>
<Row className="mb-2">
<Checkbox
style={{ margin: '0 0 0 15px', alignSelf: 'center' }}
onChange={this.toggleQueryHistory}
checked={this.isHistoryEnabled()}>
Enable query history
</Checkbox>
<Col>
<Button
className="float-right classic-ui-btn"
color="link"
onClick={() => { window.location.pathname = "../../graph" }}
size="sm">
Return to classic UI
</Button>
</Col>
</Row>
<Row>
<Col>
{this.state.timeDriftError && <Alert color="danger"><strong>Warning:</strong> Error fetching server time: {this.state.timeDriftError}</Alert>}
@ -128,6 +183,7 @@ class PanelList extends Component<any, PanelListState> {
</Row>
{this.state.panels.map(p =>
<Panel
onExecuteQuery={this.handleQueryHistory}
key={p.key}
options={p.options}
onOptionsChanged={(opts: PanelOptions) => this.handleOptionsChanged(p.key, opts)}

9
web/ui/react-app/src/TimeInput.tsx

@ -20,14 +20,11 @@ import {
} from '@fortawesome/free-solid-svg-icons';
library.add(
faChevronLeft,
faChevronRight,
faCalendarCheck,
faArrowUp,
faArrowDown,
faTimes,
);
// Sadly needed to also replace <i> within the date picker, since it's not a React component.
dom.watch();
@ -99,7 +96,7 @@ class TimeInput extends Component<TimeInputProps> {
return (
<InputGroup className="time-input" size="sm">
<InputGroupAddon addonType="prepend">
<Button title="Decrease time" onClick={this.decreaseTime}><FontAwesomeIcon icon="chevron-left" fixedWidth/></Button>
<Button title="Decrease time" onClick={this.decreaseTime}><FontAwesomeIcon icon={faChevronLeft} fixedWidth /></Button>
</InputGroupAddon>
<Input
@ -114,12 +111,12 @@ class TimeInput extends Component<TimeInputProps> {
that functionality is broken, so we create an external solution instead. */}
{this.props.time &&
<InputGroupAddon addonType="append">
<Button className="clear-time-btn" title="Clear time" onClick={this.clearTime}><FontAwesomeIcon icon="times" fixedWidth/></Button>
<Button className="clear-time-btn" title="Clear time" onClick={this.clearTime}><FontAwesomeIcon icon={faTimes} fixedWidth /></Button>
</InputGroupAddon>
}
<InputGroupAddon addonType="append">
<Button title="Increase time" onClick={this.increaseTime}><FontAwesomeIcon icon="chevron-right" fixedWidth/></Button>
<Button title="Increase time" onClick={this.increaseTime}><FontAwesomeIcon icon={faChevronRight} fixedWidth /></Button>
</InputGroupAddon>
</InputGroup>
);

1
web/ui/react-app/src/utils/func.ts

@ -0,0 +1 @@
export const uuidGen = () => '_' + Math.random().toString(36).substr(2, 9);
Loading…
Cancel
Save