From 82ce733e9e822cf14b73bd5819f7831ff87b1464 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 21 Nov 2018 11:34:01 +0100 Subject: [PATCH 01/14] react-panel: Create component for JSON formatting and use it on query inspector --- package.json | 1 + .../JSONFormatter/JSONFormatter.tsx | 30 +++++++++++++++++++ .../dashboard/dashgrid/QueriesTab.tsx | 12 ++++++-- yarn.lock | 5 ++++ 4 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 public/app/core/components/JSONFormatter/JSONFormatter.tsx diff --git a/package.json b/package.json index 452e4ecb742..fa9a0dcad2e 100644 --- a/package.json +++ b/package.json @@ -145,6 +145,7 @@ "file-saver": "^1.3.3", "immutable": "^3.8.2", "jquery": "^3.2.1", + "json-formatter-js": "^2.2.1", "lodash": "^4.17.10", "moment": "^2.22.2", "mousetrap": "^1.6.0", diff --git a/public/app/core/components/JSONFormatter/JSONFormatter.tsx b/public/app/core/components/JSONFormatter/JSONFormatter.tsx new file mode 100644 index 00000000000..4c18c5255c0 --- /dev/null +++ b/public/app/core/components/JSONFormatter/JSONFormatter.tsx @@ -0,0 +1,30 @@ +import React, { PureComponent } from 'react'; +import JSONFormatterJS from 'json-formatter-js'; + +interface Props { + className?: string; + json: any; + options?: any; +} + +export class JSONFormatter extends PureComponent { + wrapperEl: any = React.createRef(); + jsonEl: HTMLElement; + formatter: any; + + componentDidMount() { + const { json, options } = this.props; + this.formatter = new JSONFormatterJS(json, options); + this.jsonEl = this.wrapperEl.current.appendChild(this.formatter.render()); + } + + componentWillUnmount() { + this.formatter = null; + this.jsonEl = null; + } + + render() { + const { className } = this.props; + return
; + } +} diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index cba631fb141..472d68f4c94 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -3,7 +3,7 @@ import DataSourceOption from './DataSourceOption'; import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; import { EditorTabBody } from './EditorTabBody'; import { DataSourcePicker } from './DataSourcePicker'; - +import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; import './../../panel/metrics_tab'; @@ -199,6 +199,11 @@ export class QueriesTab extends PureComponent { }); }; + renderQueryInspector = () => { + const queryInspectorJson = { hello: 'world' }; // TODO + return ; + }; + render() { const { currentDatasource } = this.state; const { helpHtml } = this.state.help; @@ -220,7 +225,7 @@ export class QueriesTab extends PureComponent { const queryInspector = { title: 'Query Inspector', - render: () =>

hello

, + render: this.renderQueryInspector, }; const dsHelp = { @@ -232,7 +237,8 @@ export class QueriesTab extends PureComponent { }; const options = { - title: 'Options', + title: '', + icon: 'fa fa-cog', disabled: !hasQueryOptions, render: this.renderOptions, }; diff --git a/yarn.lock b/yarn.lock index 2cebcae1d9c..be55f6fd52e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6811,6 +6811,11 @@ json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" +json-formatter-js@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json-formatter-js/-/json-formatter-js-2.2.1.tgz#b101d628e86f028dc9cf9a7e1c83c65e536c9f87" + integrity sha1-sQHWKOhvAo3Jz5p+HIPGXlNsn4c= + json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" From 348f2e4ec4c98eca2de74843f6444559cb8c6af6 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 21 Nov 2018 14:03:27 +0100 Subject: [PATCH 02/14] react-panel: Get real datasource query for query inspector --- .../JSONFormatter/JSONFormatter.tsx | 14 +++- .../dashboard/dashgrid/QueriesTab.tsx | 65 ++++++++++++++++++- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/public/app/core/components/JSONFormatter/JSONFormatter.tsx b/public/app/core/components/JSONFormatter/JSONFormatter.tsx index 4c18c5255c0..07b3fe45145 100644 --- a/public/app/core/components/JSONFormatter/JSONFormatter.tsx +++ b/public/app/core/components/JSONFormatter/JSONFormatter.tsx @@ -4,7 +4,8 @@ import JSONFormatterJS from 'json-formatter-js'; interface Props { className?: string; json: any; - options?: any; + config?: any; + open?: number; } export class JSONFormatter extends PureComponent { @@ -12,9 +13,16 @@ export class JSONFormatter extends PureComponent { jsonEl: HTMLElement; formatter: any; + static defaultProps = { + open: 3, + config: { + animateOpen: true, + }, + }; + componentDidMount() { - const { json, options } = this.props; - this.formatter = new JSONFormatterJS(json, options); + const { json, config, open } = this.props; + this.formatter = new JSONFormatterJS(json, open, config); this.jsonEl = this.wrapperEl.current.appendChild(this.formatter.render()); } diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index 472d68f4c94..b7dbb87b9ed 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -13,6 +13,7 @@ import config from 'app/core/config'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv'; import { DataSourceSelectItem } from 'app/types'; +import appEvents from 'app/core/app_events'; import Remarkable from 'remarkable'; @@ -29,6 +30,7 @@ interface Help { interface State { currentDatasource: DataSourceSelectItem; help: Help; + dsQuery: {}; } export class QueriesTab extends PureComponent { @@ -42,12 +44,14 @@ export class QueriesTab extends PureComponent { const { panel } = props; this.state = { + dsQuery: {}, currentDatasource: this.datasources.find(datasource => datasource.value === panel.datasource), help: { isLoading: false, helpHtml: null, }, }; + appEvents.on('ds-request-response', this.onDataSourceResponse); } componentDidMount() { @@ -70,11 +74,68 @@ export class QueriesTab extends PureComponent { } componentWillUnmount() { + appEvents.off('ds-request-response', this.onDataSourceResponse); + if (this.component) { this.component.destroy(); } } + onDataSourceResponse = (response: any = {}) => { + // ignore if closed + // if (!this.isOpen) { + // return; + // } + + // if (this.isMocking) { + // this.handleMocking(data); + // return; + // } + + // this.isLoading = false; + // data = _.cloneDeep(data); + + if (response.headers) { + delete response.headers; + } + + if (response.config) { + response.request = response.config; + delete response.config; + delete response.request.transformRequest; + delete response.request.transformResponse; + delete response.request.paramSerializer; + delete response.request.jsonpCallbackParam; + delete response.request.headers; + delete response.request.requestId; + delete response.request.inspect; + delete response.request.retry; + delete response.request.timeout; + } + + if (response.data) { + response.response = response.data; + + // if (response.status === 200) { + // // if we are in error state, assume we automatically opened + // // and auto close it again + // if (this.hasError) { + // this.hasError = false; + // this.isOpen = false; + // } + // } + + delete response.data; + delete response.status; + delete response.statusText; + delete response.$$config; + } + this.setState(prevState => ({ + ...prevState, + dsQuery: response, + })); + }; + onChangeDataSource = datasource => { const { panel } = this.props; const { currentDatasource } = this.state; @@ -200,8 +261,8 @@ export class QueriesTab extends PureComponent { }; renderQueryInspector = () => { - const queryInspectorJson = { hello: 'world' }; // TODO - return ; + const { dsQuery } = this.state; + return ; }; render() { From 5cca489acddb5e435074b4dab360c6055a834319 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 22 Nov 2018 15:39:42 +0100 Subject: [PATCH 03/14] react-panel: Clean up the JSONFormatter and make sure it updates both on mount and when props update --- .../JSONFormatter/JSONFormatter.tsx | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/public/app/core/components/JSONFormatter/JSONFormatter.tsx b/public/app/core/components/JSONFormatter/JSONFormatter.tsx index 07b3fe45145..82560c07d33 100644 --- a/public/app/core/components/JSONFormatter/JSONFormatter.tsx +++ b/public/app/core/components/JSONFormatter/JSONFormatter.tsx @@ -1,38 +1,52 @@ -import React, { PureComponent } from 'react'; -import JSONFormatterJS from 'json-formatter-js'; +import React, { PureComponent, createRef } from 'react'; +import JSONFormatterJS, { JSONFormatterConfiguration } from 'json-formatter-js'; interface Props { className?: string; - json: any; - config?: any; + json: {}; + config?: JSONFormatterConfiguration; open?: number; } export class JSONFormatter extends PureComponent { - wrapperEl: any = React.createRef(); - jsonEl: HTMLElement; - formatter: any; + private wrapperRef = createRef(); + private formatter: any; static defaultProps = { open: 3, config: { animateOpen: true, + theme: 'dark', }, }; componentDidMount() { + this.renderJson(); + } + + componentDidUpdate() { + this.renderJson(); + } + + renderJson = () => { const { json, config, open } = this.props; this.formatter = new JSONFormatterJS(json, open, config); - this.jsonEl = this.wrapperEl.current.appendChild(this.formatter.render()); - } + const wrapperEl = this.wrapperRef.current; + const newJsonHtml = this.formatter.render(); + const hasChildren: boolean = wrapperEl.hasChildNodes(); + if (hasChildren) { + wrapperEl.replaceChild(newJsonHtml, wrapperEl.lastChild); + } else { + wrapperEl.appendChild(newJsonHtml); + } + }; componentWillUnmount() { this.formatter = null; - this.jsonEl = null; } render() { const { className } = this.props; - return
; + return
; } } From 6242379915420f24b756e1566609051441c7e82a Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 22 Nov 2018 15:51:59 +0100 Subject: [PATCH 04/14] react-panel: Replace JSONFormatter npm package with the current monkey patched JsonExplorer --- .../components/JSONFormatter/JSONFormatter.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/public/app/core/components/JSONFormatter/JSONFormatter.tsx b/public/app/core/components/JSONFormatter/JSONFormatter.tsx index 82560c07d33..1dcc9fd170b 100644 --- a/public/app/core/components/JSONFormatter/JSONFormatter.tsx +++ b/public/app/core/components/JSONFormatter/JSONFormatter.tsx @@ -1,16 +1,16 @@ import React, { PureComponent, createRef } from 'react'; -import JSONFormatterJS, { JSONFormatterConfiguration } from 'json-formatter-js'; +// import JSONFormatterJS, { JSONFormatterConfiguration } from 'json-formatter-js'; +import { JsonExplorer } from 'app/core/core'; // We have made some monkey-patching of json-formatter-js so we can't switch right now interface Props { className?: string; json: {}; - config?: JSONFormatterConfiguration; + config?: any; open?: number; } export class JSONFormatter extends PureComponent { private wrapperRef = createRef(); - private formatter: any; static defaultProps = { open: 3, @@ -30,21 +30,16 @@ export class JSONFormatter extends PureComponent { renderJson = () => { const { json, config, open } = this.props; - this.formatter = new JSONFormatterJS(json, open, config); const wrapperEl = this.wrapperRef.current; - const newJsonHtml = this.formatter.render(); + const formatter = new JsonExplorer(json, open, config); const hasChildren: boolean = wrapperEl.hasChildNodes(); if (hasChildren) { - wrapperEl.replaceChild(newJsonHtml, wrapperEl.lastChild); + wrapperEl.replaceChild(formatter.render(), wrapperEl.lastChild); } else { - wrapperEl.appendChild(newJsonHtml); + wrapperEl.appendChild(formatter.render()); } }; - componentWillUnmount() { - this.formatter = null; - } - render() { const { className } = this.props; return
; From be67801e845ed22241fe7701e973133a5456d0b3 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 22 Nov 2018 15:53:34 +0100 Subject: [PATCH 05/14] react-panel: Trigger panel refresh when opening inspector. Add loading-message --- .../dashboard/dashgrid/QueriesTab.tsx | 58 ++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index b7dbb87b9ed..db5bcc44f2d 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react'; +import React, { SFC, PureComponent } from 'react'; import DataSourceOption from './DataSourceOption'; import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; import { EditorTabBody } from './EditorTabBody'; @@ -27,12 +27,23 @@ interface Help { helpHtml: any; } +interface DsQuery { + isLoading: boolean; + response: {}; +} + interface State { currentDatasource: DataSourceSelectItem; help: Help; - dsQuery: {}; + dsQuery: DsQuery; } +interface LoadingPlaceholderProps { + text: string; +} + +const LoadingPlaceholder: SFC = ({ text }) =>

