mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: [],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user