From 2aa1a051f0d24d6d5bdc162d1ea1de893b40967d Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Tue, 18 May 2021 16:16:26 +0200 Subject: [PATCH] Alerting: visualizing data when creating/editing alerting rule. (#34191) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * VizWrapper to handle some state * alertingqueryrunner first edition * added so we always set name and uid when changing datasource. * wip. * wip * added support for canceling requests. * util for getting time ranges for expression queries * remove logs, store data in state * merge from run branch * incremental commit * viz working * added structure for marble testing. * paddings and move viz picker * less height for viz, less width on rows * change so the expression buttons doesnt submit form. * fixed run button. * replaced mocks with implementation that will set default query + expression. * merge with run queries * fixed so we set a datasource name for the default expression rule. * improving expression guard. * lots of styling fixes for viz * adding placeholder for relative time range. * fixed story. * added basic structure to handle open/close of time range picker. * removed section from TimeOptions since it isn't used any where. * adding mapper and tests * move relativetimepicker to its own dir * added some simple tests. * changed test. * use relativetimerangeinput * redo state management * refactored the tests. * replace timerange with relativetimerange * wip * wip * did some refactoring. * refactored time option formatting. * added proper formatting and display of time range. * add relative time description, slight refactor of height * fixed incorrect import. * added validator and changed formatting. * removed unused dep. * reverted back to internal function. * fixed display of relative time range picker. * fixed failing tests. * fixed parsing issue. * fixed position of time range picker. * some more refactorings. * fixed validation of really big values. * added another test. * restored the step2 check. * fixed merge issue. * Update packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx Co-authored-by: Hugo Häggmark * reverted change. * fixed merge conflict. * fixed todo. * sort some paddings * replace theme with theme2 Co-authored-by: Peter Holmberg Co-authored-by: Hugo Häggmark --- .../RadioButtonGroup/RadioButtonGroup.tsx | 6 + .../components/AlertingQueryEditor.tsx | 3 + .../alerting/components/AlertingQueryRows.tsx | 70 ++++------- .../components/AlertingQueryWrapper.tsx | 89 ++++++++++++++ .../alerting/state/AlertingQueryRunner.ts | 2 + .../components/rule-editor/VizWrapper.tsx | 112 ++++++++++++++++++ .../alerting/unified/mocks/grafana-queries.ts | 57 +++++++++ .../VizTypePicker/PanelTypeCard.tsx | 1 + public/app/features/panel/PanelRenderer.tsx | 18 +-- public/app/types/unified-alerting-dto.ts | 1 + 10 files changed, 305 insertions(+), 54 deletions(-) create mode 100644 public/app/features/alerting/components/AlertingQueryWrapper.tsx create mode 100644 public/app/features/alerting/unified/components/rule-editor/VizWrapper.tsx create mode 100644 public/app/features/alerting/unified/mocks/grafana-queries.ts diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx index 34cd4743866..55f4ce42b69 100644 --- a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx +++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx @@ -59,6 +59,7 @@ export function RadioButtonGroup({ fullWidth={fullWidth} > {o.icon && } + {o.imgUrl && {o.label}} {o.label} ); @@ -85,5 +86,10 @@ const getStyles = (theme: GrafanaTheme2) => { icon: css` margin-right: 6px; `, + img: css` + width: ${theme.spacing(2)}; + height: ${theme.spacing(2)}; + margin-right: ${theme.spacing(1)}; + `, }; }; diff --git a/public/app/features/alerting/components/AlertingQueryEditor.tsx b/public/app/features/alerting/components/AlertingQueryEditor.tsx index 80af1fb08ec..15d4fcaa343 100644 --- a/public/app/features/alerting/components/AlertingQueryEditor.tsx +++ b/public/app/features/alerting/components/AlertingQueryEditor.tsx @@ -155,11 +155,13 @@ export class AlertingQueryEditor extends PureComponent { render() { const { value = [] } = this.props; + const { panelDataByRefId } = this.state; const styles = getStyles(config.theme2); return (
{ if (isExpressionQuery(model)) { return; } + return getDefaultRelativeTimeRange(); }; diff --git a/public/app/features/alerting/components/AlertingQueryRows.tsx b/public/app/features/alerting/components/AlertingQueryRows.tsx index 249faa8abde..c092ea35072 100644 --- a/public/app/features/alerting/components/AlertingQueryRows.tsx +++ b/public/app/features/alerting/components/AlertingQueryRows.tsx @@ -1,21 +1,14 @@ -import React, { PureComponent, ReactNode } from 'react'; +import React, { PureComponent } from 'react'; import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd'; -import { - DataQuery, - DataSourceInstanceSettings, - getDefaultRelativeTimeRange, - PanelData, - RelativeTimeRange, -} from '@grafana/data'; +import { DataQuery, DataSourceInstanceSettings, PanelData, RelativeTimeRange } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; -import { QueryEditorRow } from 'app/features/query/components/QueryEditorRow'; -import { isExpressionQuery } from 'app/features/expressions/guards'; +import { AlertingQueryWrapper } from './AlertingQueryWrapper'; import { GrafanaQuery } from 'app/types/unified-alerting-dto'; -import { RelativeTimeRangePicker } from '@grafana/ui'; interface Props { // The query configuration queries: GrafanaQuery[]; + data: Record; // Query editing onQueriesChange: (queries: GrafanaQuery[]) => void; @@ -30,6 +23,7 @@ interface State { export class AlertingQueryRows extends PureComponent { constructor(props: Props) { super(props); + this.state = { dataPerQuery: {} }; } @@ -37,7 +31,7 @@ export class AlertingQueryRows extends PureComponent { this.props.onQueriesChange(this.props.queries.filter((item) => item.model !== query)); }; - onChangeTimeRange(timeRange: RelativeTimeRange, index: number) { + onChangeTimeRange = (timeRange: RelativeTimeRange, index: number) => { const { queries, onQueriesChange } = this.props; onQueriesChange( queries.map((item, itemIndex) => { @@ -50,9 +44,9 @@ export class AlertingQueryRows extends PureComponent { }; }) ); - } + }; - onChangeDataSource(settings: DataSourceInstanceSettings, index: number) { + onChangeDataSource = (settings: DataSourceInstanceSettings, index: number) => { const { queries, onQueriesChange } = this.props; onQueriesChange( @@ -79,9 +73,9 @@ export class AlertingQueryRows extends PureComponent { }; }) ); - } + }; - onChangeQuery(query: DataQuery, index: number) { + onChangeQuery = (query: DataQuery, index: number) => { const { queries, onQueriesChange } = this.props; onQueriesChange( @@ -100,7 +94,7 @@ export class AlertingQueryRows extends PureComponent { }; }) ); - } + }; onDragEnd = (result: DropResult) => { const { queries, onQueriesChange } = this.props; @@ -133,7 +127,7 @@ export class AlertingQueryRows extends PureComponent { }; render() { - const { queries } = this.props; + const { onDuplicateQuery, onRunQueries, queries } = this.props; return ( @@ -141,8 +135,8 @@ export class AlertingQueryRows extends PureComponent { {(provided) => { return (
- {queries.map((query: GrafanaQuery, index) => { - const data = this.state.dataPerQuery[query.refId]; + {queries.map((query, index) => { + const data = this.props.data ? this.props.data[query.refId] : ({} as PanelData); const dsSettings = this.getDataSourceSettings(query); if (!dsSettings) { @@ -150,24 +144,19 @@ export class AlertingQueryRows extends PureComponent { } return ( - this.onChangeDataSource(settings, index) - : undefined - } - id={query.refId} + this.onChangeQuery(query, index)} - renderHeaderExtras={() => this.renderTimePicker(query, index)} + query={query} + onChangeQuery={this.onChangeQuery} onRemoveQuery={this.onRemoveQuery} - onAddQuery={(duplicate) => this.onDuplicateQuery(duplicate, query)} - onRunQuery={this.props.onRunQueries} queries={queries} + onChangeDataSource={this.onChangeDataSource} + onDuplicateQuery={onDuplicateQuery} + onRunQueries={onRunQueries} + onChangeTimeRange={this.onChangeTimeRange} /> ); })} @@ -179,17 +168,4 @@ export class AlertingQueryRows extends PureComponent { ); } - - renderTimePicker(query: GrafanaQuery, index: number): ReactNode { - if (isExpressionQuery(query.model)) { - return null; - } - - return ( - this.onChangeTimeRange(range, index)} - /> - ); - } } diff --git a/public/app/features/alerting/components/AlertingQueryWrapper.tsx b/public/app/features/alerting/components/AlertingQueryWrapper.tsx new file mode 100644 index 00000000000..eefef84b066 --- /dev/null +++ b/public/app/features/alerting/components/AlertingQueryWrapper.tsx @@ -0,0 +1,89 @@ +import React, { FC, ReactNode } from 'react'; +import { css } from '@emotion/css'; +import { + DataQuery, + DataSourceInstanceSettings, + GrafanaTheme2, + PanelData, + RelativeTimeRange, + getDefaultRelativeTimeRange, +} from '@grafana/data'; +import { useStyles2, RelativeTimeRangePicker } from '@grafana/ui'; +import { QueryEditorRow } from '../../query/components/QueryEditorRow'; +import { VizWrapper } from '../unified/components/rule-editor/VizWrapper'; +import { isExpressionQuery } from '../../expressions/guards'; +import { GrafanaQuery } from 'app/types/unified-alerting-dto'; + +interface Props { + data: PanelData; + query: GrafanaQuery; + queries: GrafanaQuery[]; + dsSettings: DataSourceInstanceSettings; + onChangeDataSource: (settings: DataSourceInstanceSettings, index: number) => void; + onChangeQuery: (query: DataQuery, index: number) => void; + onChangeTimeRange?: (timeRange: RelativeTimeRange, index: number) => void; + onRemoveQuery: (query: DataQuery) => void; + onDuplicateQuery: (query: GrafanaQuery) => void; + onRunQueries: () => void; + index: number; +} + +export const AlertingQueryWrapper: FC = ({ + data, + dsSettings, + index, + onChangeDataSource, + onChangeQuery, + onChangeTimeRange, + onRunQueries, + onRemoveQuery, + onDuplicateQuery, + query, + queries, +}) => { + const styles = useStyles2(getStyles); + const isExpression = isExpressionQuery(query.model); + + const renderTimePicker = (query: GrafanaQuery, index: number): ReactNode => { + if (isExpressionQuery(query.model) || !onChangeTimeRange) { + return null; + } + + return ( + onChangeTimeRange(range, index)} + /> + ); + }; + + return ( +
+ onChangeDataSource(settings, index) : undefined} + id={query.refId} + index={index} + key={query.refId} + data={data} + query={query.model} + onChange={(query) => onChangeQuery(query, index)} + onRemoveQuery={onRemoveQuery} + onAddQuery={onDuplicateQuery} + onRunQuery={onRunQueries} + queries={queries} + renderHeaderExtras={() => renderTimePicker(query, index)} + /> + {data && } +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + wrapper: css` + margin-bottom: ${theme.spacing(1)}; + border: 1px solid ${theme.colors.border.medium}; + border-radius: ${theme.shape.borderRadius(1)}; + padding-bottom: ${theme.spacing(1)}; + `, +}); diff --git a/public/app/features/alerting/state/AlertingQueryRunner.ts b/public/app/features/alerting/state/AlertingQueryRunner.ts index ff8be173079..145fec28736 100644 --- a/public/app/features/alerting/state/AlertingQueryRunner.ts +++ b/public/app/features/alerting/state/AlertingQueryRunner.ts @@ -57,6 +57,7 @@ export class AlertingQueryRunner { this.lastResult = nextResult; this.subject.next(this.lastResult); }, + error: (error: Error) => { this.lastResult = mapErrorToPanelData(this.lastResult, error); this.subject.next(this.lastResult); @@ -92,6 +93,7 @@ export class AlertingQueryRunner { if (this.subject) { this.subject.complete(); } + this.cancel(); } } diff --git a/public/app/features/alerting/unified/components/rule-editor/VizWrapper.tsx b/public/app/features/alerting/unified/components/rule-editor/VizWrapper.tsx new file mode 100644 index 00000000000..5ac07cfbc49 --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/VizWrapper.tsx @@ -0,0 +1,112 @@ +import React, { FC, useState } from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { css } from '@emotion/css'; +import { GrafanaTheme2, PanelData, VizOrientation } from '@grafana/data'; +import { config, PanelRenderer } from '@grafana/runtime'; +import { + LegendDisplayMode, + SingleStatBaseOptions, + TooltipDisplayMode, + RadioButtonGroup, + useStyles2, +} from '@grafana/ui'; + +const TIMESERIES = 'timeseries'; +const TABLE = 'table'; +const STAT = 'stat'; + +interface Props { + data: PanelData; + defaultPanel?: 'timeseries' | 'table' | 'stat'; +} + +export const VizWrapper: FC = ({ data, defaultPanel }) => { + const [pluginId, changePluginId] = useState(defaultPanel ?? TIMESERIES); + const options = { ...getOptionsForPanelPlugin(pluginId) }; + const styles = useStyles2(getStyles); + const panels = getSupportedPanels(); + + if (!options || !data) { + return null; + } + + return ( +
+
+ +
+
+ + {({ width, height }) => { + if (width === 0 || height === 0) { + return null; + } + return ( + {}} + options={options} + /> + ); + }} + +
+
+ ); +}; + +const getSupportedPanels = () => { + return Object.values(config.panels) + .filter((p) => p.id === TIMESERIES || p.id === TABLE || p.id === STAT) + .map((panel) => ({ value: panel.id, label: panel.name, imgUrl: panel.info.logos.small })); +}; + +const getOptionsForPanelPlugin = (panelPlugin: string) => { + switch (panelPlugin) { + case STAT: + return singleStatOptions; + case TABLE: + return tableOptions; + case TIMESERIES: + return timeSeriesOptions; + default: + return undefined; + } +}; + +const timeSeriesOptions = { + legend: { + displayMode: LegendDisplayMode.List, + placement: 'bottom', + calcs: [], + }, + tooltipOptions: { + mode: TooltipDisplayMode.Single, + }, +}; + +const tableOptions = { + frameIndex: 0, + showHeader: true, +}; +const singleStatOptions: SingleStatBaseOptions = { + reduceOptions: { + calcs: [], + }, + orientation: VizOrientation.Auto, + text: undefined, +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + wrapper: css` + padding: 0 ${theme.spacing(2)}; + `, + buttonGroup: css` + display: flex; + justify-content: flex-end; + `, +}); diff --git a/public/app/features/alerting/unified/mocks/grafana-queries.ts b/public/app/features/alerting/unified/mocks/grafana-queries.ts new file mode 100644 index 00000000000..ac9a0448cd4 --- /dev/null +++ b/public/app/features/alerting/unified/mocks/grafana-queries.ts @@ -0,0 +1,57 @@ +export const SAMPLE_QUERIES = [ + { + refId: 'A', + queryType: '', + relativeTimeRange: { + from: 21600, + to: 0, + }, + datasourceUid: '6_hUDNQGz', + model: { + intervalMs: 1000, + maxDataPoints: 100, + pulseWave: { + offCount: 6, + offValue: 1, + onCount: 6, + onValue: 10, + timeStep: 5, + }, + refId: 'A', + scenarioId: 'predictable_pulse', + stringInput: '', + }, + }, + { + refId: 'B', + queryType: '', + relativeTimeRange: { + from: 0, + to: 0, + }, + datasourceUid: '-100', + model: { + conditions: [ + { + evaluator: { + params: [3], + type: 'gt', + }, + operator: { + type: 'and', + }, + query: { + params: ['A'], + }, + reducer: { + type: 'last', + }, + }, + ], + intervalMs: 1000, + maxDataPoints: 100, + refId: 'B', + type: 'classic_conditions', + }, + }, +]; diff --git a/public/app/features/dashboard/components/VizTypePicker/PanelTypeCard.tsx b/public/app/features/dashboard/components/VizTypePicker/PanelTypeCard.tsx index 9d3680b158c..58b2f7d3704 100644 --- a/public/app/features/dashboard/components/VizTypePicker/PanelTypeCard.tsx +++ b/public/app/features/dashboard/components/VizTypePicker/PanelTypeCard.tsx @@ -99,6 +99,7 @@ const getStyles = (theme: GrafanaTheme2) => { `, current: css` label: currentVisualizationItem; + border: 1px solid ${theme.colors.primary.border}; background: ${theme.colors.action.selected}; `, disabled: css` diff --git a/public/app/features/panel/PanelRenderer.tsx b/public/app/features/panel/PanelRenderer.tsx index 7e0bb242293..03d4c978c0a 100644 --- a/public/app/features/panel/PanelRenderer.tsx +++ b/public/app/features/panel/PanelRenderer.tsx @@ -30,7 +30,7 @@ export function PanelRenderer

(pr return

Failed to load plugin: {error.message}
; } - if (loading) { + if (pluginIsLoading(loading, plugin, pluginId)) { return
Loading plugin panel...
; } @@ -66,11 +66,11 @@ export function PanelRenderer

(pr ); } -const useOptionDefaults =

( +function useOptionDefaults

( plugin: PanelPlugin | undefined, options: P, fieldConfig: FieldConfigSource -): OptionDefaults | undefined => { +): OptionDefaults | undefined { return useMemo(() => { if (!plugin) { return; @@ -83,14 +83,14 @@ const useOptionDefaults =

( isAfterPluginChange: false, }); }, [plugin, fieldConfig, options]); -}; +} -const useFieldOverrides = ( +function useFieldOverrides( plugin: PanelPlugin | undefined, defaultOptions: OptionDefaults | undefined, data: PanelData | undefined, timeZone: string -): PanelData | undefined => { +): PanelData | undefined { const fieldConfig = defaultOptions?.fieldConfig; const series = data?.series; const fieldConfigRegistry = plugin?.fieldConfigRegistry; @@ -113,4 +113,8 @@ const useFieldOverrides = ( }), }; }, [fieldConfigRegistry, fieldConfig, data, series, timeZone, theme]); -}; +} + +function pluginIsLoading(loading: boolean, plugin: PanelPlugin | undefined, pluginId: string) { + return loading || plugin?.meta.id !== pluginId; +} diff --git a/public/app/types/unified-alerting-dto.ts b/public/app/types/unified-alerting-dto.ts index 8be78176656..a4fb7dc8ae8 100644 --- a/public/app/types/unified-alerting-dto.ts +++ b/public/app/types/unified-alerting-dto.ts @@ -100,6 +100,7 @@ export enum GrafanaAlertStateDecision { KeepLastState = 'KeepLastState', OK = 'OK', } + export interface GrafanaQuery { refId: string; queryType: string;