Alerting: Edit thresholds by handle in timeseries panel (#38881)

* POC/Thresholds: Allow thresholds modification directly from the time series panel

* Snapshot updates

* Optimize styles memoization

* change threshold from graph

* renames and logging

* using useeffect to update graph

* Fix react worning about setting state on unmounted component

* revert panelrenderer

* using onFieldConfig change

* use a useeffect

* simplied fieldConfig state

* Do not use plot context in ThresholdControlsPlugin

* Do not throw setState warnings when drag handle is dropped

* Update thresholds position on the graph when updating threshold drag handle

* fix issues with rerenders

* prevent thresholds on conditions with range

* only edit the first threshold

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
Peter Holmberg
2021-09-15 17:35:12 +02:00
committed by GitHub
parent b3196621f1
commit 74beb9a64c
15 changed files with 457 additions and 60 deletions

View File

@@ -1,9 +1,17 @@
import React, { PureComponent } from 'react';
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
import { DataQuery, DataSourceInstanceSettings, PanelData, RelativeTimeRange } from '@grafana/data';
import {
DataQuery,
DataSourceInstanceSettings,
PanelData,
RelativeTimeRange,
ThresholdsConfig,
ThresholdsMode,
} from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { QueryWrapper } from './QueryWrapper';
import { AlertQuery } from 'app/types/unified-alerting-dto';
import { isExpressionQuery } from 'app/features/expressions/guards';
interface Props {
// The query configuration
@@ -50,6 +58,43 @@ export class QueryRows extends PureComponent<Props, State> {
);
};
onChangeThreshold = (thresholds: ThresholdsConfig, index: number) => {
const { queries, onQueriesChange } = this.props;
const referencedRefId = queries[index].refId;
onQueriesChange(
queries.map((query) => {
if (!isExpressionQuery(query.model)) {
return query;
}
if (query.model.conditions && query.model.conditions[0].query.params[0] === referencedRefId) {
return {
...query,
model: {
...query.model,
conditions: query.model.conditions.map((condition, conditionIndex) => {
// Only update the first condition for a given refId.
if (condition.query.params[0] === referencedRefId && conditionIndex === 0) {
return {
...condition,
evaluator: {
...condition.evaluator,
params: [parseFloat(thresholds.steps[1].value.toPrecision(3))],
},
};
}
return condition;
}),
},
};
}
return query;
})
);
};
onChangeDataSource = (settings: DataSourceInstanceSettings, index: number) => {
const { queries, onQueriesChange } = this.props;
@@ -130,8 +175,53 @@ export class QueryRows extends PureComponent<Props, State> {
return getDataSourceSrv().getInstanceSettings(query.datasourceUid);
};
getThresholdsForQueries = (queries: AlertQuery[]): Record<string, ThresholdsConfig> => {
const record: Record<string, ThresholdsConfig> = {};
for (const query of queries) {
if (!isExpressionQuery(query.model)) {
continue;
}
if (!Array.isArray(query.model.conditions)) {
continue;
}
query.model.conditions.forEach((condition, index) => {
if (index > 0) {
return;
}
const threshold = condition.evaluator.params[0];
const refId = condition.query.params[0];
if (condition.evaluator.type === 'outside_range' || condition.evaluator.type === 'within_range') {
return;
}
if (!record[refId]) {
record[refId] = {
mode: ThresholdsMode.Absolute,
steps: [
{
value: -Infinity,
color: 'green',
},
],
};
}
record[refId].steps.push({
value: threshold,
color: 'red',
});
});
}
return record;
};
render() {
const { onDuplicateQuery, onRunQueries, queries } = this.props;
const thresholdByRefId = this.getThresholdsForQueries(queries);
return (
<DragDropContext onDragEnd={this.onDragEnd}>
@@ -161,6 +251,8 @@ export class QueryRows extends PureComponent<Props, State> {
onDuplicateQuery={onDuplicateQuery}
onRunQueries={onRunQueries}
onChangeTimeRange={this.onChangeTimeRange}
thresholds={thresholdByRefId[query.refId]}
onChangeThreshold={this.onChangeThreshold}
/>
);
})}

View File

@@ -4,18 +4,19 @@ import { cloneDeep } from 'lodash';
import {
DataQuery,
DataSourceInstanceSettings,
getDefaultRelativeTimeRange,
GrafanaTheme2,
PanelData,
RelativeTimeRange,
getDefaultRelativeTimeRange,
ThresholdsConfig,
} from '@grafana/data';
import { useStyles2, RelativeTimeRangePicker } from '@grafana/ui';
import { RelativeTimeRangePicker, useStyles2 } from '@grafana/ui';
import { QueryEditorRow } from 'app/features/query/components/QueryEditorRow';
import { VizWrapper } from './VizWrapper';
import { isExpressionQuery } from 'app/features/expressions/guards';
import { TABLE, TIMESERIES } from '../../utils/constants';
import { AlertQuery } from 'app/types/unified-alerting-dto';
import { SupportedPanelPlugins } from '../PanelPluginsButtonGroup';
import { AlertQuery } from 'app/types/unified-alerting-dto';
interface Props {
data: PanelData;
@@ -29,6 +30,8 @@ interface Props {
onDuplicateQuery: (query: AlertQuery) => void;
onRunQueries: () => void;
index: number;
thresholds: ThresholdsConfig;
onChangeThreshold: (thresholds: ThresholdsConfig, index: number) => void;
}
export const QueryWrapper: FC<Props> = ({
@@ -43,6 +46,8 @@ export const QueryWrapper: FC<Props> = ({
onDuplicateQuery,
query,
queries,
thresholds,
onChangeThreshold,
}) => {
const styles = useStyles2(getStyles);
const isExpression = isExpressionQuery(query.model);
@@ -77,7 +82,17 @@ export const QueryWrapper: FC<Props> = ({
onRunQuery={onRunQueries}
queries={queries}
renderHeaderExtras={() => renderTimePicker(query, index)}
visualization={data ? <VizWrapper data={data} changePanel={changePluginId} currentPanel={pluginId} /> : null}
visualization={
data ? (
<VizWrapper
data={data}
changePanel={changePluginId}
currentPanel={pluginId}
thresholds={thresholds}
onThresholdsChange={(thresholds) => onChangeThreshold(thresholds, index)}
/>
) : null
}
hideDisableQuery={true}
/>
</div>

View File

@@ -1,26 +1,55 @@
import React, { FC, useState } from 'react';
import React, { FC, useEffect, useMemo, useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { css } from '@emotion/css';
import { GrafanaTheme2, PanelData } from '@grafana/data';
import { FieldConfigSource, GrafanaTheme2, PanelData, ThresholdsConfig } from '@grafana/data';
import { PanelRenderer } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
import { PanelContext, PanelContextProvider, useStyles2 } from '@grafana/ui';
import { PanelOptions } from 'app/plugins/panel/table/models.gen';
import { useVizHeight } from '../../hooks/useVizHeight';
import { SupportedPanelPlugins, PanelPluginsButtonGroup } from '../PanelPluginsButtonGroup';
import appEvents from 'app/core/app_events';
interface Props {
data: PanelData;
currentPanel: SupportedPanelPlugins;
changePanel: (panel: SupportedPanelPlugins) => void;
thresholds: ThresholdsConfig;
onThresholdsChange: (thresholds: ThresholdsConfig) => void;
}
export const VizWrapper: FC<Props> = ({ data, currentPanel, changePanel }) => {
export const VizWrapper: FC<Props> = ({ data, currentPanel, changePanel, onThresholdsChange, thresholds }) => {
const [options, setOptions] = useState<PanelOptions>({
frameIndex: 0,
showHeader: true,
});
const vizHeight = useVizHeight(data, currentPanel, options.frameIndex);
const styles = useStyles2(getStyles(vizHeight));
const [fieldConfig, setFieldConfig] = useState<FieldConfigSource>(defaultFieldConfig(thresholds));
useEffect(() => {
setFieldConfig((fieldConfig) => ({
...fieldConfig,
defaults: {
...fieldConfig.defaults,
thresholds: thresholds,
custom: {
...fieldConfig.defaults.custom,
thresholdsStyle: {
mode: 'line',
},
},
},
}));
}, [thresholds, setFieldConfig]);
const context: PanelContext = useMemo(
() => ({
eventBus: appEvents,
canEditThresholds: true,
onThresholdsChange: onThresholdsChange,
}),
[onThresholdsChange]
);
if (!options || !data) {
return null;
@@ -38,15 +67,18 @@ export const VizWrapper: FC<Props> = ({ data, currentPanel, changePanel }) => {
}
return (
<div style={{ height: `${vizHeight}px`, width: `${width}px` }}>
<PanelRenderer
height={vizHeight}
width={width}
data={data}
pluginId={currentPanel}
title="title"
onOptionsChange={setOptions}
options={options}
/>
<PanelContextProvider value={context}>
<PanelRenderer
height={vizHeight}
width={width}
data={data}
pluginId={currentPanel}
title="title"
onOptionsChange={setOptions}
options={options}
fieldConfig={fieldConfig}
/>
</PanelContextProvider>
</div>
);
}}
@@ -65,3 +97,20 @@ const getStyles = (visHeight: number) => (theme: GrafanaTheme2) => ({
justify-content: flex-end;
`,
});
function defaultFieldConfig(thresholds: ThresholdsConfig): FieldConfigSource {
if (!thresholds) {
return { defaults: {}, overrides: [] };
}
return {
defaults: {
thresholds: thresholds,
custom: {
thresholdsStyle: {
mode: 'line',
},
},
},
overrides: [],
};
}