2025-04-01 10:38:02 +09:00

152 lines
5.3 KiB
TypeScript

import { createAction, createReducer, isAnyOf } from '@reduxjs/toolkit';
import { inRange } from 'lodash';
import { EditableRuleIdentifier, GrafanaRuleIdentifier, RuleIdentifier } from 'app/types/unified-alerting';
import { PostableRuleDTO, PostableRulerRuleGroupDTO } from 'app/types/unified-alerting-dto';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { hashRulerRule } from '../../utils/rule-id';
import {
isCloudRuleIdentifier,
isCloudRulerRule,
isGrafanaRuleIdentifier,
isGrafanaRulerRule,
} from '../../utils/rules';
// rule-scoped actions
export const addRuleAction = createAction<{ rule: PostableRuleDTO; groupName?: string; interval?: string }>(
'ruleGroup/rules/add'
);
export const updateRuleAction = createAction<{ identifier: EditableRuleIdentifier; rule: PostableRuleDTO }>(
'ruleGroup/rules/update'
);
export const pauseRuleAction = createAction<{ uid: string; pause: boolean }>('ruleGroup/rules/pause');
export const deleteRuleAction = createAction<{ identifier: EditableRuleIdentifier }>('ruleGroup/rules/delete');
// group-scoped actions
export const updateRuleGroupAction = createAction<{ interval?: string }>('ruleGroup/update');
export const moveRuleGroupAction = createAction<{ newNamespaceName: string; groupName?: string; interval?: string }>(
'ruleGroup/move'
);
export const renameRuleGroupAction = createAction<{ groupName: string; interval?: string }>('ruleGroup/rename');
export const reorderRulesInRuleGroupAction = createAction<{ swaps: SwapOperation[] }>('ruleGroup/rules/reorder');
const initialState: PostableRulerRuleGroupDTO = {
name: 'initial',
rules: [],
};
export const ruleGroupReducer = createReducer(initialState, (builder) => {
builder
.addCase(addRuleAction, (draft, { payload }) => {
const { rule } = payload;
draft.rules.push(rule);
})
.addCase(updateRuleAction, (draft, { payload }) => {
const { identifier, rule } = payload;
const index = findRuleIndex(draft.rules, identifier);
draft.rules[index] = rule;
})
.addCase(deleteRuleAction, (draft, { payload }) => {
const { identifier } = payload;
const index = findRuleIndex(draft.rules, identifier);
draft.rules.splice(index, 1);
})
.addCase(pauseRuleAction, (draft, { payload }) => {
const { uid, pause } = payload;
const identifier: GrafanaRuleIdentifier = { ruleSourceName: GRAFANA_RULES_SOURCE_NAME, uid };
const index = findRuleIndex(draft.rules, identifier);
const matchingRule = draft.rules[index];
if (isGrafanaRulerRule(matchingRule)) {
matchingRule.grafana_alert.is_paused = pause;
} else {
throw new Error('Matching rule is not a Grafana-managed rule');
}
})
.addCase(reorderRulesInRuleGroupAction, (draft, { payload }) => {
const { swaps } = payload;
reorder(draft.rules, swaps);
})
// rename and move should allow updating the group's name
.addMatcher(isAnyOf(renameRuleGroupAction, moveRuleGroupAction, addRuleAction), (draft, { payload }) => {
const { groupName } = payload;
draft.name = groupName ?? draft.name;
})
// update, rename and move should all allow updating the interval of the group
.addMatcher(
isAnyOf(updateRuleGroupAction, renameRuleGroupAction, moveRuleGroupAction, addRuleAction),
(draft, { payload }) => {
const { interval } = payload;
draft.interval = interval ?? draft.interval;
}
)
.addDefaultCase((_draft, action) => {
throw new Error(`Unknown action for rule group reducer: ${action.type}`);
});
});
/**
* Utility function for finding rules matching a identifier, pass this into .find, .findIndex, .remove
* or any other function with matching predicate.
*/
const ruleFinder = (identifier: RuleIdentifier) => {
const grafanaManagedIdentifier = isGrafanaRuleIdentifier(identifier);
const dataSourceManagedIdentifier = isCloudRuleIdentifier(identifier);
return (rule: PostableRuleDTO) => {
const isGrafanaManagedRule = isGrafanaRulerRule(rule);
const isDataSourceManagedRule = isCloudRulerRule(rule);
if (grafanaManagedIdentifier && isGrafanaManagedRule) {
return rule.grafana_alert.uid === identifier.uid;
}
if (isDataSourceManagedRule && dataSourceManagedIdentifier) {
return hashRulerRule(rule) === identifier.rulerRuleHash;
}
return;
};
};
// [oldIndex, newIndex]
export type SwapOperation = [number, number];
/**
* ⚠️ This function mutates the input array
* reorder several items in a list, given a set of swap
*/
export function reorder<T>(items: T[], swaps: Array<[number, number]>) {
for (const swap of swaps) {
swapItems(items, swap);
}
return items;
}
/**
* ⚠️ This function mutates the input array
* swaps two items in an array of items
*/
export function swapItems<T>(items: T[], [oldIndex, newIndex]: SwapOperation): void {
const inBounds = inRange(oldIndex, items.length) && inRange(newIndex, items.length);
if (!inBounds) {
throw new Error('Invalid operation: index out of bounds');
}
const [movedItem] = items.splice(oldIndex, 1);
items.splice(newIndex, 0, movedItem);
}
function findRuleIndex(rules: PostableRuleDTO[], identifier: EditableRuleIdentifier) {
const index = rules.findIndex(ruleFinder(identifier));
if (index === -1) {
throw new Error('no rule matching identifier found');
}
return index;
}