mirror of https://github.com/grafana/grafana.git
Alerting: Remove ruler from alert list view2 (#106778)
* wip * Add working actions for GMA rules based on Prom-only API * Remove Ruler-loader related code for Grafana rules Co-authored-by: Sonia Augilar <sonia.aguilar@grafana.com> * Remove outdated tests * add some comments * remove commented code * remove showLocation property * Add missing mocks in tests * Add showLocation to GrafanaRuleListItem, improve useAbilities, address PR feedback * Enhance GrafanaGroupLoader tests: Add permission checks and More button functionality - Introduced user permission grants for alerting actions in tests. - Added tests for rendering the More button with action menu options. - Verified that each rule has its own action buttons and handles permissions correctly. - Ensured the edit button is not rendered when user lacks edit permissions. - Confirmed the correct menu actions are displayed when the More button is clicked. * Update translations --------- Co-authored-by: Sonia Aguilar <soniaaguilarpeiron@gmail.com> Co-authored-by: Sonia Augilar <sonia.aguilar@grafana.com>
This commit is contained in:
parent
acdb0e151c
commit
3e6d620d2c
|
@ -10,7 +10,12 @@ import { useRulePluginLinkExtension } from 'app/features/alerting/unified/plugin
|
|||
import { Rule, RuleGroupIdentifierV2, RuleIdentifier } from 'app/types/unified-alerting';
|
||||
import { PromAlertingRuleState, RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { AlertRuleAction, useRulerRuleAbility } from '../../hooks/useAbilities';
|
||||
import {
|
||||
AlertRuleAction,
|
||||
skipToken,
|
||||
useGrafanaPromRuleAbilities,
|
||||
useRulerRuleAbilities,
|
||||
} from '../../hooks/useAbilities';
|
||||
import { createShareLink, isLocalDevEnv, isOpenSourceEdition } from '../../utils/misc';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { prometheusRuleType, rulerRuleType } from '../../utils/rules';
|
||||
|
@ -33,6 +38,8 @@ interface Props {
|
|||
/**
|
||||
* Get a list of menu items + divider elements for rendering in an alert rule's
|
||||
* dropdown menu
|
||||
* If the consumer of this component comes from the alert list view, we need to use promRule to check abilities and permissions,
|
||||
* as we have removed all requests to the ruler API in the list view.
|
||||
*/
|
||||
const AlertRuleMenu = ({
|
||||
promRule,
|
||||
|
@ -46,29 +53,51 @@ const AlertRuleMenu = ({
|
|||
buttonSize,
|
||||
fill,
|
||||
}: Props) => {
|
||||
// check all abilities and permissions
|
||||
const [pauseSupported, pauseAllowed] = useRulerRuleAbility(rulerRule, groupIdentifier, AlertRuleAction.Pause);
|
||||
const canPause = pauseSupported && pauseAllowed;
|
||||
// check all abilities and permissions using rulerRule
|
||||
const [rulerPauseAbility, rulerDeleteAbility, rulerDuplicateAbility, rulerSilenceAbility, rulerExportAbility] =
|
||||
useRulerRuleAbilities(rulerRule, groupIdentifier, [
|
||||
AlertRuleAction.Pause,
|
||||
AlertRuleAction.Delete,
|
||||
AlertRuleAction.Duplicate,
|
||||
AlertRuleAction.Silence,
|
||||
AlertRuleAction.ModifyExport,
|
||||
]);
|
||||
|
||||
const [deleteSupported, deleteAllowed] = useRulerRuleAbility(rulerRule, groupIdentifier, AlertRuleAction.Delete);
|
||||
const canDelete = deleteSupported && deleteAllowed;
|
||||
// check all abilities and permissions using promRule
|
||||
const [
|
||||
grafanaPauseAbility,
|
||||
grafanaDeleteAbility,
|
||||
grafanaDuplicateAbility,
|
||||
grafanaSilenceAbility,
|
||||
grafanaExportAbility,
|
||||
] = useGrafanaPromRuleAbilities(prometheusRuleType.grafana.rule(promRule) ? promRule : skipToken, [
|
||||
AlertRuleAction.Pause,
|
||||
AlertRuleAction.Delete,
|
||||
AlertRuleAction.Duplicate,
|
||||
AlertRuleAction.Silence,
|
||||
AlertRuleAction.ModifyExport,
|
||||
]);
|
||||
|
||||
const [duplicateSupported, duplicateAllowed] = useRulerRuleAbility(
|
||||
rulerRule,
|
||||
groupIdentifier,
|
||||
AlertRuleAction.Duplicate
|
||||
);
|
||||
const canDuplicate = duplicateSupported && duplicateAllowed;
|
||||
const [pauseSupported, pauseAllowed] = rulerPauseAbility;
|
||||
const [grafanaPauseSupported, grafanaPauseAllowed] = grafanaPauseAbility;
|
||||
const canPause = (pauseSupported && pauseAllowed) || (grafanaPauseSupported && grafanaPauseAllowed);
|
||||
|
||||
const [silenceSupported, silenceAllowed] = useRulerRuleAbility(rulerRule, groupIdentifier, AlertRuleAction.Silence);
|
||||
const canSilence = silenceSupported && silenceAllowed;
|
||||
const [deleteSupported, deleteAllowed] = rulerDeleteAbility;
|
||||
const [grafanaDeleteSupported, grafanaDeleteAllowed] = grafanaDeleteAbility;
|
||||
const canDelete = (deleteSupported && deleteAllowed) || (grafanaDeleteSupported && grafanaDeleteAllowed);
|
||||
|
||||
const [exportSupported, exportAllowed] = useRulerRuleAbility(
|
||||
rulerRule,
|
||||
groupIdentifier,
|
||||
AlertRuleAction.ModifyExport
|
||||
);
|
||||
const canExport = exportSupported && exportAllowed;
|
||||
const [duplicateSupported, duplicateAllowed] = rulerDuplicateAbility;
|
||||
const [grafanaDuplicateSupported, grafanaDuplicateAllowed] = grafanaDuplicateAbility;
|
||||
const canDuplicate =
|
||||
(duplicateSupported && duplicateAllowed) || (grafanaDuplicateSupported && grafanaDuplicateAllowed);
|
||||
|
||||
const [silenceSupported, silenceAllowed] = rulerSilenceAbility;
|
||||
const [grafanaSilenceSupported, grafanaSilenceAllowed] = grafanaSilenceAbility;
|
||||
const canSilence = (silenceSupported && silenceAllowed) || (grafanaSilenceSupported && grafanaSilenceAllowed);
|
||||
|
||||
const [exportSupported, exportAllowed] = rulerExportAbility;
|
||||
const [grafanaExportSupported, grafanaExportAllowed] = grafanaExportAbility;
|
||||
const canExport = (exportSupported && exportAllowed) || (grafanaExportSupported && grafanaExportAllowed);
|
||||
|
||||
const ruleExtensionLinks = useRulePluginLinkExtension(promRule, groupIdentifier);
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
|||
|
||||
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
|
||||
import { getCloudRule, getGrafanaRule } from '../../mocks';
|
||||
import { mimirDataSource } from '../../mocks/server/configure';
|
||||
|
||||
import { RuleDetails } from './RuleDetails';
|
||||
|
||||
|
@ -32,6 +33,8 @@ const ui = {
|
|||
|
||||
setupMswServer();
|
||||
|
||||
const { dataSource: mimirDs } = mimirDataSource();
|
||||
|
||||
beforeAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
@ -81,7 +84,7 @@ describe('RuleDetails RBAC', () => {
|
|||
});
|
||||
|
||||
describe('Cloud rules action buttons', () => {
|
||||
const cloudRule = getCloudRule({ name: 'Cloud' });
|
||||
const cloudRule = getCloudRule({ name: 'Cloud' }, { rulesSource: mimirDs });
|
||||
|
||||
it('Should not render Edit button for users with the update permission', async () => {
|
||||
// Arrange
|
||||
|
|
|
@ -4,7 +4,14 @@ import { byRole } from 'testing-library-selector';
|
|||
import { setPluginLinksHook } from '@grafana/runtime';
|
||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
|
||||
import { AlertRuleAction, useAlertRuleAbility, useRulerRuleAbility } from '../../hooks/useAbilities';
|
||||
import {
|
||||
AlertRuleAction,
|
||||
useAlertRuleAbility,
|
||||
useGrafanaPromRuleAbilities,
|
||||
useGrafanaPromRuleAbility,
|
||||
useRulerRuleAbilities,
|
||||
useRulerRuleAbility,
|
||||
} from '../../hooks/useAbilities';
|
||||
import { getCloudRule, getGrafanaRule } from '../../mocks';
|
||||
import { mimirDataSource } from '../../mocks/server/configure';
|
||||
|
||||
|
@ -13,11 +20,15 @@ import { RulesTable } from './RulesTable';
|
|||
jest.mock('../../hooks/useAbilities');
|
||||
|
||||
const mocks = {
|
||||
// This is a bit unfortunate, but we need to mock both abilities
|
||||
// RuleActionButtons still needs to use the useAlertRuleAbility hook
|
||||
// whereas AlertRuleMenu has already been refactored to use useRulerRuleAbility
|
||||
// Mock the hooks that are actually used by the components:
|
||||
// RuleActionsButtons uses: useAlertRuleAbility (singular)
|
||||
// AlertRuleMenu uses: useRulerRuleAbilities and useGrafanaPromRuleAbilities (plural)
|
||||
// We can also use useGrafanaPromRuleAbility (singular) for simpler mocking
|
||||
useRulerRuleAbility: jest.mocked(useRulerRuleAbility),
|
||||
useAlertRuleAbility: jest.mocked(useAlertRuleAbility),
|
||||
useGrafanaPromRuleAbility: jest.mocked(useGrafanaPromRuleAbility),
|
||||
useRulerRuleAbilities: jest.mocked(useRulerRuleAbilities),
|
||||
useGrafanaPromRuleAbilities: jest.mocked(useGrafanaPromRuleAbilities),
|
||||
};
|
||||
|
||||
setPluginLinksHook(() => ({
|
||||
|
@ -46,18 +57,40 @@ describe('RulesTable RBAC', () => {
|
|||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
jest.resetAllMocks();
|
||||
|
||||
// Set up default neutral mocks for all hooks
|
||||
// Singular hooks (used by RuleActionsButtons and can simplify mocking)
|
||||
mocks.useAlertRuleAbility.mockReturnValue([false, false]);
|
||||
mocks.useRulerRuleAbility.mockReturnValue([false, false]);
|
||||
mocks.useGrafanaPromRuleAbility.mockReturnValue([false, false]);
|
||||
|
||||
// Plural hooks (used by AlertRuleMenu) - need to return arrays based on input actions
|
||||
mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => {
|
||||
return actions.map(() => [false, false]);
|
||||
});
|
||||
mocks.useGrafanaPromRuleAbilities.mockImplementation((_rule, actions) => {
|
||||
return actions.map(() => [false, false]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Grafana rules action buttons', () => {
|
||||
const grafanaRule = getGrafanaRule({ name: 'Grafana' });
|
||||
|
||||
it('Should not render Edit button for users without the update permission', async () => {
|
||||
mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => {
|
||||
// Mock the specific hooks needed for Grafana rules
|
||||
// Using singular hook for simpler mocking
|
||||
mocks.useAlertRuleAbility.mockImplementation((rule, action) => {
|
||||
return action === AlertRuleAction.Update ? [true, false] : [true, true];
|
||||
});
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
mocks.useGrafanaPromRuleAbility.mockImplementation((rule, action) => {
|
||||
return action === AlertRuleAction.Update ? [true, false] : [true, true];
|
||||
});
|
||||
// Still need plural hook for AlertRuleMenu component
|
||||
mocks.useGrafanaPromRuleAbilities.mockImplementation((rule, actions) => {
|
||||
return actions.map((action) => {
|
||||
return action === AlertRuleAction.Update ? [true, false] : [true, true];
|
||||
});
|
||||
});
|
||||
|
||||
render(<RulesTable rules={[grafanaRule]} />);
|
||||
|
||||
|
@ -65,11 +98,14 @@ describe('RulesTable RBAC', () => {
|
|||
});
|
||||
|
||||
it('Should not render Delete button for users without the delete permission', async () => {
|
||||
mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => {
|
||||
// Mock the specific hooks needed for Grafana rules
|
||||
mocks.useAlertRuleAbility.mockImplementation((rule, action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, false] : [true, true];
|
||||
});
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, false] : [true, true];
|
||||
mocks.useGrafanaPromRuleAbilities.mockImplementation((rule, actions) => {
|
||||
return actions.map((action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, false] : [true, true];
|
||||
});
|
||||
});
|
||||
|
||||
render(<RulesTable rules={[grafanaRule]} />);
|
||||
|
@ -80,11 +116,14 @@ describe('RulesTable RBAC', () => {
|
|||
});
|
||||
|
||||
it('Should render Edit button for users with the update permission', async () => {
|
||||
mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => {
|
||||
// Mock the specific hooks needed for Grafana rules
|
||||
mocks.useAlertRuleAbility.mockImplementation((rule, action) => {
|
||||
return action === AlertRuleAction.Update ? [true, true] : [false, false];
|
||||
});
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Update ? [true, true] : [false, false];
|
||||
mocks.useGrafanaPromRuleAbilities.mockImplementation((rule, actions) => {
|
||||
return actions.map((action) => {
|
||||
return action === AlertRuleAction.Update ? [true, true] : [false, false];
|
||||
});
|
||||
});
|
||||
|
||||
render(<RulesTable rules={[grafanaRule]} />);
|
||||
|
@ -93,11 +132,14 @@ describe('RulesTable RBAC', () => {
|
|||
});
|
||||
|
||||
it('Should render Delete button for users with the delete permission', async () => {
|
||||
mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => {
|
||||
// Mock the specific hooks needed for Grafana rules
|
||||
mocks.useAlertRuleAbility.mockImplementation((rule, action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
|
||||
});
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
|
||||
mocks.useGrafanaPromRuleAbilities.mockImplementation((rule, actions) => {
|
||||
return actions.map((action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
|
||||
});
|
||||
});
|
||||
|
||||
render(<RulesTable rules={[grafanaRule]} />);
|
||||
|
@ -123,11 +165,15 @@ describe('RulesTable RBAC', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.useRulerRuleAbility.mockImplementation(() => {
|
||||
return [true, true];
|
||||
// Mock all hooks needed for the creating/deleting state tests
|
||||
mocks.useRulerRuleAbility.mockImplementation(() => [true, true]);
|
||||
mocks.useAlertRuleAbility.mockImplementation(() => [true, true]);
|
||||
// Mock plural hooks for AlertRuleMenu
|
||||
mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => {
|
||||
return actions.map(() => [true, true]);
|
||||
});
|
||||
mocks.useAlertRuleAbility.mockImplementation(() => {
|
||||
return [true, true];
|
||||
mocks.useGrafanaPromRuleAbilities.mockImplementation((_rule, actions) => {
|
||||
return actions.map(() => [true, true]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -164,6 +210,12 @@ describe('RulesTable RBAC', () => {
|
|||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Update ? [true, false] : [true, true];
|
||||
});
|
||||
// Cloud rules only need useRulerRuleAbilities mock (useGrafanaPromRuleAbilities gets skipToken)
|
||||
mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => {
|
||||
return actions.map((action) => {
|
||||
return action === AlertRuleAction.Update ? [true, false] : [true, true];
|
||||
});
|
||||
});
|
||||
|
||||
render(<RulesTable rules={[cloudRule]} />);
|
||||
|
||||
|
@ -177,6 +229,12 @@ describe('RulesTable RBAC', () => {
|
|||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, false] : [true, true];
|
||||
});
|
||||
// Cloud rules only need useRulerRuleAbilities mock (useGrafanaPromRuleAbilities gets skipToken)
|
||||
mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => {
|
||||
return actions.map((action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, false] : [true, true];
|
||||
});
|
||||
});
|
||||
|
||||
render(<RulesTable rules={[cloudRule]} />);
|
||||
|
||||
|
@ -191,6 +249,12 @@ describe('RulesTable RBAC', () => {
|
|||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Update ? [true, true] : [false, false];
|
||||
});
|
||||
// Cloud rules only need useRulerRuleAbilities mock (useGrafanaPromRuleAbilities gets skipToken)
|
||||
mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => {
|
||||
return actions.map((action) => {
|
||||
return action === AlertRuleAction.Update ? [true, true] : [false, false];
|
||||
});
|
||||
});
|
||||
|
||||
render(<RulesTable rules={[cloudRule]} />);
|
||||
|
||||
|
@ -204,6 +268,12 @@ describe('RulesTable RBAC', () => {
|
|||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
|
||||
});
|
||||
// Cloud rules only need useRulerRuleAbilities mock (useGrafanaPromRuleAbilities gets skipToken)
|
||||
mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => {
|
||||
return actions.map((action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
|
||||
});
|
||||
});
|
||||
|
||||
render(<RulesTable rules={[cloudRule]} />);
|
||||
|
||||
|
|
|
@ -14,15 +14,20 @@ import { useFolder } from 'app/features/alerting/unified/hooks/useFolder';
|
|||
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { CombinedRule, RuleGroupIdentifierV2 } from 'app/types/unified-alerting';
|
||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
import { GrafanaPromRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { alertmanagerApi } from '../api/alertmanagerApi';
|
||||
import { useAlertmanager } from '../state/AlertmanagerContext';
|
||||
import { getInstancesPermissions, getNotificationsPermissions, getRulesPermissions } from '../utils/access-control';
|
||||
import { getRulesSourceName } from '../utils/datasource';
|
||||
import { getGroupOriginName } from '../utils/groupIdentifier';
|
||||
import { getGroupOriginName, groupIdentifier } from '../utils/groupIdentifier';
|
||||
import { isAdmin } from '../utils/misc';
|
||||
import { isFederatedRuleGroup, isPluginProvidedRule, rulerRuleType } from '../utils/rules';
|
||||
import {
|
||||
isPluginProvidedRule,
|
||||
isProvisionedPromRule,
|
||||
isProvisionedRule,
|
||||
prometheusRuleType,
|
||||
rulerRuleType,
|
||||
} from '../utils/rules';
|
||||
|
||||
import { useIsRuleEditable } from './useIsRuleEditable';
|
||||
|
||||
|
@ -200,7 +205,7 @@ export function useRulerRuleAbility(
|
|||
}
|
||||
|
||||
export function useRulerRuleAbilities(
|
||||
rule: RulerRuleDTO,
|
||||
rule: RulerRuleDTO | undefined,
|
||||
groupIdentifier: RuleGroupIdentifierV2,
|
||||
actions: AlertRuleAction[]
|
||||
): Ability[] {
|
||||
|
@ -211,61 +216,16 @@ export function useRulerRuleAbilities(
|
|||
}, [abilities, actions]);
|
||||
}
|
||||
|
||||
// This hook is being called a lot in different places
|
||||
// In some cases multiple times for ~80 rules (e.g. on the list page)
|
||||
// We need to investigate further if some of these calls are redundant
|
||||
// In the meantime, memoizing the result helps
|
||||
/**
|
||||
* @deprecated Use {@link useAllRulerRuleAbilities} instead
|
||||
*/
|
||||
export function useAllAlertRuleAbilities(rule: CombinedRule): Abilities<AlertRuleAction> {
|
||||
const rulesSourceName = getRulesSourceName(rule.namespace.rulesSource);
|
||||
|
||||
const {
|
||||
isEditable,
|
||||
isRemovable,
|
||||
isRulerAvailable = false,
|
||||
loading,
|
||||
} = useIsRuleEditable(rulesSourceName, rule.rulerRule);
|
||||
const [_, exportAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules);
|
||||
const canSilence = useCanSilence(rule.rulerRule);
|
||||
|
||||
const abilities = useMemo<Abilities<AlertRuleAction>>(() => {
|
||||
const isProvisioned =
|
||||
rulerRuleType.grafana.rule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
|
||||
const isFederated = isFederatedRuleGroup(rule.group);
|
||||
const isGrafanaManagedAlertRule = rulerRuleType.grafana.rule(rule.rulerRule);
|
||||
const isPluginProvided = isPluginProvidedRule(rule.rulerRule);
|
||||
|
||||
// if a rule is either provisioned, federated or provided by a plugin rule, we don't allow it to be removed or edited
|
||||
const immutableRule = isProvisioned || isFederated || isPluginProvided;
|
||||
|
||||
// while we gather info, pretend it's not supported
|
||||
const MaybeSupported = loading ? NotSupported : isRulerAvailable;
|
||||
const MaybeSupportedUnlessImmutable = immutableRule ? NotSupported : MaybeSupported;
|
||||
|
||||
// Creating duplicates of plugin-provided rules does not seem to make a lot of sense
|
||||
const duplicateSupported = isPluginProvided ? NotSupported : MaybeSupported;
|
||||
|
||||
const rulesPermissions = getRulesPermissions(rulesSourceName);
|
||||
|
||||
const abilities: Abilities<AlertRuleAction> = {
|
||||
[AlertRuleAction.Duplicate]: toAbility(duplicateSupported, rulesPermissions.create),
|
||||
[AlertRuleAction.View]: toAbility(AlwaysSupported, rulesPermissions.read),
|
||||
[AlertRuleAction.Update]: [MaybeSupportedUnlessImmutable, isEditable ?? false],
|
||||
[AlertRuleAction.Delete]: [MaybeSupportedUnlessImmutable, isRemovable ?? false],
|
||||
[AlertRuleAction.Explore]: toAbility(AlwaysSupported, AccessControlAction.DataSourcesExplore),
|
||||
[AlertRuleAction.Silence]: canSilence,
|
||||
[AlertRuleAction.ModifyExport]: [isGrafanaManagedAlertRule, exportAllowed],
|
||||
[AlertRuleAction.Pause]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false],
|
||||
[AlertRuleAction.Restore]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false],
|
||||
[AlertRuleAction.DeletePermanently]: [
|
||||
MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule,
|
||||
(isRemovable && isAdmin()) ?? false,
|
||||
],
|
||||
};
|
||||
|
||||
return abilities;
|
||||
}, [rule, loading, isRulerAvailable, isEditable, isRemovable, rulesSourceName, exportAllowed, canSilence]);
|
||||
|
||||
return abilities;
|
||||
// This hook is being called a lot in different places
|
||||
// In some cases multiple times for ~80 rules (e.g. on the list page)
|
||||
// We need to investigate further if some of these calls are redundant
|
||||
// In the meantime, memoizing the result helps
|
||||
const groupIdentifierV2 = useMemo(() => groupIdentifier.fromCombinedRule(rule), [rule]);
|
||||
return useAllRulerRuleAbilities(rule.rulerRule, groupIdentifierV2);
|
||||
}
|
||||
|
||||
export function useAllRulerRuleAbilities(
|
||||
|
@ -279,7 +239,8 @@ export function useAllRulerRuleAbilities(
|
|||
const canSilence = useCanSilence(rule);
|
||||
|
||||
const abilities = useMemo<Abilities<AlertRuleAction>>(() => {
|
||||
const isProvisioned = rulerRuleType.grafana.rule(rule) && Boolean(rule.grafana_alert.provenance);
|
||||
const isProvisioned = rule ? isProvisionedRule(rule) : false;
|
||||
// TODO: Add support for federated rules
|
||||
// const isFederated = isFederatedRuleGroup();
|
||||
const isFederated = false;
|
||||
const isGrafanaManagedAlertRule = rulerRuleType.grafana.rule(rule);
|
||||
|
@ -319,6 +280,131 @@ export function useAllRulerRuleAbilities(
|
|||
return abilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for checking abilities on Grafana Prometheus rules (GrafanaPromRuleDTO)
|
||||
* This is the next version of useAllRulerRuleAbilities designed to work with GrafanaPromRuleDTO
|
||||
*/
|
||||
export function useAllGrafanaPromRuleAbilities(rule: GrafanaPromRuleDTO | undefined): Abilities<AlertRuleAction> {
|
||||
// For GrafanaPromRuleDTO, we use useIsGrafanaPromRuleEditable instead
|
||||
const { isEditable, isRemovable, loading } = useIsGrafanaPromRuleEditable(rule); // duplicate
|
||||
const [_, exportAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules);
|
||||
|
||||
const silenceSupported = useGrafanaRulesSilenceSupport();
|
||||
const canSilenceInFolder = useCanSilenceInFolder(rule?.folderUid);
|
||||
|
||||
const abilities = useMemo<Abilities<AlertRuleAction>>(() => {
|
||||
const isProvisioned = rule ? isProvisionedPromRule(rule) : false;
|
||||
|
||||
// Note: Grafana managed rules can't be federated - this is strictly a Mimir feature
|
||||
// See: https://grafana.com/docs/mimir/latest/references/architecture/components/ruler/#federated-rule-groups
|
||||
const isFederated = false;
|
||||
// All GrafanaPromRuleDTO rules are Grafana-managed by definition
|
||||
const isAlertingRule = prometheusRuleType.grafana.alertingRule(rule);
|
||||
const isPluginProvided = isPluginProvidedRule(rule);
|
||||
|
||||
// if a rule is either provisioned, federated or provided by a plugin rule, we don't allow it to be removed or edited
|
||||
const immutableRule = isProvisioned || isFederated || isPluginProvided;
|
||||
|
||||
// GrafanaPromRuleDTO rules are always supported (no loading state for ruler availability)
|
||||
const MaybeSupported = loading ? NotSupported : AlwaysSupported;
|
||||
const MaybeSupportedUnlessImmutable = immutableRule ? NotSupported : MaybeSupported;
|
||||
|
||||
// Creating duplicates of plugin-provided rules does not seem to make a lot of sense
|
||||
const duplicateSupported = isPluginProvided ? NotSupported : MaybeSupported;
|
||||
|
||||
const rulesPermissions = getRulesPermissions('grafana');
|
||||
|
||||
const abilities: Abilities<AlertRuleAction> = {
|
||||
[AlertRuleAction.Duplicate]: toAbility(duplicateSupported, rulesPermissions.create),
|
||||
[AlertRuleAction.View]: toAbility(AlwaysSupported, rulesPermissions.read),
|
||||
[AlertRuleAction.Update]: [MaybeSupportedUnlessImmutable, isEditable ?? false],
|
||||
[AlertRuleAction.Delete]: [MaybeSupportedUnlessImmutable, isRemovable ?? false],
|
||||
[AlertRuleAction.Explore]: toAbility(AlwaysSupported, AccessControlAction.DataSourcesExplore),
|
||||
[AlertRuleAction.Silence]: [silenceSupported, canSilenceInFolder && isAlertingRule],
|
||||
[AlertRuleAction.ModifyExport]: [isAlertingRule, exportAllowed],
|
||||
[AlertRuleAction.Pause]: [MaybeSupportedUnlessImmutable && isAlertingRule, isEditable ?? false],
|
||||
[AlertRuleAction.Restore]: [MaybeSupportedUnlessImmutable && isAlertingRule, isEditable ?? false],
|
||||
[AlertRuleAction.DeletePermanently]: [
|
||||
MaybeSupportedUnlessImmutable && isAlertingRule,
|
||||
(isRemovable && isAdmin()) ?? false,
|
||||
],
|
||||
};
|
||||
|
||||
return abilities;
|
||||
}, [rule, loading, isEditable, isRemovable, canSilenceInFolder, exportAllowed, silenceSupported]);
|
||||
|
||||
return abilities;
|
||||
}
|
||||
|
||||
interface IsGrafanaPromRuleEditableResult {
|
||||
isEditable: boolean;
|
||||
isRemovable: boolean;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for checking if a GrafanaPromRuleDTO is editable
|
||||
* Adapted version of useIsRuleEditable for GrafanaPromRuleDTO
|
||||
*/
|
||||
function useIsGrafanaPromRuleEditable(rule?: GrafanaPromRuleDTO): IsGrafanaPromRuleEditableResult {
|
||||
const folderUID = rule?.folderUid;
|
||||
const { folder, loading } = useFolder(folderUID);
|
||||
|
||||
return useMemo(() => {
|
||||
if (!rule || !folderUID) {
|
||||
return { isEditable: false, isRemovable: false, loading: false };
|
||||
}
|
||||
|
||||
if (!folder) {
|
||||
// Loading or invalid folder UID
|
||||
return {
|
||||
isEditable: false,
|
||||
isRemovable: false,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
||||
// For Grafana-managed rules, check folder permissions
|
||||
const rulesPermissions = getRulesPermissions('grafana');
|
||||
const canEditGrafanaRules = ctx.hasPermissionInMetadata(rulesPermissions.update, folder);
|
||||
const canRemoveGrafanaRules = ctx.hasPermissionInMetadata(rulesPermissions.delete, folder);
|
||||
|
||||
return {
|
||||
isEditable: canEditGrafanaRules,
|
||||
isRemovable: canRemoveGrafanaRules,
|
||||
loading,
|
||||
};
|
||||
}, [rule, folderUID, folder, loading]);
|
||||
}
|
||||
|
||||
export const skipToken = Symbol('ability-skip-token');
|
||||
type SkipToken = typeof skipToken;
|
||||
|
||||
/**
|
||||
* Hook for checking a single ability on a GrafanaPromRuleDTO
|
||||
*/
|
||||
export function useGrafanaPromRuleAbility(rule: GrafanaPromRuleDTO | SkipToken, action: AlertRuleAction): Ability {
|
||||
const abilities = useAllGrafanaPromRuleAbilities(rule === skipToken ? undefined : rule);
|
||||
|
||||
return useMemo(() => {
|
||||
return abilities[action];
|
||||
}, [abilities, action]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for checking multiple abilities on a GrafanaPromRuleDTO
|
||||
*/
|
||||
export function useGrafanaPromRuleAbilities(
|
||||
rule: GrafanaPromRuleDTO | SkipToken,
|
||||
actions: AlertRuleAction[]
|
||||
): Ability[] {
|
||||
const abilities = useAllGrafanaPromRuleAbilities(rule === skipToken ? undefined : rule);
|
||||
|
||||
return useMemo(() => {
|
||||
return actions.map((action) => abilities[action]);
|
||||
}, [abilities, actions]);
|
||||
}
|
||||
|
||||
export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
|
||||
const {
|
||||
selectedAlertmanager,
|
||||
|
|
|
@ -12,7 +12,7 @@ import { hashRule } from '../utils/rule-id';
|
|||
|
||||
import { DataSourceRuleLoader } from './DataSourceRuleLoader';
|
||||
import { FilterProgressState, FilterStatus } from './FilterViewStatus';
|
||||
import { GrafanaRuleLoader } from './GrafanaRuleLoader';
|
||||
import { GrafanaRuleListItem } from './GrafanaRuleListItem';
|
||||
import LoadMoreHelper from './LoadMoreHelper';
|
||||
import { UnknownRuleListItem } from './components/AlertRuleListItem';
|
||||
import { AlertRuleListItemSkeleton } from './components/AlertRuleListItemLoader';
|
||||
|
@ -154,11 +154,11 @@ function FilterViewResults({ filterState }: FilterViewProps) {
|
|||
switch (origin) {
|
||||
case 'grafana':
|
||||
return (
|
||||
<GrafanaRuleLoader
|
||||
key={key}
|
||||
ruleIdentifier={{ ruleSourceName: 'grafana', uid: rule.uid }}
|
||||
<GrafanaRuleListItem
|
||||
rule={rule}
|
||||
groupIdentifier={groupIdentifier}
|
||||
namespaceName={ruleWithOrigin.namespaceName}
|
||||
showLocation={true}
|
||||
/>
|
||||
);
|
||||
case 'datasource':
|
||||
|
|
|
@ -2,6 +2,7 @@ import { render } from 'test/test-utils';
|
|||
import { byRole, byTitle } from 'testing-library-selector';
|
||||
|
||||
import { setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
import {
|
||||
GrafanaPromRuleDTO,
|
||||
|
@ -13,13 +14,13 @@ import {
|
|||
} from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { setupMswServer } from '../mockApi';
|
||||
import { mockGrafanaPromAlertingRule, mockGrafanaRulerRule } from '../mocks';
|
||||
import { grantUserPermissions } from '../mocks';
|
||||
import { grafanaRulerGroup, grafanaRulerNamespace } from '../mocks/grafanaRulerApi';
|
||||
import { setGrafanaPromRules } from '../mocks/server/configure';
|
||||
import { setFolderAccessControl, setGrafanaPromRules } from '../mocks/server/configure';
|
||||
import { rulerRuleType } from '../utils/rules';
|
||||
import { intervalToSeconds } from '../utils/time';
|
||||
|
||||
import { GrafanaGroupLoader, matchRules } from './GrafanaGroupLoader';
|
||||
import { GrafanaGroupLoader } from './GrafanaGroupLoader';
|
||||
|
||||
setPluginLinksHook(() => ({ links: [], isLoading: false }));
|
||||
setPluginComponentsHook(() => ({ components: [], isLoading: false }));
|
||||
|
@ -32,9 +33,35 @@ const ui = {
|
|||
ruleLink: (ruleName: string) => byRole('link', { name: ruleName }),
|
||||
editButton: () => byRole('link', { name: 'Edit' }),
|
||||
moreButton: () => byRole('button', { name: 'More' }),
|
||||
// Menu items that appear when More button is clicked
|
||||
menuItems: {
|
||||
silence: () => byRole('menuitem', { name: /silence/i }),
|
||||
duplicate: () => byRole('menuitem', { name: /duplicate/i }),
|
||||
copyLink: () => byRole('menuitem', { name: /copy link/i }),
|
||||
export: () => byRole('menuitem', { name: /export/i }),
|
||||
delete: () => byRole('menuitem', { name: /delete/i }),
|
||||
},
|
||||
};
|
||||
|
||||
describe('GrafanaGroupLoader', () => {
|
||||
beforeEach(() => {
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingRuleUpdate,
|
||||
AccessControlAction.AlertingRuleDelete,
|
||||
AccessControlAction.AlertingSilenceCreate,
|
||||
AccessControlAction.AlertingRuleCreate,
|
||||
AccessControlAction.AlertingRuleRead,
|
||||
]);
|
||||
// Grant necessary permissions for editing rules
|
||||
setFolderAccessControl({
|
||||
[AccessControlAction.AlertingRuleUpdate]: true,
|
||||
[AccessControlAction.AlertingRuleDelete]: true,
|
||||
[AccessControlAction.AlertingSilenceCreate]: true,
|
||||
[AccessControlAction.AlertingRuleCreate]: true, // For duplicate action
|
||||
[AccessControlAction.AlertingRuleRead]: true, // For export action
|
||||
});
|
||||
});
|
||||
|
||||
it('should render rule with url when ruler and prom rule exist', async () => {
|
||||
setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]);
|
||||
|
||||
|
@ -55,8 +82,8 @@ describe('GrafanaGroupLoader', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should render rule with url and creating state when only ruler rule exists', async () => {
|
||||
setGrafanaPromRules([]);
|
||||
it('should render More button with action menu options', async () => {
|
||||
setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]);
|
||||
|
||||
const groupIdentifier = getGroupIdentifier(grafanaRulerGroup);
|
||||
|
||||
|
@ -65,92 +92,119 @@ describe('GrafanaGroupLoader', () => {
|
|||
const [rule1] = grafanaRulerGroup.rules;
|
||||
const ruleListItem = await ui.ruleItem(rule1.grafana_alert.title).find();
|
||||
|
||||
const creatingIcon = ui.ruleStatus('Creating').get(ruleListItem);
|
||||
expect(creatingIcon).toBeInTheDocument();
|
||||
// Check that More button is present
|
||||
const moreButton = ui.moreButton().get(ruleListItem);
|
||||
expect(moreButton).toBeInTheDocument();
|
||||
|
||||
const ruleLink = ui.ruleLink(rule1.grafana_alert.title).get(ruleListItem);
|
||||
expect(ruleLink).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringContaining(`/alerting/grafana/${rule1.grafana_alert.uid}/view`)
|
||||
);
|
||||
// Verify More button accessibility
|
||||
expect(moreButton).toHaveAttribute('aria-label', 'More');
|
||||
expect(moreButton).toHaveTextContent('More');
|
||||
});
|
||||
|
||||
it('should render delete rule operation list item when only prom rule exists', async () => {
|
||||
const promOnlyGroup: GrafanaPromRuleGroupDTO = {
|
||||
...rulerGroupToPromGroup(grafanaRulerGroup),
|
||||
name: 'prom-only-group',
|
||||
it('should render multiple rules with their own action buttons', async () => {
|
||||
// Create a group with multiple rules
|
||||
const multiRuleGroup = {
|
||||
...grafanaRulerGroup,
|
||||
rules: [
|
||||
grafanaRulerGroup.rules[0],
|
||||
{
|
||||
...grafanaRulerGroup.rules[0],
|
||||
grafana_alert: {
|
||||
...grafanaRulerGroup.rules[0].grafana_alert,
|
||||
uid: 'second-rule-uid',
|
||||
title: 'Second Rule',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
setGrafanaPromRules([promOnlyGroup]);
|
||||
setGrafanaPromRules([rulerGroupToPromGroup(multiRuleGroup)]);
|
||||
|
||||
const groupIdentifier = getGroupIdentifier(promOnlyGroup);
|
||||
const groupIdentifier = getGroupIdentifier(multiRuleGroup);
|
||||
|
||||
render(<GrafanaGroupLoader groupIdentifier={groupIdentifier} namespaceName={grafanaRulerNamespace.name} />);
|
||||
|
||||
const [rule1] = promOnlyGroup.rules;
|
||||
const promRule = await ui.ruleItem(rule1.name).find();
|
||||
// Check first rule
|
||||
const [rule1, rule2] = multiRuleGroup.rules;
|
||||
const ruleListItem1 = await ui.ruleItem(rule1.grafana_alert.title).find();
|
||||
const ruleListItem2 = await ui.ruleItem(rule2.grafana_alert.title).find();
|
||||
|
||||
const deletingIcon = ui.ruleStatus('Deleting').get(promRule);
|
||||
expect(deletingIcon).toBeInTheDocument();
|
||||
// Each rule should have its own More button
|
||||
expect(ui.moreButton().get(ruleListItem1)).toBeInTheDocument();
|
||||
expect(ui.moreButton().get(ruleListItem2)).toBeInTheDocument();
|
||||
|
||||
expect(ui.editButton().query(promRule)).not.toBeInTheDocument();
|
||||
expect(ui.moreButton().query(promRule)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
// Check that edit buttons are present and have correct URLs
|
||||
const editButton1 = ui.editButton().get(ruleListItem1);
|
||||
const editButton2 = ui.editButton().get(ruleListItem2);
|
||||
|
||||
describe('matchRules', () => {
|
||||
it('should return matches for all items and have empty promOnlyRules if all rules are matched by uid', () => {
|
||||
const rulerRules = [
|
||||
mockGrafanaRulerRule({ uid: '1' }),
|
||||
mockGrafanaRulerRule({ uid: '2' }),
|
||||
mockGrafanaRulerRule({ uid: '3' }),
|
||||
];
|
||||
expect(editButton1).toBeInTheDocument();
|
||||
expect(editButton2).toBeInTheDocument();
|
||||
|
||||
const promRules = rulerRules.map(rulerRuleToPromRule);
|
||||
|
||||
const { matches, promOnlyRules } = matchRules(promRules, rulerRules);
|
||||
|
||||
expect(matches.size).toBe(rulerRules.length);
|
||||
expect(promOnlyRules).toHaveLength(0);
|
||||
|
||||
for (const [rulerRule, promRule] of matches) {
|
||||
expect(rulerRule.grafana_alert.uid).toBe(promRule.uid);
|
||||
}
|
||||
// Check that edit buttons have correct URLs (the actual format is simpler)
|
||||
expect(editButton1).toHaveAttribute('href', expect.stringContaining(`/alerting/${rule1.grafana_alert.uid}/edit`));
|
||||
expect(editButton2).toHaveAttribute('href', expect.stringContaining(`/alerting/${rule2.grafana_alert.uid}/edit`));
|
||||
});
|
||||
|
||||
it('should return unmatched prometheus rules in promOnlyRules array', () => {
|
||||
const rulerRules = [mockGrafanaRulerRule({ uid: '1' }), mockGrafanaRulerRule({ uid: '2' })];
|
||||
it('should not render edit button when user lacks edit permissions', async () => {
|
||||
// Override permissions to deny editing
|
||||
setFolderAccessControl({
|
||||
[AccessControlAction.AlertingRuleUpdate]: false,
|
||||
[AccessControlAction.AlertingRuleDelete]: false,
|
||||
});
|
||||
|
||||
const matchingPromRules = rulerRules.map(rulerRuleToPromRule);
|
||||
const unmatchedPromRules = [mockGrafanaPromAlertingRule({ uid: '3' }), mockGrafanaPromAlertingRule({ uid: '4' })];
|
||||
setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]);
|
||||
|
||||
const allPromRules = [...matchingPromRules, ...unmatchedPromRules];
|
||||
const { matches, promOnlyRules } = matchRules(allPromRules, rulerRules);
|
||||
const groupIdentifier = getGroupIdentifier(grafanaRulerGroup);
|
||||
|
||||
expect(matches.size).toBe(rulerRules.length);
|
||||
expect(promOnlyRules).toHaveLength(unmatchedPromRules.length);
|
||||
expect(promOnlyRules).toEqual(expect.arrayContaining(unmatchedPromRules));
|
||||
render(<GrafanaGroupLoader groupIdentifier={groupIdentifier} namespaceName={grafanaRulerNamespace.name} />);
|
||||
|
||||
const [rule1] = grafanaRulerGroup.rules;
|
||||
const ruleListItem = await ui.ruleItem(rule1.grafana_alert.title).find();
|
||||
|
||||
// Edit button should not be present
|
||||
expect(ui.editButton().query(ruleListItem)).not.toBeInTheDocument();
|
||||
|
||||
// More button should still be present (for other actions like viewing)
|
||||
expect(ui.moreButton().get(ruleListItem)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not include ruler rules in matches if they have no prometheus counterpart', () => {
|
||||
const rulerRules = [
|
||||
mockGrafanaRulerRule({ uid: '1' }),
|
||||
mockGrafanaRulerRule({ uid: '2' }),
|
||||
mockGrafanaRulerRule({ uid: '3' }),
|
||||
];
|
||||
it('should render correct menu actions when More button is clicked', async () => {
|
||||
setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]);
|
||||
|
||||
// Only create prom rule for the second ruler rule
|
||||
const promRules = [rulerRuleToPromRule(rulerRules[1])];
|
||||
const groupIdentifier = getGroupIdentifier(grafanaRulerGroup);
|
||||
|
||||
const { matches, promOnlyRules } = matchRules(promRules, rulerRules);
|
||||
const { user } = render(
|
||||
<GrafanaGroupLoader groupIdentifier={groupIdentifier} namespaceName={grafanaRulerNamespace.name} />
|
||||
);
|
||||
|
||||
expect(matches.size).toBe(1);
|
||||
expect(promOnlyRules).toHaveLength(0);
|
||||
const [rule1] = grafanaRulerGroup.rules;
|
||||
const ruleListItem = await ui.ruleItem(rule1.grafana_alert.title).find();
|
||||
|
||||
// Verify that only the second ruler rule is in matches
|
||||
expect(matches.has(rulerRules[0])).toBe(false);
|
||||
expect(matches.get(rulerRules[1])).toBe(promRules[0]);
|
||||
expect(matches.has(rulerRules[2])).toBe(false);
|
||||
// Find and click the More button
|
||||
const moreButton = ui.moreButton().get(ruleListItem);
|
||||
await user.click(moreButton);
|
||||
|
||||
// Check that the dropdown menu appears
|
||||
const menu = byRole('menu').get();
|
||||
expect(menu).toBeInTheDocument();
|
||||
|
||||
// With proper permissions, all 4 menu actions should be available:
|
||||
|
||||
// 1. Silence notifications - available for alerting rules (AlertingSilenceCreate permission)
|
||||
expect(ui.menuItems.silence().get()).toBeInTheDocument();
|
||||
|
||||
// 2. Copy link - always available
|
||||
expect(ui.menuItems.copyLink().get()).toBeInTheDocument();
|
||||
|
||||
// 3. Duplicate - should be available with create permissions (AlertingRuleCreate permission)
|
||||
expect(ui.menuItems.duplicate().get()).toBeInTheDocument();
|
||||
|
||||
// 4. Export - should be available for Grafana alerting rules (AlertingRuleRead permission)
|
||||
expect(ui.menuItems.export().get()).toBeInTheDocument();
|
||||
|
||||
// Verify that the menu contains all 4 expected menu items
|
||||
const menuItems = byRole('menuitem').getAll();
|
||||
expect(menuItems.length).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,22 +1,13 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
import { Alert } from '@grafana/ui';
|
||||
import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
import { GrafanaPromRuleDTO, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { logWarning } from '../Analytics';
|
||||
import { alertRuleApi } from '../api/alertRuleApi';
|
||||
import { prometheusApi } from '../api/prometheusApi';
|
||||
import { RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants';
|
||||
import { GrafanaRulesSource } from '../utils/datasource';
|
||||
|
||||
import { GrafanaRuleListItem } from './GrafanaRuleLoader';
|
||||
import { RuleOperationListItem } from './components/AlertRuleListItem';
|
||||
import { GrafanaRuleListItem } from './GrafanaRuleListItem';
|
||||
import { AlertRuleListItemSkeleton } from './components/AlertRuleListItemLoader';
|
||||
import { RuleOperation } from './components/RuleListIcon';
|
||||
|
||||
const { useGetGrafanaRulerGroupQuery } = alertRuleApi;
|
||||
const { useGetGrafanaGroupsQuery } = prometheusApi;
|
||||
|
||||
export interface GrafanaGroupLoaderProps {
|
||||
|
@ -48,20 +39,8 @@ export function GrafanaGroupLoader({
|
|||
},
|
||||
{ pollingInterval: RULE_LIST_POLL_INTERVAL_MS }
|
||||
);
|
||||
const { data: rulerResponse, isLoading: isRulerGroupLoading } = useGetGrafanaRulerGroupQuery({
|
||||
folderUid: groupIdentifier.namespace.uid,
|
||||
groupName: groupIdentifier.groupName,
|
||||
});
|
||||
|
||||
const { matches, promOnlyRules } = useMemo(() => {
|
||||
const promRules = promResponse?.data.groups.at(0)?.rules ?? [];
|
||||
const rulerRules = rulerResponse?.rules ?? [];
|
||||
|
||||
return matchRules(promRules, rulerRules);
|
||||
}, [promResponse, rulerResponse]);
|
||||
|
||||
const isLoading = isPromResponseLoading || isRulerGroupLoading;
|
||||
if (isLoading) {
|
||||
if (isPromResponseLoading) {
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: expectedRulesCount }).map((_, index) => (
|
||||
|
@ -71,7 +50,7 @@ export function GrafanaGroupLoader({
|
|||
);
|
||||
}
|
||||
|
||||
if (!rulerResponse && !promResponse) {
|
||||
if (!promResponse) {
|
||||
return (
|
||||
<Alert
|
||||
title={t(
|
||||
|
@ -86,28 +65,11 @@ export function GrafanaGroupLoader({
|
|||
|
||||
return (
|
||||
<>
|
||||
{rulerResponse?.rules.map((rulerRule) => {
|
||||
const promRule = matches.get(rulerRule);
|
||||
|
||||
if (!promRule) {
|
||||
return (
|
||||
<GrafanaRuleListItem
|
||||
key={rulerRule.grafana_alert.uid}
|
||||
rule={promRule}
|
||||
rulerRule={rulerRule}
|
||||
groupIdentifier={groupIdentifier}
|
||||
namespaceName={namespaceName}
|
||||
operation={RuleOperation.Creating}
|
||||
showLocation={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
{promResponse.data.groups.at(0)?.rules.map((promRule) => {
|
||||
return (
|
||||
<GrafanaRuleListItem
|
||||
key={promRule.uid}
|
||||
rule={promRule}
|
||||
rulerRule={rulerRule}
|
||||
groupIdentifier={groupIdentifier}
|
||||
namespaceName={namespaceName}
|
||||
// we don't show the location again for rules, it's redundant because they are shown in a folder > group hierarchy
|
||||
|
@ -115,58 +77,6 @@ export function GrafanaGroupLoader({
|
|||
/>
|
||||
);
|
||||
})}
|
||||
{promOnlyRules.map((rule) => (
|
||||
<RuleOperationListItem
|
||||
key={rule.uid}
|
||||
name={rule.name}
|
||||
namespace={namespaceName}
|
||||
group={groupIdentifier.groupName}
|
||||
rulesSource={GrafanaRulesSource}
|
||||
application="grafana"
|
||||
operation={RuleOperation.Deleting}
|
||||
showLocation={false}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface MatchingResult {
|
||||
matches: Map<RulerGrafanaRuleDTO, GrafanaPromRuleDTO>;
|
||||
/**
|
||||
* Rules that were already removed from the Ruler but the changes has not been yet propagated to Prometheus
|
||||
*/
|
||||
promOnlyRules: GrafanaPromRuleDTO[];
|
||||
}
|
||||
|
||||
export function matchRules(
|
||||
promRules: GrafanaPromRuleDTO[],
|
||||
rulerRules: RulerGrafanaRuleDTO[]
|
||||
): Readonly<MatchingResult> {
|
||||
const promRulesMap = new Map(promRules.map((rule) => [rule.uid, rule]));
|
||||
|
||||
const matchingResult = rulerRules.reduce<MatchingResult>(
|
||||
(acc, rulerRule) => {
|
||||
const { matches } = acc;
|
||||
const promRule = promRulesMap.get(rulerRule.grafana_alert.uid);
|
||||
if (promRule) {
|
||||
matches.set(rulerRule, promRule);
|
||||
promRulesMap.delete(rulerRule.grafana_alert.uid);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ matches: new Map(), promOnlyRules: [] }
|
||||
);
|
||||
|
||||
matchingResult.promOnlyRules.push(...promRulesMap.values());
|
||||
|
||||
if (matchingResult.promOnlyRules.length > 0) {
|
||||
// Grafana Prometheus rules should be strongly consistent now so each Ruler rule should have a matching Prometheus rule
|
||||
// If not, log it as a warning
|
||||
logWarning('Grafana Managed Rules: No matching Prometheus rule found for Ruler rule', {
|
||||
promOnlyRulesCount: matchingResult.promOnlyRules.length.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
return matchingResult;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
import { GrafanaPromRuleDTO, PromRuleType } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { GrafanaRulesSource } from '../utils/datasource';
|
||||
import { totalFromStats } from '../utils/ruleStats';
|
||||
import { prometheusRuleType } from '../utils/rules';
|
||||
import { createRelativeUrl } from '../utils/url';
|
||||
|
||||
import {
|
||||
AlertRuleListItem,
|
||||
RecordingRuleListItem,
|
||||
RuleListItemCommonProps,
|
||||
UnknownRuleListItem,
|
||||
} from './components/AlertRuleListItem';
|
||||
import { RuleActionsButtons } from './components/RuleActionsButtons.V2';
|
||||
import { RuleOperation } from './components/RuleListIcon';
|
||||
|
||||
interface GrafanaRuleListItemProps {
|
||||
rule: GrafanaPromRuleDTO;
|
||||
groupIdentifier: GrafanaRuleGroupIdentifier;
|
||||
namespaceName: string;
|
||||
operation?: RuleOperation;
|
||||
showLocation?: boolean;
|
||||
}
|
||||
|
||||
export function GrafanaRuleListItem({
|
||||
rule,
|
||||
groupIdentifier,
|
||||
namespaceName,
|
||||
operation,
|
||||
showLocation = true,
|
||||
}: GrafanaRuleListItemProps) {
|
||||
const { name, uid, labels, provenance } = rule;
|
||||
|
||||
const commonProps: RuleListItemCommonProps = {
|
||||
name,
|
||||
rulesSource: GrafanaRulesSource,
|
||||
group: groupIdentifier.groupName,
|
||||
namespace: namespaceName,
|
||||
href: createRelativeUrl(`/alerting/grafana/${uid}/view`),
|
||||
health: rule?.health,
|
||||
error: rule?.lastError,
|
||||
labels: labels,
|
||||
isProvisioned: Boolean(provenance),
|
||||
isPaused: rule?.isPaused,
|
||||
application: 'grafana' as const,
|
||||
actions: <RuleActionsButtons promRule={rule} groupIdentifier={groupIdentifier} compact />,
|
||||
};
|
||||
|
||||
if (prometheusRuleType.grafana.alertingRule(rule)) {
|
||||
const promAlertingRule = rule && rule.type === PromRuleType.Alerting ? rule : undefined;
|
||||
const instancesCount = totalFromStats(promAlertingRule?.totals ?? {});
|
||||
|
||||
return (
|
||||
<AlertRuleListItem
|
||||
{...commonProps}
|
||||
summary={rule.annotations?.summary}
|
||||
state={promAlertingRule?.state}
|
||||
instancesCount={instancesCount}
|
||||
operation={operation}
|
||||
showLocation={showLocation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (prometheusRuleType.grafana.recordingRule(rule)) {
|
||||
return <RecordingRuleListItem {...commonProps} showLocation={showLocation} />;
|
||||
}
|
||||
|
||||
return <UnknownRuleListItem ruleName={name} groupIdentifier={groupIdentifier} ruleDefinition={rule} />;
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Alert } from '@grafana/ui';
|
||||
import { GrafanaRuleGroupIdentifier, GrafanaRuleIdentifier } from 'app/types/unified-alerting';
|
||||
import { GrafanaPromRuleDTO, PromRuleType, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { alertRuleApi } from '../api/alertRuleApi';
|
||||
import { prometheusApi } from '../api/prometheusApi';
|
||||
import { createReturnTo } from '../hooks/useReturnTo';
|
||||
import { GrafanaRulesSource } from '../utils/datasource';
|
||||
import { totalFromStats } from '../utils/ruleStats';
|
||||
import { rulerRuleType } from '../utils/rules';
|
||||
import { createRelativeUrl } from '../utils/url';
|
||||
|
||||
import {
|
||||
AlertRuleListItem,
|
||||
RecordingRuleListItem,
|
||||
RuleListItemCommonProps,
|
||||
UnknownRuleListItem,
|
||||
} from './components/AlertRuleListItem';
|
||||
import { AlertRuleListItemSkeleton, RulerRuleLoadingError } from './components/AlertRuleListItemLoader';
|
||||
import { RuleActionsButtons } from './components/RuleActionsButtons.V2';
|
||||
import { RuleOperation } from './components/RuleListIcon';
|
||||
|
||||
const { useGetGrafanaRulerGroupQuery } = alertRuleApi;
|
||||
const { useGetGrafanaGroupsQuery } = prometheusApi;
|
||||
|
||||
interface GrafanaRuleLoaderProps {
|
||||
ruleIdentifier: GrafanaRuleIdentifier;
|
||||
groupIdentifier: GrafanaRuleGroupIdentifier;
|
||||
namespaceName: string;
|
||||
}
|
||||
|
||||
export function GrafanaRuleLoader({ ruleIdentifier, groupIdentifier, namespaceName }: GrafanaRuleLoaderProps) {
|
||||
const {
|
||||
data: rulerRuleGroup,
|
||||
error: rulerRuleGroupError,
|
||||
isLoading: isRulerRuleGroupLoading,
|
||||
} = useGetGrafanaRulerGroupQuery({
|
||||
folderUid: groupIdentifier.namespace.uid,
|
||||
groupName: groupIdentifier.groupName,
|
||||
});
|
||||
const {
|
||||
data: promRuleGroup,
|
||||
error: promRuleGroupError,
|
||||
isLoading: isPromRuleGroupLoading,
|
||||
} = useGetGrafanaGroupsQuery({
|
||||
folderUid: groupIdentifier.namespace.uid,
|
||||
groupName: groupIdentifier.groupName,
|
||||
});
|
||||
|
||||
const rulerRule = rulerRuleGroup?.rules.find((rulerRule) => rulerRule.grafana_alert.uid === ruleIdentifier.uid);
|
||||
const promRule = promRuleGroup?.data.groups
|
||||
.flatMap((group) => group.rules)
|
||||
.find((promRule) => promRule.uid === ruleIdentifier.uid);
|
||||
|
||||
if (rulerRuleGroupError || promRuleGroupError) {
|
||||
return <RulerRuleLoadingError ruleIdentifier={ruleIdentifier} error={rulerRuleGroupError || promRuleGroupError} />;
|
||||
}
|
||||
|
||||
if (isRulerRuleGroupLoading || isPromRuleGroupLoading) {
|
||||
return <AlertRuleListItemSkeleton />;
|
||||
}
|
||||
|
||||
if (!rulerRule) {
|
||||
return (
|
||||
<Alert
|
||||
title={t('alerting.rule-list.cannot-load-rule-details-for', 'Cannot load rule details for UID {{uid}}', {
|
||||
uid: ruleIdentifier.uid,
|
||||
})}
|
||||
severity="error"
|
||||
>
|
||||
<Trans i18nKey="alerting.rule-list.cannot-find-rule-details-for">
|
||||
Cannot find rule details for UID {{ uid: ruleIdentifier.uid ?? '<empty uid>' }}
|
||||
</Trans>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GrafanaRuleListItem
|
||||
rule={promRule}
|
||||
rulerRule={rulerRule}
|
||||
groupIdentifier={groupIdentifier}
|
||||
namespaceName={namespaceName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface GrafanaRuleListItemProps {
|
||||
rule?: GrafanaPromRuleDTO;
|
||||
rulerRule: RulerGrafanaRuleDTO;
|
||||
groupIdentifier: GrafanaRuleGroupIdentifier;
|
||||
namespaceName: string;
|
||||
operation?: RuleOperation;
|
||||
showLocation?: boolean;
|
||||
}
|
||||
|
||||
export function GrafanaRuleListItem({
|
||||
rule,
|
||||
rulerRule,
|
||||
groupIdentifier,
|
||||
namespaceName,
|
||||
operation,
|
||||
showLocation = true,
|
||||
}: GrafanaRuleListItemProps) {
|
||||
const returnTo = createReturnTo();
|
||||
|
||||
const {
|
||||
grafana_alert: { uid, title, provenance, is_paused },
|
||||
annotations = {},
|
||||
labels = {},
|
||||
} = rulerRule;
|
||||
|
||||
const commonProps: RuleListItemCommonProps = {
|
||||
name: title,
|
||||
rulesSource: GrafanaRulesSource,
|
||||
group: groupIdentifier.groupName,
|
||||
namespace: namespaceName,
|
||||
href: createRelativeUrl(`/alerting/grafana/${uid}/view`, { returnTo }),
|
||||
health: rule?.health,
|
||||
error: rule?.lastError,
|
||||
labels: labels,
|
||||
isProvisioned: Boolean(provenance),
|
||||
isPaused: rule?.isPaused ?? is_paused,
|
||||
application: 'grafana' as const,
|
||||
actions: <RuleActionsButtons rule={rulerRule} promRule={rule} groupIdentifier={groupIdentifier} compact />,
|
||||
showLocation,
|
||||
};
|
||||
|
||||
if (rulerRuleType.grafana.alertingRule(rulerRule)) {
|
||||
const promAlertingRule = rule && rule.type === PromRuleType.Alerting ? rule : undefined;
|
||||
const instancesCount = totalFromStats(promAlertingRule?.totals ?? {});
|
||||
|
||||
return (
|
||||
<AlertRuleListItem
|
||||
{...commonProps}
|
||||
summary={annotations.summary}
|
||||
state={promAlertingRule?.state}
|
||||
instancesCount={instancesCount}
|
||||
operation={operation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (rulerRuleType.grafana.recordingRule(rulerRule)) {
|
||||
return <RecordingRuleListItem {...commonProps} />;
|
||||
}
|
||||
|
||||
return <UnknownRuleListItem ruleName={title} groupIdentifier={groupIdentifier} ruleDefinition={rulerRule} />;
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from 'react';
|
||||
import { RequireAtLeastOne } from 'type-fest';
|
||||
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { LinkButton, Stack } from '@grafana/ui';
|
||||
|
@ -6,24 +7,34 @@ import AlertRuleMenu from 'app/features/alerting/unified/components/rule-viewer/
|
|||
import { useDeleteModal } from 'app/features/alerting/unified/components/rule-viewer/DeleteModal';
|
||||
import { RedirectToCloneRule } from 'app/features/alerting/unified/components/rules/CloneRule';
|
||||
import SilenceGrafanaRuleDrawer from 'app/features/alerting/unified/components/silences/SilenceGrafanaRuleDrawer';
|
||||
import { Rule, RuleGroupIdentifierV2, RuleIdentifier } from 'app/types/unified-alerting';
|
||||
import {
|
||||
EditableRuleIdentifier,
|
||||
GrafanaRuleIdentifier,
|
||||
Rule,
|
||||
RuleGroupIdentifierV2,
|
||||
RuleIdentifier,
|
||||
} from 'app/types/unified-alerting';
|
||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { AlertRuleAction, useRulerRuleAbility } from '../../hooks/useAbilities';
|
||||
import { logWarning } from '../../Analytics';
|
||||
import { AlertRuleAction, skipToken, useGrafanaPromRuleAbility, useRulerRuleAbility } from '../../hooks/useAbilities';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { isProvisionedRule, rulerRuleType } from '../../utils/rules';
|
||||
import { isProvisionedPromRule, isProvisionedRule, prometheusRuleType, rulerRuleType } from '../../utils/rules';
|
||||
import { createRelativeUrl } from '../../utils/url';
|
||||
|
||||
interface Props {
|
||||
rule: RulerRuleDTO;
|
||||
type RuleProps = RequireAtLeastOne<{
|
||||
rule?: RulerRuleDTO;
|
||||
promRule?: Rule;
|
||||
}>;
|
||||
|
||||
type Props = RuleProps & {
|
||||
groupIdentifier: RuleGroupIdentifierV2;
|
||||
/**
|
||||
* Should we show the buttons in a "compact" state?
|
||||
* i.e. without text and using smaller button sizes
|
||||
*/
|
||||
compact?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
// For now this is just a copy of RuleActionsButtons.tsx but with the View button removed.
|
||||
// This is only done to keep the new list behind a feature flag and limit changes in the existing components
|
||||
|
@ -37,16 +48,26 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }:
|
|||
{ identifier: RuleIdentifier; isProvisioned: boolean } | undefined
|
||||
>(undefined);
|
||||
|
||||
const isProvisioned = isProvisionedRule(rule);
|
||||
const isProvisioned = getIsProvisioned(rule, promRule);
|
||||
|
||||
const [editRuleSupported, editRuleAllowed] = useRulerRuleAbility(rule, groupIdentifier, AlertRuleAction.Update);
|
||||
// If the consumer of this component comes from the alert list view, we need to use promRule to check abilities and permissions,
|
||||
// as we have removed all requests to the ruler API in the list view.
|
||||
const [grafanaEditRuleSupported, grafanaEditRuleAllowed] = useGrafanaPromRuleAbility(
|
||||
prometheusRuleType.grafana.rule(promRule) ? promRule : skipToken,
|
||||
AlertRuleAction.Update
|
||||
);
|
||||
|
||||
const canEditRule = editRuleSupported && editRuleAllowed;
|
||||
const canEditRule = (editRuleSupported && editRuleAllowed) || (grafanaEditRuleSupported && grafanaEditRuleAllowed);
|
||||
|
||||
const buttons: JSX.Element[] = [];
|
||||
const buttonSize = compact ? 'sm' : 'md';
|
||||
|
||||
const identifier = ruleId.fromRulerRuleAndGroupIdentifierV2(groupIdentifier, rule);
|
||||
const identifier = getEditableIdentifier(groupIdentifier, rule, promRule);
|
||||
|
||||
if (!identifier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (canEditRule) {
|
||||
const editURL = createRelativeUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`);
|
||||
|
@ -93,3 +114,38 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }:
|
|||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function getIsProvisioned(rule?: RulerRuleDTO, promRule?: Rule): boolean {
|
||||
if (rule) {
|
||||
return isProvisionedRule(rule);
|
||||
}
|
||||
|
||||
if (promRule) {
|
||||
return isProvisionedPromRule(promRule);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getEditableIdentifier(
|
||||
groupIdentifier: RuleGroupIdentifierV2,
|
||||
rule?: RulerRuleDTO,
|
||||
promRule?: Rule
|
||||
): EditableRuleIdentifier | undefined {
|
||||
if (rule) {
|
||||
return ruleId.fromRulerRuleAndGroupIdentifierV2(groupIdentifier, rule);
|
||||
}
|
||||
|
||||
if (prometheusRuleType.grafana.rule(promRule)) {
|
||||
return {
|
||||
ruleSourceName: 'grafana',
|
||||
uid: promRule.uid,
|
||||
} satisfies GrafanaRuleIdentifier;
|
||||
}
|
||||
|
||||
logWarning('Unable to construct an editable rule identifier');
|
||||
|
||||
// Returning undefined is safer than throwing here as it allows the component to gracefully handle
|
||||
// the error by returning null instead of crashing the entire component tree
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import { useDispatch } from 'app/types/store';
|
|||
import { DataSourceRulesSourceIdentifier, RuleHealth } from 'app/types/unified-alerting';
|
||||
import { PromAlertingRuleState, PromRuleGroupDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
import { PromRulesResponse, prometheusApi } from '../../api/prometheusApi';
|
||||
|
||||
const { useLazyGetGroupsQuery, useLazyGetGrafanaGroupsQuery } = prometheusApi;
|
||||
|
@ -83,13 +82,6 @@ export function useGrafanaGroupsGenerator(hookOptions: UseGeneratorHookOptions =
|
|||
// Because the user waits a bit longer for the initial load but doesn't need to wait for each group to be loaded
|
||||
if (hookOptions.populateCache) {
|
||||
const cacheAndRulerPreload = response.data.groups.map(async (group) => {
|
||||
dispatch(
|
||||
alertRuleApi.util.prefetch(
|
||||
'getGrafanaRulerGroup',
|
||||
{ folderUid: group.folderUid, groupName: group.name },
|
||||
{ force: true }
|
||||
)
|
||||
);
|
||||
await dispatch(
|
||||
prometheusApi.util.upsertQueryData(
|
||||
'getGrafanaGroups',
|
||||
|
|
|
@ -168,6 +168,10 @@ export function isProvisionedRule(rulerRule: RulerRuleDTO): boolean {
|
|||
return isGrafanaRulerRule(rulerRule) && Boolean(rulerRule.grafana_alert.provenance);
|
||||
}
|
||||
|
||||
export function isProvisionedPromRule(promRule: PromRuleDTO): boolean {
|
||||
return prometheusRuleType.grafana.rule(promRule) && Boolean(promRule.provenance);
|
||||
}
|
||||
|
||||
export function isProvisionedRuleGroup(group: RulerRuleGroupDTO): boolean {
|
||||
return group.rules.some((rule) => isProvisionedRule(rule));
|
||||
}
|
||||
|
|
|
@ -132,6 +132,7 @@ interface GrafanaPromRuleDTOBase extends PromRuleDTOBase {
|
|||
folderUid: string;
|
||||
isPaused: boolean;
|
||||
queriedDatasourceUIDs?: string[];
|
||||
provenance?: string;
|
||||
}
|
||||
|
||||
export interface PromAlertingRuleDTO extends PromRuleDTOBase {
|
||||
|
|
|
@ -2415,8 +2415,6 @@
|
|||
"title-inspect-alert-rule": "Inspect Alert rule"
|
||||
},
|
||||
"rule-list": {
|
||||
"cannot-find-rule-details-for": "Cannot find rule details for UID {{uid}}",
|
||||
"cannot-load-rule-details-for": "Cannot load rule details for UID {{uid}}",
|
||||
"configure-datasource": "Configure",
|
||||
"draft-new-rule": "Draft a new rule",
|
||||
"ds-error": {
|
||||
|
|
Loading…
Reference in New Issue