mirror of
https://github.com/grafana/grafana.git
synced 2024-12-01 13:09:22 -06:00
Alerting: Fix eval interval not being saved when creating a new group (#93821)
This commit is contained in:
parent
1aed1d8017
commit
e4698d9c52
@ -3,44 +3,25 @@ import { ui } from 'test/helpers/alertingRuleEditor';
|
||||
import { render, screen } from 'test/test-utils';
|
||||
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
|
||||
import { setFolderResponse } from 'app/features/alerting/unified/mocks/server/configure';
|
||||
import { captureRequests } from 'app/features/alerting/unified/mocks/server/events';
|
||||
import { DashboardSearchItemType } from 'app/features/search/types';
|
||||
|
||||
import { searchFolders } from '../../../../app/features/manage-dashboards/state/actions';
|
||||
import { backendSrv } from '../../../core/services/backend_srv';
|
||||
import { AccessControlAction } from '../../../types';
|
||||
|
||||
import RuleEditor from './RuleEditor';
|
||||
import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
|
||||
import { setupMswServer } from './mockApi';
|
||||
import { grantUserPermissions, mockDataSource, mockFolder } from './mocks';
|
||||
import { grafanaRulerRule } from './mocks/grafanaRulerApi';
|
||||
import { setupDataSources } from './testSetup/datasources';
|
||||
import { Annotation } from './utils/constants';
|
||||
|
||||
jest.mock('./components/rule-editor/ExpressionEditor', () => ({
|
||||
ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => (
|
||||
<input value={value} data-testid="expr" onChange={(e) => onChange(e.target.value)} />
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({
|
||||
AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) => <div>{actions}</div>,
|
||||
}));
|
||||
|
||||
jest.mock('../../../../app/features/manage-dashboards/state/actions');
|
||||
|
||||
// there's no angular scope in test and things go terribly wrong when trying to render the query editor row.
|
||||
// lets just skip it
|
||||
jest.mock('app/features/query/components/QueryEditorRow', () => ({
|
||||
QueryEditorRow: () => <p>hi</p>,
|
||||
}));
|
||||
|
||||
jest.setTimeout(60 * 1000);
|
||||
|
||||
const mocks = {
|
||||
searchFolders: jest.mocked(searchFolders),
|
||||
};
|
||||
|
||||
setupMswServer();
|
||||
|
||||
function renderRuleEditor(identifier: string) {
|
||||
@ -55,6 +36,24 @@ function renderRuleEditor(identifier: string) {
|
||||
}
|
||||
|
||||
describe('RuleEditor grafana managed rules', () => {
|
||||
const folder = {
|
||||
title: 'Folder A',
|
||||
uid: grafanaRulerRule.grafana_alert.namespace_uid,
|
||||
id: 1,
|
||||
type: DashboardSearchItemType.DashDB,
|
||||
accessControl: {
|
||||
[AccessControlAction.AlertingRuleUpdate]: true,
|
||||
},
|
||||
};
|
||||
|
||||
const slashedFolder = {
|
||||
title: 'Folder with /',
|
||||
uid: 'abcde',
|
||||
id: 2,
|
||||
accessControl: {
|
||||
[AccessControlAction.AlertingRuleUpdate]: true,
|
||||
},
|
||||
};
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
contextSrv.isEditor = true;
|
||||
@ -73,21 +72,6 @@ describe('RuleEditor grafana managed rules', () => {
|
||||
AccessControlAction.AlertingRuleExternalRead,
|
||||
AccessControlAction.AlertingRuleExternalWrite,
|
||||
]);
|
||||
});
|
||||
|
||||
it('can edit grafana managed rule', async () => {
|
||||
const folder = {
|
||||
title: 'Folder A',
|
||||
uid: grafanaRulerRule.grafana_alert.namespace_uid,
|
||||
id: 1,
|
||||
type: DashboardSearchItemType.DashDB,
|
||||
};
|
||||
|
||||
const slashedFolder = {
|
||||
title: 'Folder with /',
|
||||
uid: 'abcde',
|
||||
id: 2,
|
||||
};
|
||||
|
||||
const dataSources = {
|
||||
default: mockDataSource(
|
||||
@ -99,18 +83,12 @@ describe('RuleEditor grafana managed rules', () => {
|
||||
{ alerting: false }
|
||||
),
|
||||
};
|
||||
|
||||
jest.spyOn(backendSrv, 'getFolderByUid').mockResolvedValue({
|
||||
...mockFolder(),
|
||||
accessControl: {
|
||||
[AccessControlAction.AlertingRuleUpdate]: true,
|
||||
},
|
||||
});
|
||||
|
||||
setupDataSources(dataSources.default);
|
||||
setFolderResponse(mockFolder(folder));
|
||||
setFolderResponse(mockFolder(slashedFolder));
|
||||
});
|
||||
|
||||
// mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);
|
||||
mocks.searchFolders.mockResolvedValue([folder, slashedFolder] as DashboardSearchHit[]);
|
||||
it('can edit grafana managed rule', async () => {
|
||||
const { user } = renderRuleEditor(grafanaRulerRule.grafana_alert.uid);
|
||||
|
||||
// check that it's filled in
|
||||
@ -141,7 +119,36 @@ describe('RuleEditor grafana managed rules', () => {
|
||||
// save and check what was sent to backend
|
||||
await user.click(ui.buttons.save.get());
|
||||
|
||||
mocks.searchFolders.mockResolvedValue([] as DashboardSearchHit[]);
|
||||
expect(screen.getByText('New folder')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('saves evaluation interval correctly', async () => {
|
||||
const { user } = renderRuleEditor(grafanaRulerRule.grafana_alert.uid);
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: /new evaluation group/i }));
|
||||
await screen.findByRole('dialog');
|
||||
|
||||
await user.type(screen.getByLabelText(/evaluation group name/i), 'new group');
|
||||
const evalInterval = screen.getByLabelText(/^evaluation interval/i);
|
||||
|
||||
await user.clear(evalInterval);
|
||||
await user.type(evalInterval, '12m');
|
||||
await user.click(screen.getByRole('button', { name: /create/i }));
|
||||
|
||||
// Update the pending period as well, otherwise we'll get a form validation error
|
||||
// and the rule won't try and save
|
||||
await user.type(screen.getByLabelText(/pending period/i), '12m');
|
||||
|
||||
const capture = captureRequests(
|
||||
(req) => req.method === 'POST' && req.url.includes('/api/ruler/grafana/api/v1/rules/uuid020c61ef')
|
||||
);
|
||||
|
||||
await user.click(ui.buttons.save.get());
|
||||
|
||||
const [request] = await capture;
|
||||
const postBody = await request.json();
|
||||
|
||||
expect(postBody.name).toBe('new group');
|
||||
expect(postBody.interval).toBe('12m');
|
||||
});
|
||||
});
|
||||
|
@ -211,6 +211,7 @@ export function FolderAndGroup({
|
||||
className={styles.formInput}
|
||||
error={errors.group?.message}
|
||||
invalid={!!errors.group?.message}
|
||||
htmlFor="group"
|
||||
>
|
||||
<Controller
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
@ -356,6 +357,7 @@ function EvaluationGroupCreationModal({
|
||||
const { watch } = useFormContext<RuleFormValues>();
|
||||
|
||||
const evaluateEveryId = 'eval-every-input';
|
||||
const evaluationGroupNameId = 'new-eval-group-name';
|
||||
const [groupName, folderName] = watch(['group', 'folder.title']);
|
||||
|
||||
const groupRules =
|
||||
@ -392,8 +394,11 @@ function EvaluationGroupCreationModal({
|
||||
<form onSubmit={handleSubmit(() => onSubmit())}>
|
||||
<Field
|
||||
label={
|
||||
<Label htmlFor={'group'} description="A group evaluates all its rules over the same evaluation interval.">
|
||||
Evaluation group
|
||||
<Label
|
||||
htmlFor={evaluationGroupNameId}
|
||||
description="A group evaluates all its rules over the same evaluation interval."
|
||||
>
|
||||
Evaluation group name
|
||||
</Label>
|
||||
}
|
||||
error={formState.errors.group?.message}
|
||||
@ -403,7 +408,7 @@ function EvaluationGroupCreationModal({
|
||||
data-testid={selectors.components.AlertRules.newEvaluationGroupName}
|
||||
className={styles.formInput}
|
||||
autoFocus={true}
|
||||
id={'group'}
|
||||
id={evaluationGroupNameId}
|
||||
placeholder="Enter a name"
|
||||
{...register('group', { required: { value: true, message: 'Required.' } })}
|
||||
/>
|
||||
@ -411,29 +416,27 @@ function EvaluationGroupCreationModal({
|
||||
|
||||
<Field
|
||||
error={formState.errors.evaluateEvery?.message}
|
||||
invalid={Boolean(formState.errors.evaluateEvery) ? true : undefined}
|
||||
label={
|
||||
<Label htmlFor={evaluateEveryId} description="How often all rules in the group are evaluated.">
|
||||
Evaluation interval
|
||||
</Label>
|
||||
}
|
||||
invalid={Boolean(formState.errors.evaluateEvery)}
|
||||
>
|
||||
<Stack direction="column">
|
||||
<Input
|
||||
data-testid={selectors.components.AlertRules.newEvaluationGroupInterval}
|
||||
className={styles.formInput}
|
||||
id={evaluateEveryId}
|
||||
placeholder={DEFAULT_GROUP_EVALUATION_INTERVAL}
|
||||
{...register(
|
||||
'evaluateEvery',
|
||||
evaluateEveryValidationOptions<{ group: string; evaluateEvery: string }>(groupRules)
|
||||
)}
|
||||
/>
|
||||
<Stack direction="row" alignItems="flex-end">
|
||||
<EvaluationGroupQuickPick currentInterval={evaluationInterval} onSelect={setEvaluationInterval} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Input
|
||||
data-testid={selectors.components.AlertRules.newEvaluationGroupInterval}
|
||||
className={styles.formInput}
|
||||
id={evaluateEveryId}
|
||||
placeholder={DEFAULT_GROUP_EVALUATION_INTERVAL}
|
||||
{...register(
|
||||
'evaluateEvery',
|
||||
evaluateEveryValidationOptions<{ group: string; evaluateEvery: string }>(groupRules)
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<EvaluationGroupQuickPick currentInterval={evaluationInterval} onSelect={setEvaluationInterval} />
|
||||
|
||||
<Modal.ButtonRow>
|
||||
<Button variant="secondary" type="button" onClick={onCancel}>
|
||||
Cancel
|
||||
|
@ -115,7 +115,6 @@ function FolderGroupAndEvaluationInterval({
|
||||
const onOpenEditGroupModal = () => setIsEditingGroup(true);
|
||||
|
||||
const editGroupDisabled = groupfoldersForGrafana?.loading || isNewGroup || !folderUid || !groupName;
|
||||
|
||||
const emptyNamespace: CombinedRuleNamespace = {
|
||||
name: folderName,
|
||||
rulesSource: GRAFANA_RULES_SOURCE_NAME,
|
||||
@ -185,11 +184,11 @@ function ForInput({ evaluateEvery }: { evaluateEvery: string }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack direction="row" justify-content="flex-start" align-items="flex-start">
|
||||
<Stack direction="column" justify-content="flex-start" align-items="flex-start">
|
||||
<Field
|
||||
label={
|
||||
<Label
|
||||
htmlFor="evaluateFor"
|
||||
htmlFor={evaluateForId}
|
||||
description='Period the threshold condition must be met to trigger the alert. Selecting "None" triggers the alert immediately once the condition is met.'
|
||||
>
|
||||
<Trans i18nKey="alert-rule-form.evaluation-behaviour.pending-period">Pending period</Trans>
|
||||
@ -200,15 +199,13 @@ function ForInput({ evaluateEvery }: { evaluateEvery: string }) {
|
||||
invalid={Boolean(errors.evaluateFor?.message) ? true : undefined}
|
||||
validationMessageHorizontalOverflow={true}
|
||||
>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Input id={evaluateForId} width={8} {...register('evaluateFor', forValidationOptions(evaluateEvery))} />
|
||||
<PendingPeriodQuickPick
|
||||
selectedPendingPeriod={currentPendingPeriod}
|
||||
groupEvaluationInterval={evaluateEvery}
|
||||
onSelect={setPendingPeriod}
|
||||
/>
|
||||
</Stack>
|
||||
<Input id={evaluateForId} width={8} {...register('evaluateFor', forValidationOptions(evaluateEvery))} />
|
||||
</Field>
|
||||
<PendingPeriodQuickPick
|
||||
selectedPendingPeriod={currentPendingPeriod}
|
||||
groupEvaluationInterval={evaluateEvery}
|
||||
onSelect={setPendingPeriod}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
@ -147,21 +147,20 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
|
||||
? getRuleGroupLocationFromRuleWithLocation(existing)
|
||||
: getRuleGroupLocationFromFormValues(values);
|
||||
|
||||
// @TODO what is "evaluateEvery" being used for?
|
||||
// @TODO move this to a hook too to make sure the logic here is tested for regressions?
|
||||
if (!existing) {
|
||||
// when creating a new rule, we save the manual routing setting , and editorSettings.simplifiedQueryEditor to the local storage
|
||||
storeInLocalStorageValues(values);
|
||||
await addRuleToRuleGroup.execute(ruleGroupIdentifier, ruleDefinition, values.evaluateEvery);
|
||||
await addRuleToRuleGroup.execute(ruleGroupIdentifier, ruleDefinition, evaluateEvery);
|
||||
} else {
|
||||
const ruleIdentifier = fromRulerRuleAndRuleGroupIdentifier(ruleGroupIdentifier, existing.rule);
|
||||
const targetRuleGroupIdentifier = getRuleGroupLocationFromFormValues(values);
|
||||
|
||||
await updateRuleInRuleGroup.execute(
|
||||
ruleGroupIdentifier,
|
||||
ruleIdentifier,
|
||||
ruleDefinition,
|
||||
targetRuleGroupIdentifier
|
||||
targetRuleGroupIdentifier,
|
||||
evaluateEvery
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ import { DataSourceType, GRAFANA_DATASOURCE_NAME } from 'app/features/alerting/u
|
||||
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { grafanaRulerEmptyGroup } from '../../../../mocks/grafanaRulerApi';
|
||||
import { grafanaRulerGroup } from '../../../../mocks/grafanaRulerApi';
|
||||
import { setupDataSources } from '../../../../testSetup/datasources';
|
||||
|
||||
jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({
|
||||
@ -52,7 +52,7 @@ const selectFolderAndGroup = async () => {
|
||||
await clickSelectOption(folderInput, FOLDER_TITLE_HAPPY_PATH);
|
||||
const groupInput = await ui.inputs.group.find();
|
||||
await user.click(await byRole('combobox').find(groupInput));
|
||||
await clickSelectOption(groupInput, grafanaRulerEmptyGroup.name);
|
||||
await clickSelectOption(groupInput, grafanaRulerGroup.name);
|
||||
};
|
||||
|
||||
describe('Can create a new grafana managed alert using simplified routing', () => {
|
||||
|
@ -5,8 +5,48 @@ exports[`Can create a new grafana managed alert using simplified routing can cre
|
||||
{
|
||||
"body": {
|
||||
"interval": "1m",
|
||||
"name": "empty-group",
|
||||
"name": "grafana-group-1",
|
||||
"rules": [
|
||||
{
|
||||
"annotations": {
|
||||
"summary": "Test alert",
|
||||
},
|
||||
"for": "5m",
|
||||
"grafana_alert": {
|
||||
"condition": "A",
|
||||
"data": [
|
||||
{
|
||||
"datasourceUid": "datasource-uid",
|
||||
"model": {
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "datasource-uid",
|
||||
},
|
||||
"expression": "vector(1)",
|
||||
"queryType": "alerting",
|
||||
"refId": "A",
|
||||
},
|
||||
"queryType": "alerting",
|
||||
"refId": "A",
|
||||
"relativeTimeRange": {
|
||||
"from": 1000,
|
||||
"to": 2000,
|
||||
},
|
||||
},
|
||||
],
|
||||
"exec_err_state": "Error",
|
||||
"is_paused": false,
|
||||
"namespace_uid": "uuid020c61ef",
|
||||
"no_data_state": "NoData",
|
||||
"rule_group": "grafana-group-1",
|
||||
"title": "Grafana-rule",
|
||||
"uid": "4d7125fee983",
|
||||
},
|
||||
"labels": {
|
||||
"region": "nasa",
|
||||
"severity": "critical",
|
||||
},
|
||||
},
|
||||
{
|
||||
"annotations": {},
|
||||
"for": "1m",
|
||||
@ -110,7 +150,7 @@ exports[`Can create a new grafana managed alert using simplified routing can cre
|
||||
],
|
||||
],
|
||||
"method": "POST",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/6abdb25bc1eb?subtype=cortex",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef?subtype=cortex",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
@ -55,7 +55,8 @@ export function useUpdateRuleInRuleGroup() {
|
||||
ruleGroup: RuleGroupIdentifier,
|
||||
ruleIdentifier: EditableRuleIdentifier,
|
||||
ruleDefinition: PostableRuleDTO,
|
||||
targetRuleGroup?: RuleGroupIdentifier
|
||||
targetRuleGroup?: RuleGroupIdentifier,
|
||||
interval?: string
|
||||
) => {
|
||||
const { namespaceName } = ruleGroup;
|
||||
const finalRuleDefinition = copyGrafanaUID(ruleIdentifier, ruleDefinition);
|
||||
@ -63,7 +64,7 @@ export function useUpdateRuleInRuleGroup() {
|
||||
// check if the existing rule and the form values have the same rule group identifier
|
||||
const sameTargetRuleGroup = isEqual(ruleGroup, targetRuleGroup);
|
||||
if (targetRuleGroup && !sameTargetRuleGroup) {
|
||||
const result = moveRuleToGroup.execute(ruleGroup, targetRuleGroup, ruleIdentifier, ruleDefinition);
|
||||
const result = moveRuleToGroup.execute(ruleGroup, targetRuleGroup, ruleIdentifier, ruleDefinition, interval);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -96,12 +97,13 @@ export function useMoveRuleToRuleGroup() {
|
||||
currentRuleGroup: RuleGroupIdentifier,
|
||||
targetRuleGroup: RuleGroupIdentifier,
|
||||
ruleIdentifier: EditableRuleIdentifier,
|
||||
ruleDefinition: PostableRuleDTO
|
||||
ruleDefinition: PostableRuleDTO,
|
||||
interval?: string
|
||||
) => {
|
||||
const finalRuleDefinition = copyGrafanaUID(ruleIdentifier, ruleDefinition);
|
||||
|
||||
// 1. add the rule to the new namespace / group / ruler target
|
||||
const addRuleToGroup = addRuleAction({ rule: finalRuleDefinition });
|
||||
const addRuleToGroup = addRuleAction({ rule: finalRuleDefinition, interval });
|
||||
const { newRuleGroupDefinition: newTargetGroup, rulerConfig: targetGroupRulerConfig } = await produceNewRuleGroup(
|
||||
targetRuleGroup,
|
||||
addRuleToGroup
|
||||
|
@ -52,6 +52,14 @@ export const setFolderAccessControl = (accessControl: FolderDTO['accessControl']
|
||||
server.use(getFolderHandler(mockFolder({ hasAcl: true, accessControl })));
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes the mock server respond with different folder response, for just the folder in question
|
||||
*/
|
||||
export const setFolderResponse = (response: Partial<FolderDTO>) => {
|
||||
const handler = http.get<{ folderUid: string }>(`/api/folders/${response.uid}`, () => HttpResponse.json(response));
|
||||
server.use(handler);
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes the mock server respond with different Grafana Alertmanager config
|
||||
*/
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { HttpResponse, http } from 'msw';
|
||||
|
||||
import { grafanaRulerNamespace2 } from 'app/features/alerting/unified/mocks/grafanaRulerApi';
|
||||
import { grafanaRulerNamespace } from 'app/features/alerting/unified/mocks/grafanaRulerApi';
|
||||
import { DashboardSearchItemType } from 'app/features/search/types';
|
||||
|
||||
export const FOLDER_TITLE_HAPPY_PATH = 'Folder A';
|
||||
@ -10,7 +10,7 @@ export const FOLDER_TITLE_HAPPY_PATH = 'Folder A';
|
||||
const defaultSearchResponse = [
|
||||
{
|
||||
title: FOLDER_TITLE_HAPPY_PATH,
|
||||
uid: grafanaRulerNamespace2.uid,
|
||||
uid: grafanaRulerNamespace.uid,
|
||||
id: 1,
|
||||
type: DashboardSearchItemType.DashFolder,
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user