From 0644410b99be40be56c6acac2fcacffc0cc58169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 14 Jan 2019 14:55:22 +0100 Subject: [PATCH 001/156] wip: react query editors --- public/app/features/dashboard/dashgrid/QueriesTab.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index 47c4f358136..500dc5c4884 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -198,6 +198,11 @@ export class QueriesTab extends PureComponent { this.setState({ isAddingMixed: false }); }; + renderQueryRow(query: DataQuery) { + console.log('render query row', this.state.currentDS); + return
(this.element = element)} />; + } + render() { const { panel } = this.props; const { currentDS, isAddingMixed } = this.state; @@ -218,7 +223,7 @@ export class QueriesTab extends PureComponent { <>
-
(this.element = element)} /> + {panel.targets.map(query => this.renderQueryRow(query))}
From 0260c779e8f167bcd7a83af1b890ccd1b0b69b8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 14 Jan 2019 15:44:58 +0100 Subject: [PATCH 002/156] wip: another wip commit --- .../dashboard/panel_editor/QueriesTab.tsx | 7 +---- .../dashboard/panel_editor/QueryEditorRow.tsx | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 public/app/features/dashboard/panel_editor/QueryEditorRow.tsx diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index 500dc5c4884..47c4f358136 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -198,11 +198,6 @@ export class QueriesTab extends PureComponent { this.setState({ isAddingMixed: false }); }; - renderQueryRow(query: DataQuery) { - console.log('render query row', this.state.currentDS); - return
(this.element = element)} />; - } - render() { const { panel } = this.props; const { currentDS, isAddingMixed } = this.state; @@ -223,7 +218,7 @@ export class QueriesTab extends PureComponent { <>
- {panel.targets.map(query => this.renderQueryRow(query))} +
(this.element = element)} />
diff --git a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx new file mode 100644 index 00000000000..b90c11da7c3 --- /dev/null +++ b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx @@ -0,0 +1,30 @@ +// Libraries +import React, { PureComponent } from 'react'; + +// Utils & Services +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; + +// Types +import { PanelModel } from '../panel_model'; +import { DashboardModel } from '../dashboard_model'; + +interface Props { + panel: PanelModel; + dashboard: DashboardModel; +} + +interface State { +} + +export class VisualizationTab extends PureComponent { + element: HTMLElement; + angularQueryEditor: AngularComponent; + + constructor(props) { + super(props); + } + + render() { + + } +} From ce3117507afaedeca3bc7b91f7d036b64066bdf2 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 14 Jan 2019 17:40:47 +0100 Subject: [PATCH 003/156] Prometheus: Fix annotation step calculation - since e731c248d7d7885d8f2e2d81bbce3dda4b35c31e step calculation for annotation queries was broken - this change puts the interval in the correct parameter so it gets considered in `createQuery` - added tests --- .../datasource/prometheus/datasource.ts | 10 ++- .../prometheus/specs/datasource.test.ts | 73 +++++++++++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 2c7c62d881b..ceaf88fb477 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -219,8 +219,9 @@ export class PrometheusDatasource { }; const range = Math.ceil(end - start); + // options.interval is the dynamically calculated interval let interval = kbn.interval_to_seconds(options.interval); - // Minimum interval ("Min step"), if specified for the query. or same as interval otherwise + // Minimum interval ("Min step"), if specified for the query or datasource. or same as interval otherwise const minInterval = kbn.interval_to_seconds( this.templateSrv.replace(target.interval, options.scopedVars) || options.interval ); @@ -366,12 +367,13 @@ export class PrometheusDatasource { const step = annotation.step || '60s'; const start = this.getPrometheusTime(options.range.from, false); const end = this.getPrometheusTime(options.range.to, true); - // Unsetting min interval const queryOptions = { ...options, - interval: '0s', + interval: step, }; - const query = this.createQuery({ expr, interval: step }, queryOptions, start, end); + // Unsetting min interval for accurate event resolution + const minStep = '1s'; + const query = this.createQuery({ expr, interval: minStep }, queryOptions, start, end); const self = this; return this.performTimeSeriesQuery(query, query.start, query.end).then(results => { diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index bff35c7ba88..46b37d37b8e 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -577,6 +577,79 @@ describe('PrometheusDatasource', () => { expect(results[0].time).toEqual(1); }); }); + + describe('step parameter', () => { + beforeEach(() => { + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + }); + + it('should use default step for short range if no interval is given', () => { + const query = { + ...options, + range: { + from: time({ seconds: 63 }), + to: time({ seconds: 123 }), + }, + }; + ctx.ds.annotationQuery(query); + const req = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(req.url).toContain('step=60'); + }); + + it('should use custom step for short range', () => { + const annotation = { + ...options.annotation, + step: '10s', + }; + const query = { + ...options, + annotation, + range: { + from: time({ seconds: 63 }), + to: time({ seconds: 123 }), + }, + }; + ctx.ds.annotationQuery(query); + const req = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(req.url).toContain('step=10'); + }); + + it('should use custom step for short range', () => { + const annotation = { + ...options.annotation, + step: '10s', + }; + const query = { + ...options, + annotation, + range: { + from: time({ seconds: 63 }), + to: time({ seconds: 123 }), + }, + }; + ctx.ds.annotationQuery(query); + const req = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(req.url).toContain('step=10'); + }); + + it('should use dynamic step on long ranges if no option was given', () => { + const query = { + ...options, + range: { + from: time({ seconds: 63 }), + to: time({ hours: 24 * 30, seconds: 63 }), + }, + }; + ctx.ds.annotationQuery(query); + const req = backendSrv.datasourceRequest.mock.calls[0][0]; + // Range in seconds: (to - from) / 1000 + // Max_datapints: 11000 + // Step: range / max_datapints + const step = 236; + expect(req.url).toContain(`step=${step}`); + }); + }); }); describe('When resultFormat is table and instant = true', () => { From ac62e4a99201de9070dd736759ee12f0fb8fc491 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 14 Jan 2019 22:09:06 +0000 Subject: [PATCH 004/156] FormGroup component and implements --- .../src/components/FormGroup/FormGroup.tsx | 27 +++++++ .../src}/components/Label/Label.tsx | 0 packages/grafana-ui/src/components/index.ts | 6 +- .../SharedPreferences/SharedPreferences.tsx | 2 +- .../datasources/settings/BasicSettings.tsx | 2 +- public/app/features/teams/TeamSettings.tsx | 2 +- .../panel/gauge/GaugeOptionsEditor.tsx | 20 ++--- public/app/plugins/panel/gauge/MappingRow.tsx | 80 ++++++++++--------- .../app/plugins/panel/gauge/ValueOptions.tsx | 49 +++++++----- 9 files changed, 114 insertions(+), 74 deletions(-) create mode 100644 packages/grafana-ui/src/components/FormGroup/FormGroup.tsx rename {public/app/core => packages/grafana-ui/src}/components/Label/Label.tsx (100%) diff --git a/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx b/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx new file mode 100644 index 00000000000..ac761fa5d2c --- /dev/null +++ b/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx @@ -0,0 +1,27 @@ +import React, { SFC } from 'react'; +import { Label } from '..'; + +interface Props { + label: string; + inputProps: {}; + labelWidth?: number; + inputWidth?: number; +} + +const defaultProps = { + labelWidth: 6, + inputProps: {}, + inputWidth: 12, +}; + +const FormGroup: SFC = ({ label, labelWidth, inputProps, inputWidth }) => { + return ( +
+ + +
+ ); +}; + +FormGroup.defaultProps = defaultProps; +export { FormGroup }; diff --git a/public/app/core/components/Label/Label.tsx b/packages/grafana-ui/src/components/Label/Label.tsx similarity index 100% rename from public/app/core/components/Label/Label.tsx rename to packages/grafana-ui/src/components/Label/Label.tsx diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 5420fcf14b7..ab0edf45ed0 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -9,12 +9,16 @@ export { IndicatorsContainer } from './Select/IndicatorsContainer'; export { NoOptionsMessage } from './Select/NoOptionsMessage'; export { default as resetSelectStyles } from './Select/resetSelectStyles'; +// Forms +export { GfFormLabel } from './GfFormLabel/GfFormLabel'; +export { FormGroup } from './FormGroup/FormGroup'; +export { Label } from './Label/Label'; + export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder'; export { ColorPicker } from './ColorPicker/ColorPicker'; export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover'; export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker'; export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor'; -export { GfFormLabel } from './GfFormLabel/GfFormLabel'; export { Graph } from './Graph/Graph'; export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup'; export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid'; diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index b13393ab2e1..ca933332db9 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; -import { Label } from 'app/core/components/Label/Label'; +import { Label } from '../../../../../packages/grafana-ui/src/components/Label/Label'; import { Select } from '@grafana/ui'; import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv'; diff --git a/public/app/features/datasources/settings/BasicSettings.tsx b/public/app/features/datasources/settings/BasicSettings.tsx index 120e002ac68..55dc9b54211 100644 --- a/public/app/features/datasources/settings/BasicSettings.tsx +++ b/public/app/features/datasources/settings/BasicSettings.tsx @@ -1,5 +1,5 @@ import React, { SFC } from 'react'; -import { Label } from 'app/core/components/Label/Label'; +import { Label } from '../../../../../packages/grafana-ui/src/components/Label/Label'; import { Switch } from '../../../core/components/Switch/Switch'; export interface Props { diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 5e058289bf0..3424f39d22c 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Label } from 'app/core/components/Label/Label'; +import { Label } from '../../../../packages/grafana-ui/src/components/Label/Label'; import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences'; import { updateTeam } from './state/actions'; import { getRouteParamsId } from 'app/core/selectors/location'; diff --git a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx b/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx index f1f78ab1172..c7758642080 100644 --- a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx +++ b/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import { GaugeOptions, PanelOptionsProps, PanelOptionsGroup } from '@grafana/ui'; import { Switch } from 'app/core/components/Switch/Switch'; -import { Label } from '../../../core/components/Label/Label'; +import { FormGroup } from '@grafana/ui/src'; export default class GaugeOptionsEditor extends PureComponent> { onToggleThresholdLabels = () => @@ -21,14 +21,16 @@ export default class GaugeOptionsEditor extends PureComponent -
- - -
-
- - -
+ this.onMinValueChange(event), value: minValue }} + /> + this.onMaxValueChange(event), value: maxValue }} + /> { if (type === MappingType.RangeToText) { return ( <> -
- - -
-
- - -
-
- - -
+ this.onMappingFromChange(event), + onBlur: () => this.updateMapping(), + value: from, + }} + inputWidth={8} + /> + this.updateMapping, + onChange: event => this.onMappingToChange(event), + value: to, + }} + inputWidth={8} + /> + this.updateMapping, + onChange: event => this.onMappingTextChange(event), + value: text, + }} + inputWidth={10} + /> ); } return ( <> -
- - -
+ this.updateMapping, + onChange: event => this.onMappingValueChange(event), + value: value, + }} + inputWidth={8} + />
Unit
-
- - -
-
- - -
-
- - -
+ this.onDecimalChange(event), + value: decimals || '', + type: 'number', + }} + /> + this.onPrefixChange(event), + value: prefix || '', + }} + /> + this.onSuffixChange(event), + value: suffix || '', + }} + /> ); } From 33feb26fb5239d3d05b2653d3224632428311174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 15 Jan 2019 11:40:12 +0100 Subject: [PATCH 005/156] WIP: good progress on react query editor support --- .../dashboard/panel_editor/QueriesTab.tsx | 73 ++++-------- .../dashboard/panel_editor/QueryEditorRow.tsx | 106 ++++++++++++++++-- public/app/features/panel/metrics_tab.ts | 31 ----- .../features/panel/partials/metrics_tab.html | 24 ---- .../app/features/plugins/plugin_component.ts | 28 ++--- public/app/types/plugins.ts | 1 + public/app/types/series.ts | 6 +- 7 files changed, 135 insertions(+), 134 deletions(-) delete mode 100644 public/app/features/panel/metrics_tab.ts delete mode 100644 public/app/features/panel/partials/metrics_tab.html diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index 47c4f358136..1c842e6572c 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -3,18 +3,16 @@ import React, { PureComponent } from 'react'; import _ from 'lodash'; // Components -import 'app/features/panel/metrics_tab'; import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { QueryInspector } from './QueryInspector'; import { QueryOptions } from './QueryOptions'; -import { AngularQueryComponentScope } from 'app/features/panel/metrics_tab'; import { PanelOptionsGroup } from '@grafana/ui'; +import { QueryEditorRow } from './QueryEditorRow'; // Services import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv'; -import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; import config from 'app/core/config'; // Types @@ -37,63 +35,22 @@ interface State { } export class QueriesTab extends PureComponent { - element: HTMLElement; - component: AngularComponent; datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources(); backendSrv: BackendSrv = getBackendSrv(); - constructor(props) { - super(props); - - this.state = { - isLoadingHelp: false, - currentDS: this.findCurrentDataSource(), - helpContent: null, - isPickerOpen: false, - isAddingMixed: false, - }; - } + state: State = { + isLoadingHelp: false, + currentDS: this.findCurrentDataSource(), + helpContent: null, + isPickerOpen: false, + isAddingMixed: false, + }; findCurrentDataSource(): DataSourceSelectItem { const { panel } = this.props; return this.datasources.find(datasource => datasource.value === panel.datasource) || this.datasources[0]; } - getAngularQueryComponentScope(): AngularQueryComponentScope { - const { panel, dashboard } = this.props; - - return { - panel: panel, - dashboard: dashboard, - refresh: () => panel.refresh(), - render: () => panel.render, - addQuery: this.onAddQuery, - moveQuery: this.onMoveQuery, - removeQuery: this.onRemoveQuery, - events: panel.events, - }; - } - - componentDidMount() { - if (!this.element) { - return; - } - - const loader = getAngularLoader(); - const template = ''; - const scopeProps = { - ctrl: this.getAngularQueryComponentScope(), - }; - - this.component = loader.load(this.element, scopeProps, template); - } - - componentWillUnmount() { - if (this.component) { - this.component.destroy(); - } - } - onChangeDataSource = datasource => { const { panel } = this.props; const { currentDS } = this.state; @@ -147,7 +104,6 @@ export class QueriesTab extends PureComponent { } this.props.panel.addQuery(); - this.component.digest(); this.forceUpdate(); }; @@ -190,7 +146,6 @@ export class QueriesTab extends PureComponent { onAddMixedQuery = datasource => { this.onAddQuery({ datasource: datasource.name }); - this.component.digest(); this.setState({ isAddingMixed: false }); }; @@ -218,7 +173,17 @@ export class QueriesTab extends PureComponent { <>
-
(this.element = element)} /> + {panel.targets.map((query, index) => ( + + ))}
diff --git a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx index b90c11da7c3..1028815cf08 100644 --- a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx +++ b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx @@ -2,29 +2,121 @@ import React, { PureComponent } from 'react'; // Utils & Services +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; +import { Emitter } from 'app/core/utils/emitter'; // Types import { PanelModel } from '../panel_model'; -import { DashboardModel } from '../dashboard_model'; +import { DataQuery, DataSourceApi } from 'app/types/series'; interface Props { panel: PanelModel; - dashboard: DashboardModel; + query: DataQuery; + onAddQuery: (query?: DataQuery) => void; + onRemoveQuery: (query: DataQuery) => void; + onMoveQuery: (query: DataQuery, direction: number) => void; + datasourceName: string | null; } interface State { + datasource: DataSourceApi | null; } -export class VisualizationTab extends PureComponent { - element: HTMLElement; - angularQueryEditor: AngularComponent; +export class QueryEditorRow extends PureComponent { + element: HTMLElement | null = null; + angularQueryEditor: AngularComponent | null = null; - constructor(props) { - super(props); + state: State = { + datasource: null, + }; + + componentDidMount() { + this.loadDatasource(); + } + + getAngularQueryComponentScope(): AngularQueryComponentScope { + const { panel, onAddQuery, onMoveQuery, onRemoveQuery, query } = this.props; + const { datasource } = this.state; + + return { + datasource: datasource, + target: query, + panel: panel, + refresh: () => panel.refresh(), + render: () => panel.render, + addQuery: onAddQuery, + moveQuery: onMoveQuery, + removeQuery: onRemoveQuery, + events: panel.events, + }; + } + + async loadDatasource() { + const { query, panel } = this.props; + const dataSourceSrv = getDatasourceSrv(); + const datasource = await dataSourceSrv.get(query.datasource || panel.datasource); + + this.setState({ datasource }); + } + + componentDidUpdate() { + const { datasource } = this.state; + + // check if we need to load another datasource + if (datasource && datasource.name !== this.props.datasourceName) { + if (this.angularQueryEditor) { + this.angularQueryEditor.destroy(); + this.angularQueryEditor = null; + } + this.loadDatasource(); + return; + } + + if (!this.element || this.angularQueryEditor) { + return; + } + + const loader = getAngularLoader(); + const template = ''; + const scopeProps = { ctrl: this.getAngularQueryComponentScope() }; + + this.angularQueryEditor = loader.load(this.element, scopeProps, template); + } + + componentWillUnmount() { + if (this.angularQueryEditor) { + this.angularQueryEditor.destroy(); + } } render() { + const { datasource } = this.state; + if (!datasource) { + return null; + } + + if (datasource.pluginExports.QueryCtrl) { + return
(this.element = element)} />; + } else if (datasource.pluginExports.QueryEditor) { + const QueryEditor = datasource.pluginExports.QueryEditor; + return ; + } + + return
Data source plugin does not export any Query Editor component
; } } + +export interface AngularQueryComponentScope { + target: DataQuery; + panel: PanelModel; + events: Emitter; + refresh: () => void; + render: () => void; + removeQuery: (query: DataQuery) => void; + addQuery: (query?: DataQuery) => void; + moveQuery: (query: DataQuery, direction: number) => void; + datasource: DataSourceApi; +} + diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts deleted file mode 100644 index 74418484e3a..00000000000 --- a/public/app/features/panel/metrics_tab.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Services & utils -import coreModule from 'app/core/core_module'; -import { Emitter } from 'app/core/utils/emitter'; - -// Types -import { DashboardModel } from '../dashboard/dashboard_model'; -import { PanelModel } from '../dashboard/panel_model'; -import { DataQuery } from 'app/types'; - -export interface AngularQueryComponentScope { - panel: PanelModel; - dashboard: DashboardModel; - events: Emitter; - refresh: () => void; - render: () => void; - removeQuery: (query: DataQuery) => void; - addQuery: (query?: DataQuery) => void; - moveQuery: (query: DataQuery, direction: number) => void; -} - -/** @ngInject */ -export function metricsTabDirective() { - 'use strict'; - return { - restrict: 'E', - scope: true, - templateUrl: 'public/app/features/panel/partials/metrics_tab.html', - }; -} - -coreModule.directive('metricsTab', metricsTabDirective); diff --git a/public/app/features/panel/partials/metrics_tab.html b/public/app/features/panel/partials/metrics_tab.html deleted file mode 100644 index 5e9f23ba2ef..00000000000 --- a/public/app/features/panel/partials/metrics_tab.html +++ /dev/null @@ -1,24 +0,0 @@ -
- - - - -
- - - - - - - - - - - - - - - - - - diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index 7092608085d..0b305e05f5b 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -105,23 +105,17 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ switch (attrs.type) { // QueryCtrl case 'query-ctrl': { - const datasource = scope.target.datasource || scope.ctrl.panel.datasource; - return datasourceSrv.get(datasource).then(ds => { - scope.datasource = ds; - - return importPluginModule(ds.meta.module).then(dsModule => { - return { - baseUrl: ds.meta.baseUrl, - name: 'query-ctrl-' + ds.meta.id, - bindings: { target: '=', panelCtrl: '=', datasource: '=' }, - attrs: { - target: 'target', - 'panel-ctrl': 'ctrl', - datasource: 'datasource', - }, - Component: dsModule.QueryCtrl, - }; - }); + const ds = scope.ctrl.datasource; + return $q.when({ + baseUrl: ds.meta.baseUrl, + name: 'query-ctrl-' + ds.meta.id, + bindings: { target: '=', panelCtrl: '=', datasource: '=' }, + attrs: { + target: 'ctrl.target', + 'panel-ctrl': 'ctrl', + datasource: 'ctrl.datasource', + }, + Component: ds.pluginExports.QueryCtrl, }); } // Annotations diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index a1403c7a71c..4dacb3f8ccb 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -4,6 +4,7 @@ import { PanelProps, PanelOptionsProps } from '@grafana/ui'; export interface PluginExports { Datasource?: any; QueryCtrl?: any; + QueryEditor?: any; ConfigCtrl?: any; AnnotationsQueryCtrl?: any; VariableQueryEditor?: any; diff --git a/public/app/types/series.ts b/public/app/types/series.ts index 9fe68955da5..6f1795ef544 100644 --- a/public/app/types/series.ts +++ b/public/app/types/series.ts @@ -1,4 +1,4 @@ -import { PluginMeta } from './plugins'; +import { PluginMeta, PluginExports } from './plugins'; import { TimeSeries, TimeRange, RawTimeRange } from '@grafana/ui'; export interface DataQueryResponse { @@ -25,6 +25,10 @@ export interface DataQueryOptions { } export interface DataSourceApi { + name: string; + meta: PluginMeta; + pluginExports: PluginExports; + /** * min interval range */ From 1b6203e430ade30712752d71dcff8a4755fe4fdf Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 15 Jan 2019 11:49:18 +0100 Subject: [PATCH 006/156] removes error2 logger --- pkg/components/imguploader/imguploader.go | 7 +++++-- pkg/log/log.go | 14 ++------------ pkg/services/alerting/notifiers/telegram.go | 2 +- pkg/services/alerting/test_notification.go | 7 +++++-- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/pkg/components/imguploader/imguploader.go b/pkg/components/imguploader/imguploader.go index 93f69cadd46..422a03d3501 100644 --- a/pkg/components/imguploader/imguploader.go +++ b/pkg/components/imguploader/imguploader.go @@ -6,7 +6,6 @@ import ( "regexp" "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/setting" ) @@ -21,6 +20,10 @@ func (NopImageUploader) Upload(ctx context.Context, path string) (string, error) return "", nil } +var ( + logger = log.New("imguploader") +) + func NewImageUploader() (ImageUploader, error) { switch setting.ImageUploadProvider { @@ -94,7 +97,7 @@ func NewImageUploader() (ImageUploader, error) { } if setting.ImageUploadProvider != "" { - log.Error2("The external image storage configuration is invalid", "unsupported provider", setting.ImageUploadProvider) + logger.Error("The external image storage configuration is invalid", "unsupported provider", setting.ImageUploadProvider) } return NopImageUploader{}, nil diff --git a/pkg/log/log.go b/pkg/log/log.go index 8f0522748ef..47da0784f9f 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -10,13 +10,11 @@ import ( "path/filepath" "strings" - "gopkg.in/ini.v1" - "github.com/go-stack/stack" + "github.com/grafana/grafana/pkg/util" "github.com/inconshreveable/log15" isatty "github.com/mattn/go-isatty" - - "github.com/grafana/grafana/pkg/util" + "gopkg.in/ini.v1" ) var Root log15.Logger @@ -88,18 +86,10 @@ func Warn(format string, v ...interface{}) { Root.Warn(message) } -func Warn2(message string, v ...interface{}) { - Root.Warn(message, v...) -} - func Error(skip int, format string, v ...interface{}) { Root.Error(fmt.Sprintf(format, v...)) } -func Error2(message string, v ...interface{}) { - Root.Error(message, v...) -} - func Critical(skip int, format string, v ...interface{}) { Root.Crit(fmt.Sprintf(format, v...)) } diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index 4a4a989d873..ab43f3bce35 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -130,7 +130,7 @@ func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.Eval defer func() { err := imageFile.Close() if err != nil { - log.Error2("Could not close Telegram inline image.", "err", err) + this.log.Error("Could not close Telegram inline image.", "err", err) } }() diff --git a/pkg/services/alerting/test_notification.go b/pkg/services/alerting/test_notification.go index b6e59f694c8..5ffc8dc58fc 100644 --- a/pkg/services/alerting/test_notification.go +++ b/pkg/services/alerting/test_notification.go @@ -18,9 +18,12 @@ type NotificationTestCommand struct { Settings *simplejson.Json } +var ( + logger = log.New("alerting.testnotification") +) + func init() { bus.AddHandler("alerting", handleNotificationTestCommand) - } func handleNotificationTestCommand(cmd *NotificationTestCommand) error { @@ -35,7 +38,7 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error { notifiers, err := InitNotifier(model) if err != nil { - log.Error2("Failed to create notifier", "error", err.Error()) + logger.Error("Failed to create notifier", "error", err.Error()) return err } From 3cd0cb3d3f70ff1722e759751e767eb496bed95d Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 15 Jan 2019 11:52:39 +0100 Subject: [PATCH 007/156] removes debug2 logging --- pkg/log/log.go | 8 -------- pkg/login/ext_user.go | 6 +++++- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/pkg/log/log.go b/pkg/log/log.go index 47da0784f9f..2e3b6303a6e 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -56,10 +56,6 @@ func Debug(format string, v ...interface{}) { Root.Debug(message) } -func Debug2(message string, v ...interface{}) { - Root.Debug(message, v...) -} - func Info(format string, v ...interface{}) { var message string if len(v) > 0 { @@ -71,10 +67,6 @@ func Info(format string, v ...interface{}) { Root.Info(message) } -func Info2(message string, v ...interface{}) { - Root.Info(message, v...) -} - func Warn(format string, v ...interface{}) { var message string if len(v) > 0 { diff --git a/pkg/login/ext_user.go b/pkg/login/ext_user.go index 1262c1cc44f..42fb37ff9d0 100644 --- a/pkg/login/ext_user.go +++ b/pkg/login/ext_user.go @@ -11,6 +11,10 @@ func init() { bus.AddHandler("auth", UpsertUser) } +var ( + logger = log.New("login.ext_user") +) + func UpsertUser(cmd *m.UpsertUserCommand) error { extUser := cmd.ExternalUser @@ -135,7 +139,7 @@ func updateUser(user *m.User, extUser *m.ExternalUserInfo) error { return nil } - log.Debug2("Syncing user info", "id", user.Id, "update", updateCmd) + logger.Debug("Syncing user info", "id", user.Id, "update", updateCmd) return bus.Dispatch(updateCmd) } From d59f1fe629e83ddabe9ec8b296521eb61cee7934 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 15 Jan 2019 14:14:38 +0100 Subject: [PATCH 008/156] fix: It should be possible to scroll in the unit picker before selecting a value #14871 --- .../app/core/directives/dropdown_typeahead.ts | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/public/app/core/directives/dropdown_typeahead.ts b/public/app/core/directives/dropdown_typeahead.ts index a4bed4fe2b7..dfc3eddbcbb 100644 --- a/public/app/core/directives/dropdown_typeahead.ts +++ b/public/app/core/directives/dropdown_typeahead.ts @@ -141,6 +141,9 @@ export function dropdownTypeahead2($compile) { link: ($scope, elem, attrs) => { const $input = $(inputTemplate); const $button = $(buttonTemplate); + const timeoutId = { + blur: null + }; $input.appendTo(elem); $button.appendTo(elem); @@ -177,6 +180,14 @@ export function dropdownTypeahead2($compile) { [] ); + const closeDropdownMenu = () => { + $input.hide(); + $input.val(''); + $button.show(); + $button.focus(); + elem.removeClass('open'); + }; + $scope.menuItemSelected = (index, subIndex) => { const menuItem = $scope.menuItems[index]; const payload: any = { $item: menuItem }; @@ -184,6 +195,7 @@ export function dropdownTypeahead2($compile) { payload.$subItem = menuItem.submenu[subIndex]; } $scope.dropdownTypeaheadOnSelect(payload); + closeDropdownMenu(); }; $input.attr('data-provide', 'typeahead'); @@ -223,16 +235,15 @@ export function dropdownTypeahead2($compile) { elem.toggleClass('open', $input.val() === ''); }); + elem.mousedown((evt: Event) => { + evt.preventDefault(); + timeoutId.blur = null; + }); + $input.blur(() => { - $input.hide(); - $input.val(''); - $button.show(); - $button.focus(); - // clicking the function dropdown menu won't - // work if you remove class at once - setTimeout(() => { - elem.removeClass('open'); - }, 200); + timeoutId.blur = setTimeout(() => { + closeDropdownMenu(); + }, 1); }); $compile(elem.contents())($scope); From 89a0d1090d7369568fd1d9f69d2e8aca05344348 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 15 Jan 2019 15:43:17 +0100 Subject: [PATCH 009/156] Move ColorPicker leftovers to @grafana/ui --- .../grafana-ui/src/components/ColorPicker/SpectrumPicker.tsx | 2 +- .../grafana-ui/src/components/ColorPicker/_ColorPicker.scss | 0 packages/grafana-ui/src/components/index.scss | 2 ++ packages/grafana-ui/src/index.scss | 2 ++ .../vendor/css => packages/grafana-ui/src/vendor}/spectrum.css | 0 {public => packages/grafana-ui/src}/vendor/spectrum.js | 0 public/sass/_grafana.scss | 2 -- 7 files changed, 5 insertions(+), 3 deletions(-) rename public/sass/components/_color_picker.scss => packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss (100%) rename {public/vendor/css => packages/grafana-ui/src/vendor}/spectrum.css (100%) rename {public => packages/grafana-ui/src}/vendor/spectrum.js (100%) diff --git a/packages/grafana-ui/src/components/ColorPicker/SpectrumPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/SpectrumPicker.tsx index 6974eed142e..a225db09046 100644 --- a/packages/grafana-ui/src/components/ColorPicker/SpectrumPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SpectrumPicker.tsx @@ -1,7 +1,7 @@ import React from 'react'; import _ from 'lodash'; import $ from 'jquery'; -import 'vendor/spectrum'; +import '../../vendor/spectrum'; export interface Props { color: string; diff --git a/public/sass/components/_color_picker.scss b/packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss similarity index 100% rename from public/sass/components/_color_picker.scss rename to packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss index 5a9263844a4..31c8857a130 100644 --- a/packages/grafana-ui/src/components/index.scss +++ b/packages/grafana-ui/src/components/index.scss @@ -5,3 +5,5 @@ @import 'Select/Select'; @import 'PanelOptionsGroup/PanelOptionsGroup'; @import 'PanelOptionsGrid/PanelOptionsGrid'; +@import 'PanelOptionsGrid/PanelOptionsGrid'; +@import 'ColorPicker/ColorPicker'; diff --git a/packages/grafana-ui/src/index.scss b/packages/grafana-ui/src/index.scss index 841415620d6..74fdcde3072 100644 --- a/packages/grafana-ui/src/index.scss +++ b/packages/grafana-ui/src/index.scss @@ -1 +1,3 @@ +@import 'vendor/spectrum'; @import 'components/index'; + diff --git a/public/vendor/css/spectrum.css b/packages/grafana-ui/src/vendor/spectrum.css similarity index 100% rename from public/vendor/css/spectrum.css rename to packages/grafana-ui/src/vendor/spectrum.css diff --git a/public/vendor/spectrum.js b/packages/grafana-ui/src/vendor/spectrum.js similarity index 100% rename from public/vendor/spectrum.js rename to packages/grafana-ui/src/vendor/spectrum.js diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index b8498f18b19..93fd26b7ca8 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -3,7 +3,6 @@ // VENDOR @import '../vendor/css/timepicker.css'; -@import '../vendor/css/spectrum.css'; @import '../vendor/css/rc-cascader.scss'; // MIXINS @@ -76,7 +75,6 @@ @import 'components/typeahead'; @import 'components/modals'; @import 'components/dropdown'; -@import 'components/color_picker'; @import 'components/footer'; @import 'components/infobox'; @import 'components/shortcuts'; From 3bdd4a5c33eb1c62db4d105f3cebe8a84eb6f11d Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 15 Jan 2019 17:14:58 +0100 Subject: [PATCH 010/156] changelog: adds note for #14795 --- CHANGELOG.md | 1 + .../plugins/datasource/prometheus/specs/datasource.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd0339d1991..9225d6545e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ ### Bug fixes * **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486) +* **Prometheus**: Query for annotation always uses 60s step regardless of dashboard range, fixes [#14795](https://github.com/grafana/grafana/issues/14795) # 5.4.3 (2019-01-14) diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index 46b37d37b8e..77ccbf2eadf 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -644,8 +644,8 @@ describe('PrometheusDatasource', () => { ctx.ds.annotationQuery(query); const req = backendSrv.datasourceRequest.mock.calls[0][0]; // Range in seconds: (to - from) / 1000 - // Max_datapints: 11000 - // Step: range / max_datapints + // Max_datapoints: 11000 + // Step: range / max_datapoints const step = 236; expect(req.url).toContain(`step=${step}`); }); From 1faa5819a8f76ddb78a5b323e6fd3cc5e4e9964c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 15 Jan 2019 17:15:46 +0100 Subject: [PATCH 011/156] Initial commit --- packages/grafana-ui/src/types/panel.ts | 3 +++ .../app/features/dashboard/dashgrid/PanelChrome.tsx | 8 +++++++- .../dashboard/dashgrid/PanelHeader/PanelHeader.tsx | 7 +++++-- public/app/features/templating/variable_srv.ts | 2 +- public/app/plugins/panel/gauge/GaugePanel.tsx | 13 ++++--------- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 0b995f932f0..17ef712b0dd 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -1,6 +1,8 @@ import { TimeSeries, LoadingState } from './series'; import { TimeRange } from './time'; +export type InterpolateFunction = (value: string, format?: string | Function) => string; + export interface PanelProps { timeSeries: TimeSeries[]; timeRange: TimeRange; @@ -9,6 +11,7 @@ export interface PanelProps { renderCounter: number; width: number; height: number; + onInterpolate: InterpolateFunction; } export interface PanelOptionsProps { diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 46534cac065..6b4ef48c32e 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -20,6 +20,7 @@ import { PanelPlugin } from 'app/types'; import { TimeRange } from '@grafana/ui'; import variables from 'sass/_variables.scss'; +import templateSrv from 'app/features/templating/template_srv'; export interface Props { panel: PanelModel; @@ -78,6 +79,10 @@ export class PanelChrome extends PureComponent { }); }; + onInterpolate = (value: string, format?: string) => { + return templateSrv.replace(value, this.props.panel.scopedVars, format); + }; + get isVisible() { return !this.props.dashboard.otherPanelInFullscreen(this.props.panel); } @@ -124,9 +129,10 @@ export class PanelChrome extends PureComponent { timeSeries={timeSeries} timeRange={timeRange} options={panel.getOptions(plugin.exports.PanelDefaults)} - width={width - 2 * variables.panelHorizontalPadding } + width={width - 2 * variables.panelHorizontalPadding} height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding} renderCounter={renderCounter} + onInterpolate={this.onInterpolate} />
); diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index 8b7afd7d09e..b5cd9258c08 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames'; import PanelHeaderCorner from './PanelHeaderCorner'; import { PanelHeaderMenu } from './PanelHeaderMenu'; +import templateSrv from 'app/features/templating/template_srv'; import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { PanelModel } from 'app/features/dashboard/panel_model'; @@ -45,7 +46,9 @@ export class PanelHeader extends Component { const isFullscreen = false; const isLoading = false; const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); - const { panel, dashboard, timeInfo } = this.props; + const { panel, dashboard, timeInfo, scopedVars } = this.props; + const title = templateSrv.replaceWithText(panel.title, scopedVars); + return ( <> {
- {panel.title} + {title} {this.state.panelMenuOpen && ( diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index bc0362f0678..896987de706 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -132,7 +132,7 @@ export class VariableSrv { return this.$q.all(promises).then(() => { if (emitChangeEvents) { - this.$rootScope.$emit('template-variable-value-updated'); + this.$rootScope.appEvent('template-variable-value-updated'); this.dashboard.startRefresh(); } }); diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 38a88428be7..008889782eb 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -8,20 +8,15 @@ interface Props extends PanelProps {} export class GaugePanel extends PureComponent { render() { - const { timeSeries, width, height } = this.props; + const { timeSeries, width, height, onInterpolate, options } = this.props; + + const prefix = onInterpolate(options.prefix); const vmSeries = getTimeSeriesVMs({ timeSeries: timeSeries, nullValueMode: NullValueMode.Ignore, }); - return ( - - ); + return ; } } From 8cfa2c00d1f8ba4695a2db85d60d02d413206558 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 15 Jan 2019 17:35:24 +0100 Subject: [PATCH 012/156] Remove duplicated import --- packages/grafana-ui/src/components/index.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss index 31c8857a130..b894cf73c1a 100644 --- a/packages/grafana-ui/src/components/index.scss +++ b/packages/grafana-ui/src/components/index.scss @@ -5,5 +5,4 @@ @import 'Select/Select'; @import 'PanelOptionsGroup/PanelOptionsGroup'; @import 'PanelOptionsGrid/PanelOptionsGrid'; -@import 'PanelOptionsGrid/PanelOptionsGrid'; @import 'ColorPicker/ColorPicker'; From 7105d16131d648a59eef410e3e2961eb86346cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 15 Jan 2019 18:02:05 +0100 Subject: [PATCH 013/156] Scrollbar select fix --- .../src/components/CustomScrollbar/CustomScrollbar.tsx | 7 ++++--- packages/grafana-ui/src/components/Select/Select.tsx | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx index cf1657e1c83..519e755b474 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx @@ -6,6 +6,7 @@ interface Props { autoHide?: boolean; autoHideTimeout?: number; autoHideDuration?: number; + autoMaxHeight?: string; hideTracksWhenNotNeeded?: boolean; } @@ -18,11 +19,12 @@ export class CustomScrollbar extends PureComponent { autoHide: true, autoHideTimeout: 200, autoHideDuration: 200, + autoMaxHeight: '100%', hideTracksWhenNotNeeded: false, }; render() { - const { customClassName, children, ...scrollProps } = this.props; + const { customClassName, children, autoMaxHeight } = this.props; return ( { // These autoHeightMin & autoHeightMax options affect firefox and chrome differently. // Before these where set to inhert but that caused problems with cut of legends in firefox autoHeightMin={'0'} - autoHeightMax={'100%'} + autoHeightMax={autoMaxHeight} renderTrackHorizontal={props =>
} renderTrackVertical={props =>
} renderThumbHorizontal={props =>
} renderThumbVertical={props =>
} renderView={props =>
} - {...scrollProps} > {children} diff --git a/packages/grafana-ui/src/components/Select/Select.tsx b/packages/grafana-ui/src/components/Select/Select.tsx index b3b0c8efbbb..348133c6c28 100644 --- a/packages/grafana-ui/src/components/Select/Select.tsx +++ b/packages/grafana-ui/src/components/Select/Select.tsx @@ -61,7 +61,7 @@ interface AsyncProps { export const MenuList = (props: any) => { return ( - {props.children} + {props.children} ); }; From 6bb2d5ff243bd93b5ebf5a5a447368fc7850a5d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 15 Jan 2019 18:05:55 +0100 Subject: [PATCH 014/156] Added suffix interpolation --- public/app/plugins/panel/gauge/GaugePanel.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 008889782eb..fd3d812f21e 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -11,12 +11,22 @@ export class GaugePanel extends PureComponent { const { timeSeries, width, height, onInterpolate, options } = this.props; const prefix = onInterpolate(options.prefix); + const suffix = onInterpolate(options.suffix); const vmSeries = getTimeSeriesVMs({ timeSeries: timeSeries, nullValueMode: NullValueMode.Ignore, }); - return ; + return ( + + ); } } From 9bf3d49393f9b5d66544bc25d8f280e4acbdf0d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 15 Jan 2019 18:26:21 +0100 Subject: [PATCH 015/156] updated snapshot --- .../__snapshots__/CustomScrollbar.test.tsx.snap | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap b/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap index 0a7de5fcffe..60b4a2e0aa5 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap +++ b/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap @@ -42,9 +42,7 @@ exports[`CustomScrollbar renders correctly 1`] = ` Object { "display": "none", "height": 6, - "opacity": 0, "position": "absolute", - "transition": "opacity 200ms", } } > @@ -64,9 +62,7 @@ exports[`CustomScrollbar renders correctly 1`] = ` style={ Object { "display": "none", - "opacity": 0, "position": "absolute", - "transition": "opacity 200ms", "width": 6, } } From 58094faa12f8440083ea8a4c0aee0761470ee45b Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 15 Jan 2019 17:31:42 +0000 Subject: [PATCH 016/156] test and minor fix on mapping row --- .../components/FormGroup/FormGroup.test.tsx | 26 +++++++++++++++++++ .../__snapshots__/FormGroup.test.tsx.snap | 19 ++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 packages/grafana-ui/src/components/FormGroup/FormGroup.test.tsx create mode 100644 packages/grafana-ui/src/components/FormGroup/__snapshots__/FormGroup.test.tsx.snap diff --git a/packages/grafana-ui/src/components/FormGroup/FormGroup.test.tsx b/packages/grafana-ui/src/components/FormGroup/FormGroup.test.tsx new file mode 100644 index 00000000000..4f8b4be9540 --- /dev/null +++ b/packages/grafana-ui/src/components/FormGroup/FormGroup.test.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { FormGroup, Props } from './FormGroup'; + +const setup = (propOverrides?: object) => { + const props: Props = { + label: 'Test', + labelWidth: 11, + inputProps: { + value: 10, + onChange: jest.fn(), + }, + }; + + Object.assign(props, propOverrides); + + return shallow(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/packages/grafana-ui/src/components/FormGroup/__snapshots__/FormGroup.test.tsx.snap b/packages/grafana-ui/src/components/FormGroup/__snapshots__/FormGroup.test.tsx.snap new file mode 100644 index 00000000000..e88ff774981 --- /dev/null +++ b/packages/grafana-ui/src/components/FormGroup/__snapshots__/FormGroup.test.tsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
+ + Test + + +
+`; From 83fbf52aac51fa6cc0c16eed9b48529b26fb489c Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 15 Jan 2019 17:33:42 +0000 Subject: [PATCH 017/156] fixing imports, minor fix on mapping row --- .../src/components/FormGroup/FormGroup.tsx | 2 +- .../grafana-ui/src/components/Label/Label.tsx | 2 +- .../SharedPreferences/SharedPreferences.tsx | 3 +-- .../datasources/settings/BasicSettings.tsx | 2 +- public/app/features/teams/TeamSettings.tsx | 2 +- public/app/plugins/panel/gauge/MappingRow.tsx | 19 +++++++++---------- 6 files changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx b/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx index ac761fa5d2c..a0088032079 100644 --- a/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx +++ b/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx @@ -1,7 +1,7 @@ import React, { SFC } from 'react'; import { Label } from '..'; -interface Props { +export interface Props { label: string; inputProps: {}; labelWidth?: number; diff --git a/packages/grafana-ui/src/components/Label/Label.tsx b/packages/grafana-ui/src/components/Label/Label.tsx index 5d60efa056a..b31ed45e32a 100644 --- a/packages/grafana-ui/src/components/Label/Label.tsx +++ b/packages/grafana-ui/src/components/Label/Label.tsx @@ -1,5 +1,5 @@ import React, { SFC, ReactNode } from 'react'; -import { Tooltip } from '@grafana/ui'; +import { Tooltip } from '..'; interface Props { tooltip?: string; diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index ca933332db9..0b11d32b668 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -1,7 +1,6 @@ import React, { PureComponent } from 'react'; -import { Label } from '../../../../../packages/grafana-ui/src/components/Label/Label'; -import { Select } from '@grafana/ui'; +import { Label, Select } from '@grafana/ui'; import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv'; import { DashboardSearchHit } from 'app/types'; diff --git a/public/app/features/datasources/settings/BasicSettings.tsx b/public/app/features/datasources/settings/BasicSettings.tsx index 55dc9b54211..21a548a5045 100644 --- a/public/app/features/datasources/settings/BasicSettings.tsx +++ b/public/app/features/datasources/settings/BasicSettings.tsx @@ -1,5 +1,5 @@ import React, { SFC } from 'react'; -import { Label } from '../../../../../packages/grafana-ui/src/components/Label/Label'; +import { Label } from '@grafana/ui'; import { Switch } from '../../../core/components/Switch/Switch'; export interface Props { diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 3424f39d22c..22815dbb7ec 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Label } from '../../../../packages/grafana-ui/src/components/Label/Label'; +import { Label } from '@grafana/ui'; import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences'; import { updateTeam } from './state/actions'; import { getRouteParamsId } from 'app/core/selectors/location'; diff --git a/public/app/plugins/panel/gauge/MappingRow.tsx b/public/app/plugins/panel/gauge/MappingRow.tsx index b05da5514aa..91dff549677 100644 --- a/public/app/plugins/panel/gauge/MappingRow.tsx +++ b/public/app/plugins/panel/gauge/MappingRow.tsx @@ -81,16 +81,15 @@ export default class MappingRow extends PureComponent { }} inputWidth={8} /> - this.updateMapping, - onChange: event => this.onMappingTextChange(event), - value: text, - }} - inputWidth={10} - /> +
+ + +
); } From 5c9c0244751e1477427a092b506d3786426d91ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 15 Jan 2019 18:41:04 +0100 Subject: [PATCH 018/156] Fixed a bug with prefix and suffix not showing when using value mappings --- public/app/viz/Gauge.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/viz/Gauge.tsx b/public/app/viz/Gauge.tsx index 5112ff9aa1b..d5e4eb94884 100644 --- a/public/app/viz/Gauge.tsx +++ b/public/app/viz/Gauge.tsx @@ -80,9 +80,9 @@ export class Gauge extends PureComponent { const { rangeMap, valueMap } = this.formatWithMappings(mappings, formattedValue); if (valueMap) { - return valueMap; + return `${prefix} ${valueMap} ${suffix}`; } else if (rangeMap) { - return rangeMap; + return `${prefix} ${rangeMap} ${suffix}`; } } From 2be2deddb86d73fccb858d2ef379f60cac000b0c Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 10 Jan 2019 14:24:31 +0100 Subject: [PATCH 019/156] WIP Explore redux migration --- public/app/core/utils/explore.ts | 89 +- public/app/features/explore/Explore.tsx | 928 ++++-------------- public/app/features/explore/state/actions.ts | 694 +++++++++++++ public/app/features/explore/state/reducers.ts | 412 ++++++++ public/app/store/configureStore.ts | 2 + public/app/types/explore.ts | 23 +- public/app/types/index.ts | 2 + 7 files changed, 1413 insertions(+), 737 deletions(-) create mode 100644 public/app/features/explore/state/actions.ts create mode 100644 public/app/features/explore/state/reducers.ts diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index f3273ffa16d..871a020ccc2 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -1,6 +1,7 @@ import _ from 'lodash'; -import { colors } from '@grafana/ui'; +import { colors, RawTimeRange, IntervalValues } from '@grafana/ui'; +import * as dateMath from 'app/core/utils/datemath'; import { renderUrl } from 'app/core/utils/url'; import kbn from 'app/core/utils/kbn'; import store from 'app/core/store'; @@ -8,9 +9,15 @@ import { parse as parseDate } from 'app/core/utils/datemath'; import TimeSeries from 'app/core/time_series2'; import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; -import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore'; +import { + ExploreUrlState, + HistoryItem, + QueryTransaction, + ResultType, + QueryIntervals, + QueryOptions, +} from 'app/types/explore'; import { DataQuery, DataSourceApi } from 'app/types/series'; -import { RawTimeRange, IntervalValues } from '@grafana/ui'; export const DEFAULT_RANGE = { from: 'now-6h', @@ -19,6 +26,8 @@ export const DEFAULT_RANGE = { const MAX_HISTORY_ITEMS = 100; +export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource'; + /** * Returns an Explore-URL that contains a panel's queries and the dashboard time range. * @@ -77,6 +86,62 @@ export async function getExploreUrl( return url; } +export function buildQueryTransaction( + query: DataQuery, + rowIndex: number, + resultType: ResultType, + queryOptions: QueryOptions, + range: RawTimeRange, + queryIntervals: QueryIntervals, + scanning: boolean +): QueryTransaction { + const { interval, intervalMs } = queryIntervals; + + const configuredQueries = [ + { + ...query, + ...queryOptions, + }, + ]; + + // Clone range for query request + // const queryRange: RawTimeRange = { ...range }; + // const { from, to, raw } = this.timeSrv.timeRange(); + // Most datasource is using `panelId + query.refId` for cancellation logic. + // Using `format` here because it relates to the view panel that the request is for. + // However, some datasources don't use `panelId + query.refId`, but only `panelId`. + // Therefore panel id has to be unique. + const panelId = `${queryOptions.format}-${query.key}`; + + const options = { + interval, + intervalMs, + panelId, + targets: configuredQueries, // Datasources rely on DataQueries being passed under the targets key. + range: { + from: dateMath.parse(range.from, false), + to: dateMath.parse(range.to, true), + raw: range, + }, + rangeRaw: range, + scopedVars: { + __interval: { text: interval, value: interval }, + __interval_ms: { text: intervalMs, value: intervalMs }, + }, + }; + + return { + options, + query, + resultType, + rowIndex, + scanning, + id: generateKey(), // reusing for unique ID + done: false, + latency: 0, + }; +} + const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest; export function parseUrlState(initial: string | undefined): ExploreUrlState { @@ -103,12 +168,12 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { return { datasource: null, queries: [], range: DEFAULT_RANGE }; } -export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string { - const urlState: ExploreUrlState = { - datasource: state.initialDatasource, - queries: state.initialQueries.map(clearQueryKeys), - range: state.range, - }; +export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string { + // const urlState: ExploreUrlState = { + // datasource: state.initialDatasource, + // queries: state.initialQueries.map(clearQueryKeys), + // range: state.range, + // }; if (compact) { return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]); } @@ -123,7 +188,7 @@ export function generateRefId(index = 0): string { return `${index + 1}`; } -export function generateQueryKeys(index = 0): { refId: string; key: string } { +export function generateEmptyQuery(index = 0): { refId: string; key: string } { return { refId: generateRefId(index), key: generateKey(index) }; } @@ -132,9 +197,9 @@ export function generateQueryKeys(index = 0): { refId: string; key: string } { */ export function ensureQueries(queries?: DataQuery[]): DataQuery[] { if (queries && typeof queries === 'object' && queries.length > 0) { - return queries.map((query, i) => ({ ...query, ...generateQueryKeys(i) })); + return queries.map((query, i) => ({ ...query, ...generateEmptyQuery(i) })); } - return [{ ...generateQueryKeys() }]; + return [{ ...generateEmptyQuery() }]; } /** diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index d4d645950c1..64e9c66ece5 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -1,35 +1,38 @@ import React from 'react'; import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; import _ from 'lodash'; +import { withSize } from 'react-sizeme'; +import { RawTimeRange, TimeRange } from '@grafana/ui'; -import { DataSource } from 'app/types/datasources'; -import { - ExploreState, - ExploreUrlState, - QueryTransaction, - ResultType, - QueryHintGetter, - QueryHint, -} from 'app/types/explore'; -import { TimeRange } from '@grafana/ui'; +import { DataSourceSelectItem } from 'app/types/datasources'; +import { ExploreUrlState, HistoryItem, QueryTransaction, RangeScanner } from 'app/types/explore'; import { DataQuery } from 'app/types/series'; import store from 'app/core/store'; -import { - DEFAULT_RANGE, - calculateResultsFromQueryTransactions, - ensureQueries, - getIntervals, - generateKey, - generateQueryKeys, - hasNonEmptyQuery, - makeTimeSeriesList, - updateHistory, -} from 'app/core/utils/explore'; +import { LAST_USED_DATASOURCE_KEY, ensureQueries } from 'app/core/utils/explore'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; -import TableModel from 'app/core/table_model'; -import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { Emitter } from 'app/core/utils/emitter'; -import * as dateMath from 'app/core/utils/datemath'; + +import { + addQueryRow, + changeDatasource, + changeQuery, + changeSize, + changeTime, + clickClear, + clickExample, + clickGraphButton, + clickLogsButton, + clickTableButton, + highlightLogsExpression, + initializeExplore, + modifyQueries, + removeQueryRow, + runQueries, + scanStart, + scanStop, +} from './state/actions'; +import { ExploreState } from './state/reducers'; import Panel from './Panel'; import QueryRows from './QueryRows'; @@ -39,17 +42,57 @@ import Table from './Table'; import ErrorBoundary from './ErrorBoundary'; import { Alert } from './Error'; import TimePicker, { parseTime } from './TimePicker'; - -const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource'; +import { LogsModel } from 'app/core/logs_model'; +import TableModel from 'app/core/table_model'; interface ExploreProps { - datasourceSrv: DatasourceSrv; + StartPage?: any; + addQueryRow: typeof addQueryRow; + changeDatasource: typeof changeDatasource; + changeQuery: typeof changeQuery; + changeTime: typeof changeTime; + clickClear: typeof clickClear; + clickExample: typeof clickExample; + clickGraphButton: typeof clickGraphButton; + clickLogsButton: typeof clickLogsButton; + clickTableButton: typeof clickTableButton; + datasourceError: string; + datasourceInstance: any; + datasourceLoading: boolean | null; + datasourceMissing: boolean; + exploreDatasources: DataSourceSelectItem[]; + graphResult?: any[]; + highlightLogsExpression: typeof highlightLogsExpression; + history: HistoryItem[]; + initialDatasource?: string; + initialQueries: DataQuery[]; + initializeExplore: typeof initializeExplore; + logsHighlighterExpressions?: string[]; + logsResult?: LogsModel; + modifyQueries: typeof modifyQueries; onChangeSplit: (split: boolean, state?: ExploreState) => void; onSaveState: (key: string, state: ExploreState) => void; position: string; + queryTransactions: QueryTransaction[]; + removeQueryRow: typeof removeQueryRow; + range: RawTimeRange; + runQueries: typeof runQueries; + scanner?: RangeScanner; + scanning?: boolean; + scanRange?: RawTimeRange; + scanStart: typeof scanStart; + scanStop: typeof scanStop; split: boolean; splitState?: ExploreState; stateKey: string; + showingGraph: boolean; + showingLogs: boolean; + showingStartPage?: boolean; + showingTable: boolean; + supportsGraph: boolean | null; + supportsLogs: boolean | null; + supportsTable: boolean | null; + tableResult?: TableModel; urlState: ExploreUrlState; } @@ -89,23 +132,9 @@ interface ExploreProps { * The result viewers determine some of the query options sent to the datasource, e.g., * `format`, to indicate eventual transformations by the datasources' result transformers. */ -export class Explore extends React.PureComponent { +export class Explore extends React.PureComponent { el: any; exploreEvents: Emitter; - /** - * Set via URL or local storage - */ - initialDatasource: string; - /** - * Current query expressions of the rows including their modifications, used for running queries. - * Not kept in component state to prevent edit-render roundtrips. - */ - modifiedQueries: DataQuery[]; - /** - * Local ID cache to compare requested vs selected datasource - */ - requestedDatasourceId: string; - scanTimer: NodeJS.Timer; /** * Timepicker to control scanning */ @@ -113,166 +142,22 @@ export class Explore extends React.PureComponent { constructor(props) { super(props); - const splitState: ExploreState = props.splitState; - let initialQueries: DataQuery[]; - if (splitState) { - // Split state overrides everything - this.state = splitState; - initialQueries = splitState.initialQueries; - } else { - const { datasource, queries, range } = props.urlState as ExploreUrlState; - const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY); - initialQueries = ensureQueries(queries); - const initialRange = { from: parseTime(range.from), to: parseTime(range.to) } || { ...DEFAULT_RANGE }; - // Millies step for helper bar charts - const initialGraphInterval = 15 * 1000; - this.state = { - datasource: null, - datasourceError: null, - datasourceLoading: null, - datasourceMissing: false, - exploreDatasources: [], - graphInterval: initialGraphInterval, - graphResult: [], - initialDatasource, - initialQueries, - history: [], - logsResult: null, - queryTransactions: [], - range: initialRange, - scanning: false, - showingGraph: true, - showingLogs: true, - showingStartPage: false, - showingTable: true, - supportsGraph: null, - supportsLogs: null, - supportsTable: null, - tableResult: new TableModel(), - }; - } - this.modifiedQueries = initialQueries.slice(); this.exploreEvents = new Emitter(); this.timepickerRef = React.createRef(); } async componentDidMount() { - const { datasourceSrv } = this.props; - const { initialDatasource } = this.state; - if (!datasourceSrv) { - throw new Error('No datasource service passed as props.'); - } - - const datasources = datasourceSrv.getExternal(); - const exploreDatasources = datasources.map(ds => ({ - value: ds.name, - name: ds.name, - meta: ds.meta, - })); - - if (datasources.length > 0) { - this.setState({ datasourceLoading: true, exploreDatasources }); - // Priority for datasource preselection: URL, localstorage, default datasource - let datasource; - if (initialDatasource) { - datasource = await datasourceSrv.get(initialDatasource); - } else { - datasource = await datasourceSrv.get(); - } - await this.setDatasource(datasource); - } else { - this.setState({ datasourceMissing: true }); - } + // Load URL state and parse range + const { datasource, queries, range } = this.props.urlState as ExploreUrlState; + const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY); + const initialQueries: DataQuery[] = ensureQueries(queries); + const initialRange = { from: parseTime(range.from), to: parseTime(range.to) }; + const width = this.el ? this.el.offsetWidth : 0; + this.props.initializeExplore(initialDatasource, initialQueries, initialRange, width, this.exploreEvents); } componentWillUnmount() { this.exploreEvents.removeAllListeners(); - clearTimeout(this.scanTimer); - } - - async setDatasource(datasource: any, origin?: DataSource) { - const { initialQueries, range } = this.state; - - const supportsGraph = datasource.meta.metrics; - const supportsLogs = datasource.meta.logs; - const supportsTable = datasource.meta.tables; - const datasourceId = datasource.meta.id; - let datasourceError = null; - - // Keep ID to track selection - this.requestedDatasourceId = datasourceId; - - try { - const testResult = await datasource.testDatasource(); - datasourceError = testResult.status === 'success' ? null : testResult.message; - } catch (error) { - datasourceError = (error && error.statusText) || 'Network error'; - } - - if (datasourceId !== this.requestedDatasourceId) { - // User already changed datasource again, discard results - return; - } - - const historyKey = `grafana.explore.history.${datasourceId}`; - const history = store.getObject(historyKey, []); - - if (datasource.init) { - datasource.init(); - } - - // Check if queries can be imported from previously selected datasource - let modifiedQueries = this.modifiedQueries; - if (origin) { - if (origin.meta.id === datasource.meta.id) { - // Keep same queries if same type of datasource - modifiedQueries = [...this.modifiedQueries]; - } else if (datasource.importQueries) { - // Datasource-specific importers - modifiedQueries = await datasource.importQueries(this.modifiedQueries, origin.meta); - } else { - // Default is blank queries - modifiedQueries = ensureQueries(); - } - } - - // Reset edit state with new queries - const nextQueries = initialQueries.map((q, i) => ({ - ...modifiedQueries[i], - ...generateQueryKeys(i), - })); - this.modifiedQueries = modifiedQueries; - - // Custom components - const StartPage = datasource.pluginExports.ExploreStartPage; - - // Calculate graph bucketing interval - const graphInterval = getIntervals(range, datasource, this.el ? this.el.offsetWidth : 0).intervalMs; - - this.setState( - { - StartPage, - datasource, - datasourceError, - graphInterval, - history, - supportsGraph, - supportsLogs, - supportsTable, - datasourceLoading: false, - initialDatasource: datasource.name, - initialQueries: nextQueries, - logsHighlighterExpressions: undefined, - showingStartPage: Boolean(StartPage), - }, - () => { - if (datasourceError === null) { - // Save last-used datasource - store.set(LAST_USED_DATASOURCE_KEY, datasource.name); - this.onSubmit(); - } - } - ); } getRef = el => { @@ -280,106 +165,32 @@ export class Explore extends React.PureComponent { }; onAddQueryRow = index => { - // Local cache - this.modifiedQueries[index + 1] = { ...generateQueryKeys(index + 1) }; - - this.setState(state => { - const { initialQueries, queryTransactions } = state; - - const nextQueries = [ - ...initialQueries.slice(0, index + 1), - { ...this.modifiedQueries[index + 1] }, - ...initialQueries.slice(index + 1), - ]; - - // Ongoing transactions need to update their row indices - const nextQueryTransactions = queryTransactions.map(qt => { - if (qt.rowIndex > index) { - return { - ...qt, - rowIndex: qt.rowIndex + 1, - }; - } - return qt; - }); - - return { - initialQueries: nextQueries, - logsHighlighterExpressions: undefined, - queryTransactions: nextQueryTransactions, - }; - }); + this.props.addQueryRow(index); }; onChangeDatasource = async option => { - const origin = this.state.datasource; - this.setState({ - datasource: null, - datasourceError: null, - datasourceLoading: true, - queryTransactions: [], - }); - const datasourceName = option.value; - const datasource = await this.props.datasourceSrv.get(datasourceName); - this.setDatasource(datasource as any, origin); + this.props.changeDatasource(option.value); }; - onChangeQuery = (value: DataQuery, index: number, override?: boolean) => { - // Null value means reset - if (value === null) { - value = { ...generateQueryKeys(index) }; - } + onChangeQuery = (query: DataQuery, index: number, override?: boolean) => { + const { changeQuery, datasourceInstance } = this.props; - // Keep current value in local cache - this.modifiedQueries[index] = value; - - if (override) { - this.setState(state => { - // Replace query row by injecting new key - const { initialQueries, queryTransactions } = state; - const query: DataQuery = { - ...value, - ...generateQueryKeys(index), - }; - const nextQueries = [...initialQueries]; - nextQueries[index] = query; - this.modifiedQueries = [...nextQueries]; - - // Discard ongoing transaction related to row query - const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); - - return { - initialQueries: nextQueries, - queryTransactions: nextQueryTransactions, - }; - }, this.onSubmit); - } else if (this.state.datasource.getHighlighterExpression && this.modifiedQueries.length === 1) { - // Live preview of log search matches. Can only work on single row query for now - this.updateLogsHighlights(value); + changeQuery(query, index, override); + if (query && !override && datasourceInstance.getHighlighterExpression && index === 0) { + // Live preview of log search matches. Only use on first row for now + this.updateLogsHighlights(query); } }; - onChangeTime = (nextRange: TimeRange, scanning?: boolean) => { - const range: TimeRange = { - ...nextRange, - }; - if (this.state.scanning && !scanning) { + onChangeTime = (range: TimeRange, changedByScanner?: boolean) => { + if (this.props.scanning && !changedByScanner) { this.onStopScanning(); } - this.setState({ range, scanning }, () => this.onSubmit()); + this.props.changeTime(range); }; onClickClear = () => { - this.onStopScanning(); - this.modifiedQueries = ensureQueries(); - this.setState( - prevState => ({ - initialQueries: [...this.modifiedQueries], - queryTransactions: [], - showingStartPage: Boolean(prevState.StartPage), - }), - this.saveState - ); + this.props.clickClear(); }; onClickCloseSplit = () => { @@ -390,82 +201,28 @@ export class Explore extends React.PureComponent { }; onClickGraphButton = () => { - this.setState( - state => { - const showingGraph = !state.showingGraph; - let nextQueryTransactions = state.queryTransactions; - if (!showingGraph) { - // Discard transactions related to Graph query - nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph'); - } - return { queryTransactions: nextQueryTransactions, showingGraph }; - }, - () => { - if (this.state.showingGraph) { - this.onSubmit(); - } - } - ); + this.props.clickGraphButton(); }; onClickLogsButton = () => { - this.setState( - state => { - const showingLogs = !state.showingLogs; - let nextQueryTransactions = state.queryTransactions; - if (!showingLogs) { - // Discard transactions related to Logs query - nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs'); - } - return { queryTransactions: nextQueryTransactions, showingLogs }; - }, - () => { - if (this.state.showingLogs) { - this.onSubmit(); - } - } - ); + this.props.clickLogsButton(); }; // Use this in help pages to set page to a single query onClickExample = (query: DataQuery) => { - const nextQueries = [{ ...query, ...generateQueryKeys() }]; - this.modifiedQueries = [...nextQueries]; - this.setState({ initialQueries: nextQueries }, this.onSubmit); + this.props.clickExample(query); }; onClickSplit = () => { const { onChangeSplit } = this.props; if (onChangeSplit) { - const state = this.cloneState(); - onChangeSplit(true, state); + // const state = this.cloneState(); + // onChangeSplit(true, state); } }; onClickTableButton = () => { - this.setState( - state => { - const showingTable = !state.showingTable; - if (showingTable) { - return { showingTable, queryTransactions: state.queryTransactions }; - } - - // Toggle off needs discarding of table queries - const nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Table'); - const results = calculateResultsFromQueryTransactions( - nextQueryTransactions, - state.datasource, - state.graphInterval - ); - - return { ...results, queryTransactions: nextQueryTransactions, showingTable }; - }, - () => { - if (this.state.showingTable) { - this.onSubmit(); - } - } - ); + this.props.clickTableButton(); }; onClickLabel = (key: string, value: string) => { @@ -473,404 +230,62 @@ export class Explore extends React.PureComponent { }; onModifyQueries = (action, index?: number) => { - const { datasource } = this.state; - if (datasource && datasource.modifyQuery) { - const preventSubmit = action.preventSubmit; - this.setState( - state => { - const { initialQueries, queryTransactions } = state; - let nextQueries: DataQuery[]; - let nextQueryTransactions; - if (index === undefined) { - // Modify all queries - nextQueries = initialQueries.map((query, i) => ({ - ...datasource.modifyQuery(this.modifiedQueries[i], action), - ...generateQueryKeys(i), - })); - // Discard all ongoing transactions - nextQueryTransactions = []; - } else { - // Modify query only at index - nextQueries = initialQueries.map((query, i) => { - // Synchronize all queries with local query cache to ensure consistency - // TODO still needed? - return i === index - ? { - ...datasource.modifyQuery(this.modifiedQueries[i], action), - ...generateQueryKeys(i), - } - : query; - }); - nextQueryTransactions = queryTransactions - // Consume the hint corresponding to the action - .map(qt => { - if (qt.hints != null && qt.rowIndex === index) { - qt.hints = qt.hints.filter(hint => hint.fix.action !== action); - } - return qt; - }) - // Preserve previous row query transaction to keep results visible if next query is incomplete - .filter(qt => preventSubmit || qt.rowIndex !== index); - } - this.modifiedQueries = [...nextQueries]; - return { - initialQueries: nextQueries, - queryTransactions: nextQueryTransactions, - }; - }, - // Accepting certain fixes do not result in a well-formed query which should not be submitted - !preventSubmit ? () => this.onSubmit() : null - ); + const { datasourceInstance } = this.props; + if (datasourceInstance && datasourceInstance.modifyQuery) { + const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action); + this.props.modifyQueries(action, index, modifier); } }; onRemoveQueryRow = index => { - // Remove from local cache - this.modifiedQueries = [...this.modifiedQueries.slice(0, index), ...this.modifiedQueries.slice(index + 1)]; - - this.setState( - state => { - const { initialQueries, queryTransactions } = state; - if (initialQueries.length <= 1) { - return null; - } - // Remove row from react state - const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)]; - - // Discard transactions related to row query - const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); - const results = calculateResultsFromQueryTransactions( - nextQueryTransactions, - state.datasource, - state.graphInterval - ); - - return { - ...results, - initialQueries: nextQueries, - logsHighlighterExpressions: undefined, - queryTransactions: nextQueryTransactions, - }; - }, - () => this.onSubmit() - ); + this.props.removeQueryRow(index); }; onStartScanning = () => { - this.setState({ scanning: true }, this.scanPreviousRange); + // Scanner will trigger a query + const scanner = this.scanPreviousRange; + this.props.scanStart(scanner); }; - scanPreviousRange = () => { - const scanRange = this.timepickerRef.current.move(-1, true); - this.setState({ scanRange }); + scanPreviousRange = (): RawTimeRange => { + // Calling move() on the timepicker will trigger this.onChangeTime() + return this.timepickerRef.current.move(-1, true); }; onStopScanning = () => { - clearTimeout(this.scanTimer); - this.setState(state => { - const { queryTransactions } = state; - const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done); - return { queryTransactions: nextQueryTransactions, scanning: false, scanRange: undefined }; - }); + this.props.scanStop(); }; onSubmit = () => { - const { showingLogs, showingGraph, showingTable, supportsGraph, supportsLogs, supportsTable } = this.state; - // Keep table queries first since they need to return quickly - if (showingTable && supportsTable) { - this.runQueries( - 'Table', - { - format: 'table', - instant: true, - valueWithRefId: true, - }, - data => data[0] - ); - } - if (showingGraph && supportsGraph) { - this.runQueries( - 'Graph', - { - format: 'time_series', - instant: false, - }, - makeTimeSeriesList - ); - } - if (showingLogs && supportsLogs) { - this.runQueries('Logs', { format: 'logs' }); - } - this.saveState(); + this.props.runQueries(); }; - buildQueryOptions(query: DataQuery, queryOptions: { format: string; hinting?: boolean; instant?: boolean }) { - const { datasource, range } = this.state; - const { interval, intervalMs } = getIntervals(range, datasource, this.el.offsetWidth); - - const configuredQueries = [ - { - ...query, - ...queryOptions, - }, - ]; - - // Clone range for query request - // const queryRange: RawTimeRange = { ...range }; - // const { from, to, raw } = this.timeSrv.timeRange(); - // Most datasource is using `panelId + query.refId` for cancellation logic. - // Using `format` here because it relates to the view panel that the request is for. - // However, some datasources don't use `panelId + query.refId`, but only `panelId`. - // Therefore panel id has to be unique. - const panelId = `${queryOptions.format}-${query.key}`; - - return { - interval, - intervalMs, - panelId, - targets: configuredQueries, // Datasources rely on DataQueries being passed under the targets key. - range: { - from: dateMath.parse(range.from, false), - to: dateMath.parse(range.to, true), - raw: range, - }, - rangeRaw: range, - scopedVars: { - __interval: { text: interval, value: interval }, - __interval_ms: { text: intervalMs, value: intervalMs }, - }, - }; - } - - startQueryTransaction(query: DataQuery, rowIndex: number, resultType: ResultType, options: any): QueryTransaction { - const queryOptions = this.buildQueryOptions(query, options); - const transaction: QueryTransaction = { - query, - resultType, - rowIndex, - id: generateKey(), // reusing for unique ID - done: false, - latency: 0, - options: queryOptions, - scanning: this.state.scanning, - }; - - // Using updater style because we might be modifying queryTransactions in quick succession - this.setState(state => { - const { queryTransactions } = state; - // Discarding existing transactions of same type - const remainingTransactions = queryTransactions.filter( - qt => !(qt.resultType === resultType && qt.rowIndex === rowIndex) - ); - - // Append new transaction - const nextQueryTransactions = [...remainingTransactions, transaction]; - - const results = calculateResultsFromQueryTransactions( - nextQueryTransactions, - state.datasource, - state.graphInterval - ); - - return { - ...results, - queryTransactions: nextQueryTransactions, - showingStartPage: false, - graphInterval: queryOptions.intervalMs, - }; - }); - - return transaction; - } - - completeQueryTransaction( - transactionId: string, - result: any, - latency: number, - queries: DataQuery[], - datasourceId: string - ) { - const { datasource } = this.state; - if (datasource.meta.id !== datasourceId) { - // Navigated away, queries did not matter - return; + updateLogsHighlights = _.debounce((value: DataQuery) => { + const { datasourceInstance } = this.props; + if (datasourceInstance.getHighlighterExpression) { + const expressions = [datasourceInstance.getHighlighterExpression(value)]; + this.props.highlightLogsExpression(expressions); } - - this.setState(state => { - const { history, queryTransactions } = state; - let { scanning } = state; - - // Transaction might have been discarded - const transaction = queryTransactions.find(qt => qt.id === transactionId); - if (!transaction) { - return null; - } - - // Get query hints - let hints: QueryHint[]; - if (datasource.getQueryHints as QueryHintGetter) { - hints = datasource.getQueryHints(transaction.query, result); - } - - // Mark transactions as complete - const nextQueryTransactions = queryTransactions.map(qt => { - if (qt.id === transactionId) { - return { - ...qt, - hints, - latency, - result, - done: true, - }; - } - return qt; - }); - - const results = calculateResultsFromQueryTransactions( - nextQueryTransactions, - state.datasource, - state.graphInterval - ); - - const nextHistory = updateHistory(history, datasourceId, queries); - - // Keep scanning for results if this was the last scanning transaction - if (scanning) { - if (_.size(result) === 0) { - const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done); - if (!other) { - this.scanTimer = setTimeout(this.scanPreviousRange, 1000); - } - } else { - // We can stop scanning if we have a result - scanning = false; - } - } - - return { - ...results, - scanning, - history: nextHistory, - queryTransactions: nextQueryTransactions, - }; - }); - } - - failQueryTransaction(transactionId: string, response: any, datasourceId: string) { - const { datasource } = this.state; - if (datasource.meta.id !== datasourceId || response.cancelled) { - // Navigated away, queries did not matter - return; - } - - console.error(response); - - let error: string | JSX.Element; - if (response.data) { - if (typeof response.data === 'string') { - error = response.data; - } else if (response.data.error) { - error = response.data.error; - if (response.data.response) { - error = ( - <> - {response.data.error} -
{response.data.response}
- - ); - } - } else { - throw new Error('Could not handle error response'); - } - } else if (response.message) { - error = response.message; - } else if (typeof response === 'string') { - error = response; - } else { - error = 'Unknown error during query transaction. Please check JS console logs.'; - } - - this.setState(state => { - // Transaction might have been discarded - if (!state.queryTransactions.find(qt => qt.id === transactionId)) { - return null; - } - - // Mark transactions as complete - const nextQueryTransactions = state.queryTransactions.map(qt => { - if (qt.id === transactionId) { - return { - ...qt, - error, - done: true, - }; - } - return qt; - }); - - return { - queryTransactions: nextQueryTransactions, - }; - }); - } - - async runQueries(resultType: ResultType, queryOptions: any, resultGetter?: any) { - const queries = [...this.modifiedQueries]; - if (!hasNonEmptyQuery(queries)) { - this.setState({ - queryTransactions: [], - }); - return; - } - const { datasource } = this.state; - const datasourceId = datasource.meta.id; - // Run all queries concurrentlyso - queries.forEach(async (query, rowIndex) => { - const transaction = this.startQueryTransaction(query, rowIndex, resultType, queryOptions); - try { - const now = Date.now(); - const res = await datasource.query(transaction.options); - this.exploreEvents.emit('data-received', res.data || []); - const latency = Date.now() - now; - const results = resultGetter ? resultGetter(res.data) : res.data; - this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId); - } catch (response) { - this.exploreEvents.emit('data-error', response); - this.failQueryTransaction(transaction.id, response, datasourceId); - } - }); - } - - updateLogsHighlights = _.debounce((value: DataQuery, index: number) => { - this.setState(state => { - const { datasource } = state; - if (datasource.getHighlighterExpression) { - const logsHighlighterExpressions = [state.datasource.getHighlighterExpression(value)]; - return { logsHighlighterExpressions }; - } - return null; - }); }, 500); - cloneState(): ExploreState { - // Copy state, but copy queries including modifications - return { - ...this.state, - queryTransactions: [], - initialQueries: [...this.modifiedQueries], - }; - } + // cloneState(): ExploreState { + // // Copy state, but copy queries including modifications + // return { + // ...this.state, + // queryTransactions: [], + // initialQueries: [...this.modifiedQueries], + // }; + // } - saveState = () => { - const { stateKey, onSaveState } = this.props; - onSaveState(stateKey, this.cloneState()); - }; + // saveState = () => { + // const { stateKey, onSaveState } = this.props; + // onSaveState(stateKey, this.cloneState()); + // }; render() { - const { position, split } = this.props; const { StartPage, - datasource, + datasourceInstance, datasourceError, datasourceLoading, datasourceMissing, @@ -881,6 +296,7 @@ export class Explore extends React.PureComponent { logsHighlighterExpressions, logsResult, queryTransactions, + position, range, scanning, scanRange, @@ -888,14 +304,17 @@ export class Explore extends React.PureComponent { showingLogs, showingStartPage, showingTable, + split, supportsGraph, supportsLogs, supportsTable, tableResult, - } = this.state; + } = this.props; const graphHeight = showingGraph && showingTable ? '200px' : '400px'; const exploreClass = split ? 'explore explore-split' : 'explore'; - const selectedDatasource = datasource ? exploreDatasources.find(d => d.name === datasource.name) : undefined; + const selectedDatasource = datasourceInstance + ? exploreDatasources.find(d => d.name === datasourceInstance.name) + : undefined; const graphLoading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done); const tableLoading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done); const logsLoading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done); @@ -959,10 +378,10 @@ export class Explore extends React.PureComponent {
)} - {datasource && !datasourceError ? ( + {datasourceInstance && !datasourceError ? (
{ } } -export default hot(module)(Explore); +function mapStateToProps({ explore }) { + const { + StartPage, + datasourceError, + datasourceInstance, + datasourceLoading, + datasourceMissing, + exploreDatasources, + graphResult, + initialDatasource, + initialQueries, + history, + logsHighlighterExpressions, + logsResult, + queryTransactions, + range, + scanning, + scanRange, + showingGraph, + showingLogs, + showingStartPage, + showingTable, + supportsGraph, + supportsLogs, + supportsTable, + tableResult, + } = explore as ExploreState; + return { + StartPage, + datasourceError, + datasourceInstance, + datasourceLoading, + datasourceMissing, + exploreDatasources, + graphResult, + initialDatasource, + initialQueries, + history, + logsHighlighterExpressions, + logsResult, + queryTransactions, + range, + scanning, + scanRange, + showingGraph, + showingLogs, + showingStartPage, + showingTable, + supportsGraph, + supportsLogs, + supportsTable, + tableResult, + }; +} + +const mapDispatchToProps = { + addQueryRow, + changeDatasource, + changeQuery, + changeTime, + clickClear, + clickExample, + clickGraphButton, + clickLogsButton, + clickTableButton, + highlightLogsExpression, + initializeExplore, + modifyQueries, + onSize: changeSize, // used by withSize HOC + removeQueryRow, + runQueries, + scanStart, + scanStop, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(withSize()(Explore))); diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts new file mode 100644 index 00000000000..d70a458059e --- /dev/null +++ b/public/app/features/explore/state/actions.ts @@ -0,0 +1,694 @@ +import _ from 'lodash'; +import { ThunkAction } from 'redux-thunk'; +import { RawTimeRange, TimeRange } from '@grafana/ui'; + +import { + LAST_USED_DATASOURCE_KEY, + ensureQueries, + generateEmptyQuery, + hasNonEmptyQuery, + makeTimeSeriesList, + updateHistory, + buildQueryTransaction, +} from 'app/core/utils/explore'; + +import store from 'app/core/store'; +import { DataSourceSelectItem } from 'app/types/datasources'; +import { DataQuery, StoreState } from 'app/types'; +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { + HistoryItem, + RangeScanner, + ResultType, + QueryOptions, + QueryTransaction, + QueryHint, + QueryHintGetter, +} from 'app/types/explore'; +import { Emitter } from 'app/core/core'; +import { dispatch } from 'rxjs/internal/observable/pairs'; + +export enum ActionTypes { + AddQueryRow = 'ADD_QUERY_ROW', + ChangeDatasource = 'CHANGE_DATASOURCE', + ChangeQuery = 'CHANGE_QUERY', + ChangeSize = 'CHANGE_SIZE', + ChangeTime = 'CHANGE_TIME', + ClickClear = 'CLICK_CLEAR', + ClickExample = 'CLICK_EXAMPLE', + ClickGraphButton = 'CLICK_GRAPH_BUTTON', + ClickLogsButton = 'CLICK_LOGS_BUTTON', + ClickTableButton = 'CLICK_TABLE_BUTTON', + HighlightLogsExpression = 'HIGHLIGHT_LOGS_EXPRESSION', + InitializeExplore = 'INITIALIZE_EXPLORE', + LoadDatasourceFailure = 'LOAD_DATASOURCE_FAILURE', + LoadDatasourceMissing = 'LOAD_DATASOURCE_MISSING', + LoadDatasourcePending = 'LOAD_DATASOURCE_PENDING', + LoadDatasourceSuccess = 'LOAD_DATASOURCE_SUCCESS', + ModifyQueries = 'MODIFY_QUERIES', + QueryTransactionFailure = 'QUERY_TRANSACTION_FAILURE', + QueryTransactionStart = 'QUERY_TRANSACTION_START', + QueryTransactionSuccess = 'QUERY_TRANSACTION_SUCCESS', + RemoveQueryRow = 'REMOVE_QUERY_ROW', + RunQueries = 'RUN_QUERIES', + RunQueriesEmpty = 'RUN_QUERIES', + ScanRange = 'SCAN_RANGE', + ScanStart = 'SCAN_START', + ScanStop = 'SCAN_STOP', +} + +export interface AddQueryRowAction { + type: ActionTypes.AddQueryRow; + index: number; + query: DataQuery; +} + +export interface ChangeQueryAction { + type: ActionTypes.ChangeQuery; + query: DataQuery; + index: number; + override: boolean; +} + +export interface ChangeSizeAction { + type: ActionTypes.ChangeSize; + width: number; + height: number; +} + +export interface ChangeTimeAction { + type: ActionTypes.ChangeTime; + range: TimeRange; +} + +export interface ClickClearAction { + type: ActionTypes.ClickClear; +} + +export interface ClickExampleAction { + type: ActionTypes.ClickExample; + query: DataQuery; +} + +export interface ClickGraphButtonAction { + type: ActionTypes.ClickGraphButton; +} + +export interface ClickLogsButtonAction { + type: ActionTypes.ClickLogsButton; +} + +export interface ClickTableButtonAction { + type: ActionTypes.ClickTableButton; +} + +export interface InitializeExploreAction { + type: ActionTypes.InitializeExplore; + containerWidth: number; + datasource: string; + eventBridge: Emitter; + exploreDatasources: DataSourceSelectItem[]; + queries: DataQuery[]; + range: RawTimeRange; +} + +export interface HighlightLogsExpressionAction { + type: ActionTypes.HighlightLogsExpression; + expressions: string[]; +} + +export interface LoadDatasourceFailureAction { + type: ActionTypes.LoadDatasourceFailure; + error: string; +} + +export interface LoadDatasourcePendingAction { + type: ActionTypes.LoadDatasourcePending; + datasourceId: number; +} + +export interface LoadDatasourceMissingAction { + type: ActionTypes.LoadDatasourceMissing; +} + +export interface LoadDatasourceSuccessAction { + type: ActionTypes.LoadDatasourceSuccess; + StartPage?: any; + datasourceInstance: any; + history: HistoryItem[]; + initialDatasource: string; + initialQueries: DataQuery[]; + logsHighlighterExpressions?: any[]; + showingStartPage: boolean; + supportsGraph: boolean; + supportsLogs: boolean; + supportsTable: boolean; +} + +export interface ModifyQueriesAction { + type: ActionTypes.ModifyQueries; + modification: any; + index: number; + modifier: (queries: DataQuery[], modification: any) => DataQuery[]; +} + +export interface QueryTransactionFailureAction { + type: ActionTypes.QueryTransactionFailure; + queryTransactions: QueryTransaction[]; +} + +export interface QueryTransactionStartAction { + type: ActionTypes.QueryTransactionStart; + resultType: ResultType; + rowIndex: number; + transaction: QueryTransaction; +} + +export interface QueryTransactionSuccessAction { + type: ActionTypes.QueryTransactionSuccess; + history: HistoryItem[]; + queryTransactions: QueryTransaction[]; +} + +export interface RemoveQueryRowAction { + type: ActionTypes.RemoveQueryRow; + index: number; +} + +export interface ScanStartAction { + type: ActionTypes.ScanStart; + scanner: RangeScanner; +} + +export interface ScanRangeAction { + type: ActionTypes.ScanRange; + range: RawTimeRange; +} + +export interface ScanStopAction { + type: ActionTypes.ScanStop; +} + +export type Action = + | AddQueryRowAction + | ChangeQueryAction + | ChangeSizeAction + | ChangeTimeAction + | ClickClearAction + | ClickExampleAction + | ClickGraphButtonAction + | ClickLogsButtonAction + | ClickTableButtonAction + | HighlightLogsExpressionAction + | InitializeExploreAction + | LoadDatasourceFailureAction + | LoadDatasourceMissingAction + | LoadDatasourcePendingAction + | LoadDatasourceSuccessAction + | ModifyQueriesAction + | QueryTransactionFailureAction + | QueryTransactionStartAction + | QueryTransactionSuccessAction + | RemoveQueryRowAction + | ScanRangeAction + | ScanStartAction + | ScanStopAction; +type ThunkResult = ThunkAction; + +export function addQueryRow(index: number): AddQueryRowAction { + const query = generateEmptyQuery(index + 1); + return { type: ActionTypes.AddQueryRow, index, query }; +} + +export function changeDatasource(datasource: string): ThunkResult { + return async dispatch => { + const instance = await getDatasourceSrv().get(datasource); + dispatch(loadDatasource(instance)); + }; +} + +export function changeQuery(query: DataQuery, index: number, override: boolean): ThunkResult { + return dispatch => { + // Null query means reset + if (query === null) { + query = { ...generateEmptyQuery(index) }; + } + + dispatch({ type: ActionTypes.ChangeQuery, query, index, override }); + if (override) { + dispatch(runQueries()); + } + }; +} + +export function changeSize({ height, width }: { height: number; width: number }): ChangeSizeAction { + return { type: ActionTypes.ChangeSize, height, width }; +} + +export function changeTime(range: TimeRange): ThunkResult { + return dispatch => { + dispatch({ type: ActionTypes.ChangeTime, range }); + dispatch(runQueries()); + }; +} + +export function clickExample(rawQuery: DataQuery): ThunkResult { + return dispatch => { + const query = { ...rawQuery, ...generateEmptyQuery() }; + dispatch({ + type: ActionTypes.ClickExample, + query, + }); + dispatch(runQueries()); + }; +} + +export function clickClear(): ThunkResult { + return dispatch => { + dispatch(scanStop()); + dispatch({ type: ActionTypes.ClickClear }); + // TODO save state + }; +} + +export function clickGraphButton(): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ClickGraphButton }); + if (getState().explore.showingGraph) { + dispatch(runQueries()); + } + }; +} + +export function clickLogsButton(): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ClickLogsButton }); + if (getState().explore.showingLogs) { + dispatch(runQueries()); + } + }; +} + +export function clickTableButton(): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ClickTableButton }); + if (getState().explore.showingTable) { + dispatch(runQueries()); + } + }; +} + +export function highlightLogsExpression(expressions: string[]): HighlightLogsExpressionAction { + return { type: ActionTypes.HighlightLogsExpression, expressions }; +} + +export function initializeExplore( + datasource: string, + queries: DataQuery[], + range: RawTimeRange, + containerWidth: number, + eventBridge: Emitter +): ThunkResult { + return async dispatch => { + const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv() + .getExternal() + .map(ds => ({ + value: ds.name, + name: ds.name, + meta: ds.meta, + })); + + dispatch({ + type: ActionTypes.InitializeExplore, + containerWidth, + datasource, + eventBridge, + exploreDatasources, + queries, + range, + }); + + if (exploreDatasources.length > 1) { + let instance; + if (datasource) { + instance = await getDatasourceSrv().get(datasource); + } else { + instance = await getDatasourceSrv().get(); + } + dispatch(loadDatasource(instance)); + } else { + dispatch(loadDatasourceMissing); + } + }; +} + +export const loadDatasourceFailure = (error: string): LoadDatasourceFailureAction => ({ + type: ActionTypes.LoadDatasourceFailure, + error, +}); + +export const loadDatasourceMissing: LoadDatasourceMissingAction = { type: ActionTypes.LoadDatasourceMissing }; + +export const loadDatasourcePending = (datasourceId: number): LoadDatasourcePendingAction => ({ + type: ActionTypes.LoadDatasourcePending, + datasourceId, +}); + +export const loadDatasourceSuccess = (instance: any, queries: DataQuery[]): LoadDatasourceSuccessAction => { + // Capabilities + const supportsGraph = instance.meta.metrics; + const supportsLogs = instance.meta.logs; + const supportsTable = instance.meta.tables; + // Custom components + const StartPage = instance.pluginExports.ExploreStartPage; + + const historyKey = `grafana.explore.history.${instance.meta.id}`; + const history = store.getObject(historyKey, []); + // Save last-used datasource + store.set(LAST_USED_DATASOURCE_KEY, instance.name); + + return { + type: ActionTypes.LoadDatasourceSuccess, + StartPage, + datasourceInstance: instance, + history, + initialDatasource: instance.name, + initialQueries: queries, + showingStartPage: Boolean(StartPage), + supportsGraph, + supportsLogs, + supportsTable, + }; +}; + +export function loadDatasource(instance: any): ThunkResult { + return async (dispatch, getState) => { + const datasourceId = instance.meta.id; + + // Keep ID to track selection + dispatch(loadDatasourcePending(datasourceId)); + + let datasourceError = null; + try { + const testResult = await instance.testDatasource(); + datasourceError = testResult.status === 'success' ? null : testResult.message; + } catch (error) { + datasourceError = (error && error.statusText) || 'Network error'; + } + if (datasourceError) { + dispatch(loadDatasourceFailure(datasourceError)); + return; + } + + if (datasourceId !== getState().explore.requestedDatasourceId) { + // User already changed datasource again, discard results + return; + } + + if (instance.init) { + instance.init(); + } + + // Check if queries can be imported from previously selected datasource + const queries = getState().explore.modifiedQueries; + let importedQueries = queries; + const origin = getState().explore.datasourceInstance; + if (origin) { + if (origin.meta.id === instance.meta.id) { + // Keep same queries if same type of datasource + importedQueries = [...queries]; + } else if (instance.importQueries) { + // Datasource-specific importers + importedQueries = await instance.importQueries(queries, origin.meta); + } else { + // Default is blank queries + importedQueries = ensureQueries(); + } + } + + if (datasourceId !== getState().explore.requestedDatasourceId) { + // User already changed datasource again, discard results + return; + } + + // Reset edit state with new queries + const nextQueries = importedQueries.map((q, i) => ({ + ...importedQueries[i], + ...generateEmptyQuery(i), + })); + + dispatch(loadDatasourceSuccess(instance, nextQueries)); + dispatch(runQueries()); + }; +} + +export function modifyQueries(modification: any, index: number, modifier: any): ThunkResult { + return dispatch => { + dispatch({ type: ActionTypes.ModifyQueries, modification, index, modifier }); + if (!modification.preventSubmit) { + dispatch(runQueries()); + } + }; +} + +export function queryTransactionFailure(transactionId: string, response: any, datasourceId: string): ThunkResult { + return (dispatch, getState) => { + const { datasourceInstance, queryTransactions } = getState().explore; + if (datasourceInstance.meta.id !== datasourceId || response.cancelled) { + // Navigated away, queries did not matter + return; + } + + // Transaction might have been discarded + if (!queryTransactions.find(qt => qt.id === transactionId)) { + return null; + } + + console.error(response); + + let error: string; + let errorDetails: string; + if (response.data) { + if (typeof response.data === 'string') { + error = response.data; + } else if (response.data.error) { + error = response.data.error; + if (response.data.response) { + errorDetails = response.data.response; + } + } else { + throw new Error('Could not handle error response'); + } + } else if (response.message) { + error = response.message; + } else if (typeof response === 'string') { + error = response; + } else { + error = 'Unknown error during query transaction. Please check JS console logs.'; + } + + // Mark transactions as complete + const nextQueryTransactions = queryTransactions.map(qt => { + if (qt.id === transactionId) { + return { + ...qt, + error, + errorDetails, + done: true, + }; + } + return qt; + }); + + dispatch({ type: ActionTypes.QueryTransactionFailure, queryTransactions: nextQueryTransactions }); + }; +} + +export function queryTransactionStart( + transaction: QueryTransaction, + resultType: ResultType, + rowIndex: number +): QueryTransactionStartAction { + return { type: ActionTypes.QueryTransactionStart, resultType, rowIndex, transaction }; +} + +export function queryTransactionSuccess( + transactionId: string, + result: any, + latency: number, + queries: DataQuery[], + datasourceId: string +): ThunkResult { + return (dispatch, getState) => { + const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore; + + // If datasource already changed, results do not matter + if (datasourceInstance.meta.id !== datasourceId) { + return; + } + + // Transaction might have been discarded + const transaction = queryTransactions.find(qt => qt.id === transactionId); + if (!transaction) { + return; + } + + // Get query hints + let hints: QueryHint[]; + if (datasourceInstance.getQueryHints as QueryHintGetter) { + hints = datasourceInstance.getQueryHints(transaction.query, result); + } + + // Mark transactions as complete and attach result + const nextQueryTransactions = queryTransactions.map(qt => { + if (qt.id === transactionId) { + return { + ...qt, + hints, + latency, + result, + done: true, + }; + } + return qt; + }); + + // Side-effect: Saving history in localstorage + const nextHistory = updateHistory(history, datasourceId, queries); + + dispatch({ + type: ActionTypes.QueryTransactionSuccess, + history: nextHistory, + queryTransactions: nextQueryTransactions, + }); + + // Keep scanning for results if this was the last scanning transaction + if (scanning) { + if (_.size(result) === 0) { + const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done); + if (!other) { + const range = scanner(); + dispatch({ type: ActionTypes.ScanRange, range }); + } + } else { + // We can stop scanning if we have a result + dispatch(scanStop()); + } + } + }; +} + +export function removeQueryRow(index: number): ThunkResult { + return dispatch => { + dispatch({ type: ActionTypes.RemoveQueryRow, index }); + dispatch(runQueries()); + }; +} + +export function runQueries() { + return (dispatch, getState) => { + const { + datasourceInstance, + modifiedQueries, + showingLogs, + showingGraph, + showingTable, + supportsGraph, + supportsLogs, + supportsTable, + } = getState().explore; + + if (!hasNonEmptyQuery(modifiedQueries)) { + dispatch({ type: ActionTypes.RunQueriesEmpty }); + return; + } + + // Some datasource's query builders allow per-query interval limits, + // but we're using the datasource interval limit for now + const interval = datasourceInstance.interval; + + // Keep table queries first since they need to return quickly + if (showingTable && supportsTable) { + dispatch( + runQueriesForType( + 'Table', + { + interval, + format: 'table', + instant: true, + valueWithRefId: true, + }, + data => data[0] + ) + ); + } + if (showingGraph && supportsGraph) { + dispatch( + runQueriesForType( + 'Graph', + { + interval, + format: 'time_series', + instant: false, + }, + makeTimeSeriesList + ) + ); + } + if (showingLogs && supportsLogs) { + dispatch(runQueriesForType('Logs', { interval, format: 'logs' })); + } + // TODO save state + }; +} + +function runQueriesForType(resultType: ResultType, queryOptions: QueryOptions, resultGetter?: any) { + return async (dispatch, getState) => { + const { + datasourceInstance, + eventBridge, + modifiedQueries: queries, + queryIntervals, + range, + scanning, + } = getState().explore; + const datasourceId = datasourceInstance.meta.id; + + // Run all queries concurrently + queries.forEach(async (query, rowIndex) => { + const transaction = buildQueryTransaction( + query, + rowIndex, + resultType, + queryOptions, + range, + queryIntervals, + scanning + ); + dispatch(queryTransactionStart(transaction, resultType, rowIndex)); + try { + const now = Date.now(); + const res = await datasourceInstance.query(transaction.options); + eventBridge.emit('data-received', res.data || []); + const latency = Date.now() - now; + const results = resultGetter ? resultGetter(res.data) : res.data; + dispatch(queryTransactionSuccess(transaction.id, results, latency, queries, datasourceId)); + } catch (response) { + eventBridge.emit('data-error', response); + dispatch(queryTransactionFailure(transaction.id, response, datasourceId)); + } + }); + }; +} + +export function scanStart(scanner: RangeScanner): ThunkResult { + return dispatch => { + dispatch({ type: ActionTypes.ScanStart, scanner }); + const range = scanner(); + dispatch({ type: ActionTypes.ScanRange, range }); + }; +} + +export function scanStop(): ScanStopAction { + return { type: ActionTypes.ScanStop }; +} diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts new file mode 100644 index 00000000000..b49d54405d1 --- /dev/null +++ b/public/app/features/explore/state/reducers.ts @@ -0,0 +1,412 @@ +import { RawTimeRange, TimeRange } from '@grafana/ui'; + +import { + calculateResultsFromQueryTransactions, + generateEmptyQuery, + getIntervals, + ensureQueries, +} from 'app/core/utils/explore'; +import { DataSourceSelectItem } from 'app/types/datasources'; +import { HistoryItem, QueryTransaction, QueryIntervals, RangeScanner } from 'app/types/explore'; +import { DataQuery } from 'app/types/series'; + +import { Action, ActionTypes } from './actions'; +import { Emitter } from 'app/core/core'; +import { LogsModel } from 'app/core/logs_model'; +import TableModel from 'app/core/table_model'; + +// TODO move to types +export interface ExploreState { + StartPage?: any; + containerWidth: number; + datasourceInstance: any; + datasourceError: string; + datasourceLoading: boolean | null; + datasourceMissing: boolean; + eventBridge?: Emitter; + exploreDatasources: DataSourceSelectItem[]; + graphResult?: any[]; + history: HistoryItem[]; + initialDatasource?: string; + initialQueries: DataQuery[]; + logsHighlighterExpressions?: string[]; + logsResult?: LogsModel; + modifiedQueries: DataQuery[]; + queryIntervals: QueryIntervals; + queryTransactions: QueryTransaction[]; + requestedDatasourceId?: number; + range: TimeRange | RawTimeRange; + scanner?: RangeScanner; + scanning?: boolean; + scanRange?: RawTimeRange; + showingGraph: boolean; + showingLogs: boolean; + showingStartPage?: boolean; + showingTable: boolean; + supportsGraph: boolean | null; + supportsLogs: boolean | null; + supportsTable: boolean | null; + tableResult?: TableModel; +} + +export const DEFAULT_RANGE = { + from: 'now-6h', + to: 'now', +}; + +// Millies step for helper bar charts +const DEFAULT_GRAPH_INTERVAL = 15 * 1000; + +const initialExploreState: ExploreState = { + StartPage: undefined, + containerWidth: 0, + datasourceInstance: null, + datasourceError: null, + datasourceLoading: null, + datasourceMissing: false, + exploreDatasources: [], + history: [], + initialQueries: [], + modifiedQueries: [], + queryTransactions: [], + queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL }, + range: DEFAULT_RANGE, + scanning: false, + scanRange: null, + showingGraph: true, + showingLogs: true, + showingTable: true, + supportsGraph: null, + supportsLogs: null, + supportsTable: null, +}; + +export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => { + switch (action.type) { + case ActionTypes.AddQueryRow: { + const { initialQueries, modifiedQueries, queryTransactions } = state; + const { index, query } = action; + modifiedQueries[index + 1] = query; + + const nextQueries = [ + ...initialQueries.slice(0, index + 1), + { ...modifiedQueries[index + 1] }, + ...initialQueries.slice(index + 1), + ]; + + // Ongoing transactions need to update their row indices + const nextQueryTransactions = queryTransactions.map(qt => { + if (qt.rowIndex > index) { + return { + ...qt, + rowIndex: qt.rowIndex + 1, + }; + } + return qt; + }); + + return { + ...state, + modifiedQueries, + initialQueries: nextQueries, + logsHighlighterExpressions: undefined, + queryTransactions: nextQueryTransactions, + }; + } + + case ActionTypes.ChangeQuery: { + const { initialQueries, queryTransactions } = state; + let { modifiedQueries } = state; + const { query, index, override } = action; + modifiedQueries[index] = query; + if (override) { + const nextQuery: DataQuery = { + ...query, + ...generateEmptyQuery(index), + }; + const nextQueries = [...initialQueries]; + nextQueries[index] = nextQuery; + modifiedQueries = [...nextQueries]; + + // Discard ongoing transaction related to row query + const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + + return { + ...state, + initialQueries: nextQueries, + modifiedQueries: nextQueries.slice(), + queryTransactions: nextQueryTransactions, + }; + } + return { + ...state, + modifiedQueries, + }; + } + + case ActionTypes.ChangeSize: { + const { range, datasourceInstance } = state; + if (!datasourceInstance) { + return state; + } + const containerWidth = action.width; + const queryIntervals = getIntervals(range, datasourceInstance.interval, containerWidth); + return { ...state, containerWidth, queryIntervals }; + } + + case ActionTypes.ChangeTime: { + return { + ...state, + range: action.range, + }; + } + + case ActionTypes.ClickClear: { + const queries = ensureQueries(); + return { + ...state, + initialQueries: queries.slice(), + modifiedQueries: queries.slice(), + showingStartPage: Boolean(state.StartPage), + }; + } + + case ActionTypes.ClickExample: { + const modifiedQueries = [action.query]; + return { ...state, initialQueries: modifiedQueries.slice(), modifiedQueries }; + } + + case ActionTypes.ClickGraphButton: { + const showingGraph = !state.showingGraph; + let nextQueryTransactions = state.queryTransactions; + if (!showingGraph) { + // Discard transactions related to Graph query + nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph'); + } + return { ...state, queryTransactions: nextQueryTransactions, showingGraph }; + } + + case ActionTypes.ClickLogsButton: { + const showingLogs = !state.showingLogs; + let nextQueryTransactions = state.queryTransactions; + if (!showingLogs) { + // Discard transactions related to Logs query + nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs'); + } + return { ...state, queryTransactions: nextQueryTransactions, showingLogs }; + } + + case ActionTypes.ClickTableButton: { + const showingTable = !state.showingTable; + if (showingTable) { + return { ...state, showingTable, queryTransactions: state.queryTransactions }; + } + + // Toggle off needs discarding of table queries and results + const nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Table'); + const results = calculateResultsFromQueryTransactions( + nextQueryTransactions, + state.datasourceInstance, + state.queryIntervals.intervalMs + ); + + return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable }; + } + + case ActionTypes.InitializeExplore: { + const { containerWidth, eventBridge, exploreDatasources, range } = action; + return { + ...state, + containerWidth, + eventBridge, + exploreDatasources, + range, + initialDatasource: action.datasource, + initialQueries: action.queries, + modifiedQueries: action.queries.slice(), + }; + } + + case ActionTypes.LoadDatasourceFailure: { + return { ...state, datasourceError: action.error, datasourceLoading: false }; + } + + case ActionTypes.LoadDatasourceMissing: { + return { ...state, datasourceMissing: true, datasourceLoading: false }; + } + + case ActionTypes.LoadDatasourcePending: { + return { ...state, datasourceLoading: true, requestedDatasourceId: action.datasourceId }; + } + + case ActionTypes.LoadDatasourceSuccess: { + const { containerWidth, range } = state; + const queryIntervals = getIntervals(range, action.datasourceInstance.interval, containerWidth); + + return { + ...state, + queryIntervals, + StartPage: action.StartPage, + datasourceInstance: action.datasourceInstance, + datasourceLoading: false, + datasourceMissing: false, + history: action.history, + initialDatasource: action.initialDatasource, + initialQueries: action.initialQueries, + logsHighlighterExpressions: undefined, + modifiedQueries: action.initialQueries.slice(), + showingStartPage: action.showingStartPage, + supportsGraph: action.supportsGraph, + supportsLogs: action.supportsLogs, + supportsTable: action.supportsTable, + }; + } + + case ActionTypes.ModifyQueries: { + const { initialQueries, modifiedQueries, queryTransactions } = state; + const { action: modification, index, modifier } = action as any; + let nextQueries: DataQuery[]; + let nextQueryTransactions; + if (index === undefined) { + // Modify all queries + nextQueries = initialQueries.map((query, i) => ({ + ...modifier(modifiedQueries[i], modification), + ...generateEmptyQuery(i), + })); + // Discard all ongoing transactions + nextQueryTransactions = []; + } else { + // Modify query only at index + nextQueries = initialQueries.map((query, i) => { + // Synchronize all queries with local query cache to ensure consistency + // TODO still needed? + return i === index + ? { + ...modifier(modifiedQueries[i], modification), + ...generateEmptyQuery(i), + } + : query; + }); + nextQueryTransactions = queryTransactions + // Consume the hint corresponding to the action + .map(qt => { + if (qt.hints != null && qt.rowIndex === index) { + qt.hints = qt.hints.filter(hint => hint.fix.action !== modification); + } + return qt; + }) + // Preserve previous row query transaction to keep results visible if next query is incomplete + .filter(qt => modification.preventSubmit || qt.rowIndex !== index); + } + return { + ...state, + initialQueries: nextQueries, + modifiedQueries: nextQueries.slice(), + queryTransactions: nextQueryTransactions, + }; + } + + case ActionTypes.RemoveQueryRow: { + const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state; + let { modifiedQueries } = state; + const { index } = action; + + modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)]; + + if (initialQueries.length <= 1) { + return state; + } + + const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)]; + + // Discard transactions related to row query + const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + const results = calculateResultsFromQueryTransactions( + nextQueryTransactions, + datasourceInstance, + queryIntervals.intervalMs + ); + + return { + ...state, + ...results, + initialQueries: nextQueries, + logsHighlighterExpressions: undefined, + modifiedQueries: nextQueries.slice(), + queryTransactions: nextQueryTransactions, + }; + } + + case ActionTypes.QueryTransactionFailure: { + const { queryTransactions } = action; + return { + ...state, + queryTransactions, + showingStartPage: false, + }; + } + + case ActionTypes.QueryTransactionStart: { + const { datasourceInstance, queryIntervals, queryTransactions } = state; + const { resultType, rowIndex, transaction } = action; + // Discarding existing transactions of same type + const remainingTransactions = queryTransactions.filter( + qt => !(qt.resultType === resultType && qt.rowIndex === rowIndex) + ); + + // Append new transaction + const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction]; + + const results = calculateResultsFromQueryTransactions( + nextQueryTransactions, + datasourceInstance, + queryIntervals.intervalMs + ); + + return { + ...state, + ...results, + queryTransactions: nextQueryTransactions, + showingStartPage: false, + }; + } + + case ActionTypes.QueryTransactionSuccess: { + const { datasourceInstance, queryIntervals } = state; + const { history, queryTransactions } = action; + const results = calculateResultsFromQueryTransactions( + queryTransactions, + datasourceInstance, + queryIntervals.intervalMs + ); + + return { + ...state, + ...results, + history, + queryTransactions, + showingStartPage: false, + }; + } + + case ActionTypes.ScanRange: { + return { ...state, scanRange: action.range }; + } + + case ActionTypes.ScanStart: { + return { ...state, scanning: true }; + } + + case ActionTypes.ScanStop: { + const { queryTransactions } = state; + const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done); + return { ...state, queryTransactions: nextQueryTransactions, scanning: false, scanRange: undefined }; + } + } + + return state; +}; + +export default { + explore: exploreReducer, +}; diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 943aff80a70..570a387cd74 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -7,6 +7,7 @@ import teamsReducers from 'app/features/teams/state/reducers'; import apiKeysReducers from 'app/features/api-keys/state/reducers'; import foldersReducers from 'app/features/folders/state/reducers'; import dashboardReducers from 'app/features/dashboard/state/reducers'; +import exploreReducers from 'app/features/explore/state/reducers'; import pluginReducers from 'app/features/plugins/state/reducers'; import dataSourcesReducers from 'app/features/datasources/state/reducers'; import usersReducers from 'app/features/users/state/reducers'; @@ -20,6 +21,7 @@ const rootReducers = { ...apiKeysReducers, ...foldersReducers, ...dashboardReducers, + ...exploreReducers, ...pluginReducers, ...dataSourcesReducers, ...usersReducers, diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index c2c59d35f5b..c64ce6133cf 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -4,7 +4,6 @@ import { DataQuery } from './series'; import { RawTimeRange } from '@grafana/ui'; import TableModel from 'app/core/table_model'; import { LogsModel } from 'app/core/logs_model'; -import { DataSourceSelectItem } from 'app/types/datasources'; export interface CompletionItem { /** @@ -128,6 +127,19 @@ export interface QueryHintGetter { (query: DataQuery, results: any[], ...rest: any): QueryHint[]; } +export interface QueryIntervals { + interval: string; + intervalMs: number; +} + +export interface QueryOptions { + interval: string; + format: string; + hinting?: boolean; + instant?: boolean; + valueWithRefId?: boolean; +} + export interface QueryTransaction { id: string; done: boolean; @@ -142,6 +154,8 @@ export interface QueryTransaction { scanning?: boolean; } +export type RangeScanner = () => RawTimeRange; + export interface TextMatch { text: string; start: number; @@ -153,18 +167,11 @@ export interface ExploreState { StartPage?: any; datasource: any; datasourceError: any; - datasourceLoading: boolean | null; - datasourceMissing: boolean; - exploreDatasources: DataSourceSelectItem[]; - graphInterval: number; // in ms graphResult?: any[]; history: HistoryItem[]; - initialDatasource?: string; - initialQueries: DataQuery[]; logsHighlighterExpressions?: string[]; logsResult?: LogsModel; queryTransactions: QueryTransaction[]; - range: RawTimeRange; scanning?: boolean; scanRange?: RawTimeRange; showingGraph: boolean; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 72da1c76ea8..018c4c51d3d 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -19,6 +19,7 @@ import { } from './appNotifications'; import { DashboardSearchHit } from './search'; import { ValidationEvents, ValidationRule } from './form'; +import { ExploreState } from 'app/features/explore/state/reducers'; export { Team, TeamsState, @@ -81,6 +82,7 @@ export interface StoreState { folder: FolderState; dashboard: DashboardState; dataSources: DataSourcesState; + explore: ExploreState; users: UsersState; organization: OrganizationState; appNotifications: AppNotificationsState; From 68c039b28901908aa91d9a9fea83f5e0d2655237 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 11 Jan 2019 18:26:56 +0100 Subject: [PATCH 020/156] Allow multiple Explore items for split --- public/app/core/utils/explore.ts | 13 +- public/app/features/explore/Explore.tsx | 272 +++++++++--------- public/app/features/explore/Logs.tsx | 6 +- public/app/features/explore/Wrapper.tsx | 86 ++---- public/app/features/explore/state/actions.ts | 266 +++++++++++------ public/app/features/explore/state/reducers.ts | 47 ++- public/app/types/explore.ts | 5 + public/sass/pages/_explore.scss | 2 +- 8 files changed, 413 insertions(+), 284 deletions(-) diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 871a020ccc2..026d1ba324c 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -206,11 +206,14 @@ export function ensureQueries(queries?: DataQuery[]): DataQuery[] { * A target is non-empty when it has keys (with non-empty values) other than refId and key. */ export function hasNonEmptyQuery(queries: DataQuery[]): boolean { - return queries.some( - query => - Object.keys(query) - .map(k => query[k]) - .filter(v => v).length > 2 + return ( + queries && + queries.some( + query => + Object.keys(query) + .map(k => query[k]) + .filter(v => v).length > 2 + ) ); } diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 64e9c66ece5..a3177bebe79 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -2,14 +2,15 @@ import React from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import _ from 'lodash'; -import { withSize } from 'react-sizeme'; +import { AutoSizer } from 'react-virtualized'; import { RawTimeRange, TimeRange } from '@grafana/ui'; import { DataSourceSelectItem } from 'app/types/datasources'; -import { ExploreUrlState, HistoryItem, QueryTransaction, RangeScanner } from 'app/types/explore'; +import { ExploreUrlState, HistoryItem, QueryTransaction, RangeScanner, ExploreId } from 'app/types/explore'; import { DataQuery } from 'app/types/series'; +import { StoreState } from 'app/types'; import store from 'app/core/store'; -import { LAST_USED_DATASOURCE_KEY, ensureQueries } from 'app/core/utils/explore'; +import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { Emitter } from 'app/core/utils/emitter'; @@ -20,9 +21,11 @@ import { changeSize, changeTime, clickClear, + clickCloseSplit, clickExample, clickGraphButton, clickLogsButton, + clickSplit, clickTableButton, highlightLogsExpression, initializeExplore, @@ -32,7 +35,7 @@ import { scanStart, scanStop, } from './state/actions'; -import { ExploreState } from './state/reducers'; +import { ExploreItemState } from './state/reducers'; import Panel from './Panel'; import QueryRows from './QueryRows'; @@ -50,17 +53,21 @@ interface ExploreProps { addQueryRow: typeof addQueryRow; changeDatasource: typeof changeDatasource; changeQuery: typeof changeQuery; + changeSize: typeof changeSize; changeTime: typeof changeTime; clickClear: typeof clickClear; + clickCloseSplit: typeof clickCloseSplit; clickExample: typeof clickExample; clickGraphButton: typeof clickGraphButton; clickLogsButton: typeof clickLogsButton; + clickSplit: typeof clickSplit; clickTableButton: typeof clickTableButton; datasourceError: string; datasourceInstance: any; datasourceLoading: boolean | null; datasourceMissing: boolean; exploreDatasources: DataSourceSelectItem[]; + exploreId: ExploreId; graphResult?: any[]; highlightLogsExpression: typeof highlightLogsExpression; history: HistoryItem[]; @@ -70,9 +77,6 @@ interface ExploreProps { logsHighlighterExpressions?: string[]; logsResult?: LogsModel; modifyQueries: typeof modifyQueries; - onChangeSplit: (split: boolean, state?: ExploreState) => void; - onSaveState: (key: string, state: ExploreState) => void; - position: string; queryTransactions: QueryTransaction[]; removeQueryRow: typeof removeQueryRow; range: RawTimeRange; @@ -83,8 +87,6 @@ interface ExploreProps { scanStart: typeof scanStart; scanStop: typeof scanStop; split: boolean; - splitState?: ExploreState; - stateKey: string; showingGraph: boolean; showingLogs: boolean; showingStartPage?: boolean; @@ -132,7 +134,7 @@ interface ExploreProps { * The result viewers determine some of the query options sent to the datasource, e.g., * `format`, to indicate eventual transformations by the datasources' result transformers. */ -export class Explore extends React.PureComponent { +export class Explore extends React.PureComponent { el: any; exploreEvents: Emitter; /** @@ -147,13 +149,23 @@ export class Explore extends React.PureComponent { } async componentDidMount() { - // Load URL state and parse range - const { datasource, queries, range } = this.props.urlState as ExploreUrlState; - const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY); - const initialQueries: DataQuery[] = ensureQueries(queries); - const initialRange = { from: parseTime(range.from), to: parseTime(range.to) }; - const width = this.el ? this.el.offsetWidth : 0; - this.props.initializeExplore(initialDatasource, initialQueries, initialRange, width, this.exploreEvents); + const { exploreId, split, urlState } = this.props; + if (!split) { + // Load URL state and parse range + const { datasource, queries, range = DEFAULT_RANGE } = (urlState || {}) as ExploreUrlState; + const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY); + const initialQueries: DataQuery[] = ensureQueries(queries); + const initialRange = { from: parseTime(range.from), to: parseTime(range.to) }; + const width = this.el ? this.el.offsetWidth : 0; + this.props.initializeExplore( + exploreId, + initialDatasource, + initialQueries, + initialRange, + width, + this.exploreEvents + ); + } } componentWillUnmount() { @@ -165,17 +177,17 @@ export class Explore extends React.PureComponent { }; onAddQueryRow = index => { - this.props.addQueryRow(index); + this.props.addQueryRow(this.props.exploreId, index); }; onChangeDatasource = async option => { - this.props.changeDatasource(option.value); + this.props.changeDatasource(this.props.exploreId, option.value); }; onChangeQuery = (query: DataQuery, index: number, override?: boolean) => { - const { changeQuery, datasourceInstance } = this.props; + const { changeQuery, datasourceInstance, exploreId } = this.props; - changeQuery(query, index, override); + changeQuery(exploreId, query, index, override); if (query && !override && datasourceInstance.getHighlighterExpression && index === 0) { // Live preview of log search matches. Only use on first row for now this.updateLogsHighlights(query); @@ -186,43 +198,36 @@ export class Explore extends React.PureComponent { if (this.props.scanning && !changedByScanner) { this.onStopScanning(); } - this.props.changeTime(range); + this.props.changeTime(this.props.exploreId, range); }; onClickClear = () => { - this.props.clickClear(); + this.props.clickClear(this.props.exploreId); }; onClickCloseSplit = () => { - const { onChangeSplit } = this.props; - if (onChangeSplit) { - onChangeSplit(false); - } + this.props.clickCloseSplit(); }; onClickGraphButton = () => { - this.props.clickGraphButton(); + this.props.clickGraphButton(this.props.exploreId); }; onClickLogsButton = () => { - this.props.clickLogsButton(); + this.props.clickLogsButton(this.props.exploreId); }; // Use this in help pages to set page to a single query onClickExample = (query: DataQuery) => { - this.props.clickExample(query); + this.props.clickExample(this.props.exploreId, query); }; onClickSplit = () => { - const { onChangeSplit } = this.props; - if (onChangeSplit) { - // const state = this.cloneState(); - // onChangeSplit(true, state); - } + this.props.clickSplit(); }; onClickTableButton = () => { - this.props.clickTableButton(); + this.props.clickTableButton(this.props.exploreId); }; onClickLabel = (key: string, value: string) => { @@ -233,18 +238,22 @@ export class Explore extends React.PureComponent { const { datasourceInstance } = this.props; if (datasourceInstance && datasourceInstance.modifyQuery) { const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action); - this.props.modifyQueries(action, index, modifier); + this.props.modifyQueries(this.props.exploreId, action, index, modifier); } }; onRemoveQueryRow = index => { - this.props.removeQueryRow(index); + this.props.removeQueryRow(this.props.exploreId, index); + }; + + onResize = (size: { height: number; width: number }) => { + this.props.changeSize(this.props.exploreId, size); }; onStartScanning = () => { // Scanner will trigger a query const scanner = this.scanPreviousRange; - this.props.scanStart(scanner); + this.props.scanStart(this.props.exploreId, scanner); }; scanPreviousRange = (): RawTimeRange => { @@ -253,30 +262,21 @@ export class Explore extends React.PureComponent { }; onStopScanning = () => { - this.props.scanStop(); + this.props.scanStop(this.props.exploreId); }; onSubmit = () => { - this.props.runQueries(); + this.props.runQueries(this.props.exploreId); }; updateLogsHighlights = _.debounce((value: DataQuery) => { const { datasourceInstance } = this.props; if (datasourceInstance.getHighlighterExpression) { const expressions = [datasourceInstance.getHighlighterExpression(value)]; - this.props.highlightLogsExpression(expressions); + this.props.highlightLogsExpression(this.props.exploreId, expressions); } }, 500); - // cloneState(): ExploreState { - // // Copy state, but copy queries including modifications - // return { - // ...this.state, - // queryTransactions: [], - // initialQueries: [...this.modifiedQueries], - // }; - // } - // saveState = () => { // const { stateKey, onSaveState } = this.props; // onSaveState(stateKey, this.cloneState()); @@ -290,13 +290,13 @@ export class Explore extends React.PureComponent { datasourceLoading, datasourceMissing, exploreDatasources, + exploreId, graphResult, history, initialQueries, logsHighlighterExpressions, logsResult, queryTransactions, - position, range, scanning, scanRange, @@ -323,7 +323,7 @@ export class Explore extends React.PureComponent { return (
- {position === 'left' ? ( + {exploreId === 'left' ? ( ) : null}
- {position === 'left' && !split ? ( + {exploreId === 'left' && !split ? (
)} - {datasourceInstance && !datasourceError ? ( -
- -
- - {showingStartPage && } - {!showingStartPage && ( - <> - {supportsGraph && ( - - - - )} - {supportsTable && ( - - - - )} - {supportsLogs && ( - - - - )} - + {datasourceInstance && + !datasourceError && ( +
+ + + {({ width }) => ( +
+ + {showingStartPage && } + {!showingStartPage && ( + <> + {supportsGraph && ( + + + + )} + {supportsTable && ( + +
+ + )} + {supportsLogs && ( + + + + )} + + )} + + )} - - - - ) : null} + + + )} ); } } -function mapStateToProps({ explore }) { +function mapStateToProps(state: StoreState, { exploreId }) { + const explore = state.explore; + const { split } = explore; + const item: ExploreItemState = explore[exploreId]; const { StartPage, datasourceError, @@ -480,7 +493,7 @@ function mapStateToProps({ explore }) { supportsLogs, supportsTable, tableResult, - } = explore as ExploreState; + } = item; return { StartPage, datasourceError, @@ -502,6 +515,7 @@ function mapStateToProps({ explore }) { showingLogs, showingStartPage, showingTable, + split, supportsGraph, supportsLogs, supportsTable, @@ -513,20 +527,22 @@ const mapDispatchToProps = { addQueryRow, changeDatasource, changeQuery, + changeSize, changeTime, clickClear, + clickCloseSplit, clickExample, clickGraphButton, clickLogsButton, + clickSplit, clickTableButton, highlightLogsExpression, initializeExplore, modifyQueries, - onSize: changeSize, // used by withSize HOC removeQueryRow, runQueries, scanStart, scanStop, }; -export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(withSize()(Explore))); +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Explore)); diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 1a384cf011d..d07b31e2ff1 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -241,9 +241,9 @@ function renderMetaItem(value: any, kind: LogsMetaKind) { interface LogsProps { data: LogsModel; + exploreId: string; highlighterExpressions: string[]; loading: boolean; - position: string; range?: RawTimeRange; scanning?: boolean; scanRange?: RawTimeRange; @@ -348,10 +348,10 @@ export default class Logs extends PureComponent { render() { const { data, + exploreId, highlighterExpressions, loading = false, onClickLabel, - position, range, scanning, scanRange, @@ -400,7 +400,7 @@ export default class Logs extends PureComponent { data={data.series} height="100px" range={range} - id={`explore-logs-graph-${position}`} + id={`explore-logs-graph-${exploreId}`} onChangeTime={this.props.onChangeTime} onToggleSeries={this.onToggleLogLevel} userOptions={graphOptions} diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index de1eee4c662..04f189749bf 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -3,9 +3,9 @@ import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import { updateLocation } from 'app/core/actions'; -import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore'; +// import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore'; import { StoreState } from 'app/types'; -import { ExploreState } from 'app/types/explore'; +import { ExploreId } from 'app/types/explore'; import ErrorBoundary from './ErrorBoundary'; import Explore from './Explore'; @@ -13,81 +13,41 @@ import Explore from './Explore'; interface WrapperProps { backendSrv?: any; datasourceSrv?: any; - updateLocation: typeof updateLocation; - urlStates: { [key: string]: string }; -} - -interface WrapperState { split: boolean; - splitState: ExploreState; + updateLocation: typeof updateLocation; + // urlStates: { [key: string]: string }; } -const STATE_KEY_LEFT = 'state'; -const STATE_KEY_RIGHT = 'stateRight'; - -export class Wrapper extends Component { - urlStates: { [key: string]: string }; +export class Wrapper extends Component { + // urlStates: { [key: string]: string }; constructor(props: WrapperProps) { super(props); - this.urlStates = props.urlStates; - this.state = { - split: Boolean(props.urlStates[STATE_KEY_RIGHT]), - splitState: undefined, - }; + // this.urlStates = props.urlStates; } - onChangeSplit = (split: boolean, splitState: ExploreState) => { - this.setState({ split, splitState }); - // When closing split, remove URL state for split part - if (!split) { - delete this.urlStates[STATE_KEY_RIGHT]; - this.props.updateLocation({ - query: this.urlStates, - }); - } - }; - - onSaveState = (key: string, state: ExploreState) => { - const urlState = serializeStateToUrlParam(state, true); - this.urlStates[key] = urlState; - this.props.updateLocation({ - query: this.urlStates, - }); - }; + // onSaveState = (key: string, state: ExploreState) => { + // const urlState = serializeStateToUrlParam(state, true); + // this.urlStates[key] = urlState; + // this.props.updateLocation({ + // query: this.urlStates, + // }); + // }; render() { - const { datasourceSrv } = this.props; + const { split } = this.props; // State overrides for props from first Explore - const { split, splitState } = this.state; - const urlStateLeft = parseUrlState(this.urlStates[STATE_KEY_LEFT]); - const urlStateRight = parseUrlState(this.urlStates[STATE_KEY_RIGHT]); + // const urlStateLeft = parseUrlState(this.urlStates[STATE_KEY_LEFT]); + // const urlStateRight = parseUrlState(this.urlStates[STATE_KEY_RIGHT]); return (
- + {split && ( - + )}
@@ -95,9 +55,11 @@ export class Wrapper extends Component { } } -const mapStateToProps = (state: StoreState) => ({ - urlStates: state.location.query, -}); +const mapStateToProps = (state: StoreState) => { + // urlStates: state.location.query, + const { split } = state.explore; + return { split }; +}; const mapDispatchToProps = { updateLocation, diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index d70a458059e..145b7506e79 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -17,6 +17,7 @@ import { DataSourceSelectItem } from 'app/types/datasources'; import { DataQuery, StoreState } from 'app/types'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { + ExploreId, HistoryItem, RangeScanner, ResultType, @@ -26,7 +27,7 @@ import { QueryHintGetter, } from 'app/types/explore'; import { Emitter } from 'app/core/core'; -import { dispatch } from 'rxjs/internal/observable/pairs'; +import { ExploreItemState } from './reducers'; export enum ActionTypes { AddQueryRow = 'ADD_QUERY_ROW', @@ -35,9 +36,11 @@ export enum ActionTypes { ChangeSize = 'CHANGE_SIZE', ChangeTime = 'CHANGE_TIME', ClickClear = 'CLICK_CLEAR', + ClickCloseSplit = 'CLICK_CLOSE_SPLIT', ClickExample = 'CLICK_EXAMPLE', ClickGraphButton = 'CLICK_GRAPH_BUTTON', ClickLogsButton = 'CLICK_LOGS_BUTTON', + ClickSplit = 'CLICK_SPLIT', ClickTableButton = 'CLICK_TABLE_BUTTON', HighlightLogsExpression = 'HIGHLIGHT_LOGS_EXPRESSION', InitializeExplore = 'INITIALIZE_EXPLORE', @@ -59,12 +62,14 @@ export enum ActionTypes { export interface AddQueryRowAction { type: ActionTypes.AddQueryRow; + exploreId: ExploreId; index: number; query: DataQuery; } export interface ChangeQueryAction { type: ActionTypes.ChangeQuery; + exploreId: ExploreId; query: DataQuery; index: number; override: boolean; @@ -72,38 +77,55 @@ export interface ChangeQueryAction { export interface ChangeSizeAction { type: ActionTypes.ChangeSize; + exploreId: ExploreId; width: number; height: number; } export interface ChangeTimeAction { type: ActionTypes.ChangeTime; + exploreId: ExploreId; range: TimeRange; } export interface ClickClearAction { type: ActionTypes.ClickClear; + exploreId: ExploreId; +} + +export interface ClickCloseSplitAction { + type: ActionTypes.ClickCloseSplit; } export interface ClickExampleAction { type: ActionTypes.ClickExample; + exploreId: ExploreId; query: DataQuery; } export interface ClickGraphButtonAction { type: ActionTypes.ClickGraphButton; + exploreId: ExploreId; } export interface ClickLogsButtonAction { type: ActionTypes.ClickLogsButton; + exploreId: ExploreId; +} + +export interface ClickSplitAction { + type: ActionTypes.ClickSplit; + itemState: ExploreItemState; } export interface ClickTableButtonAction { type: ActionTypes.ClickTableButton; + exploreId: ExploreId; } export interface InitializeExploreAction { type: ActionTypes.InitializeExplore; + exploreId: ExploreId; containerWidth: number; datasource: string; eventBridge: Emitter; @@ -114,25 +136,30 @@ export interface InitializeExploreAction { export interface HighlightLogsExpressionAction { type: ActionTypes.HighlightLogsExpression; + exploreId: ExploreId; expressions: string[]; } export interface LoadDatasourceFailureAction { type: ActionTypes.LoadDatasourceFailure; + exploreId: ExploreId; error: string; } export interface LoadDatasourcePendingAction { type: ActionTypes.LoadDatasourcePending; + exploreId: ExploreId; datasourceId: number; } export interface LoadDatasourceMissingAction { type: ActionTypes.LoadDatasourceMissing; + exploreId: ExploreId; } export interface LoadDatasourceSuccessAction { type: ActionTypes.LoadDatasourceSuccess; + exploreId: ExploreId; StartPage?: any; datasourceInstance: any; history: HistoryItem[]; @@ -147,6 +174,7 @@ export interface LoadDatasourceSuccessAction { export interface ModifyQueriesAction { type: ActionTypes.ModifyQueries; + exploreId: ExploreId; modification: any; index: number; modifier: (queries: DataQuery[], modification: any) => DataQuery[]; @@ -154,11 +182,13 @@ export interface ModifyQueriesAction { export interface QueryTransactionFailureAction { type: ActionTypes.QueryTransactionFailure; + exploreId: ExploreId; queryTransactions: QueryTransaction[]; } export interface QueryTransactionStartAction { type: ActionTypes.QueryTransactionStart; + exploreId: ExploreId; resultType: ResultType; rowIndex: number; transaction: QueryTransaction; @@ -166,27 +196,32 @@ export interface QueryTransactionStartAction { export interface QueryTransactionSuccessAction { type: ActionTypes.QueryTransactionSuccess; + exploreId: ExploreId; history: HistoryItem[]; queryTransactions: QueryTransaction[]; } export interface RemoveQueryRowAction { type: ActionTypes.RemoveQueryRow; + exploreId: ExploreId; index: number; } export interface ScanStartAction { type: ActionTypes.ScanStart; + exploreId: ExploreId; scanner: RangeScanner; } export interface ScanRangeAction { type: ActionTypes.ScanRange; + exploreId: ExploreId; range: RawTimeRange; } export interface ScanStopAction { type: ActionTypes.ScanStop; + exploreId: ExploreId; } export type Action = @@ -195,9 +230,11 @@ export type Action = | ChangeSizeAction | ChangeTimeAction | ClickClearAction + | ClickCloseSplitAction | ClickExampleAction | ClickGraphButtonAction | ClickLogsButtonAction + | ClickSplitAction | ClickTableButtonAction | HighlightLogsExpressionAction | InitializeExploreAction @@ -215,94 +252,126 @@ export type Action = | ScanStopAction; type ThunkResult = ThunkAction; -export function addQueryRow(index: number): AddQueryRowAction { +export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction { const query = generateEmptyQuery(index + 1); - return { type: ActionTypes.AddQueryRow, index, query }; + return { type: ActionTypes.AddQueryRow, exploreId, index, query }; } -export function changeDatasource(datasource: string): ThunkResult { +export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult { return async dispatch => { const instance = await getDatasourceSrv().get(datasource); - dispatch(loadDatasource(instance)); + dispatch(loadDatasource(exploreId, instance)); }; } -export function changeQuery(query: DataQuery, index: number, override: boolean): ThunkResult { +export function changeQuery( + exploreId: ExploreId, + query: DataQuery, + index: number, + override: boolean +): ThunkResult { return dispatch => { // Null query means reset if (query === null) { query = { ...generateEmptyQuery(index) }; } - dispatch({ type: ActionTypes.ChangeQuery, query, index, override }); + dispatch({ type: ActionTypes.ChangeQuery, exploreId, query, index, override }); if (override) { - dispatch(runQueries()); + dispatch(runQueries(exploreId)); } }; } -export function changeSize({ height, width }: { height: number; width: number }): ChangeSizeAction { - return { type: ActionTypes.ChangeSize, height, width }; +export function changeSize( + exploreId: ExploreId, + { height, width }: { height: number; width: number } +): ChangeSizeAction { + return { type: ActionTypes.ChangeSize, exploreId, height, width }; } -export function changeTime(range: TimeRange): ThunkResult { +export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.ChangeTime, range }); - dispatch(runQueries()); + dispatch({ type: ActionTypes.ChangeTime, exploreId, range }); + dispatch(runQueries(exploreId)); }; } -export function clickExample(rawQuery: DataQuery): ThunkResult { +export function clickClear(exploreId: ExploreId): ThunkResult { return dispatch => { - const query = { ...rawQuery, ...generateEmptyQuery() }; - dispatch({ - type: ActionTypes.ClickExample, - query, - }); - dispatch(runQueries()); - }; -} - -export function clickClear(): ThunkResult { - return dispatch => { - dispatch(scanStop()); - dispatch({ type: ActionTypes.ClickClear }); + dispatch(scanStop(exploreId)); + dispatch({ type: ActionTypes.ClickClear, exploreId }); // TODO save state }; } -export function clickGraphButton(): ThunkResult { +export function clickCloseSplit(): ThunkResult { + return dispatch => { + dispatch({ type: ActionTypes.ClickCloseSplit }); + // When closing split, remove URL state for split part + // TODO save state + }; +} + +export function clickExample(exploreId: ExploreId, rawQuery: DataQuery): ThunkResult { + return dispatch => { + const query = { ...rawQuery, ...generateEmptyQuery() }; + dispatch({ + type: ActionTypes.ClickExample, + exploreId, + query, + }); + dispatch(runQueries(exploreId)); + }; +} + +export function clickGraphButton(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ClickGraphButton }); - if (getState().explore.showingGraph) { - dispatch(runQueries()); + dispatch({ type: ActionTypes.ClickGraphButton, exploreId }); + if (getState().explore[exploreId].showingGraph) { + dispatch(runQueries(exploreId)); } }; } -export function clickLogsButton(): ThunkResult { +export function clickLogsButton(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ClickLogsButton }); - if (getState().explore.showingLogs) { - dispatch(runQueries()); + dispatch({ type: ActionTypes.ClickLogsButton, exploreId }); + if (getState().explore[exploreId].showingLogs) { + dispatch(runQueries(exploreId)); } }; } -export function clickTableButton(): ThunkResult { +export function clickSplit(): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ClickTableButton }); - if (getState().explore.showingTable) { - dispatch(runQueries()); + // Clone left state to become the right state + const leftState = getState().explore.left; + const itemState = { + ...leftState, + queryTransactions: [], + initialQueries: leftState.modifiedQueries.slice(), + }; + dispatch({ type: ActionTypes.ClickSplit, itemState }); + // TODO save state + }; +} + +export function clickTableButton(exploreId: ExploreId): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ClickTableButton, exploreId }); + if (getState().explore[exploreId].showingTable) { + dispatch(runQueries(exploreId)); } }; } -export function highlightLogsExpression(expressions: string[]): HighlightLogsExpressionAction { - return { type: ActionTypes.HighlightLogsExpression, expressions }; +export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction { + return { type: ActionTypes.HighlightLogsExpression, exploreId, expressions }; } export function initializeExplore( + exploreId: ExploreId, datasource: string, queries: DataQuery[], range: RawTimeRange, @@ -320,6 +389,7 @@ export function initializeExplore( dispatch({ type: ActionTypes.InitializeExplore, + exploreId, containerWidth, datasource, eventBridge, @@ -335,26 +405,35 @@ export function initializeExplore( } else { instance = await getDatasourceSrv().get(); } - dispatch(loadDatasource(instance)); + dispatch(loadDatasource(exploreId, instance)); } else { - dispatch(loadDatasourceMissing); + dispatch(loadDatasourceMissing(exploreId)); } }; } -export const loadDatasourceFailure = (error: string): LoadDatasourceFailureAction => ({ +export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({ type: ActionTypes.LoadDatasourceFailure, + exploreId, error, }); -export const loadDatasourceMissing: LoadDatasourceMissingAction = { type: ActionTypes.LoadDatasourceMissing }; +export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({ + type: ActionTypes.LoadDatasourceMissing, + exploreId, +}); -export const loadDatasourcePending = (datasourceId: number): LoadDatasourcePendingAction => ({ +export const loadDatasourcePending = (exploreId: ExploreId, datasourceId: number): LoadDatasourcePendingAction => ({ type: ActionTypes.LoadDatasourcePending, + exploreId, datasourceId, }); -export const loadDatasourceSuccess = (instance: any, queries: DataQuery[]): LoadDatasourceSuccessAction => { +export const loadDatasourceSuccess = ( + exploreId: ExploreId, + instance: any, + queries: DataQuery[] +): LoadDatasourceSuccessAction => { // Capabilities const supportsGraph = instance.meta.metrics; const supportsLogs = instance.meta.logs; @@ -369,6 +448,7 @@ export const loadDatasourceSuccess = (instance: any, queries: DataQuery[]): Load return { type: ActionTypes.LoadDatasourceSuccess, + exploreId, StartPage, datasourceInstance: instance, history, @@ -381,12 +461,12 @@ export const loadDatasourceSuccess = (instance: any, queries: DataQuery[]): Load }; }; -export function loadDatasource(instance: any): ThunkResult { +export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult { return async (dispatch, getState) => { const datasourceId = instance.meta.id; // Keep ID to track selection - dispatch(loadDatasourcePending(datasourceId)); + dispatch(loadDatasourcePending(exploreId, datasourceId)); let datasourceError = null; try { @@ -396,11 +476,11 @@ export function loadDatasource(instance: any): ThunkResult { datasourceError = (error && error.statusText) || 'Network error'; } if (datasourceError) { - dispatch(loadDatasourceFailure(datasourceError)); + dispatch(loadDatasourceFailure(exploreId, datasourceError)); return; } - if (datasourceId !== getState().explore.requestedDatasourceId) { + if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) { // User already changed datasource again, discard results return; } @@ -410,9 +490,9 @@ export function loadDatasource(instance: any): ThunkResult { } // Check if queries can be imported from previously selected datasource - const queries = getState().explore.modifiedQueries; + const queries = getState().explore[exploreId].modifiedQueries; let importedQueries = queries; - const origin = getState().explore.datasourceInstance; + const origin = getState().explore[exploreId].datasourceInstance; if (origin) { if (origin.meta.id === instance.meta.id) { // Keep same queries if same type of datasource @@ -426,7 +506,7 @@ export function loadDatasource(instance: any): ThunkResult { } } - if (datasourceId !== getState().explore.requestedDatasourceId) { + if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) { // User already changed datasource again, discard results return; } @@ -437,23 +517,33 @@ export function loadDatasource(instance: any): ThunkResult { ...generateEmptyQuery(i), })); - dispatch(loadDatasourceSuccess(instance, nextQueries)); - dispatch(runQueries()); + dispatch(loadDatasourceSuccess(exploreId, instance, nextQueries)); + dispatch(runQueries(exploreId)); }; } -export function modifyQueries(modification: any, index: number, modifier: any): ThunkResult { +export function modifyQueries( + exploreId: ExploreId, + modification: any, + index: number, + modifier: any +): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.ModifyQueries, modification, index, modifier }); + dispatch({ type: ActionTypes.ModifyQueries, exploreId, modification, index, modifier }); if (!modification.preventSubmit) { - dispatch(runQueries()); + dispatch(runQueries(exploreId)); } }; } -export function queryTransactionFailure(transactionId: string, response: any, datasourceId: string): ThunkResult { +export function queryTransactionFailure( + exploreId: ExploreId, + transactionId: string, + response: any, + datasourceId: string +): ThunkResult { return (dispatch, getState) => { - const { datasourceInstance, queryTransactions } = getState().explore; + const { datasourceInstance, queryTransactions } = getState().explore[exploreId]; if (datasourceInstance.meta.id !== datasourceId || response.cancelled) { // Navigated away, queries did not matter return; @@ -500,19 +590,21 @@ export function queryTransactionFailure(transactionId: string, response: any, da return qt; }); - dispatch({ type: ActionTypes.QueryTransactionFailure, queryTransactions: nextQueryTransactions }); + dispatch({ type: ActionTypes.QueryTransactionFailure, exploreId, queryTransactions: nextQueryTransactions }); }; } export function queryTransactionStart( + exploreId: ExploreId, transaction: QueryTransaction, resultType: ResultType, rowIndex: number ): QueryTransactionStartAction { - return { type: ActionTypes.QueryTransactionStart, resultType, rowIndex, transaction }; + return { type: ActionTypes.QueryTransactionStart, exploreId, resultType, rowIndex, transaction }; } export function queryTransactionSuccess( + exploreId: ExploreId, transactionId: string, result: any, latency: number, @@ -520,7 +612,7 @@ export function queryTransactionSuccess( datasourceId: string ): ThunkResult { return (dispatch, getState) => { - const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore; + const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore[exploreId]; // If datasource already changed, results do not matter if (datasourceInstance.meta.id !== datasourceId) { @@ -558,6 +650,7 @@ export function queryTransactionSuccess( dispatch({ type: ActionTypes.QueryTransactionSuccess, + exploreId, history: nextHistory, queryTransactions: nextQueryTransactions, }); @@ -568,24 +661,24 @@ export function queryTransactionSuccess( const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done); if (!other) { const range = scanner(); - dispatch({ type: ActionTypes.ScanRange, range }); + dispatch({ type: ActionTypes.ScanRange, exploreId, range }); } } else { // We can stop scanning if we have a result - dispatch(scanStop()); + dispatch(scanStop(exploreId)); } } }; } -export function removeQueryRow(index: number): ThunkResult { +export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.RemoveQueryRow, index }); - dispatch(runQueries()); + dispatch({ type: ActionTypes.RemoveQueryRow, exploreId, index }); + dispatch(runQueries(exploreId)); }; } -export function runQueries() { +export function runQueries(exploreId: ExploreId) { return (dispatch, getState) => { const { datasourceInstance, @@ -596,10 +689,10 @@ export function runQueries() { supportsGraph, supportsLogs, supportsTable, - } = getState().explore; + } = getState().explore[exploreId]; if (!hasNonEmptyQuery(modifiedQueries)) { - dispatch({ type: ActionTypes.RunQueriesEmpty }); + dispatch({ type: ActionTypes.RunQueriesEmpty, exploreId }); return; } @@ -611,6 +704,7 @@ export function runQueries() { if (showingTable && supportsTable) { dispatch( runQueriesForType( + exploreId, 'Table', { interval, @@ -625,6 +719,7 @@ export function runQueries() { if (showingGraph && supportsGraph) { dispatch( runQueriesForType( + exploreId, 'Graph', { interval, @@ -636,13 +731,18 @@ export function runQueries() { ); } if (showingLogs && supportsLogs) { - dispatch(runQueriesForType('Logs', { interval, format: 'logs' })); + dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' })); } // TODO save state }; } -function runQueriesForType(resultType: ResultType, queryOptions: QueryOptions, resultGetter?: any) { +function runQueriesForType( + exploreId: ExploreId, + resultType: ResultType, + queryOptions: QueryOptions, + resultGetter?: any +) { return async (dispatch, getState) => { const { datasourceInstance, @@ -651,7 +751,7 @@ function runQueriesForType(resultType: ResultType, queryOptions: QueryOptions, r queryIntervals, range, scanning, - } = getState().explore; + } = getState().explore[exploreId]; const datasourceId = datasourceInstance.meta.id; // Run all queries concurrently @@ -665,30 +765,30 @@ function runQueriesForType(resultType: ResultType, queryOptions: QueryOptions, r queryIntervals, scanning ); - dispatch(queryTransactionStart(transaction, resultType, rowIndex)); + dispatch(queryTransactionStart(exploreId, transaction, resultType, rowIndex)); try { const now = Date.now(); const res = await datasourceInstance.query(transaction.options); eventBridge.emit('data-received', res.data || []); const latency = Date.now() - now; const results = resultGetter ? resultGetter(res.data) : res.data; - dispatch(queryTransactionSuccess(transaction.id, results, latency, queries, datasourceId)); + dispatch(queryTransactionSuccess(exploreId, transaction.id, results, latency, queries, datasourceId)); } catch (response) { eventBridge.emit('data-error', response); - dispatch(queryTransactionFailure(transaction.id, response, datasourceId)); + dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId)); } }); }; } -export function scanStart(scanner: RangeScanner): ThunkResult { +export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.ScanStart, scanner }); + dispatch({ type: ActionTypes.ScanStart, exploreId, scanner }); const range = scanner(); - dispatch({ type: ActionTypes.ScanRange, range }); + dispatch({ type: ActionTypes.ScanRange, exploreId, range }); }; } -export function scanStop(): ScanStopAction { - return { type: ActionTypes.ScanStop }; +export function scanStop(exploreId: ExploreId): ScanStopAction { + return { type: ActionTypes.ScanStop, exploreId }; } diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index b49d54405d1..cea4d766c58 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -16,7 +16,14 @@ import { LogsModel } from 'app/core/logs_model'; import TableModel from 'app/core/table_model'; // TODO move to types + export interface ExploreState { + split: boolean; + left: ExploreItemState; + right: ExploreItemState; +} + +export interface ExploreItemState { StartPage?: any; containerWidth: number; datasourceInstance: any; @@ -57,7 +64,7 @@ export const DEFAULT_RANGE = { // Millies step for helper bar charts const DEFAULT_GRAPH_INTERVAL = 15 * 1000; -const initialExploreState: ExploreState = { +const makeExploreItemState = (): ExploreItemState => ({ StartPage: undefined, containerWidth: 0, datasourceInstance: null, @@ -79,9 +86,15 @@ const initialExploreState: ExploreState = { supportsGraph: null, supportsLogs: null, supportsTable: null, +}); + +const initialExploreState: ExploreState = { + split: false, + left: makeExploreItemState(), + right: makeExploreItemState(), }; -export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => { +const itemReducer = (state, action: Action): ExploreItemState => { switch (action.type) { case ActionTypes.AddQueryRow: { const { initialQueries, modifiedQueries, queryTransactions } = state; @@ -407,6 +420,36 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp return state; }; +export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => { + switch (action.type) { + case ActionTypes.ClickCloseSplit: { + return { + ...state, + split: false, + }; + } + + case ActionTypes.ClickSplit: { + return { + ...state, + split: true, + right: action.itemState, + }; + } + } + + const { exploreId } = action as any; + if (exploreId !== undefined) { + const exploreItemState = state[exploreId]; + return { + ...state, + [exploreId]: itemReducer(exploreItemState, action), + }; + } + + return state; +}; + export default { explore: exploreReducer, }; diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index c64ce6133cf..525c8f74c81 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -75,6 +75,11 @@ export interface CompletionItemGroup { skipSort?: boolean; } +export enum ExploreId { + left = 'left', + right = 'right', +} + export interface HistoryItem { ts: number; query: DataQuery; diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 098dae1a4a2..abd13a10368 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -1,5 +1,5 @@ .explore { - width: 100%; + flex: 1 1 auto; &-container { padding: $dashboard-padding; From be172d3e4a4c22e6f389a85aaa0fec02fe9ff75a Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sat, 12 Jan 2019 23:22:28 +0100 Subject: [PATCH 021/156] Save state in URL and fix tests --- public/app/core/utils/explore.test.ts | 90 +++++++------------ public/app/core/utils/explore.ts | 7 +- public/app/features/explore/Explore.tsx | 17 ++-- public/app/features/explore/Wrapper.tsx | 48 +++++----- public/app/features/explore/state/actions.ts | 60 +++++++++++-- public/app/features/explore/state/reducers.ts | 14 ++- 6 files changed, 131 insertions(+), 105 deletions(-) diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index a3b08516d16..32135eab90a 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -6,26 +6,13 @@ import { clearHistory, hasNonEmptyQuery, } from './explore'; -import { ExploreState } from 'app/types/explore'; +import { ExploreUrlState } from 'app/types/explore'; import store from 'app/core/store'; -const DEFAULT_EXPLORE_STATE: ExploreState = { +const DEFAULT_EXPLORE_STATE: ExploreUrlState = { datasource: null, - datasourceError: null, - datasourceLoading: null, - datasourceMissing: false, - exploreDatasources: [], - graphInterval: 1000, - history: [], - initialQueries: [], - queryTransactions: [], + queries: [], range: DEFAULT_RANGE, - showingGraph: true, - showingLogs: true, - showingTable: true, - supportsGraph: null, - supportsLogs: null, - supportsTable: null, }; describe('state functions', () => { @@ -68,21 +55,19 @@ describe('state functions', () => { it('returns url parameter value for a state object', () => { const state = { ...DEFAULT_EXPLORE_STATE, - initialDatasource: 'foo', + datasource: 'foo', + queries: [ + { + expr: 'metric{test="a/b"}', + }, + { + expr: 'super{foo="x/z"}', + }, + ], range: { from: 'now-5h', to: 'now', }, - initialQueries: [ - { - refId: '1', - expr: 'metric{test="a/b"}', - }, - { - refId: '2', - expr: 'super{foo="x/z"}', - }, - ], }; expect(serializeStateToUrlParam(state)).toBe( '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' + @@ -93,21 +78,19 @@ describe('state functions', () => { it('returns url parameter value for a state object', () => { const state = { ...DEFAULT_EXPLORE_STATE, - initialDatasource: 'foo', + datasource: 'foo', + queries: [ + { + expr: 'metric{test="a/b"}', + }, + { + expr: 'super{foo="x/z"}', + }, + ], range: { from: 'now-5h', to: 'now', }, - initialQueries: [ - { - refId: '1', - expr: 'metric{test="a/b"}', - }, - { - refId: '2', - expr: 'super{foo="x/z"}', - }, - ], }; expect(serializeStateToUrlParam(state, true)).toBe( '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]' @@ -119,35 +102,24 @@ describe('state functions', () => { it('can parse the serialized state into the original state', () => { const state = { ...DEFAULT_EXPLORE_STATE, - initialDatasource: 'foo', + datasource: 'foo', + queries: [ + { + expr: 'metric{test="a/b"}', + }, + { + expr: 'super{foo="x/z"}', + }, + ], range: { from: 'now - 5h', to: 'now', }, - initialQueries: [ - { - refId: '1', - expr: 'metric{test="a/b"}', - }, - { - refId: '2', - expr: 'super{foo="x/z"}', - }, - ], }; const serialized = serializeStateToUrlParam(state); const parsed = parseUrlState(serialized); - // Account for datasource vs datasourceName - const { datasource, queries, ...rest } = parsed; - const resultState = { - ...rest, - datasource: DEFAULT_EXPLORE_STATE.datasource, - initialDatasource: datasource, - initialQueries: queries, - }; - - expect(state).toMatchObject(resultState); + expect(state).toMatchObject(parsed); }); }); }); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 026d1ba324c..b0dcf2117d0 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -142,7 +142,7 @@ export function buildQueryTransaction( }; } -const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest; +export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest; export function parseUrlState(initial: string | undefined): ExploreUrlState { if (initial) { @@ -169,11 +169,6 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { } export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string { - // const urlState: ExploreUrlState = { - // datasource: state.initialDatasource, - // queries: state.initialQueries.map(clearQueryKeys), - // range: state.range, - // }; if (compact) { return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]); } diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index a3177bebe79..b0a17884fc3 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -13,6 +13,8 @@ import store from 'app/core/store'; import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { Emitter } from 'app/core/utils/emitter'; +import { LogsModel } from 'app/core/logs_model'; +import TableModel from 'app/core/table_model'; import { addQueryRow, @@ -45,8 +47,6 @@ import Table from './Table'; import ErrorBoundary from './ErrorBoundary'; import { Alert } from './Error'; import TimePicker, { parseTime } from './TimePicker'; -import { LogsModel } from 'app/core/logs_model'; -import TableModel from 'app/core/table_model'; interface ExploreProps { StartPage?: any; @@ -74,6 +74,7 @@ interface ExploreProps { initialDatasource?: string; initialQueries: DataQuery[]; initializeExplore: typeof initializeExplore; + initialized: boolean; logsHighlighterExpressions?: string[]; logsResult?: LogsModel; modifyQueries: typeof modifyQueries; @@ -149,8 +150,9 @@ export class Explore extends React.PureComponent { } async componentDidMount() { - const { exploreId, split, urlState } = this.props; - if (!split) { + const { exploreId, initialized, urlState } = this.props; + // Don't initialize on split, but need to initialize urlparameters when present + if (!initialized) { // Load URL state and parse range const { datasource, queries, range = DEFAULT_RANGE } = (urlState || {}) as ExploreUrlState; const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY); @@ -277,11 +279,6 @@ export class Explore extends React.PureComponent { } }, 500); - // saveState = () => { - // const { stateKey, onSaveState } = this.props; - // onSaveState(stateKey, this.cloneState()); - // }; - render() { const { StartPage, @@ -478,6 +475,7 @@ function mapStateToProps(state: StoreState, { exploreId }) { graphResult, initialDatasource, initialQueries, + initialized, history, logsHighlighterExpressions, logsResult, @@ -504,6 +502,7 @@ function mapStateToProps(state: StoreState, { exploreId }) { graphResult, initialDatasource, initialQueries, + initialized, history, logsHighlighterExpressions, logsResult, diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index 04f189749bf..7ea8f228af8 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -3,51 +3,56 @@ import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import { updateLocation } from 'app/core/actions'; -// import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore'; import { StoreState } from 'app/types'; -import { ExploreId } from 'app/types/explore'; +import { ExploreId, ExploreUrlState } from 'app/types/explore'; +import { parseUrlState } from 'app/core/utils/explore'; +import { initializeExploreSplit } from './state/actions'; import ErrorBoundary from './ErrorBoundary'; import Explore from './Explore'; interface WrapperProps { - backendSrv?: any; - datasourceSrv?: any; + initializeExploreSplit: typeof initializeExploreSplit; split: boolean; updateLocation: typeof updateLocation; - // urlStates: { [key: string]: string }; + urlStates: { [key: string]: string }; } export class Wrapper extends Component { - // urlStates: { [key: string]: string }; + initialSplit: boolean; + urlStates: { [key: string]: ExploreUrlState }; constructor(props: WrapperProps) { super(props); - // this.urlStates = props.urlStates; + this.urlStates = {}; + const { left, right } = props.urlStates; + if (props.urlStates.left) { + this.urlStates.leftState = parseUrlState(left); + } + if (props.urlStates.right) { + this.urlStates.rightState = parseUrlState(right); + this.initialSplit = true; + } } - // onSaveState = (key: string, state: ExploreState) => { - // const urlState = serializeStateToUrlParam(state, true); - // this.urlStates[key] = urlState; - // this.props.updateLocation({ - // query: this.urlStates, - // }); - // }; + componentDidMount() { + if (this.initialSplit) { + this.props.initializeExploreSplit(); + } + } render() { const { split } = this.props; - // State overrides for props from first Explore - // const urlStateLeft = parseUrlState(this.urlStates[STATE_KEY_LEFT]); - // const urlStateRight = parseUrlState(this.urlStates[STATE_KEY_RIGHT]); + const { leftState, rightState } = this.urlStates; return (
- + {split && ( - + )}
@@ -56,12 +61,13 @@ export class Wrapper extends Component { } const mapStateToProps = (state: StoreState) => { - // urlStates: state.location.query, + const urlStates = state.location.query; const { split } = state.explore; - return { split }; + return { split, urlStates }; }; const mapDispatchToProps = { + initializeExploreSplit, updateLocation, }; diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 145b7506e79..979c80395c3 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -4,14 +4,17 @@ import { RawTimeRange, TimeRange } from '@grafana/ui'; import { LAST_USED_DATASOURCE_KEY, + clearQueryKeys, ensureQueries, generateEmptyQuery, hasNonEmptyQuery, makeTimeSeriesList, updateHistory, buildQueryTransaction, + serializeStateToUrlParam, } from 'app/core/utils/explore'; +import { updateLocation } from 'app/core/actions'; import store from 'app/core/store'; import { DataSourceSelectItem } from 'app/types/datasources'; import { DataQuery, StoreState } from 'app/types'; @@ -25,6 +28,7 @@ import { QueryTransaction, QueryHint, QueryHintGetter, + ExploreUrlState, } from 'app/types/explore'; import { Emitter } from 'app/core/core'; import { ExploreItemState } from './reducers'; @@ -44,6 +48,7 @@ export enum ActionTypes { ClickTableButton = 'CLICK_TABLE_BUTTON', HighlightLogsExpression = 'HIGHLIGHT_LOGS_EXPRESSION', InitializeExplore = 'INITIALIZE_EXPLORE', + InitializeExploreSplit = 'INITIALIZE_EXPLORE_SPLIT', LoadDatasourceFailure = 'LOAD_DATASOURCE_FAILURE', LoadDatasourceMissing = 'LOAD_DATASOURCE_MISSING', LoadDatasourcePending = 'LOAD_DATASOURCE_PENDING', @@ -58,6 +63,7 @@ export enum ActionTypes { ScanRange = 'SCAN_RANGE', ScanStart = 'SCAN_START', ScanStop = 'SCAN_STOP', + StateSave = 'STATE_SAVE', } export interface AddQueryRowAction { @@ -123,6 +129,12 @@ export interface ClickTableButtonAction { exploreId: ExploreId; } +export interface HighlightLogsExpressionAction { + type: ActionTypes.HighlightLogsExpression; + exploreId: ExploreId; + expressions: string[]; +} + export interface InitializeExploreAction { type: ActionTypes.InitializeExplore; exploreId: ExploreId; @@ -134,10 +146,8 @@ export interface InitializeExploreAction { range: RawTimeRange; } -export interface HighlightLogsExpressionAction { - type: ActionTypes.HighlightLogsExpression; - exploreId: ExploreId; - expressions: string[]; +export interface InitializeExploreSplitAction { + type: ActionTypes.InitializeExploreSplit; } export interface LoadDatasourceFailureAction { @@ -224,6 +234,10 @@ export interface ScanStopAction { exploreId: ExploreId; } +export interface StateSaveAction { + type: ActionTypes.StateSave; +} + export type Action = | AddQueryRowAction | ChangeQueryAction @@ -238,6 +252,7 @@ export type Action = | ClickTableButtonAction | HighlightLogsExpressionAction | InitializeExploreAction + | InitializeExploreSplitAction | LoadDatasourceFailureAction | LoadDatasourceMissingAction | LoadDatasourcePendingAction @@ -301,15 +316,14 @@ export function clickClear(exploreId: ExploreId): ThunkResult { return dispatch => { dispatch(scanStop(exploreId)); dispatch({ type: ActionTypes.ClickClear, exploreId }); - // TODO save state + dispatch(stateSave()); }; } export function clickCloseSplit(): ThunkResult { return dispatch => { dispatch({ type: ActionTypes.ClickCloseSplit }); - // When closing split, remove URL state for split part - // TODO save state + dispatch(stateSave()); }; } @@ -353,7 +367,7 @@ export function clickSplit(): ThunkResult { initialQueries: leftState.modifiedQueries.slice(), }; dispatch({ type: ActionTypes.ClickSplit, itemState }); - // TODO save state + dispatch(stateSave()); }; } @@ -412,6 +426,12 @@ export function initializeExplore( }; } +export function initializeExploreSplit() { + return async dispatch => { + dispatch({ type: ActionTypes.InitializeExploreSplit }); + }; +} + export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({ type: ActionTypes.LoadDatasourceFailure, exploreId, @@ -733,7 +753,7 @@ export function runQueries(exploreId: ExploreId) { if (showingLogs && supportsLogs) { dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' })); } - // TODO save state + dispatch(stateSave()); }; } @@ -792,3 +812,25 @@ export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkRes export function scanStop(exploreId: ExploreId): ScanStopAction { return { type: ActionTypes.ScanStop, exploreId }; } + +export function stateSave() { + return (dispatch, getState) => { + const { left, right, split } = getState().explore; + const urlStates: { [index: string]: string } = {}; + const leftUrlState: ExploreUrlState = { + datasource: left.datasourceInstance.name, + queries: left.modifiedQueries.map(clearQueryKeys), + range: left.range, + }; + urlStates.left = serializeStateToUrlParam(leftUrlState, true); + if (split) { + const rightUrlState: ExploreUrlState = { + datasource: right.datasourceInstance.name, + queries: right.modifiedQueries.map(clearQueryKeys), + range: right.range, + }; + urlStates.right = serializeStateToUrlParam(rightUrlState, true); + } + dispatch(updateLocation({ query: urlStates })); + }; +} diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index cea4d766c58..dda3b37fdae 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -36,6 +36,7 @@ export interface ExploreItemState { history: HistoryItem[]; initialDatasource?: string; initialQueries: DataQuery[]; + initialized: boolean; logsHighlighterExpressions?: string[]; logsResult?: LogsModel; modifiedQueries: DataQuery[]; @@ -74,6 +75,7 @@ const makeExploreItemState = (): ExploreItemState => ({ exploreDatasources: [], history: [], initialQueries: [], + initialized: false, modifiedQueries: [], queryTransactions: [], queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL }, @@ -89,7 +91,7 @@ const makeExploreItemState = (): ExploreItemState => ({ }); const initialExploreState: ExploreState = { - split: false, + split: null, left: makeExploreItemState(), right: makeExploreItemState(), }; @@ -236,6 +238,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { range, initialDatasource: action.datasource, initialQueries: action.queries, + initialized: true, modifiedQueries: action.queries.slice(), }; } @@ -436,6 +439,13 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp right: action.itemState, }; } + + case ActionTypes.InitializeExploreSplit: { + return { + ...state, + split: true, + }; + } } const { exploreId } = action as any; @@ -447,6 +457,8 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp }; } + console.error('Unhandled action', action.type); + return state; }; From f02f41c9b0d753e00397fb3d26a5e81a3cbb8aad Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sat, 12 Jan 2019 23:28:51 +0100 Subject: [PATCH 022/156] Move types to types/explore --- public/app/features/explore/Explore.tsx | 10 ++- public/app/features/explore/state/actions.ts | 4 +- public/app/features/explore/state/reducers.ts | 50 +---------- public/app/types/explore.ts | 82 ++++++++++++------- public/app/types/index.ts | 2 +- 5 files changed, 64 insertions(+), 84 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index b0a17884fc3..b34986f81ef 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -6,7 +6,14 @@ import { AutoSizer } from 'react-virtualized'; import { RawTimeRange, TimeRange } from '@grafana/ui'; import { DataSourceSelectItem } from 'app/types/datasources'; -import { ExploreUrlState, HistoryItem, QueryTransaction, RangeScanner, ExploreId } from 'app/types/explore'; +import { + ExploreItemState, + ExploreUrlState, + HistoryItem, + QueryTransaction, + RangeScanner, + ExploreId, +} from 'app/types/explore'; import { DataQuery } from 'app/types/series'; import { StoreState } from 'app/types'; import store from 'app/core/store'; @@ -37,7 +44,6 @@ import { scanStart, scanStop, } from './state/actions'; -import { ExploreItemState } from './state/reducers'; import Panel from './Panel'; import QueryRows from './QueryRows'; diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 979c80395c3..26811606bd9 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -21,6 +21,8 @@ import { DataQuery, StoreState } from 'app/types'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { ExploreId, + ExploreItemState, + ExploreUrlState, HistoryItem, RangeScanner, ResultType, @@ -28,10 +30,8 @@ import { QueryTransaction, QueryHint, QueryHintGetter, - ExploreUrlState, } from 'app/types/explore'; import { Emitter } from 'app/core/core'; -import { ExploreItemState } from './reducers'; export enum ActionTypes { AddQueryRow = 'ADD_QUERY_ROW', diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index dda3b37fdae..9474bc717e0 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -1,61 +1,13 @@ -import { RawTimeRange, TimeRange } from '@grafana/ui'; - import { calculateResultsFromQueryTransactions, generateEmptyQuery, getIntervals, ensureQueries, } from 'app/core/utils/explore'; -import { DataSourceSelectItem } from 'app/types/datasources'; -import { HistoryItem, QueryTransaction, QueryIntervals, RangeScanner } from 'app/types/explore'; +import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore'; import { DataQuery } from 'app/types/series'; import { Action, ActionTypes } from './actions'; -import { Emitter } from 'app/core/core'; -import { LogsModel } from 'app/core/logs_model'; -import TableModel from 'app/core/table_model'; - -// TODO move to types - -export interface ExploreState { - split: boolean; - left: ExploreItemState; - right: ExploreItemState; -} - -export interface ExploreItemState { - StartPage?: any; - containerWidth: number; - datasourceInstance: any; - datasourceError: string; - datasourceLoading: boolean | null; - datasourceMissing: boolean; - eventBridge?: Emitter; - exploreDatasources: DataSourceSelectItem[]; - graphResult?: any[]; - history: HistoryItem[]; - initialDatasource?: string; - initialQueries: DataQuery[]; - initialized: boolean; - logsHighlighterExpressions?: string[]; - logsResult?: LogsModel; - modifiedQueries: DataQuery[]; - queryIntervals: QueryIntervals; - queryTransactions: QueryTransaction[]; - requestedDatasourceId?: number; - range: TimeRange | RawTimeRange; - scanner?: RangeScanner; - scanning?: boolean; - scanRange?: RawTimeRange; - showingGraph: boolean; - showingLogs: boolean; - showingStartPage?: boolean; - showingTable: boolean; - supportsGraph: boolean | null; - supportsLogs: boolean | null; - supportsTable: boolean | null; - tableResult?: TableModel; -} export const DEFAULT_RANGE = { from: 'now-6h', diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 525c8f74c81..3cef4124ee4 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -1,9 +1,12 @@ import { Value } from 'slate'; +import { RawTimeRange, TimeRange } from '@grafana/ui'; + +import { Emitter } from 'app/core/core'; +import { LogsModel } from 'app/core/logs_model'; +import TableModel from 'app/core/table_model'; +import { DataSourceSelectItem } from 'app/types/datasources'; import { DataQuery } from './series'; -import { RawTimeRange } from '@grafana/ui'; -import TableModel from 'app/core/table_model'; -import { LogsModel } from 'app/core/logs_model'; export interface CompletionItem { /** @@ -80,6 +83,52 @@ export enum ExploreId { right = 'right', } +export interface ExploreState { + split: boolean; + left: ExploreItemState; + right: ExploreItemState; +} + +export interface ExploreItemState { + StartPage?: any; + containerWidth: number; + datasourceInstance: any; + datasourceError: string; + datasourceLoading: boolean | null; + datasourceMissing: boolean; + eventBridge?: Emitter; + exploreDatasources: DataSourceSelectItem[]; + graphResult?: any[]; + history: HistoryItem[]; + initialDatasource?: string; + initialQueries: DataQuery[]; + initialized: boolean; + logsHighlighterExpressions?: string[]; + logsResult?: LogsModel; + modifiedQueries: DataQuery[]; + queryIntervals: QueryIntervals; + queryTransactions: QueryTransaction[]; + requestedDatasourceId?: number; + range: TimeRange | RawTimeRange; + scanner?: RangeScanner; + scanning?: boolean; + scanRange?: RawTimeRange; + showingGraph: boolean; + showingLogs: boolean; + showingStartPage?: boolean; + showingTable: boolean; + supportsGraph: boolean | null; + supportsLogs: boolean | null; + supportsTable: boolean | null; + tableResult?: TableModel; +} + +export interface ExploreUrlState { + datasource: string; + queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense + range: RawTimeRange; +} + export interface HistoryItem { ts: number; query: DataQuery; @@ -168,31 +217,4 @@ export interface TextMatch { end: number; } -export interface ExploreState { - StartPage?: any; - datasource: any; - datasourceError: any; - graphResult?: any[]; - history: HistoryItem[]; - logsHighlighterExpressions?: string[]; - logsResult?: LogsModel; - queryTransactions: QueryTransaction[]; - scanning?: boolean; - scanRange?: RawTimeRange; - showingGraph: boolean; - showingLogs: boolean; - showingStartPage?: boolean; - showingTable: boolean; - supportsGraph: boolean | null; - supportsLogs: boolean | null; - supportsTable: boolean | null; - tableResult?: TableModel; -} - -export interface ExploreUrlState { - datasource: string; - queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense - range: RawTimeRange; -} - export type ResultType = 'Graph' | 'Logs' | 'Table'; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 018c4c51d3d..ad9f19e2c9f 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -19,7 +19,7 @@ import { } from './appNotifications'; import { DashboardSearchHit } from './search'; import { ValidationEvents, ValidationRule } from './form'; -import { ExploreState } from 'app/features/explore/state/reducers'; +import { ExploreState } from './explore'; export { Team, TeamsState, From 607f7c25de670ba608ea5ba281d6af8a9e5aea6d Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sat, 12 Jan 2019 23:44:24 +0100 Subject: [PATCH 023/156] Update comments --- public/app/features/explore/Explore.tsx | 24 +++++-------------- public/app/features/explore/state/reducers.ts | 15 ++++++++++-- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index b34986f81ef..2e9c71e5517 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -110,26 +110,14 @@ interface ExploreProps { * Once a datasource is selected it populates the query section at the top. * When queries are run, their results are being displayed in the main section. * The datasource determines what kind of query editor it brings, and what kind - * of results viewers it supports. + * of results viewers it supports. The state is managed entirely in Redux. * - * QUERY HANDLING + * SPLIT VIEW * - * TLDR: to not re-render Explore during edits, query editing is not "controlled" - * in a React sense: values need to be pushed down via `initialQueries`, while - * edits travel up via `this.modifiedQueries`. - * - * By default the query rows start without prior state: `initialQueries` will - * contain one empty DataQuery. While the user modifies the DataQuery, the - * modifications are being tracked in `this.modifiedQueries`, which need to be - * used whenever a query is sent to the datasource to reflect what the user sees - * on the screen. Query"react-popper": "^0.7.5", rows can be initialized or reset using `initialQueries`, - * by giving the respec"react-popper": "^0.7.5",tive row a new key. This wipes the old row and its state. - * This property is als"react-popper": "^0.7.5",o used to govern how many query rows there are (minimum 1). - * - * This flow makes sure that a query row can be arbitrarily complex without the - * fear of being wiped or re-initialized via props. The query row is free to keep - * its own state while the user edits or builds a query. Valid queries can be sent - * up to Explore via the `onChangeQuery` prop. + * Explore can have two Explore areas side-by-side. This is handled in `Wrapper.tsx`. + * Since there can be multiple Explores (e.g., left and right) each action needs + * the `exploreId` as first parameter so that the reducer knows which Explore state + * is affected. * * DATASOURCE REQUESTS * diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 9474bc717e0..97a02c33e67 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -17,6 +17,9 @@ export const DEFAULT_RANGE = { // Millies step for helper bar charts const DEFAULT_GRAPH_INTERVAL = 15 * 1000; +/** + * Returns a fresh Explore area state + */ const makeExploreItemState = (): ExploreItemState => ({ StartPage: undefined, containerWidth: 0, @@ -42,12 +45,18 @@ const makeExploreItemState = (): ExploreItemState => ({ supportsTable: null, }); +/** + * Global Explore state that handles multiple Explore areas and the split state + */ const initialExploreState: ExploreState = { split: null, left: makeExploreItemState(), right: makeExploreItemState(), }; +/** + * Reducer for an Explore area, to be used by the global Explore reducer. + */ const itemReducer = (state, action: Action): ExploreItemState => { switch (action.type) { case ActionTypes.AddQueryRow: { @@ -375,6 +384,10 @@ const itemReducer = (state, action: Action): ExploreItemState => { return state; }; +/** + * Global Explore reducer that handles multiple Explore areas (left and right). + * Actions that have an `exploreId` get routed to the ExploreItemReducer. + */ export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => { switch (action.type) { case ActionTypes.ClickCloseSplit: { @@ -409,8 +422,6 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp }; } - console.error('Unhandled action', action.type); - return state; }; From 546a3a9d983c001240dbd6f76c81575cb0b972fc Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sun, 13 Jan 2019 23:10:23 +0100 Subject: [PATCH 024/156] Connect Explore child components to store --- public/app/features/explore/Explore.tsx | 201 ++---------------- .../app/features/explore/GraphContainer.tsx | 61 ++++++ public/app/features/explore/LogsContainer.tsx | 91 ++++++++ public/app/features/explore/QueryRow.tsx | 163 ++++++++++++++ public/app/features/explore/QueryRows.tsx | 152 +------------ .../app/features/explore/TableContainer.tsx | 49 +++++ public/app/features/explore/state/reducers.ts | 1 + 7 files changed, 394 insertions(+), 324 deletions(-) create mode 100644 public/app/features/explore/GraphContainer.tsx create mode 100644 public/app/features/explore/LogsContainer.tsx create mode 100644 public/app/features/explore/QueryRow.tsx create mode 100644 public/app/features/explore/TableContainer.tsx diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 2e9c71e5517..a70135c00ad 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -6,86 +6,58 @@ import { AutoSizer } from 'react-virtualized'; import { RawTimeRange, TimeRange } from '@grafana/ui'; import { DataSourceSelectItem } from 'app/types/datasources'; -import { - ExploreItemState, - ExploreUrlState, - HistoryItem, - QueryTransaction, - RangeScanner, - ExploreId, -} from 'app/types/explore'; +import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore'; import { DataQuery } from 'app/types/series'; import { StoreState } from 'app/types'; import store from 'app/core/store'; import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { Emitter } from 'app/core/utils/emitter'; -import { LogsModel } from 'app/core/logs_model'; -import TableModel from 'app/core/table_model'; import { - addQueryRow, changeDatasource, - changeQuery, changeSize, changeTime, clickClear, clickCloseSplit, clickExample, - clickGraphButton, - clickLogsButton, clickSplit, - clickTableButton, - highlightLogsExpression, initializeExplore, modifyQueries, - removeQueryRow, runQueries, scanStart, scanStop, } from './state/actions'; -import Panel from './Panel'; -import QueryRows from './QueryRows'; -import Graph from './Graph'; -import Logs from './Logs'; -import Table from './Table'; -import ErrorBoundary from './ErrorBoundary'; import { Alert } from './Error'; +import ErrorBoundary from './ErrorBoundary'; +import GraphContainer from './GraphContainer'; +import LogsContainer from './LogsContainer'; +import QueryRows from './QueryRows'; +import TableContainer from './TableContainer'; import TimePicker, { parseTime } from './TimePicker'; interface ExploreProps { StartPage?: any; - addQueryRow: typeof addQueryRow; changeDatasource: typeof changeDatasource; - changeQuery: typeof changeQuery; changeSize: typeof changeSize; changeTime: typeof changeTime; clickClear: typeof clickClear; clickCloseSplit: typeof clickCloseSplit; clickExample: typeof clickExample; - clickGraphButton: typeof clickGraphButton; - clickLogsButton: typeof clickLogsButton; clickSplit: typeof clickSplit; - clickTableButton: typeof clickTableButton; datasourceError: string; datasourceInstance: any; datasourceLoading: boolean | null; datasourceMissing: boolean; exploreDatasources: DataSourceSelectItem[]; exploreId: ExploreId; - graphResult?: any[]; - highlightLogsExpression: typeof highlightLogsExpression; - history: HistoryItem[]; initialDatasource?: string; initialQueries: DataQuery[]; initializeExplore: typeof initializeExplore; initialized: boolean; - logsHighlighterExpressions?: string[]; - logsResult?: LogsModel; + loading: boolean; modifyQueries: typeof modifyQueries; - queryTransactions: QueryTransaction[]; - removeQueryRow: typeof removeQueryRow; range: RawTimeRange; runQueries: typeof runQueries; scanner?: RangeScanner; @@ -94,14 +66,10 @@ interface ExploreProps { scanStart: typeof scanStart; scanStop: typeof scanStop; split: boolean; - showingGraph: boolean; - showingLogs: boolean; showingStartPage?: boolean; - showingTable: boolean; supportsGraph: boolean | null; supportsLogs: boolean | null; supportsTable: boolean | null; - tableResult?: TableModel; urlState: ExploreUrlState; } @@ -172,24 +140,10 @@ export class Explore extends React.PureComponent { this.el = el; }; - onAddQueryRow = index => { - this.props.addQueryRow(this.props.exploreId, index); - }; - onChangeDatasource = async option => { this.props.changeDatasource(this.props.exploreId, option.value); }; - onChangeQuery = (query: DataQuery, index: number, override?: boolean) => { - const { changeQuery, datasourceInstance, exploreId } = this.props; - - changeQuery(exploreId, query, index, override); - if (query && !override && datasourceInstance.getHighlighterExpression && index === 0) { - // Live preview of log search matches. Only use on first row for now - this.updateLogsHighlights(query); - } - }; - onChangeTime = (range: TimeRange, changedByScanner?: boolean) => { if (this.props.scanning && !changedByScanner) { this.onStopScanning(); @@ -205,14 +159,6 @@ export class Explore extends React.PureComponent { this.props.clickCloseSplit(); }; - onClickGraphButton = () => { - this.props.clickGraphButton(this.props.exploreId); - }; - - onClickLogsButton = () => { - this.props.clickLogsButton(this.props.exploreId); - }; - // Use this in help pages to set page to a single query onClickExample = (query: DataQuery) => { this.props.clickExample(this.props.exploreId, query); @@ -222,10 +168,6 @@ export class Explore extends React.PureComponent { this.props.clickSplit(); }; - onClickTableButton = () => { - this.props.clickTableButton(this.props.exploreId); - }; - onClickLabel = (key: string, value: string) => { this.onModifyQueries({ type: 'ADD_FILTER', key, value }); }; @@ -238,10 +180,6 @@ export class Explore extends React.PureComponent { } }; - onRemoveQueryRow = index => { - this.props.removeQueryRow(this.props.exploreId, index); - }; - onResize = (size: { height: number; width: number }) => { this.props.changeSize(this.props.exploreId, size); }; @@ -265,14 +203,6 @@ export class Explore extends React.PureComponent { this.props.runQueries(this.props.exploreId); }; - updateLogsHighlights = _.debounce((value: DataQuery) => { - const { datasourceInstance } = this.props; - if (datasourceInstance.getHighlighterExpression) { - const expressions = [datasourceInstance.getHighlighterExpression(value)]; - this.props.highlightLogsExpression(this.props.exploreId, expressions); - } - }, 500); - render() { const { StartPage, @@ -282,34 +212,19 @@ export class Explore extends React.PureComponent { datasourceMissing, exploreDatasources, exploreId, - graphResult, - history, + loading, initialQueries, - logsHighlighterExpressions, - logsResult, - queryTransactions, range, - scanning, - scanRange, - showingGraph, - showingLogs, showingStartPage, - showingTable, split, supportsGraph, supportsLogs, supportsTable, - tableResult, } = this.props; - const graphHeight = showingGraph && showingTable ? '200px' : '400px'; const exploreClass = split ? 'explore explore-split' : 'explore'; const selectedDatasource = datasourceInstance ? exploreDatasources.find(d => d.name === datasourceInstance.name) : undefined; - const graphLoading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done); - const tableLoading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done); - const logsLoading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done); - const loading = queryTransactions.some(qt => !qt.done); return (
@@ -372,19 +287,7 @@ export class Explore extends React.PureComponent { {datasourceInstance && !datasourceError && (
- + {({ width }) => (
@@ -392,55 +295,16 @@ export class Explore extends React.PureComponent { {showingStartPage && } {!showingStartPage && ( <> - {supportsGraph && ( - - - - )} - {supportsTable && ( - -
- - )} + {supportsGraph && } + {supportsTable && } {supportsLogs && ( - - - + )} )} @@ -466,26 +330,17 @@ function mapStateToProps(state: StoreState, { exploreId }) { datasourceLoading, datasourceMissing, exploreDatasources, - graphResult, initialDatasource, initialQueries, initialized, - history, - logsHighlighterExpressions, - logsResult, queryTransactions, range, - scanning, - scanRange, - showingGraph, - showingLogs, showingStartPage, - showingTable, supportsGraph, supportsLogs, supportsTable, - tableResult, } = item; + const loading = queryTransactions.some(qt => !qt.done); return { StartPage, datasourceError, @@ -493,46 +348,30 @@ function mapStateToProps(state: StoreState, { exploreId }) { datasourceLoading, datasourceMissing, exploreDatasources, - graphResult, initialDatasource, initialQueries, initialized, - history, - logsHighlighterExpressions, - logsResult, + loading, queryTransactions, range, - scanning, - scanRange, - showingGraph, - showingLogs, showingStartPage, - showingTable, split, supportsGraph, supportsLogs, supportsTable, - tableResult, }; } const mapDispatchToProps = { - addQueryRow, changeDatasource, - changeQuery, changeSize, changeTime, clickClear, clickCloseSplit, clickExample, - clickGraphButton, - clickLogsButton, clickSplit, - clickTableButton, - highlightLogsExpression, initializeExplore, modifyQueries, - removeQueryRow, runQueries, scanStart, scanStop, diff --git a/public/app/features/explore/GraphContainer.tsx b/public/app/features/explore/GraphContainer.tsx new file mode 100644 index 00000000000..da098f0c92d --- /dev/null +++ b/public/app/features/explore/GraphContainer.tsx @@ -0,0 +1,61 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import { RawTimeRange, TimeRange } from '@grafana/ui'; + +import { ExploreId, ExploreItemState } from 'app/types/explore'; +import { StoreState } from 'app/types'; + +import { clickGraphButton } from './state/actions'; +import Graph from './Graph'; +import Panel from './Panel'; + +interface GraphContainerProps { + onChangeTime: (range: TimeRange) => void; + clickGraphButton: typeof clickGraphButton; + exploreId: ExploreId; + graphResult?: any[]; + loading: boolean; + range: RawTimeRange; + showingGraph: boolean; + showingTable: boolean; + split: boolean; +} + +export class GraphContainer extends PureComponent { + onClickGraphButton = () => { + this.props.clickGraphButton(this.props.exploreId); + }; + + render() { + const { exploreId, graphResult, loading, onChangeTime, showingGraph, showingTable, range, split } = this.props; + const graphHeight = showingGraph && showingTable ? '200px' : '400px'; + return ( + + + + ); + } +} + +function mapStateToProps(state: StoreState, { exploreId }) { + const explore = state.explore; + const { split } = explore; + const item: ExploreItemState = explore[exploreId]; + const { graphResult, queryTransactions, range, showingGraph, showingTable } = item; + const loading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done); + return { graphResult, loading, range, showingGraph, showingTable, split }; +} + +const mapDispatchToProps = { + clickGraphButton, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(GraphContainer)); diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx new file mode 100644 index 00000000000..db2f681a9c5 --- /dev/null +++ b/public/app/features/explore/LogsContainer.tsx @@ -0,0 +1,91 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import { RawTimeRange, TimeRange } from '@grafana/ui'; + +import { ExploreId, ExploreItemState } from 'app/types/explore'; +import { LogsModel } from 'app/core/logs_model'; +import { StoreState } from 'app/types'; + +import { clickLogsButton } from './state/actions'; +import Logs from './Logs'; +import Panel from './Panel'; + +interface LogsContainerProps { + clickLogsButton: typeof clickLogsButton; + exploreId: ExploreId; + loading: boolean; + logsHighlighterExpressions?: string[]; + logsResult?: LogsModel; + onChangeTime: (range: TimeRange) => void; + onClickLabel: (key: string, value: string) => void; + onStartScanning: () => void; + onStopScanning: () => void; + range: RawTimeRange; + scanning?: boolean; + scanRange?: RawTimeRange; + showingLogs: boolean; +} + +export class LogsContainer extends PureComponent { + onClickLogsButton = () => { + this.props.clickLogsButton(this.props.exploreId); + }; + + render() { + const { + exploreId, + loading, + logsHighlighterExpressions, + logsResult, + onChangeTime, + onClickLabel, + onStartScanning, + onStopScanning, + range, + showingLogs, + scanning, + scanRange, + } = this.props; + return ( + + + + ); + } +} + +function mapStateToProps(state: StoreState, { exploreId }) { + const explore = state.explore; + const item: ExploreItemState = explore[exploreId]; + const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, showingLogs, range } = item; + const loading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done); + return { + loading, + logsHighlighterExpressions, + logsResult, + scanning, + scanRange, + showingLogs, + range, + }; +} + +const mapDispatchToProps = { + clickLogsButton, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer)); diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx new file mode 100644 index 00000000000..b5b150b3ba8 --- /dev/null +++ b/public/app/features/explore/QueryRow.tsx @@ -0,0 +1,163 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import { RawTimeRange } from '@grafana/ui'; +import _ from 'lodash'; + +import { QueryTransaction, HistoryItem, QueryHint, ExploreItemState, ExploreId } from 'app/types/explore'; +import { Emitter } from 'app/core/utils/emitter'; +import { DataQuery, StoreState } from 'app/types'; + +// import DefaultQueryField from './QueryField'; +import QueryEditor from './QueryEditor'; +import QueryTransactionStatus from './QueryTransactionStatus'; +import { + addQueryRow, + changeQuery, + highlightLogsExpression, + modifyQueries, + removeQueryRow, + runQueries, +} from './state/actions'; + +function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint { + const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0); + if (transaction) { + return transaction.hints[0]; + } + return undefined; +} + +interface QueryRowProps { + addQueryRow: typeof addQueryRow; + changeQuery: typeof changeQuery; + className?: string; + exploreId: ExploreId; + datasourceInstance: any; + highlightLogsExpression: typeof highlightLogsExpression; + history: HistoryItem[]; + index: number; + initialQuery: DataQuery; + modifyQueries: typeof modifyQueries; + queryTransactions: QueryTransaction[]; + exploreEvents: Emitter; + range: RawTimeRange; + removeQueryRow: typeof removeQueryRow; + runQueries: typeof runQueries; +} + +export class QueryRow extends PureComponent { + onExecuteQuery = () => { + const { exploreId } = this.props; + this.props.runQueries(exploreId); + }; + + onChangeQuery = (query: DataQuery, override?: boolean) => { + const { datasourceInstance, exploreId, index } = this.props; + this.props.changeQuery(exploreId, query, index, override); + if (query && !override && datasourceInstance.getHighlighterExpression && index === 0) { + // Live preview of log search matches. Only use on first row for now + this.updateLogsHighlights(query); + } + }; + + onClickAddButton = () => { + const { exploreId, index } = this.props; + this.props.addQueryRow(exploreId, index); + }; + + onClickClearButton = () => { + this.onChangeQuery(null, true); + }; + + onClickHintFix = action => { + const { datasourceInstance, exploreId, index } = this.props; + if (datasourceInstance && datasourceInstance.modifyQuery) { + const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action); + this.props.modifyQueries(exploreId, action, index, modifier); + } + }; + + onClickRemoveButton = () => { + const { exploreId, index } = this.props; + this.props.removeQueryRow(exploreId, index); + }; + + updateLogsHighlights = _.debounce((value: DataQuery) => { + const { datasourceInstance } = this.props; + if (datasourceInstance.getHighlighterExpression) { + const expressions = [datasourceInstance.getHighlighterExpression(value)]; + this.props.highlightLogsExpression(this.props.exploreId, expressions); + } + }, 500); + + render() { + const { datasourceInstance, history, index, initialQuery, queryTransactions, exploreEvents, range } = this.props; + const transactions = queryTransactions.filter(t => t.rowIndex === index); + const transactionWithError = transactions.find(t => t.error !== undefined); + const hint = getFirstHintFromTransactions(transactions); + const queryError = transactionWithError ? transactionWithError.error : null; + const QueryField = datasourceInstance.pluginExports.ExploreQueryField; + return ( +
+
+ +
+
+ {QueryField ? ( + + ) : ( + + )} +
+
+ + + +
+
+ ); + } +} + +function mapStateToProps(state: StoreState, { exploreId, index }) { + const explore = state.explore; + const item: ExploreItemState = explore[exploreId]; + const { datasourceInstance, history, initialQueries, queryTransactions, range } = item; + const initialQuery = initialQueries[index]; + return { datasourceInstance, history, initialQuery, queryTransactions, range }; +} + +const mapDispatchToProps = { + addQueryRow, + changeQuery, + highlightLogsExpression, + modifyQueries, + removeQueryRow, + runQueries, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(QueryRow)); diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index 4101475092b..01bd409f444 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -1,159 +1,25 @@ import React, { PureComponent } from 'react'; -import { QueryTransaction, HistoryItem, QueryHint } from 'app/types/explore'; import { Emitter } from 'app/core/utils/emitter'; +import { DataQuery } from 'app/types'; +import { ExploreId } from 'app/types/explore'; -// import DefaultQueryField from './QueryField'; -import QueryEditor from './QueryEditor'; -import QueryTransactionStatus from './QueryTransactionStatus'; -import { DataSource, DataQuery } from 'app/types'; -import { RawTimeRange } from '@grafana/ui'; +import QueryRow from './QueryRow'; -function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint { - const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0); - if (transaction) { - return transaction.hints[0]; - } - return undefined; -} - -interface QueryRowEventHandlers { - onAddQueryRow: (index: number) => void; - onChangeQuery: (value: DataQuery, index: number, override?: boolean) => void; - onClickHintFix: (action: object, index?: number) => void; - onExecuteQuery: () => void; - onRemoveQueryRow: (index: number) => void; -} - -interface QueryRowCommonProps { +interface QueryRowsProps { className?: string; - datasource: DataSource; - history: HistoryItem[]; - transactions: QueryTransaction[]; exploreEvents: Emitter; - range: RawTimeRange; + exploreId: ExploreId; + initialQueries: DataQuery[]; } - -type QueryRowProps = QueryRowCommonProps & - QueryRowEventHandlers & { - index: number; - initialQuery: DataQuery; - }; - -class QueryRow extends PureComponent { - onExecuteQuery = () => { - const { onExecuteQuery } = this.props; - onExecuteQuery(); - }; - - onChangeQuery = (value: DataQuery, override?: boolean) => { - const { index, onChangeQuery } = this.props; - if (onChangeQuery) { - onChangeQuery(value, index, override); - } - }; - - onClickAddButton = () => { - const { index, onAddQueryRow } = this.props; - if (onAddQueryRow) { - onAddQueryRow(index); - } - }; - - onClickClearButton = () => { - this.onChangeQuery(null, true); - }; - - onClickHintFix = action => { - const { index, onClickHintFix } = this.props; - if (onClickHintFix) { - onClickHintFix(action, index); - } - }; - - onClickRemoveButton = () => { - const { index, onRemoveQueryRow } = this.props; - if (onRemoveQueryRow) { - onRemoveQueryRow(index); - } - }; - - onPressEnter = () => { - const { onExecuteQuery } = this.props; - if (onExecuteQuery) { - onExecuteQuery(); - } - }; - - render() { - const { datasource, history, initialQuery, transactions, exploreEvents, range } = this.props; - const transactionWithError = transactions.find(t => t.error !== undefined); - const hint = getFirstHintFromTransactions(transactions); - const queryError = transactionWithError ? transactionWithError.error : null; - const QueryField = datasource.pluginExports.ExploreQueryField; - return ( -
-
- -
-
- {QueryField ? ( - - ) : ( - - )} -
-
- - - -
-
- ); - } -} - -type QueryRowsProps = QueryRowCommonProps & - QueryRowEventHandlers & { - initialQueries: DataQuery[]; - }; - export default class QueryRows extends PureComponent { render() { - const { className = '', initialQueries, transactions, ...handlers } = this.props; + const { className = '', exploreEvents, exploreId, initialQueries } = this.props; return (
{initialQueries.map((query, index) => ( - t.rowIndex === index)} - {...handlers} - /> + // TODO instead of relying on initialQueries, move to react key list in redux + ))}
); diff --git a/public/app/features/explore/TableContainer.tsx b/public/app/features/explore/TableContainer.tsx new file mode 100644 index 00000000000..e510a77c97f --- /dev/null +++ b/public/app/features/explore/TableContainer.tsx @@ -0,0 +1,49 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; + +import { ExploreId, ExploreItemState } from 'app/types/explore'; +import { StoreState } from 'app/types'; + +import { clickTableButton } from './state/actions'; +import Table from './Table'; +import Panel from './Panel'; +import TableModel from 'app/core/table_model'; + +interface TableContainerProps { + clickTableButton: typeof clickTableButton; + exploreId: ExploreId; + loading: boolean; + onClickLabel: (key: string, value: string) => void; + showingTable: boolean; + tableResult?: TableModel; +} + +export class TableContainer extends PureComponent { + onClickTableButton = () => { + this.props.clickTableButton(this.props.exploreId); + }; + + render() { + const { loading, onClickLabel, showingTable, tableResult } = this.props; + return ( + +
+ + ); + } +} + +function mapStateToProps(state: StoreState, { exploreId }) { + const explore = state.explore; + const item: ExploreItemState = explore[exploreId]; + const { queryTransactions, showingTable, tableResult } = item; + const loading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done); + return { loading, showingTable, tableResult }; +} + +const mapDispatchToProps = { + clickTableButton, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer)); diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 97a02c33e67..b8273051ffe 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -232,6 +232,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { initialQueries: action.initialQueries, logsHighlighterExpressions: undefined, modifiedQueries: action.initialQueries.slice(), + queryTransactions: [], showingStartPage: action.showingStartPage, supportsGraph: action.supportsGraph, supportsLogs: action.supportsLogs, From 9aede9e6368947543d6488a0c6f99e4367ba1790 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sun, 13 Jan 2019 23:26:04 +0100 Subject: [PATCH 025/156] Fix reducer issues --- public/app/core/utils/explore.ts | 8 ++++---- public/app/features/explore/QueryEditor.tsx | 2 +- public/app/features/explore/state/reducers.ts | 12 +++++++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index b0dcf2117d0..b05e38a4b33 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -17,7 +17,7 @@ import { QueryIntervals, QueryOptions, } from 'app/types/explore'; -import { DataQuery, DataSourceApi } from 'app/types/series'; +import { DataQuery } from 'app/types/series'; export const DEFAULT_RANGE = { from: 'now-6h', @@ -243,8 +243,8 @@ export function calculateResultsFromQueryTransactions( }; } -export function getIntervals(range: RawTimeRange, datasource: DataSourceApi, resolution: number): IntervalValues { - if (!datasource || !resolution) { +export function getIntervals(range: RawTimeRange, lowLimit: string, resolution: number): IntervalValues { + if (!resolution) { return { interval: '1s', intervalMs: 1000 }; } @@ -253,7 +253,7 @@ export function getIntervals(range: RawTimeRange, datasource: DataSourceApi, res to: parseDate(range.to, true), }; - return kbn.calculateInterval(absoluteRange, resolution, datasource.interval); + return kbn.calculateInterval(absoluteRange, resolution, lowLimit); } export function makeTimeSeriesList(dataList) { diff --git a/public/app/features/explore/QueryEditor.tsx b/public/app/features/explore/QueryEditor.tsx index ce0a8a6e03e..dde674d3fcd 100644 --- a/public/app/features/explore/QueryEditor.tsx +++ b/public/app/features/explore/QueryEditor.tsx @@ -48,7 +48,7 @@ export default class QueryEditor extends PureComponent { getNextQueryLetter: x => '', }, hideEditorRowActions: true, - ...getIntervals(range, datasource, null), // Possible to get resolution? + ...getIntervals(range, (datasource || {}).interval, null), // Possible to get resolution? }, }; diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index b8273051ffe..91d5c4cf925 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -122,11 +122,12 @@ const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.ChangeSize: { const { range, datasourceInstance } = state; - if (!datasourceInstance) { - return state; + let interval = '1s'; + if (datasourceInstance && datasourceInstance.interval) { + interval = datasourceInstance.interval; } const containerWidth = action.width; - const queryIntervals = getIntervals(range, datasourceInstance.interval, containerWidth); + const queryIntervals = getIntervals(range, interval, containerWidth); return { ...state, containerWidth, queryIntervals }; } @@ -189,6 +190,11 @@ const itemReducer = (state, action: Action): ExploreItemState => { return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable }; } + case ActionTypes.HighlightLogsExpression: { + const { expressions } = action; + return { ...state, logsHighlighterExpressions: expressions }; + } + case ActionTypes.InitializeExplore: { const { containerWidth, eventBridge, exploreDatasources, range } = action; return { From 6ff15039a9bcb6fa79f98ad8fb0224f633aa4936 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 15 Jan 2019 19:52:53 +0100 Subject: [PATCH 026/156] File organization, action naming, comments - moved ActionTypes to `./state/actionTypes` - renamed click-related actions - added comments to actions and state types - prefixed Explore actions with `explore/` - fixed query override issue when row was added --- public/app/features/explore/Explore.tsx | 34 +- .../app/features/explore/GraphContainer.tsx | 8 +- public/app/features/explore/LogsContainer.tsx | 8 +- .../app/features/explore/TableContainer.tsx | 14 +- .../app/features/explore/state/actionTypes.ts | 252 +++++++++ public/app/features/explore/state/actions.ts | 498 +++++++----------- public/app/features/explore/state/reducers.ts | 210 ++++---- public/app/types/explore.ts | 117 ++++ 8 files changed, 714 insertions(+), 427 deletions(-) create mode 100644 public/app/features/explore/state/actionTypes.ts diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index a70135c00ad..a8acab50137 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -18,15 +18,15 @@ import { changeDatasource, changeSize, changeTime, - clickClear, - clickCloseSplit, - clickExample, - clickSplit, + clearQueries, initializeExplore, modifyQueries, runQueries, scanStart, scanStop, + setQueries, + splitClose, + splitOpen, } from './state/actions'; import { Alert } from './Error'; @@ -42,10 +42,7 @@ interface ExploreProps { changeDatasource: typeof changeDatasource; changeSize: typeof changeSize; changeTime: typeof changeTime; - clickClear: typeof clickClear; - clickCloseSplit: typeof clickCloseSplit; - clickExample: typeof clickExample; - clickSplit: typeof clickSplit; + clearQueries: typeof clearQueries; datasourceError: string; datasourceInstance: any; datasourceLoading: boolean | null; @@ -65,7 +62,10 @@ interface ExploreProps { scanRange?: RawTimeRange; scanStart: typeof scanStart; scanStop: typeof scanStop; + setQueries: typeof setQueries; split: boolean; + splitClose: typeof splitClose; + splitOpen: typeof splitOpen; showingStartPage?: boolean; supportsGraph: boolean | null; supportsLogs: boolean | null; @@ -152,20 +152,20 @@ export class Explore extends React.PureComponent { }; onClickClear = () => { - this.props.clickClear(this.props.exploreId); + this.props.clearQueries(this.props.exploreId); }; onClickCloseSplit = () => { - this.props.clickCloseSplit(); + this.props.splitClose(); }; // Use this in help pages to set page to a single query onClickExample = (query: DataQuery) => { - this.props.clickExample(this.props.exploreId, query); + this.props.setQueries(this.props.exploreId, [query]); }; onClickSplit = () => { - this.props.clickSplit(); + this.props.splitOpen(); }; onClickLabel = (key: string, value: string) => { @@ -175,7 +175,7 @@ export class Explore extends React.PureComponent { onModifyQueries = (action, index?: number) => { const { datasourceInstance } = this.props; if (datasourceInstance && datasourceInstance.modifyQuery) { - const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action); + const modifier = (queries: DataQuery, modification: any) => datasourceInstance.modifyQuery(queries, modification); this.props.modifyQueries(this.props.exploreId, action, index, modifier); } }; @@ -366,15 +366,15 @@ const mapDispatchToProps = { changeDatasource, changeSize, changeTime, - clickClear, - clickCloseSplit, - clickExample, - clickSplit, + clearQueries, initializeExplore, modifyQueries, runQueries, scanStart, scanStop, + setQueries, + splitClose, + splitOpen, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Explore)); diff --git a/public/app/features/explore/GraphContainer.tsx b/public/app/features/explore/GraphContainer.tsx index da098f0c92d..e2610bcc781 100644 --- a/public/app/features/explore/GraphContainer.tsx +++ b/public/app/features/explore/GraphContainer.tsx @@ -6,13 +6,12 @@ import { RawTimeRange, TimeRange } from '@grafana/ui'; import { ExploreId, ExploreItemState } from 'app/types/explore'; import { StoreState } from 'app/types'; -import { clickGraphButton } from './state/actions'; +import { toggleGraph } from './state/actions'; import Graph from './Graph'; import Panel from './Panel'; interface GraphContainerProps { onChangeTime: (range: TimeRange) => void; - clickGraphButton: typeof clickGraphButton; exploreId: ExploreId; graphResult?: any[]; loading: boolean; @@ -20,11 +19,12 @@ interface GraphContainerProps { showingGraph: boolean; showingTable: boolean; split: boolean; + toggleGraph: typeof toggleGraph; } export class GraphContainer extends PureComponent { onClickGraphButton = () => { - this.props.clickGraphButton(this.props.exploreId); + this.props.toggleGraph(this.props.exploreId); }; render() { @@ -55,7 +55,7 @@ function mapStateToProps(state: StoreState, { exploreId }) { } const mapDispatchToProps = { - clickGraphButton, + toggleGraph, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(GraphContainer)); diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index db2f681a9c5..e58cd2b5e95 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -7,12 +7,11 @@ import { ExploreId, ExploreItemState } from 'app/types/explore'; import { LogsModel } from 'app/core/logs_model'; import { StoreState } from 'app/types'; -import { clickLogsButton } from './state/actions'; +import { toggleLogs } from './state/actions'; import Logs from './Logs'; import Panel from './Panel'; interface LogsContainerProps { - clickLogsButton: typeof clickLogsButton; exploreId: ExploreId; loading: boolean; logsHighlighterExpressions?: string[]; @@ -25,11 +24,12 @@ interface LogsContainerProps { scanning?: boolean; scanRange?: RawTimeRange; showingLogs: boolean; + toggleLogs: typeof toggleLogs; } export class LogsContainer extends PureComponent { onClickLogsButton = () => { - this.props.clickLogsButton(this.props.exploreId); + this.props.toggleLogs(this.props.exploreId); }; render() { @@ -85,7 +85,7 @@ function mapStateToProps(state: StoreState, { exploreId }) { } const mapDispatchToProps = { - clickLogsButton, + toggleLogs, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer)); diff --git a/public/app/features/explore/TableContainer.tsx b/public/app/features/explore/TableContainer.tsx index e510a77c97f..1d00a441e14 100644 --- a/public/app/features/explore/TableContainer.tsx +++ b/public/app/features/explore/TableContainer.tsx @@ -5,30 +5,30 @@ import { connect } from 'react-redux'; import { ExploreId, ExploreItemState } from 'app/types/explore'; import { StoreState } from 'app/types'; -import { clickTableButton } from './state/actions'; +import { toggleGraph } from './state/actions'; import Table from './Table'; import Panel from './Panel'; import TableModel from 'app/core/table_model'; interface TableContainerProps { - clickTableButton: typeof clickTableButton; exploreId: ExploreId; loading: boolean; - onClickLabel: (key: string, value: string) => void; + onClickCell: (key: string, value: string) => void; showingTable: boolean; tableResult?: TableModel; + toggleGraph: typeof toggleGraph; } export class TableContainer extends PureComponent { onClickTableButton = () => { - this.props.clickTableButton(this.props.exploreId); + this.props.toggleGraph(this.props.exploreId); }; render() { - const { loading, onClickLabel, showingTable, tableResult } = this.props; + const { loading, onClickCell, showingTable, tableResult } = this.props; return ( -
+
); } @@ -43,7 +43,7 @@ function mapStateToProps(state: StoreState, { exploreId }) { } const mapDispatchToProps = { - clickTableButton, + toggleGraph, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer)); diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts new file mode 100644 index 00000000000..ed0995cff17 --- /dev/null +++ b/public/app/features/explore/state/actionTypes.ts @@ -0,0 +1,252 @@ +import { RawTimeRange, TimeRange } from '@grafana/ui'; + +import { Emitter } from 'app/core/core'; +import { + ExploreId, + ExploreItemState, + HistoryItem, + RangeScanner, + ResultType, + QueryTransaction, +} from 'app/types/explore'; +import { DataSourceSelectItem } from 'app/types/datasources'; +import { DataQuery } from 'app/types'; + +export enum ActionTypes { + AddQueryRow = 'explore/ADD_QUERY_ROW', + ChangeDatasource = 'explore/CHANGE_DATASOURCE', + ChangeQuery = 'explore/CHANGE_QUERY', + ChangeSize = 'explore/CHANGE_SIZE', + ChangeTime = 'explore/CHANGE_TIME', + ClearQueries = 'explore/CLEAR_QUERIES', + HighlightLogsExpression = 'explore/HIGHLIGHT_LOGS_EXPRESSION', + InitializeExplore = 'explore/INITIALIZE_EXPLORE', + InitializeExploreSplit = 'explore/INITIALIZE_EXPLORE_SPLIT', + LoadDatasourceFailure = 'explore/LOAD_DATASOURCE_FAILURE', + LoadDatasourceMissing = 'explore/LOAD_DATASOURCE_MISSING', + LoadDatasourcePending = 'explore/LOAD_DATASOURCE_PENDING', + LoadDatasourceSuccess = 'explore/LOAD_DATASOURCE_SUCCESS', + ModifyQueries = 'explore/MODIFY_QUERIES', + QueryTransactionFailure = 'explore/QUERY_TRANSACTION_FAILURE', + QueryTransactionStart = 'explore/QUERY_TRANSACTION_START', + QueryTransactionSuccess = 'explore/QUERY_TRANSACTION_SUCCESS', + RemoveQueryRow = 'explore/REMOVE_QUERY_ROW', + RunQueries = 'explore/RUN_QUERIES', + RunQueriesEmpty = 'explore/RUN_QUERIES_EMPTY', + ScanRange = 'explore/SCAN_RANGE', + ScanStart = 'explore/SCAN_START', + ScanStop = 'explore/SCAN_STOP', + SetQueries = 'explore/SET_QUERIES', + SplitClose = 'explore/SPLIT_CLOSE', + SplitOpen = 'explore/SPLIT_OPEN', + StateSave = 'explore/STATE_SAVE', + ToggleGraph = 'explore/TOGGLE_GRAPH', + ToggleLogs = 'explore/TOGGLE_LOGS', + ToggleTable = 'explore/TOGGLE_TABLE', +} + +export interface AddQueryRowAction { + type: ActionTypes.AddQueryRow; + exploreId: ExploreId; + index: number; + query: DataQuery; +} + +export interface ChangeQueryAction { + type: ActionTypes.ChangeQuery; + exploreId: ExploreId; + query: DataQuery; + index: number; + override: boolean; +} + +export interface ChangeSizeAction { + type: ActionTypes.ChangeSize; + exploreId: ExploreId; + width: number; + height: number; +} + +export interface ChangeTimeAction { + type: ActionTypes.ChangeTime; + exploreId: ExploreId; + range: TimeRange; +} + +export interface ClearQueriesAction { + type: ActionTypes.ClearQueries; + exploreId: ExploreId; +} + +export interface HighlightLogsExpressionAction { + type: ActionTypes.HighlightLogsExpression; + exploreId: ExploreId; + expressions: string[]; +} + +export interface InitializeExploreAction { + type: ActionTypes.InitializeExplore; + exploreId: ExploreId; + containerWidth: number; + datasource: string; + eventBridge: Emitter; + exploreDatasources: DataSourceSelectItem[]; + queries: DataQuery[]; + range: RawTimeRange; +} + +export interface InitializeExploreSplitAction { + type: ActionTypes.InitializeExploreSplit; +} + +export interface LoadDatasourceFailureAction { + type: ActionTypes.LoadDatasourceFailure; + exploreId: ExploreId; + error: string; +} + +export interface LoadDatasourcePendingAction { + type: ActionTypes.LoadDatasourcePending; + exploreId: ExploreId; + datasourceId: number; +} + +export interface LoadDatasourceMissingAction { + type: ActionTypes.LoadDatasourceMissing; + exploreId: ExploreId; +} + +export interface LoadDatasourceSuccessAction { + type: ActionTypes.LoadDatasourceSuccess; + exploreId: ExploreId; + StartPage?: any; + datasourceInstance: any; + history: HistoryItem[]; + initialDatasource: string; + initialQueries: DataQuery[]; + logsHighlighterExpressions?: any[]; + showingStartPage: boolean; + supportsGraph: boolean; + supportsLogs: boolean; + supportsTable: boolean; +} + +export interface ModifyQueriesAction { + type: ActionTypes.ModifyQueries; + exploreId: ExploreId; + modification: any; + index: number; + modifier: (queries: DataQuery[], modification: any) => DataQuery[]; +} + +export interface QueryTransactionFailureAction { + type: ActionTypes.QueryTransactionFailure; + exploreId: ExploreId; + queryTransactions: QueryTransaction[]; +} + +export interface QueryTransactionStartAction { + type: ActionTypes.QueryTransactionStart; + exploreId: ExploreId; + resultType: ResultType; + rowIndex: number; + transaction: QueryTransaction; +} + +export interface QueryTransactionSuccessAction { + type: ActionTypes.QueryTransactionSuccess; + exploreId: ExploreId; + history: HistoryItem[]; + queryTransactions: QueryTransaction[]; +} + +export interface RemoveQueryRowAction { + type: ActionTypes.RemoveQueryRow; + exploreId: ExploreId; + index: number; +} + +export interface RunQueriesEmptyAction { + type: ActionTypes.RunQueriesEmpty; + exploreId: ExploreId; +} + +export interface ScanStartAction { + type: ActionTypes.ScanStart; + exploreId: ExploreId; + scanner: RangeScanner; +} + +export interface ScanRangeAction { + type: ActionTypes.ScanRange; + exploreId: ExploreId; + range: RawTimeRange; +} + +export interface ScanStopAction { + type: ActionTypes.ScanStop; + exploreId: ExploreId; +} + +export interface SetQueriesAction { + type: ActionTypes.SetQueries; + exploreId: ExploreId; + queries: DataQuery[]; +} + +export interface SplitCloseAction { + type: ActionTypes.SplitClose; +} + +export interface SplitOpenAction { + type: ActionTypes.SplitOpen; + itemState: ExploreItemState; +} + +export interface StateSaveAction { + type: ActionTypes.StateSave; +} + +export interface ToggleTableAction { + type: ActionTypes.ToggleTable; + exploreId: ExploreId; +} + +export interface ToggleGraphAction { + type: ActionTypes.ToggleGraph; + exploreId: ExploreId; +} + +export interface ToggleLogsAction { + type: ActionTypes.ToggleLogs; + exploreId: ExploreId; +} + +export type Action = + | AddQueryRowAction + | ChangeQueryAction + | ChangeSizeAction + | ChangeTimeAction + | ClearQueriesAction + | HighlightLogsExpressionAction + | InitializeExploreAction + | InitializeExploreSplitAction + | LoadDatasourceFailureAction + | LoadDatasourceMissingAction + | LoadDatasourcePendingAction + | LoadDatasourceSuccessAction + | ModifyQueriesAction + | QueryTransactionFailureAction + | QueryTransactionStartAction + | QueryTransactionSuccessAction + | RemoveQueryRowAction + | RunQueriesEmptyAction + | ScanRangeAction + | ScanStartAction + | ScanStopAction + | SetQueriesAction + | SplitCloseAction + | SplitOpenAction + | ToggleGraphAction + | ToggleLogsAction + | ToggleTableAction; diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 26811606bd9..ecfb35c8c2f 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -21,9 +21,7 @@ import { DataQuery, StoreState } from 'app/types'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { ExploreId, - ExploreItemState, ExploreUrlState, - HistoryItem, RangeScanner, ResultType, QueryOptions, @@ -33,245 +31,33 @@ import { } from 'app/types/explore'; import { Emitter } from 'app/core/core'; -export enum ActionTypes { - AddQueryRow = 'ADD_QUERY_ROW', - ChangeDatasource = 'CHANGE_DATASOURCE', - ChangeQuery = 'CHANGE_QUERY', - ChangeSize = 'CHANGE_SIZE', - ChangeTime = 'CHANGE_TIME', - ClickClear = 'CLICK_CLEAR', - ClickCloseSplit = 'CLICK_CLOSE_SPLIT', - ClickExample = 'CLICK_EXAMPLE', - ClickGraphButton = 'CLICK_GRAPH_BUTTON', - ClickLogsButton = 'CLICK_LOGS_BUTTON', - ClickSplit = 'CLICK_SPLIT', - ClickTableButton = 'CLICK_TABLE_BUTTON', - HighlightLogsExpression = 'HIGHLIGHT_LOGS_EXPRESSION', - InitializeExplore = 'INITIALIZE_EXPLORE', - InitializeExploreSplit = 'INITIALIZE_EXPLORE_SPLIT', - LoadDatasourceFailure = 'LOAD_DATASOURCE_FAILURE', - LoadDatasourceMissing = 'LOAD_DATASOURCE_MISSING', - LoadDatasourcePending = 'LOAD_DATASOURCE_PENDING', - LoadDatasourceSuccess = 'LOAD_DATASOURCE_SUCCESS', - ModifyQueries = 'MODIFY_QUERIES', - QueryTransactionFailure = 'QUERY_TRANSACTION_FAILURE', - QueryTransactionStart = 'QUERY_TRANSACTION_START', - QueryTransactionSuccess = 'QUERY_TRANSACTION_SUCCESS', - RemoveQueryRow = 'REMOVE_QUERY_ROW', - RunQueries = 'RUN_QUERIES', - RunQueriesEmpty = 'RUN_QUERIES', - ScanRange = 'SCAN_RANGE', - ScanStart = 'SCAN_START', - ScanStop = 'SCAN_STOP', - StateSave = 'STATE_SAVE', -} +import { + Action as ThunkableAction, + ActionTypes, + AddQueryRowAction, + ChangeSizeAction, + HighlightLogsExpressionAction, + LoadDatasourceFailureAction, + LoadDatasourceMissingAction, + LoadDatasourcePendingAction, + LoadDatasourceSuccessAction, + QueryTransactionStartAction, + ScanStopAction, +} from './actionTypes'; -export interface AddQueryRowAction { - type: ActionTypes.AddQueryRow; - exploreId: ExploreId; - index: number; - query: DataQuery; -} - -export interface ChangeQueryAction { - type: ActionTypes.ChangeQuery; - exploreId: ExploreId; - query: DataQuery; - index: number; - override: boolean; -} - -export interface ChangeSizeAction { - type: ActionTypes.ChangeSize; - exploreId: ExploreId; - width: number; - height: number; -} - -export interface ChangeTimeAction { - type: ActionTypes.ChangeTime; - exploreId: ExploreId; - range: TimeRange; -} - -export interface ClickClearAction { - type: ActionTypes.ClickClear; - exploreId: ExploreId; -} - -export interface ClickCloseSplitAction { - type: ActionTypes.ClickCloseSplit; -} - -export interface ClickExampleAction { - type: ActionTypes.ClickExample; - exploreId: ExploreId; - query: DataQuery; -} - -export interface ClickGraphButtonAction { - type: ActionTypes.ClickGraphButton; - exploreId: ExploreId; -} - -export interface ClickLogsButtonAction { - type: ActionTypes.ClickLogsButton; - exploreId: ExploreId; -} - -export interface ClickSplitAction { - type: ActionTypes.ClickSplit; - itemState: ExploreItemState; -} - -export interface ClickTableButtonAction { - type: ActionTypes.ClickTableButton; - exploreId: ExploreId; -} - -export interface HighlightLogsExpressionAction { - type: ActionTypes.HighlightLogsExpression; - exploreId: ExploreId; - expressions: string[]; -} - -export interface InitializeExploreAction { - type: ActionTypes.InitializeExplore; - exploreId: ExploreId; - containerWidth: number; - datasource: string; - eventBridge: Emitter; - exploreDatasources: DataSourceSelectItem[]; - queries: DataQuery[]; - range: RawTimeRange; -} - -export interface InitializeExploreSplitAction { - type: ActionTypes.InitializeExploreSplit; -} - -export interface LoadDatasourceFailureAction { - type: ActionTypes.LoadDatasourceFailure; - exploreId: ExploreId; - error: string; -} - -export interface LoadDatasourcePendingAction { - type: ActionTypes.LoadDatasourcePending; - exploreId: ExploreId; - datasourceId: number; -} - -export interface LoadDatasourceMissingAction { - type: ActionTypes.LoadDatasourceMissing; - exploreId: ExploreId; -} - -export interface LoadDatasourceSuccessAction { - type: ActionTypes.LoadDatasourceSuccess; - exploreId: ExploreId; - StartPage?: any; - datasourceInstance: any; - history: HistoryItem[]; - initialDatasource: string; - initialQueries: DataQuery[]; - logsHighlighterExpressions?: any[]; - showingStartPage: boolean; - supportsGraph: boolean; - supportsLogs: boolean; - supportsTable: boolean; -} - -export interface ModifyQueriesAction { - type: ActionTypes.ModifyQueries; - exploreId: ExploreId; - modification: any; - index: number; - modifier: (queries: DataQuery[], modification: any) => DataQuery[]; -} - -export interface QueryTransactionFailureAction { - type: ActionTypes.QueryTransactionFailure; - exploreId: ExploreId; - queryTransactions: QueryTransaction[]; -} - -export interface QueryTransactionStartAction { - type: ActionTypes.QueryTransactionStart; - exploreId: ExploreId; - resultType: ResultType; - rowIndex: number; - transaction: QueryTransaction; -} - -export interface QueryTransactionSuccessAction { - type: ActionTypes.QueryTransactionSuccess; - exploreId: ExploreId; - history: HistoryItem[]; - queryTransactions: QueryTransaction[]; -} - -export interface RemoveQueryRowAction { - type: ActionTypes.RemoveQueryRow; - exploreId: ExploreId; - index: number; -} - -export interface ScanStartAction { - type: ActionTypes.ScanStart; - exploreId: ExploreId; - scanner: RangeScanner; -} - -export interface ScanRangeAction { - type: ActionTypes.ScanRange; - exploreId: ExploreId; - range: RawTimeRange; -} - -export interface ScanStopAction { - type: ActionTypes.ScanStop; - exploreId: ExploreId; -} - -export interface StateSaveAction { - type: ActionTypes.StateSave; -} - -export type Action = - | AddQueryRowAction - | ChangeQueryAction - | ChangeSizeAction - | ChangeTimeAction - | ClickClearAction - | ClickCloseSplitAction - | ClickExampleAction - | ClickGraphButtonAction - | ClickLogsButtonAction - | ClickSplitAction - | ClickTableButtonAction - | HighlightLogsExpressionAction - | InitializeExploreAction - | InitializeExploreSplitAction - | LoadDatasourceFailureAction - | LoadDatasourceMissingAction - | LoadDatasourcePendingAction - | LoadDatasourceSuccessAction - | ModifyQueriesAction - | QueryTransactionFailureAction - | QueryTransactionStartAction - | QueryTransactionSuccessAction - | RemoveQueryRowAction - | ScanRangeAction - | ScanStartAction - | ScanStopAction; -type ThunkResult = ThunkAction; +type ThunkResult = ThunkAction; +/** + * Adds a query row after the row with the given index. + */ export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction { const query = generateEmptyQuery(index + 1); return { type: ActionTypes.AddQueryRow, exploreId, index, query }; } +/** + * Loads a new datasource identified by the given name. + */ export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult { return async dispatch => { const instance = await getDatasourceSrv().get(datasource); @@ -279,6 +65,10 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun }; } +/** + * Query change handler for the query row with the given index. + * If `override` is reset the query modifications and run the queries. Use this to set queries via a link. + */ export function changeQuery( exploreId: ExploreId, query: DataQuery, @@ -298,6 +88,10 @@ export function changeQuery( }; } +/** + * Keep track of the Explore container size, in particular the width. + * The width will be used to calculate graph intervals (number of datapoints). + */ export function changeSize( exploreId: ExploreId, { height, width }: { height: number; width: number } @@ -305,6 +99,9 @@ export function changeSize( return { type: ActionTypes.ChangeSize, exploreId, height, width }; } +/** + * Change the time range of Explore. Usually called from the Timepicker or a graph interaction. + */ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult { return dispatch => { dispatch({ type: ActionTypes.ChangeTime, exploreId, range }); @@ -312,78 +109,28 @@ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult< }; } -export function clickClear(exploreId: ExploreId): ThunkResult { +/** + * Clear all queries and results. + */ +export function clearQueries(exploreId: ExploreId): ThunkResult { return dispatch => { dispatch(scanStop(exploreId)); - dispatch({ type: ActionTypes.ClickClear, exploreId }); + dispatch({ type: ActionTypes.ClearQueries, exploreId }); dispatch(stateSave()); }; } -export function clickCloseSplit(): ThunkResult { - return dispatch => { - dispatch({ type: ActionTypes.ClickCloseSplit }); - dispatch(stateSave()); - }; -} - -export function clickExample(exploreId: ExploreId, rawQuery: DataQuery): ThunkResult { - return dispatch => { - const query = { ...rawQuery, ...generateEmptyQuery() }; - dispatch({ - type: ActionTypes.ClickExample, - exploreId, - query, - }); - dispatch(runQueries(exploreId)); - }; -} - -export function clickGraphButton(exploreId: ExploreId): ThunkResult { - return (dispatch, getState) => { - dispatch({ type: ActionTypes.ClickGraphButton, exploreId }); - if (getState().explore[exploreId].showingGraph) { - dispatch(runQueries(exploreId)); - } - }; -} - -export function clickLogsButton(exploreId: ExploreId): ThunkResult { - return (dispatch, getState) => { - dispatch({ type: ActionTypes.ClickLogsButton, exploreId }); - if (getState().explore[exploreId].showingLogs) { - dispatch(runQueries(exploreId)); - } - }; -} - -export function clickSplit(): ThunkResult { - return (dispatch, getState) => { - // Clone left state to become the right state - const leftState = getState().explore.left; - const itemState = { - ...leftState, - queryTransactions: [], - initialQueries: leftState.modifiedQueries.slice(), - }; - dispatch({ type: ActionTypes.ClickSplit, itemState }); - dispatch(stateSave()); - }; -} - -export function clickTableButton(exploreId: ExploreId): ThunkResult { - return (dispatch, getState) => { - dispatch({ type: ActionTypes.ClickTableButton, exploreId }); - if (getState().explore[exploreId].showingTable) { - dispatch(runQueries(exploreId)); - } - }; -} - +/** + * Highlight expressions in the log results + */ export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction { return { type: ActionTypes.HighlightLogsExpression, exploreId, expressions }; } +/** + * Initialize Explore state with state from the URL and the React component. + * Call this only on components for with the Explore state has not been initialized. + */ export function initializeExplore( exploreId: ExploreId, datasource: string, @@ -426,29 +173,46 @@ export function initializeExplore( }; } +/** + * Initialize the wrapper split state + */ export function initializeExploreSplit() { return async dispatch => { dispatch({ type: ActionTypes.InitializeExploreSplit }); }; } +/** + * Display an error that happened during the selection of a datasource + */ export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({ type: ActionTypes.LoadDatasourceFailure, exploreId, error, }); +/** + * Display an error when no datasources have been configured + */ export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({ type: ActionTypes.LoadDatasourceMissing, exploreId, }); +/** + * Start the async process of loading a datasource to display a loading indicator + */ export const loadDatasourcePending = (exploreId: ExploreId, datasourceId: number): LoadDatasourcePendingAction => ({ type: ActionTypes.LoadDatasourcePending, exploreId, datasourceId, }); +/** + * Datasource loading was successfully completed. The instance is stored in the state as well in case we need to + * run datasource-specific code. Existing queries are imported to the new datasource if an importer exists, + * e.g., Prometheus -> Loki queries. + */ export const loadDatasourceSuccess = ( exploreId: ExploreId, instance: any, @@ -481,6 +245,9 @@ export const loadDatasourceSuccess = ( }; }; +/** + * Main action to asynchronously load a datasource. Dispatches lots of smaller actions for feedback. + */ export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult { return async (dispatch, getState) => { const datasourceId = instance.meta.id; @@ -542,6 +309,13 @@ export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult }; } +/** + * Action to modify a query given a datasource-specific modifier action. + * @param exploreId Explore area + * @param modification Action object with a type, e.g., ADD_FILTER + * @param index Optional query row index. If omitted, the modification is applied to all query rows. + * @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`. + */ export function modifyQueries( exploreId: ExploreId, modification: any, @@ -556,6 +330,10 @@ export function modifyQueries( }; } +/** + * Mark a query transaction as failed with an error extracted from the query response. + * The transaction will be marked as `done`. + */ export function queryTransactionFailure( exploreId: ExploreId, transactionId: string, @@ -614,6 +392,13 @@ export function queryTransactionFailure( }; } +/** + * Start a query transaction for the given result type. + * @param exploreId Explore area + * @param transaction Query options and `done` status. + * @param resultType Associate the transaction with a result viewer, e.g., Graph + * @param rowIndex Index is used to associate latency for this transaction with a query row + */ export function queryTransactionStart( exploreId: ExploreId, transaction: QueryTransaction, @@ -623,6 +408,17 @@ export function queryTransactionStart( return { type: ActionTypes.QueryTransactionStart, exploreId, resultType, rowIndex, transaction }; } +/** + * Complete a query transaction, mark the transaction as `done` and store query state in URL. + * If the transaction was started by a scanner, it keeps on scanning for more results. + * Side-effect: the query is stored in localStorage. + * @param exploreId Explore area + * @param transactionId ID + * @param result Response from `datasourceInstance.query()` + * @param latency Duration between request and response + * @param queries Queries from all query rows + * @param datasourceId Origin datasource instance, used to discard results if current datasource is different + */ export function queryTransactionSuccess( exploreId: ExploreId, transactionId: string, @@ -691,6 +487,9 @@ export function queryTransactionSuccess( }; } +/** + * Remove query row of the given index, as well as associated query results. + */ export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult { return dispatch => { dispatch({ type: ActionTypes.RemoveQueryRow, exploreId, index }); @@ -698,6 +497,9 @@ export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult }; } +/** + * Main action to run queries and dispatches sub-actions based on which result viewers are active + */ export function runQueries(exploreId: ExploreId) { return (dispatch, getState) => { const { @@ -757,6 +559,13 @@ export function runQueries(exploreId: ExploreId) { }; } +/** + * Helper action to build a query transaction object and handing the query to the datasource. + * @param exploreId Explore area + * @param resultType Result viewer that will be associated with this query result + * @param queryOptions Query options as required by the datasource's `query()` function. + * @param resultGetter Optional result extractor, e.g., if the result is a list and you only need the first element. + */ function runQueriesForType( exploreId: ExploreId, resultType: ResultType, @@ -801,18 +610,79 @@ function runQueriesForType( }; } +/** + * Start a scan for more results using the given scanner. + * @param exploreId Explore area + * @param scanner Function that a) returns a new time range and b) triggers a query run for the new range + */ export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult { return dispatch => { + // Register the scanner dispatch({ type: ActionTypes.ScanStart, exploreId, scanner }); + // Scanning must trigger query run, and return the new range const range = scanner(); + // Set the new range to be displayed dispatch({ type: ActionTypes.ScanRange, exploreId, range }); }; } +/** + * Stop any scanning for more results. + */ export function scanStop(exploreId: ExploreId): ScanStopAction { return { type: ActionTypes.ScanStop, exploreId }; } +/** + * Reset queries to the given queries. Any modifications will be discarded. + * Use this action for clicks on query examples. Triggers a query run. + */ +export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult { + return dispatch => { + // Inject react keys into query objects + const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() })); + dispatch({ + type: ActionTypes.SetQueries, + exploreId, + queries, + }); + dispatch(runQueries(exploreId)); + }; +} + +/** + * Close the split view and save URL state. + */ +export function splitClose(): ThunkResult { + return dispatch => { + dispatch({ type: ActionTypes.SplitClose }); + dispatch(stateSave()); + }; +} + +/** + * Open the split view and copy the left state to be the right state. + * The right state is automatically initialized. + * The copy keeps all query modifications but wipes the query results. + */ +export function splitOpen(): ThunkResult { + return (dispatch, getState) => { + // Clone left state to become the right state + const leftState = getState().explore.left; + const itemState = { + ...leftState, + queryTransactions: [], + initialQueries: leftState.modifiedQueries.slice(), + }; + dispatch({ type: ActionTypes.SplitOpen, itemState }); + dispatch(stateSave()); + }; +} + +/** + * Saves Explore state to URL using the `left` and `right` parameters. + * If split view is not active, `right` will not be set. + */ export function stateSave() { return (dispatch, getState) => { const { left, right, split } = getState().explore; @@ -834,3 +704,39 @@ export function stateSave() { dispatch(updateLocation({ query: urlStates })); }; } + +/** + * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run. + */ +export function toggleGraph(exploreId: ExploreId): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ToggleGraph, exploreId }); + if (getState().explore[exploreId].showingGraph) { + dispatch(runQueries(exploreId)); + } + }; +} + +/** + * Expand/collapse the logs result viewer. When collapsed, log queries won't be run. + */ +export function toggleLogs(exploreId: ExploreId): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ToggleLogs, exploreId }); + if (getState().explore[exploreId].showingLogs) { + dispatch(runQueries(exploreId)); + } + }; +} + +/** + * Expand/collapse the table result viewer. When collapsed, table queries won't be run. + */ +export function toggleTable(exploreId: ExploreId): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ToggleTable, exploreId }); + if (getState().explore[exploreId].showingTable) { + dispatch(runQueries(exploreId)); + } + }; +} diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 91d5c4cf925..73790ba14bc 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -7,7 +7,7 @@ import { import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore'; import { DataQuery } from 'app/types/series'; -import { Action, ActionTypes } from './actions'; +import { Action, ActionTypes } from './actionTypes'; export const DEFAULT_RANGE = { from: 'now-6h', @@ -62,14 +62,17 @@ const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.AddQueryRow: { const { initialQueries, modifiedQueries, queryTransactions } = state; const { index, query } = action; - modifiedQueries[index + 1] = query; - const nextQueries = [ - ...initialQueries.slice(0, index + 1), - { ...modifiedQueries[index + 1] }, + // Add new query row after given index, keep modifications of existing rows + const nextModifiedQueries = [ + ...modifiedQueries.slice(0, index + 1), + { ...query }, ...initialQueries.slice(index + 1), ]; + // Add to initialQueries, which will cause a new row to be rendered + const nextQueries = [...initialQueries.slice(0, index + 1), { ...query }, ...initialQueries.slice(index + 1)]; + // Ongoing transactions need to update their row indices const nextQueryTransactions = queryTransactions.map(qt => { if (qt.rowIndex > index) { @@ -83,9 +86,9 @@ const itemReducer = (state, action: Action): ExploreItemState => { return { ...state, - modifiedQueries, initialQueries: nextQueries, logsHighlighterExpressions: undefined, + modifiedQueries: nextModifiedQueries, queryTransactions: nextQueryTransactions, }; } @@ -94,29 +97,33 @@ const itemReducer = (state, action: Action): ExploreItemState => { const { initialQueries, queryTransactions } = state; let { modifiedQueries } = state; const { query, index, override } = action; + + // Fast path: only change modifiedQueries to not trigger an update modifiedQueries[index] = query; - if (override) { - const nextQuery: DataQuery = { - ...query, - ...generateEmptyQuery(index), - }; - const nextQueries = [...initialQueries]; - nextQueries[index] = nextQuery; - modifiedQueries = [...nextQueries]; - - // Discard ongoing transaction related to row query - const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); - + if (!override) { return { ...state, - initialQueries: nextQueries, - modifiedQueries: nextQueries.slice(), - queryTransactions: nextQueryTransactions, + modifiedQueries, }; } + + // Override path: queries are completely reset + const nextQuery: DataQuery = { + ...query, + ...generateEmptyQuery(index), + }; + const nextQueries = [...initialQueries]; + nextQueries[index] = nextQuery; + modifiedQueries = [...nextQueries]; + + // Discard ongoing transaction related to row query + const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + return { ...state, - modifiedQueries, + initialQueries: nextQueries, + modifiedQueries: nextQueries.slice(), + queryTransactions: nextQueryTransactions, }; } @@ -138,58 +145,17 @@ const itemReducer = (state, action: Action): ExploreItemState => { }; } - case ActionTypes.ClickClear: { + case ActionTypes.ClearQueries: { const queries = ensureQueries(); return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice(), + queryTransactions: [], showingStartPage: Boolean(state.StartPage), }; } - case ActionTypes.ClickExample: { - const modifiedQueries = [action.query]; - return { ...state, initialQueries: modifiedQueries.slice(), modifiedQueries }; - } - - case ActionTypes.ClickGraphButton: { - const showingGraph = !state.showingGraph; - let nextQueryTransactions = state.queryTransactions; - if (!showingGraph) { - // Discard transactions related to Graph query - nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph'); - } - return { ...state, queryTransactions: nextQueryTransactions, showingGraph }; - } - - case ActionTypes.ClickLogsButton: { - const showingLogs = !state.showingLogs; - let nextQueryTransactions = state.queryTransactions; - if (!showingLogs) { - // Discard transactions related to Logs query - nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs'); - } - return { ...state, queryTransactions: nextQueryTransactions, showingLogs }; - } - - case ActionTypes.ClickTableButton: { - const showingTable = !state.showingTable; - if (showingTable) { - return { ...state, showingTable, queryTransactions: state.queryTransactions }; - } - - // Toggle off needs discarding of table queries and results - const nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Table'); - const results = calculateResultsFromQueryTransactions( - nextQueryTransactions, - state.datasourceInstance, - state.queryIntervals.intervalMs - ); - - return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable }; - } - case ActionTypes.HighlightLogsExpression: { const { expressions } = action; return { ...state, logsHighlighterExpressions: expressions }; @@ -248,7 +214,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.ModifyQueries: { const { initialQueries, modifiedQueries, queryTransactions } = state; - const { action: modification, index, modifier } = action as any; + const { modification, index, modifier } = action as any; let nextQueries: DataQuery[]; let nextQueryTransactions; if (index === undefined) { @@ -290,37 +256,6 @@ const itemReducer = (state, action: Action): ExploreItemState => { }; } - case ActionTypes.RemoveQueryRow: { - const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state; - let { modifiedQueries } = state; - const { index } = action; - - modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)]; - - if (initialQueries.length <= 1) { - return state; - } - - const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)]; - - // Discard transactions related to row query - const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); - const results = calculateResultsFromQueryTransactions( - nextQueryTransactions, - datasourceInstance, - queryIntervals.intervalMs - ); - - return { - ...state, - ...results, - initialQueries: nextQueries, - logsHighlighterExpressions: undefined, - modifiedQueries: nextQueries.slice(), - queryTransactions: nextQueryTransactions, - }; - } - case ActionTypes.QueryTransactionFailure: { const { queryTransactions } = action; return { @@ -373,6 +308,41 @@ const itemReducer = (state, action: Action): ExploreItemState => { }; } + case ActionTypes.RemoveQueryRow: { + const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state; + let { modifiedQueries } = state; + const { index } = action; + + modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)]; + + if (initialQueries.length <= 1) { + return state; + } + + const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)]; + + // Discard transactions related to row query + const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + const results = calculateResultsFromQueryTransactions( + nextQueryTransactions, + datasourceInstance, + queryIntervals.intervalMs + ); + + return { + ...state, + ...results, + initialQueries: nextQueries, + logsHighlighterExpressions: undefined, + modifiedQueries: nextQueries.slice(), + queryTransactions: nextQueryTransactions, + }; + } + + case ActionTypes.RunQueriesEmpty: { + return { ...state, queryTransactions: [] }; + } + case ActionTypes.ScanRange: { return { ...state, scanRange: action.range }; } @@ -386,6 +356,48 @@ const itemReducer = (state, action: Action): ExploreItemState => { const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done); return { ...state, queryTransactions: nextQueryTransactions, scanning: false, scanRange: undefined }; } + + case ActionTypes.SetQueries: { + const { queries } = action; + return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() }; + } + + case ActionTypes.ToggleGraph: { + const showingGraph = !state.showingGraph; + let nextQueryTransactions = state.queryTransactions; + if (!showingGraph) { + // Discard transactions related to Graph query + nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph'); + } + return { ...state, queryTransactions: nextQueryTransactions, showingGraph }; + } + + case ActionTypes.ToggleLogs: { + const showingLogs = !state.showingLogs; + let nextQueryTransactions = state.queryTransactions; + if (!showingLogs) { + // Discard transactions related to Logs query + nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs'); + } + return { ...state, queryTransactions: nextQueryTransactions, showingLogs }; + } + + case ActionTypes.ToggleTable: { + const showingTable = !state.showingTable; + if (showingTable) { + return { ...state, showingTable, queryTransactions: state.queryTransactions }; + } + + // Toggle off needs discarding of table queries and results + const nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Table'); + const results = calculateResultsFromQueryTransactions( + nextQueryTransactions, + state.datasourceInstance, + state.queryIntervals.intervalMs + ); + + return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable }; + } } return state; @@ -397,14 +409,14 @@ const itemReducer = (state, action: Action): ExploreItemState => { */ export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => { switch (action.type) { - case ActionTypes.ClickCloseSplit: { + case ActionTypes.SplitClose: { return { ...state, split: false, }; } - case ActionTypes.ClickSplit: { + case ActionTypes.SplitOpen: { return { ...state, split: true, diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 3cef4124ee4..5636bb3acdb 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -83,43 +83,160 @@ export enum ExploreId { right = 'right', } +/** + * Global Explore state + */ export interface ExploreState { + /** + * True if split view is active. + */ split: boolean; + /** + * Explore state of the left split (left is default in non-split view). + */ left: ExploreItemState; + /** + * Explore state of the right area in split view. + */ right: ExploreItemState; } export interface ExploreItemState { + /** + * React component to be shown when no queries have been run yet, e.g., for a query language cheat sheet. + */ StartPage?: any; + /** + * Width used for calculating the graph interval (can't have more datapoints than pixels) + */ containerWidth: number; + /** + * Datasource instance that has been selected. Datasource-specific logic can be run on this object. + */ datasourceInstance: any; + /** + * Error to be shown when datasource loading or testing failed. + */ datasourceError: string; + /** + * True if the datasource is loading. `null` if the loading has not started yet. + */ datasourceLoading: boolean | null; + /** + * True if there is no datasource to be selected. + */ datasourceMissing: boolean; + /** + * Emitter to send events to the rest of Grafana. + */ eventBridge?: Emitter; + /** + * List of datasources to be shown in the datasource selector. + */ exploreDatasources: DataSourceSelectItem[]; + /** + * List of timeseries to be shown in the Explore graph result viewer. + */ graphResult?: any[]; + /** + * History of recent queries. Datasource-specific and initialized via localStorage. + */ history: HistoryItem[]; + /** + * Initial datasource for this Explore, e.g., set via URL. + */ initialDatasource?: string; + /** + * Initial queries for this Explore, e.g., set via URL. Each query will be + * converted to a query row. Query edits should be tracked in `modifiedQueries` though. + */ initialQueries: DataQuery[]; + /** + * True if this Explore area has been initialized. + * Used to distinguish URL state injection versus split view state injection. + */ initialized: boolean; + /** + * Log line substrings to be highlighted as you type in a query field. + * Currently supports only the first query row. + */ logsHighlighterExpressions?: string[]; + /** + * Log query result to be displayed in the logs result viewer. + */ logsResult?: LogsModel; + /** + * Copy of `initialQueries` that tracks user edits. + * Don't connect this property to a react component as it is updated on every query change. + * Used when running queries. Needs to be reset to `initialQueries` when those are reset as well. + */ modifiedQueries: DataQuery[]; + /** + * Query intervals for graph queries to determine how many datapoints to return. + * Needs to be updated when `datasourceInstance` or `containerWidth` is changed. + */ queryIntervals: QueryIntervals; + /** + * List of query transaction to track query duration and query result. + * Graph/Logs/Table results are calculated on the fly from the transaction, + * based on the transaction's result types. Transaction also holds the row index + * so that results can be dropped and re-computed without running queries again + * when query rows are removed. + */ queryTransactions: QueryTransaction[]; + /** + * Tracks datasource when selected in the datasource selector. + * Allows the selection to be discarded if something went wrong during the asynchronous + * loading of the datasource. + */ requestedDatasourceId?: number; + /** + * Time range for this Explore. Managed by the time picker and used by all query runs. + */ range: TimeRange | RawTimeRange; + /** + * Scanner function that calculates a new range, triggers a query run, and returns the new range. + */ scanner?: RangeScanner; + /** + * True if scanning for more results is active. + */ scanning?: boolean; + /** + * Current scanning range to be shown to the user while scanning is active. + */ scanRange?: RawTimeRange; + /** + * True if graph result viewer is expanded. Query runs will contain graph queries. + */ showingGraph: boolean; + /** + * True if logs result viewer is expanded. Query runs will contain logs queries. + */ showingLogs: boolean; + /** + * True StartPage needs to be shown. Typically set to `false` once queries have been run. + */ showingStartPage?: boolean; + /** + * True if table result viewer is expanded. Query runs will contain table queries. + */ showingTable: boolean; + /** + * True if `datasourceInstance` supports graph queries. + */ supportsGraph: boolean | null; + /** + * True if `datasourceInstance` supports logs queries. + */ supportsLogs: boolean | null; + /** + * True if `datasourceInstance` supports table queries. + */ supportsTable: boolean | null; + /** + * Table model that combines all query table results into a single table. + */ tableResult?: TableModel; } From a431efa0da03bdf4d6f89f50563dba471e03c7c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 14 Jan 2019 13:50:06 +0100 Subject: [PATCH 027/156] Refactored logic in ThresholdEditor --- .../ThresholdsEditor.test.tsx | 59 +++++++++++++++---- .../ThresholdsEditor/ThresholdsEditor.tsx | 43 +++++++------- 2 files changed, 67 insertions(+), 35 deletions(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx index 14f84e00f80..30a7c8fc257 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -15,25 +15,56 @@ const setup = (propOverrides?: object) => { return shallow().instance() as ThresholdsEditor; }; +describe('Initialization', () => { + it('should add a base threshold if missing', () => { + const instance = setup(); + + expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]); + }); +}); + describe('Add threshold', () => { it('should add threshold', () => { const instance = setup(); - instance.onAddThreshold(0); + instance.onAddThreshold(1); - expect(instance.state.thresholds).toEqual([{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }]); + expect(instance.state.thresholds).toEqual([ + { index: 1, value: 50, color: '#EAB839' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, + ]); }); it('should add another threshold above a first', () => { const instance = setup({ - thresholds: [{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }], + thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }], }); - instance.onAddThreshold(1); + instance.onAddThreshold(2); expect(instance.state.thresholds).toEqual([ - { index: 1, value: 75, color: 'rgb(170, 95, 61)' }, - { index: 0, value: 50, color: 'rgb(127, 115, 64)' }, + { index: 2, value: 75, color: '#6ED0E0' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, + ]); + }); + + it('should add another threshold between first and second index', () => { + const instance = setup({ + thresholds: [ + { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, + ], + }); + + instance.onAddThreshold(2); + + expect(instance.state.thresholds).toEqual([ + { index: 3, value: 75, color: '#EF843C' }, + { index: 2, value: 62.5, color: '#6ED0E0' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, ]); }); }); @@ -41,23 +72,25 @@ describe('Add threshold', () => { describe('change threshold value', () => { it('should update value and resort rows', () => { const instance = setup(); - const mockThresholds = [ - { index: 0, value: 50, color: 'rgba(237, 129, 40, 0.89)' }, - { index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' }, + const thresholds = [ + { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, ]; instance.state = { baseColor: BasicGaugeColor.Green, - thresholds: mockThresholds, + thresholds, }; const mockEvent = { target: { value: 78 } }; - instance.onChangeThresholdValue(mockEvent, mockThresholds[0]); + instance.onChangeThresholdValue(mockEvent, thresholds[1]); expect(instance.state.thresholds).toEqual([ - { index: 0, value: 78, color: 'rgba(237, 129, 40, 0.89)' }, - { index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 78, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, ]); }); }); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index c635b9cb4f5..50986bac5d6 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -1,9 +1,10 @@ import React, { PureComponent } from 'react'; -import tinycolor, { ColorInput } from 'tinycolor2'; +// import tinycolor, { ColorInput } from 'tinycolor2'; import { Threshold, BasicGaugeColor } from '../../types'; import { ColorPicker } from '../ColorPicker/ColorPicker'; import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup'; +import { colors } from '../../utils'; export interface Props { thresholds: Threshold[]; @@ -19,43 +20,41 @@ export class ThresholdsEditor extends PureComponent { constructor(props: Props) { super(props); - this.state = { thresholds: props.thresholds, baseColor: BasicGaugeColor.Green }; + const thresholds: Threshold[] = + props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: colors[0] }]; + this.state = { thresholds, baseColor: BasicGaugeColor.Green }; } onAddThreshold = (index: number) => { - const maxValue = 100; // hardcoded for now before we add the base threshold - const minValue = 0; // hardcoded for now before we add the base threshold const { thresholds } = this.state; + const maxValue = 100; + const minValue = 0; + + if (index === 0) { + return; + } const newThresholds = thresholds.map(threshold => { if (threshold.index >= index) { + const index = threshold.index + 1; threshold = { ...threshold, - index: threshold.index + 1, + index, + color: colors[index], }; } - return threshold; }); // Setting value to a value between the previous thresholds - let value; + const beforeThreshold = newThresholds.filter(threshold => threshold.index === index - 1)[0]; + const afterThreshold = newThresholds.filter(threshold => threshold.index === index + 1)[0]; + const beforeThresholdValue = beforeThreshold !== undefined ? Math.max(beforeThreshold.value, minValue) : minValue; + const afterThresholdValue = afterThreshold !== undefined ? Math.min(afterThreshold.value, maxValue) : maxValue; + const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2; - if (index === 0 && thresholds.length === 0) { - value = maxValue - (maxValue - minValue) / 2; - } else if (index === 0 && thresholds.length > 0) { - value = newThresholds[index + 1].value - (newThresholds[index + 1].value - minValue) / 2; - } else if (index > newThresholds[newThresholds.length - 1].index) { - value = maxValue - (maxValue - newThresholds[index - 1].value) / 2; - } - - // Set a color that lies between the previous thresholds - let color; - if (index === 0 && thresholds.length === 0) { - color = tinycolor.mix(BasicGaugeColor.Green, BasicGaugeColor.Red, 50).toRgbString(); - } else { - color = tinycolor.mix(thresholds[index - 1].color as ColorInput, BasicGaugeColor.Red, 50).toRgbString(); - } + // Set a color + const color = colors[index]; this.setState( { From 7934116e5c1733cbf692c481d4e39dfcbcdfc4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 14 Jan 2019 16:40:44 +0100 Subject: [PATCH 028/156] Fixed styling --- .../ThresholdsEditor.test.tsx | 16 +- .../ThresholdsEditor/ThresholdsEditor.tsx | 118 +++++---------- .../ThresholdsEditor/_ThresholdsEditor.scss | 140 ++++++++---------- 3 files changed, 109 insertions(+), 165 deletions(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx index 30a7c8fc257..f87d56a5bb5 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -19,7 +19,7 @@ describe('Initialization', () => { it('should add a base threshold if missing', () => { const instance = setup(); - expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]); + expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#299c46' }]); }); }); @@ -31,13 +31,13 @@ describe('Add threshold', () => { expect(instance.state.thresholds).toEqual([ { index: 1, value: 50, color: '#EAB839' }, - { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 0, value: -Infinity, color: '#299c46' }, ]); }); it('should add another threshold above a first', () => { const instance = setup({ - thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }], + thresholds: [{ index: 0, value: -Infinity, color: '#299c46' }, { index: 1, value: 50, color: '#EAB839' }], }); instance.onAddThreshold(2); @@ -45,14 +45,14 @@ describe('Add threshold', () => { expect(instance.state.thresholds).toEqual([ { index: 2, value: 75, color: '#6ED0E0' }, { index: 1, value: 50, color: '#EAB839' }, - { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 0, value: -Infinity, color: '#299c46' }, ]); }); it('should add another threshold between first and second index', () => { const instance = setup({ thresholds: [ - { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 0, value: -Infinity, color: '#299c46' }, { index: 1, value: 50, color: '#EAB839' }, { index: 2, value: 75, color: '#6ED0E0' }, ], @@ -64,7 +64,7 @@ describe('Add threshold', () => { { index: 3, value: 75, color: '#EF843C' }, { index: 2, value: 62.5, color: '#6ED0E0' }, { index: 1, value: 50, color: '#EAB839' }, - { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 0, value: -Infinity, color: '#299c46' }, ]); }); }); @@ -73,7 +73,7 @@ describe('change threshold value', () => { it('should update value and resort rows', () => { const instance = setup(); const thresholds = [ - { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 0, value: -Infinity, color: '#299c46' }, { index: 1, value: 50, color: '#EAB839' }, { index: 2, value: 75, color: '#6ED0E0' }, ]; @@ -88,7 +88,7 @@ describe('change threshold value', () => { instance.onChangeThresholdValue(mockEvent, thresholds[1]); expect(instance.state.thresholds).toEqual([ - { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 0, value: -Infinity, color: '#299c46' }, { index: 1, value: 78, color: '#EAB839' }, { index: 2, value: 75, color: '#6ED0E0' }, ]); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index 50986bac5d6..e649629ee8e 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -21,7 +21,7 @@ export class ThresholdsEditor extends PureComponent { super(props); const thresholds: Threshold[] = - props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: colors[0] }]; + props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: '#299c46' }]; this.state = { thresholds, baseColor: BasicGaugeColor.Green }; } @@ -37,11 +37,7 @@ export class ThresholdsEditor extends PureComponent { const newThresholds = thresholds.map(threshold => { if (threshold.index >= index) { const index = threshold.index + 1; - threshold = { - ...threshold, - index, - color: colors[index], - }; + threshold = { ...threshold, index, color: colors[index] }; } return threshold; }); @@ -128,92 +124,52 @@ export class ThresholdsEditor extends PureComponent { }); }; - renderThresholds() { - const { thresholds } = this.state; - - return thresholds.map((threshold, index) => { - return ( -
-
-
- {threshold.color && ( -
- this.onChangeThresholdColor(threshold, color)} - /> -
- )} -
- this.onChangeThresholdValue(event, threshold)} - value={threshold.value} - onBlur={this.onBlur} - /> -
this.onRemoveThreshold(threshold)} className="threshold-row-remove"> - -
-
-
- ); - }); - } - - renderIndicator() { - const { thresholds } = this.state; - - return thresholds.map((t, i) => { - return ( -
-
this.onAddThreshold(t.index + 1)} style={{ height: '50%', backgroundColor: t.color }} /> -
this.onAddThreshold(t.index)} style={{ height: '50%', backgroundColor: t.color }} /> -
- ); - }); - } - - renderBaseIndicator() { + renderInput = (threshold: Threshold) => { + const value = threshold.index === 0 ? 'Base' : threshold.value; return ( -
-
this.onAddThreshold(0)} - style={{ height: '100%', backgroundColor: BasicGaugeColor.Green }} +
+
+ this.onChangeThresholdValue(event, threshold)} + value={value} + onBlur={this.onBlur} + readOnly={threshold.index === 0} /> -
- ); - } - - renderBase() { - const baseColor = BasicGaugeColor.Green; - - return ( -
-
-
-
- this.onChangeBaseColor(color)} /> +
+ {threshold.color && ( +
+ this.onChangeThresholdColor(threshold, color)} />
-
-
Base
+ )}
+ {threshold.index > 0 && ( +
this.onRemoveThreshold(threshold)}> + +
+ )}
); - } + }; render() { + const { thresholds } = this.state; + return (
-
- {this.renderIndicator()} - {this.renderBaseIndicator()} -
-
- {this.renderThresholds()} - {this.renderBase()} -
+ {thresholds.map((threshold, index) => { + return ( +
+
+ this.onAddThreshold(threshold.index + 1)} /> +
+
+
{this.renderInput(threshold)}
+
+ ); + })}
); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss index ff89a6b6ea6..7f77f671fd0 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss +++ b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss @@ -1,38 +1,67 @@ -.thresholds { +.thresholds-row { display: flex; + flex-direction: row; + height: 70px; } -.threshold-rows { - margin-left: 5px; -} - -.threshold-row { - display: flex; - align-items: center; - margin-top: 3px; - padding: 5px; - - &::before { - font-family: 'FontAwesome'; - content: '\f0d9'; - color: $input-label-border-color; - } -} - -.threshold-row-inner { - border: 1px solid $input-label-border-color; - border-radius: $border-radius; - display: flex; +.thresholds-row:last-child > .thresholds-row-color-indicator { + border-bottom-left-radius: $border-radius; + border-bottom-right-radius: $border-radius; overflow: hidden; - height: 37px; - - &--base { - width: auto; - } } -.threshold-row-color { - width: 36px; +.thresholds-row-add-button { + align-self: center; + margin-right: 5px; + color: $green; +} + +.thresholds-row-add-button > i { + cursor: pointer; +} + +.thresholds-row-color-indicator { + width: 20px; +} + +.thresholds-row-input { + margin-top: 51px; + margin-left: 2px; +} + +.thresholds-row-input-inner { + display: flex; + justify-content: center; + flex-direction: row; + height: 37px; +} + +.thresholds-row-input-inner > div:last-child { + border-top-right-radius: $border-radius; + border-bottom-right-radius: $border-radius; +} + +.thresholds-row-input-inner-arrow { + align-self: center; + width: 0; + height: 0; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-right: 5px solid $gray-5; +} + +.thresholds-row-input-inner-value { + border: 1px solid $input-label-border-color; + border-top-left-radius: $border-radius; + border-bottom-left-radius: $border-radius; + padding: 8px 10px; + width: 150px; +} + +.thresholds-row-input-inner-color { + width: 37px; + border-top: 1px solid $input-label-border-color; + border-bottom: 1px solid $input-label-border-color; border-right: 1px solid $input-label-border-color; display: flex; align-items: center; @@ -40,7 +69,7 @@ background-color: $input-bg; } -.threshold-row-color-inner { +.thresholds-row-input-inner-color-colorpicker { border-radius: 10px; overflow: hidden; display: flex; @@ -48,56 +77,15 @@ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25); } -.threshold-row-input { - padding: 8px 10px; - width: 150px; -} - -.threshold-row-label { - background-color: $input-label-bg; - padding: 5px; - display: flex; - align-items: center; -} - -.threshold-row-add-label { - align-items: center; - display: flex; - padding: 5px 8px; -} - -.threshold-row-remove { +.thresholds-row-input-inner-remove { display: flex; align-items: center; justify-content: center; height: 37px; width: 37px; + background-color: $gray-5; + border-top: 1px solid $input-label-border-color; + border-bottom: 1px solid $input-label-border-color; + border-right: 1px solid $input-label-border-color; cursor: pointer; } - -.threshold-row-add { - border-right: $border-width solid $input-label-border-color; - display: flex; - align-items: center; - justify-content: center; - width: 36px; - background-color: $green; -} - -.threshold-row-label { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - -.indicator-section { - width: 100%; - height: 50px; - cursor: pointer; -} - -.color-indicators { - width: 15px; - border-bottom-left-radius: $border-radius; - border-bottom-right-radius: $border-radius; - overflow: hidden; -} From 39c672cb1ff2dd5668fcbf95219d81017bb5a3d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 14 Jan 2019 16:57:54 +0100 Subject: [PATCH 029/156] Fixed the circle --- .../ThresholdsEditor/ThresholdsEditor.tsx | 4 ++-- .../ThresholdsEditor/_ThresholdsEditor.scss | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index e649629ee8e..720c70c3ec5 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -162,8 +162,8 @@ export class ThresholdsEditor extends PureComponent { {thresholds.map((threshold, index) => { return (
-
- this.onAddThreshold(threshold.index + 1)} /> +
this.onAddThreshold(threshold.index + 1)}> +
{this.renderInput(threshold)}
diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss index 7f77f671fd0..9bc79d23e76 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss +++ b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss @@ -1,3 +1,7 @@ +.thresholds { + margin-bottom: 10px; +} + .thresholds-row { display: flex; flex-direction: row; @@ -14,10 +18,18 @@ align-self: center; margin-right: 5px; color: $green; + height: 24px; + width: 24px; + background-color: $green; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; } .thresholds-row-add-button > i { - cursor: pointer; + color: $white; } .thresholds-row-color-indicator { @@ -47,7 +59,7 @@ height: 0; border-top: 5px solid transparent; border-bottom: 5px solid transparent; - border-right: 5px solid $gray-5; + border-right: 5px solid $input-label-border-color; } .thresholds-row-input-inner-value { @@ -83,7 +95,7 @@ justify-content: center; height: 37px; width: 37px; - background-color: $gray-5; + background-color: $input-label-border-color; border-top: 1px solid $input-label-border-color; border-bottom: 1px solid $input-label-border-color; border-right: 1px solid $input-label-border-color; From ad1505b346da7b07f97c480e718724ea19fedd6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 15 Jan 2019 09:35:37 +0100 Subject: [PATCH 030/156] Reordered the input row --- .../ThresholdsEditor.test.tsx | 20 +++++++++++ .../ThresholdsEditor/ThresholdsEditor.tsx | 33 +++++++++++++------ .../ThresholdsEditor/_ThresholdsEditor.scss | 30 +++++++++-------- 3 files changed, 59 insertions(+), 24 deletions(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx index f87d56a5bb5..304e210d3df 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -69,6 +69,26 @@ describe('Add threshold', () => { }); }); +describe('Remove threshold', () => { + it('should remove threshold', () => { + const thresholds = [ + { index: 0, value: -Infinity, color: '#299c46' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, + ]; + const instance = setup({ + thresholds, + }); + + instance.onRemoveThreshold(thresholds[1]); + + expect(instance.state.thresholds).toEqual([ + { index: 0, value: -Infinity, color: '#299c46' }, + { index: 1, value: 75, color: '#6ED0E0' }, + ]); + }); +}); + describe('change threshold value', () => { it('should update value and resort rows', () => { const instance = setup(); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index 720c70c3ec5..8c336f38063 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -69,7 +69,19 @@ export class ThresholdsEditor extends PureComponent { onRemoveThreshold = (threshold: Threshold) => { this.setState( - prevState => ({ thresholds: prevState.thresholds.filter(t => t !== threshold) }), + prevState => { + const newThresholds = prevState.thresholds.map(t => { + if (t.index > threshold.index) { + const index = t.index - 1; + t = { ...t, index }; + } + return t; + }); + + return { + thresholds: newThresholds.filter(t => t !== threshold), + }; + }, () => this.updateGauge() ); }; @@ -128,15 +140,7 @@ export class ThresholdsEditor extends PureComponent { const value = threshold.index === 0 ? 'Base' : threshold.value; return (
-
- this.onChangeThresholdValue(event, threshold)} - value={value} - onBlur={this.onBlur} - readOnly={threshold.index === 0} - /> +
{threshold.color && (
@@ -144,6 +148,15 @@ export class ThresholdsEditor extends PureComponent {
)}
+
+ this.onChangeThresholdValue(event, threshold)} + value={value} + onBlur={this.onBlur} + readOnly={threshold.index === 0} + /> +
{threshold.index > 0 && (
this.onRemoveThreshold(threshold)}> diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss index 9bc79d23e76..809f2936d27 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss +++ b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss @@ -48,7 +48,18 @@ height: 37px; } -.thresholds-row-input-inner > div:last-child { +.thresholds-row-input-inner > div { + border-left: 1px solid $input-label-border-color; + border-top: 1px solid $input-label-border-color; + border-bottom: 1px solid $input-label-border-color; +} + +.thresholds-row-input-inner > *:nth-child(2) { + border-top-left-radius: $border-radius; + border-bottom-left-radius: $border-radius; +} + +.thresholds-row-input-inner > *:last-child { border-top-right-radius: $border-radius; border-bottom-right-radius: $border-radius; } @@ -57,24 +68,18 @@ align-self: center; width: 0; height: 0; - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; - border-right: 5px solid $input-label-border-color; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-right: 6px solid $input-label-border-color; } -.thresholds-row-input-inner-value { - border: 1px solid $input-label-border-color; - border-top-left-radius: $border-radius; - border-bottom-left-radius: $border-radius; +.thresholds-row-input-inner-value > input { padding: 8px 10px; width: 150px; } .thresholds-row-input-inner-color { width: 37px; - border-top: 1px solid $input-label-border-color; - border-bottom: 1px solid $input-label-border-color; - border-right: 1px solid $input-label-border-color; display: flex; align-items: center; justify-content: center; @@ -96,8 +101,5 @@ height: 37px; width: 37px; background-color: $input-label-border-color; - border-top: 1px solid $input-label-border-color; - border-bottom: 1px solid $input-label-border-color; - border-right: 1px solid $input-label-border-color; cursor: pointer; } From 92a1b55a4ac1c52fda81df77d8c9045a9ab8e7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 15 Jan 2019 09:43:02 +0100 Subject: [PATCH 031/156] Fixed styling for small screens --- .../ThresholdsEditor/_ThresholdsEditor.scss | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss index 809f2936d27..50c92a6bcc5 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss +++ b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss @@ -37,7 +37,7 @@ } .thresholds-row-input { - margin-top: 51px; + margin-top: 49px; margin-left: 2px; } @@ -45,7 +45,7 @@ display: flex; justify-content: center; flex-direction: row; - height: 37px; + height: 42px; } .thresholds-row-input-inner > div { @@ -74,12 +74,13 @@ } .thresholds-row-input-inner-value > input { + height: 100%; padding: 8px 10px; width: 150px; } .thresholds-row-input-inner-color { - width: 37px; + width: 42px; display: flex; align-items: center; justify-content: center; @@ -98,8 +99,8 @@ display: flex; align-items: center; justify-content: center; - height: 37px; - width: 37px; + height: 42px; + width: 42px; background-color: $input-label-border-color; cursor: pointer; } From 2836bc2a13b5e8a11344abfa13167b586c07eb69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 15 Jan 2019 10:52:15 +0100 Subject: [PATCH 032/156] Fixed issue with changing value not changing index --- .../ThresholdsEditor.test.tsx | 47 ++++++++++++++++++- .../ThresholdsEditor/ThresholdsEditor.tsx | 15 +++++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx index 304e210d3df..f5a99816c81 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -24,6 +24,14 @@ describe('Initialization', () => { }); describe('Add threshold', () => { + it('should not add threshold at index 0', () => { + const instance = setup(); + + instance.onAddThreshold(0); + + expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#299c46' }]); + }); + it('should add threshold', () => { const instance = setup(); @@ -70,6 +78,19 @@ describe('Add threshold', () => { }); describe('Remove threshold', () => { + it('should not remove threshold at index 0', () => { + const thresholds = [ + { index: 0, value: -Infinity, color: '#299c46' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, + ]; + const instance = setup({ thresholds }); + + instance.onRemoveThreshold(thresholds[0]); + + expect(instance.state.thresholds).toEqual(thresholds); + }); + it('should remove threshold', () => { const thresholds = [ { index: 0, value: -Infinity, color: '#299c46' }, @@ -90,7 +111,7 @@ describe('Remove threshold', () => { }); describe('change threshold value', () => { - it('should update value and resort rows', () => { + it('should update value', () => { const instance = setup(); const thresholds = [ { index: 0, value: -Infinity, color: '#299c46' }, @@ -114,3 +135,27 @@ describe('change threshold value', () => { ]); }); }); + +describe('on blur threshold value', () => { + it('should resort rows and update indexes', () => { + const instance = setup(); + const thresholds = [ + { index: 0, value: -Infinity, color: '#299c46' }, + { index: 1, value: 78, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, + ]; + + instance.state = { + baseColor: BasicGaugeColor.Green, + thresholds, + }; + + instance.onBlur(); + + expect(instance.state.thresholds).toEqual([ + { index: 2, value: 78, color: '#EAB839' }, + { index: 1, value: 75, color: '#6ED0E0' }, + { index: 0, value: -Infinity, color: '#299c46' }, + ]); + }); +}); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index 8c336f38063..0e1b448a95d 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -68,6 +68,10 @@ export class ThresholdsEditor extends PureComponent { }; onRemoveThreshold = (threshold: Threshold) => { + if (threshold.index === 0) { + return; + } + this.setState( prevState => { const newThresholds = prevState.thresholds.map(t => { @@ -91,7 +95,7 @@ export class ThresholdsEditor extends PureComponent { const newThresholds = thresholds.map(t => { if (t === threshold) { - t = { ...t, value: event.target.value }; + t = { ...t, value: parseInt(event.target.value, 10) }; } return t; @@ -121,7 +125,14 @@ export class ThresholdsEditor extends PureComponent { onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds); onBlur = () => { - this.setState(prevState => ({ thresholds: this.sortThresholds(prevState.thresholds) })); + this.setState(prevState => { + const sortThresholds = this.sortThresholds([...prevState.thresholds]); + let index = sortThresholds.length - 1; + sortThresholds.forEach(t => { + t.index = index--; + }); + return { thresholds: sortThresholds }; + }); this.updateGauge(); }; From aaaf3f7ff1f44a276b144ba2379e1c621b86c78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 15 Jan 2019 11:07:06 +0100 Subject: [PATCH 033/156] Fixed so added threshold colors are always unique --- .../ThresholdsEditor.test.tsx | 32 +++++++++---------- .../ThresholdsEditor/ThresholdsEditor.tsx | 6 ++-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx index f5a99816c81..d8e10debf18 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -19,7 +19,7 @@ describe('Initialization', () => { it('should add a base threshold if missing', () => { const instance = setup(); - expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#299c46' }]); + expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]); }); }); @@ -29,7 +29,7 @@ describe('Add threshold', () => { instance.onAddThreshold(0); - expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#299c46' }]); + expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]); }); it('should add threshold', () => { @@ -39,13 +39,13 @@ describe('Add threshold', () => { expect(instance.state.thresholds).toEqual([ { index: 1, value: 50, color: '#EAB839' }, - { index: 0, value: -Infinity, color: '#299c46' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, ]); }); it('should add another threshold above a first', () => { const instance = setup({ - thresholds: [{ index: 0, value: -Infinity, color: '#299c46' }, { index: 1, value: 50, color: '#EAB839' }], + thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }], }); instance.onAddThreshold(2); @@ -53,14 +53,14 @@ describe('Add threshold', () => { expect(instance.state.thresholds).toEqual([ { index: 2, value: 75, color: '#6ED0E0' }, { index: 1, value: 50, color: '#EAB839' }, - { index: 0, value: -Infinity, color: '#299c46' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, ]); }); it('should add another threshold between first and second index', () => { const instance = setup({ thresholds: [ - { index: 0, value: -Infinity, color: '#299c46' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }, { index: 2, value: 75, color: '#6ED0E0' }, ], @@ -69,10 +69,10 @@ describe('Add threshold', () => { instance.onAddThreshold(2); expect(instance.state.thresholds).toEqual([ - { index: 3, value: 75, color: '#EF843C' }, - { index: 2, value: 62.5, color: '#6ED0E0' }, + { index: 3, value: 75, color: '#6ED0E0' }, + { index: 2, value: 62.5, color: '#EF843C' }, { index: 1, value: 50, color: '#EAB839' }, - { index: 0, value: -Infinity, color: '#299c46' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, ]); }); }); @@ -80,7 +80,7 @@ describe('Add threshold', () => { describe('Remove threshold', () => { it('should not remove threshold at index 0', () => { const thresholds = [ - { index: 0, value: -Infinity, color: '#299c46' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }, { index: 2, value: 75, color: '#6ED0E0' }, ]; @@ -93,7 +93,7 @@ describe('Remove threshold', () => { it('should remove threshold', () => { const thresholds = [ - { index: 0, value: -Infinity, color: '#299c46' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }, { index: 2, value: 75, color: '#6ED0E0' }, ]; @@ -104,7 +104,7 @@ describe('Remove threshold', () => { instance.onRemoveThreshold(thresholds[1]); expect(instance.state.thresholds).toEqual([ - { index: 0, value: -Infinity, color: '#299c46' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 75, color: '#6ED0E0' }, ]); }); @@ -114,7 +114,7 @@ describe('change threshold value', () => { it('should update value', () => { const instance = setup(); const thresholds = [ - { index: 0, value: -Infinity, color: '#299c46' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }, { index: 2, value: 75, color: '#6ED0E0' }, ]; @@ -129,7 +129,7 @@ describe('change threshold value', () => { instance.onChangeThresholdValue(mockEvent, thresholds[1]); expect(instance.state.thresholds).toEqual([ - { index: 0, value: -Infinity, color: '#299c46' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 78, color: '#EAB839' }, { index: 2, value: 75, color: '#6ED0E0' }, ]); @@ -140,7 +140,7 @@ describe('on blur threshold value', () => { it('should resort rows and update indexes', () => { const instance = setup(); const thresholds = [ - { index: 0, value: -Infinity, color: '#299c46' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 78, color: '#EAB839' }, { index: 2, value: 75, color: '#6ED0E0' }, ]; @@ -155,7 +155,7 @@ describe('on blur threshold value', () => { expect(instance.state.thresholds).toEqual([ { index: 2, value: 78, color: '#EAB839' }, { index: 1, value: 75, color: '#6ED0E0' }, - { index: 0, value: -Infinity, color: '#299c46' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, ]); }); }); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index 0e1b448a95d..9139ae9c1bc 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -21,7 +21,7 @@ export class ThresholdsEditor extends PureComponent { super(props); const thresholds: Threshold[] = - props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: '#299c46' }]; + props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: colors[0] }]; this.state = { thresholds, baseColor: BasicGaugeColor.Green }; } @@ -37,7 +37,7 @@ export class ThresholdsEditor extends PureComponent { const newThresholds = thresholds.map(threshold => { if (threshold.index >= index) { const index = threshold.index + 1; - threshold = { ...threshold, index, color: colors[index] }; + threshold = { ...threshold, index }; } return threshold; }); @@ -50,7 +50,7 @@ export class ThresholdsEditor extends PureComponent { const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2; // Set a color - const color = colors[index]; + const color = colors.filter(c => newThresholds.some(t => t.color === c) === false)[0]; this.setState( { From 0b1aea905a373ebb1389f9632978214585139a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 15 Jan 2019 11:22:38 +0100 Subject: [PATCH 034/156] Fixed so that we can not change base threshold --- .../ThresholdsEditor/ThresholdsEditor.test.tsx | 15 +++++++++++++++ .../ThresholdsEditor/ThresholdsEditor.tsx | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx index d8e10debf18..e07cf3ae862 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -111,6 +111,21 @@ describe('Remove threshold', () => { }); describe('change threshold value', () => { + it('should not change threshold at index 0', () => { + const thresholds = [ + { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, + ]; + const instance = setup({ thresholds }); + + const mockEvent = { target: { value: 12 } }; + + instance.onChangeThresholdValue(mockEvent, thresholds[0]); + + expect(instance.state.thresholds).toEqual(thresholds); + }); + it('should update value', () => { const instance = setup(); const thresholds = [ diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index 9139ae9c1bc..e2717433f87 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -91,6 +91,10 @@ export class ThresholdsEditor extends PureComponent { }; onChangeThresholdValue = (event: any, threshold: Threshold) => { + if (threshold.index === 0) { + return; + } + const { thresholds } = this.state; const newThresholds = thresholds.map(t => { From 38c0e91666539559aa1d9e8ab6ce31a86951d48c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 15 Jan 2019 11:26:13 +0100 Subject: [PATCH 035/156] Remove BasicGaugeColor from state --- .../components/ThresholdsEditor/ThresholdsEditor.test.tsx | 3 --- .../src/components/ThresholdsEditor/ThresholdsEditor.tsx | 5 ++--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx index e07cf3ae862..845ff5f6bf4 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { shallow } from 'enzyme'; import { ThresholdsEditor, Props } from './ThresholdsEditor'; -import { BasicGaugeColor } from '../../types'; const setup = (propOverrides?: object) => { const props: Props = { @@ -135,7 +134,6 @@ describe('change threshold value', () => { ]; instance.state = { - baseColor: BasicGaugeColor.Green, thresholds, }; @@ -161,7 +159,6 @@ describe('on blur threshold value', () => { ]; instance.state = { - baseColor: BasicGaugeColor.Green, thresholds, }; diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index e2717433f87..87845dedd7a 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; // import tinycolor, { ColorInput } from 'tinycolor2'; -import { Threshold, BasicGaugeColor } from '../../types'; +import { Threshold } from '../../types'; import { ColorPicker } from '../ColorPicker/ColorPicker'; import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup'; import { colors } from '../../utils'; @@ -13,7 +13,6 @@ export interface Props { interface State { thresholds: Threshold[]; - baseColor: string; } export class ThresholdsEditor extends PureComponent { @@ -22,7 +21,7 @@ export class ThresholdsEditor extends PureComponent { const thresholds: Threshold[] = props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: colors[0] }]; - this.state = { thresholds, baseColor: BasicGaugeColor.Green }; + this.state = { thresholds }; } onAddThreshold = (index: number) => { From f16a2c0f48fde1e22ab1ee227007b910fce446b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 15 Jan 2019 19:45:46 +0100 Subject: [PATCH 036/156] Fixed NaN issue when parsing --- .../src/components/ThresholdsEditor/ThresholdsEditor.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index 87845dedd7a..9fee8025ec3 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -95,10 +95,12 @@ export class ThresholdsEditor extends PureComponent { } const { thresholds } = this.state; + const parsedValue = parseInt(event.target.value, 10); + const value = isNaN(parsedValue) ? null : parsedValue; const newThresholds = thresholds.map(t => { if (t === threshold) { - t = { ...t, value: parseInt(event.target.value, 10) }; + t = { ...t, value: value as number }; } return t; From 45a08792befad7f944ea317686758ff7337730bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 16 Jan 2019 09:27:46 +0100 Subject: [PATCH 037/156] Fixed small bug with entries outside the min max values --- .../src/components/ThresholdsEditor/ThresholdsEditor.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index 9fee8025ec3..6e8d88051f3 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -42,10 +42,10 @@ export class ThresholdsEditor extends PureComponent { }); // Setting value to a value between the previous thresholds - const beforeThreshold = newThresholds.filter(threshold => threshold.index === index - 1)[0]; - const afterThreshold = newThresholds.filter(threshold => threshold.index === index + 1)[0]; - const beforeThresholdValue = beforeThreshold !== undefined ? Math.max(beforeThreshold.value, minValue) : minValue; - const afterThresholdValue = afterThreshold !== undefined ? Math.min(afterThreshold.value, maxValue) : maxValue; + const beforeThreshold = newThresholds.filter(t => t.index === index - 1 && t.index !== 0)[0]; + const afterThreshold = newThresholds.filter(t => t.index === index + 1 && t.index !== 0)[0]; + const beforeThresholdValue = beforeThreshold !== undefined ? beforeThreshold.value : minValue; + const afterThresholdValue = afterThreshold !== undefined ? afterThreshold.value : maxValue; const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2; // Set a color From 9575a4a2c0c8ba1b249859b70fc425bf86695c71 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 16 Jan 2019 10:21:11 +0100 Subject: [PATCH 038/156] Move action properties to payload --- .../app/features/explore/state/actionTypes.ts | 180 +++++++++++------- public/app/features/explore/state/actions.ts | 109 ++++++----- public/app/features/explore/state/reducers.ts | 87 +++++---- 3 files changed, 227 insertions(+), 149 deletions(-) diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index ed0995cff17..b267da4f2c1 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -47,52 +47,66 @@ export enum ActionTypes { export interface AddQueryRowAction { type: ActionTypes.AddQueryRow; - exploreId: ExploreId; - index: number; - query: DataQuery; + payload: { + exploreId: ExploreId; + index: number; + query: DataQuery; + }; } export interface ChangeQueryAction { type: ActionTypes.ChangeQuery; - exploreId: ExploreId; - query: DataQuery; - index: number; - override: boolean; + payload: { + exploreId: ExploreId; + query: DataQuery; + index: number; + override: boolean; + }; } export interface ChangeSizeAction { type: ActionTypes.ChangeSize; - exploreId: ExploreId; - width: number; - height: number; + payload: { + exploreId: ExploreId; + width: number; + height: number; + }; } export interface ChangeTimeAction { type: ActionTypes.ChangeTime; - exploreId: ExploreId; - range: TimeRange; + payload: { + exploreId: ExploreId; + range: TimeRange; + }; } export interface ClearQueriesAction { type: ActionTypes.ClearQueries; - exploreId: ExploreId; + payload: { + exploreId: ExploreId; + }; } export interface HighlightLogsExpressionAction { type: ActionTypes.HighlightLogsExpression; - exploreId: ExploreId; - expressions: string[]; + payload: { + exploreId: ExploreId; + expressions: string[]; + }; } export interface InitializeExploreAction { type: ActionTypes.InitializeExplore; - exploreId: ExploreId; - containerWidth: number; - datasource: string; - eventBridge: Emitter; - exploreDatasources: DataSourceSelectItem[]; - queries: DataQuery[]; - range: RawTimeRange; + payload: { + exploreId: ExploreId; + containerWidth: number; + datasource: string; + eventBridge: Emitter; + exploreDatasources: DataSourceSelectItem[]; + queries: DataQuery[]; + range: RawTimeRange; + }; } export interface InitializeExploreSplitAction { @@ -101,97 +115,125 @@ export interface InitializeExploreSplitAction { export interface LoadDatasourceFailureAction { type: ActionTypes.LoadDatasourceFailure; - exploreId: ExploreId; - error: string; + payload: { + exploreId: ExploreId; + error: string; + }; } export interface LoadDatasourcePendingAction { type: ActionTypes.LoadDatasourcePending; - exploreId: ExploreId; - datasourceId: number; + payload: { + exploreId: ExploreId; + datasourceId: number; + }; } export interface LoadDatasourceMissingAction { type: ActionTypes.LoadDatasourceMissing; - exploreId: ExploreId; + payload: { + exploreId: ExploreId; + }; } export interface LoadDatasourceSuccessAction { type: ActionTypes.LoadDatasourceSuccess; - exploreId: ExploreId; - StartPage?: any; - datasourceInstance: any; - history: HistoryItem[]; - initialDatasource: string; - initialQueries: DataQuery[]; - logsHighlighterExpressions?: any[]; - showingStartPage: boolean; - supportsGraph: boolean; - supportsLogs: boolean; - supportsTable: boolean; + payload: { + exploreId: ExploreId; + StartPage?: any; + datasourceInstance: any; + history: HistoryItem[]; + initialDatasource: string; + initialQueries: DataQuery[]; + logsHighlighterExpressions?: any[]; + showingStartPage: boolean; + supportsGraph: boolean; + supportsLogs: boolean; + supportsTable: boolean; + }; } export interface ModifyQueriesAction { type: ActionTypes.ModifyQueries; - exploreId: ExploreId; - modification: any; - index: number; - modifier: (queries: DataQuery[], modification: any) => DataQuery[]; + payload: { + exploreId: ExploreId; + modification: any; + index: number; + modifier: (queries: DataQuery[], modification: any) => DataQuery[]; + }; } export interface QueryTransactionFailureAction { type: ActionTypes.QueryTransactionFailure; - exploreId: ExploreId; - queryTransactions: QueryTransaction[]; + payload: { + exploreId: ExploreId; + queryTransactions: QueryTransaction[]; + }; } export interface QueryTransactionStartAction { type: ActionTypes.QueryTransactionStart; - exploreId: ExploreId; - resultType: ResultType; - rowIndex: number; - transaction: QueryTransaction; + payload: { + exploreId: ExploreId; + resultType: ResultType; + rowIndex: number; + transaction: QueryTransaction; + }; } export interface QueryTransactionSuccessAction { type: ActionTypes.QueryTransactionSuccess; - exploreId: ExploreId; - history: HistoryItem[]; - queryTransactions: QueryTransaction[]; + payload: { + exploreId: ExploreId; + history: HistoryItem[]; + queryTransactions: QueryTransaction[]; + }; } export interface RemoveQueryRowAction { type: ActionTypes.RemoveQueryRow; - exploreId: ExploreId; - index: number; + payload: { + exploreId: ExploreId; + index: number; + }; } export interface RunQueriesEmptyAction { type: ActionTypes.RunQueriesEmpty; - exploreId: ExploreId; + payload: { + exploreId: ExploreId; + }; } export interface ScanStartAction { type: ActionTypes.ScanStart; - exploreId: ExploreId; - scanner: RangeScanner; + payload: { + exploreId: ExploreId; + scanner: RangeScanner; + }; } export interface ScanRangeAction { type: ActionTypes.ScanRange; - exploreId: ExploreId; - range: RawTimeRange; + payload: { + exploreId: ExploreId; + range: RawTimeRange; + }; } export interface ScanStopAction { type: ActionTypes.ScanStop; - exploreId: ExploreId; + payload: { + exploreId: ExploreId; + }; } export interface SetQueriesAction { type: ActionTypes.SetQueries; - exploreId: ExploreId; - queries: DataQuery[]; + payload: { + exploreId: ExploreId; + queries: DataQuery[]; + }; } export interface SplitCloseAction { @@ -200,7 +242,9 @@ export interface SplitCloseAction { export interface SplitOpenAction { type: ActionTypes.SplitOpen; - itemState: ExploreItemState; + payload: { + itemState: ExploreItemState; + }; } export interface StateSaveAction { @@ -209,17 +253,23 @@ export interface StateSaveAction { export interface ToggleTableAction { type: ActionTypes.ToggleTable; - exploreId: ExploreId; + payload: { + exploreId: ExploreId; + }; } export interface ToggleGraphAction { type: ActionTypes.ToggleGraph; - exploreId: ExploreId; + payload: { + exploreId: ExploreId; + }; } export interface ToggleLogsAction { type: ActionTypes.ToggleLogs; - exploreId: ExploreId; + payload: { + exploreId: ExploreId; + }; } export type Action = diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index ecfb35c8c2f..ae0bce6a019 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -52,7 +52,7 @@ type ThunkResult = ThunkAction; */ export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction { const query = generateEmptyQuery(index + 1); - return { type: ActionTypes.AddQueryRow, exploreId, index, query }; + return { type: ActionTypes.AddQueryRow, payload: { exploreId, index, query } }; } /** @@ -81,7 +81,7 @@ export function changeQuery( query = { ...generateEmptyQuery(index) }; } - dispatch({ type: ActionTypes.ChangeQuery, exploreId, query, index, override }); + dispatch({ type: ActionTypes.ChangeQuery, payload: { exploreId, query, index, override } }); if (override) { dispatch(runQueries(exploreId)); } @@ -96,7 +96,7 @@ export function changeSize( exploreId: ExploreId, { height, width }: { height: number; width: number } ): ChangeSizeAction { - return { type: ActionTypes.ChangeSize, exploreId, height, width }; + return { type: ActionTypes.ChangeSize, payload: { exploreId, height, width } }; } /** @@ -104,7 +104,7 @@ export function changeSize( */ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.ChangeTime, exploreId, range }); + dispatch({ type: ActionTypes.ChangeTime, payload: { exploreId, range } }); dispatch(runQueries(exploreId)); }; } @@ -115,7 +115,7 @@ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult< export function clearQueries(exploreId: ExploreId): ThunkResult { return dispatch => { dispatch(scanStop(exploreId)); - dispatch({ type: ActionTypes.ClearQueries, exploreId }); + dispatch({ type: ActionTypes.ClearQueries, payload: { exploreId } }); dispatch(stateSave()); }; } @@ -124,7 +124,7 @@ export function clearQueries(exploreId: ExploreId): ThunkResult { * Highlight expressions in the log results */ export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction { - return { type: ActionTypes.HighlightLogsExpression, exploreId, expressions }; + return { type: ActionTypes.HighlightLogsExpression, payload: { exploreId, expressions } }; } /** @@ -150,13 +150,15 @@ export function initializeExplore( dispatch({ type: ActionTypes.InitializeExplore, - exploreId, - containerWidth, - datasource, - eventBridge, - exploreDatasources, - queries, - range, + payload: { + exploreId, + containerWidth, + datasource, + eventBridge, + exploreDatasources, + queries, + range, + }, }); if (exploreDatasources.length > 1) { @@ -187,8 +189,10 @@ export function initializeExploreSplit() { */ export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({ type: ActionTypes.LoadDatasourceFailure, - exploreId, - error, + payload: { + exploreId, + error, + }, }); /** @@ -196,7 +200,7 @@ export const loadDatasourceFailure = (exploreId: ExploreId, error: string): Load */ export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({ type: ActionTypes.LoadDatasourceMissing, - exploreId, + payload: { exploreId }, }); /** @@ -204,8 +208,10 @@ export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissi */ export const loadDatasourcePending = (exploreId: ExploreId, datasourceId: number): LoadDatasourcePendingAction => ({ type: ActionTypes.LoadDatasourcePending, - exploreId, - datasourceId, + payload: { + exploreId, + datasourceId, + }, }); /** @@ -232,16 +238,18 @@ export const loadDatasourceSuccess = ( return { type: ActionTypes.LoadDatasourceSuccess, - exploreId, - StartPage, - datasourceInstance: instance, - history, - initialDatasource: instance.name, - initialQueries: queries, - showingStartPage: Boolean(StartPage), - supportsGraph, - supportsLogs, - supportsTable, + payload: { + exploreId, + StartPage, + datasourceInstance: instance, + history, + initialDatasource: instance.name, + initialQueries: queries, + showingStartPage: Boolean(StartPage), + supportsGraph, + supportsLogs, + supportsTable, + }, }; }; @@ -323,7 +331,7 @@ export function modifyQueries( modifier: any ): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.ModifyQueries, exploreId, modification, index, modifier }); + dispatch({ type: ActionTypes.ModifyQueries, payload: { exploreId, modification, index, modifier } }); if (!modification.preventSubmit) { dispatch(runQueries(exploreId)); } @@ -349,7 +357,7 @@ export function queryTransactionFailure( // Transaction might have been discarded if (!queryTransactions.find(qt => qt.id === transactionId)) { - return null; + return; } console.error(response); @@ -388,7 +396,10 @@ export function queryTransactionFailure( return qt; }); - dispatch({ type: ActionTypes.QueryTransactionFailure, exploreId, queryTransactions: nextQueryTransactions }); + dispatch({ + type: ActionTypes.QueryTransactionFailure, + payload: { exploreId, queryTransactions: nextQueryTransactions }, + }); }; } @@ -405,7 +416,7 @@ export function queryTransactionStart( resultType: ResultType, rowIndex: number ): QueryTransactionStartAction { - return { type: ActionTypes.QueryTransactionStart, exploreId, resultType, rowIndex, transaction }; + return { type: ActionTypes.QueryTransactionStart, payload: { exploreId, resultType, rowIndex, transaction } }; } /** @@ -466,9 +477,11 @@ export function queryTransactionSuccess( dispatch({ type: ActionTypes.QueryTransactionSuccess, - exploreId, - history: nextHistory, - queryTransactions: nextQueryTransactions, + payload: { + exploreId, + history: nextHistory, + queryTransactions: nextQueryTransactions, + }, }); // Keep scanning for results if this was the last scanning transaction @@ -477,7 +490,7 @@ export function queryTransactionSuccess( const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done); if (!other) { const range = scanner(); - dispatch({ type: ActionTypes.ScanRange, exploreId, range }); + dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } }); } } else { // We can stop scanning if we have a result @@ -492,7 +505,7 @@ export function queryTransactionSuccess( */ export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.RemoveQueryRow, exploreId, index }); + dispatch({ type: ActionTypes.RemoveQueryRow, payload: { exploreId, index } }); dispatch(runQueries(exploreId)); }; } @@ -514,7 +527,7 @@ export function runQueries(exploreId: ExploreId) { } = getState().explore[exploreId]; if (!hasNonEmptyQuery(modifiedQueries)) { - dispatch({ type: ActionTypes.RunQueriesEmpty, exploreId }); + dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } }); return; } @@ -618,11 +631,11 @@ function runQueriesForType( export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult { return dispatch => { // Register the scanner - dispatch({ type: ActionTypes.ScanStart, exploreId, scanner }); + dispatch({ type: ActionTypes.ScanStart, payload: { exploreId, scanner } }); // Scanning must trigger query run, and return the new range const range = scanner(); // Set the new range to be displayed - dispatch({ type: ActionTypes.ScanRange, exploreId, range }); + dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } }); }; } @@ -630,7 +643,7 @@ export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkRes * Stop any scanning for more results. */ export function scanStop(exploreId: ExploreId): ScanStopAction { - return { type: ActionTypes.ScanStop, exploreId }; + return { type: ActionTypes.ScanStop, payload: { exploreId } }; } /** @@ -643,8 +656,10 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() })); dispatch({ type: ActionTypes.SetQueries, - exploreId, - queries, + payload: { + exploreId, + queries, + }, }); dispatch(runQueries(exploreId)); }; @@ -674,7 +689,7 @@ export function splitOpen(): ThunkResult { queryTransactions: [], initialQueries: leftState.modifiedQueries.slice(), }; - dispatch({ type: ActionTypes.SplitOpen, itemState }); + dispatch({ type: ActionTypes.SplitOpen, payload: { itemState } }); dispatch(stateSave()); }; } @@ -710,7 +725,7 @@ export function stateSave() { */ export function toggleGraph(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleGraph, exploreId }); + dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } }); if (getState().explore[exploreId].showingGraph) { dispatch(runQueries(exploreId)); } @@ -722,7 +737,7 @@ export function toggleGraph(exploreId: ExploreId): ThunkResult { */ export function toggleLogs(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleLogs, exploreId }); + dispatch({ type: ActionTypes.ToggleLogs, payload: { exploreId } }); if (getState().explore[exploreId].showingLogs) { dispatch(runQueries(exploreId)); } @@ -734,7 +749,7 @@ export function toggleLogs(exploreId: ExploreId): ThunkResult { */ export function toggleTable(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleTable, exploreId }); + dispatch({ type: ActionTypes.ToggleTable, payload: { exploreId } }); if (getState().explore[exploreId].showingTable) { dispatch(runQueries(exploreId)); } diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 73790ba14bc..b112a5370e3 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -61,7 +61,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { switch (action.type) { case ActionTypes.AddQueryRow: { const { initialQueries, modifiedQueries, queryTransactions } = state; - const { index, query } = action; + const { index, query } = action.payload; // Add new query row after given index, keep modifications of existing rows const nextModifiedQueries = [ @@ -96,7 +96,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.ChangeQuery: { const { initialQueries, queryTransactions } = state; let { modifiedQueries } = state; - const { query, index, override } = action; + const { query, index, override } = action.payload; // Fast path: only change modifiedQueries to not trigger an update modifiedQueries[index] = query; @@ -133,7 +133,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { if (datasourceInstance && datasourceInstance.interval) { interval = datasourceInstance.interval; } - const containerWidth = action.width; + const containerWidth = action.payload.width; const queryIntervals = getIntervals(range, interval, containerWidth); return { ...state, containerWidth, queryIntervals }; } @@ -141,7 +141,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.ChangeTime: { return { ...state, - range: action.range, + range: action.payload.range, }; } @@ -157,27 +157,27 @@ const itemReducer = (state, action: Action): ExploreItemState => { } case ActionTypes.HighlightLogsExpression: { - const { expressions } = action; + const { expressions } = action.payload; return { ...state, logsHighlighterExpressions: expressions }; } case ActionTypes.InitializeExplore: { - const { containerWidth, eventBridge, exploreDatasources, range } = action; + const { containerWidth, datasource, eventBridge, exploreDatasources, queries, range } = action.payload; return { ...state, containerWidth, eventBridge, exploreDatasources, range, - initialDatasource: action.datasource, - initialQueries: action.queries, + initialDatasource: datasource, + initialQueries: queries, initialized: true, - modifiedQueries: action.queries.slice(), + modifiedQueries: queries.slice(), }; } case ActionTypes.LoadDatasourceFailure: { - return { ...state, datasourceError: action.error, datasourceLoading: false }; + return { ...state, datasourceError: action.payload.error, datasourceLoading: false }; } case ActionTypes.LoadDatasourceMissing: { @@ -185,36 +185,47 @@ const itemReducer = (state, action: Action): ExploreItemState => { } case ActionTypes.LoadDatasourcePending: { - return { ...state, datasourceLoading: true, requestedDatasourceId: action.datasourceId }; + return { ...state, datasourceLoading: true, requestedDatasourceId: action.payload.datasourceId }; } case ActionTypes.LoadDatasourceSuccess: { const { containerWidth, range } = state; - const queryIntervals = getIntervals(range, action.datasourceInstance.interval, containerWidth); + const { + StartPage, + datasourceInstance, + history, + initialDatasource, + initialQueries, + showingStartPage, + supportsGraph, + supportsLogs, + supportsTable, + } = action.payload; + const queryIntervals = getIntervals(range, datasourceInstance.interval, containerWidth); return { ...state, queryIntervals, - StartPage: action.StartPage, - datasourceInstance: action.datasourceInstance, + StartPage, + datasourceInstance, + history, + initialDatasource, + initialQueries, + showingStartPage, + supportsGraph, + supportsLogs, + supportsTable, datasourceLoading: false, datasourceMissing: false, - history: action.history, - initialDatasource: action.initialDatasource, - initialQueries: action.initialQueries, logsHighlighterExpressions: undefined, - modifiedQueries: action.initialQueries.slice(), + modifiedQueries: initialQueries.slice(), queryTransactions: [], - showingStartPage: action.showingStartPage, - supportsGraph: action.supportsGraph, - supportsLogs: action.supportsLogs, - supportsTable: action.supportsTable, }; } case ActionTypes.ModifyQueries: { const { initialQueries, modifiedQueries, queryTransactions } = state; - const { modification, index, modifier } = action as any; + const { modification, index, modifier } = action.payload as any; let nextQueries: DataQuery[]; let nextQueryTransactions; if (index === undefined) { @@ -257,7 +268,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { } case ActionTypes.QueryTransactionFailure: { - const { queryTransactions } = action; + const { queryTransactions } = action.payload; return { ...state, queryTransactions, @@ -267,7 +278,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.QueryTransactionStart: { const { datasourceInstance, queryIntervals, queryTransactions } = state; - const { resultType, rowIndex, transaction } = action; + const { resultType, rowIndex, transaction } = action.payload; // Discarding existing transactions of same type const remainingTransactions = queryTransactions.filter( qt => !(qt.resultType === resultType && qt.rowIndex === rowIndex) @@ -292,7 +303,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.QueryTransactionSuccess: { const { datasourceInstance, queryIntervals } = state; - const { history, queryTransactions } = action; + const { history, queryTransactions } = action.payload; const results = calculateResultsFromQueryTransactions( queryTransactions, datasourceInstance, @@ -311,7 +322,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.RemoveQueryRow: { const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state; let { modifiedQueries } = state; - const { index } = action; + const { index } = action.payload; modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)]; @@ -344,7 +355,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { } case ActionTypes.ScanRange: { - return { ...state, scanRange: action.range }; + return { ...state, scanRange: action.payload.range }; } case ActionTypes.ScanStart: { @@ -358,7 +369,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { } case ActionTypes.SetQueries: { - const { queries } = action; + const { queries } = action.payload; return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() }; } @@ -420,7 +431,7 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp return { ...state, split: true, - right: action.itemState, + right: action.payload.itemState, }; } @@ -432,13 +443,15 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp } } - const { exploreId } = action as any; - if (exploreId !== undefined) { - const exploreItemState = state[exploreId]; - return { - ...state, - [exploreId]: itemReducer(exploreItemState, action), - }; + if (action.payload) { + const { exploreId } = action.payload as any; + if (exploreId !== undefined) { + const exploreItemState = state[exploreId]; + return { + ...state, + [exploreId]: itemReducer(exploreItemState, action), + }; + } } return state; From b3512f43a37953ba5dd4c7d1c4db47b611528d4e Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 16 Jan 2019 11:11:00 +0100 Subject: [PATCH 039/156] build: repo update testable and more robus. - adds script for integration testing - package path parameterized - more robust updates --- .circleci/config.yml | 8 +++--- scripts/build/update_repo/init-deb-repo.sh | 12 ++++++++ .../build/update_repo/test-update-deb-repo.sh | 5 ++++ scripts/build/update_repo/update-deb.sh | 28 +++++++++++-------- scripts/build/update_repo/update-rpm.sh | 12 ++++---- 5 files changed, 45 insertions(+), 20 deletions(-) create mode 100755 scripts/build/update_repo/init-deb-repo.sh create mode 100755 scripts/build/update_repo/test-update-deb-repo.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index ec1fcfb411f..509dce3d761 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -370,10 +370,10 @@ jobs: command: './scripts/build/load-signing-key.sh' - run: name: Update Debian repository - command: './scripts/build/update_repo/update-deb.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' + command: './scripts/build/update_repo/update-deb.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "enterprise-dist"' - run: name: Update RPM repository - command: './scripts/build/update_repo/update-rpm.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' + command: './scripts/build/update_repo/update-rpm.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "enterprise-dist"' deploy-master: @@ -433,10 +433,10 @@ jobs: command: './scripts/build/load-signing-key.sh' - run: name: Update Debian repository - command: './scripts/build/update_repo/update-deb.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' + command: './scripts/build/update_repo/update-deb.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "dist"' - run: name: Update RPM repository - command: './scripts/build/update_repo/update-rpm.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' + command: './scripts/build/update_repo/update-rpm.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "dist"' workflows: version: 2 diff --git a/scripts/build/update_repo/init-deb-repo.sh b/scripts/build/update_repo/init-deb-repo.sh new file mode 100755 index 00000000000..2b245dc2d42 --- /dev/null +++ b/scripts/build/update_repo/init-deb-repo.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# Run this if you need to recreate the debian repository for some reason + +# Setup environment +cp scripts/build/update_repo/aptly.conf /etc/aptly.conf +mkdir -p /deb-repo/db \ + /deb-repo/repo \ + /deb-repo/tmp + +aptly repo create -distribution=stable -component=main grafana +aptly repo create -distribution=beta -component=main beta diff --git a/scripts/build/update_repo/test-update-deb-repo.sh b/scripts/build/update_repo/test-update-deb-repo.sh new file mode 100755 index 00000000000..f27e9bec265 --- /dev/null +++ b/scripts/build/update_repo/test-update-deb-repo.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +GPG_PASS=${1:-} + +./scripts/build/update_repo/update-deb.sh "oss" "$GPG_PASS" "v5.4.3" "dist" "grafana-testing-aptly-db" "grafana-testing-repo" diff --git a/scripts/build/update_repo/update-deb.sh b/scripts/build/update_repo/update-deb.sh index 89c5937b064..0f80de9674c 100755 --- a/scripts/build/update_repo/update-deb.sh +++ b/scripts/build/update_repo/update-deb.sh @@ -3,10 +3,14 @@ RELEASE_TYPE="${1:-}" GPG_PASS="${2:-}" RELEASE_TAG="${3:-}" +DIST_PATH="${4:-}" +GCP_DB_BUCKET="${5:-grafana-aptly-db}" +GCP_REPO_BUCKET="${6:-grafana-repo}" + REPO="grafana" -if [ -z "$RELEASE_TYPE" -o -z "$GPG_PASS" ]; then - echo "Both RELEASE_TYPE (arg 1) and GPG_PASS (arg 2) has to be set" +if [ -z "$RELEASE_TYPE" -o -z "$GPG_PASS" -o -z "$DIST_PATH" ]; then + echo "Both RELEASE_TYPE (arg 1), GPG_PASS (arg 2) and DIST_PATH (arg 4) has to be set" exit 1 fi @@ -28,30 +32,32 @@ mkdir -p /deb-repo/db \ /deb-repo/tmp # Download the database -gsutil -m rsync -r "gs://grafana-aptly-db/$RELEASE_TYPE" /deb-repo/db +gsutil -m rsync -r -d "gs://$GCP_DB_BUCKET/$RELEASE_TYPE" /deb-repo/db # Add the new release to the repo -aptly publish drop grafana filesystem:repo:grafana || true -aptly publish drop beta filesystem:repo:grafana || true -cp ./dist/*.deb /deb-repo/tmp +cp $DIST_PATH/*.deb /deb-repo/tmp rm /deb-repo/tmp/grafana_latest*.deb || true -aptly repo add "$REPO" ./dist +aptly repo add "$REPO" /deb-repo/tmp #adds too many packages in enterprise # Setup signing and sign the repo echo "allow-loopback-pinentry" > ~/.gnupg/gpg-agent.conf echo "pinentry-mode loopback" > ~/.gnupg/gpg.conf +pkill gpg-agent || true touch /tmp/sign-this +rm /tmp/sign-this.asc || true ./scripts/build/update_repo/unlock-gpg-key.sh "$GPG_PASS" rm /tmp/sign-this /tmp/sign-this.asc -aptly publish repo grafana filesystem:repo:grafana -aptly publish repo beta filesystem:repo:grafana +aptly publish update stable filesystem:repo:grafana +aptly publish update beta filesystem:repo:grafana # Update the repo and db on gcp -gsutil -m rsync -r -d /deb-repo/db "gs://grafana-aptly-db/$RELEASE_TYPE" -gsutil -m rsync -r -d /deb-repo/repo/grafana "gs://grafana-repo/$RELEASE_TYPE/deb" +## TODO: need to update this to push the binaries first and then the metadata so that we dont cache the binaries missing. + +gsutil -m rsync -r -d /deb-repo/db "gs://$GCP_DB_BUCKET/$RELEASE_TYPE" +gsutil -m rsync -r -d /deb-repo/repo/grafana "gs://$GCP_REPO_BUCKET/$RELEASE_TYPE/deb" # usage: # diff --git a/scripts/build/update_repo/update-rpm.sh b/scripts/build/update_repo/update-rpm.sh index caed3918216..7b28412df37 100755 --- a/scripts/build/update_repo/update-rpm.sh +++ b/scripts/build/update_repo/update-rpm.sh @@ -2,12 +2,13 @@ RELEASE_TYPE="${1:-}" GPG_PASS="${2:-}" - RELEASE_TAG="${3:-}" +DIST_PATH="${4:-}" + REPO="rpm" -if [ -z "$RELEASE_TYPE" -o -z "$GPG_PASS" ]; then - echo "Both RELEASE_TYPE (arg 1) and GPG_PASS (arg 2) has to be set" +if [ -z "$RELEASE_TYPE" -o -z "$GPG_PASS" -o -z "$DIST_PATH" ]; then + echo "Both RELEASE_TYPE (arg 1), GPG_PASS (arg 2) and DIST_PATH (arg 4) has to be set" exit 1 fi @@ -30,10 +31,11 @@ mkdir -p /rpm-repo gsutil -m rsync -r "$BUCKET" /rpm-repo # Add the new release to the repo -cp ./dist/*.rpm /rpm-repo +cp $DIST_PATH/*.rpm /rpm-repo # adds to many files for enterprise rm /rpm-repo/grafana-latest-1*.rpm || true cd /rpm-repo createrepo . +cd /go/src/github.com/grafana/grafana # Setup signing and sign the repo @@ -56,4 +58,4 @@ gsutil -m rsync -r -d /rpm-repo "$BUCKET" # gpgcheck=1 # gpgkey=https://packages.grafana.com/gpg.key # sslverify=1 -# sslcacert=/etc/pki/tls/certs/ca-bundle.crt \ No newline at end of file +# sslcacert=/etc/pki/tls/certs/ca-bundle.crt From 639dc6c3c35df2c7aa9558f80da917aaa027d0a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 16 Jan 2019 10:28:05 +0100 Subject: [PATCH 040/156] Moved Label to grafana/ui/components --- .../grafana-ui/src}/components/Label/Label.tsx | 2 +- packages/grafana-ui/src/components/index.ts | 1 + .../core/components/SharedPreferences/SharedPreferences.tsx | 3 +-- public/app/features/datasources/settings/BasicSettings.tsx | 3 ++- public/app/features/teams/TeamSettings.tsx | 2 +- public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx | 3 +-- public/app/plugins/panel/gauge/MappingRow.tsx | 4 +--- public/app/plugins/panel/gauge/ValueOptions.tsx | 4 +--- 8 files changed, 9 insertions(+), 13 deletions(-) rename {public/app/core => packages/grafana-ui/src}/components/Label/Label.tsx (93%) diff --git a/public/app/core/components/Label/Label.tsx b/packages/grafana-ui/src/components/Label/Label.tsx similarity index 93% rename from public/app/core/components/Label/Label.tsx rename to packages/grafana-ui/src/components/Label/Label.tsx index 5d60efa056a..270b0161226 100644 --- a/public/app/core/components/Label/Label.tsx +++ b/packages/grafana-ui/src/components/Label/Label.tsx @@ -1,5 +1,5 @@ import React, { SFC, ReactNode } from 'react'; -import { Tooltip } from '@grafana/ui'; +import { Tooltip } from '../Tooltip/Tooltip'; interface Props { tooltip?: string; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 5420fcf14b7..51b601f2921 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -2,6 +2,7 @@ export { DeleteButton } from './DeleteButton/DeleteButton'; export { Tooltip } from './Tooltip/Tooltip'; export { Portal } from './Portal/Portal'; export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar'; +export { Label } from './Label/Label'; // Select export { Select, AsyncSelect, SelectOptionItem } from './Select/Select'; diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index b13393ab2e1..ed27de39cb8 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -1,7 +1,6 @@ import React, { PureComponent } from 'react'; +import { Select, Label } from '@grafana/ui'; -import { Label } from 'app/core/components/Label/Label'; -import { Select } from '@grafana/ui'; import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv'; import { DashboardSearchHit } from 'app/types'; diff --git a/public/app/features/datasources/settings/BasicSettings.tsx b/public/app/features/datasources/settings/BasicSettings.tsx index 120e002ac68..2d36b79a44c 100644 --- a/public/app/features/datasources/settings/BasicSettings.tsx +++ b/public/app/features/datasources/settings/BasicSettings.tsx @@ -1,5 +1,6 @@ import React, { SFC } from 'react'; -import { Label } from 'app/core/components/Label/Label'; +import { Label } from '@grafana/ui'; + import { Switch } from '../../../core/components/Switch/Switch'; export interface Props { diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 5e058289bf0..87c67b6e597 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; +import { Label } from '@grafana/ui'; -import { Label } from 'app/core/components/Label/Label'; import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences'; import { updateTeam } from './state/actions'; import { getRouteParamsId } from 'app/core/selectors/location'; diff --git a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx b/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx index f1f78ab1172..cf60411ffe2 100644 --- a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx +++ b/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx @@ -1,8 +1,7 @@ import React, { PureComponent } from 'react'; -import { GaugeOptions, PanelOptionsProps, PanelOptionsGroup } from '@grafana/ui'; +import { GaugeOptions, PanelOptionsProps, PanelOptionsGroup, Label } from '@grafana/ui'; import { Switch } from 'app/core/components/Switch/Switch'; -import { Label } from '../../../core/components/Label/Label'; export default class GaugeOptionsEditor extends PureComponent> { onToggleThresholdLabels = () => diff --git a/public/app/plugins/panel/gauge/MappingRow.tsx b/public/app/plugins/panel/gauge/MappingRow.tsx index b975821f27a..4aeb85a52d3 100644 --- a/public/app/plugins/panel/gauge/MappingRow.tsx +++ b/public/app/plugins/panel/gauge/MappingRow.tsx @@ -1,7 +1,5 @@ import React, { PureComponent } from 'react'; -import { MappingType, RangeMap, Select, ValueMap } from '@grafana/ui'; - -import { Label } from 'app/core/components/Label/Label'; +import { MappingType, RangeMap, Select, ValueMap, Label } from '@grafana/ui'; interface Props { mapping: ValueMap | RangeMap; diff --git a/public/app/plugins/panel/gauge/ValueOptions.tsx b/public/app/plugins/panel/gauge/ValueOptions.tsx index 7cfbb382f7b..accdb0b03fa 100644 --- a/public/app/plugins/panel/gauge/ValueOptions.tsx +++ b/public/app/plugins/panel/gauge/ValueOptions.tsx @@ -1,8 +1,6 @@ import React, { PureComponent } from 'react'; -import { GaugeOptions, PanelOptionsProps, PanelOptionsGroup } from '@grafana/ui'; +import { GaugeOptions, PanelOptionsProps, PanelOptionsGroup, Label, Select } from '@grafana/ui'; -import { Label } from 'app/core/components/Label/Label'; -import { Select} from '@grafana/ui'; import UnitPicker from 'app/core/components/Select/UnitPicker'; const statOptions = [ From c90979a8f0602703942dd2c143c20e89ff64ea4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 16 Jan 2019 10:49:48 +0100 Subject: [PATCH 041/156] Moved ValueMappings to grafana/ui/component and renamed it ValueMappingsEditor --- .../ValueMappingsEditor}/MappingRow.tsx | 5 +- .../ValueMappingsEditor.test.tsx | 11 ++-- .../ValueMappingsEditor.tsx | 6 +- .../_ValueMappingsEditor.scss | 0 .../ValueMappingsEditor.test.tsx.snap | 56 +++++++++++++++++++ packages/grafana-ui/src/components/index.scss | 1 + packages/grafana-ui/src/components/index.ts | 1 + .../plugins/panel/gauge/GaugePanelOptions.tsx | 4 +- public/sass/_grafana.scss | 3 +- 9 files changed, 75 insertions(+), 12 deletions(-) rename {public/app/plugins/panel/gauge => packages/grafana-ui/src/components/ValueMappingsEditor}/MappingRow.tsx (95%) rename public/app/plugins/panel/gauge/ValueMappings.test.tsx => packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx (84%) rename public/app/plugins/panel/gauge/ValueMappings.tsx => packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx (88%) rename public/sass/components/_value-mappings.scss => packages/grafana-ui/src/components/ValueMappingsEditor/_ValueMappingsEditor.scss (100%) create mode 100644 packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap diff --git a/public/app/plugins/panel/gauge/MappingRow.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx similarity index 95% rename from public/app/plugins/panel/gauge/MappingRow.tsx rename to packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx index 4aeb85a52d3..8d3407329bf 100644 --- a/public/app/plugins/panel/gauge/MappingRow.tsx +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx @@ -1,5 +1,8 @@ import React, { PureComponent } from 'react'; -import { MappingType, RangeMap, Select, ValueMap, Label } from '@grafana/ui'; + +import { ValueMap, RangeMap, MappingType } from '../../types/panel'; +import { Label } from '../Label/Label'; +import { Select } from '../Select/Select'; interface Props { mapping: ValueMap | RangeMap; diff --git a/public/app/plugins/panel/gauge/ValueMappings.test.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx similarity index 84% rename from public/app/plugins/panel/gauge/ValueMappings.test.tsx rename to packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx index 07db4028c68..fcb614f8e6f 100644 --- a/public/app/plugins/panel/gauge/ValueMappings.test.tsx +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { GaugeOptions, MappingType, PanelOptionsProps } from '@grafana/ui'; -import { defaultProps } from 'app/plugins/panel/gauge/GaugePanelOptions'; -import ValueMappings from './ValueMappings'; +import { defaultProps } from 'app/plugins/panel/gauge/GaugePanelOptions'; +import { ValueMappingsEditor } from './ValueMappingsEditor'; +import { PanelOptionsProps, MappingType } from '../../types/panel'; +import { GaugeOptions } from '../../types/gauge'; const setup = (propOverrides?: object) => { const props: PanelOptionsProps = { @@ -19,9 +20,9 @@ const setup = (propOverrides?: object) => { Object.assign(props, propOverrides); - const wrapper = shallow(); + const wrapper = shallow(); - const instance = wrapper.instance() as ValueMappings; + const instance = wrapper.instance() as ValueMappingsEditor; return { instance, diff --git a/public/app/plugins/panel/gauge/ValueMappings.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx similarity index 88% rename from public/app/plugins/panel/gauge/ValueMappings.tsx rename to packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx index 9a3f87450f4..ae0ff092a06 100644 --- a/public/app/plugins/panel/gauge/ValueMappings.tsx +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx @@ -1,14 +1,16 @@ import React, { PureComponent } from 'react'; -import { GaugeOptions, PanelOptionsProps, MappingType, RangeMap, ValueMap, PanelOptionsGroup } from '@grafana/ui'; import MappingRow from './MappingRow'; +import { PanelOptionsProps, ValueMap, RangeMap, MappingType } from '../../types/panel'; +import { GaugeOptions } from '../../types/gauge'; +import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup'; interface State { mappings: Array; nextIdToAdd: number; } -export default class ValueMappings extends PureComponent, State> { +export class ValueMappingsEditor extends PureComponent, State> { constructor(props) { super(props); diff --git a/public/sass/components/_value-mappings.scss b/packages/grafana-ui/src/components/ValueMappingsEditor/_ValueMappingsEditor.scss similarity index 100% rename from public/sass/components/_value-mappings.scss rename to packages/grafana-ui/src/components/ValueMappingsEditor/_ValueMappingsEditor.scss diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap b/packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap new file mode 100644 index 00000000000..592b3326421 --- /dev/null +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` + +
+ + +
+
+
+ +
+
+ Add mapping +
+
+
+`; diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss index b894cf73c1a..fa8d0135756 100644 --- a/packages/grafana-ui/src/components/index.scss +++ b/packages/grafana-ui/src/components/index.scss @@ -6,3 +6,4 @@ @import 'PanelOptionsGroup/PanelOptionsGroup'; @import 'PanelOptionsGrid/PanelOptionsGrid'; @import 'ColorPicker/ColorPicker'; +@import 'ValueMappingsEditor/ValueMappingsEditor'; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 51b601f2921..1b2d2fce972 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -19,3 +19,4 @@ export { GfFormLabel } from './GfFormLabel/GfFormLabel'; export { Graph } from './Graph/Graph'; export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup'; export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid'; +export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor'; diff --git a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx b/public/app/plugins/panel/gauge/GaugePanelOptions.tsx index a5334b0c6e1..3e82ca0e27b 100644 --- a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx +++ b/public/app/plugins/panel/gauge/GaugePanelOptions.tsx @@ -6,10 +6,10 @@ import { ThresholdsEditor, Threshold, PanelOptionsGrid, + ValueMappingsEditor, } from '@grafana/ui'; import ValueOptions from 'app/plugins/panel/gauge/ValueOptions'; -import ValueMappings from 'app/plugins/panel/gauge/ValueMappings'; import GaugeOptionsEditor from './GaugeOptionsEditor'; export const defaultProps = { @@ -44,7 +44,7 @@ export default class GaugePanelOptions extends PureComponent - + ); } diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 93fd26b7ca8..e8859ff8255 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -1,4 +1,4 @@ - // DEPENDENCIES +// DEPENDENCIES @import '../../node_modules/react-table/react-table.css'; // VENDOR @@ -97,7 +97,6 @@ @import 'components/add_data_source.scss'; @import 'components/page_loader'; @import 'components/toggle_button_group'; -@import 'components/value-mappings'; @import 'components/popover-box'; // LOAD @grafana/ui components From 013d46b707f82522d1f3c0ae973fdde5a6fa7455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 16 Jan 2019 12:14:43 +0100 Subject: [PATCH 042/156] Refactored ValueMappings --- .../ValueMappingsEditor/MappingRow.tsx | 36 ++++++------- .../ValueMappingsEditor.test.tsx | 27 +++++----- .../ValueMappingsEditor.tsx | 52 ++++++++++--------- .../ValueMappingsEditor.test.tsx.snap | 12 ++--- packages/grafana-ui/src/types/gauge.ts | 16 ------ packages/grafana-ui/src/types/index.ts | 1 - packages/grafana-ui/src/types/panel.ts | 2 + .../panel/gauge/GaugeOptionsEditor.tsx | 3 +- public/app/plugins/panel/gauge/GaugePanel.tsx | 3 +- .../plugins/panel/gauge/GaugePanelOptions.tsx | 19 +++++-- .../app/plugins/panel/gauge/ValueOptions.tsx | 3 +- public/app/plugins/panel/gauge/types.ts | 16 +++++- public/app/viz/Gauge.test.tsx | 2 +- public/app/viz/Gauge.tsx | 31 ++++------- 14 files changed, 112 insertions(+), 111 deletions(-) delete mode 100644 packages/grafana-ui/src/types/gauge.ts diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx index 8d3407329bf..9705304d354 100644 --- a/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx @@ -1,23 +1,23 @@ import React, { PureComponent } from 'react'; -import { ValueMap, RangeMap, MappingType } from '../../types/panel'; +import { MappingType, ValueMapping } from '../../types/panel'; import { Label } from '../Label/Label'; import { Select } from '../Select/Select'; -interface Props { - mapping: ValueMap | RangeMap; - updateMapping: (mapping) => void; - removeMapping: () => void; +export interface Props { + valueMapping: ValueMapping; + updateValueMapping: (valueMapping: ValueMapping) => void; + removeValueMapping: () => void; } interface State { - from: string; + from?: string; id: number; operator: string; text: string; - to: string; + to?: string; type: MappingType; - value: string; + value?: string; } const mappingOptions = [ @@ -26,36 +26,34 @@ const mappingOptions = [ ]; export default class MappingRow extends PureComponent { - constructor(props) { + constructor(props: Props) { super(props); - this.state = { - ...props.mapping, - }; + this.state = { ...props.valueMapping }; } - onMappingValueChange = event => { + onMappingValueChange = (event: React.ChangeEvent) => { this.setState({ value: event.target.value }); }; - onMappingFromChange = event => { + onMappingFromChange = (event: React.ChangeEvent) => { this.setState({ from: event.target.value }); }; - onMappingToChange = event => { + onMappingToChange = (event: React.ChangeEvent) => { this.setState({ to: event.target.value }); }; - onMappingTextChange = event => { + onMappingTextChange = (event: React.ChangeEvent) => { this.setState({ text: event.target.value }); }; - onMappingTypeChange = mappingType => { + onMappingTypeChange = (mappingType: MappingType) => { this.setState({ type: mappingType }); }; updateMapping = () => { - this.props.updateMapping({ ...this.state }); + this.props.updateValueMapping({ ...this.state } as ValueMapping); }; renderRow() { @@ -137,7 +135,7 @@ export default class MappingRow extends PureComponent {
{this.renderRow()}
-
diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx index fcb614f8e6f..bbad3e5a7ca 100644 --- a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx @@ -1,21 +1,16 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { defaultProps } from 'app/plugins/panel/gauge/GaugePanelOptions'; -import { ValueMappingsEditor } from './ValueMappingsEditor'; -import { PanelOptionsProps, MappingType } from '../../types/panel'; -import { GaugeOptions } from '../../types/gauge'; +import { ValueMappingsEditor, Props } from './ValueMappingsEditor'; +import { MappingType } from '../../types/panel'; const setup = (propOverrides?: object) => { - const props: PanelOptionsProps = { + const props: Props = { onChange: jest.fn(), - options: { - ...defaultProps.options, - mappings: [ - { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' }, - { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' }, - ], - }, + valueMappings: [ + { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' }, + { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' }, + ], }; Object.assign(props, propOverrides); @@ -41,18 +36,20 @@ describe('Render', () => { describe('On remove mapping', () => { it('Should remove mapping with id 0', () => { const { instance } = setup(); + instance.onRemoveMapping(1); - expect(instance.state.mappings).toEqual([ + expect(instance.state.valueMappings).toEqual([ { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' }, ]); }); it('should remove mapping with id 1', () => { const { instance } = setup(); + instance.onRemoveMapping(2); - expect(instance.state.mappings).toEqual([ + expect(instance.state.valueMappings).toEqual([ { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' }, ]); }); @@ -68,7 +65,7 @@ describe('Next id to add', () => { }); it('should default to 1', () => { - const { instance } = setup({ options: { ...defaultProps.options } }); + const { instance } = setup({ valueMappings: [] }); expect(instance.state.nextIdToAdd).toEqual(1); }); diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx index ae0ff092a06..ca0a6e71f4a 100644 --- a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx @@ -1,35 +1,39 @@ import React, { PureComponent } from 'react'; import MappingRow from './MappingRow'; -import { PanelOptionsProps, ValueMap, RangeMap, MappingType } from '../../types/panel'; -import { GaugeOptions } from '../../types/gauge'; +import { MappingType, ValueMapping } from '../../types/panel'; import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup'; +export interface Props { + valueMappings: ValueMapping[]; + onChange: (valueMappings: ValueMapping[]) => void; +} + interface State { - mappings: Array; + valueMappings: ValueMapping[]; nextIdToAdd: number; } -export class ValueMappingsEditor extends PureComponent, State> { - constructor(props) { +export class ValueMappingsEditor extends PureComponent { + constructor(props: Props) { super(props); - const mappings = props.options.mappings; + const mappings = props.valueMappings; this.state = { - mappings: mappings || [], - nextIdToAdd: mappings.length > 0 ? this.getMaxIdFromMappings(mappings) : 1, + valueMappings: mappings, + nextIdToAdd: mappings.length > 0 ? this.getMaxIdFromValueMappings(mappings) : 1, }; } - getMaxIdFromMappings(mappings) { + getMaxIdFromValueMappings(mappings: ValueMapping[]) { return Math.max.apply(null, mappings.map(mapping => mapping.id).map(m => m)) + 1; } addMapping = () => this.setState(prevState => ({ - mappings: [ - ...prevState.mappings, + valueMappings: [ + ...prevState.valueMappings, { id: prevState.nextIdToAdd, operator: '', @@ -43,23 +47,23 @@ export class ValueMappingsEditor extends PureComponent { + onRemoveMapping = (id: number) => { this.setState( prevState => ({ - mappings: prevState.mappings.filter(m => { + valueMappings: prevState.valueMappings.filter(m => { return m.id !== id; }), }), () => { - this.props.onChange({ ...this.props.options, mappings: this.state.mappings }); + this.props.onChange(this.state.valueMappings); } ); }; - updateGauge = mapping => { + updateGauge = (mapping: ValueMapping) => { this.setState( prevState => ({ - mappings: prevState.mappings.map(m => { + valueMappings: prevState.valueMappings.map(m => { if (m.id === mapping.id) { return { ...mapping }; } @@ -68,24 +72,24 @@ export class ValueMappingsEditor extends PureComponent { - this.props.onChange({ ...this.props.options, mappings: this.state.mappings }); + this.props.onChange(this.state.valueMappings); } ); }; render() { - const { mappings } = this.state; + const { valueMappings } = this.state; return (
- {mappings.length > 0 && - mappings.map((mapping, index) => ( + {valueMappings.length > 0 && + valueMappings.map((valueMapping, index) => ( this.onRemoveMapping(mapping.id)} + key={`${valueMapping.text}-${index}`} + valueMapping={valueMapping} + updateValueMapping={this.updateGauge} + removeValueMapping={() => this.onRemoveMapping(valueMapping.id)} /> ))}
diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap b/packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap index 592b3326421..8a465ff88df 100644 --- a/packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap @@ -7,7 +7,9 @@ exports[`Render should render component 1`] = `
; - maxValue: number; - minValue: number; - prefix: string; - showThresholdLabels: boolean; - showThresholdMarkers: boolean; - stat: string; - suffix: string; - thresholds: Threshold[]; - unit: string; -} diff --git a/packages/grafana-ui/src/types/index.ts b/packages/grafana-ui/src/types/index.ts index 814ab0478db..f618ce6db34 100644 --- a/packages/grafana-ui/src/types/index.ts +++ b/packages/grafana-ui/src/types/index.ts @@ -1,4 +1,3 @@ export * from './series'; export * from './time'; export * from './panel'; -export * from './gauge'; diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 17ef712b0dd..7e4012ad529 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -56,6 +56,8 @@ interface BaseMap { type: MappingType; } +export type ValueMapping = ValueMap | RangeMap; + export interface ValueMap extends BaseMap { value: string; } diff --git a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx b/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx index cf60411ffe2..93fd67d608c 100644 --- a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx +++ b/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx @@ -1,7 +1,8 @@ import React, { PureComponent } from 'react'; -import { GaugeOptions, PanelOptionsProps, PanelOptionsGroup, Label } from '@grafana/ui'; +import { PanelOptionsProps, PanelOptionsGroup, Label } from '@grafana/ui'; import { Switch } from 'app/core/components/Switch/Switch'; +import { GaugeOptions } from './types'; export default class GaugeOptionsEditor extends PureComponent> { onToggleThresholdLabels = () => diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index fd3d812f21e..cfce719b5a6 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -1,8 +1,9 @@ import React, { PureComponent } from 'react'; -import { GaugeOptions, PanelProps, NullValueMode } from '@grafana/ui'; +import { PanelProps, NullValueMode } from '@grafana/ui'; import { getTimeSeriesVMs } from 'app/viz/state/timeSeries'; import Gauge from 'app/viz/Gauge'; +import { GaugeOptions } from './types'; interface Props extends PanelProps {} diff --git a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx b/public/app/plugins/panel/gauge/GaugePanelOptions.tsx index 3e82ca0e27b..9729416b7e6 100644 --- a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx +++ b/public/app/plugins/panel/gauge/GaugePanelOptions.tsx @@ -1,16 +1,17 @@ import React, { PureComponent } from 'react'; import { BasicGaugeColor, - GaugeOptions, PanelOptionsProps, ThresholdsEditor, Threshold, PanelOptionsGrid, ValueMappingsEditor, + ValueMapping, } from '@grafana/ui'; import ValueOptions from 'app/plugins/panel/gauge/ValueOptions'; import GaugeOptionsEditor from './GaugeOptionsEditor'; +import { GaugeOptions } from './types'; export const defaultProps = { options: { @@ -24,7 +25,7 @@ export const defaultProps = { decimals: 0, stat: 'avg', unit: 'none', - mappings: [], + valueMappings: [], thresholds: [], }, }; @@ -32,7 +33,17 @@ export const defaultProps = { export default class GaugePanelOptions extends PureComponent> { static defaultProps = defaultProps; - onThresholdsChanged = (thresholds: Threshold[]) => this.props.onChange({ ...this.props.options, thresholds }); + onThresholdsChanged = (thresholds: Threshold[]) => + this.props.onChange({ + ...this.props.options, + thresholds, + }); + + onValueMappingsChanged = (valueMappings: ValueMapping[]) => + this.props.onChange({ + ...this.props.options, + valueMappings, + }); render() { const { onChange, options } = this.props; @@ -44,7 +55,7 @@ export default class GaugePanelOptions extends PureComponent - + ); } diff --git a/public/app/plugins/panel/gauge/ValueOptions.tsx b/public/app/plugins/panel/gauge/ValueOptions.tsx index accdb0b03fa..3bc529f0611 100644 --- a/public/app/plugins/panel/gauge/ValueOptions.tsx +++ b/public/app/plugins/panel/gauge/ValueOptions.tsx @@ -1,7 +1,8 @@ import React, { PureComponent } from 'react'; -import { GaugeOptions, PanelOptionsProps, PanelOptionsGroup, Label, Select } from '@grafana/ui'; +import { PanelOptionsProps, PanelOptionsGroup, Label, Select } from '@grafana/ui'; import UnitPicker from 'app/core/components/Select/UnitPicker'; +import { GaugeOptions } from './types'; const statOptions = [ { value: 'min', label: 'Min' }, diff --git a/public/app/plugins/panel/gauge/types.ts b/public/app/plugins/panel/gauge/types.ts index 139597f9cb0..b698a3389c2 100644 --- a/public/app/plugins/panel/gauge/types.ts +++ b/public/app/plugins/panel/gauge/types.ts @@ -1,2 +1,16 @@ +import { Threshold, ValueMapping } from '@grafana/ui'; - +export interface GaugeOptions { + baseColor: string; + decimals: number; + valueMappings: ValueMapping[]; + maxValue: number; + minValue: number; + prefix: string; + showThresholdLabels: boolean; + showThresholdMarkers: boolean; + stat: string; + suffix: string; + thresholds: Threshold[]; + unit: string; +} diff --git a/public/app/viz/Gauge.test.tsx b/public/app/viz/Gauge.test.tsx index f0c4a874649..69c7733f44b 100644 --- a/public/app/viz/Gauge.test.tsx +++ b/public/app/viz/Gauge.test.tsx @@ -12,7 +12,7 @@ const setup = (propOverrides?: object) => { const props: Props = { baseColor: BasicGaugeColor.Green, maxValue: 100, - mappings: [], + valueMappings: [], minValue: 0, prefix: '', showThresholdMarkers: true, diff --git a/public/app/viz/Gauge.tsx b/public/app/viz/Gauge.tsx index d5e4eb94884..094e630a1c0 100644 --- a/public/app/viz/Gauge.tsx +++ b/public/app/viz/Gauge.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import $ from 'jquery'; -import { BasicGaugeColor, Threshold, TimeSeriesVMs, RangeMap, ValueMap, MappingType } from '@grafana/ui'; +import { BasicGaugeColor, Threshold, TimeSeriesVMs, MappingType, ValueMapping } from '@grafana/ui'; import config from '../core/config'; import kbn from '../core/utils/kbn'; @@ -9,7 +9,7 @@ export interface Props { baseColor: string; decimals: number; height: number; - mappings: Array; + valueMappings: ValueMapping[]; maxValue: number; minValue: number; prefix: string; @@ -29,7 +29,7 @@ export class Gauge extends PureComponent { static defaultProps = { baseColor: BasicGaugeColor.Green, maxValue: 100, - mappings: [], + valueMappings: [], minValue: 0, prefix: '', showThresholdMarkers: true, @@ -64,20 +64,17 @@ export class Gauge extends PureComponent { } })[0]; - return { - rangeMap, - valueMap, - }; + return { rangeMap, valueMap }; } formatValue(value) { - const { decimals, mappings, prefix, suffix, unit } = this.props; + const { decimals, valueMappings, prefix, suffix, unit } = this.props; const formatFunc = kbn.valueFormats[unit]; const formattedValue = formatFunc(value, decimals); - if (mappings.length > 0) { - const { rangeMap, valueMap } = this.formatWithMappings(mappings, formattedValue); + if (valueMappings.length > 0) { + const { rangeMap, valueMap } = this.formatWithMappings(valueMappings, formattedValue); if (valueMap) { return `${prefix} ${valueMap} ${suffix}`; @@ -148,10 +145,7 @@ export class Gauge extends PureComponent { color: index === 0 ? threshold.color : thresholds[index].color, }; }), - { - value: maxValue, - color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor, - }, + { value: maxValue, color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor }, ]; const options = { @@ -184,19 +178,14 @@ export class Gauge extends PureComponent { formatter: () => { return this.formatValue(value); }, - font: { - size: fontSize, - family: '"Helvetica Neue", Helvetica, Arial, sans-serif', - }, + font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' }, }, show: true, }, }, }; - const plotSeries = { - data: [[0, value]], - }; + const plotSeries = { data: [[0, value]] }; try { $.plot(this.canvasElement, [plotSeries], options); From 8df11e93a1aaca5dda4d2a029b1e8f6e08a9f5a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 16 Jan 2019 12:46:43 +0100 Subject: [PATCH 043/156] Removed snapshot --- .../__snapshots__/ValueMappings.test.tsx.snap | 56 ------------------- 1 file changed, 56 deletions(-) delete mode 100644 public/app/plugins/panel/gauge/__snapshots__/ValueMappings.test.tsx.snap diff --git a/public/app/plugins/panel/gauge/__snapshots__/ValueMappings.test.tsx.snap b/public/app/plugins/panel/gauge/__snapshots__/ValueMappings.test.tsx.snap deleted file mode 100644 index 592b3326421..00000000000 --- a/public/app/plugins/panel/gauge/__snapshots__/ValueMappings.test.tsx.snap +++ /dev/null @@ -1,56 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Render should render component 1`] = ` - -
- - -
-
-
- -
-
- Add mapping -
-
-
-`; From 166e5edebd39ff6c8073f86f48ec7577b5a428a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 16 Jan 2019 14:00:29 +0100 Subject: [PATCH 044/156] wip: testing new query editor row design --- .../dashboard/panel_editor/QueriesTab.tsx | 63 +++++++++---------- .../dashboard/panel_editor/QueryEditorRow.tsx | 44 ++++++++++++- .../panel/partials/query_editor_row.html | 44 +------------ public/sass/components/_query_editor.scss | 56 +++++++++++++++-- 4 files changed, 123 insertions(+), 84 deletions(-) diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index 1c842e6572c..b1d2bd7284b 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -171,40 +171,39 @@ export class QueriesTab extends PureComponent { return ( <> - -
- {panel.targets.map((query, index) => ( - - ))} - -
-
- -
-
- {!isAddingMixed && ( - - )} - {isAddingMixed && this.renderMixedPicker()} -
+
+ {panel.targets.map((query, index) => ( + + ))} +
+
+
+
+ +
+
+ {!isAddingMixed && ( + + )} + {isAddingMixed && this.renderMixedPicker()}
- +
diff --git a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx index 1028815cf08..def0e85f07b 100644 --- a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx +++ b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx @@ -1,5 +1,6 @@ // Libraries import React, { PureComponent } from 'react'; +import classNames from 'classnames'; // Utils & Services import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -21,6 +22,7 @@ interface Props { interface State { datasource: DataSourceApi | null; + isCollapsed: boolean; } export class QueryEditorRow extends PureComponent { @@ -29,6 +31,7 @@ export class QueryEditorRow extends PureComponent { state: State = { datasource: null, + isCollapsed: false, }; componentDidMount() { @@ -90,15 +93,51 @@ export class QueryEditorRow extends PureComponent { } } + onToggleCollapse = () => { + this.setState({ isCollapsed: !this.state.isCollapsed }); + }; + render() { - const { datasource } = this.state; + const { query } = this.props; + const { datasource, isCollapsed } = this.state; + const bodyClasses = classNames('query-editor-box__body gf-form-query', {hide: isCollapsed}); if (!datasource) { return null; } if (datasource.pluginExports.QueryCtrl) { - return
(this.element = element)} />; + return ( +
+
+
+ {isCollapsed && } + {!isCollapsed && } + {query.refId} +
+
+ + + + + +
+
+
+
(this.element = element)} /> +
+
+ ); } else if (datasource.pluginExports.QueryEditor) { const QueryEditor = datasource.pluginExports.QueryEditor; return ; @@ -119,4 +158,3 @@ export interface AngularQueryComponentScope { moveQuery: (query: DataQuery, direction: number) => void; datasource: DataSourceApi; } - diff --git a/public/app/features/panel/partials/query_editor_row.html b/public/app/features/panel/partials/query_editor_row.html index 34a86813d1d..fc2e3602630 100644 --- a/public/app/features/panel/partials/query_editor_row.html +++ b/public/app/features/panel/partials/query_editor_row.html @@ -1,44 +1,2 @@ -
- +
-
-
- -
-
- -
- -
- - - -
-
diff --git a/public/sass/components/_query_editor.scss b/public/sass/components/_query_editor.scss index 8b876624294..fe455df1bff 100644 --- a/public/sass/components/_query_editor.scss +++ b/public/sass/components/_query_editor.scss @@ -18,12 +18,6 @@ } .gf-form-query { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - align-content: flex-start; - align-items: flex-start; - .gf-form, .gf-form-filler { margin-bottom: 2px; @@ -188,3 +182,53 @@ input[type='text'].tight-form-func-param { .rst-literal-block .rst-text { display: block; } + +.query-editor-box { + background: $page-bg; + margin-bottom: 2px; + + &:hover { + .query-editor-box__actions { + display: flex; + } + } +} + +.query-editor-box__header { + display: flex; + padding: 4px 0px 4px 8px; + position: relative; + height: 35px; +} + +.query-editor-box__ref-id { + font-weight: $font-weight-semi-bold; + color: $blue; + font-size: $font-size-md; + flex-grow: 1; + cursor: pointer; + display: flex; + align-items: center; + + i { + padding-right: 5px; + color: $text-muted; + position: relative; + } +} + +.query-editor-box__actions { + display: flex; + justify-content: flex-end; + display: none; +} + +.query-editor-box__action { + @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color, $btn-inverse-text-shadow); + border: 1px solid $navbar-button-border; + margin-right: 3px; +} + + .query-editor-box__body { + padding: 10px 20px; + } From cc8b450799852028a5cf9cb8ddab030aff502198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 16 Jan 2019 14:36:01 +0100 Subject: [PATCH 045/156] minor style change --- .../components/ThresholdsEditor/_ThresholdsEditor.scss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss index 50c92a6bcc5..f95ecac46a2 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss +++ b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss @@ -8,6 +8,12 @@ height: 70px; } +.thresholds-row:first-child > .thresholds-row-color-indicator { + border-top-left-radius: $border-radius; + border-top-right-radius: $border-radius; + overflow: hidden; +} + .thresholds-row:last-child > .thresholds-row-color-indicator { border-bottom-left-radius: $border-radius; border-bottom-right-radius: $border-radius; @@ -33,7 +39,7 @@ } .thresholds-row-color-indicator { - width: 20px; + width: 10px; } .thresholds-row-input { From 4c40274313f38cf57a525c3fd780213e9ee0e0a2 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 16 Jan 2019 13:46:57 +0000 Subject: [PATCH 046/156] renaming after pr feedback --- .../{FormGroup.test.tsx => FormField.test.tsx} | 4 ++-- .../FormGroup/{FormGroup.tsx => FormField.tsx} | 10 +++++----- .../GfFormLabel.tsx => FormLabel/FormLabel.tsx} | 2 +- packages/grafana-ui/src/components/index.ts | 4 ++-- .../features/dashboard/panel_editor/QueryOptions.tsx | 4 ++-- public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx | 6 +++--- public/app/plugins/panel/gauge/MappingRow.tsx | 8 ++++---- public/app/plugins/panel/gauge/ValueOptions.tsx | 8 ++++---- 8 files changed, 23 insertions(+), 23 deletions(-) rename packages/grafana-ui/src/components/FormGroup/{FormGroup.test.tsx => FormField.test.tsx} (82%) rename packages/grafana-ui/src/components/FormGroup/{FormGroup.tsx => FormField.tsx} (58%) rename packages/grafana-ui/src/components/{GfFormLabel/GfFormLabel.tsx => FormLabel/FormLabel.tsx} (81%) diff --git a/packages/grafana-ui/src/components/FormGroup/FormGroup.test.tsx b/packages/grafana-ui/src/components/FormGroup/FormField.test.tsx similarity index 82% rename from packages/grafana-ui/src/components/FormGroup/FormGroup.test.tsx rename to packages/grafana-ui/src/components/FormGroup/FormField.test.tsx index 4f8b4be9540..4474b0680c5 100644 --- a/packages/grafana-ui/src/components/FormGroup/FormGroup.test.tsx +++ b/packages/grafana-ui/src/components/FormGroup/FormField.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { FormGroup, Props } from './FormGroup'; +import { FormField, Props } from './FormField'; const setup = (propOverrides?: object) => { const props: Props = { @@ -14,7 +14,7 @@ const setup = (propOverrides?: object) => { Object.assign(props, propOverrides); - return shallow(); + return shallow(); }; describe('Render', () => { diff --git a/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx b/packages/grafana-ui/src/components/FormGroup/FormField.tsx similarity index 58% rename from packages/grafana-ui/src/components/FormGroup/FormGroup.tsx rename to packages/grafana-ui/src/components/FormGroup/FormField.tsx index a0088032079..ae86d4115b0 100644 --- a/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx +++ b/packages/grafana-ui/src/components/FormGroup/FormField.tsx @@ -1,9 +1,9 @@ -import React, { SFC } from 'react'; +import React, { InputHTMLAttributes, FunctionComponent } from 'react'; import { Label } from '..'; export interface Props { label: string; - inputProps: {}; + inputProps: InputHTMLAttributes; labelWidth?: number; inputWidth?: number; } @@ -14,7 +14,7 @@ const defaultProps = { inputWidth: 12, }; -const FormGroup: SFC = ({ label, labelWidth, inputProps, inputWidth }) => { +const FormField: FunctionComponent = ({ label, labelWidth, inputProps, inputWidth }) => { return (
@@ -23,5 +23,5 @@ const FormGroup: SFC = ({ label, labelWidth, inputProps, inputWidth }) => ); }; -FormGroup.defaultProps = defaultProps; -export { FormGroup }; +FormField.defaultProps = defaultProps; +export { FormField }; diff --git a/packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx b/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx similarity index 81% rename from packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx rename to packages/grafana-ui/src/components/FormLabel/FormLabel.tsx index 8b80de64696..d6ac3da9394 100644 --- a/packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx +++ b/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx @@ -9,7 +9,7 @@ interface Props { isInvalid?: boolean; } -export const GfFormLabel: SFC = ({ children, isFocused, isInvalid, className, htmlFor, ...rest }) => { +export const FormLabel: SFC = ({ children, isFocused, isInvalid, className, htmlFor, ...rest }) => { const classes = classNames('gf-form-label', className, { 'gf-form-label--is-focused': isFocused, 'gf-form-label--is-invalid': isInvalid, diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index ab0edf45ed0..3a29623838a 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -10,8 +10,8 @@ export { NoOptionsMessage } from './Select/NoOptionsMessage'; export { default as resetSelectStyles } from './Select/resetSelectStyles'; // Forms -export { GfFormLabel } from './GfFormLabel/GfFormLabel'; -export { FormGroup } from './FormGroup/FormGroup'; +export { FormLabel } from './FormLabel/FormLabel'; +export { FormField } from './FormGroup/FormField'; export { Label } from './Label/Label'; export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder'; diff --git a/public/app/features/dashboard/panel_editor/QueryOptions.tsx b/public/app/features/dashboard/panel_editor/QueryOptions.tsx index fad70d92990..d6187a89b7b 100644 --- a/public/app/features/dashboard/panel_editor/QueryOptions.tsx +++ b/public/app/features/dashboard/panel_editor/QueryOptions.tsx @@ -10,7 +10,7 @@ import { Input } from 'app/core/components/Form'; import { EventsWithValidation } from 'app/core/components/Form/Input'; import { InputStatus } from 'app/core/components/Form/Input'; import DataSourceOption from './DataSourceOption'; -import { GfFormLabel } from '@grafana/ui'; +import { FormLabel } from '@grafana/ui'; // Types import { PanelModel } from '../panel_model'; @@ -164,7 +164,7 @@ export class QueryOptions extends PureComponent { {this.renderOptions()}
- Relative time + Relative time > { onToggleThresholdLabels = () => @@ -21,12 +21,12 @@ export default class GaugeOptionsEditor extends PureComponent - this.onMinValueChange(event), value: minValue }} /> - this.onMaxValueChange(event), value: maxValue }} diff --git a/public/app/plugins/panel/gauge/MappingRow.tsx b/public/app/plugins/panel/gauge/MappingRow.tsx index 91dff549677..47647b1b9ae 100644 --- a/public/app/plugins/panel/gauge/MappingRow.tsx +++ b/public/app/plugins/panel/gauge/MappingRow.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { FormGroup, Label, MappingType, RangeMap, Select, ValueMap } from '@grafana/ui'; +import { FormField, Label, MappingType, RangeMap, Select, ValueMap } from '@grafana/ui'; interface Props { mapping: ValueMap | RangeMap; @@ -61,7 +61,7 @@ export default class MappingRow extends PureComponent { if (type === MappingType.RangeToText) { return ( <> - { }} inputWidth={8} /> - { return ( <> - Unit
- - - Date: Wed, 16 Jan 2019 13:52:38 +0000 Subject: [PATCH 047/156] move styling --- .../{FormGroup => FormField}/FormField.test.tsx | 0 .../{FormGroup => FormField}/FormField.tsx | 2 +- .../src/components/FormField/_FormField.scss | 12 ++++++++++++ .../__snapshots__/FormField.test.tsx.snap} | 0 packages/grafana-ui/src/components/index.scss | 1 + packages/grafana-ui/src/components/index.ts | 2 +- 6 files changed, 15 insertions(+), 2 deletions(-) rename packages/grafana-ui/src/components/{FormGroup => FormField}/FormField.test.tsx (100%) rename packages/grafana-ui/src/components/{FormGroup => FormField}/FormField.tsx (95%) create mode 100644 packages/grafana-ui/src/components/FormField/_FormField.scss rename packages/grafana-ui/src/components/{FormGroup/__snapshots__/FormGroup.test.tsx.snap => FormField/__snapshots__/FormField.test.tsx.snap} (100%) diff --git a/packages/grafana-ui/src/components/FormGroup/FormField.test.tsx b/packages/grafana-ui/src/components/FormField/FormField.test.tsx similarity index 100% rename from packages/grafana-ui/src/components/FormGroup/FormField.test.tsx rename to packages/grafana-ui/src/components/FormField/FormField.test.tsx diff --git a/packages/grafana-ui/src/components/FormGroup/FormField.tsx b/packages/grafana-ui/src/components/FormField/FormField.tsx similarity index 95% rename from packages/grafana-ui/src/components/FormGroup/FormField.tsx rename to packages/grafana-ui/src/components/FormField/FormField.tsx index ae86d4115b0..aa026a74197 100644 --- a/packages/grafana-ui/src/components/FormGroup/FormField.tsx +++ b/packages/grafana-ui/src/components/FormField/FormField.tsx @@ -16,7 +16,7 @@ const defaultProps = { const FormField: FunctionComponent = ({ label, labelWidth, inputProps, inputWidth }) => { return ( -
+
diff --git a/packages/grafana-ui/src/components/FormField/_FormField.scss b/packages/grafana-ui/src/components/FormField/_FormField.scss new file mode 100644 index 00000000000..36955e2fca6 --- /dev/null +++ b/packages/grafana-ui/src/components/FormField/_FormField.scss @@ -0,0 +1,12 @@ +.form-field { + margin-bottom: $gf-form-margin; + display: flex; + flex-direction: row; + align-items: center; + text-align: left; + position: relative; + + &--grow { + flex-grow: 1; + } +} diff --git a/packages/grafana-ui/src/components/FormGroup/__snapshots__/FormGroup.test.tsx.snap b/packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap similarity index 100% rename from packages/grafana-ui/src/components/FormGroup/__snapshots__/FormGroup.test.tsx.snap rename to packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss index b894cf73c1a..eaf64561ae8 100644 --- a/packages/grafana-ui/src/components/index.scss +++ b/packages/grafana-ui/src/components/index.scss @@ -6,3 +6,4 @@ @import 'PanelOptionsGroup/PanelOptionsGroup'; @import 'PanelOptionsGrid/PanelOptionsGrid'; @import 'ColorPicker/ColorPicker'; +@import "FormField/FormField"; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 3a29623838a..ac06c07951b 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -11,7 +11,7 @@ export { default as resetSelectStyles } from './Select/resetSelectStyles'; // Forms export { FormLabel } from './FormLabel/FormLabel'; -export { FormField } from './FormGroup/FormField'; +export { FormField } from './FormField/FormField'; export { Label } from './Label/Label'; export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder'; From a558e76a68824ed9187859a79cce3fc8129b470d Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 16 Jan 2019 15:09:48 +0100 Subject: [PATCH 048/156] fix: Manually trigger a change-event when autofill is used in webkit-browsers #12133 --- public/app/core/core.ts | 1 + .../app/core/directives/autofill_event_fix.ts | 35 +++++++++++++++++++ public/app/partials/login.html | 2 +- public/sass/_grafana.scss | 1 + public/sass/utils/_hacks.scss | 11 ++++++ 5 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 public/app/core/directives/autofill_event_fix.ts create mode 100644 public/sass/utils/_hacks.scss diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 6713d8bcd14..fb38cefd435 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -1,5 +1,6 @@ import './directives/dash_class'; import './directives/dropdown_typeahead'; +import './directives/autofill_event_fix'; import './directives/metric_segment'; import './directives/misc'; import './directives/ng_model_on_blur'; diff --git a/public/app/core/directives/autofill_event_fix.ts b/public/app/core/directives/autofill_event_fix.ts new file mode 100644 index 00000000000..51d278fe7c9 --- /dev/null +++ b/public/app/core/directives/autofill_event_fix.ts @@ -0,0 +1,35 @@ +import coreModule from '../core_module'; + +/** @ngInject */ +export function autofillEventFix($compile) { + return { + link: ($scope: any, elem: any) => { + const input = elem[0]; + const dispatchChangeEvent = () => { + const event = new Event('change'); + return input.dispatchEvent(event); + }; + const onAnimationStart = ({ animationName }: AnimationEvent) => { + switch (animationName) { + case 'onAutoFillStart': + return dispatchChangeEvent(); + case 'onAutoFillCancel': + return dispatchChangeEvent(); + } + return null; + }; + + // const onChange = (evt: Event) => console.log(evt); + + input.addEventListener('animationstart', onAnimationStart); + // input.addEventListener('change', onChange); + + $scope.$on('$destroy', () => { + input.removeEventListener('animationstart', onAnimationStart); + // input.removeEventListener('change', onChange); + }); + } + }; +} + +coreModule.directive('autofillEventFix', autofillEventFix); diff --git a/public/app/partials/login.html b/public/app/partials/login.html index f4237e7b1ec..d629244e0ae 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -9,7 +9,7 @@
- - - - - - + + + + + - - - - - - - - - - - + - - + + + + + + + - + - - test-2 - - - + - + - - - - + + - + - - test-3 - - - + - + - - - - + + - + - - test-4 - - - + - + - - - - + + - + - - test-5 - - - + - + - - - -
- - Name - - Email - - Members - +
+ + Name + + Email + + Members + -
- - - - - - test-1 - - - - test-1@test.com - - - - 1 - - - -
+ +
+ + test-1 + + + + test-1@test.com + + + + 1 + + + - - +
- + + + - test-2@test.com - - - + test-2 + + - 2 - - - -
- + test-2@test.com + + - + 2 + + + - - +
- + + + - test-3@test.com - - - + test-3 + + - 3 - - - -
- + test-3@test.com + + - + 3 + + + - - +
- + + + - test-4@test.com - - - + test-4 + + - 4 - - - -
- + test-4@test.com + + - + 4 + + + - - +
- + + + - test-5@test.com - - - + test-5 + + - 5 - - - -
+ + test-5@test.com + + + + + 5 + + + + + + + + +
-
-
+ + `; diff --git a/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap b/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap index 429322eac98..a6c71f63d53 100644 --- a/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap +++ b/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap @@ -1,12 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Render should render List page 1`] = ` -
+ -
-
-
+ + `; exports[`Render should render component 1`] = ` -
+ -
- -
-
+ + `; From feeb3ea56dae1cb3384466797dfda88de985925c Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 16 Jan 2019 15:59:05 +0100 Subject: [PATCH 063/156] feat: Generate page titles from navModel --- public/app/core/selectors/navModel.ts | 4 ++++ public/app/features/datasources/DataSourcesListPage.tsx | 5 +++-- public/app/features/plugins/PluginListPage.tsx | 6 +++--- public/app/features/teams/TeamList.tsx | 6 +++--- public/app/features/users/UsersListPage.tsx | 4 ++-- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index aa508616962..7d745b58002 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -41,3 +41,7 @@ export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel) return getNotFoundModel(); } + +export const getTitleFromNavModel = (navModel: NavModel) => { + return `${navModel.main.text}${navModel.node.text ? ': ' + navModel.node.text : '' }`; +}; diff --git a/public/app/features/datasources/DataSourcesListPage.tsx b/public/app/features/datasources/DataSourcesListPage.tsx index 12eb49cb856..f1c1c45ded8 100644 --- a/public/app/features/datasources/DataSourcesListPage.tsx +++ b/public/app/features/datasources/DataSourcesListPage.tsx @@ -8,7 +8,8 @@ import DataSourcesList from './DataSourcesList'; import { DataSource, NavModel, StoreState } from 'app/types'; import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector'; import { loadDataSources, setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/actions'; -import { getNavModel } from 'app/core/selectors/navModel'; +import { getNavModel, getTitleFromNavModel } from 'app/core/selectors/navModel'; + import { getDataSources, getDataSourcesCount, @@ -66,7 +67,7 @@ export class DataSourcesListPage extends PureComponent { }; return ( - + <> diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx index ba61f637609..14bd05f0537 100644 --- a/public/app/features/plugins/PluginListPage.tsx +++ b/public/app/features/plugins/PluginListPage.tsx @@ -6,9 +6,9 @@ import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar'; import PluginList from './PluginList'; import { NavModel, Plugin } from 'app/types'; import { loadPlugins, setPluginsLayoutMode, setPluginsSearchQuery } from './state/actions'; -import { getNavModel } from '../../core/selectors/navModel'; +import { getNavModel, getTitleFromNavModel } from 'app/core/selectors/navModel'; import { getLayoutMode, getPlugins, getPluginsSearchQuery } from './state/selectors'; -import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector'; +import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector'; export interface Props { navModel: NavModel; @@ -47,7 +47,7 @@ export class PluginListPage extends PureComponent { }; return ( - + <> diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 8eac05be3bf..5dd83ca385c 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -4,10 +4,10 @@ import { hot } from 'react-hot-loader'; import Page from 'app/core/components/Page/Page'; import { DeleteButton } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; -import { NavModel, Team } from '../../types'; +import { NavModel, Team } from 'app/types'; import { loadTeams, deleteTeam, setSearchQuery } from './state/actions'; import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors'; -import { getNavModel } from 'app/core/selectors/navModel'; +import { getNavModel, getTitleFromNavModel } from 'app/core/selectors/navModel'; export interface Props { navModel: NavModel; @@ -140,7 +140,7 @@ export class TeamList extends PureComponent { const { hasFetched, navModel } = this.props; return ( - + {hasFetched && this.renderList()} diff --git a/public/app/features/users/UsersListPage.tsx b/public/app/features/users/UsersListPage.tsx index ece431158fd..43fcc67e470 100644 --- a/public/app/features/users/UsersListPage.tsx +++ b/public/app/features/users/UsersListPage.tsx @@ -9,7 +9,7 @@ import InviteesTable from './InviteesTable'; import { Invitee, NavModel, OrgUser } from 'app/types'; import appEvents from 'app/core/app_events'; import { loadUsers, loadInvitees, setUsersSearchQuery, updateUser, removeUser } from './state/actions'; -import { getNavModel } from '../../core/selectors/navModel'; +import { getNavModel, getTitleFromNavModel } from 'app/core/selectors/navModel'; import { getInvitees, getUsers, getUsersSearchQuery } from './state/selectors'; export interface Props { @@ -104,7 +104,7 @@ export class UsersListPage extends PureComponent { const externalUserMngInfoHtml = this.externalUserMngInfoHtml; return ( - + <> From 38bcb88d8aa2c71add4373fc2d9c1c975e3e3df6 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 16 Jan 2019 16:05:45 +0100 Subject: [PATCH 064/156] build: uploads binaries before metadata in deb repo. --- scripts/build/update_repo/update-deb.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/build/update_repo/update-deb.sh b/scripts/build/update_repo/update-deb.sh index 0f80de9674c..8aef5a71892 100755 --- a/scripts/build/update_repo/update-deb.sh +++ b/scripts/build/update_repo/update-deb.sh @@ -54,9 +54,12 @@ aptly publish update stable filesystem:repo:grafana aptly publish update beta filesystem:repo:grafana # Update the repo and db on gcp -## TODO: need to update this to push the binaries first and then the metadata so that we dont cache the binaries missing. gsutil -m rsync -r -d /deb-repo/db "gs://$GCP_DB_BUCKET/$RELEASE_TYPE" + +# Uploads the binaries before the metadata (to prevent 404's for debs) +gsutil -m rsync -r /deb-repo/repo/grafana/pool "gs://$GCP_REPO_BUCKET/$RELEASE_TYPE/deb/pool" + gsutil -m rsync -r -d /deb-repo/repo/grafana "gs://$GCP_REPO_BUCKET/$RELEASE_TYPE/deb" # usage: From 3ca7523a0207ad514692d4ae132fc543ac060a86 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 16 Jan 2019 16:16:19 +0100 Subject: [PATCH 065/156] fix: Use Page component on "Api Keys" and "Preferences" under Configuration --- public/app/features/api-keys/ApiKeysPage.tsx | 29 ++++++++-------- public/app/features/org/OrgDetailsPage.tsx | 36 ++++++++++---------- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index e14873fa9f6..4cbd8ce4170 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -3,11 +3,10 @@ import ReactDOMServer from 'react-dom/server'; import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; import { NavModel, ApiKey, NewApiKey, OrgRole } from 'app/types'; -import { getNavModel } from 'app/core/selectors/navModel'; +import { getNavModel, getTitleFromNavModel } from 'app/core/selectors/navModel'; import { getApiKeys, getApiKeysCount } from './state/selectors'; import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions'; -import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import PageLoader from 'app/core/components/PageLoader/PageLoader'; +import Page from 'app/core/components/Page/Page'; import SlideDown from 'app/core/components/Animations/SlideDown'; import ApiKeysAddedModal from './ApiKeysAddedModal'; import config from 'app/core/config'; @@ -240,18 +239,18 @@ export class ApiKeysPage extends PureComponent { const { hasFetched, navModel, apiKeysCount } = this.props; return ( -
- - {hasFetched ? ( - apiKeysCount > 0 ? ( - this.renderApiKeyList() - ) : ( - this.renderEmptyList() - ) - ) : ( - - )} -
+ + + + {hasFetched && ( + apiKeysCount > 0 ? ( + this.renderApiKeyList() + ) : ( + this.renderEmptyList() + ) + )} + + ); } } diff --git a/public/app/features/org/OrgDetailsPage.tsx b/public/app/features/org/OrgDetailsPage.tsx index b011901aa71..ce84bdefafd 100644 --- a/public/app/features/org/OrgDetailsPage.tsx +++ b/public/app/features/org/OrgDetailsPage.tsx @@ -1,13 +1,12 @@ import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; -import PageHeader from '../../core/components/PageHeader/PageHeader'; -import PageLoader from '../../core/components/PageLoader/PageLoader'; +import Page from 'app/core/components/Page/Page'; import OrgProfile from './OrgProfile'; import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences'; import { loadOrganization, setOrganizationName, updateOrganization } from './state/actions'; import { NavModel, Organization, StoreState } from 'app/types'; -import { getNavModel } from '../../core/selectors/navModel'; +import { getNavModel, getTitleFromNavModel } from 'app/core/selectors/navModel'; export interface Props { navModel: NavModel; @@ -35,22 +34,23 @@ export class OrgDetailsPage extends PureComponent { const isLoading = Object.keys(organization).length === 0; return ( -
- -
- {isLoading && } - {!isLoading && ( -
- this.onOrgNameChange(name)} - onSubmit={this.onUpdateOrganization} - orgName={organization.name} - /> - + + + +
+ {!isLoading && ( +
+ this.onOrgNameChange(name)} + onSubmit={this.onUpdateOrganization} + orgName={organization.name} + /> + +
+ )}
- )} -
-
+ + ); } } From cae2bdf994ab906661d62feb8bad292a01f4de8f Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 16 Jan 2019 16:18:10 +0100 Subject: [PATCH 066/156] build: deb repo update test usage instructions. --- .../build/update_repo/test-update-deb-repo.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/scripts/build/update_repo/test-update-deb-repo.sh b/scripts/build/update_repo/test-update-deb-repo.sh index f27e9bec265..192de6591df 100755 --- a/scripts/build/update_repo/test-update-deb-repo.sh +++ b/scripts/build/update_repo/test-update-deb-repo.sh @@ -1,5 +1,22 @@ #!/usr/bin/env bash +# Usage +# docker run -ti --rm -u 0:0 grafana/grafana-ci-deploy:1.1.0 bash +# in the container: +# mkdir -p /go/src/github.com/grafana/dist +# cd /go/src/github.com/grafana +# +# outside of container: +# cd /.. +# docker cp grafana :/go/src/github.com/grafana/. +# docker cp :/private.key +# +# in container: +# gpg --batch --allow-secret-key-import --import /private.key +# cd dist && wget https://dl.grafana.com/oss/release/grafana_5.4.3_amd64.deb && cd .. +# run this script: +# ./script/build/update_repo/test-update-deb-repo.sh + GPG_PASS=${1:-} ./scripts/build/update_repo/update-deb.sh "oss" "$GPG_PASS" "v5.4.3" "dist" "grafana-testing-aptly-db" "grafana-testing-repo" From 234713466e655c1f4167fbede9ee4509f8c8456f Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 16 Jan 2019 16:29:07 +0100 Subject: [PATCH 067/156] test: Update snapshots and mocks --- .../CustomScrollbar.test.tsx.snap | 4 +- .../features/api-keys/ApiKeysPage.test.tsx | 9 +- .../__snapshots__/ApiKeysPage.test.tsx.snap | 242 ++++++++++-------- .../datasources/DataSourcesListPage.test.tsx | 9 +- .../DataSourcesListPage.test.tsx.snap | 22 +- .../app/features/org/OrgDetailsPage.test.tsx | 9 +- .../OrgDetailsPage.test.tsx.snap | 72 ++++-- .../features/plugins/PluginListPage.test.tsx | 9 +- .../PluginListPage.test.tsx.snap | 22 +- public/app/features/teams/TeamList.test.tsx | 9 +- .../__snapshots__/TeamList.test.tsx.snap | 26 +- .../app/features/users/UsersListPage.test.tsx | 9 +- .../__snapshots__/UsersListPage.test.tsx.snap | 22 +- 13 files changed, 315 insertions(+), 149 deletions(-) diff --git a/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap b/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap index 60b4a2e0aa5..aabe3dd98c5 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap +++ b/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap @@ -7,7 +7,7 @@ exports[`CustomScrollbar renders correctly 1`] = ` Object { "height": "auto", "maxHeight": "100%", - "minHeight": "0", + "minHeight": 0, "overflow": "hidden", "position": "relative", "width": "100%", @@ -24,7 +24,7 @@ exports[`CustomScrollbar renders correctly 1`] = ` "marginBottom": 0, "marginRight": 0, "maxHeight": "calc(100% + 0px)", - "minHeight": "calc(0 + 0px)", + "minHeight": 0, "overflow": "scroll", "position": "relative", "right": undefined, diff --git a/public/app/features/api-keys/ApiKeysPage.test.tsx b/public/app/features/api-keys/ApiKeysPage.test.tsx index 54200234ddc..cd640b5a357 100644 --- a/public/app/features/api-keys/ApiKeysPage.test.tsx +++ b/public/app/features/api-keys/ApiKeysPage.test.tsx @@ -6,7 +6,14 @@ import { getMultipleMockKeys, getMockKey } from './__mocks__/apiKeysMock'; const setup = (propOverrides?: object) => { const props: Props = { - navModel: {} as NavModel, + navModel: { + main: { + text: 'Configuration' + }, + node: { + text: 'Api Keys' + } + } as NavModel, apiKeys: [] as ApiKey[], searchQuery: '', hasFetched: false, diff --git a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap index 7ede9618250..fd05b79da81 100644 --- a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap +++ b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap @@ -1,132 +1,158 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Render should render API keys table if there are any keys 1`] = ` -
+ - -
+ `; exports[`Render should render CTA if there are no API keys 1`] = ` -
+ -
- - + +
-
+ - -
- Add API Key -
- -
+ + +
+ Add API Key +
+
- - Key name - - -
-
- - Role - - - +
+
+ + Role + + + - -
-
-
+
- Add - + +
-
- -
-
-
-
+ +
+ +
+ + `; diff --git a/public/app/features/datasources/DataSourcesListPage.test.tsx b/public/app/features/datasources/DataSourcesListPage.test.tsx index 0ea716d62c9..33f5790978d 100644 --- a/public/app/features/datasources/DataSourcesListPage.test.tsx +++ b/public/app/features/datasources/DataSourcesListPage.test.tsx @@ -10,7 +10,14 @@ const setup = (propOverrides?: object) => { dataSources: [] as DataSource[], layoutMode: LayoutModes.Grid, loadDataSources: jest.fn(), - navModel: {} as NavModel, + navModel: { + main: { + text: 'Configuration' + }, + node: { + text: 'Data Sources' + } + } as NavModel, dataSourcesCount: 0, searchQuery: '', setDataSourcesSearchQuery: jest.fn(), diff --git a/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap b/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap index 145623bbad0..8c351d1dc2d 100644 --- a/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap +++ b/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap @@ -5,7 +5,16 @@ exports[`Render should render action bar and datasources 1`] = ` title="Configuration: Data Sources" > { const props: Props = { organization: {} as Organization, - navModel: {} as NavModel, + navModel: { + main: { + text: 'Configuration' + }, + node: { + text: 'Org details' + } + } as NavModel, loadOrganization: jest.fn(), setOrganizationName: jest.fn(), updateOrganization: jest.fn(), diff --git a/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap b/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap index 582d626d315..84c4f17fba8 100644 --- a/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap +++ b/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap @@ -1,38 +1,64 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Render should render component 1`] = ` -
+ -
- -
-
+
+ `; exports[`Render should render organization and preferences 1`] = ` -
+ -
-
- - +
+
+ + +
-
-
+ +
`; diff --git a/public/app/features/plugins/PluginListPage.test.tsx b/public/app/features/plugins/PluginListPage.test.tsx index 31b2f128436..31956f41cc1 100644 --- a/public/app/features/plugins/PluginListPage.test.tsx +++ b/public/app/features/plugins/PluginListPage.test.tsx @@ -6,7 +6,14 @@ import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector const setup = (propOverrides?: object) => { const props: Props = { - navModel: {} as NavModel, + navModel: { + main: { + text: 'Configuration' + }, + node: { + text: 'Plugins' + } + } as NavModel, plugins: [] as Plugin[], searchQuery: '', setPluginsSearchQuery: jest.fn(), diff --git a/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap index 6444c0c82cc..48f7d638637 100644 --- a/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap +++ b/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap @@ -5,7 +5,16 @@ exports[`Render should render component 1`] = ` title="Configuration: Plugins" > { const props: Props = { - navModel: {} as NavModel, + navModel: { + main: { + text: 'Configuration' + }, + node: { + text: 'Team List' + } + } as NavModel, teams: [] as Team[], loadTeams: jest.fn(), deleteTeam: jest.fn(), diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap index d10a2fd2052..cfa9533e1b6 100644 --- a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -2,10 +2,19 @@ exports[`Render should render component 1`] = ` ({ const setup = (propOverrides?: object) => { const props: Props = { - navModel: {} as NavModel, + navModel: { + main: { + text: 'Configuration' + }, + node: { + text: 'Users' + } + } as NavModel, users: [] as OrgUser[], invitees: [] as Invitee[], searchQuery: '', diff --git a/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap b/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap index a6c71f63d53..c6c02562115 100644 --- a/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap +++ b/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap @@ -5,7 +5,16 @@ exports[`Render should render List page 1`] = ` title="Configuration: Users" > Date: Wed, 16 Jan 2019 17:53:40 +0100 Subject: [PATCH 068/156] Toggle edit mode works --- .../dashboard/panel_editor/QueryEditorRow.tsx | 118 ++++++++++++------ public/app/features/panel/query_editor_row.ts | 5 + public/sass/components/_query_editor.scss | 20 +-- 3 files changed, 96 insertions(+), 47 deletions(-) diff --git a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx index def0e85f07b..a7724eed814 100644 --- a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx +++ b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx @@ -23,6 +23,7 @@ interface Props { interface State { datasource: DataSourceApi | null; isCollapsed: boolean; + angularScope: AngularQueryComponentScope | null; } export class QueryEditorRow extends PureComponent { @@ -32,6 +33,7 @@ export class QueryEditorRow extends PureComponent { state: State = { datasource: null, isCollapsed: false, + angularScope: null, }; componentDidMount() { @@ -85,6 +87,11 @@ export class QueryEditorRow extends PureComponent { const scopeProps = { ctrl: this.getAngularQueryComponentScope() }; this.angularQueryEditor = loader.load(this.element, scopeProps, template); + + // give angular time to compile + setTimeout(() => { + this.setState({ angularScope: scopeProps.ctrl }); + }, 10); } componentWillUnmount() { @@ -97,54 +104,84 @@ export class QueryEditorRow extends PureComponent { this.setState({ isCollapsed: !this.state.isCollapsed }); }; - render() { - const { query } = this.props; - const { datasource, isCollapsed } = this.state; - const bodyClasses = classNames('query-editor-box__body gf-form-query', {hide: isCollapsed}); - - if (!datasource) { - return null; - } + renderPluginEditor() { + const { datasource } = this.state; if (datasource.pluginExports.QueryCtrl) { - return ( -
-
-
- {isCollapsed && } - {!isCollapsed && } - {query.refId} -
-
- - - - - -
-
-
-
(this.element = element)} /> -
-
- ); - } else if (datasource.pluginExports.QueryEditor) { + } + return
(this.element = element)} />; + + if (datasource.pluginExports.QueryEditor) { const QueryEditor = datasource.pluginExports.QueryEditor; return ; } return
Data source plugin does not export any Query Editor component
; } + + onToggleEditMode = () => { + const { angularScope } = this.state; + + if (angularScope && angularScope.toggleEditorMode) { + angularScope.toggleEditorMode(); + this.angularQueryEditor.digest(); + } + } + + get hasTextEditMode() { + const { angularScope } = this.state; + return angularScope && angularScope.toggleEditorMode; + } + + render() { + const { query } = this.props; + const { datasource, isCollapsed, angularScope } = this.state; + const bodyClasses = classNames('query-editor-box__body gf-form-query', { hide: isCollapsed }); + + if (!datasource) { + return null; + } + + console.log('Query render'); + if (angularScope !== null && angularScope.toggleEditorMode) { + console.log('Query editor has text edit mode'); + } + + return ( +
+
+
+ {isCollapsed && } + {!isCollapsed && } + {query.refId} +
+
+ {this.hasTextEditMode && ( + + )} + + + + + +
+
+
{this.renderPluginEditor()}
+
+ ); + } } export interface AngularQueryComponentScope { @@ -157,4 +194,5 @@ export interface AngularQueryComponentScope { addQuery: (query?: DataQuery) => void; moveQuery: (query: DataQuery, direction: number) => void; datasource: DataSourceApi; + toggleEditorMode?: () => void; } diff --git a/public/app/features/panel/query_editor_row.ts b/public/app/features/panel/query_editor_row.ts index a44c1e8be6d..82d93ca5cae 100644 --- a/public/app/features/panel/query_editor_row.ts +++ b/public/app/features/panel/query_editor_row.ts @@ -12,6 +12,7 @@ export class QueryRowCtrl { panel: any; collapsed: any; hideEditorRowActions: boolean; + hasTextEditMode: boolean; constructor() { this.panelCtrl = this.queryCtrl.panelCtrl; @@ -19,6 +20,10 @@ export class QueryRowCtrl { this.panel = this.panelCtrl.panel; this.hideEditorRowActions = this.panelCtrl.hideEditorRowActions; + if (this.hasTextEditMode) { + this.panelCtrl.toggleEditorMode = this.toggleEditorMode.bind(this); + } + if (!this.target.refId) { this.target.refId = this.panel.getNextQueryLetter(); } diff --git a/public/sass/components/_query_editor.scss b/public/sass/components/_query_editor.scss index fe455df1bff..7e8eddea414 100644 --- a/public/sass/components/_query_editor.scss +++ b/public/sass/components/_query_editor.scss @@ -184,7 +184,6 @@ input[type='text'].tight-form-func-param { } .query-editor-box { - background: $page-bg; margin-bottom: 2px; &:hover { @@ -199,13 +198,13 @@ input[type='text'].tight-form-func-param { padding: 4px 0px 4px 8px; position: relative; height: 35px; + background: $page-bg; } .query-editor-box__ref-id { font-weight: $font-weight-semi-bold; color: $blue; font-size: $font-size-md; - flex-grow: 1; cursor: pointer; display: flex; align-items: center; @@ -218,17 +217,24 @@ input[type='text'].tight-form-func-param { } .query-editor-box__actions { + flex-grow: 1; display: flex; justify-content: flex-end; - display: none; + color: $text-muted; } .query-editor-box__action { - @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color, $btn-inverse-text-shadow); - border: 1px solid $navbar-button-border; - margin-right: 3px; + margin-left: 3px; + background: transparent; + border: none; + box-shadow: none; + + &:hover { + color: $text-color; + } } .query-editor-box__body { - padding: 10px 20px; + margin: 0 0 10px 40px; + background: $page-bg; } From 736db86d6e7cf963a297b70ac3a57e57714c6a45 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 16 Jan 2019 18:02:27 +0000 Subject: [PATCH 069/156] removing Label and going with FormLabel --- .../src/components/FormField/FormField.tsx | 4 +-- .../src/components/FormLabel/FormLabel.tsx | 27 ++++++++++++++++--- .../grafana-ui/src/components/Label/Label.tsx | 25 ----------------- .../ValueMappingsEditor/MappingRow.tsx | 8 +++--- packages/grafana-ui/src/components/index.ts | 1 - .../SharedPreferences/SharedPreferences.tsx | 6 ++--- .../datasources/settings/BasicSettings.tsx | 6 ++--- public/app/features/teams/TeamSettings.tsx | 8 +++--- .../app/plugins/panel/gauge/ValueOptions.tsx | 6 ++--- 9 files changed, 42 insertions(+), 49 deletions(-) delete mode 100644 packages/grafana-ui/src/components/Label/Label.tsx diff --git a/packages/grafana-ui/src/components/FormField/FormField.tsx b/packages/grafana-ui/src/components/FormField/FormField.tsx index 14bec79b57f..593678c7383 100644 --- a/packages/grafana-ui/src/components/FormField/FormField.tsx +++ b/packages/grafana-ui/src/components/FormField/FormField.tsx @@ -1,5 +1,5 @@ import React, { InputHTMLAttributes, FunctionComponent } from 'react'; -import { Label } from '..'; +import { FormLabel } from '..'; export interface Props extends InputHTMLAttributes { label: string; @@ -15,7 +15,7 @@ const defaultProps = { const FormField: FunctionComponent = ({ label, labelWidth, inputWidth, ...inputProps }) => { return (
- + {label}
); diff --git a/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx b/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx index d6ac3da9394..2bd4fbc153b 100644 --- a/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx +++ b/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx @@ -1,16 +1,28 @@ -import React, { SFC, ReactNode } from 'react'; +import React, { FunctionComponent, ReactNode } from 'react'; import classNames from 'classnames'; +import { Tooltip } from '..'; interface Props { children: ReactNode; - htmlFor?: string; className?: string; + htmlFor?: string; isFocused?: boolean; isInvalid?: boolean; + tooltip?: string; + width?: number; } -export const FormLabel: SFC = ({ children, isFocused, isInvalid, className, htmlFor, ...rest }) => { - const classes = classNames('gf-form-label', className, { +export const FormLabel: FunctionComponent = ({ + children, + isFocused, + isInvalid, + className, + htmlFor, + tooltip, + width, + ...rest +}) => { + const classes = classNames(`gf-form-label width-${width ? width : '10'}`, className, { 'gf-form-label--is-focused': isFocused, 'gf-form-label--is-invalid': isInvalid, }); @@ -18,6 +30,13 @@ export const FormLabel: SFC = ({ children, isFocused, isInvalid, classNam return ( ); }; diff --git a/packages/grafana-ui/src/components/Label/Label.tsx b/packages/grafana-ui/src/components/Label/Label.tsx deleted file mode 100644 index 270b0161226..00000000000 --- a/packages/grafana-ui/src/components/Label/Label.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { SFC, ReactNode } from 'react'; -import { Tooltip } from '../Tooltip/Tooltip'; - -interface Props { - tooltip?: string; - for?: string; - children: ReactNode; - width?: number; - className?: string; -} - -export const Label: SFC = props => { - return ( - - {props.children} - {props.tooltip && ( - -
- -
-
- )} -
- ); -}; diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx index db970046fc4..deb73460354 100644 --- a/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx @@ -1,7 +1,7 @@ import React, { ChangeEvent, PureComponent } from 'react'; import { MappingType, ValueMapping } from '../../types'; -import { FormField, Label, Select } from '..'; +import { FormField, FormLabel, Select } from '..'; export interface Props { valueMapping: ValueMapping; @@ -78,7 +78,7 @@ export default class MappingRow extends PureComponent { value={to} />
- + Text { inputWidth={8} />
- + Text { return (
- + Type dashboard.id === homeDashboardId)} getOptionValue={i => i.id} diff --git a/public/app/features/datasources/settings/BasicSettings.tsx b/public/app/features/datasources/settings/BasicSettings.tsx index 21a548a5045..56d4570e3a3 100644 --- a/public/app/features/datasources/settings/BasicSettings.tsx +++ b/public/app/features/datasources/settings/BasicSettings.tsx @@ -1,5 +1,5 @@ import React, { SFC } from 'react'; -import { Label } from '@grafana/ui'; +import { FormLabel } from '@grafana/ui'; import { Switch } from '../../../core/components/Switch/Switch'; export interface Props { @@ -14,14 +14,14 @@ const BasicSettings: SFC = ({ dataSourceName, isDefault, onDefaultChange,
- + {

Team Settings

- + Name {
- +
- + Stat +
+
); } } diff --git a/public/app/plugins/datasource/testdata/module.ts b/public/app/plugins/datasource/testdata/module.ts index ea01849b997..d3b376e3307 100644 --- a/public/app/plugins/datasource/testdata/module.ts +++ b/public/app/plugins/datasource/testdata/module.ts @@ -1,5 +1,6 @@ import { TestDataDatasource } from './datasource'; import { TestDataQueryCtrl } from './query_ctrl'; +// import { QueryEditor } from './QueryEditor'; class TestDataAnnotationsQueryCtrl { annotation: any; @@ -10,7 +11,7 @@ class TestDataAnnotationsQueryCtrl { } export { - TestDataDatasource, + // QueryEditor, TestDataDatasource as Datasource, TestDataQueryCtrl as QueryCtrl, TestDataAnnotationsQueryCtrl as AnnotationsQueryCtrl, From 533b938fcd330bd920fad6a2c55ae8c38083534f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 17 Jan 2019 08:19:40 +0100 Subject: [PATCH 099/156] Removed baseColor --- .../ThresholdsEditor/ThresholdsEditor.tsx | 21 +++++---- .../plugins/panel/gauge/GaugePanelOptions.tsx | 2 - public/app/plugins/panel/gauge/types.ts | 1 - public/app/viz/Gauge.test.tsx | 9 ++-- public/app/viz/Gauge.tsx | 47 +++++++++---------- 5 files changed, 40 insertions(+), 40 deletions(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index 6e8d88051f3..bd4f83f4dba 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -19,9 +19,15 @@ export class ThresholdsEditor extends PureComponent { constructor(props: Props) { super(props); - const thresholds: Threshold[] = - props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: colors[0] }]; + const addDefaultThreshold = this.props.thresholds.length === 0; + const thresholds: Threshold[] = addDefaultThreshold + ? [{ index: 0, value: -Infinity, color: colors[0] }] + : props.thresholds; this.state = { thresholds }; + + if (addDefaultThreshold) { + this.onChange(); + } } onAddThreshold = (index: number) => { @@ -62,7 +68,7 @@ export class ThresholdsEditor extends PureComponent { }, ]), }, - () => this.updateGauge() + () => this.onChange() ); }; @@ -85,7 +91,7 @@ export class ThresholdsEditor extends PureComponent { thresholds: newThresholds.filter(t => t !== threshold), }; }, - () => this.updateGauge() + () => this.onChange() ); }; @@ -124,11 +130,10 @@ export class ThresholdsEditor extends PureComponent { { thresholds: newThresholds, }, - () => this.updateGauge() + () => this.onChange() ); }; - onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds); onBlur = () => { this.setState(prevState => { const sortThresholds = this.sortThresholds([...prevState.thresholds]); @@ -139,10 +144,10 @@ export class ThresholdsEditor extends PureComponent { return { thresholds: sortThresholds }; }); - this.updateGauge(); + this.onChange(); }; - updateGauge = () => { + onChange = () => { this.props.onChange(this.state.thresholds); }; diff --git a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx b/public/app/plugins/panel/gauge/GaugePanelOptions.tsx index 9729416b7e6..18a445d840d 100644 --- a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx +++ b/public/app/plugins/panel/gauge/GaugePanelOptions.tsx @@ -1,6 +1,5 @@ import React, { PureComponent } from 'react'; import { - BasicGaugeColor, PanelOptionsProps, ThresholdsEditor, Threshold, @@ -15,7 +14,6 @@ import { GaugeOptions } from './types'; export const defaultProps = { options: { - baseColor: BasicGaugeColor.Green, minValue: 0, maxValue: 100, prefix: '', diff --git a/public/app/plugins/panel/gauge/types.ts b/public/app/plugins/panel/gauge/types.ts index b698a3389c2..42262178dc8 100644 --- a/public/app/plugins/panel/gauge/types.ts +++ b/public/app/plugins/panel/gauge/types.ts @@ -1,7 +1,6 @@ import { Threshold, ValueMapping } from '@grafana/ui'; export interface GaugeOptions { - baseColor: string; decimals: number; valueMappings: ValueMapping[]; maxValue: number; diff --git a/public/app/viz/Gauge.test.tsx b/public/app/viz/Gauge.test.tsx index 69c7733f44b..2678b3f2ad1 100644 --- a/public/app/viz/Gauge.test.tsx +++ b/public/app/viz/Gauge.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { BasicGaugeColor, TimeSeriesVMs } from '@grafana/ui'; +import { TimeSeriesVMs } from '@grafana/ui'; import { Gauge, Props } from './Gauge'; @@ -10,7 +10,6 @@ jest.mock('jquery', () => ({ const setup = (propOverrides?: object) => { const props: Props = { - baseColor: BasicGaugeColor.Green, maxValue: 100, valueMappings: [], minValue: 0, @@ -18,7 +17,7 @@ const setup = (propOverrides?: object) => { showThresholdMarkers: true, showThresholdLabels: false, suffix: '', - thresholds: [], + thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }], unit: 'none', stat: 'avg', height: 300, @@ -42,12 +41,12 @@ describe('Get font color', () => { it('should get base color if no threshold', () => { const { instance } = setup(); - expect(instance.getFontColor(40)).toEqual(BasicGaugeColor.Green); + expect(instance.getFontColor(40)).toEqual('#7EB26D'); }); it('should be f2f2f2', () => { const { instance } = setup({ - thresholds: [{ value: 59, color: '#f2f2f2' }], + thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 59, color: '#f2f2f2' }], }); expect(instance.getFontColor(58)).toEqual('#f2f2f2'); diff --git a/public/app/viz/Gauge.tsx b/public/app/viz/Gauge.tsx index 094e630a1c0..069a8dbcc3c 100644 --- a/public/app/viz/Gauge.tsx +++ b/public/app/viz/Gauge.tsx @@ -6,7 +6,6 @@ import config from '../core/config'; import kbn from '../core/utils/kbn'; export interface Props { - baseColor: string; decimals: number; height: number; valueMappings: ValueMapping[]; @@ -27,7 +26,6 @@ export class Gauge extends PureComponent { canvasElement: any; static defaultProps = { - baseColor: BasicGaugeColor.Green, maxValue: 100, valueMappings: [], minValue: 0, @@ -91,24 +89,25 @@ export class Gauge extends PureComponent { } getFontColor(value) { - const { baseColor, maxValue, thresholds } = this.props; + const { maxValue, thresholds } = this.props; - if (thresholds.length > 0) { - const atThreshold = thresholds.filter(threshold => value <= threshold.value); - - if (atThreshold.length > 0) { - return atThreshold[0].color; - } else if (value <= maxValue) { - return BasicGaugeColor.Red; - } + if (thresholds.length === 1) { + return thresholds[0].color; } - return baseColor; + const atThreshold = thresholds.filter(threshold => value < threshold.value); + + if (atThreshold.length > 0) { + return atThreshold[0].color; + } else if (value <= maxValue) { + return BasicGaugeColor.Red; + } + + return ''; } draw() { const { - baseColor, maxValue, minValue, timeSeries, @@ -137,16 +136,16 @@ export class Gauge extends PureComponent { const thresholdMarkersWidth = gaugeWidth / 5; const thresholdLabelFontSize = fontSize / 2.5; - const formattedThresholds = [ - { value: minValue, color: BasicGaugeColor.Green }, - ...thresholds.map((threshold, index) => { - return { - value: threshold.value, - color: index === 0 ? threshold.color : thresholds[index].color, - }; - }), - { value: maxValue, color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor }, - ]; + // const formattedThresholds = [ + // { value: minValue, color: BasicGaugeColor.Green }, + // ...thresholds.map((threshold, index) => { + // return { + // value: threshold.value, + // color: index === 0 ? threshold.color : thresholds[index].color, + // }; + // }), + // { value: maxValue, color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor }, + // ]; const options = { series: { @@ -164,7 +163,7 @@ export class Gauge extends PureComponent { layout: { margin: 0, thresholdWidth: 0 }, cell: { border: { width: 0 } }, threshold: { - values: formattedThresholds, + values: thresholds, label: { show: showThresholdLabels, margin: thresholdMarkersWidth + 1, From 9dcf3d58ea77cbee729fd26022ce83bdcc2358d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 17 Jan 2019 09:54:31 +0100 Subject: [PATCH 100/156] Fixed getFontColor, added tests and fixed thresholds logic --- public/app/viz/Gauge.test.tsx | 28 ++++++++++++++++++++++------ public/app/viz/Gauge.tsx | 35 +++++++++++++++++------------------ 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/public/app/viz/Gauge.test.tsx b/public/app/viz/Gauge.test.tsx index 2678b3f2ad1..3fed641c9a2 100644 --- a/public/app/viz/Gauge.test.tsx +++ b/public/app/viz/Gauge.test.tsx @@ -38,17 +38,33 @@ const setup = (propOverrides?: object) => { }; describe('Get font color', () => { - it('should get base color if no threshold', () => { - const { instance } = setup(); + it('should get first threshold color when only one threshold', () => { + const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] }); - expect(instance.getFontColor(40)).toEqual('#7EB26D'); + expect(instance.getFontColor(49)).toEqual('#7EB26D'); }); - it('should be f2f2f2', () => { + it('should get the next threshold color if value is same as a threshold', () => { const { instance } = setup({ - thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 59, color: '#f2f2f2' }], + thresholds: [ + { index: 2, value: 75, color: '#6ED0E0' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, + ], }); - expect(instance.getFontColor(58)).toEqual('#f2f2f2'); + expect(instance.getFontColor(50)).toEqual('#6ED0E0'); + }); + + it('should get the nearest threshold color', () => { + const { instance } = setup({ + thresholds: [ + { index: 2, value: 75, color: '#6ED0E0' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, + ], + }); + + expect(instance.getFontColor(6.5)).toEqual('#EAB839'); }); }); diff --git a/public/app/viz/Gauge.tsx b/public/app/viz/Gauge.tsx index 069a8dbcc3c..75ad799d322 100644 --- a/public/app/viz/Gauge.tsx +++ b/public/app/viz/Gauge.tsx @@ -82,14 +82,14 @@ export class Gauge extends PureComponent { } if (isNaN(value)) { - return '-'; + return value; } return `${prefix} ${formattedValue} ${suffix}`; } - getFontColor(value) { - const { maxValue, thresholds } = this.props; + getFontColor(value: string | number) { + const { thresholds } = this.props; if (thresholds.length === 1) { return thresholds[0].color; @@ -98,12 +98,11 @@ export class Gauge extends PureComponent { const atThreshold = thresholds.filter(threshold => value < threshold.value); if (atThreshold.length > 0) { - return atThreshold[0].color; - } else if (value <= maxValue) { - return BasicGaugeColor.Red; + const nearestThreshold = atThreshold.sort((t1, t2) => t1.value - t2.value)[0]; + return nearestThreshold.color; } - return ''; + return BasicGaugeColor.Red; } draw() { @@ -136,16 +135,16 @@ export class Gauge extends PureComponent { const thresholdMarkersWidth = gaugeWidth / 5; const thresholdLabelFontSize = fontSize / 2.5; - // const formattedThresholds = [ - // { value: minValue, color: BasicGaugeColor.Green }, - // ...thresholds.map((threshold, index) => { - // return { - // value: threshold.value, - // color: index === 0 ? threshold.color : thresholds[index].color, - // }; - // }), - // { value: maxValue, color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor }, - // ]; + const formattedThresholds = [ + { value: minValue, color: thresholds.length === 1 ? thresholds[0].color : BasicGaugeColor.Green }, + ...thresholds.map((threshold, index) => { + return { + value: threshold.value, + color: thresholds[index].color, + }; + }), + { value: maxValue, color: thresholds.length === 1 ? thresholds[0].color : BasicGaugeColor.Red }, + ]; const options = { series: { @@ -163,7 +162,7 @@ export class Gauge extends PureComponent { layout: { margin: 0, thresholdWidth: 0 }, cell: { border: { width: 0 } }, threshold: { - values: thresholds, + values: formattedThresholds, label: { show: showThresholdLabels, margin: thresholdMarkersWidth + 1, From 554d010332cf1aceecbfdad15f6ed54445a4264f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 17 Jan 2019 11:01:49 +0100 Subject: [PATCH 101/156] Preparing move to ui/viz --- packages/grafana-ui/src/types/panel.ts | 7 +++++++ public/app/viz/Gauge.tsx | 20 ++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 7e4012ad529..340bec9d37b 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -66,3 +66,10 @@ export interface RangeMap extends BaseMap { from: string; to: string; } + +export type Theme = 'dark' | 'light'; + +export enum Themes { + Dark = 'dark', + Light = 'light', +} diff --git a/public/app/viz/Gauge.tsx b/public/app/viz/Gauge.tsx index 75ad799d322..1a611d79783 100644 --- a/public/app/viz/Gauge.tsx +++ b/public/app/viz/Gauge.tsx @@ -1,9 +1,15 @@ import React, { PureComponent } from 'react'; import $ from 'jquery'; -import { BasicGaugeColor, Threshold, TimeSeriesVMs, MappingType, ValueMapping } from '@grafana/ui'; - -import config from '../core/config'; -import kbn from '../core/utils/kbn'; +import { + BasicGaugeColor, + Threshold, + TimeSeriesVMs, + MappingType, + ValueMapping, + getValueFormat, + Theme, + Themes, +} from '@grafana/ui'; export interface Props { decimals: number; @@ -20,6 +26,7 @@ export interface Props { suffix: string; unit: string; width: number; + theme?: Theme; } export class Gauge extends PureComponent { @@ -68,7 +75,7 @@ export class Gauge extends PureComponent { formatValue(value) { const { decimals, valueMappings, prefix, suffix, unit } = this.props; - const formatFunc = kbn.valueFormats[unit]; + const formatFunc = getValueFormat(unit); const formattedValue = formatFunc(value, decimals); if (valueMappings.length > 0) { @@ -116,6 +123,7 @@ export class Gauge extends PureComponent { width, height, stat, + theme, } = this.props; let value: string | number = ''; @@ -127,7 +135,7 @@ export class Gauge extends PureComponent { } const dimension = Math.min(width, height * 1.3); - const backgroundColor = config.bootData.user.lightTheme ? 'rgb(230,230,230)' : 'rgb(38,38,38)'; + const backgroundColor = theme === Themes.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)'; const fontScale = parseInt('80', 10) / 100; const fontSize = Math.min(dimension / 5, 100) * fontScale; const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1; From 9a01f3e5178a49b625496c4ca9d6eeada588c36d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 17 Jan 2019 11:55:22 +0100 Subject: [PATCH 102/156] Moved Gauge to ui/components --- .../src/components/Gauge}/Gauge.test.tsx | 2 +- .../grafana-ui/src/components/Gauge}/Gauge.tsx | 14 ++++---------- packages/grafana-ui/src/components/index.ts | 1 + public/app/plugins/panel/gauge/GaugePanel.tsx | 5 ++--- .../state => plugins/panel/gauge}/timeSeries.ts | 0 5 files changed, 8 insertions(+), 14 deletions(-) rename {public/app/viz => packages/grafana-ui/src/components/Gauge}/Gauge.test.tsx (97%) rename {public/app/viz => packages/grafana-ui/src/components/Gauge}/Gauge.tsx (95%) rename public/app/{viz/state => plugins/panel/gauge}/timeSeries.ts (100%) diff --git a/public/app/viz/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx similarity index 97% rename from public/app/viz/Gauge.test.tsx rename to packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index 3fed641c9a2..999f3f581ab 100644 --- a/public/app/viz/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { TimeSeriesVMs } from '@grafana/ui'; import { Gauge, Props } from './Gauge'; +import { TimeSeriesVMs } from '../../types/series'; jest.mock('jquery', () => ({ plot: jest.fn(), diff --git a/public/app/viz/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx similarity index 95% rename from public/app/viz/Gauge.tsx rename to packages/grafana-ui/src/components/Gauge/Gauge.tsx index 1a611d79783..b658ef4e023 100644 --- a/public/app/viz/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -1,15 +1,9 @@ import React, { PureComponent } from 'react'; import $ from 'jquery'; -import { - BasicGaugeColor, - Threshold, - TimeSeriesVMs, - MappingType, - ValueMapping, - getValueFormat, - Theme, - Themes, -} from '@grafana/ui'; + +import { ValueMapping, Threshold, Theme, MappingType, BasicGaugeColor, Themes } from '../../types/panel'; +import { TimeSeriesVMs } from '../../types/series'; +import { getValueFormat } from '../../utils/valueFormats/valueFormats'; export interface Props { decimals: number; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 88959bd8cb9..584992f8803 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -22,3 +22,4 @@ export { Graph } from './Graph/Graph'; export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup'; export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid'; export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor'; +export { Gauge } from './Gauge/Gauge'; diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index cfce719b5a6..eb00caf55fb 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -1,8 +1,7 @@ import React, { PureComponent } from 'react'; -import { PanelProps, NullValueMode } from '@grafana/ui'; +import { PanelProps, NullValueMode, Gauge } from '@grafana/ui'; -import { getTimeSeriesVMs } from 'app/viz/state/timeSeries'; -import Gauge from 'app/viz/Gauge'; +import { getTimeSeriesVMs } from './timeSeries'; import { GaugeOptions } from './types'; interface Props extends PanelProps {} diff --git a/public/app/viz/state/timeSeries.ts b/public/app/plugins/panel/gauge/timeSeries.ts similarity index 100% rename from public/app/viz/state/timeSeries.ts rename to public/app/plugins/panel/gauge/timeSeries.ts From a6e2be862c099ead673373f7774189ab6c9e4b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 17 Jan 2019 14:04:39 +0100 Subject: [PATCH 103/156] Added typings and refactored valuemappings code --- .../grafana-ui/src/components/Gauge/Gauge.tsx | 115 +++++++++++++----- packages/grafana-ui/src/types/series.ts | 5 +- .../grafana-ui/src/utils/processTimeSeries.ts | 4 +- public/app/plugins/panel/gauge/timeSeries.ts | 4 +- 4 files changed, 92 insertions(+), 36 deletions(-) diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index b658ef4e023..aefd6ed7882 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -1,10 +1,21 @@ import React, { PureComponent } from 'react'; import $ from 'jquery'; -import { ValueMapping, Threshold, Theme, MappingType, BasicGaugeColor, Themes } from '../../types/panel'; +import { + ValueMapping, + Threshold, + Theme, + MappingType, + BasicGaugeColor, + Themes, + ValueMap, + RangeMap, +} from '../../types/panel'; import { TimeSeriesVMs } from '../../types/series'; import { getValueFormat } from '../../utils/valueFormats/valueFormats'; +type TimeSeriesValue = string | number | null; + export interface Props { decimals: number; height: number; @@ -47,56 +58,100 @@ export class Gauge extends PureComponent { this.draw(); } - formatWithMappings(mappings, value) { - const valueMaps = mappings.filter(m => m.type === MappingType.ValueToText); - const rangeMaps = mappings.filter(m => m.type === MappingType.RangeToText); + addValueToTextMappingText( + allTexts: Array<{ text: string; type: MappingType }>, + valueToTextMapping: ValueMap, + value: TimeSeriesValue + ) { + if (!valueToTextMapping.value) { + return allTexts; + } - const valueMap = valueMaps.map(mapping => { - if (mapping.value && value === mapping.value) { - return mapping.text; - } - })[0]; + const valueAsNumber = parseFloat(value as string); + const valueToTextMappingAsNumber = parseFloat(valueToTextMapping.value as string); - const rangeMap = rangeMaps.map(mapping => { - if (mapping.from && mapping.to && value > mapping.from && value < mapping.to) { - return mapping.text; - } - })[0]; + if (isNaN(valueAsNumber) || isNaN(valueToTextMappingAsNumber)) { + return allTexts; + } - return { rangeMap, valueMap }; + if (valueAsNumber !== valueToTextMappingAsNumber) { + return allTexts; + } + + return allTexts.concat({ text: valueToTextMapping.text, type: MappingType.ValueToText }); } - formatValue(value) { + addRangeToTextMappingText( + allTexts: Array<{ text: string; type: MappingType }>, + rangeToTextMapping: RangeMap, + value: TimeSeriesValue + ) { + if ( + rangeToTextMapping.from && + rangeToTextMapping.to && + value && + value >= rangeToTextMapping.from && + value <= rangeToTextMapping.to + ) { + return allTexts.concat({ text: rangeToTextMapping.text, type: MappingType.RangeToText }); + } + + return allTexts; + } + + getAllMappingTexts(valueMappings: ValueMapping[], value: TimeSeriesValue) { + const allMappingTexts = valueMappings.reduce( + (allTexts, valueMapping) => { + if (valueMapping.type === MappingType.ValueToText) { + allTexts = this.addValueToTextMappingText(allTexts, valueMapping as ValueMap, value); + } else if (valueMapping.type === MappingType.RangeToText) { + allTexts = this.addRangeToTextMappingText(allTexts, valueMapping as RangeMap, value); + } + + return allTexts; + }, + [] as Array<{ text: string; type: MappingType }> + ); + + allMappingTexts.sort((t1, t2) => { + return t1.type - t2.type; + }); + + return allMappingTexts; + } + + formatWithValueMappings(valueMappings: ValueMapping[], value: TimeSeriesValue) { + return this.getAllMappingTexts(valueMappings, value)[0]; + } + + formatValue(value: TimeSeriesValue) { const { decimals, valueMappings, prefix, suffix, unit } = this.props; - const formatFunc = getValueFormat(unit); - const formattedValue = formatFunc(value, decimals); + if (isNaN(value as number)) { + return value; + } if (valueMappings.length > 0) { - const { rangeMap, valueMap } = this.formatWithMappings(valueMappings, formattedValue); - - if (valueMap) { - return `${prefix} ${valueMap} ${suffix}`; - } else if (rangeMap) { - return `${prefix} ${rangeMap} ${suffix}`; + const valueMappedValue = this.formatWithValueMappings(valueMappings, value); + if (valueMappedValue) { + return `${prefix} ${valueMappedValue.text} ${suffix}`; } } - if (isNaN(value)) { - return value; - } + const formatFunc = getValueFormat(unit); + const formattedValue = formatFunc(value as number, decimals); return `${prefix} ${formattedValue} ${suffix}`; } - getFontColor(value: string | number) { + getFontColor(value: TimeSeriesValue) { const { thresholds } = this.props; if (thresholds.length === 1) { return thresholds[0].color; } - const atThreshold = thresholds.filter(threshold => value < threshold.value); + const atThreshold = thresholds.filter(threshold => (value as number) < threshold.value); if (atThreshold.length > 0) { const nearestThreshold = atThreshold.sort((t1, t2) => t1.value - t2.value)[0]; @@ -120,7 +175,7 @@ export class Gauge extends PureComponent { theme, } = this.props; - let value: string | number = ''; + let value: TimeSeriesValue = ''; if (timeSeries[0]) { value = timeSeries[0].stats[stat]; diff --git a/packages/grafana-ui/src/types/series.ts b/packages/grafana-ui/src/types/series.ts index 49662e9872d..5cad1e4a72a 100644 --- a/packages/grafana-ui/src/types/series.ts +++ b/packages/grafana-ui/src/types/series.ts @@ -21,9 +21,12 @@ export interface TimeSeriesVM { color: string; data: TimeSeriesValue[][]; stats: TimeSeriesStats; + allIsNull: boolean; + allIsZero: boolean; } export interface TimeSeriesStats { + [key: string]: number | null; total: number | null; max: number | null; min: number | null; @@ -36,8 +39,6 @@ export interface TimeSeriesStats { range: number | null; timeStep: number; count: number; - allIsNull: boolean; - allIsZero: boolean; } export enum NullValueMode { diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index e92aaf0c1a6..7254354a21b 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -151,6 +151,8 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O data: result, label: label, color: colorPalette[colorIndex], + allIsZero, + allIsNull, stats: { total, min, @@ -164,8 +166,6 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O range, count, first, - allIsZero, - allIsNull, }, }; }); diff --git a/public/app/plugins/panel/gauge/timeSeries.ts b/public/app/plugins/panel/gauge/timeSeries.ts index 5f27974a33b..18054fe0d5e 100644 --- a/public/app/plugins/panel/gauge/timeSeries.ts +++ b/public/app/plugins/panel/gauge/timeSeries.ts @@ -145,6 +145,8 @@ export function getTimeSeriesVMs({ timeSeries, nullValueMode }: Options): TimeSe data: result, label: label, color: colors[colorIndex], + allIsZero, + allIsNull, stats: { total, min, @@ -158,8 +160,6 @@ export function getTimeSeriesVMs({ timeSeries, nullValueMode }: Options): TimeSe range, count, first, - allIsZero, - allIsNull, }, }; }); From 4f6e87bbbf1ea66dfd780956caaf09c15649c41f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 17 Jan 2019 14:48:20 +0100 Subject: [PATCH 104/156] Small refactor of Gauge and tests --- .../src/components/Gauge/Gauge.test.tsx | 78 +++++++++++++++++++ .../grafana-ui/src/components/Gauge/Gauge.tsx | 50 +++++------- 2 files changed, 99 insertions(+), 29 deletions(-) diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index 999f3f581ab..84f6b921a38 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -3,6 +3,7 @@ import { shallow } from 'enzyme'; import { Gauge, Props } from './Gauge'; import { TimeSeriesVMs } from '../../types/series'; +import { ValueMapping, MappingType } from '../../types'; jest.mock('jquery', () => ({ plot: jest.fn(), @@ -68,3 +69,80 @@ describe('Get font color', () => { expect(instance.getFontColor(6.5)).toEqual('#EAB839'); }); }); + +describe('Format value with value mappings', () => { + it('should return undefined with no valuemappings', () => { + const valueMappings: ValueMapping[] = []; + const value = 10; + const { instance } = setup({ valueMappings }); + + const result = instance.getFirstFormattedValueMapping(valueMappings, value); + + expect(result).toBeUndefined(); + }); + + it('should return undefined with no matching valuemappings', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + { id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' }, + ]; + const value = 10; + const { instance } = setup({ valueMappings }); + + const result = instance.getFirstFormattedValueMapping(valueMappings, value); + + expect(result).toBeUndefined(); + }); + + it('should return first matching mapping with lowest id', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, + { id: 1, operator: '', text: 'tio', type: MappingType.ValueToText, value: '10' }, + ]; + const value = 10; + const { instance } = setup({ valueMappings }); + + const result = instance.getFirstFormattedValueMapping(valueMappings, value); + + expect(result.text).toEqual('1-20'); + }); + + it('should return rangeToText mapping where value equals to', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '1-10', type: MappingType.RangeToText, from: '1', to: '10' }, + { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + ]; + const value = 10; + const { instance } = setup({ valueMappings }); + + const result = instance.getFirstFormattedValueMapping(valueMappings, value); + + expect(result.text).toEqual('1-10'); + }); + + it('should return rangeToText mapping where value equals from', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '10-20', type: MappingType.RangeToText, from: '10', to: '20' }, + { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + ]; + const value = 10; + const { instance } = setup({ valueMappings }); + + const result = instance.getFirstFormattedValueMapping(valueMappings, value); + + expect(result.text).toEqual('10-20'); + }); + + it('should return rangeToText mapping where value is between from and to', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, + { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + ]; + const value = 10; + const { instance } = setup({ valueMappings }); + + const result = instance.getFirstFormattedValueMapping(valueMappings, value); + + expect(result.text).toEqual('1-20'); + }); +}); diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index aefd6ed7882..6a219a580c7 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -58,34 +58,26 @@ export class Gauge extends PureComponent { this.draw(); } - addValueToTextMappingText( - allTexts: Array<{ text: string; type: MappingType }>, - valueToTextMapping: ValueMap, - value: TimeSeriesValue - ) { + addValueToTextMappingText(allValueMappings: ValueMapping[], valueToTextMapping: ValueMap, value: TimeSeriesValue) { if (!valueToTextMapping.value) { - return allTexts; + return allValueMappings; } const valueAsNumber = parseFloat(value as string); const valueToTextMappingAsNumber = parseFloat(valueToTextMapping.value as string); if (isNaN(valueAsNumber) || isNaN(valueToTextMappingAsNumber)) { - return allTexts; + return allValueMappings; } if (valueAsNumber !== valueToTextMappingAsNumber) { - return allTexts; + return allValueMappings; } - return allTexts.concat({ text: valueToTextMapping.text, type: MappingType.ValueToText }); + return allValueMappings.concat(valueToTextMapping); } - addRangeToTextMappingText( - allTexts: Array<{ text: string; type: MappingType }>, - rangeToTextMapping: RangeMap, - value: TimeSeriesValue - ) { + addRangeToTextMappingText(allValueMappings: ValueMapping[], rangeToTextMapping: RangeMap, value: TimeSeriesValue) { if ( rangeToTextMapping.from && rangeToTextMapping.to && @@ -93,35 +85,35 @@ export class Gauge extends PureComponent { value >= rangeToTextMapping.from && value <= rangeToTextMapping.to ) { - return allTexts.concat({ text: rangeToTextMapping.text, type: MappingType.RangeToText }); + return allValueMappings.concat(rangeToTextMapping); } - return allTexts; + return allValueMappings; } - getAllMappingTexts(valueMappings: ValueMapping[], value: TimeSeriesValue) { - const allMappingTexts = valueMappings.reduce( - (allTexts, valueMapping) => { + getAllFormattedValueMappings(valueMappings: ValueMapping[], value: TimeSeriesValue) { + const allFormattedValueMappings = valueMappings.reduce( + (allValueMappings, valueMapping) => { if (valueMapping.type === MappingType.ValueToText) { - allTexts = this.addValueToTextMappingText(allTexts, valueMapping as ValueMap, value); + allValueMappings = this.addValueToTextMappingText(allValueMappings, valueMapping as ValueMap, value); } else if (valueMapping.type === MappingType.RangeToText) { - allTexts = this.addRangeToTextMappingText(allTexts, valueMapping as RangeMap, value); + allValueMappings = this.addRangeToTextMappingText(allValueMappings, valueMapping as RangeMap, value); } - return allTexts; + return allValueMappings; }, - [] as Array<{ text: string; type: MappingType }> + [] as ValueMapping[] ); - allMappingTexts.sort((t1, t2) => { - return t1.type - t2.type; + allFormattedValueMappings.sort((t1, t2) => { + return t1.id - t2.id; }); - return allMappingTexts; + return allFormattedValueMappings; } - formatWithValueMappings(valueMappings: ValueMapping[], value: TimeSeriesValue) { - return this.getAllMappingTexts(valueMappings, value)[0]; + getFirstFormattedValueMapping(valueMappings: ValueMapping[], value: TimeSeriesValue) { + return this.getAllFormattedValueMappings(valueMappings, value)[0]; } formatValue(value: TimeSeriesValue) { @@ -132,7 +124,7 @@ export class Gauge extends PureComponent { } if (valueMappings.length > 0) { - const valueMappedValue = this.formatWithValueMappings(valueMappings, value); + const valueMappedValue = this.getFirstFormattedValueMapping(valueMappings, value); if (valueMappedValue) { return `${prefix} ${valueMappedValue.text} ${suffix}`; } From 8ccf212f343cfe77c9184ca7ce4d40a722df6624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 17 Jan 2019 15:14:07 +0100 Subject: [PATCH 105/156] Added tests for formatted value --- .../src/components/Gauge/Gauge.test.tsx | 60 +++++++++++++++++-- .../grafana-ui/src/components/Gauge/Gauge.tsx | 20 ++++--- 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index 84f6b921a38..f8f545694dc 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -73,7 +73,7 @@ describe('Get font color', () => { describe('Format value with value mappings', () => { it('should return undefined with no valuemappings', () => { const valueMappings: ValueMapping[] = []; - const value = 10; + const value = '10'; const { instance } = setup({ valueMappings }); const result = instance.getFirstFormattedValueMapping(valueMappings, value); @@ -86,7 +86,7 @@ describe('Format value with value mappings', () => { { id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, { id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' }, ]; - const value = 10; + const value = '10'; const { instance } = setup({ valueMappings }); const result = instance.getFirstFormattedValueMapping(valueMappings, value); @@ -99,7 +99,7 @@ describe('Format value with value mappings', () => { { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, { id: 1, operator: '', text: 'tio', type: MappingType.ValueToText, value: '10' }, ]; - const value = 10; + const value = '10'; const { instance } = setup({ valueMappings }); const result = instance.getFirstFormattedValueMapping(valueMappings, value); @@ -112,7 +112,7 @@ describe('Format value with value mappings', () => { { id: 0, operator: '', text: '1-10', type: MappingType.RangeToText, from: '1', to: '10' }, { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, ]; - const value = 10; + const value = '10'; const { instance } = setup({ valueMappings }); const result = instance.getFirstFormattedValueMapping(valueMappings, value); @@ -125,7 +125,7 @@ describe('Format value with value mappings', () => { { id: 0, operator: '', text: '10-20', type: MappingType.RangeToText, from: '10', to: '20' }, { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, ]; - const value = 10; + const value = '10'; const { instance } = setup({ valueMappings }); const result = instance.getFirstFormattedValueMapping(valueMappings, value); @@ -138,7 +138,7 @@ describe('Format value with value mappings', () => { { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, ]; - const value = 10; + const value = '10'; const { instance } = setup({ valueMappings }); const result = instance.getFirstFormattedValueMapping(valueMappings, value); @@ -146,3 +146,51 @@ describe('Format value with value mappings', () => { expect(result.text).toEqual('1-20'); }); }); + +describe('Format value', () => { + it('should return if value isNaN', () => { + const valueMappings: ValueMapping[] = []; + const value = 'N/A'; + const { instance } = setup({ valueMappings }); + + const result = instance.formatValue(value); + + expect(result).toEqual('N/A'); + }); + + it('should return formatted value if there are no value mappings', () => { + const valueMappings: ValueMapping[] = []; + const value = '6'; + const { instance } = setup({ valueMappings, decimals: 1 }); + + const result = instance.formatValue(value); + + expect(result).toEqual(' 6.0 '); + }); + + it('should return formatted value if there are no matching value mappings', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + { id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' }, + ]; + const value = '10'; + const { instance } = setup({ valueMappings, decimals: 1 }); + + const result = instance.formatValue(value); + + expect(result).toEqual(' 10.0 '); + }); + + it('should return mapped value if there are matching value mappings', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, + { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + ]; + const value = '11'; + const { instance } = setup({ valueMappings, decimals: 1 }); + + const result = instance.formatValue(value); + + expect(result).toEqual(' 1-20 '); + }); +}); diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index 6a219a580c7..c590b1ad9b7 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -78,13 +78,19 @@ export class Gauge extends PureComponent { } addRangeToTextMappingText(allValueMappings: ValueMapping[], rangeToTextMapping: RangeMap, value: TimeSeriesValue) { - if ( - rangeToTextMapping.from && - rangeToTextMapping.to && - value && - value >= rangeToTextMapping.from && - value <= rangeToTextMapping.to - ) { + if (!rangeToTextMapping.from || !rangeToTextMapping.to || !value) { + return allValueMappings; + } + + const valueAsNumber = parseFloat(value as string); + const fromAsNumber = parseFloat(rangeToTextMapping.from as string); + const toAsNumber = parseFloat(rangeToTextMapping.to as string); + + if (isNaN(valueAsNumber) || isNaN(fromAsNumber) || isNaN(toAsNumber)) { + return allValueMappings; + } + + if (valueAsNumber >= fromAsNumber && valueAsNumber <= toAsNumber) { return allValueMappings.concat(rangeToTextMapping); } From 5448b72f7ca1c58dcee38213ccf7453156624e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 18 Jan 2019 06:57:00 +0100 Subject: [PATCH 106/156] Passed the theme to Gauge --- public/app/plugins/panel/gauge/GaugePanel.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index eb00caf55fb..52ee273ef21 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -1,12 +1,17 @@ import React, { PureComponent } from 'react'; -import { PanelProps, NullValueMode, Gauge } from '@grafana/ui'; +import { PanelProps, NullValueMode, Gauge, Themes } from '@grafana/ui'; import { getTimeSeriesVMs } from './timeSeries'; import { GaugeOptions } from './types'; +import { contextSrv } from 'app/core/core'; interface Props extends PanelProps {} export class GaugePanel extends PureComponent { + getTheme() { + return contextSrv.user.lightTheme ? Themes.Light : Themes.Dark; + } + render() { const { timeSeries, width, height, onInterpolate, options } = this.props; @@ -26,6 +31,7 @@ export class GaugePanel extends PureComponent { height={height} prefix={prefix} suffix={suffix} + theme={this.getTheme()} /> ); } From c17ccf2289945792c1a399bf56c590b169c82baa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 18 Jan 2019 07:10:00 +0100 Subject: [PATCH 107/156] Make sure we do not change -Infinity --- .../src/components/ThresholdsEditor/ThresholdsEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index bd4f83f4dba..590aca5c7a1 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -105,7 +105,7 @@ export class ThresholdsEditor extends PureComponent { const value = isNaN(parsedValue) ? null : parsedValue; const newThresholds = thresholds.map(t => { - if (t === threshold) { + if (t === threshold && t.index !== 0) { t = { ...t, value: value as number }; } From 4cc0be2568148b8d480b626ecd271cf1530be3a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 18 Jan 2019 09:54:25 +0100 Subject: [PATCH 108/156] Redid logic for fontcolor and thresholds in Gauge and added tests --- .../src/components/Gauge/Gauge.test.tsx | 36 +++++++++++++-- .../grafana-ui/src/components/Gauge/Gauge.tsx | 46 ++++++++++++------- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index f8f545694dc..b3396841d4d 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -45,7 +45,7 @@ describe('Get font color', () => { expect(instance.getFontColor(49)).toEqual('#7EB26D'); }); - it('should get the next threshold color if value is same as a threshold', () => { + it('should get the threshold color if value is same as a threshold', () => { const { instance } = setup({ thresholds: [ { index: 2, value: 75, color: '#6ED0E0' }, @@ -54,10 +54,10 @@ describe('Get font color', () => { ], }); - expect(instance.getFontColor(50)).toEqual('#6ED0E0'); + expect(instance.getFontColor(50)).toEqual('#EAB839'); }); - it('should get the nearest threshold color', () => { + it('should get the nearest threshold color between thresholds', () => { const { instance } = setup({ thresholds: [ { index: 2, value: 75, color: '#6ED0E0' }, @@ -66,7 +66,35 @@ describe('Get font color', () => { ], }); - expect(instance.getFontColor(6.5)).toEqual('#EAB839'); + expect(instance.getFontColor(55)).toEqual('#EAB839'); + }); +}); + +describe('Get thresholds formatted', () => { + it('should return first thresholds color for min and max', () => { + const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] }); + + expect(instance.getFormattedThresholds()).toEqual([ + { value: 0, color: '#7EB26D' }, + { value: 100, color: '#7EB26D' }, + ]); + }); + + it('should get the correct formatted values when thresholds are added', () => { + const { instance } = setup({ + thresholds: [ + { index: 2, value: 75, color: '#6ED0E0' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, + ], + }); + + expect(instance.getFormattedThresholds()).toEqual([ + { value: 0, color: '#7EB26D' }, + { value: 50, color: '#7EB26D' }, + { value: 75, color: '#EAB839' }, + { value: 100, color: '#6ED0E0' }, + ]); }); }); diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index c590b1ad9b7..8013387812a 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -149,16 +149,42 @@ export class Gauge extends PureComponent { return thresholds[0].color; } - const atThreshold = thresholds.filter(threshold => (value as number) < threshold.value); + const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0]; + if (atThreshold) { + return atThreshold.color; + } - if (atThreshold.length > 0) { - const nearestThreshold = atThreshold.sort((t1, t2) => t1.value - t2.value)[0]; + const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value); + + if (belowThreshold.length > 0) { + const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0]; return nearestThreshold.color; } return BasicGaugeColor.Red; } + getFormattedThresholds() { + const { maxValue, minValue, thresholds } = this.props; + + const thresholdsSortedByIndex = [...thresholds].sort((t1, t2) => t1.index - t2.index); + const lastThreshold = thresholdsSortedByIndex[thresholdsSortedByIndex.length - 1]; + + const formattedThresholds = [ + ...thresholdsSortedByIndex.map(threshold => { + if (threshold.index === 0) { + return { value: minValue, color: threshold.color }; + } + + const previousThreshold = thresholdsSortedByIndex[threshold.index - 1]; + return { value: threshold.value, color: previousThreshold.color }; + }), + { value: maxValue, color: lastThreshold.color }, + ]; + + return formattedThresholds; + } + draw() { const { maxValue, @@ -166,7 +192,6 @@ export class Gauge extends PureComponent { timeSeries, showThresholdLabels, showThresholdMarkers, - thresholds, width, height, stat, @@ -190,17 +215,6 @@ export class Gauge extends PureComponent { const thresholdMarkersWidth = gaugeWidth / 5; const thresholdLabelFontSize = fontSize / 2.5; - const formattedThresholds = [ - { value: minValue, color: thresholds.length === 1 ? thresholds[0].color : BasicGaugeColor.Green }, - ...thresholds.map((threshold, index) => { - return { - value: threshold.value, - color: thresholds[index].color, - }; - }), - { value: maxValue, color: thresholds.length === 1 ? thresholds[0].color : BasicGaugeColor.Red }, - ]; - const options = { series: { gauges: { @@ -217,7 +231,7 @@ export class Gauge extends PureComponent { layout: { margin: 0, thresholdWidth: 0 }, cell: { border: { width: 0 } }, threshold: { - values: formattedThresholds, + values: this.getFormattedThresholds(), label: { show: showThresholdLabels, margin: thresholdMarkersWidth + 1, From f2498645d071afb210b5ccbda97efd0b9057b9fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Jan 2019 11:20:55 +0100 Subject: [PATCH 109/156] Fixed issue with explore angular query editor support introduced by recent angular query editor changes --- .../features/dashboard/panel_editor/QueryEditorRow.tsx | 2 +- public/app/features/explore/QueryEditor.tsx | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx index 1289ad3bac7..a7638e82a23 100644 --- a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx +++ b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx @@ -51,7 +51,7 @@ export class QueryEditorRow extends PureComponent { target: query, panel: panel, refresh: () => panel.refresh(), - render: () => panel.render, + render: () => panel.render(), events: panel.events, }; } diff --git a/public/app/features/explore/QueryEditor.tsx b/public/app/features/explore/QueryEditor.tsx index 3414d93a8ad..7bca9cab99b 100644 --- a/public/app/features/explore/QueryEditor.tsx +++ b/public/app/features/explore/QueryEditor.tsx @@ -3,7 +3,6 @@ import React, { PureComponent } from 'react'; // Services import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; -import { getIntervals } from 'app/core/utils/explore'; import { getTimeSrv } from 'app/features/dashboard/time_srv'; // Types @@ -37,8 +36,9 @@ export default class QueryEditor extends PureComponent { const template = ' '; const target = { datasource: datasource.name, ...initialQuery }; const scopeProps = { - target, ctrl: { + datasource, + target, refresh: () => { this.props.onQueryChange(target, false); this.props.onExecuteQuery(); @@ -48,11 +48,7 @@ export default class QueryEditor extends PureComponent { datasource, targets: [target], }, - dashboard: { - getNextQueryLetter: x => '', - }, - hideEditorRowActions: true, - ...getIntervals(range, (datasource || {}).interval, null), // Possible to get resolution? + dashboard: {}, }, }; From f16101101d2035f6e5ce1f06f4e40efe8748fae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Jan 2019 11:58:29 +0100 Subject: [PATCH 110/156] Minor refactoring and name changes --- .../grafana-ui/src/components/Gauge/Gauge.tsx | 9 +- packages/grafana-ui/src/types/panel.ts | 4 +- .../grafana-ui/src/utils/processTimeSeries.ts | 9 +- public/app/core/services/context_srv.ts | 5 + public/app/plugins/panel/gauge/GaugePanel.tsx | 20 ++- public/app/plugins/panel/gauge/timeSeries.ts | 168 ------------------ .../app/plugins/panel/graph2/GraphPanel.tsx | 2 - 7 files changed, 29 insertions(+), 188 deletions(-) delete mode 100644 public/app/plugins/panel/gauge/timeSeries.ts diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index 8013387812a..63d875e9cd5 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -4,10 +4,10 @@ import $ from 'jquery'; import { ValueMapping, Threshold, - Theme, + ThemeName, MappingType, BasicGaugeColor, - Themes, + ThemeNames, ValueMap, RangeMap, } from '../../types/panel'; @@ -31,7 +31,7 @@ export interface Props { suffix: string; unit: string; width: number; - theme?: Theme; + theme?: ThemeName; } export class Gauge extends PureComponent { @@ -48,6 +48,7 @@ export class Gauge extends PureComponent { thresholds: [], unit: 'none', stat: 'avg', + theme: ThemeNames.Dark, }; componentDidMount() { @@ -207,7 +208,7 @@ export class Gauge extends PureComponent { } const dimension = Math.min(width, height * 1.3); - const backgroundColor = theme === Themes.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)'; + const backgroundColor = theme === ThemeNames.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)'; const fontScale = parseInt('80', 10) / 100; const fontSize = Math.min(dimension / 5, 100) * fontScale; const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1; diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 340bec9d37b..881bf920c27 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -67,9 +67,9 @@ export interface RangeMap extends BaseMap { to: string; } -export type Theme = 'dark' | 'light'; +export type ThemeName = 'dark' | 'light'; -export enum Themes { +export enum ThemeNames { Dark = 'dark', Light = 'light', } diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index 7254354a21b..7b0c8e55239 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -1,18 +1,19 @@ // Libraries import _ from 'lodash'; +import { colors } from './colors'; + // Types import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types'; interface Options { timeSeries: TimeSeries[]; nullValueMode: NullValueMode; - colorPalette: string[]; } -export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: Options): TimeSeriesVMs { +export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeSeriesVMs { const vmSeries = timeSeries.map((item, index) => { - const colorIndex = index % colorPalette.length; + const colorIndex = index % colors.length; const label = item.target; const result = []; @@ -150,7 +151,7 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O return { data: result, label: label, - color: colorPalette[colorIndex], + color: colors[colorIndex], allIsZero, allIsNull, stats: { diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index c4134598175..5353fb507cc 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -2,6 +2,7 @@ import config from 'app/core/config'; import _ from 'lodash'; import coreModule from 'app/core/core_module'; import store from 'app/core/store'; +import { ThemeNames, ThemeName } from '@grafana/ui'; export class User { isGrafanaAdmin: any; @@ -59,6 +60,10 @@ export class ContextSrv { this.sidemenu = !this.sidemenu; store.set('grafana.sidemenu', this.sidemenu); } + + getTheme(): ThemeName { + return this.user.lightTheme ? ThemeNames.Light : ThemeNames.Dark; + } } const contextSrv = new ContextSrv(); diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 52ee273ef21..8b62171f31b 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -1,16 +1,20 @@ +// Libraries import React, { PureComponent } from 'react'; -import { PanelProps, NullValueMode, Gauge, Themes } from '@grafana/ui'; -import { getTimeSeriesVMs } from './timeSeries'; -import { GaugeOptions } from './types'; +// Services & Utils import { contextSrv } from 'app/core/core'; +import { processTimeSeries } from '@grafana/ui'; + +// Components +import { Gauge } from '@grafana/ui'; + +// Types +import { GaugeOptions } from './types'; +import { PanelProps, NullValueMode } from '@grafana/ui/src/types'; interface Props extends PanelProps {} export class GaugePanel extends PureComponent { - getTheme() { - return contextSrv.user.lightTheme ? Themes.Light : Themes.Dark; - } render() { const { timeSeries, width, height, onInterpolate, options } = this.props; @@ -18,7 +22,7 @@ export class GaugePanel extends PureComponent { const prefix = onInterpolate(options.prefix); const suffix = onInterpolate(options.suffix); - const vmSeries = getTimeSeriesVMs({ + const vmSeries = processTimeSeries({ timeSeries: timeSeries, nullValueMode: NullValueMode.Ignore, }); @@ -31,7 +35,7 @@ export class GaugePanel extends PureComponent { height={height} prefix={prefix} suffix={suffix} - theme={this.getTheme()} + theme={contextSrv.getTheme()} /> ); } diff --git a/public/app/plugins/panel/gauge/timeSeries.ts b/public/app/plugins/panel/gauge/timeSeries.ts deleted file mode 100644 index 18054fe0d5e..00000000000 --- a/public/app/plugins/panel/gauge/timeSeries.ts +++ /dev/null @@ -1,168 +0,0 @@ -// Libraries -import _ from 'lodash'; - -// Utils -import { colors } from '@grafana/ui'; - -// Types -import { TimeSeries, TimeSeriesVMs, NullValueMode } from '@grafana/ui'; - -interface Options { - timeSeries: TimeSeries[]; - nullValueMode: NullValueMode; -} - -export function getTimeSeriesVMs({ timeSeries, nullValueMode }: Options): TimeSeriesVMs { - const vmSeries = timeSeries.map((item, index) => { - const colorIndex = index % colors.length; - const label = item.target; - const result = []; - - // stat defaults - let total = 0; - let max = -Number.MAX_VALUE; - let min = Number.MAX_VALUE; - let logmin = Number.MAX_VALUE; - let avg = null; - let current = null; - let first = null; - let delta = 0; - let diff = null; - let range = null; - let timeStep = Number.MAX_VALUE; - let allIsNull = true; - let allIsZero = true; - - const ignoreNulls = nullValueMode === NullValueMode.Ignore; - const nullAsZero = nullValueMode === NullValueMode.AsZero; - - let currentTime; - let currentValue; - let nonNulls = 0; - let previousTime; - let previousValue = 0; - let previousDeltaUp = true; - - for (let i = 0; i < item.datapoints.length; i++) { - currentValue = item.datapoints[i][0]; - currentTime = item.datapoints[i][1]; - - // Due to missing values we could have different timeStep all along the series - // so we have to find the minimum one (could occur with aggregators such as ZimSum) - if (previousTime !== undefined) { - const currentStep = currentTime - previousTime; - if (currentStep < timeStep) { - timeStep = currentStep; - } - } - - previousTime = currentTime; - - if (currentValue === null) { - if (ignoreNulls) { - continue; - } - if (nullAsZero) { - currentValue = 0; - } - } - - if (currentValue !== null) { - if (_.isNumber(currentValue)) { - total += currentValue; - allIsNull = false; - nonNulls++; - } - - if (currentValue > max) { - max = currentValue; - } - - if (currentValue < min) { - min = currentValue; - } - - if (first === null) { - first = currentValue; - } else { - if (previousValue > currentValue) { - // counter reset - previousDeltaUp = false; - if (i === item.datapoints.length - 1) { - // reset on last - delta += currentValue; - } - } else { - if (previousDeltaUp) { - delta += currentValue - previousValue; // normal increment - } else { - delta += currentValue; // account for counter reset - } - previousDeltaUp = true; - } - } - previousValue = currentValue; - - if (currentValue < logmin && currentValue > 0) { - logmin = currentValue; - } - - if (currentValue !== 0) { - allIsZero = false; - } - } - - result.push([currentTime, currentValue]); - } - - if (max === -Number.MAX_VALUE) { - max = null; - } - - if (min === Number.MAX_VALUE) { - min = null; - } - - if (result.length && !allIsNull) { - avg = total / nonNulls; - current = result[result.length - 1][1]; - if (current === null && result.length > 1) { - current = result[result.length - 2][1]; - } - } - - if (max !== null && min !== null) { - range = max - min; - } - - if (current !== null && first !== null) { - diff = current - first; - } - - const count = result.length; - - return { - data: result, - label: label, - color: colors[colorIndex], - allIsZero, - allIsNull, - stats: { - total, - min, - max, - current, - logmin, - avg, - diff, - delta, - timeStep, - range, - count, - first, - }, - }; - }); - - return vmSeries; -} diff --git a/public/app/plugins/panel/graph2/GraphPanel.tsx b/public/app/plugins/panel/graph2/GraphPanel.tsx index 28c17dbad2c..2fef35b4f5f 100644 --- a/public/app/plugins/panel/graph2/GraphPanel.tsx +++ b/public/app/plugins/panel/graph2/GraphPanel.tsx @@ -1,7 +1,6 @@ // Libraries import _ from 'lodash'; import React, { PureComponent } from 'react'; -import { colors } from '@grafana/ui'; // Utils import { processTimeSeries } from '@grafana/ui/src/utils'; @@ -23,7 +22,6 @@ export class GraphPanel extends PureComponent { const vmSeries = processTimeSeries({ timeSeries: timeSeries, nullValueMode: NullValueMode.Ignore, - colorPalette: colors, }); return ( From 11944adc9cf509c4e8ba4314a4a6d032db0bf074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 18 Jan 2019 12:03:16 +0100 Subject: [PATCH 111/156] fix: Hack for getting the same height in splitted view, view could use refactor IMHO #14853 --- public/app/features/explore/Explore.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index d77448c10d7..b6f57a76004 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -242,11 +242,14 @@ export class Explore extends React.PureComponent {
) : ( -
- -
+ <> +
+
+ +
+ )} {!datasourceMissing ? (
@@ -274,7 +277,11 @@ export class Explore extends React.PureComponent {
From 3a827fc2f128a366a30017728055af7e6f93fa47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Jan 2019 13:12:32 +0100 Subject: [PATCH 112/156] Added test case dashboard --- devenv/dev-dashboards/panel_tests_gauge.json | 1250 +++++++++++++++++ .../grafana-ui/src/utils/processTimeSeries.ts | 4 +- public/app/features/dashboard/panel_model.ts | 1 - public/app/plugins/panel/gauge/GaugePanel.tsx | 2 +- 4 files changed, 1253 insertions(+), 4 deletions(-) create mode 100644 devenv/dev-dashboards/panel_tests_gauge.json diff --git a/devenv/dev-dashboards/panel_tests_gauge.json b/devenv/dev-dashboards/panel_tests_gauge.json new file mode 100644 index 00000000000..c6e81ececc8 --- /dev/null +++ b/devenv/dev-dashboards/panel_tests_gauge.json @@ -0,0 +1,1250 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "iteration": 1547810606599, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 11, + "panels": [], + "title": "Value options tests", + "type": "row" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 5, + "x": 0, + "y": 1 + }, + "id": 2, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "2", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "ms", + "valueMappings": [] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Average, 2 decimals, ms unit", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 6, + "x": 5, + "y": 1 + }, + "id": 5, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "max", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "ms", + "valueMappings": [] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Max (90 ms), no decimals", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 5, + "x": 11, + "y": 1 + }, + "id": 6, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "p", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "s", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Current (10 ms), no unit, prefix (p), suffix (s)", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 4, + "w": 3, + "x": 16, + "y": 1 + }, + "id": 16, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 4, + "w": 5, + "x": 19, + "y": 1 + }, + "id": 18, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10,91" + } + ], + "timeFrom": "1h", + "timeShift": null, + "title": "", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 4, + "w": 3, + "x": 16, + "y": 5 + }, + "id": 17, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 4, + "w": 5, + "x": 19, + "y": 5 + }, + "id": 19, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10,81" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "", + "type": "gauge" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 15, + "panels": [], + "title": "Value Mappings", + "type": "row" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 4, + "x": 0, + "y": 10 + }, + "id": 12, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [ + { + "from": "", + "id": 1, + "operator": "", + "text": "TEN", + "to": "", + "type": 1, + "value": "10" + } + ] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "value mapping 10 -> TEN", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "description": "should read N/A", + "gridPos": { + "h": 8, + "w": 4, + "x": 4, + "y": 10 + }, + "id": 13, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [ + { + "from": "", + "id": 1, + "operator": "", + "text": "N/A", + "to": "", + "type": 1, + "value": "null" + } + ] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10,null,null,null,null" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "value mapping null -> N/A", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "description": "should read N/A", + "gridPos": { + "h": 8, + "w": 6, + "x": 8, + "y": 10 + }, + "id": 20, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [ + { + "from": "0", + "id": 1, + "operator": "", + "text": "OK", + "to": "10", + "type": 2, + "value": "null" + } + ] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10,null,null,null,null,10" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "value mapping range, 0-10 -> OK, value 10", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "description": "should read N/A", + "gridPos": { + "h": 8, + "w": 6, + "x": 14, + "y": 10 + }, + "id": 21, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [ + { + "from": "0", + "id": 1, + "operator": "", + "text": "OK", + "to": "90", + "type": 2, + "value": "null" + }, + { + "from": "90", + "id": 2, + "operator": "", + "text": "BAD", + "to": "100", + "type": 2, + "value": "" + } + ] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10,null,null,null,null,10,95" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "value mapping range, 90-100 -> BAD, value 90", + "type": "gauge" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 9, + "panels": [], + "title": "Templating & Repeat", + "type": "row" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 19 + }, + "id": 7, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "2", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "$Servers", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "ms", + "valueMappings": [] + }, + "repeat": "Servers", + "repeatDirection": "h", + "scopedVars": { + "Servers": { + "selected": false, + "text": "server1", + "value": "server1" + } + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "repeat $Servers", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 19 + }, + "id": 22, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "2", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "$Servers", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "ms", + "valueMappings": [] + }, + "repeat": null, + "repeatDirection": "h", + "repeatIteration": 1547810606599, + "repeatPanelId": 7, + "scopedVars": { + "Servers": { + "selected": false, + "text": "server2", + "value": "server2" + } + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "repeat $Servers", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 6, + "x": 12, + "y": 19 + }, + "id": 23, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "2", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "$Servers", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "ms", + "valueMappings": [] + }, + "repeat": null, + "repeatDirection": "h", + "repeatIteration": 1547810606599, + "repeatPanelId": 7, + "scopedVars": { + "Servers": { + "selected": false, + "text": "server3", + "value": "server3" + } + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "repeat $Servers", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 6, + "x": 18, + "y": 19 + }, + "id": 24, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "2", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "$Servers", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "ms", + "valueMappings": [] + }, + "repeat": null, + "repeatDirection": "h", + "repeatIteration": 1547810606599, + "repeatPanelId": 7, + "scopedVars": { + "Servers": { + "selected": false, + "text": "server4", + "value": "server4" + } + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "repeat $Servers", + "type": "gauge" + } + ], + "refresh": false, + "schemaVersion": 17, + "style": "dark", + "tags": [ + "gdev", + "panel-tests" + ], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "selected": true, + "tags": [], + "text": "All", + "value": [ + "$__all" + ] + }, + "hide": 0, + "includeAll": true, + "label": null, + "multi": true, + "name": "Servers", + "options": [ + { + "selected": true, + "text": "All", + "value": "$__all" + }, + { + "selected": false, + "text": "server1", + "value": "server1" + }, + { + "selected": false, + "text": "server2", + "value": "server2" + }, + { + "selected": false, + "text": "server3", + "value": "server3" + }, + { + "selected": false, + "text": "server4", + "value": "server4" + } + ], + "query": "server1,server2,server3,server4", + "skipUrlSync": false, + "type": "custom" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "Panel Tests - Gauge", + "uid": "_5rDmaQiz", + "version": 5 +} diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index 7b0c8e55239..f5389f1b2bd 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -50,8 +50,8 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS continue; } - if (typeof currentValue !== 'number') { - continue; + if (currentValue !== null && typeof currentValue !== 'number') { + throw {message: 'Time series contains non number values'}; } // Due to missing values we could have different timeStep all along the series diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index 2fec8e379dd..f60b207e015 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -52,7 +52,6 @@ const mustKeepProps: { [str: string]: boolean } = { hasRefreshed: true, events: true, cacheTimeout: true, - nullPointMode: true, cachedPluginOptions: true, transparent: true, }; diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 8b62171f31b..cd92f697ced 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -24,7 +24,7 @@ export class GaugePanel extends PureComponent { const vmSeries = processTimeSeries({ timeSeries: timeSeries, - nullValueMode: NullValueMode.Ignore, + nullValueMode: NullValueMode.Null, }); return ( From 46ff9dda71048a4847a78ef99a04c87b10524273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Jan 2019 13:24:26 +0100 Subject: [PATCH 113/156] Fixed scrollbar issue where it jumped to the top --- .../src/components/CustomScrollbar/CustomScrollbar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx index c3fb3f0f0ab..eb50944ad35 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx @@ -25,7 +25,6 @@ export class CustomScrollbar extends PureComponent { autoHideDuration: 200, autoMaxHeight: '100%', hideTracksWhenNotNeeded: false, - scrollTop: 0, setScrollTop: () => {}, autoHeightMin: '0' }; From 69e3dc2d4f0c278597be0dd90339b5e5bbb69a56 Mon Sep 17 00:00:00 2001 From: SamuelToh Date: Fri, 18 Jan 2019 22:46:43 +1000 Subject: [PATCH 114/156] 14947: fixed incorrect flag --- docs/sources/auth/gitlab.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/auth/gitlab.md b/docs/sources/auth/gitlab.md index 541aed3fd1f..b6028b0a2a7 100644 --- a/docs/sources/auth/gitlab.md +++ b/docs/sources/auth/gitlab.md @@ -47,7 +47,7 @@ authentication: ```bash [auth.gitlab] -enabled = false +enabled = true allow_sign_up = false client_id = GITLAB_APPLICATION_ID client_secret = GITLAB_SECRET From 5f4e4a813debd97331b17c06052f9741088aea34 Mon Sep 17 00:00:00 2001 From: flopp999 <21694965+flopp999@users.noreply.github.com> Date: Fri, 18 Jan 2019 14:06:15 +0100 Subject: [PATCH 115/156] change enabled to true if it is false it will not work --- docs/sources/auth/gitlab.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/auth/gitlab.md b/docs/sources/auth/gitlab.md index 541aed3fd1f..b6028b0a2a7 100644 --- a/docs/sources/auth/gitlab.md +++ b/docs/sources/auth/gitlab.md @@ -47,7 +47,7 @@ authentication: ```bash [auth.gitlab] -enabled = false +enabled = true allow_sign_up = false client_id = GITLAB_APPLICATION_ID client_secret = GITLAB_SECRET From 521c8f34f7ba8f3d503746f7faba2f49f8205d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Jan 2019 15:29:58 +0100 Subject: [PATCH 116/156] Moved add query button to the right --- .../dashboard/panel_editor/EditorTabBody.tsx | 13 +++++-------- .../dashboard/panel_editor/QueriesTab.tsx | 17 ++++++++--------- public/sass/components/_toolbar.scss | 6 ++++++ 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/public/app/features/dashboard/panel_editor/EditorTabBody.tsx b/public/app/features/dashboard/panel_editor/EditorTabBody.tsx index 59cdf6c1215..0413cae8a7b 100644 --- a/public/app/features/dashboard/panel_editor/EditorTabBody.tsx +++ b/public/app/features/dashboard/panel_editor/EditorTabBody.tsx @@ -111,14 +111,11 @@ export class EditorTabBody extends PureComponent { return ( <>
-
{heading}
- {renderToolbar && renderToolbar()} - {toolbarItems.length > 0 && ( - <> -
- {toolbarItems.map(item => this.renderButton(item))} - - )} +
+
{heading}
+ {renderToolbar && renderToolbar()} +
+ {toolbarItems.map(item => this.renderButton(item))}
diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index 5e90187d171..ca06098debd 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -18,7 +18,7 @@ import config from 'app/core/config'; // Types import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; -import { DataQuery, DataSourceSelectItem } from '@grafana/ui/src/types'; +import { DataQuery, DataSourceSelectItem } from '@grafana/ui/src/types'; import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; interface Props { @@ -133,14 +133,13 @@ export class QueriesTab extends PureComponent { return ( <> -
- {!isAddingMixed && ( - - )} - {isAddingMixed && this.renderMixedPicker()} -
+
+ {!isAddingMixed && ( + + )} + {isAddingMixed && this.renderMixedPicker()} ); }; diff --git a/public/sass/components/_toolbar.scss b/public/sass/components/_toolbar.scss index 5f557f89cad..14db85f7e65 100644 --- a/public/sass/components/_toolbar.scss +++ b/public/sass/components/_toolbar.scss @@ -16,6 +16,12 @@ padding-right: 20px; } +.toolbar__left { + display: flex; + flex-grow: 1; + align-items: center; +} + .toolbar__main { padding: 0 $input-padding-x; font-size: $font-size-md; From 98fabfae3e00f9876142ccde77313961494cb374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Jan 2019 17:19:35 +0100 Subject: [PATCH 117/156] wip: typings --- packages/grafana-ui/src/types/datasource.ts | 6 +- packages/grafana-ui/src/types/plugin.ts | 21 +++-- .../app/features/.all.ts@neomake_22624_74.ts | 14 +++ .../prometheus/components/PromQueryField.tsx | 8 +- .../datasource/prometheus/datasource.ts | 94 ++++++++++--------- .../plugins/datasource/prometheus/types.ts | 6 ++ .../datasource/testdata/QueryEditor.tsx | 11 +-- .../plugins/datasource/testdata/datasource.ts | 9 +- .../app/plugins/datasource/testdata/module.ts | 8 +- .../app/plugins/datasource/testdata/types.ts | 11 +++ 10 files changed, 112 insertions(+), 76 deletions(-) create mode 100644 public/app/features/.all.ts@neomake_22624_74.ts create mode 100644 public/app/plugins/datasource/prometheus/types.ts create mode 100644 public/app/plugins/datasource/testdata/types.ts diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index f47e2473a85..cb6115486b1 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/packages/grafana-ui/src/types/datasource.ts @@ -8,14 +8,14 @@ export interface DataQueryResponse { export interface DataQuery { refId: string; - [key: string]: any; + hide?: boolean; } -export interface DataQueryOptions { +export interface DataQueryOptions { timezone: string; range: TimeRange; rangeRaw: RawTimeRange; - targets: DataQuery[]; + targets: TQuery[]; panelId: number; dashboardId: number; cacheTimeout?: string; diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index 30a958420de..8ded328b2da 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -2,10 +2,11 @@ import { ComponentClass } from 'react'; import { PanelProps, PanelOptionsProps } from './panel'; import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint } from './datasource'; -export interface DataSourceApi { - name: string; - meta: PluginMeta; - pluginExports: PluginExports; +export interface DataSourceApi { + // set externally by grafana + name?: string; + meta?: PluginMeta; + pluginExports?: PluginExports; /** * min interval range @@ -15,7 +16,7 @@ export interface DataSourceApi { /** * Imports queries from a different datasource */ - importQueries?(queries: DataQuery[], originMeta: PluginMeta): Promise; + importQueries?(queries: TQuery[], originMeta: PluginMeta): Promise; /** * Initializes a datasource after instantiation @@ -25,7 +26,7 @@ export interface DataSourceApi { /** * Main metrics / data query action */ - query(options: DataQueryOptions): Promise; + query(options: DataQueryOptions): Promise; /** * Test & verify datasource settings & connection details @@ -35,12 +36,12 @@ export interface DataSourceApi { /** * Get hints for query improvements */ - getQueryHints(query: DataQuery, results: any[], ...rest: any): QueryHint[]; + getQueryHints?(query: TQuery, results: any[], ...rest: any): QueryHint[]; } -export interface QueryEditorProps { - datasource: DataSourceApi; - query: DataQuery; +export interface QueryEditorProps { + datasource: DSType; + query: TQuery; onExecuteQuery?: () => void; onQueryChange?: (value: DataQuery) => void; } diff --git a/public/app/features/.all.ts@neomake_22624_74.ts b/public/app/features/.all.ts@neomake_22624_74.ts new file mode 100644 index 00000000000..99de12bf215 --- /dev/null +++ b/public/app/features/.all.ts@neomake_22624_74.ts @@ -0,0 +1,14 @@ +import './annotations/all'; +import './templating/all'; +import './plugins/all'; +import './dashboard/all'; +import './playlist/all'; +import './panel/all'; +import './org/all'; +import './admin'; +import './alerting/NotificationsEditCtrl'; +import './alerting/NotificationsListCtrl'; +import './manage-dashboards'; +import './teams/CreateTeamCtrl'; +import './profile/all'; +import './datasources/settings/dsHttpSettings'; diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index 391d39836ca..6fd450394a3 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -11,7 +11,7 @@ import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/ import BracesPlugin from 'app/features/explore/slate-plugins/braces'; import RunnerPlugin from 'app/features/explore/slate-plugins/runner'; import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; -import { DataQuery } from '@grafana/ui/src/types'; +import { PromQuery } from '../types'; const HISTOGRAM_GROUP = '__histograms__'; const METRIC_MARK = 'metric'; @@ -88,13 +88,13 @@ interface CascaderOption { interface PromQueryFieldProps { datasource: any; error?: string | JSX.Element; - initialQuery: DataQuery; + initialQuery: PromQuery; hint?: any; history?: any[]; metricsByPrefix?: CascaderOption[]; onClickHintFix?: (action: any) => void; onPressEnter?: () => void; - onQueryChange?: (value: DataQuery, override?: boolean) => void; + onQueryChange?: (value: PromQuery, override?: boolean) => void; } interface PromQueryFieldState { @@ -166,7 +166,7 @@ class PromQueryField extends React.PureComponent - group.rules.filter(rule => rule.type === 'recording').reduce( - (acc, rule) => ({ - ...acc, - [rule.name]: rule.query, - }), - mapping - ), - {} - ); -} - -export function prometheusRegularEscape(value) { - if (typeof value === 'string') { - return value.replace(/'/g, "\\\\'"); - } - return value; -} - -export function prometheusSpecialRegexEscape(value) { - if (typeof value === 'string') { - return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&')); - } - return value; -} - -export class PrometheusDatasource { +export class PrometheusDatasource implements DataSourceApi { type: string; editorSrc: string; name: string; @@ -149,7 +116,7 @@ export class PrometheusDatasource { return this.templateSrv.variableExists(target.expr); } - query(options) { + query(options: DataQueryOptions) { const start = this.getPrometheusTime(options.range.from, false); const end = this.getPrometheusTime(options.range.to, true); @@ -423,7 +390,7 @@ export class PrometheusDatasource { }); } - getExploreState(queries: DataQuery[]): Partial { + getExploreState(queries: PromQuery[]): Partial { let state: Partial = { datasource: this.name }; if (queries && queries.length > 0) { const expandedQueries = queries.map(query => ({ @@ -438,7 +405,7 @@ export class PrometheusDatasource { return state; } - getQueryHints(query: DataQuery, result: any[]) { + getQueryHints(query: PromQuery, result: any[]) { return getQueryHints(query.expr || '', result, this); } @@ -457,7 +424,7 @@ export class PrometheusDatasource { }); } - modifyQuery(query: DataQuery, action: any): DataQuery { + modifyQuery(query: PromQuery, action: any): PromQuery { let expression = query.expr || ''; switch (action.type) { case 'ADD_FILTER': { @@ -507,3 +474,40 @@ export class PrometheusDatasource { return this.resultTransformer.getOriginalMetricName(labelData); } } + +export function alignRange(start, end, step) { + const alignedEnd = Math.ceil(end / step) * step; + const alignedStart = Math.floor(start / step) * step; + return { + end: alignedEnd, + start: alignedStart, + }; +} + +export function extractRuleMappingFromGroups(groups: any[]) { + return groups.reduce( + (mapping, group) => + group.rules.filter(rule => rule.type === 'recording').reduce( + (acc, rule) => ({ + ...acc, + [rule.name]: rule.query, + }), + mapping + ), + {} + ); +} + +export function prometheusRegularEscape(value) { + if (typeof value === 'string') { + return value.replace(/'/g, "\\\\'"); + } + return value; +} + +export function prometheusSpecialRegexEscape(value) { + if (typeof value === 'string') { + return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&')); + } + return value; +} diff --git a/public/app/plugins/datasource/prometheus/types.ts b/public/app/plugins/datasource/prometheus/types.ts new file mode 100644 index 00000000000..5bdc687d774 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/types.ts @@ -0,0 +1,6 @@ +import { DataQuery } from '@grafana/ui/src/types'; + +export interface PromQuery extends DataQuery { + expr: string; +} + diff --git a/public/app/plugins/datasource/testdata/QueryEditor.tsx b/public/app/plugins/datasource/testdata/QueryEditor.tsx index 25a811da42d..ef49505d2c6 100644 --- a/public/app/plugins/datasource/testdata/QueryEditor.tsx +++ b/public/app/plugins/datasource/testdata/QueryEditor.tsx @@ -10,18 +10,17 @@ import { FormLabel, Select, SelectOptionItem } from '@grafana/ui'; // Types import { QueryEditorProps } from '@grafana/ui/src/types'; - -interface Scenario { - id: string; - name: string; -} +import { TestDataDatasource } from './datasource'; +import { TestDataQuery, Scenario } from './types'; interface State { scenarioList: Scenario[]; current: Scenario | null; } -export class QueryEditor extends PureComponent { +type Props = QueryEditorProps; + +export class QueryEditor extends PureComponent { backendSrv: BackendSrv = getBackendSrv(); state: State = { diff --git a/public/app/plugins/datasource/testdata/datasource.ts b/public/app/plugins/datasource/testdata/datasource.ts index 989209792fb..c329389ee8c 100644 --- a/public/app/plugins/datasource/testdata/datasource.ts +++ b/public/app/plugins/datasource/testdata/datasource.ts @@ -1,15 +1,17 @@ import _ from 'lodash'; import TableModel from 'app/core/table_model'; +import { DataSourceApi, DataQueryOptions } from '@grafana/ui'; +import { TestDataQuery } from './types'; -class TestDataDatasource { - id: any; +export class TestDataDatasource implements DataSourceApi { + id: number; /** @ngInject */ constructor(instanceSettings, private backendSrv, private $q) { this.id = instanceSettings.id; } - query(options) { + query(options: DataQueryOptions) { const queries = _.filter(options.targets, item => { return item.hide !== true; }).map(item => { @@ -93,4 +95,3 @@ class TestDataDatasource { } } -export { TestDataDatasource }; diff --git a/public/app/plugins/datasource/testdata/module.ts b/public/app/plugins/datasource/testdata/module.ts index d3b376e3307..efd6c207407 100644 --- a/public/app/plugins/datasource/testdata/module.ts +++ b/public/app/plugins/datasource/testdata/module.ts @@ -1,6 +1,6 @@ import { TestDataDatasource } from './datasource'; -import { TestDataQueryCtrl } from './query_ctrl'; -// import { QueryEditor } from './QueryEditor'; +// import { TestDataQueryCtrl } from './query_ctrl'; +import { QueryEditor } from './QueryEditor'; class TestDataAnnotationsQueryCtrl { annotation: any; @@ -11,8 +11,8 @@ class TestDataAnnotationsQueryCtrl { } export { - // QueryEditor, + QueryEditor, TestDataDatasource as Datasource, - TestDataQueryCtrl as QueryCtrl, + // TestDataQueryCtrl as QueryCtrl, TestDataAnnotationsQueryCtrl as AnnotationsQueryCtrl, }; diff --git a/public/app/plugins/datasource/testdata/types.ts b/public/app/plugins/datasource/testdata/types.ts new file mode 100644 index 00000000000..e233c3ef7a0 --- /dev/null +++ b/public/app/plugins/datasource/testdata/types.ts @@ -0,0 +1,11 @@ +import { DataQuery } from '@grafana/ui/src/types'; + +export interface TestDataQuery extends DataQuery { + scenarioId: string; +} + +export interface Scenario { + id: string; + name: string; +} + From a69f79caed27b19b7f0652928823e51b4262f407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Jan 2019 17:43:58 +0100 Subject: [PATCH 118/156] wip: more typings --- public/app/features/dashboard/panel_model.ts | 2 -- public/app/plugins/datasource/loki/datasource.ts | 15 ++++++++++----- public/app/plugins/datasource/loki/types.ts | 6 ++++++ 3 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 public/app/plugins/datasource/loki/types.ts diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index b54cabc9012..b7e8a68c722 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -243,8 +243,6 @@ export class PanelModel { addQuery(query?: Partial) { query = query || { refId: 'A' }; query.refId = this.getNextQueryLetter(); - query.isNew = true; - this.targets.push(query); } diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 893ce649613..6c5bda8bcf2 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -1,13 +1,18 @@ +// Libraries import _ from 'lodash'; +// Services & Utils import * as dateMath from 'app/core/utils/datemath'; -import { LogsStream, LogsModel, makeSeriesForLogs } from 'app/core/logs_model'; -import { PluginMeta, DataQuery } from '@grafana/ui/src/types'; import { addLabelToSelector } from 'app/plugins/datasource/prometheus/add_label_to_query'; - import LanguageProvider from './language_provider'; import { mergeStreamsToLogs } from './result_transformer'; import { formatQuery, parseQuery } from './query_utils'; +import { makeSeriesForLogs } from 'app/core/logs_model'; + +// Types +import { LogsStream, LogsModel } from 'app/core/logs_model'; +import { PluginMeta, DataQueryOptions, DataSourceApi } from '@grafana/ui/src/types'; +import { LokiQuery } from './types'; export const DEFAULT_MAX_LINES = 1000; @@ -27,7 +32,7 @@ function serializeParams(data: any) { .join('&'); } -export default class LokiDatasource { +export default class LokiDatasource implements DataSourceApi { languageProvider: LanguageProvider; maxLines: number; @@ -68,7 +73,7 @@ export default class LokiDatasource { }; } - query(options): Promise<{ data: LogsStream[] }> { + query(options: DataQueryOptions): Promise<{ data: LogsStream[] }> { const queryTargets = options.targets .filter(target => target.expr) .map(target => this.prepareQueryTarget(target, options)); diff --git a/public/app/plugins/datasource/loki/types.ts b/public/app/plugins/datasource/loki/types.ts new file mode 100644 index 00000000000..7325239bb3a --- /dev/null +++ b/public/app/plugins/datasource/loki/types.ts @@ -0,0 +1,6 @@ +import { DataQuery } from '@grafana/ui/src/types'; + +export interface LokiQuery extends DataQuery { + expr: string; +} + From 1d2902715f33f06fc75a06f2c324a79d6b8e281f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Jan 2019 18:14:27 +0100 Subject: [PATCH 119/156] wip: progress on adding query types --- .../plugins/datasource/loki/datasource.test.ts | 14 +++++++++++--- public/app/plugins/datasource/loki/datasource.ts | 10 +++++----- .../plugins/datasource/loki/language_provider.ts | 16 +++++++++++----- public/app/types/explore.ts | 4 ++-- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index b7f67ffc0e7..8b84f1073fb 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -1,3 +1,4 @@ +import moment from 'moment'; import LokiDatasource from './datasource'; describe('LokiDatasource', () => { @@ -13,12 +14,19 @@ describe('LokiDatasource', () => { replace: a => a, }; - const range = { from: 'now-6h', to: 'now' }; + const range = { + from: moment(), + to: moment(), + raw: { + from: 'now-6h', + to: 'now' + } + }; test('should use default max lines when no limit given', () => { const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock); backendSrvMock.datasourceRequest = jest.fn(); - ds.query({ range, targets: [{ expr: 'foo' }] }); + ds.query({ range, targets: [{ expr: 'foo', refId: 'B' }] }); expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1); expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=1000'); }); @@ -28,7 +36,7 @@ describe('LokiDatasource', () => { const customSettings = { ...instanceSettings, jsonData: customData }; const ds = new LokiDatasource(customSettings, backendSrvMock, templateSrvMock); backendSrvMock.datasourceRequest = jest.fn(); - ds.query({ range, targets: [{ expr: 'foo' }] }); + ds.query({ range, targets: [{ expr: 'foo', refId: 'A' }] }); expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1); expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=20'); }); diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 6c5bda8bcf2..95d7a2a830e 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -11,7 +11,7 @@ import { makeSeriesForLogs } from 'app/core/logs_model'; // Types import { LogsStream, LogsModel } from 'app/core/logs_model'; -import { PluginMeta, DataQueryOptions, DataSourceApi } from '@grafana/ui/src/types'; +import { PluginMeta, DataQueryOptions } from '@grafana/ui/src/types'; import { LokiQuery } from './types'; export const DEFAULT_MAX_LINES = 1000; @@ -32,7 +32,7 @@ function serializeParams(data: any) { .join('&'); } -export default class LokiDatasource implements DataSourceApi { +export default class LokiDatasource { languageProvider: LanguageProvider; maxLines: number; @@ -101,7 +101,7 @@ export default class LokiDatasource implements DataSourceApi { }); } - async importQueries(queries: DataQuery[], originMeta: PluginMeta): Promise { + async importQueries(queries: LokiQuery[], originMeta: PluginMeta): Promise { return this.languageProvider.importQueries(queries, originMeta.id); } @@ -114,7 +114,7 @@ export default class LokiDatasource implements DataSourceApi { }); } - modifyQuery(query: DataQuery, action: any): DataQuery { + modifyQuery(query: LokiQuery, action: any): LokiQuery { const parsed = parseQuery(query.expr || ''); let selector = parsed.query; switch (action.type) { @@ -129,7 +129,7 @@ export default class LokiDatasource implements DataSourceApi { return { ...query, expr: expression }; } - getHighlighterExpression(query: DataQuery): string { + getHighlighterExpression(query: LokiQuery): string { return parseQuery(query.expr).regexp; } diff --git a/public/app/plugins/datasource/loki/language_provider.ts b/public/app/plugins/datasource/loki/language_provider.ts index 3532e81ccd9..115a0a5f11f 100644 --- a/public/app/plugins/datasource/loki/language_provider.ts +++ b/public/app/plugins/datasource/loki/language_provider.ts @@ -1,6 +1,12 @@ +// Libraries import _ from 'lodash'; import moment from 'moment'; +// Services & Utils +import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils'; +import syntax from './syntax'; + +// Types import { CompletionItem, CompletionItemGroup, @@ -9,9 +15,7 @@ import { TypeaheadOutput, HistoryItem, } from 'app/types/explore'; -import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils'; -import syntax from './syntax'; -import { DataQuery } from '@grafana/ui/src/types'; +import { LokiQuery } from './types'; const DEFAULT_KEYS = ['job', 'namespace']; const EMPTY_SELECTOR = '{}'; @@ -20,7 +24,9 @@ const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h const wrapLabel = (label: string) => ({ label }); -export function addHistoryMetadata(item: CompletionItem, history: HistoryItem[]): CompletionItem { +type LokiHistoryItem = HistoryItem; + +export function addHistoryMetadata(item: CompletionItem, history: LokiHistoryItem[]): CompletionItem { const cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF; const historyForItem = history.filter(h => h.ts > cutoffTs && (h.query.expr as string) === item.label); const count = historyForItem.length; @@ -155,7 +161,7 @@ export default class LokiLanguageProvider extends LanguageProvider { return { context, refresher, suggestions }; } - async importQueries(queries: DataQuery[], datasourceType: string): Promise { + async importQueries(queries: LokiQuery[], datasourceType: string): Promise { if (datasourceType === 'prometheus') { return Promise.all( queries.map(async query => { diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index e5ae676ba25..c69e93ff88e 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -243,9 +243,9 @@ export interface ExploreUrlState { range: RawTimeRange; } -export interface HistoryItem { +export interface HistoryItem { ts: number; - query: DataQuery; + query: TQuery; } export abstract class LanguageProvider { From 5d17ad110399fb71aa50880948e8f9aa3c590dc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Jan 2019 18:59:32 +0100 Subject: [PATCH 120/156] more typings work around data query and data source --- packages/grafana-ui/src/types/datasource.ts | 18 +++++++++++++ public/app/core/utils/explore.ts | 8 ++++-- .../loki/components/LokiQueryField.tsx | 15 +++++++---- .../datasource/loki/datasource.test.ts | 22 ++++++++-------- public/test/helpers/getQueryOptions.ts | 25 +++++++++++++++++++ 5 files changed, 69 insertions(+), 19 deletions(-) create mode 100644 public/test/helpers/getQueryOptions.ts diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index cb6115486b1..ffcbbb5fe64 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/packages/grafana-ui/src/types/datasource.ts @@ -7,8 +7,26 @@ export interface DataQueryResponse { } export interface DataQuery { + /** + * A - Z + */ refId: string; + + /** + * true if query is disabled (ie not executed / sent to TSDB) + */ hide?: boolean; + + /** + * Unique, guid like, string used in explore mode + */ + key?: string; + + /** + * For mixed data sources the selected datasource is on the query level. + * For non mixed scenarios this is undefined. + */ + datasource?: string | null; } export interface DataQueryOptions { diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 2d1c0d2ad71..45b70672bc6 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -203,7 +203,7 @@ export function ensureQueries(queries?: DataQuery[]): DataQuery[] { /** * A target is non-empty when it has keys (with non-empty values) other than refId and key. */ -export function hasNonEmptyQuery(queries: DataQuery[]): boolean { +export function hasNonEmptyQuery(queries: TQuery[]): boolean { return ( queries && queries.some( @@ -280,7 +280,11 @@ export function makeTimeSeriesList(dataList) { /** * Update the query history. Side-effect: store history in local storage */ -export function updateHistory(history: HistoryItem[], datasourceId: string, queries: DataQuery[]): HistoryItem[] { +export function updateHistory( + history: Array>, + datasourceId: string, + queries: T[] +): Array> { const ts = Date.now(); queries.forEach(query => { history = [{ query, ts }, ...history]; diff --git a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx index 98c8a5f6da9..febb322acca 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx @@ -1,16 +1,21 @@ +// Libraries import React from 'react'; import Cascader from 'rc-cascader'; import PluginPrism from 'slate-prism'; import Prism from 'prismjs'; -import { DataQuery } from '@grafana/ui/src/types'; -import { TypeaheadOutput } from 'app/types/explore'; +// Components +import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; +// Utils & Services // dom also includes Element polyfills import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom'; import BracesPlugin from 'app/features/explore/slate-plugins/braces'; import RunnerPlugin from 'app/features/explore/slate-plugins/runner'; -import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; + +// Types +import { LokiQuery } from '../types'; +import { TypeaheadOutput } from 'app/types/explore'; const PRISM_SYNTAX = 'promql'; @@ -63,10 +68,10 @@ interface LokiQueryFieldProps { error?: string | JSX.Element; hint?: any; history?: any[]; - initialQuery?: DataQuery; + initialQuery?: LokiQuery; onClickHintFix?: (action: any) => void; onPressEnter?: () => void; - onQueryChange?: (value: DataQuery, override?: boolean) => void; + onQueryChange?: (value: LokiQuery, override?: boolean) => void; } interface LokiQueryFieldState { diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index 8b84f1073fb..195ac194dad 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -1,5 +1,6 @@ -import moment from 'moment'; import LokiDatasource from './datasource'; +import { LokiQuery } from './types'; +import { getQueryOptions } from 'test/helpers/getQueryOptions'; describe('LokiDatasource', () => { const instanceSettings: any = { @@ -14,19 +15,13 @@ describe('LokiDatasource', () => { replace: a => a, }; - const range = { - from: moment(), - to: moment(), - raw: { - from: 'now-6h', - to: 'now' - } - }; - test('should use default max lines when no limit given', () => { const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock); backendSrvMock.datasourceRequest = jest.fn(); - ds.query({ range, targets: [{ expr: 'foo', refId: 'B' }] }); + const options = getQueryOptions({ targets: [{ expr: 'foo', refId: 'B' }] }); + + ds.query(options); + expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1); expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=1000'); }); @@ -36,7 +31,10 @@ describe('LokiDatasource', () => { const customSettings = { ...instanceSettings, jsonData: customData }; const ds = new LokiDatasource(customSettings, backendSrvMock, templateSrvMock); backendSrvMock.datasourceRequest = jest.fn(); - ds.query({ range, targets: [{ expr: 'foo', refId: 'A' }] }); + + const options = getQueryOptions({ targets: [{ expr: 'foo', refId: 'B' }] }); + ds.query(options); + expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1); expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=20'); }); diff --git a/public/test/helpers/getQueryOptions.ts b/public/test/helpers/getQueryOptions.ts new file mode 100644 index 00000000000..ac92c2afb55 --- /dev/null +++ b/public/test/helpers/getQueryOptions.ts @@ -0,0 +1,25 @@ +import { DataQueryOptions, DataQuery } from '@grafana/ui'; +import moment from 'moment'; + + +export function getQueryOptions(options: Partial>): DataQueryOptions { + const raw = {from: 'now', to: 'now-1h'}; + const range = { from: moment(), to: moment(), raw: raw}; + + const defaults: DataQueryOptions = { + range: range, + rangeRaw: raw, + targets: [], + scopedVars: {}, + timezone: 'browser', + panelId: 1, + dashboardId: 1, + interval: '60s', + intervalMs: 60000, + maxDataPoints: 500, + }; + + Object.assign(defaults, options); + + return defaults; +} From 5ab9a7c2b8809a9e73aeb1ed754000daacace79a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Jan 2019 19:11:30 +0100 Subject: [PATCH 121/156] Further refinements of typings --- packages/grafana-ui/src/types/plugin.ts | 20 ++++++++++--------- .../datasource/testdata/QueryEditor.tsx | 5 +++-- .../plugins/datasource/testdata/datasource.ts | 6 +++++- .../app/plugins/datasource/testdata/module.ts | 8 ++++---- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index 8ded328b2da..420a54e5840 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -3,11 +3,6 @@ import { PanelProps, PanelOptionsProps } from './panel'; import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint } from './datasource'; export interface DataSourceApi { - // set externally by grafana - name?: string; - meta?: PluginMeta; - pluginExports?: PluginExports; - /** * min interval range */ @@ -37,19 +32,26 @@ export interface DataSourceApi { * Get hints for query improvements */ getQueryHints?(query: TQuery, results: any[], ...rest: any): QueryHint[]; + + /** + * Set after constructor is called by Grafana + */ + name?: string; + meta?: PluginMeta; + pluginExports?: PluginExports; } -export interface QueryEditorProps { +export interface QueryEditorProps { datasource: DSType; query: TQuery; onExecuteQuery?: () => void; - onQueryChange?: (value: DataQuery) => void; + onQueryChange?: (value: TQuery) => void; } export interface PluginExports { - Datasource?: any; + Datasource?: DataSourceApi; QueryCtrl?: any; - QueryEditor?: ComponentClass; + QueryEditor?: ComponentClass>; ConfigCtrl?: any; AnnotationsQueryCtrl?: any; VariableQueryEditor?: any; diff --git a/public/app/plugins/datasource/testdata/QueryEditor.tsx b/public/app/plugins/datasource/testdata/QueryEditor.tsx index ef49505d2c6..20b86d571df 100644 --- a/public/app/plugins/datasource/testdata/QueryEditor.tsx +++ b/public/app/plugins/datasource/testdata/QueryEditor.tsx @@ -29,11 +29,12 @@ export class QueryEditor extends PureComponent { }; async componentDidMount() { - const { query } = this.props; + const { query, datasource } = this.props; query.scenarioId = query.scenarioId || 'random_walk'; - const scenarioList = await this.backendSrv.get('/api/tsdb/testdata/scenarios'); + // const scenarioList = await this.backendSrv.get('/api/tsdb/testdata/scenarios'); + const scenarioList = await datasource.getScenarios(); const current = _.find(scenarioList, { id: query.scenarioId }); this.setState({ scenarioList: scenarioList, current: current }); diff --git a/public/app/plugins/datasource/testdata/datasource.ts b/public/app/plugins/datasource/testdata/datasource.ts index c329389ee8c..6ba0da59457 100644 --- a/public/app/plugins/datasource/testdata/datasource.ts +++ b/public/app/plugins/datasource/testdata/datasource.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import TableModel from 'app/core/table_model'; import { DataSourceApi, DataQueryOptions } from '@grafana/ui'; -import { TestDataQuery } from './types'; +import { TestDataQuery, Scenario } from './types'; export class TestDataDatasource implements DataSourceApi { id: number; @@ -93,5 +93,9 @@ export class TestDataDatasource implements DataSourceApi { message: 'Data source is working', }); } + + getScenarios(): Promise { + return this.backendSrv.get('/api/tsdb/testdata/scenarios'); + } } diff --git a/public/app/plugins/datasource/testdata/module.ts b/public/app/plugins/datasource/testdata/module.ts index efd6c207407..d3b376e3307 100644 --- a/public/app/plugins/datasource/testdata/module.ts +++ b/public/app/plugins/datasource/testdata/module.ts @@ -1,6 +1,6 @@ import { TestDataDatasource } from './datasource'; -// import { TestDataQueryCtrl } from './query_ctrl'; -import { QueryEditor } from './QueryEditor'; +import { TestDataQueryCtrl } from './query_ctrl'; +// import { QueryEditor } from './QueryEditor'; class TestDataAnnotationsQueryCtrl { annotation: any; @@ -11,8 +11,8 @@ class TestDataAnnotationsQueryCtrl { } export { - QueryEditor, + // QueryEditor, TestDataDatasource as Datasource, - // TestDataQueryCtrl as QueryCtrl, + TestDataQueryCtrl as QueryCtrl, TestDataAnnotationsQueryCtrl as AnnotationsQueryCtrl, }; From 12468c6033811bae9a21ab1ba09a994a538fce40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Jan 2019 19:14:22 +0100 Subject: [PATCH 122/156] Delete .all.ts@neomake_22624_74.ts --- public/app/features/.all.ts@neomake_22624_74.ts | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 public/app/features/.all.ts@neomake_22624_74.ts diff --git a/public/app/features/.all.ts@neomake_22624_74.ts b/public/app/features/.all.ts@neomake_22624_74.ts deleted file mode 100644 index 99de12bf215..00000000000 --- a/public/app/features/.all.ts@neomake_22624_74.ts +++ /dev/null @@ -1,14 +0,0 @@ -import './annotations/all'; -import './templating/all'; -import './plugins/all'; -import './dashboard/all'; -import './playlist/all'; -import './panel/all'; -import './org/all'; -import './admin'; -import './alerting/NotificationsEditCtrl'; -import './alerting/NotificationsListCtrl'; -import './manage-dashboards'; -import './teams/CreateTeamCtrl'; -import './profile/all'; -import './datasources/settings/dsHttpSettings'; From 16d476d22898b8a599d570107f2d955506d1fb07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Jan 2019 19:22:40 +0100 Subject: [PATCH 123/156] Query editor row style update & sass cleanup --- .../dashboard/panel_editor/QueryInspector.tsx | 16 +---- public/sass/components/_query_editor.scss | 68 +------------------ 2 files changed, 3 insertions(+), 81 deletions(-) diff --git a/public/app/features/dashboard/panel_editor/QueryInspector.tsx b/public/app/features/dashboard/panel_editor/QueryInspector.tsx index 8e490f6b622..25c3c68e21e 100644 --- a/public/app/features/dashboard/panel_editor/QueryInspector.tsx +++ b/public/app/features/dashboard/panel_editor/QueryInspector.tsx @@ -177,7 +177,6 @@ export class QueryInspector extends PureComponent { render() { const { response, isLoading } = this.state.dsQuery; - const { isMocking } = this.state; const openNodes = this.getNrOfOpenNodes(); if (isLoading) { @@ -199,20 +198,7 @@ export class QueryInspector extends PureComponent {
- {!isMocking && } - {isMocking && ( -
-
-