mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
3db0b4ad93
commit
2aa1a051f0
@ -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)};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -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();
|
||||
};
|
||||
|
||||
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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)};
|
||||
`,
|
||||
});
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
`,
|
||||
});
|
@ -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',
|
||||
},
|
||||
},
|
||||
];
|
@ -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`
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -100,6 +100,7 @@ export enum GrafanaAlertStateDecision {
|
||||
KeepLastState = 'KeepLastState',
|
||||
OK = 'OK',
|
||||
}
|
||||
|
||||
export interface GrafanaQuery {
|
||||
refId: string;
|
||||
queryType: string;
|
||||
|
Loading…
Reference in New Issue
Block a user