1import { getBackendSrv, locationService } from '@grafana/runtime';
2import { createAsyncThunk } from '@reduxjs/toolkit';
3import {
4  AlertmanagerAlert,
5  AlertManagerCortexConfig,
6  AlertmanagerGroup,
7  ExternalAlertmanagersResponse,
8  Receiver,
9  Silence,
10  SilenceCreatePayload,
11  TestReceiversAlert,
12} from 'app/plugins/datasource/alertmanager/types';
13import { FolderDTO, NotifierDTO, ThunkResult } from 'app/types';
14import { RuleIdentifier, RuleNamespace, RuleWithLocation, StateHistoryItem } from 'app/types/unified-alerting';
15import {
16  PostableRulerRuleGroupDTO,
17  RulerGrafanaRuleDTO,
18  RulerRuleGroupDTO,
19  RulerRulesConfigDTO,
20} from 'app/types/unified-alerting-dto';
21import { fetchNotifiers } from '../api/grafana';
22import { fetchAnnotations } from '../api/annotations';
23import {
24  expireSilence,
25  fetchAlertManagerConfig,
26  fetchAlerts,
27  fetchAlertGroups,
28  fetchSilences,
29  createOrUpdateSilence,
30  updateAlertManagerConfig,
31  fetchStatus,
32  deleteAlertManagerConfig,
33  testReceivers,
34  addAlertManagers,
35  fetchExternalAlertmanagers,
36  fetchExternalAlertmanagerConfig,
37} from '../api/alertmanager';
38import { FetchPromRulesFilter, fetchRules } from '../api/prometheus';
39import {
40  deleteNamespace,
41  deleteRulerRulesGroup,
42  fetchRulerRules,
43  fetchRulerRulesGroup,
44  fetchRulerRulesNamespace,
45  FetchRulerRulesFilter,
46  setRulerRuleGroup,
47} from '../api/ruler';
48import { RuleFormType, RuleFormValues } from '../types/rule-form';
49import {
50  getAllRulesSourceNames,
51  GRAFANA_RULES_SOURCE_NAME,
52  isGrafanaRulesSource,
53  isVanillaPrometheusAlertManagerDataSource,
54} from '../utils/datasource';
55import { makeAMLink, retryWhile } from '../utils/misc';
56import { withAppEvents, withSerializedError } from '../utils/redux';
57import { formValuesToRulerRuleDTO, formValuesToRulerGrafanaRuleDTO } from '../utils/rule-form';
58import {
59  isCloudRuleIdentifier,
60  isGrafanaRuleIdentifier,
61  isGrafanaRulerRule,
62  isPrometheusRuleIdentifier,
63  isRulerNotSupportedResponse,
64} from '../utils/rules';
65import { addDefaultsToAlertmanagerConfig, isFetchError } from '../utils/alertmanager';
66import * as ruleId from '../utils/rule-id';
67import { isEmpty } from 'lodash';
68import messageFromError from 'app/plugins/datasource/grafana-azure-monitor-datasource/utils/messageFromError';
69import { RULER_NOT_SUPPORTED_MSG } from '../utils/constants';
70
71const FETCH_CONFIG_RETRY_TIMEOUT = 30 * 1000;
72
73export const fetchPromRulesAction = createAsyncThunk(
74  'unifiedalerting/fetchPromRules',
75  ({ rulesSourceName, filter }: { rulesSourceName: string; filter?: FetchPromRulesFilter }): Promise<RuleNamespace[]> =>
76    withSerializedError(fetchRules(rulesSourceName, filter))
77);
78
79export const fetchAlertManagerConfigAction = createAsyncThunk(
80  'unifiedalerting/fetchAmConfig',
81  (alertManagerSourceName: string): Promise<AlertManagerCortexConfig> =>
82    withSerializedError(
83      (async () => {
84        // for vanilla prometheus, there is no config endpoint. Only fetch config from status
85        if (isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName)) {
86          return fetchStatus(alertManagerSourceName).then((status) => ({
87            alertmanager_config: status.config,
88            template_files: {},
89          }));
90        }
91
92        return retryWhile(
93          () => fetchAlertManagerConfig(alertManagerSourceName),
94          // if config has been recently deleted, it takes a while for cortex start returning the default one.
95          // retry for a short while instead of failing
96          (e) => !!messageFromError(e)?.includes('alertmanager storage object not found'),
97          FETCH_CONFIG_RETRY_TIMEOUT
98        ).then((result) => {
99          // if user config is empty for cortex alertmanager, try to get config from status endpoint
100          if (
101            isEmpty(result.alertmanager_config) &&
102            isEmpty(result.template_files) &&
103            alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME
104          ) {
105            return fetchStatus(alertManagerSourceName).then((status) => ({
106              alertmanager_config: status.config,
107              template_files: {},
108            }));
109          }
110          return result;
111        });
112      })()
113    )
114);
115
116export const fetchExternalAlertmanagersAction = createAsyncThunk(
117  'unifiedAlerting/fetchExternalAlertmanagers',
118  (): Promise<ExternalAlertmanagersResponse> => {
119    return withSerializedError(fetchExternalAlertmanagers());
120  }
121);
122
123export const fetchExternalAlertmanagersConfigAction = createAsyncThunk(
124  'unifiedAlerting/fetchExternAlertmanagersConfig',
125  (): Promise<{ alertmanagers: string[] }> => {
126    return withSerializedError(fetchExternalAlertmanagerConfig());
127  }
128);
129
130export const fetchRulerRulesAction = createAsyncThunk(
131  'unifiedalerting/fetchRulerRules',
132  ({
133    rulesSourceName,
134    filter,
135  }: {
136    rulesSourceName: string;
137    filter?: FetchRulerRulesFilter;
138  }): Promise<RulerRulesConfigDTO | null> => {
139    return withSerializedError(fetchRulerRules(rulesSourceName, filter));
140  }
141);
142
143export const fetchSilencesAction = createAsyncThunk(
144  'unifiedalerting/fetchSilences',
145  (alertManagerSourceName: string): Promise<Silence[]> => {
146    return withSerializedError(fetchSilences(alertManagerSourceName));
147  }
148);
149
150// this will only trigger ruler rules fetch if rules are not loaded yet and request is not in flight
151export function fetchRulerRulesIfNotFetchedYet(rulesSourceName: string): ThunkResult<void> {
152  return (dispatch, getStore) => {
153    const { rulerRules } = getStore().unifiedAlerting;
154    const resp = rulerRules[rulesSourceName];
155    if (!resp?.result && !(resp && isRulerNotSupportedResponse(resp)) && !resp?.loading) {
156      dispatch(fetchRulerRulesAction({ rulesSourceName }));
157    }
158  };
159}
160
161export function fetchAllPromAndRulerRulesAction(force = false): ThunkResult<void> {
162  return (dispatch, getStore) => {
163    const { promRules, rulerRules } = getStore().unifiedAlerting;
164    getAllRulesSourceNames().map((rulesSourceName) => {
165      if (force || !promRules[rulesSourceName]?.loading) {
166        dispatch(fetchPromRulesAction({ rulesSourceName }));
167      }
168      if (force || !rulerRules[rulesSourceName]?.loading) {
169        dispatch(fetchRulerRulesAction({ rulesSourceName }));
170      }
171    });
172  };
173}
174
175export function fetchAllPromRulesAction(force = false): ThunkResult<void> {
176  return (dispatch, getStore) => {
177    const { promRules } = getStore().unifiedAlerting;
178    getAllRulesSourceNames().map((rulesSourceName) => {
179      if (force || !promRules[rulesSourceName]?.loading) {
180        dispatch(fetchPromRulesAction({ rulesSourceName }));
181      }
182    });
183  };
184}
185
186async function findEditableRule(ruleIdentifier: RuleIdentifier): Promise<RuleWithLocation | null> {
187  if (isGrafanaRuleIdentifier(ruleIdentifier)) {
188    const namespaces = await fetchRulerRules(GRAFANA_RULES_SOURCE_NAME);
189    // find namespace and group that contains the uid for the rule
190    for (const [namespace, groups] of Object.entries(namespaces)) {
191      for (const group of groups) {
192        const rule = group.rules.find(
193          (rule) => isGrafanaRulerRule(rule) && rule.grafana_alert?.uid === ruleIdentifier.uid
194        );
195        if (rule) {
196          return {
197            group,
198            ruleSourceName: GRAFANA_RULES_SOURCE_NAME,
199            namespace: namespace,
200            rule,
201          };
202        }
203      }
204    }
205  }
206
207  if (isCloudRuleIdentifier(ruleIdentifier)) {
208    const { ruleSourceName, namespace, groupName } = ruleIdentifier;
209    const group = await fetchRulerRulesGroup(ruleSourceName, namespace, groupName);
210
211    if (!group) {
212      return null;
213    }
214
215    const rule = group.rules.find((rule) => {
216      const identifier = ruleId.fromRulerRule(ruleSourceName, namespace, group.name, rule);
217      return ruleId.equal(identifier, ruleIdentifier);
218    });
219
220    if (!rule) {
221      return null;
222    }
223
224    return {
225      group,
226      ruleSourceName,
227      namespace,
228      rule,
229    };
230  }
231
232  if (isPrometheusRuleIdentifier(ruleIdentifier)) {
233    throw new Error('Native prometheus rules can not be edited in grafana.');
234  }
235
236  return null;
237}
238
239export const fetchEditableRuleAction = createAsyncThunk(
240  'unifiedalerting/fetchEditableRule',
241  (ruleIdentifier: RuleIdentifier): Promise<RuleWithLocation | null> =>
242    withSerializedError(findEditableRule(ruleIdentifier))
243);
244
245async function deleteRule(ruleWithLocation: RuleWithLocation): Promise<void> {
246  const { ruleSourceName, namespace, group, rule } = ruleWithLocation;
247  // in case of GRAFANA, each group implicitly only has one rule. delete the group.
248  if (isGrafanaRulesSource(ruleSourceName)) {
249    await deleteRulerRulesGroup(GRAFANA_RULES_SOURCE_NAME, namespace, group.name);
250    return;
251  }
252  // in case of CLOUD
253  // it was the last rule, delete the entire group
254  if (group.rules.length === 1) {
255    await deleteRulerRulesGroup(ruleSourceName, namespace, group.name);
256    return;
257  }
258  // post the group with rule removed
259  await setRulerRuleGroup(ruleSourceName, namespace, {
260    ...group,
261    rules: group.rules.filter((r) => r !== rule),
262  });
263}
264
265export function deleteRuleAction(
266  ruleIdentifier: RuleIdentifier,
267  options: { navigateTo?: string } = {}
268): ThunkResult<void> {
269  /*
270   * fetch the rules group from backend, delete group if it is found and+
271   * reload ruler rules
272   */
273  return async (dispatch) => {
274    withAppEvents(
275      (async () => {
276        const ruleWithLocation = await findEditableRule(ruleIdentifier);
277        if (!ruleWithLocation) {
278          throw new Error('Rule not found.');
279        }
280        await deleteRule(ruleWithLocation);
281        // refetch rules for this rules source
282        dispatch(fetchRulerRulesAction({ rulesSourceName: ruleWithLocation.ruleSourceName }));
283        dispatch(fetchPromRulesAction({ rulesSourceName: ruleWithLocation.ruleSourceName }));
284
285        if (options.navigateTo) {
286          locationService.replace(options.navigateTo);
287        }
288      })(),
289      {
290        successMessage: 'Rule deleted.',
291      }
292    );
293  };
294}
295
296async function saveLotexRule(values: RuleFormValues, existing?: RuleWithLocation): Promise<RuleIdentifier> {
297  const { dataSourceName, group, namespace } = values;
298  const formRule = formValuesToRulerRuleDTO(values);
299  if (dataSourceName && group && namespace) {
300    // if we're updating a rule...
301    if (existing) {
302      // refetch it so we always have the latest greatest
303      const freshExisting = await findEditableRule(ruleId.fromRuleWithLocation(existing));
304      if (!freshExisting) {
305        throw new Error('Rule not found.');
306      }
307      // if namespace or group was changed, delete the old rule
308      if (freshExisting.namespace !== namespace || freshExisting.group.name !== group) {
309        await deleteRule(freshExisting);
310      } else {
311        // if same namespace or group, update the group replacing the old rule with new
312        const payload = {
313          ...freshExisting.group,
314          rules: freshExisting.group.rules.map((existingRule) =>
315            existingRule === freshExisting.rule ? formRule : existingRule
316          ),
317        };
318        await setRulerRuleGroup(dataSourceName, namespace, payload);
319        return ruleId.fromRulerRule(dataSourceName, namespace, group, formRule);
320      }
321    }
322
323    // if creating new rule or existing rule was in a different namespace/group, create new rule in target group
324
325    const targetGroup = await fetchRulerRulesGroup(dataSourceName, namespace, group);
326
327    const payload: RulerRuleGroupDTO = targetGroup
328      ? {
329          ...targetGroup,
330          rules: [...targetGroup.rules, formRule],
331        }
332      : {
333          name: group,
334          rules: [formRule],
335        };
336
337    await setRulerRuleGroup(dataSourceName, namespace, payload);
338    return ruleId.fromRulerRule(dataSourceName, namespace, group, formRule);
339  } else {
340    throw new Error('Data source and location must be specified');
341  }
342}
343
344async function saveGrafanaRule(values: RuleFormValues, existing?: RuleWithLocation): Promise<RuleIdentifier> {
345  const { folder, evaluateEvery } = values;
346  const formRule = formValuesToRulerGrafanaRuleDTO(values);
347
348  if (!folder) {
349    throw new Error('Folder must be specified');
350  }
351
352  // updating an existing rule...
353  if (existing) {
354    // refetch it to be sure we have the latest
355    const freshExisting = await findEditableRule(ruleId.fromRuleWithLocation(existing));
356    if (!freshExisting) {
357      throw new Error('Rule not found.');
358    }
359
360    // if same folder, repost the group with updated rule
361    if (freshExisting.namespace === folder.title) {
362      const uid = (freshExisting.rule as RulerGrafanaRuleDTO).grafana_alert.uid!;
363      formRule.grafana_alert.uid = uid;
364      await setRulerRuleGroup(GRAFANA_RULES_SOURCE_NAME, freshExisting.namespace, {
365        name: freshExisting.group.name,
366        interval: evaluateEvery,
367        rules: [formRule],
368      });
369      return { uid };
370    }
371  }
372
373  // if creating new rule or folder was changed, create rule in a new group
374  const targetFolderGroups = await fetchRulerRulesNamespace(GRAFANA_RULES_SOURCE_NAME, folder.title);
375
376  // set group name to rule name, but be super paranoid and check that this group does not already exist
377  const groupName = getUniqueGroupName(values.name, targetFolderGroups);
378  formRule.grafana_alert.title = groupName;
379
380  const payload: PostableRulerRuleGroupDTO = {
381    name: groupName,
382    interval: evaluateEvery,
383    rules: [formRule],
384  };
385  await setRulerRuleGroup(GRAFANA_RULES_SOURCE_NAME, folder.title, payload);
386
387  // now refetch this group to get the uid, hah
388  const result = await fetchRulerRulesGroup(GRAFANA_RULES_SOURCE_NAME, folder.title, groupName);
389  const newUid = (result?.rules[0] as RulerGrafanaRuleDTO)?.grafana_alert?.uid;
390  if (newUid) {
391    // if folder has changed, delete the old one
392    if (existing) {
393      const freshExisting = await findEditableRule(ruleId.fromRuleWithLocation(existing));
394      if (freshExisting && freshExisting.namespace !== folder.title) {
395        await deleteRule(freshExisting);
396      }
397    }
398
399    return { uid: newUid };
400  } else {
401    throw new Error('Failed to fetch created rule.');
402  }
403}
404
405export function getUniqueGroupName(currentGroupName: string, existingGroups: RulerRuleGroupDTO[]) {
406  let newGroupName = currentGroupName;
407  let idx = 1;
408  while (!!existingGroups.find((g) => g.name === newGroupName)) {
409    newGroupName = `${currentGroupName}-${++idx}`;
410  }
411
412  return newGroupName;
413}
414
415export const saveRuleFormAction = createAsyncThunk(
416  'unifiedalerting/saveRuleForm',
417  ({
418    values,
419    existing,
420    redirectOnSave,
421  }: {
422    values: RuleFormValues;
423    existing?: RuleWithLocation;
424    redirectOnSave?: string;
425  }): Promise<void> =>
426    withAppEvents(
427      withSerializedError(
428        (async () => {
429          const { type } = values;
430          // in case of system (cortex/loki)
431          let identifier: RuleIdentifier;
432          if (type === RuleFormType.cloudAlerting || type === RuleFormType.cloudRecording) {
433            identifier = await saveLotexRule(values, existing);
434            // in case of grafana managed
435          } else if (type === RuleFormType.grafana) {
436            identifier = await saveGrafanaRule(values, existing);
437          } else {
438            throw new Error('Unexpected rule form type');
439          }
440          if (redirectOnSave) {
441            locationService.push(redirectOnSave);
442          } else {
443            // redirect to edit page
444            const newLocation = `/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`;
445            if (locationService.getLocation().pathname !== newLocation) {
446              locationService.replace(newLocation);
447            }
448          }
449        })()
450      ),
451      {
452        successMessage: existing ? `Rule "${values.name}" updated.` : `Rule "${values.name}" saved.`,
453        errorMessage: 'Failed to save rule',
454      }
455    )
456);
457
458export const fetchGrafanaNotifiersAction = createAsyncThunk(
459  'unifiedalerting/fetchGrafanaNotifiers',
460  (): Promise<NotifierDTO[]> => withSerializedError(fetchNotifiers())
461);
462
463export const fetchGrafanaAnnotationsAction = createAsyncThunk(
464  'unifiedalerting/fetchGrafanaAnnotations',
465  (alertId: string): Promise<StateHistoryItem[]> => withSerializedError(fetchAnnotations(alertId))
466);
467
468interface UpdateAlertManagerConfigActionOptions {
469  alertManagerSourceName: string;
470  oldConfig: AlertManagerCortexConfig; // it will be checked to make sure it didn't change in the meanwhile
471  newConfig: AlertManagerCortexConfig;
472  successMessage?: string; // show toast on success
473  redirectPath?: string; // where to redirect on success
474  refetch?: boolean; // refetch config on success
475}
476
477export const updateAlertManagerConfigAction = createAsyncThunk<void, UpdateAlertManagerConfigActionOptions, {}>(
478  'unifiedalerting/updateAMConfig',
479  ({ alertManagerSourceName, oldConfig, newConfig, successMessage, redirectPath, refetch }, thunkAPI): Promise<void> =>
480    withAppEvents(
481      withSerializedError(
482        (async () => {
483          const latestConfig = await fetchAlertManagerConfig(alertManagerSourceName);
484          if (
485            !(isEmpty(latestConfig.alertmanager_config) && isEmpty(latestConfig.template_files)) &&
486            JSON.stringify(latestConfig) !== JSON.stringify(oldConfig)
487          ) {
488            throw new Error(
489              'It seems configuration has been recently updated. Please reload page and try again to make sure that recent changes are not overwritten.'
490            );
491          }
492          await updateAlertManagerConfig(alertManagerSourceName, addDefaultsToAlertmanagerConfig(newConfig));
493          if (refetch) {
494            await thunkAPI.dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
495          }
496          if (redirectPath) {
497            locationService.push(makeAMLink(redirectPath, alertManagerSourceName));
498          }
499        })()
500      ),
501      {
502        successMessage,
503      }
504    )
505);
506
507export const fetchAmAlertsAction = createAsyncThunk(
508  'unifiedalerting/fetchAmAlerts',
509  (alertManagerSourceName: string): Promise<AlertmanagerAlert[]> =>
510    withSerializedError(fetchAlerts(alertManagerSourceName, [], true, true, true))
511);
512
513export const expireSilenceAction = (alertManagerSourceName: string, silenceId: string): ThunkResult<void> => {
514  return async (dispatch) => {
515    await withAppEvents(expireSilence(alertManagerSourceName, silenceId), {
516      successMessage: 'Silence expired.',
517    });
518    dispatch(fetchSilencesAction(alertManagerSourceName));
519    dispatch(fetchAmAlertsAction(alertManagerSourceName));
520  };
521};
522
523type UpdateSilenceActionOptions = {
524  alertManagerSourceName: string;
525  payload: SilenceCreatePayload;
526  exitOnSave: boolean;
527  successMessage?: string;
528};
529
530export const createOrUpdateSilenceAction = createAsyncThunk<void, UpdateSilenceActionOptions, {}>(
531  'unifiedalerting/updateSilence',
532  ({ alertManagerSourceName, payload, exitOnSave, successMessage }): Promise<void> =>
533    withAppEvents(
534      withSerializedError(
535        (async () => {
536          await createOrUpdateSilence(alertManagerSourceName, payload);
537          if (exitOnSave) {
538            locationService.push('/alerting/silences');
539          }
540        })()
541      ),
542      {
543        successMessage,
544      }
545    )
546);
547
548export const deleteReceiverAction = (receiverName: string, alertManagerSourceName: string): ThunkResult<void> => {
549  return (dispatch, getState) => {
550    const config = getState().unifiedAlerting.amConfigs?.[alertManagerSourceName]?.result;
551    if (!config) {
552      throw new Error(`Config for ${alertManagerSourceName} not found`);
553    }
554    if (!config.alertmanager_config.receivers?.find((receiver) => receiver.name === receiverName)) {
555      throw new Error(`Cannot delete receiver ${receiverName}: not found in config.`);
556    }
557    const newConfig: AlertManagerCortexConfig = {
558      ...config,
559      alertmanager_config: {
560        ...config.alertmanager_config,
561        receivers: config.alertmanager_config.receivers.filter((receiver) => receiver.name !== receiverName),
562      },
563    };
564    return dispatch(
565      updateAlertManagerConfigAction({
566        newConfig,
567        oldConfig: config,
568        alertManagerSourceName,
569        successMessage: 'Contact point deleted.',
570        refetch: true,
571      })
572    );
573  };
574};
575
576export const deleteTemplateAction = (templateName: string, alertManagerSourceName: string): ThunkResult<void> => {
577  return (dispatch, getState) => {
578    const config = getState().unifiedAlerting.amConfigs?.[alertManagerSourceName]?.result;
579    if (!config) {
580      throw new Error(`Config for ${alertManagerSourceName} not found`);
581    }
582    if (typeof config.template_files?.[templateName] !== 'string') {
583      throw new Error(`Cannot delete template ${templateName}: not found in config.`);
584    }
585    const newTemplates = { ...config.template_files };
586    delete newTemplates[templateName];
587    const newConfig: AlertManagerCortexConfig = {
588      ...config,
589      alertmanager_config: {
590        ...config.alertmanager_config,
591        templates: config.alertmanager_config.templates?.filter((existing) => existing !== templateName),
592      },
593      template_files: newTemplates,
594    };
595    return dispatch(
596      updateAlertManagerConfigAction({
597        newConfig,
598        oldConfig: config,
599        alertManagerSourceName,
600        successMessage: 'Template deleted.',
601        refetch: true,
602      })
603    );
604  };
605};
606
607export const fetchFolderAction = createAsyncThunk(
608  'unifiedalerting/fetchFolder',
609  (uid: string): Promise<FolderDTO> => withSerializedError((getBackendSrv() as any).getFolderByUid(uid))
610);
611
612export const fetchFolderIfNotFetchedAction = (uid: string): ThunkResult<void> => {
613  return (dispatch, getState) => {
614    if (!getState().unifiedAlerting.folders[uid]?.dispatched) {
615      dispatch(fetchFolderAction(uid));
616    }
617  };
618};
619
620export const fetchAlertGroupsAction = createAsyncThunk(
621  'unifiedalerting/fetchAlertGroups',
622  (alertManagerSourceName: string): Promise<AlertmanagerGroup[]> => {
623    return withSerializedError(fetchAlertGroups(alertManagerSourceName));
624  }
625);
626
627export const checkIfLotexSupportsEditingRulesAction = createAsyncThunk<boolean, string>(
628  'unifiedalerting/checkIfLotexRuleEditingSupported',
629  async (rulesSourceName: string): Promise<boolean> =>
630    withAppEvents(
631      (async () => {
632        try {
633          await fetchRulerRulesGroup(rulesSourceName, 'test', 'test');
634          return true;
635        } catch (e) {
636          if (
637            (isFetchError(e) &&
638              (e.data.message?.includes('GetRuleGroup unsupported in rule local store') || // "local" rule storage
639                e.data.message?.includes('page not found'))) || // ruler api disabled
640            e.message?.includes('404 from rules config endpoint') || // ruler api disabled
641            e.data.message?.includes(RULER_NOT_SUPPORTED_MSG) // ruler api not supported
642          ) {
643            return false;
644          }
645          throw e;
646        }
647      })(),
648      {
649        errorMessage: `Failed to determine if "${rulesSourceName}" allows editing rules`,
650      }
651    )
652);
653
654export const deleteAlertManagerConfigAction = createAsyncThunk(
655  'unifiedalerting/deleteAlertManagerConfig',
656  async (alertManagerSourceName: string, thunkAPI): Promise<void> => {
657    return withAppEvents(
658      withSerializedError(
659        (async () => {
660          await deleteAlertManagerConfig(alertManagerSourceName);
661          await thunkAPI.dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
662        })()
663      ),
664      {
665        errorMessage: 'Failed to reset Alertmanager configuration',
666        successMessage: 'Alertmanager configuration reset.',
667      }
668    );
669  }
670);
671
672interface TestReceiversOptions {
673  alertManagerSourceName: string;
674  receivers: Receiver[];
675  alert?: TestReceiversAlert;
676}
677
678export const testReceiversAction = createAsyncThunk(
679  'unifiedalerting/testReceivers',
680  ({ alertManagerSourceName, receivers, alert }: TestReceiversOptions): Promise<void> => {
681    return withAppEvents(withSerializedError(testReceivers(alertManagerSourceName, receivers, alert)), {
682      errorMessage: 'Failed to send test alert.',
683      successMessage: 'Test alert sent.',
684    });
685  }
686);
687
688interface UpdateNamespaceAndGroupOptions {
689  rulesSourceName: string;
690  namespaceName: string;
691  groupName: string;
692  newNamespaceName: string;
693  newGroupName: string;
694  groupInterval?: string;
695}
696
697// allows renaming namespace, renaming group and changing group interval, all in one go
698export const updateLotexNamespaceAndGroupAction = createAsyncThunk(
699  'unifiedalerting/updateLotexNamespaceAndGroup',
700  async (options: UpdateNamespaceAndGroupOptions, thunkAPI): Promise<void> => {
701    return withAppEvents(
702      withSerializedError(
703        (async () => {
704          const { rulesSourceName, namespaceName, groupName, newNamespaceName, newGroupName, groupInterval } = options;
705          if (options.rulesSourceName === GRAFANA_RULES_SOURCE_NAME) {
706            throw new Error(`this action does not support Grafana rules`);
707          }
708          // fetch rules and perform sanity checks
709          const rulesResult = await fetchRulerRules(rulesSourceName);
710          if (!rulesResult[namespaceName]) {
711            throw new Error(`Namespace "${namespaceName}" not found.`);
712          }
713          const existingGroup = rulesResult[namespaceName].find((group) => group.name === groupName);
714          if (!existingGroup) {
715            throw new Error(`Group "${groupName}" not found.`);
716          }
717          if (newGroupName !== groupName && !!rulesResult[namespaceName].find((group) => group.name === newGroupName)) {
718            throw new Error(`Group "${newGroupName}" already exists.`);
719          }
720          if (newNamespaceName !== namespaceName && !!rulesResult[newNamespaceName]) {
721            throw new Error(`Namespace "${newNamespaceName}" already exists.`);
722          }
723          if (
724            newNamespaceName === namespaceName &&
725            groupName === newGroupName &&
726            groupInterval === existingGroup.interval
727          ) {
728            throw new Error('Nothing changed.');
729          }
730
731          // if renaming namespace - make new copies of all groups, then delete old namespace
732          if (newNamespaceName !== namespaceName) {
733            for (const group of rulesResult[namespaceName]) {
734              await setRulerRuleGroup(
735                rulesSourceName,
736                newNamespaceName,
737                group.name === groupName
738                  ? {
739                      ...group,
740                      name: newGroupName,
741                      interval: groupInterval,
742                    }
743                  : group
744              );
745            }
746            await deleteNamespace(rulesSourceName, namespaceName);
747
748            // if only modifying group...
749          } else {
750            // save updated group
751            await setRulerRuleGroup(rulesSourceName, namespaceName, {
752              ...existingGroup,
753              name: newGroupName,
754              interval: groupInterval,
755            });
756            // if group name was changed, delete old group
757            if (newGroupName !== groupName) {
758              await deleteRulerRulesGroup(rulesSourceName, namespaceName, groupName);
759            }
760          }
761
762          // refetch all rules
763          await thunkAPI.dispatch(fetchRulerRulesAction({ rulesSourceName }));
764        })()
765      ),
766      {
767        errorMessage: 'Failed to update namespace / group',
768        successMessage: 'Update successful',
769      }
770    );
771  }
772);
773
774export const addExternalAlertmanagersAction = createAsyncThunk(
775  'unifiedAlerting/addExternalAlertmanagers',
776  async (alertManagerUrls: string[], thunkAPI): Promise<void> => {
777    return withAppEvents(
778      withSerializedError(
779        (async () => {
780          await addAlertManagers(alertManagerUrls);
781          thunkAPI.dispatch(fetchExternalAlertmanagersConfigAction());
782        })()
783      ),
784      {
785        errorMessage: 'Failed adding alertmanagers',
786        successMessage: 'Alertmanagers updated',
787      }
788    );
789  }
790);
791