{text}

; + export class QueriesTab extends PureComponent { element: any; component: AngularComponent; @@ -44,14 +55,18 @@ export class QueriesTab extends PureComponent { const { panel } = props; this.state = { - dsQuery: {}, currentDatasource: this.datasources.find(datasource => datasource.value === panel.datasource), help: { isLoading: false, helpHtml: null, }, + dsQuery: { + isLoading: false, + response: {}, + }, }; appEvents.on('ds-request-response', this.onDataSourceResponse); + panel.events.on('refresh', this.onPanelRefresh); } componentDidMount() { @@ -74,13 +89,25 @@ export class QueriesTab extends PureComponent { } componentWillUnmount() { + const { panel } = this.props; appEvents.off('ds-request-response', this.onDataSourceResponse); + panel.events.off('refresh', this.onPanelRefresh); if (this.component) { this.component.destroy(); } } + onPanelRefresh = () => { + this.setState(prevState => ({ + ...prevState, + dsQuery: { + isLoading: true, + response: {}, + }, + })); + }; + onDataSourceResponse = (response: any = {}) => { // ignore if closed // if (!this.isOpen) { @@ -94,6 +121,7 @@ export class QueriesTab extends PureComponent { // this.isLoading = false; // data = _.cloneDeep(data); + response = { ...response }; // clone if (response.headers) { delete response.headers; @@ -132,7 +160,10 @@ export class QueriesTab extends PureComponent { } this.setState(prevState => ({ ...prevState, - dsQuery: response, + dsQuery: { + isLoading: false, + response: response, + }, })); }; @@ -260,14 +291,24 @@ export class QueriesTab extends PureComponent { }); }; + loadQueryInspector = () => { + const { panel } = this.props; + panel.refresh(); + }; + renderQueryInspector = () => { - const { dsQuery } = this.state; - return ; + const { response, isLoading } = this.state.dsQuery; + return isLoading ? : ; + }; + + renderHelp = () => { + const { helpHtml, isLoading } = this.state.help; + return isLoading ? : helpHtml; }; render() { const { currentDatasource } = this.state; - const { helpHtml } = this.state.help; + const { hasQueryHelp, queryOptions } = currentDatasource.meta; const hasQueryOptions = !!queryOptions; const dsInformation = { @@ -286,6 +327,7 @@ export class QueriesTab extends PureComponent { const queryInspector = { title: 'Query Inspector', + onClick: this.loadQueryInspector, render: this.renderQueryInspector, }; @@ -294,7 +336,7 @@ export class QueriesTab extends PureComponent { icon: 'fa fa-question', disabled: !hasQueryHelp, onClick: this.loadHelp, - render: () => helpHtml, + render: this.renderHelp, }; const options = { From 8254086ecb632b506d8b059efed4ea8ba356fa67 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 26 Nov 2018 13:57:31 +0100 Subject: [PATCH 06/14] react-panel: Add CopyToClipboard-component and separate QueryInspector to its own component from QueriesTab --- .../CopyToClipboard/CopyToClipboard.tsx | 67 +++++++++++++++++++ .../JSONFormatter/JSONFormatter.tsx | 8 ++- .../dashboard/dashgrid/QueriesTab.tsx | 8 ++- .../dashboard/dashgrid/QueryInspector.tsx | 61 +++++++++++++++++ 4 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 public/app/core/components/CopyToClipboard/CopyToClipboard.tsx create mode 100644 public/app/features/dashboard/dashgrid/QueryInspector.tsx diff --git a/public/app/core/components/CopyToClipboard/CopyToClipboard.tsx b/public/app/core/components/CopyToClipboard/CopyToClipboard.tsx new file mode 100644 index 00000000000..153dae2a9c6 --- /dev/null +++ b/public/app/core/components/CopyToClipboard/CopyToClipboard.tsx @@ -0,0 +1,67 @@ +import React, { PureComponent } from 'react'; +import ClipboardJS from 'clipboard'; + +interface Props { + text: () => string; + elType?: string; + onSuccess?: (evt: any) => void; + onError?: (evt: any) => void; + className?: string; + children?: JSX.Element | string; +} + +export class CopyToClipboard extends PureComponent { + clipboardjs: any; + myRef: any; + + constructor(props) { + super(props); + this.myRef = React.createRef(); + } + + componentDidMount() { + const { text, onSuccess, onError } = this.props; + + this.clipboardjs = new ClipboardJS(this.myRef.current, { + text: text, + }); + + if (onSuccess) { + this.clipboardjs.on('success', evt => { + evt.clearSelection(); + onSuccess(evt); + }); + } + + if (onError) { + this.clipboardjs.on('error', evt => { + console.error('Action:', evt.action); + console.error('Trigger:', evt.trigger); + onError(evt); + }); + } + } + + componentWillUnmount() { + if (this.clipboardjs) { + this.clipboardjs.destroy(); + } + } + + getElementType = () => { + return this.props.elType || 'button'; + }; + + render() { + const { elType, text, children, onError, onSuccess, ...restProps } = this.props; + + return React.createElement( + this.getElementType(), + { + ref: this.myRef, + ...restProps, + }, + this.props.children + ); + } +} diff --git a/public/app/core/components/JSONFormatter/JSONFormatter.tsx b/public/app/core/components/JSONFormatter/JSONFormatter.tsx index 1dcc9fd170b..73c055de94b 100644 --- a/public/app/core/components/JSONFormatter/JSONFormatter.tsx +++ b/public/app/core/components/JSONFormatter/JSONFormatter.tsx @@ -7,6 +7,7 @@ interface Props { json: {}; config?: any; open?: number; + onDidRender?: (formattedJson: any) => void; } export class JSONFormatter extends PureComponent { @@ -16,7 +17,6 @@ export class JSONFormatter extends PureComponent { open: 3, config: { animateOpen: true, - theme: 'dark', }, }; @@ -29,7 +29,7 @@ export class JSONFormatter extends PureComponent { } renderJson = () => { - const { json, config, open } = this.props; + const { json, config, open, onDidRender } = this.props; const wrapperEl = this.wrapperRef.current; const formatter = new JsonExplorer(json, open, config); const hasChildren: boolean = wrapperEl.hasChildNodes(); @@ -38,6 +38,10 @@ export class JSONFormatter extends PureComponent { } else { wrapperEl.appendChild(formatter.render()); } + + if (onDidRender) { + onDidRender(formatter.json); + } }; render() { diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index db5bcc44f2d..d35e55f591d 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -3,11 +3,11 @@ import DataSourceOption from './DataSourceOption'; import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; import { EditorTabBody } from './EditorTabBody'; import { DataSourcePicker } from './DataSourcePicker'; -import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; import './../../panel/metrics_tab'; import config from 'app/core/config'; +import { QueryInspector } from './QueryInspector'; // Services import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -298,7 +298,11 @@ export class QueriesTab extends PureComponent { renderQueryInspector = () => { const { response, isLoading } = this.state.dsQuery; - return isLoading ? : ; + return isLoading ? ( + + ) : ( + + ); }; renderHelp = () => { diff --git a/public/app/features/dashboard/dashgrid/QueryInspector.tsx b/public/app/features/dashboard/dashgrid/QueryInspector.tsx new file mode 100644 index 00000000000..ace155fec43 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/QueryInspector.tsx @@ -0,0 +1,61 @@ +import React, { PureComponent } from 'react'; +import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter'; +import appEvents from 'app/core/app_events'; +import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard'; + +interface Props { + response: any; +} + +export class QueryInspector extends PureComponent { + formattedJson: any; + clipboard: any; + + constructor(props) { + super(props); + } + + setFormattedJson = formattedJson => { + this.formattedJson = formattedJson; + }; + + getTextForClipboard = () => { + return JSON.stringify(this.formattedJson, null, 2); + }; + + onClipboardSuccess = () => { + appEvents.emit('alert-success', ['Content copied to clipboard']); + }; + + render() { + const { response } = this.props; + return ( + <> + {/* */} + {/* + */} + + + <> + Copy to Clipboard + + + + + ); + } +} From 857bd3d8adfac76c6079a3d6596563c62540e9f9 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 26 Nov 2018 14:26:28 +0100 Subject: [PATCH 07/14] react-panel: Toggle Expand/Collapse json nodes in Query Inspector --- .../dashboard/dashgrid/QueryInspector.tsx | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/QueryInspector.tsx b/public/app/features/dashboard/dashgrid/QueryInspector.tsx index ace155fec43..9733ae58263 100644 --- a/public/app/features/dashboard/dashgrid/QueryInspector.tsx +++ b/public/app/features/dashboard/dashgrid/QueryInspector.tsx @@ -7,12 +7,20 @@ interface Props { response: any; } -export class QueryInspector extends PureComponent { +interface State { + allNodesExpanded: boolean; +} + +export class QueryInspector extends PureComponent { formattedJson: any; clipboard: any; constructor(props) { super(props); + + this.state = { + allNodesExpanded: null, + }; } setFormattedJson = formattedJson => { @@ -27,8 +35,26 @@ export class QueryInspector extends PureComponent { appEvents.emit('alert-success', ['Content copied to clipboard']); }; + onToggleExpand = () => { + this.setState(prevState => ({ + ...prevState, + allNodesExpanded: !this.state.allNodesExpanded, + })); + }; + + getNrOfOpenNodes = () => { + if (this.state.allNodesExpanded === null) { + return 3; + } else if (this.state.allNodesExpanded) { + return 20; + } + return 1; + }; + render() { const { response } = this.props; + const { allNodesExpanded } = this.state; + const openNodes = this.getNrOfOpenNodes(); return ( <> {/*
@@ -44,6 +70,17 @@ export class QueryInspector extends PureComponent {
*/} {/* */} + { Copy to Clipboard - + ); } From 23ae1c7184e007fa6d289c1f57dbe0ad53098a33 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 27 Nov 2018 10:52:47 +0100 Subject: [PATCH 08/14] react-panel: Move all query inspector logic into QueryInspector component and start with the "Mock response" --- .../dashboard/dashgrid/QueriesTab.tsx | 100 +------- .../dashboard/dashgrid/QueryInspector.tsx | 213 +++++++++++++++--- public/sass/components/_buttons.scss | 6 + 3 files changed, 187 insertions(+), 132 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index d35e55f591d..9a679832048 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -13,7 +13,6 @@ import { QueryInspector } from './QueryInspector'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv'; import { DataSourceSelectItem } from 'app/types'; -import appEvents from 'app/core/app_events'; import Remarkable from 'remarkable'; @@ -27,15 +26,9 @@ interface Help { helpHtml: any; } -interface DsQuery { - isLoading: boolean; - response: {}; -} - interface State { currentDatasource: DataSourceSelectItem; help: Help; - dsQuery: DsQuery; } interface LoadingPlaceholderProps { @@ -60,13 +53,7 @@ export class QueriesTab extends PureComponent { isLoading: false, helpHtml: null, }, - dsQuery: { - isLoading: false, - response: {}, - }, }; - appEvents.on('ds-request-response', this.onDataSourceResponse); - panel.events.on('refresh', this.onPanelRefresh); } componentDidMount() { @@ -89,84 +76,11 @@ export class QueriesTab extends PureComponent { } componentWillUnmount() { - const { panel } = this.props; - appEvents.off('ds-request-response', this.onDataSourceResponse); - panel.events.off('refresh', this.onPanelRefresh); - if (this.component) { this.component.destroy(); } } - onPanelRefresh = () => { - this.setState(prevState => ({ - ...prevState, - dsQuery: { - isLoading: true, - response: {}, - }, - })); - }; - - onDataSourceResponse = (response: any = {}) => { - // ignore if closed - // if (!this.isOpen) { - // return; - // } - - // if (this.isMocking) { - // this.handleMocking(data); - // return; - // } - - // this.isLoading = false; - // data = _.cloneDeep(data); - response = { ...response }; // clone - - if (response.headers) { - delete response.headers; - } - - if (response.config) { - response.request = response.config; - delete response.config; - delete response.request.transformRequest; - delete response.request.transformResponse; - delete response.request.paramSerializer; - delete response.request.jsonpCallbackParam; - delete response.request.headers; - delete response.request.requestId; - delete response.request.inspect; - delete response.request.retry; - delete response.request.timeout; - } - - if (response.data) { - response.response = response.data; - - // if (response.status === 200) { - // // if we are in error state, assume we automatically opened - // // and auto close it again - // if (this.hasError) { - // this.hasError = false; - // this.isOpen = false; - // } - // } - - delete response.data; - delete response.status; - delete response.statusText; - delete response.$$config; - } - this.setState(prevState => ({ - ...prevState, - dsQuery: { - isLoading: false, - response: response, - }, - })); - }; - onChangeDataSource = datasource => { const { panel } = this.props; const { currentDatasource } = this.state; @@ -291,18 +205,9 @@ export class QueriesTab extends PureComponent { }); }; - loadQueryInspector = () => { - const { panel } = this.props; - panel.refresh(); - }; - renderQueryInspector = () => { - const { response, isLoading } = this.state.dsQuery; - return isLoading ? ( - - ) : ( - - ); + const { panel } = this.props; + return ; }; renderHelp = () => { @@ -331,7 +236,6 @@ export class QueriesTab extends PureComponent { const queryInspector = { title: 'Query Inspector', - onClick: this.loadQueryInspector, render: this.renderQueryInspector, }; diff --git a/public/app/features/dashboard/dashgrid/QueryInspector.tsx b/public/app/features/dashboard/dashgrid/QueryInspector.tsx index 9733ae58263..08da527c035 100644 --- a/public/app/features/dashboard/dashgrid/QueryInspector.tsx +++ b/public/app/features/dashboard/dashgrid/QueryInspector.tsx @@ -3,12 +3,21 @@ import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter'; import appEvents from 'app/core/app_events'; import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard'; +interface DsQuery { + isLoading: boolean; + response: {}; +} + interface Props { - response: any; + panel: any; + LoadingPlaceholder: any; } interface State { allNodesExpanded: boolean; + isMocking: boolean; + mockedResponse: string; + dsQuery: DsQuery; } export class QueryInspector extends PureComponent { @@ -17,12 +26,112 @@ export class QueryInspector extends PureComponent { constructor(props) { super(props); - this.state = { allNodesExpanded: null, + isMocking: false, + mockedResponse: '', + dsQuery: { + isLoading: false, + response: {}, + }, }; } + componentDidMount() { + const { panel } = this.props; + panel.events.on('refresh', this.onPanelRefresh); + appEvents.on('ds-request-response', this.onDataSourceResponse); + panel.refresh(); + } + + componentWillUnmount() { + const { panel } = this.props; + appEvents.off('ds-request-response', this.onDataSourceResponse); + panel.events.off('refresh', this.onPanelRefresh); + } + + handleMocking(response) { + const { mockedResponse } = this.state; + let mockedData; + try { + mockedData = JSON.parse(mockedResponse); + } catch (err) { + appEvents.emit('alert-error', ['R: Failed to parse mocked response']); + return; + } + + response.data = mockedData; + } + + onPanelRefresh = () => { + this.setState(prevState => ({ + ...prevState, + dsQuery: { + isLoading: true, + response: {}, + }, + })); + }; + + onDataSourceResponse = (response: any = {}) => { + // ignore if closed + // if (!this.isOpen) { + // return; + // } + + if (this.state.isMocking) { + this.handleMocking(response); + return; + } + + // this.isLoading = false; + // data = _.cloneDeep(data); + response = { ...response }; // clone + + if (response.headers) { + delete response.headers; + } + + if (response.config) { + response.request = response.config; + delete response.config; + delete response.request.transformRequest; + delete response.request.transformResponse; + delete response.request.paramSerializer; + delete response.request.jsonpCallbackParam; + delete response.request.headers; + delete response.request.requestId; + delete response.request.inspect; + delete response.request.retry; + delete response.request.timeout; + } + + if (response.data) { + response.response = response.data; + + // if (response.status === 200) { + // // if we are in error state, assume we automatically opened + // // and auto close it again + // if (this.hasError) { + // this.hasError = false; + // this.isOpen = false; + // } + // } + + delete response.data; + delete response.status; + delete response.statusText; + delete response.$$config; + } + this.setState(prevState => ({ + ...prevState, + dsQuery: { + isLoading: false, + response: response, + }, + })); + }; + setFormattedJson = formattedJson => { this.formattedJson = formattedJson; }; @@ -42,56 +151,92 @@ export class QueryInspector extends PureComponent { })); }; + onToggleMocking = () => { + this.setState(prevState => ({ + ...prevState, + isMocking: !this.state.isMocking, + })); + }; + getNrOfOpenNodes = () => { if (this.state.allNodesExpanded === null) { - return 3; + return 3; // 3 is default, ie when state is null } else if (this.state.allNodesExpanded) { return 20; } return 1; }; + setMockedResponse = evt => { + const mockedResponse = evt.target.value; + this.setState(prevState => ({ + ...prevState, + mockedResponse, + })); + }; + render() { - const { response } = this.props; - const { allNodesExpanded } = this.state; + const { response, isLoading } = this.state.dsQuery; + const { LoadingPlaceholder } = this.props; + const { allNodesExpanded, isMocking } = this.state; const openNodes = this.getNrOfOpenNodes(); + + if (isLoading) { + return ; + } + return ( <> {/*
Mock Response - - Expand All - - - Collapse All - - Copy to Clipboard + */} +
+ + -
*/} - {/* - */} - + +
- - <> - Copy to Clipboard - - - + {!isMocking && } + {isMocking && ( +
+
+ */} +
+
+ )} ); } diff --git a/public/sass/components/_buttons.scss b/public/sass/components/_buttons.scss index dcb4686701d..87947965fc3 100644 --- a/public/sass/components/_buttons.scss +++ b/public/sass/components/_buttons.scss @@ -172,6 +172,12 @@ padding-right: 20px; } +// No horizontal padding +.btn-p-x-0 { + padding-left: 0; + padding-right: 0; +} + // External services // Usage: // From 13d0a117980cdec06dfbe82f4ebc847523857e7b Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 27 Nov 2018 12:13:31 +0100 Subject: [PATCH 09/14] react-panel: Remove json-formatter-js since we will continue with the "patched" version --- package.json | 1 - yarn.lock | 5 ----- 2 files changed, 6 deletions(-) diff --git a/package.json b/package.json index fa9a0dcad2e..452e4ecb742 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,6 @@ "file-saver": "^1.3.3", "immutable": "^3.8.2", "jquery": "^3.2.1", - "json-formatter-js": "^2.2.1", "lodash": "^4.17.10", "moment": "^2.22.2", "mousetrap": "^1.6.0", diff --git a/yarn.lock b/yarn.lock index be55f6fd52e..2cebcae1d9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6811,11 +6811,6 @@ json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" -json-formatter-js@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/json-formatter-js/-/json-formatter-js-2.2.1.tgz#b101d628e86f028dc9cf9a7e1c83c65e536c9f87" - integrity sha1-sQHWKOhvAo3Jz5p+HIPGXlNsn4c= - json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" From 3908e64ef0319093f758ec7b052ef33979f4e190 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 27 Nov 2018 12:23:11 +0100 Subject: [PATCH 10/14] react-panel: Use correct type for children prop to avoid the use of fragments <> --- .../app/core/components/CopyToClipboard/CopyToClipboard.tsx | 4 ++-- public/app/features/dashboard/dashgrid/QueryInspector.tsx | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/public/app/core/components/CopyToClipboard/CopyToClipboard.tsx b/public/app/core/components/CopyToClipboard/CopyToClipboard.tsx index 153dae2a9c6..ea63de58b47 100644 --- a/public/app/core/components/CopyToClipboard/CopyToClipboard.tsx +++ b/public/app/core/components/CopyToClipboard/CopyToClipboard.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent, ReactNode } from 'react'; import ClipboardJS from 'clipboard'; interface Props { @@ -7,7 +7,7 @@ interface Props { onSuccess?: (evt: any) => void; onError?: (evt: any) => void; className?: string; - children?: JSX.Element | string; + children?: ReactNode; } export class CopyToClipboard extends PureComponent { diff --git a/public/app/features/dashboard/dashgrid/QueryInspector.tsx b/public/app/features/dashboard/dashgrid/QueryInspector.tsx index 08da527c035..6fc2669ab33 100644 --- a/public/app/features/dashboard/dashgrid/QueryInspector.tsx +++ b/public/app/features/dashboard/dashgrid/QueryInspector.tsx @@ -211,9 +211,7 @@ export class QueryInspector extends PureComponent { text={this.getTextForClipboard} onSuccess={this.onClipboardSuccess} > - <> - Copy to Clipboard - + Copy to Clipboard
From a03900e6cf795b51b973041620bc2f7ebce82238 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 27 Nov 2018 12:33:30 +0100 Subject: [PATCH 11/14] react-panel: Remove comments and improve readability in render() --- .../dashboard/dashgrid/QueryInspector.tsx | 49 +++++++------------ 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/QueryInspector.tsx b/public/app/features/dashboard/dashgrid/QueryInspector.tsx index 6fc2669ab33..870fb4e7c96 100644 --- a/public/app/features/dashboard/dashgrid/QueryInspector.tsx +++ b/public/app/features/dashboard/dashgrid/QueryInspector.tsx @@ -74,19 +74,12 @@ export class QueryInspector extends PureComponent { }; onDataSourceResponse = (response: any = {}) => { - // ignore if closed - // if (!this.isOpen) { - // return; - // } - if (this.state.isMocking) { this.handleMocking(response); return; } - // this.isLoading = false; - // data = _.cloneDeep(data); - response = { ...response }; // clone + response = { ...response }; // clone - dont modify the response if (response.headers) { delete response.headers; @@ -109,15 +102,6 @@ export class QueryInspector extends PureComponent { if (response.data) { response.response = response.data; - // if (response.status === 200) { - // // if we are in error state, assume we automatically opened - // // and auto close it again - // if (this.hasError) { - // this.hasError = false; - // this.isOpen = false; - // } - // } - delete response.data; delete response.status; delete response.statusText; @@ -175,10 +159,26 @@ export class QueryInspector extends PureComponent { })); }; + renderExpandCollapse = () => { + const { allNodesExpanded } = this.state; + + const collapse = ( + <> + Collapse All + + ); + const expand = ( + <> + Expand All + + ); + return allNodesExpanded ? collapse : expand; + }; + render() { const { response, isLoading } = this.state.dsQuery; const { LoadingPlaceholder } = this.props; - const { allNodesExpanded, isMocking } = this.state; + const { isMocking } = this.state; const openNodes = this.getNrOfOpenNodes(); if (isLoading) { @@ -187,23 +187,12 @@ export class QueryInspector extends PureComponent { return ( <> - {/*
- Mock Response - */}
Date: Tue, 27 Nov 2018 12:36:57 +0100 Subject: [PATCH 12/14] react-panel: Remove mock response button for now --- public/app/features/dashboard/dashgrid/QueryInspector.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/QueryInspector.tsx b/public/app/features/dashboard/dashgrid/QueryInspector.tsx index 870fb4e7c96..ec618404651 100644 --- a/public/app/features/dashboard/dashgrid/QueryInspector.tsx +++ b/public/app/features/dashboard/dashgrid/QueryInspector.tsx @@ -188,9 +188,11 @@ export class QueryInspector extends PureComponent { return ( <>
+ {/* + */} @@ -215,12 +217,6 @@ export class QueryInspector extends PureComponent { onInput={this.setMockedResponse} placeholder="JSON" /> - {/* */}
)} From 69ae3d2e6ac0540809ed061b38ca45f18b9624f9 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 28 Nov 2018 15:15:31 +0100 Subject: [PATCH 13/14] react-panel: Time range options moved to "Queries" tab --- public/app/core/components/Form/Element.tsx | 43 ++++++++ public/app/core/components/Form/Input.tsx | 96 +++++++++++++++++ public/app/core/components/Form/Label.tsx | 19 ++++ public/app/core/components/Form/index.ts | 3 + public/app/core/utils/rangeutil.ts | 9 ++ .../dashboard/dashgrid/QueriesTab.tsx | 101 +++++++++++++++++- public/app/types/form.ts | 4 + public/app/types/index.ts | 3 +- public/sass/utils/_validation.scss | 6 +- 9 files changed, 280 insertions(+), 4 deletions(-) create mode 100644 public/app/core/components/Form/Element.tsx create mode 100644 public/app/core/components/Form/Input.tsx create mode 100644 public/app/core/components/Form/Label.tsx create mode 100644 public/app/core/components/Form/index.ts create mode 100644 public/app/types/form.ts diff --git a/public/app/core/components/Form/Element.tsx b/public/app/core/components/Form/Element.tsx new file mode 100644 index 00000000000..997d7f0e717 --- /dev/null +++ b/public/app/core/components/Form/Element.tsx @@ -0,0 +1,43 @@ +import React, { PureComponent, ReactNode, ReactElement } from 'react'; +import { Label } from './Label'; +import { uniqueId } from 'lodash'; + +interface Props { + label?: ReactNode; + labelClassName?: string; + id?: string; + children: ReactElement; +} + +export class Element extends PureComponent { + elementId: string = this.props.id || uniqueId('form-element-'); + + get elementLabel() { + const { label, labelClassName } = this.props; + + if (label) { + return ( + + ); + } + + return null; + } + + get children() { + const { children } = this.props; + + return React.cloneElement(children, { id: this.elementId }); + } + + render() { + return ( +
+ {this.elementLabel} + {this.children} +
+ ); + } +} diff --git a/public/app/core/components/Form/Input.tsx b/public/app/core/components/Form/Input.tsx new file mode 100644 index 00000000000..a261203b3f3 --- /dev/null +++ b/public/app/core/components/Form/Input.tsx @@ -0,0 +1,96 @@ +import React, { PureComponent } from 'react'; +import { ValidationRule } from 'app/types'; + +export enum InputStatus { + Default = 'default', + Loading = 'loading', + Invalid = 'invalid', + Valid = 'valid', +} + +export enum InputTypes { + Text = 'text', + Number = 'number', + Password = 'password', + Email = 'email', +} + +interface Props { + status?: InputStatus; + validationRules: ValidationRule[]; + hideErrorMessage?: boolean; + onBlurWithStatus?: (evt, status: InputStatus) => void; + emptyToNull?: boolean; +} + +const validator = (value: string, validationRules: ValidationRule[]) => { + const errors = validationRules.reduce((acc, currRule) => { + if (!currRule.rule(value)) { + return acc.concat(currRule.errorMessage); + } + return acc; + }, []); + return errors.length > 0 ? errors : null; +}; + +export class Input extends PureComponent> { + state = { + error: null, + }; + + get status() { + const { error } = this.state; + if (error) { + return InputStatus.Invalid; + } + return InputStatus.Valid; + } + + onBlurWithValidation = evt => { + const { validationRules, onBlurWithStatus, onBlur } = this.props; + + let errors = null; + if (validationRules) { + errors = validator(evt.currentTarget.value, validationRules); + this.setState(prevState => { + return { + ...prevState, + error: errors ? errors[0] : null, + }; + }); + } + + if (onBlurWithStatus) { + onBlurWithStatus(evt, errors ? InputStatus.Invalid : InputStatus.Valid); + } + + if (onBlur) { + onBlur(evt); + } + }; + + render() { + const { + status, + validationRules, + onBlurWithStatus, + onBlur, + className, + hideErrorMessage, + emptyToNull, + ...restProps + } = this.props; + + const { error } = this.state; + + let inputClassName = 'gf-form-input'; + inputClassName = this.status === InputStatus.Invalid ? inputClassName + ' invalid' : inputClassName; + + return ( +
+ + {error && !hideErrorMessage && {error}} +
+ ); + } +} diff --git a/public/app/core/components/Form/Label.tsx b/public/app/core/components/Form/Label.tsx new file mode 100644 index 00000000000..385a1b325be --- /dev/null +++ b/public/app/core/components/Form/Label.tsx @@ -0,0 +1,19 @@ +import React, { PureComponent, ReactNode } from 'react'; + +interface Props { + children: ReactNode; + htmlFor?: string; + className?: string; +} + +export class Label extends PureComponent { + render() { + const { children, htmlFor, className } = this.props; + + return ( + + ); + } +} diff --git a/public/app/core/components/Form/index.ts b/public/app/core/components/Form/index.ts new file mode 100644 index 00000000000..e4c8197aaa9 --- /dev/null +++ b/public/app/core/components/Form/index.ts @@ -0,0 +1,3 @@ +export { Element } from './Element'; +export { Input } from './Input'; +export { Label } from './Label'; diff --git a/public/app/core/utils/rangeutil.ts b/public/app/core/utils/rangeutil.ts index 2079aa39006..0150e80f1ed 100644 --- a/public/app/core/utils/rangeutil.ts +++ b/public/app/core/utils/rangeutil.ts @@ -159,3 +159,12 @@ export function describeTimeRange(range: RawTimeRange): string { return range.from.toString() + ' to ' + range.to.toString(); } + +export const isValidTimeSpan = (value: string) => { + if (value.indexOf('$') === 0 || value.indexOf('+$') === 0) { + return true; + } + + const info = describeTextRange(value); + return info.invalid !== true; +}; diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index 9a679832048..3c40c8a3568 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -8,6 +8,11 @@ import { DashboardModel } from '../dashboard_model'; import './../../panel/metrics_tab'; import config from 'app/core/config'; import { QueryInspector } from './QueryInspector'; +import { Switch } from 'app/core/components/Switch/Switch'; +import { Input } from 'app/core/components/Form'; +import { InputStatus } from 'app/core/components/Form/Input'; +import { isValidTimeSpan } from 'app/core/utils/rangeutil'; +import { ValidationRule } from 'app/types'; // Services import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -29,6 +34,7 @@ interface Help { interface State { currentDatasource: DataSourceSelectItem; help: Help; + hideTimeOverride: boolean; } interface LoadingPlaceholderProps { @@ -36,6 +42,17 @@ interface LoadingPlaceholderProps { } const LoadingPlaceholder: SFC = ({ text }) =>

{text}

; +const validationRules: ValidationRule[] = [ + { + rule: value => { + if (!value) { + return true; + } + return isValidTimeSpan(value); + }, + errorMessage: 'Not a valid timespan', + }, +]; export class QueriesTab extends PureComponent { element: any; @@ -53,6 +70,7 @@ export class QueriesTab extends PureComponent { isLoading: false, helpHtml: null, }, + hideTimeOverride: false, }; } @@ -215,9 +233,40 @@ export class QueriesTab extends PureComponent { return isLoading ? : helpHtml; }; + emptyToNull = (value: string) => { + return value === '' ? null : value; + }; + + onOverrideTime = (evt, status: InputStatus) => { + const { value } = evt.target; + const { panel } = this.props; + const emptyToNullValue = this.emptyToNull(value); + if (status === InputStatus.Valid && panel.timeFrom !== emptyToNullValue) { + panel.timeFrom = emptyToNullValue; + panel.refresh(); + } + }; + + onTimeShift = (evt, status: InputStatus) => { + const { value } = evt.target; + const { panel } = this.props; + const emptyToNullValue = this.emptyToNull(value); + if (status === InputStatus.Valid && panel.timeShift !== emptyToNullValue) { + panel.timeShift = emptyToNullValue; + panel.refresh(); + } + }; + + onToggleTimeOverride = () => { + const { panel } = this.props; + panel.hideTimeOverride = !panel.hideTimeOverride; + panel.refresh(); + }; + render() { const { currentDatasource } = this.state; - + const hideTimeOverride = this.props.panel.hideTimeOverride; + console.log('hideTimeOverride', hideTimeOverride); const { hasQueryHelp, queryOptions } = currentDatasource.meta; const hasQueryOptions = !!queryOptions; const dsInformation = { @@ -256,7 +305,55 @@ export class QueriesTab extends PureComponent { return ( -
(this.element = element)} style={{ width: '100%' }} /> + <> +
(this.element = element)} style={{ width: '100%' }} /> + +
Time Range
+ +
+
+ + + + + Override relative time + Last + +
+ +
+ + + + Add time shift + Amount + +
+ +
+
+ + + +
+ +
+
+ ); } diff --git a/public/app/types/form.ts b/public/app/types/form.ts new file mode 100644 index 00000000000..180b41d8730 --- /dev/null +++ b/public/app/types/form.ts @@ -0,0 +1,4 @@ +export interface ValidationRule { + rule: (value: string) => boolean; + errorMessage: string; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index bf19e52468b..e60dcb0993d 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -30,7 +30,7 @@ import { AppNotificationTimeout, } from './appNotifications'; import { DashboardSearchHit } from './search'; - +import { ValidationRule } from './form'; export { Team, TeamsState, @@ -89,6 +89,7 @@ export { AppNotificationTimeout, DashboardSearchHit, UserState, + ValidationRule, }; export interface StoreState { diff --git a/public/sass/utils/_validation.scss b/public/sass/utils/_validation.scss index 86b7c008bfd..657d1f0414b 100644 --- a/public/sass/utils/_validation.scss +++ b/public/sass/utils/_validation.scss @@ -1,7 +1,11 @@ -input[type="text"].ng-dirty.ng-invalid { +input[type='text'].ng-dirty.ng-invalid { } input.validation-error, input.ng-dirty.ng-invalid { box-shadow: inset 0 0px 5px $red; } + +input.invalid { + box-shadow: inset 0 0px 5px $red; +} From a8e184c02572ecb06fcc622d490d5bce7440eaa7 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Fri, 30 Nov 2018 10:16:04 +0100 Subject: [PATCH 14/14] react-panel: Clean up input validation and increase code readability --- public/app/core/components/Form/Input.tsx | 94 +++++++++---------- public/app/core/utils/validate.ts | 11 +++ .../dashboard/dashgrid/QueriesTab.tsx | 35 +++---- public/app/types/form.ts | 6 +- public/app/types/index.ts | 3 +- 5 files changed, 79 insertions(+), 70 deletions(-) create mode 100644 public/app/core/utils/validate.ts diff --git a/public/app/core/components/Form/Input.tsx b/public/app/core/components/Form/Input.tsx index a261203b3f3..315acbd645c 100644 --- a/public/app/core/components/Form/Input.tsx +++ b/public/app/core/components/Form/Input.tsx @@ -1,9 +1,8 @@ import React, { PureComponent } from 'react'; -import { ValidationRule } from 'app/types'; +import { ValidationEvents, ValidationRule } from 'app/types'; +import { validate } from 'app/core/utils/validate'; export enum InputStatus { - Default = 'default', - Loading = 'loading', Invalid = 'invalid', Valid = 'valid', } @@ -15,80 +14,71 @@ export enum InputTypes { Email = 'email', } -interface Props { - status?: InputStatus; - validationRules: ValidationRule[]; - hideErrorMessage?: boolean; - onBlurWithStatus?: (evt, status: InputStatus) => void; - emptyToNull?: boolean; +export enum EventsWithValidation { + onBlur = 'onBlur', + onFocus = 'onFocus', + onChange = 'onChange', } -const validator = (value: string, validationRules: ValidationRule[]) => { - const errors = validationRules.reduce((acc, currRule) => { - if (!currRule.rule(value)) { - return acc.concat(currRule.errorMessage); - } - return acc; - }, []); - return errors.length > 0 ? errors : null; -}; +interface Props extends React.HTMLProps { + validationEvents: ValidationEvents; + hideErrorMessage?: boolean; -export class Input extends PureComponent> { + // Override event props and append status as argument + onBlur?: (event: React.FocusEvent, status?: InputStatus) => void; + onFocus?: (event: React.FocusEvent, status?: InputStatus) => void; + onChange?: (event: React.FormEvent, status?: InputStatus) => void; +} + +export class Input extends PureComponent { state = { error: null, }; get status() { - const { error } = this.state; - if (error) { - return InputStatus.Invalid; - } - return InputStatus.Valid; + return this.state.error ? InputStatus.Invalid : InputStatus.Valid; } - onBlurWithValidation = evt => { - const { validationRules, onBlurWithStatus, onBlur } = this.props; + get isInvalid() { + return this.status === InputStatus.Invalid; + } - let errors = null; - if (validationRules) { - errors = validator(evt.currentTarget.value, validationRules); + validatorAsync = (validationRules: ValidationRule[]) => { + return evt => { + const errors = validate(evt.currentTarget.value, validationRules); this.setState(prevState => { return { ...prevState, error: errors ? errors[0] : null, }; }); - } + }; + }; - if (onBlurWithStatus) { - onBlurWithStatus(evt, errors ? InputStatus.Invalid : InputStatus.Valid); - } - - if (onBlur) { - onBlur(evt); - } + populateEventPropsWithStatus = (restProps, validationEvents: ValidationEvents) => { + const inputElementProps = { ...restProps }; + Object.keys(EventsWithValidation).forEach(eventName => { + inputElementProps[eventName] = async evt => { + if (validationEvents[eventName]) { + await this.validatorAsync(validationEvents[eventName]).apply(this, [evt]); + } + if (restProps[eventName]) { + restProps[eventName].apply(null, [evt, this.status]); + } + }; + }); + return inputElementProps; }; render() { - const { - status, - validationRules, - onBlurWithStatus, - onBlur, - className, - hideErrorMessage, - emptyToNull, - ...restProps - } = this.props; - + const { validationEvents, className, hideErrorMessage, ...restProps } = this.props; const { error } = this.state; - - let inputClassName = 'gf-form-input'; - inputClassName = this.status === InputStatus.Invalid ? inputClassName + ' invalid' : inputClassName; + const inputClassName = 'gf-form-input' + (this.isInvalid ? ' invalid' : ''); + const inputElementProps = this.populateEventPropsWithStatus(restProps, validationEvents); return (
- + {error && !hideErrorMessage && {error}}
); diff --git a/public/app/core/utils/validate.ts b/public/app/core/utils/validate.ts new file mode 100644 index 00000000000..34f8125833f --- /dev/null +++ b/public/app/core/utils/validate.ts @@ -0,0 +1,11 @@ +import { ValidationRule } from 'app/types'; + +export const validate = (value: string, validationRules: ValidationRule[]) => { + const errors = validationRules.reduce((acc, currRule) => { + if (!currRule.rule(value)) { + return acc.concat(currRule.errorMessage); + } + return acc; + }, []); + return errors.length > 0 ? errors : null; +}; diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index 3c40c8a3568..016299f574e 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -10,9 +10,9 @@ import config from 'app/core/config'; import { QueryInspector } from './QueryInspector'; import { Switch } from 'app/core/components/Switch/Switch'; import { Input } from 'app/core/components/Form'; -import { InputStatus } from 'app/core/components/Form/Input'; +import { InputStatus, EventsWithValidation } from 'app/core/components/Form/Input'; import { isValidTimeSpan } from 'app/core/utils/rangeutil'; -import { ValidationRule } from 'app/types'; +import { ValidationEvents } from 'app/types'; // Services import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -42,17 +42,20 @@ interface LoadingPlaceholderProps { } const LoadingPlaceholder: SFC = ({ text }) =>

{text}

; -const validationRules: ValidationRule[] = [ - { - rule: value => { - if (!value) { - return true; - } - return isValidTimeSpan(value); + +const timeRangeValidationEvents: ValidationEvents = { + [EventsWithValidation.onBlur]: [ + { + rule: value => { + if (!value) { + return true; + } + return isValidTimeSpan(value); + }, + errorMessage: 'Not a valid timespan', }, - errorMessage: 'Not a valid timespan', - }, -]; + ], +}; export class QueriesTab extends PureComponent { element: any; @@ -322,8 +325,8 @@ export class QueriesTab extends PureComponent { type="text" className="gf-form-input max-width-8" placeholder="1h" - onBlurWithStatus={this.onOverrideTime} - validationRules={validationRules} + onBlur={this.onOverrideTime} + validationEvents={timeRangeValidationEvents} hideErrorMessage={true} />
@@ -338,8 +341,8 @@ export class QueriesTab extends PureComponent { type="text" className="gf-form-input max-width-8" placeholder="1h" - onBlurWithStatus={this.onTimeShift} - validationRules={validationRules} + onBlur={this.onTimeShift} + validationEvents={timeRangeValidationEvents} hideErrorMessage={true} />
diff --git a/public/app/types/form.ts b/public/app/types/form.ts index 180b41d8730..95026c30be9 100644 --- a/public/app/types/form.ts +++ b/public/app/types/form.ts @@ -1,4 +1,8 @@ export interface ValidationRule { - rule: (value: string) => boolean; + rule: (valueToValidate: string) => boolean; errorMessage: string; } + +export interface ValidationEvents { + [eventName: string]: ValidationRule[]; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index e60dcb0993d..1a56fad7bf0 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -30,7 +30,7 @@ import { AppNotificationTimeout, } from './appNotifications'; import { DashboardSearchHit } from './search'; -import { ValidationRule } from './form'; +import { ValidationEvents, ValidationRule } from './form'; export { Team, TeamsState, @@ -89,6 +89,7 @@ export { AppNotificationTimeout, DashboardSearchHit, UserState, + ValidationEvents, ValidationRule, };