mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: allow creating/editing recording rules for Loki and Cortex (#38064)
This commit is contained in:
parent
309d263531
commit
9d8f61c738
@ -1,6 +1,6 @@
|
|||||||
import { Matcher, render, waitFor } from '@testing-library/react';
|
import { Matcher, render, waitFor } from '@testing-library/react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { locationService, setDataSourceSrv } from '@grafana/runtime';
|
import { BackendSrv, locationService, setBackendSrv, setDataSourceSrv } from '@grafana/runtime';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
import RuleEditor from './RuleEditor';
|
import RuleEditor from './RuleEditor';
|
||||||
import { Route, Router } from 'react-router-dom';
|
import { Route, Router } from 'react-router-dom';
|
||||||
@ -19,6 +19,7 @@ import { DashboardSearchHit } from 'app/features/search/types';
|
|||||||
import { getDefaultQueries } from './utils/rule-form';
|
import { getDefaultQueries } from './utils/rule-form';
|
||||||
import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
|
import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
|
||||||
import * as api from 'app/features/manage-dashboards/state/actions';
|
import * as api from 'app/features/manage-dashboards/state/actions';
|
||||||
|
import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
jest.mock('./components/rule-editor/ExpressionEditor', () => ({
|
jest.mock('./components/rule-editor/ExpressionEditor', () => ({
|
||||||
// eslint-disable-next-line react/display-name
|
// eslint-disable-next-line react/display-name
|
||||||
@ -64,7 +65,7 @@ function renderRuleEditor(identifier?: string) {
|
|||||||
|
|
||||||
const ui = {
|
const ui = {
|
||||||
inputs: {
|
inputs: {
|
||||||
name: byLabelText('Alert name'),
|
name: byLabelText('Rule name'),
|
||||||
alertType: byTestId('alert-type-picker'),
|
alertType: byTestId('alert-type-picker'),
|
||||||
dataSource: byTestId('datasource-picker'),
|
dataSource: byTestId('datasource-picker'),
|
||||||
folder: byTestId('folder-picker'),
|
folder: byTestId('folder-picker'),
|
||||||
@ -228,6 +229,169 @@ describe('RuleEditor', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can create a new cloud recording rule', async () => {
|
||||||
|
const dataSources = {
|
||||||
|
default: mockDataSource(
|
||||||
|
{
|
||||||
|
type: 'prometheus',
|
||||||
|
name: 'Prom',
|
||||||
|
isDefault: true,
|
||||||
|
},
|
||||||
|
{ alerting: true }
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||||
|
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
||||||
|
mocks.api.setRulerRuleGroup.mockResolvedValue();
|
||||||
|
mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);
|
||||||
|
mocks.api.fetchRulerRulesGroup.mockResolvedValue({
|
||||||
|
name: 'group2',
|
||||||
|
rules: [],
|
||||||
|
});
|
||||||
|
mocks.api.fetchRulerRules.mockResolvedValue({
|
||||||
|
namespace1: [
|
||||||
|
{
|
||||||
|
name: 'group1',
|
||||||
|
rules: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
namespace2: [
|
||||||
|
{
|
||||||
|
name: 'group2',
|
||||||
|
rules: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await renderRuleEditor();
|
||||||
|
await userEvent.type(await ui.inputs.name.find(), 'my great new recording rule');
|
||||||
|
await clickSelectOption(ui.inputs.alertType.get(), /Cortex\/Loki managed recording rule/);
|
||||||
|
const dataSourceSelect = ui.inputs.dataSource.get();
|
||||||
|
userEvent.click(byRole('textbox').get(dataSourceSelect));
|
||||||
|
await clickSelectOption(dataSourceSelect, 'Prom (default)');
|
||||||
|
await waitFor(() => expect(mocks.api.fetchRulerRules).toHaveBeenCalled());
|
||||||
|
await clickSelectOption(ui.inputs.namespace.get(), 'namespace2');
|
||||||
|
await clickSelectOption(ui.inputs.group.get(), 'group2');
|
||||||
|
|
||||||
|
await userEvent.type(ui.inputs.expr.get(), 'up == 1');
|
||||||
|
|
||||||
|
userEvent.click(ui.buttons.addLabel.get());
|
||||||
|
|
||||||
|
await userEvent.type(ui.inputs.labelKey(1).get(), 'team');
|
||||||
|
await userEvent.type(ui.inputs.labelValue(1).get(), 'the a-team');
|
||||||
|
|
||||||
|
// save and check what was sent to backend
|
||||||
|
userEvent.click(ui.buttons.save.get());
|
||||||
|
await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled());
|
||||||
|
expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith('Prom', 'namespace2', {
|
||||||
|
name: 'group2',
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
record: 'my great new recording rule',
|
||||||
|
labels: { team: 'the a-team' },
|
||||||
|
expr: 'up == 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can edit grafana managed rule', async () => {
|
||||||
|
const uid = 'FOOBAR123';
|
||||||
|
const folder = {
|
||||||
|
title: 'Folder A',
|
||||||
|
uid: 'abcd',
|
||||||
|
id: 1,
|
||||||
|
};
|
||||||
|
jest.spyOn(api, 'searchFolders').mockResolvedValue([folder] as DashboardSearchHit[]);
|
||||||
|
const getFolderByUid = jest.fn().mockResolvedValue({
|
||||||
|
...folder,
|
||||||
|
canSave: true,
|
||||||
|
});
|
||||||
|
const dataSources = {
|
||||||
|
default: mockDataSource({
|
||||||
|
type: 'prometheus',
|
||||||
|
name: 'Prom',
|
||||||
|
isDefault: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const backendSrv = ({
|
||||||
|
getFolderByUid,
|
||||||
|
} as any) as BackendSrv;
|
||||||
|
setBackendSrv(backendSrv);
|
||||||
|
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||||
|
mocks.api.setRulerRuleGroup.mockResolvedValue();
|
||||||
|
mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);
|
||||||
|
mocks.api.fetchRulerRules.mockResolvedValue({
|
||||||
|
[folder.title]: [
|
||||||
|
{
|
||||||
|
interval: '1m',
|
||||||
|
name: 'my great new rule',
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
annotations: { description: 'some description', summary: 'some summary' },
|
||||||
|
labels: { severity: 'warn', team: 'the a-team' },
|
||||||
|
for: '5m',
|
||||||
|
grafana_alert: {
|
||||||
|
uid,
|
||||||
|
namespace_uid: 'abcd',
|
||||||
|
namespace_id: 1,
|
||||||
|
condition: 'B',
|
||||||
|
data: getDefaultQueries(),
|
||||||
|
exec_err_state: GrafanaAlertStateDecision.Alerting,
|
||||||
|
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||||
|
title: 'my great new rule',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await renderRuleEditor(uid);
|
||||||
|
|
||||||
|
// check that it's filled in
|
||||||
|
const nameInput = await ui.inputs.name.find();
|
||||||
|
expect(nameInput).toHaveValue('my great new rule');
|
||||||
|
expect(ui.inputs.folder.get()).toHaveTextContent(new RegExp(folder.title));
|
||||||
|
expect(ui.inputs.annotationValue(0).get()).toHaveValue('some description');
|
||||||
|
expect(ui.inputs.annotationValue(1).get()).toHaveValue('some summary');
|
||||||
|
|
||||||
|
// add an annotation
|
||||||
|
await clickSelectOption(ui.inputs.annotationKey(2).get(), /Add new/);
|
||||||
|
await userEvent.type(byRole('textbox').get(ui.inputs.annotationKey(2).get()), 'custom');
|
||||||
|
await userEvent.type(ui.inputs.annotationValue(2).get(), 'value');
|
||||||
|
|
||||||
|
//add a label
|
||||||
|
await userEvent.type(ui.inputs.labelKey(2).get(), 'custom');
|
||||||
|
await userEvent.type(ui.inputs.labelValue(2).get(), 'value');
|
||||||
|
|
||||||
|
// save and check what was sent to backend
|
||||||
|
userEvent.click(ui.buttons.save.get());
|
||||||
|
await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled());
|
||||||
|
|
||||||
|
expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, 'Folder A', {
|
||||||
|
interval: '1m',
|
||||||
|
name: 'my great new rule',
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
annotations: { description: 'some description', summary: 'some summary', custom: 'value' },
|
||||||
|
labels: { severity: 'warn', team: 'the a-team', custom: 'value' },
|
||||||
|
for: '5m',
|
||||||
|
grafana_alert: {
|
||||||
|
uid,
|
||||||
|
condition: 'B',
|
||||||
|
data: getDefaultQueries(),
|
||||||
|
exec_err_state: 'Alerting',
|
||||||
|
no_data_state: 'NoData',
|
||||||
|
title: 'my great new rule',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('for cloud alerts, should only allow to select editable rules sources', async () => {
|
it('for cloud alerts, should only allow to select editable rules sources', async () => {
|
||||||
const dataSources: Record<string, DataSourceInstanceSettings<any>> = {
|
const dataSources: Record<string, DataSourceInstanceSettings<any>> = {
|
||||||
// can edit rules
|
// can edit rules
|
||||||
|
@ -121,7 +121,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
|||||||
{showStep2 && (
|
{showStep2 && (
|
||||||
<>
|
<>
|
||||||
<QueryStep />
|
<QueryStep />
|
||||||
{type === RuleFormType.cloud ? <CloudConditionsStep /> : <GrafanaConditionsStep />}
|
{type === RuleFormType.grafana ? <GrafanaConditionsStep /> : <CloudConditionsStep />}
|
||||||
<DetailsStep />
|
<DetailsStep />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -40,19 +40,24 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
|||||||
if (contextSrv.isEditor) {
|
if (contextSrv.isEditor) {
|
||||||
result.push({
|
result.push({
|
||||||
label: 'Cortex/Loki managed alert',
|
label: 'Cortex/Loki managed alert',
|
||||||
value: RuleFormType.cloud,
|
value: RuleFormType.cloudAlerting,
|
||||||
description: 'Alert based on a system or application behavior. Based on Prometheus.',
|
description: 'Alert based on a system or application behavior. Based on Prometheus.',
|
||||||
});
|
});
|
||||||
|
result.push({
|
||||||
|
label: 'Cortex/Loki managed recording rule',
|
||||||
|
value: RuleFormType.cloudRecording,
|
||||||
|
description: 'Recording rule to pre-compute frequently needed or expensive calculations. Based on Prometheus.',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RuleEditorSection stepNo={1} title="Alert type">
|
<RuleEditorSection stepNo={1} title="Rule type">
|
||||||
<Field
|
<Field
|
||||||
className={styles.formInput}
|
className={styles.formInput}
|
||||||
label="Alert name"
|
label="Rule name"
|
||||||
error={errors?.name?.message}
|
error={errors?.name?.message}
|
||||||
invalid={!!errors.name?.message}
|
invalid={!!errors.name?.message}
|
||||||
>
|
>
|
||||||
@ -65,7 +70,7 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
|||||||
<div className={styles.flexRow}>
|
<div className={styles.flexRow}>
|
||||||
<Field
|
<Field
|
||||||
disabled={editingExistingRule}
|
disabled={editingExistingRule}
|
||||||
label="Alert type"
|
label="Rule type"
|
||||||
className={styles.formInput}
|
className={styles.formInput}
|
||||||
error={errors.type?.message}
|
error={errors.type?.message}
|
||||||
invalid={!!errors.type?.message}
|
invalid={!!errors.type?.message}
|
||||||
@ -87,7 +92,7 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
{ruleFormType === RuleFormType.cloud && (
|
{(ruleFormType === RuleFormType.cloudRecording || ruleFormType === RuleFormType.cloudAlerting) && (
|
||||||
<Field
|
<Field
|
||||||
className={styles.formInput}
|
className={styles.formInput}
|
||||||
label="Select data source"
|
label="Select data source"
|
||||||
@ -115,9 +120,9 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
|||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{ruleFormType === RuleFormType.cloud && dataSourceName && (
|
{(ruleFormType === RuleFormType.cloudRecording || ruleFormType === RuleFormType.cloudAlerting) &&
|
||||||
<GroupAndNamespaceFields dataSourceName={dataSourceName} />
|
dataSourceName && <GroupAndNamespaceFields dataSourceName={dataSourceName} />}
|
||||||
)}
|
|
||||||
{ruleFormType === RuleFormType.grafana && (
|
{ruleFormType === RuleFormType.grafana && (
|
||||||
<Field
|
<Field
|
||||||
label="Folder"
|
label="Folder"
|
||||||
|
@ -3,7 +3,7 @@ import { css } from '@emotion/css';
|
|||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { Field, Input, InputControl, Select, useStyles } from '@grafana/ui';
|
import { Field, Input, InputControl, Select, useStyles } from '@grafana/ui';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import { RuleFormValues } from '../../types/rule-form';
|
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||||
import { timeOptions } from '../../utils/time';
|
import { timeOptions } from '../../utils/time';
|
||||||
import { RuleEditorSection } from './RuleEditorSection';
|
import { RuleEditorSection } from './RuleEditorSection';
|
||||||
import { PreviewRule } from './PreviewRule';
|
import { PreviewRule } from './PreviewRule';
|
||||||
@ -13,9 +13,17 @@ export const CloudConditionsStep: FC = () => {
|
|||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
|
watch,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useFormContext<RuleFormValues>();
|
} = useFormContext<RuleFormValues>();
|
||||||
|
|
||||||
|
const type = watch('type');
|
||||||
|
|
||||||
|
// cloud recording rules do not have alert conditions
|
||||||
|
if (type === RuleFormType.cloudRecording) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RuleEditorSection stepNo={3} title="Define alert conditions">
|
<RuleEditorSection stepNo={3} title="Define alert conditions">
|
||||||
<Field label="For" description="Expression has to be true for this long for the alert to be fired.">
|
<Field label="For" description="Expression has to be true for this long for the alert to be fired.">
|
||||||
|
@ -2,15 +2,27 @@ import React, { FC } from 'react';
|
|||||||
import LabelsField from './LabelsField';
|
import LabelsField from './LabelsField';
|
||||||
import AnnotationsField from './AnnotationsField';
|
import AnnotationsField from './AnnotationsField';
|
||||||
import { RuleEditorSection } from './RuleEditorSection';
|
import { RuleEditorSection } from './RuleEditorSection';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||||
|
|
||||||
export const DetailsStep: FC = () => {
|
export const DetailsStep: FC = () => {
|
||||||
|
const { watch } = useFormContext<RuleFormValues>();
|
||||||
|
|
||||||
|
const type = watch('type');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RuleEditorSection
|
<RuleEditorSection
|
||||||
stepNo={4}
|
stepNo={type === RuleFormType.cloudRecording ? 3 : 4}
|
||||||
title="Add details for your alert"
|
title={
|
||||||
description="Write a summary and add labels to help you better manage your alerts"
|
type === RuleFormType.cloudRecording ? 'Add details for your recording rule' : 'Add details for your alert'
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
type === RuleFormType.cloudRecording
|
||||||
|
? 'Add labels to help you better manage your rules'
|
||||||
|
: 'Write a summary and add labels to help you better manage your alerts'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<AnnotationsField />
|
{type !== RuleFormType.cloudRecording && <AnnotationsField />}
|
||||||
<LabelsField />
|
<LabelsField />
|
||||||
</RuleEditorSection>
|
</RuleEditorSection>
|
||||||
);
|
);
|
||||||
|
@ -18,7 +18,7 @@ export function PreviewRule(): React.ReactElement | null {
|
|||||||
const { getValues } = useFormContext();
|
const { getValues } = useFormContext();
|
||||||
const [type] = getValues(fields);
|
const [type] = getValues(fields);
|
||||||
|
|
||||||
if (type === RuleFormType.cloud) {
|
if (type === RuleFormType.cloudRecording || type === RuleFormType.cloudAlerting) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ function createPreviewRequest(values: any[]): PreviewRuleRequest {
|
|||||||
const [type, dataSourceName, condition, queries, expression] = values;
|
const [type, dataSourceName, condition, queries, expression] = values;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case RuleFormType.cloud:
|
case RuleFormType.cloudAlerting:
|
||||||
return {
|
return {
|
||||||
dataSourceName,
|
dataSourceName,
|
||||||
expr: expression,
|
expr: expression,
|
||||||
|
@ -15,8 +15,11 @@ export const QueryStep: FC = () => {
|
|||||||
const type = watch('type');
|
const type = watch('type');
|
||||||
const dataSourceName = watch('dataSourceName');
|
const dataSourceName = watch('dataSourceName');
|
||||||
return (
|
return (
|
||||||
<RuleEditorSection stepNo={2} title="Create a query to be alerted on">
|
<RuleEditorSection
|
||||||
{type === RuleFormType.cloud && dataSourceName && (
|
stepNo={2}
|
||||||
|
title={type === RuleFormType.cloudRecording ? 'Create a query to be recorded' : 'Create a query to be alerted on'}
|
||||||
|
>
|
||||||
|
{(type === RuleFormType.cloudRecording || type === RuleFormType.cloudAlerting) && dataSourceName && (
|
||||||
<Field error={errors.expression?.message} invalid={!!errors.expression?.message}>
|
<Field error={errors.expression?.message} invalid={!!errors.expression?.message}>
|
||||||
<InputControl
|
<InputControl
|
||||||
name="expression"
|
name="expression"
|
||||||
|
@ -63,7 +63,7 @@ export function RuleListErrors(): ReactElement {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{errors.length && !closed && (
|
{!!errors.length && !closed && (
|
||||||
<Alert
|
<Alert
|
||||||
data-testid="cloud-rulessource-errors"
|
data-testid="cloud-rulessource-errors"
|
||||||
title="Errors loading rules"
|
title="Errors loading rules"
|
||||||
|
@ -6,6 +6,7 @@ import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
|
|||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { checkIfLotexSupportsEditingRulesAction } from '../state/actions';
|
import { checkIfLotexSupportsEditingRulesAction } from '../state/actions';
|
||||||
|
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||||
|
|
||||||
interface ResultBag {
|
interface ResultBag {
|
||||||
isEditable?: boolean;
|
isEditable?: boolean;
|
||||||
@ -20,7 +21,7 @@ export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO):
|
|||||||
const { folder, loading } = useFolder(folderUID);
|
const { folder, loading } = useFolder(folderUID);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (checkEditingRequests[rulesSourceName] === undefined) {
|
if (checkEditingRequests[rulesSourceName] === undefined && rulesSourceName !== GRAFANA_RULES_SOURCE_NAME) {
|
||||||
dispatch(checkIfLotexSupportsEditingRulesAction(rulesSourceName));
|
dispatch(checkIfLotexSupportsEditingRulesAction(rulesSourceName));
|
||||||
}
|
}
|
||||||
}, [rulesSourceName, checkEditingRequests, dispatch]);
|
}, [rulesSourceName, checkEditingRequests, dispatch]);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { locationService } from '@grafana/runtime';
|
import { getBackendSrv, locationService } from '@grafana/runtime';
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import {
|
import {
|
||||||
AlertmanagerAlert,
|
AlertmanagerAlert,
|
||||||
@ -41,7 +41,7 @@ import { RuleFormType, RuleFormValues } from '../types/rule-form';
|
|||||||
import { getAllRulesSourceNames, GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from '../utils/datasource';
|
import { getAllRulesSourceNames, GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from '../utils/datasource';
|
||||||
import { makeAMLink, retryWhile } from '../utils/misc';
|
import { makeAMLink, retryWhile } from '../utils/misc';
|
||||||
import { isFetchError, withAppEvents, withSerializedError } from '../utils/redux';
|
import { isFetchError, withAppEvents, withSerializedError } from '../utils/redux';
|
||||||
import { formValuesToRulerAlertingRuleDTO, formValuesToRulerGrafanaRuleDTO } from '../utils/rule-form';
|
import { formValuesToRulerRuleDTO, formValuesToRulerGrafanaRuleDTO } from '../utils/rule-form';
|
||||||
import {
|
import {
|
||||||
isCloudRuleIdentifier,
|
isCloudRuleIdentifier,
|
||||||
isGrafanaRuleIdentifier,
|
isGrafanaRuleIdentifier,
|
||||||
@ -50,7 +50,6 @@ import {
|
|||||||
isRulerNotSupportedResponse,
|
isRulerNotSupportedResponse,
|
||||||
} from '../utils/rules';
|
} from '../utils/rules';
|
||||||
import { addDefaultsToAlertmanagerConfig } from '../utils/alertmanager';
|
import { addDefaultsToAlertmanagerConfig } from '../utils/alertmanager';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv';
|
|
||||||
import * as ruleId from '../utils/rule-id';
|
import * as ruleId from '../utils/rule-id';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import messageFromError from 'app/plugins/datasource/grafana-azure-monitor-datasource/utils/messageFromError';
|
import messageFromError from 'app/plugins/datasource/grafana-azure-monitor-datasource/utils/messageFromError';
|
||||||
@ -251,7 +250,7 @@ export function deleteRuleAction(
|
|||||||
|
|
||||||
async function saveLotexRule(values: RuleFormValues, existing?: RuleWithLocation): Promise<RuleIdentifier> {
|
async function saveLotexRule(values: RuleFormValues, existing?: RuleWithLocation): Promise<RuleIdentifier> {
|
||||||
const { dataSourceName, group, namespace } = values;
|
const { dataSourceName, group, namespace } = values;
|
||||||
const formRule = formValuesToRulerAlertingRuleDTO(values);
|
const formRule = formValuesToRulerRuleDTO(values);
|
||||||
if (dataSourceName && group && namespace) {
|
if (dataSourceName && group && namespace) {
|
||||||
// if we're updating a rule...
|
// if we're updating a rule...
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@ -373,7 +372,7 @@ export const saveRuleFormAction = createAsyncThunk(
|
|||||||
const { type } = values;
|
const { type } = values;
|
||||||
// in case of system (cortex/loki)
|
// in case of system (cortex/loki)
|
||||||
let identifier: RuleIdentifier;
|
let identifier: RuleIdentifier;
|
||||||
if (type === RuleFormType.cloud) {
|
if (type === RuleFormType.cloudAlerting || type === RuleFormType.cloudRecording) {
|
||||||
identifier = await saveLotexRule(values, existing);
|
identifier = await saveLotexRule(values, existing);
|
||||||
// in case of grafana managed
|
// in case of grafana managed
|
||||||
} else if (type === RuleFormType.grafana) {
|
} else if (type === RuleFormType.grafana) {
|
||||||
@ -545,7 +544,7 @@ export const deleteTemplateAction = (templateName: string, alertManagerSourceNam
|
|||||||
|
|
||||||
export const fetchFolderAction = createAsyncThunk(
|
export const fetchFolderAction = createAsyncThunk(
|
||||||
'unifiedalerting/fetchFolder',
|
'unifiedalerting/fetchFolder',
|
||||||
(uid: string): Promise<FolderDTO> => withSerializedError(backendSrv.getFolderByUid(uid))
|
(uid: string): Promise<FolderDTO> => withSerializedError((getBackendSrv() as any).getFolderByUid(uid))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const fetchFolderIfNotFetchedAction = (uid: string): ThunkResult<void> => {
|
export const fetchFolderIfNotFetchedAction = (uid: string): ThunkResult<void> => {
|
||||||
|
@ -2,7 +2,8 @@ import { AlertQuery, GrafanaAlertStateDecision } from 'app/types/unified-alertin
|
|||||||
|
|
||||||
export enum RuleFormType {
|
export enum RuleFormType {
|
||||||
grafana = 'grafana',
|
grafana = 'grafana',
|
||||||
cloud = 'cloud',
|
cloudAlerting = 'cloud-alerting',
|
||||||
|
cloudRecording = 'cloud-recording',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RuleFormValues {
|
export interface RuleFormValues {
|
||||||
|
@ -20,14 +20,14 @@ import {
|
|||||||
AlertQuery,
|
AlertQuery,
|
||||||
Labels,
|
Labels,
|
||||||
PostableRuleGrafanaRuleDTO,
|
PostableRuleGrafanaRuleDTO,
|
||||||
RulerAlertingRuleDTO,
|
RulerRuleDTO,
|
||||||
} from 'app/types/unified-alerting-dto';
|
} from 'app/types/unified-alerting-dto';
|
||||||
import { EvalFunction } from '../../state/alertDef';
|
import { EvalFunction } from '../../state/alertDef';
|
||||||
import { RuleFormType, RuleFormValues } from '../types/rule-form';
|
import { RuleFormType, RuleFormValues } from '../types/rule-form';
|
||||||
import { Annotation } from './constants';
|
import { Annotation } from './constants';
|
||||||
import { isGrafanaRulesSource } from './datasource';
|
import { isGrafanaRulesSource } from './datasource';
|
||||||
import { arrayToRecord, recordToArray } from './misc';
|
import { arrayToRecord, recordToArray } from './misc';
|
||||||
import { isAlertingRulerRule, isGrafanaRulerRule } from './rules';
|
import { isAlertingRulerRule, isGrafanaRulerRule, isRecordingRulerRule } from './rules';
|
||||||
import { parseInterval } from './time';
|
import { parseInterval } from './time';
|
||||||
|
|
||||||
export const getDefaultFormValues = (): RuleFormValues =>
|
export const getDefaultFormValues = (): RuleFormValues =>
|
||||||
@ -59,15 +59,24 @@ export const getDefaultFormValues = (): RuleFormValues =>
|
|||||||
forTimeUnit: 'm',
|
forTimeUnit: 'm',
|
||||||
});
|
});
|
||||||
|
|
||||||
export function formValuesToRulerAlertingRuleDTO(values: RuleFormValues): RulerAlertingRuleDTO {
|
export function formValuesToRulerRuleDTO(values: RuleFormValues): RulerRuleDTO {
|
||||||
const { name, expression, forTime, forTimeUnit } = values;
|
const { name, expression, forTime, forTimeUnit, type } = values;
|
||||||
return {
|
if (type === RuleFormType.cloudAlerting) {
|
||||||
alert: name,
|
return {
|
||||||
for: `${forTime}${forTimeUnit}`,
|
alert: name,
|
||||||
annotations: arrayToRecord(values.annotations || []),
|
for: `${forTime}${forTimeUnit}`,
|
||||||
labels: arrayToRecord(values.labels || []),
|
annotations: arrayToRecord(values.annotations || []),
|
||||||
expr: expression,
|
labels: arrayToRecord(values.labels || []),
|
||||||
};
|
expr: expression,
|
||||||
|
};
|
||||||
|
} else if (type === RuleFormType.cloudRecording) {
|
||||||
|
return {
|
||||||
|
record: name,
|
||||||
|
labels: arrayToRecord(values.labels || []),
|
||||||
|
expr: expression,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected rule type: ${type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function listifyLabelsOrAnnotations(item: Labels | Annotations | undefined): Array<{ key: string; value: string }> {
|
function listifyLabelsOrAnnotations(item: Labels | Annotations | undefined): Array<{ key: string; value: string }> {
|
||||||
@ -125,7 +134,7 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
|
|||||||
return {
|
return {
|
||||||
...defaultFormValues,
|
...defaultFormValues,
|
||||||
name: rule.alert,
|
name: rule.alert,
|
||||||
type: RuleFormType.cloud,
|
type: RuleFormType.cloudAlerting,
|
||||||
dataSourceName: ruleSourceName,
|
dataSourceName: ruleSourceName,
|
||||||
namespace,
|
namespace,
|
||||||
group: group.name,
|
group: group.name,
|
||||||
@ -135,8 +144,19 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
|
|||||||
annotations: listifyLabelsOrAnnotations(rule.annotations),
|
annotations: listifyLabelsOrAnnotations(rule.annotations),
|
||||||
labels: listifyLabelsOrAnnotations(rule.labels),
|
labels: listifyLabelsOrAnnotations(rule.labels),
|
||||||
};
|
};
|
||||||
|
} else if (isRecordingRulerRule(rule)) {
|
||||||
|
return {
|
||||||
|
...defaultFormValues,
|
||||||
|
name: rule.record,
|
||||||
|
type: RuleFormType.cloudRecording,
|
||||||
|
dataSourceName: ruleSourceName,
|
||||||
|
namespace,
|
||||||
|
group: group.name,
|
||||||
|
expression: rule.expr,
|
||||||
|
labels: listifyLabelsOrAnnotations(rule.labels),
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Editing recording rules not supported (yet)');
|
throw new Error('Unexpected type of rule for cloud rules source');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user