Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alerting: Enable preview for recording rules #63260

Merged
merged 19 commits into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,99 @@
import React, { FC, useEffect, useState } from 'react';
import { useAsync } from 'react-use';

import { PanelData, CoreApp } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { DataQuery, LoadingState } from '@grafana/schema';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { isExpressionQuery } from 'app/features/expressions/guards';
import { AlertQuery } from 'app/types/unified-alerting-dto';

import { TABLE, TIMESERIES } from '../../utils/constants';
import { SupportedPanelPlugins } from '../PanelPluginsButtonGroup';

import { VizWrapper } from './VizWrapper';

export interface RecordingRuleEditorProps {
queries: AlertQuery[];
onChangeQuery: (updatedQueries: AlertQuery[]) => void;
runQueries: (queries: AlertQuery[]) => void;
panelData: Record<string, PanelData>;
dataSourceName: string;
}

export const RecordingRuleEditor: FC<RecordingRuleEditorProps> = ({
queries,
onChangeQuery,
runQueries,
panelData,
dataSourceName,
}) => {
const [data, setData] = useState<PanelData>({
series: [],
state: LoadingState.NotStarted,
timeRange: getTimeSrv().timeRange(),
});

const isExpression = isExpressionQuery(queries[0].model);

const [pluginId, changePluginId] = useState<SupportedPanelPlugins>(isExpression ? TABLE : TIMESERIES);

useEffect(() => {
setData(panelData?.[queries[0].refId]);
}, [panelData, queries]);

const {
error,
loading,
value: dataSource,
} = useAsync(() => {
return getDataSourceSrv().get(dataSourceName);
}, [dataSourceName]);

const handleChangedQuery = (changedQuery: DataQuery) => {
const query = queries[0];

const merged = {
...query,
refId: changedQuery.refId,
queryType: query.model.queryType ?? '',
//@ts-ignore
expr: changedQuery?.expr,
model: {
refId: changedQuery.refId,
//@ts-ignore
expr: changedQuery?.expr,
editorMode: 'code',
},
};
onChangeQuery([merged]);
};

if (loading || dataSource?.name !== dataSourceName) {
return null;
}

const dsi = getDataSourceSrv().getInstanceSettings(dataSourceName);

if (error || !dataSource || !dataSource?.components?.QueryEditor || !dsi) {
const errorMessage = error?.message || 'Data source plugin does not export any Query Editor component';
return <div>Could not load query editor due to: {errorMessage}</div>;
}

const QueryEditor = dataSource.components.QueryEditor;

return (
<>
<QueryEditor
query={queries[0]}
queries={queries}
app={CoreApp.UnifiedAlerting}
onChange={handleChangedQuery}
onRunQuery={() => runQueries(queries)}
datasource={dataSource}
/>

{data && <VizWrapper data={data} currentPanel={pluginId} changePanel={changePluginId} />}
</>
);
};
Expand Up @@ -52,7 +52,7 @@ export const AlertType = ({ editingExistingRule }: Props) => {
)}

