diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx index a8a96dfacc1..60acdd0011a 100644 --- a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx @@ -9,7 +9,7 @@ import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOp import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; -import { EmptyQueryWrapper, QueryWrapper } from './QueryWrapper'; +import { AlertQueryOptions, EmptyQueryWrapper, QueryWrapper } from './QueryWrapper'; import { errorFromSeries, getThresholdsForQueries } from './util'; interface Props { @@ -51,6 +51,21 @@ export class QueryRows extends PureComponent { ); }; + onChangeQueryOptions = (options: AlertQueryOptions, index: number) => { + const { queries, onQueriesChange } = this.props; + onQueriesChange( + queries.map((item, itemIndex) => { + if (itemIndex !== index) { + return item; + } + return { + ...item, + model: { ...item.model, maxDataPoints: options.maxDataPoints }, + }; + }) + ); + }; + onChangeDataSource = (settings: DataSourceInstanceSettings, index: number) => { const { queries, onQueriesChange } = this.props; @@ -170,6 +185,7 @@ export class QueryRows extends PureComponent { onChangeDataSource={this.onChangeDataSource} onDuplicateQuery={this.props.onDuplicateQuery} onChangeTimeRange={this.onChangeTimeRange} + onChangeQueryOptions={this.onChangeQueryOptions} thresholds={thresholdByRefId[query.refId]?.config} thresholdsType={thresholdByRefId[query.refId]?.mode} onRunQueries={this.props.onRunQueries} diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx index a373e91ffee..37154731ce3 100644 --- a/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { cloneDeep } from 'lodash'; -import React, { FC, useState } from 'react'; +import React, { ChangeEvent, FC, useState } from 'react'; import { CoreApp, @@ -14,7 +14,15 @@ import { ThresholdsConfig, } from '@grafana/data'; import { Stack } from '@grafana/experimental'; -import { RelativeTimeRangePicker, useStyles2, Tooltip, Icon, GraphTresholdsStyleMode } from '@grafana/ui'; +import { + GraphTresholdsStyleMode, + Icon, + InlineFormLabel, + Input, + RelativeTimeRangePicker, + Tooltip, + useStyles2, +} from '@grafana/ui'; import { isExpressionQuery } from 'app/features/expressions/guards'; import { QueryEditorRow } from 'app/features/query/components/QueryEditorRow'; import { AlertQuery } from 'app/types/unified-alerting-dto'; @@ -25,6 +33,12 @@ import { AlertConditionIndicator } from '../expressions/AlertConditionIndicator' import { VizWrapper } from './VizWrapper'; +export const DEFAULT_MAX_DATA_POINTS = 43200; + +export interface AlertQueryOptions { + maxDataPoints?: number | undefined; +} + interface Props { data: PanelData; error?: Error; @@ -43,6 +57,7 @@ interface Props { onChangeThreshold?: (thresholds: ThresholdsConfig, index: number) => void; condition: string | null; onSetCondition: (refId: string) => void; + onChangeQueryOptions: (options: AlertQueryOptions, index: number) => void; } export const QueryWrapper: FC = ({ @@ -63,6 +78,7 @@ export const QueryWrapper: FC = ({ onChangeThreshold, condition, onSetCondition, + onChangeQueryOptions, }) => { const styles = useStyles2(getStyles); const isExpression = isExpressionQuery(query.model); @@ -96,11 +112,16 @@ export const QueryWrapper: FC = ({ // TODO add a warning label here too when the data looks like time series data and is used as an alert condition function HeaderExtras({ query, error, index }: { query: AlertQuery; error?: Error; index: number }) { + const queryOptions: AlertQueryOptions = { maxDataPoints: query.model.maxDataPoints }; + const alertQueryOptions: AlertQueryOptions = { + maxDataPoints: queryOptions.maxDataPoints, + }; + if (isExpressionQuery(query.model)) { return null; } else { return ( - + {onChangeTimeRange && ( = ({ onChange={(range) => onChangeTimeRange(range, index)} /> )} +
+ onChangeQueryOptions(options, index)} + /> +
onSetCondition(query.refId)} enabled={condition === query.refId} @@ -159,6 +186,53 @@ export const EmptyQueryWrapper = ({ children }: React.PropsWithChildren<{}>) => return
{children}
; }; +function MaxDataPointsOption({ + options, + onChange, +}: { + options: AlertQueryOptions; + onChange: (options: AlertQueryOptions) => void; +}) { + const value = options.maxDataPoints ?? ''; + + const onMaxDataPointsBlur = (event: ChangeEvent) => { + const maxDataPointsNumber = parseInt(event.target.value, 10); + + const maxDataPoints = isNaN(maxDataPointsNumber) || maxDataPointsNumber === 0 ? undefined : maxDataPointsNumber; + + if (maxDataPoints !== options.maxDataPoints) { + onChange({ + ...options, + maxDataPoints, + }); + } + }; + + return ( + + + The maximum data points per series. Used directly by some data sources and used in calculation of auto + interval. With streaming data this value is used for the rolling buffer. + + } + > + Max data points + + + + ); +} + const getStyles = (theme: GrafanaTheme2) => ({ wrapper: css` label: AlertingQueryWrapper; @@ -166,6 +240,9 @@ const getStyles = (theme: GrafanaTheme2) => ({ border: 1px solid ${theme.colors.border.medium}; border-radius: ${theme.shape.borderRadius(1)}; `, + queryOptions: css` + margin-bottom: -${theme.spacing(2)}; + `, dsTooltip: css` display: flex; align-items: center; diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts index c4bdd8a2e77..fdd6c8396e8 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts @@ -1,6 +1,6 @@ import { createAction, createReducer } from '@reduxjs/toolkit'; -import { DataQuery, RelativeTimeRange, getDefaultRelativeTimeRange } from '@grafana/data'; +import { DataQuery, getDefaultRelativeTimeRange, RelativeTimeRange } from '@grafana/data'; import { getNextRefIdChar } from 'app/core/utils/query'; import { findDataSourceFromExpressionRecursive } from 'app/features/alerting/utils/dataSourceFromExpression'; import { @@ -43,6 +43,7 @@ export const updateExpressionRefId = createAction<{ oldRefId: string; newRefId: export const rewireExpressions = createAction<{ oldRefId: string; newRefId: string }>('rewireExpressions'); export const updateExpressionType = createAction<{ refId: string; type: ExpressionQueryType }>('updateExpressionType'); export const updateExpressionTimeRange = createAction('updateExpressionTimeRange'); +export const updateMaxDataPoints = createAction<{ refId: string; maxDataPoints: number }>('updateMaxDataPoints'); export const queriesAndExpressionsReducer = createReducer(initialState, (builder) => { // data queries actions @@ -70,6 +71,19 @@ export const queriesAndExpressionsReducer = createReducer(initialState, (builder .addCase(setDataQueries, (state, { payload }) => { const expressionQueries = state.queries.filter((query) => isExpressionQuery(query.model)); state.queries = [...payload, ...expressionQueries]; + }) + .addCase(updateMaxDataPoints, (state, action) => { + state.queries = state.queries.map((query) => { + return query.refId === action.payload.refId + ? { + ...query, + model: { + ...query.model, + maxDataPoints: action.payload.maxDataPoints, + }, + } + : query; + }); }); // expressions actions