diff --git a/web/ui/react-app/package.json b/web/ui/react-app/package.json
index 11d3c3b63..777750d2b 100644
--- a/web/ui/react-app/package.json
+++ b/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"
diff --git a/web/ui/react-app/src/App.css b/web/ui/react-app/src/App.css
index a540874f1..da6288a71 100644
--- a/web/ui/react-app/src/App.css
+++ b/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;
}
diff --git a/web/ui/react-app/src/App.tsx b/web/ui/react-app/src/App.tsx
index 4c2dfb5d6..471ced047 100755
--- a/web/ui/react-app/src/App.tsx
+++ b/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 (
-
-
-
-
-
);
diff --git a/web/ui/react-app/src/Checkbox.tsx b/web/ui/react-app/src/Checkbox.tsx
new file mode 100644
index 000000000..1d4719e35
--- /dev/null
+++ b/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> = ({ children, onChange, style }) => {
+ const id = uuidGen();
+ return (
+
+
+
+
+ )
+}
+
+export default memo(Checkbox);
diff --git a/web/ui/react-app/src/ExpressionInput.tsx b/web/ui/react-app/src/ExpressionInput.tsx
index 23026cad8..2b9b98a56 100644
--- a/web/ui/react-app/src/ExpressionInput.tsx
+++ b/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 this.setState({ value });
-
+ handleDropdownSelection = (value: string) => {
+ this.setState({ value, height: 'auto' }, this.setHeight)
+ };
+
handleKeyPress = (event: React.KeyboardEvent) => {
if (event.key === 'Enter' && !event.shiftKey) {
this.props.executeQuery(this.exprInputRef.current!.value);
@@ -73,7 +72,9 @@ class ExpressionInput extends Component) => {
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
- {this.props.loading ? : }
+ {this.props.loading ? : }
{
);
diff --git a/web/ui/react-app/src/Panel.tsx b/web/ui/react-app/src/Panel.tsx
index 6450eb1ee..51e65e7c7 100644
--- a/web/ui/react-app/src/Panel.tsx
+++ b/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 {
executeQuery = (expr: string): void => {
const queryStart = Date.now();
+ this.props.onExecuteQuery(expr)
if (this.props.options.expr !== expr) {
this.setOptions({expr: expr});
}
diff --git a/web/ui/react-app/src/PanelList.tsx b/web/ui/react-app/src/PanelList.tsx
index 6d1b49428..44768eca2 100644
--- a/web/ui/react-app/src/PanelList.tsx
+++ b/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 {
private key: number = 0;
-
+ private initialMetricNames: string[] = [];
constructor(props: any) {
super(props);
@@ -45,7 +46,10 @@ class PanelList extends Component {
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 {
}
}
+ isHistoryEnabled = () => JSON.parse(localStorage.getItem('enable-query-history') || 'false') as boolean;
+
+ getHistoryItems = () => JSON.parse(localStorage.getItem('history') || '[]') as string[];
+
+ toggleQueryHistory = (e: ChangeEvent) => {
+ 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 {
render() {
return (
<>
+
+
+ Enable query history
+
+
+
+
+
{this.state.timeDriftError && Warning: Error fetching server time: {this.state.timeDriftError}}
@@ -128,6 +183,7 @@ class PanelList extends Component {
{this.state.panels.map(p =>
this.handleOptionsChanged(p.key, opts)}
diff --git a/web/ui/react-app/src/TimeInput.tsx b/web/ui/react-app/src/TimeInput.tsx
index 35c738831..903471423 100644
--- a/web/ui/react-app/src/TimeInput.tsx
+++ b/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 within the date picker, since it's not a React component.
dom.watch();
@@ -99,7 +96,7 @@ class TimeInput extends Component {
return (
-
+
{
that functionality is broken, so we create an external solution instead. */}
{this.props.time &&
-
+
}
-
+
);
diff --git a/web/ui/react-app/src/utils/func.ts b/web/ui/react-app/src/utils/func.ts
new file mode 100644
index 000000000..9ad243e66
--- /dev/null
+++ b/web/ui/react-app/src/utils/func.ts
@@ -0,0 +1 @@
+export const uuidGen = () => '_' + Math.random().toString(36).substr(2, 9);