<div className={styles.flexRow}>
{(ruleFormType === RuleFormType.cloudRecording || ruleFormType === RuleFormType.cloudAlerting) && (
{(ruleFormType === RuleFormType.cloudAlerting || ruleFormType === RuleFormType.cloudRecording) && (
<Field
className={styles.formInput}
label="Select data source"
Expand Down
@@ -1,20 +1,22 @@
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
import { useFormContext } from 'react-hook-form';

import { LoadingState, PanelData } from '@grafana/data';
import { LoadingState, PanelData, getDefaultRelativeTimeRange } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Stack } from '@grafana/experimental';
import { config } from '@grafana/runtime';
import { Alert, Button, Field, InputControl, Tooltip } from '@grafana/ui';
import { config, getDataSourceSrv } from '@grafana/runtime';
import { Alert, Button, Field, Tooltip, InputControl } from '@grafana/ui';
import { isExpressionQuery } from 'app/features/expressions/guards';
import { AlertQuery } from 'app/types/unified-alerting-dto';

import { useRulesSourcesWithRuler } from '../../../hooks/useRuleSourcesWithRuler';
import { AlertingQueryRunner } from '../../../state/AlertingQueryRunner';
import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
import { getDefaultOrFirstCompatibleDataSource } from '../../../utils/datasource';
import { ExpressionEditor } from '../ExpressionEditor';
import { ExpressionsEditor } from '../ExpressionsEditor';
import { QueryEditor } from '../QueryEditor';
import { RecordingRuleEditor } from '../RecordingRuleEditor';
import { RuleEditorSection } from '../RuleEditorSection';
import { errorFromSeries, refIdExists } from '../util';

Expand All @@ -27,6 +29,7 @@ import {
removeExpression,
rewireExpressions,
setDataQueries,
setRecordingRulesQueries,
updateExpression,
updateExpressionRefId,
updateExpressionTimeRange,
Expand All @@ -40,6 +43,7 @@ interface Props {

export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: Props) => {
const runner = useRef(new AlertingQueryRunner());

const {
setValue,
getValues,
Expand All @@ -53,15 +57,15 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
queries: getValues('queries'),
panelData: {},
};
const [{ queries }, dispatch] = useReducer(queriesAndExpressionsReducer, initialState);

const [{ queries }, dispatch] = useReducer(queriesAndExpressionsReducer, initialState);
const [type, condition, dataSourceName] = watch(['type', 'condition', 'dataSourceName']);

const isGrafanaManagedType = type === RuleFormType.grafana;
const isCloudAlertRuleType = type === RuleFormType.cloudAlerting;
const isRecordingRuleType = type === RuleFormType.cloudRecording;
const isCloudAlertRuleType = type === RuleFormType.cloudAlerting;

const showCloudExpressionEditor = (isRecordingRuleType || isCloudAlertRuleType) && dataSourceName;
const rulesSourcesWithRuler = useRulesSourcesWithRuler();

const cancelQueries = useCallback(() => {
runner.current.cancel();
Expand Down Expand Up @@ -164,6 +168,43 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
[queries]
);

const onChangeRecordingRulesQueries = useCallback(
(updatedQueries) => {
const dataSourceSettings = getDataSourceSrv().getInstanceSettings(updatedQueries[0].datasourceUid);
if (!dataSourceSettings) {
throw new Error('The Data source has not been defined.');
}

const expression = updatedQueries[0].model?.expr || '';

setValue('dataSourceName', dataSourceSettings.name);
setValue('expression', expression);

dispatch(setRecordingRulesQueries({ recordingRuleQueries: updatedQueries, expression }));
runQueries();
},
[runQueries, setValue]
);

const recordingRuleDefaultDatasource = rulesSourcesWithRuler[0];

useEffect(() => {
setPanelData({});
if (type === RuleFormType.cloudRecording) {
const defaultQuery = {
refId: 'A',
datasourceUid: recordingRuleDefaultDatasource.uid,
queryType: '',
relativeTimeRange: getDefaultRelativeTimeRange(),
model: {
refId: 'A',
hide: false,
},
};
dispatch(setRecordingRulesQueries({ recordingRuleQueries: [defaultQuery], expression: getValues('expression') }));
}
}, [type, recordingRuleDefaultDatasource, editingExistingRule, getValues]);

const onDuplicateQuery = useCallback((query: AlertQuery) => {
dispatch(duplicateQuery(query));
}, []);
Expand All @@ -180,8 +221,21 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
<RuleEditorSection stepNo={2} title="Set a query and alert condition">
<AlertType editingExistingRule={editingExistingRule} />

{/* This is the PromQL Editor for Cloud rules and recording rules */}
{showCloudExpressionEditor && (
{/* This is the PromQL Editor for recording rules */}
{isRecordingRuleType && dataSourceName && (
<Field error={errors.expression?.message} invalid={!!errors.expression?.message}>
<RecordingRuleEditor
dataSourceName={dataSourceName}
queries={queries}
runQueries={runQueries}
onChangeQuery={onChangeRecordingRulesQueries}
panelData={panelData}
/>
</Field>
)}

{/* This is the PromQL Editor for Cloud rules */}
{isCloudAlertRuleType && dataSourceName && (
<Field error={errors.expression?.message} invalid={!!errors.expression?.message}>
<InputControl
name="expression"
Expand Down
Expand Up @@ -42,6 +42,10 @@ export const updateExpressionType = createAction<{ refId: string; type: Expressi
export const updateExpressionTimeRange = createAction('updateExpressionTimeRange');
export const updateMaxDataPoints = createAction<{ refId: string; maxDataPoints: number }>('updateMaxDataPoints');

export const setRecordingRulesQueries = createAction<{ recordingRuleQueries: AlertQuery[]; expression: string }>(
'setRecordingRulesQueries'
);

export const queriesAndExpressionsReducer = createReducer(initialState, (builder) => {
// data queries actions
builder
Expand Down Expand Up @@ -69,6 +73,15 @@ export const queriesAndExpressionsReducer = createReducer(initialState, (builder
const expressionQueries = state.queries.filter((query) => isExpressionQuery(query.model));
state.queries = [...payload, ...expressionQueries];
})
.addCase(setRecordingRulesQueries, (state, { payload }) => {
const query = payload.recordingRuleQueries[0];
const recordingRuleQuery = {
...query,
...{ model: { expr: payload.expression, refId: query.model.refId } },
};

state.queries = [recordingRuleQuery];
})
.addCase(updateMaxDataPoints, (state, action) => {
state.queries = state.queries.map((query) => {
return query.refId === action.payload.refId
Expand Down
22 changes: 22 additions & 0 deletions public/app/features/alerting/unified/utils/rule-form.ts
Expand Up @@ -7,9 +7,11 @@ import {
RelativeTimeRange,
ScopedVars,
TimeRange,
DataSourceInstanceSettings,
} from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend';
import { DataSourceJsonData } from '@grafana/schema';
import { getNextRefIdChar } from 'app/core/utils/query';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { ExpressionQuery, ExpressionQueryType, ExpressionDatasourceUID } from 'app/features/expressions/types';
Expand Down Expand Up @@ -56,6 +58,7 @@ export const getDefaultFormValues = (): RuleFormValues => {
// grafana
folder: null,
queries: [],
recordingRulesQueries: [],
condition: '',
noDataState: GrafanaAlertStateDecision.NoData,
execErrState: GrafanaAlertStateDecision.Error,
Expand Down Expand Up @@ -221,6 +224,25 @@ export const getDefaultQueries = (): AlertQuery[] => {
];
};

export const getDefaultRecordingRulesQueries = (
rulesSourcesWithRuler: Array<DataSourceInstanceSettings<DataSourceJsonData>>
): AlertQuery[] => {
const relativeTimeRange = getDefaultRelativeTimeRange();

return [
{
refId: 'A',
datasourceUid: rulesSourcesWithRuler[0]?.uid || '',
queryType: '',
relativeTimeRange,
model: {
refId: 'A',
hide: false,
},
},
];
};

const getDefaultExpressions = (...refIds: [string, string]): AlertQuery[] => {
const refOne = refIds[0];
const refTwo = refIds[1];
Expand Down