Alerting: visualizing data when creating/editing alerting rule. (#34191)

* 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 <hugo.haggmark@grafana.com>

* reverted change.

* fixed merge conflict.

* fixed todo.

* sort some paddings

* replace theme with theme2

Co-authored-by: Peter Holmberg <peter.hlmbrg@gmail.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
This commit is contained in:
Marcus Andersson 2021-05-18 16:16:26 +02:00 committed by GitHub
parent 3db0b4ad93
commit 2aa1a051f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 305 additions and 54 deletions

View File

@ -59,6 +59,7 @@ export function RadioButtonGroup<T>({
fullWidth={fullWidth}
>
{o.icon && <Icon name={o.icon as IconName} className={styles.icon} />}
{o.imgUrl && <img src={o.imgUrl} alt={o.label} className={styles.img} />}
{o.label}
</RadioButton>
);
@ -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)};
`,
};
};

View File

@ -155,11 +155,13 @@ export class AlertingQueryEditor extends PureComponent<Props, State> {
render() {
const { value = [] } = this.props;
const { panelDataByRefId } = this.state;
const styles = getStyles(config.theme2);
return (
<div className={styles.container}>
<AlertingQueryRows
data={panelDataByRefId}
queries={value}
onQueriesChange={this.props.onChange}
onDuplicateQuery={this.onDuplicateQuery}
@ -197,6 +199,7 @@ const defaultTimeRange = (model: DataQuery): RelativeTimeRange | undefined => {
if (isExpressionQuery(model)) {
return;
}
return getDefaultRelativeTimeRange();
};

View File

@ -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<string, PanelData>;
// Query editing
onQueriesChange: (queries: GrafanaQuery[]) => void;
@ -30,6 +23,7 @@ interface State {
export class AlertingQueryRows extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = { dataPerQuery: {} };
}
@ -37,7 +31,7 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
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<Props, State> {
};
})
);
}
};
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<Props, State> {
};
})
);
}
};
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<Props, State> {
};
})
);
}
};
onDragEnd = (result: DropResult) => {
const { queries, onQueriesChange } = this.props;
@ -133,7 +127,7 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
};
render() {
const { queries } = this.props;
const { onDuplicateQuery, onRunQueries, queries } = this.props;
return (
<DragDropContext onDragEnd={this.onDragEnd}>
@ -141,8 +135,8 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
{(provided) => {
return (
<div ref={provided.innerRef} {...provided.droppableProps}>
{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<Props, State> {
}
return (
<QueryEditorRow
dataSource={dsSettings}
onChangeDataSource={
!isExpressionQuery(query.model)
? (settings) => this.onChangeDataSource(settings, index)
: undefined
}
id={query.refId}
<AlertingQueryWrapper
index={index}
key={query.refId}
key={`${query.refId}-${index}`}
dsSettings={dsSettings}
data={data}
query={query.model}
onChange={(query) => 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<Props, State> {
</DragDropContext>
);
}
renderTimePicker(query: GrafanaQuery, index: number): ReactNode {
if (isExpressionQuery(query.model)) {
return null;
}
return (
<RelativeTimeRangePicker
timeRange={query.relativeTimeRange ?? getDefaultRelativeTimeRange()}
onChange={(range) => this.onChangeTimeRange(range, index)}
/>
);
}
}

View File

@ -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<Props> = ({
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 (
<RelativeTimeRangePicker
timeRange={query.relativeTimeRange ?? getDefaultRelativeTimeRange()}
onChange={(range) => onChangeTimeRange(range, index)}
/>
);
};
return (
<div className={styles.wrapper}>
<QueryEditorRow
dataSource={dsSettings}
onChangeDataSource={!isExpression ? (settings) => 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 && <VizWrapper data={data} defaultPanel={isExpression ? 'table' : 'timeseries'} />}
</div>
);
};
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)};
`,
});

View File

@ -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();
}
}

View File

@ -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<Props> = ({ data, defaultPanel }) => {
const [pluginId, changePluginId] = useState<string>(defaultPanel ?? TIMESERIES);
const options = { ...getOptionsForPanelPlugin(pluginId) };
const styles = useStyles2(getStyles);
const panels = getSupportedPanels();
if (!options || !data) {
return null;
}
return (
<div className={styles.wrapper}>
<div className={styles.buttonGroup}>
<RadioButtonGroup options={panels} value={pluginId} onChange={changePluginId} />
</div>
<div style={{ height: '200px', width: '100%' }}>
<AutoSizer style={{ width: '100%', height: '100%' }}>
{({ width, height }) => {
if (width === 0 || height === 0) {
return null;
}
return (
<PanelRenderer
height={height}
width={width}
data={data}
pluginId={pluginId}
title="title"
onOptionsChange={() => {}}
options={options}
/>
);
}}
</AutoSizer>
</div>
</div>
);
};
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;
`,
});

View File

@ -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',
},
},
];

View File

@ -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`

View File

@ -30,7 +30,7 @@ export function PanelRenderer<P extends object = any, F extends object = any>(pr
return <div>Failed to load plugin: {error.message}</div>;
}
if (loading) {
if (pluginIsLoading(loading, plugin, pluginId)) {
return <div>Loading plugin panel...</div>;
}
@ -66,11 +66,11 @@ export function PanelRenderer<P extends object = any, F extends object = any>(pr
);
}
const useOptionDefaults = <P extends object = any, F extends object = any>(
function useOptionDefaults<P extends object = any, F extends object = any>(
plugin: PanelPlugin | undefined,
options: P,
fieldConfig: FieldConfigSource<F>
): OptionDefaults | undefined => {
): OptionDefaults | undefined {
return useMemo(() => {
if (!plugin) {
return;
@ -83,14 +83,14 @@ const useOptionDefaults = <P extends object = any, F extends object = any>(
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<any, any> | undefined, pluginId: string) {
return loading || plugin?.meta.id !== pluginId;
}

View File

@ -100,6 +100,7 @@ export enum GrafanaAlertStateDecision {
KeepLastState = 'KeepLastState',
OK = 'OK',
}
export interface GrafanaQuery {
refId: string;
queryType: string;