mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Add GMA action buttons to the new list view (#98449)
* Add GMA action buttons based on the ruler rule definition * Improve imports * Remove rulesSource from Grafana group identifier * Improve ruler loader error handling * Clean imports, add details page link * Remove unnecessary property from the API: * Change Prometheus page size in the FilterView * Fix lint errors and tests * Revert filtered items page size * Fix cache invalidation for RTKQ ruler requests * Fix tags ids * Naming improvements * Fix lint errors, use util function for pause checking * Alerting: Add ruleGroupIdentifierV2toV1 function to PR 98449 (#99326) * Move params validation to the rulerUrlBuilder, tidy up code --------- Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
parent
49f8359ce5
commit
c5ff5d89df
@ -3,7 +3,12 @@ import { set } from 'lodash';
|
||||
import { RelativeTimeRange } from '@grafana/data';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { RuleIdentifier, RuleNamespace, RulerDataSourceConfig } from 'app/types/unified-alerting';
|
||||
import {
|
||||
GrafanaRuleGroupIdentifier,
|
||||
RuleIdentifier,
|
||||
RuleNamespace,
|
||||
RulerDataSourceConfig,
|
||||
} from 'app/types/unified-alerting';
|
||||
import {
|
||||
AlertQuery,
|
||||
Annotations,
|
||||
@ -23,6 +28,7 @@ import { arrayKeyValuesToObject } from '../utils/labels';
|
||||
import { isCloudRuleIdentifier, isGrafanaRulerRule, isPrometheusRuleIdentifier } from '../utils/rules';
|
||||
|
||||
import { WithNotificationOptions, alertingApi } from './alertingApi';
|
||||
import { GRAFANA_RULER_CONFIG } from './featureDiscoveryApi';
|
||||
import {
|
||||
FetchPromRulesFilter,
|
||||
getRulesFilterSearchParams,
|
||||
@ -257,12 +263,20 @@ export const alertRuleApi = alertingApi.injectEndpoints({
|
||||
notificationOptions,
|
||||
};
|
||||
},
|
||||
providesTags: (_result, _error, { namespace, group }) => [
|
||||
{
|
||||
type: 'RuleGroup',
|
||||
id: `${namespace}/${group}`,
|
||||
},
|
||||
{ type: 'RuleNamespace', id: namespace },
|
||||
providesTags: (_result, _error, { namespace, group, rulerConfig }) => [
|
||||
{ type: 'RuleGroup', id: `${rulerConfig.dataSourceUid}/${namespace}/${group}` },
|
||||
{ type: 'RuleNamespace', id: `${rulerConfig.dataSourceUid}/${namespace}` },
|
||||
],
|
||||
}),
|
||||
|
||||
getGrafanaRulerGroup: build.query<RulerRuleGroupDTO<RulerGrafanaRuleDTO>, GrafanaRuleGroupIdentifier>({
|
||||
query: ({ namespace, groupName }) => {
|
||||
const { path, params } = rulerUrlBuilder(GRAFANA_RULER_CONFIG).namespaceGroup(namespace.uid, groupName);
|
||||
return { url: path, params };
|
||||
},
|
||||
providesTags: (_result, _error, { namespace, groupName }) => [
|
||||
{ type: 'RuleGroup', id: `grafana/${namespace.uid}/${groupName}` },
|
||||
{ type: 'RuleNamespace', id: `grafana/${namespace.uid}` },
|
||||
],
|
||||
}),
|
||||
|
||||
@ -284,12 +298,9 @@ export const alertRuleApi = alertingApi.injectEndpoints({
|
||||
},
|
||||
};
|
||||
},
|
||||
invalidatesTags: (_result, _error, { namespace, group }) => [
|
||||
{
|
||||
type: 'RuleGroup',
|
||||
id: `${namespace}/${group}`,
|
||||
},
|
||||
{ type: 'RuleNamespace', id: namespace },
|
||||
invalidatesTags: (_result, _error, { namespace, group, rulerConfig }) => [
|
||||
{ type: 'RuleGroup', id: `${rulerConfig.dataSourceUid}/${namespace}/${group}` },
|
||||
{ type: 'RuleNamespace', id: `${rulerConfig.dataSourceUid}/${namespace}` },
|
||||
],
|
||||
}),
|
||||
|
||||
@ -317,12 +328,9 @@ export const alertRuleApi = alertingApi.injectEndpoints({
|
||||
},
|
||||
};
|
||||
},
|
||||
invalidatesTags: (result, _error, { namespace, payload }) => [
|
||||
{ type: 'RuleNamespace', id: namespace },
|
||||
{
|
||||
type: 'RuleGroup',
|
||||
id: `${namespace}/${payload.name}`,
|
||||
},
|
||||
invalidatesTags: (result, _error, { namespace, payload, rulerConfig }) => [
|
||||
{ type: 'RuleNamespace', id: `${rulerConfig.dataSourceUid}/${namespace}` },
|
||||
{ type: 'RuleGroup', id: `${rulerConfig.dataSourceUid}/${namespace}/${payload.name}` },
|
||||
...payload.rules
|
||||
.filter((rule) => isGrafanaRulerRule(rule))
|
||||
.map((rule) => ({ type: 'GrafanaRulerRule', id: rule.grafana_alert.uid }) as const),
|
||||
|
@ -12,6 +12,7 @@ import { discoverAlertmanagerFeatures, discoverFeaturesByUid } from './buildInfo
|
||||
|
||||
export const GRAFANA_RULER_CONFIG: RulerDataSourceConfig = {
|
||||
dataSourceName: 'grafana',
|
||||
dataSourceUid: 'grafana',
|
||||
apiVersion: 'legacy',
|
||||
};
|
||||
|
||||
@ -63,6 +64,7 @@ export const featureDiscoveryApi = alertingApi.injectEndpoints({
|
||||
const rulerConfig = features.features.rulerApiEnabled
|
||||
? ({
|
||||
dataSourceName: dataSourceSettings.name,
|
||||
dataSourceUid: dataSourceSettings.uid,
|
||||
apiVersion: features.application === PromApplication.Cortex ? 'legacy' : 'config',
|
||||
} satisfies RulerDataSourceConfig)
|
||||
: undefined;
|
||||
|
@ -2,17 +2,14 @@ import { RulerDataSourceConfig } from 'app/types/unified-alerting';
|
||||
|
||||
import { mockDataSource } from '../mocks';
|
||||
import { setupDataSources } from '../testSetup/datasources';
|
||||
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
import { DataSourceType } from '../utils/datasource';
|
||||
|
||||
import { GRAFANA_RULER_CONFIG } from './featureDiscoveryApi';
|
||||
import { rulerUrlBuilder } from './ruler';
|
||||
|
||||
const grafanaConfig: RulerDataSourceConfig = {
|
||||
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
apiVersion: 'legacy',
|
||||
};
|
||||
|
||||
const mimirConfig: RulerDataSourceConfig = {
|
||||
dataSourceName: 'Mimir-cloud',
|
||||
dataSourceUid: 'mimir-1',
|
||||
apiVersion: 'config',
|
||||
};
|
||||
|
||||
@ -28,6 +25,7 @@ describe('rulerUrlBuilder', () => {
|
||||
// Arrange
|
||||
const config: RulerDataSourceConfig = {
|
||||
dataSourceName: 'Cortex',
|
||||
dataSourceUid: 'cortex-1',
|
||||
apiVersion: 'legacy',
|
||||
};
|
||||
|
||||
@ -121,7 +119,7 @@ describe('rulerUrlBuilder', () => {
|
||||
// GMA uses folderUIDs as namespaces and they should never contain slashes
|
||||
it('Should only replace the group segment for Grafana-managed rules', () => {
|
||||
// Act
|
||||
const builder = rulerUrlBuilder(grafanaConfig);
|
||||
const builder = rulerUrlBuilder(GRAFANA_RULER_CONFIG);
|
||||
|
||||
const group = builder.namespaceGroup('test/ns', 'test/gr');
|
||||
|
||||
|
@ -47,6 +47,13 @@ export function rulerUrlBuilder(rulerConfig: RulerDataSourceConfig) {
|
||||
},
|
||||
|
||||
namespaceGroup: (namespaceUID: string, group: string): RulerRequestUrl => {
|
||||
if (!namespaceUID) {
|
||||
throw new Error('Namespace UID is required to fetch ruler group');
|
||||
}
|
||||
if (!group) {
|
||||
throw new Error('Group name is required to fetch ruler group');
|
||||
}
|
||||
|
||||
const { namespace: finalNs, searchParams: nsParams } = queryDetailsProvider.namespace(namespaceUID);
|
||||
const { group: finalGroup, searchParams: groupParams } = queryDetailsProvider.group(group);
|
||||
|
||||
@ -107,7 +114,7 @@ function getQueryDetailsProvider(rulerConfig: RulerDataSourceConfig): RulerQuery
|
||||
}
|
||||
|
||||
function getRulerPath(rulerConfig: RulerDataSourceConfig) {
|
||||
const grafanaServerPath = `/api/ruler/${getDatasourceAPIUid(rulerConfig.dataSourceName)}`;
|
||||
const grafanaServerPath = `/api/ruler/${rulerConfig.dataSourceUid}`;
|
||||
return `${grafanaServerPath}/api/v1/rules`;
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { Menu } from '@grafana/ui';
|
||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||
import { isGrafanaRulerRule, isGrafanaRulerRulePaused } from 'app/features/alerting/unified/utils/rules';
|
||||
import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
import { RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { usePauseRuleInGroup } from '../hooks/ruleGroup/usePauseAlertRule';
|
||||
import { isLoading } from '../hooks/useAsync';
|
||||
import { stringifyErrorLike } from '../utils/misc';
|
||||
import { isGrafanaRulerRulePaused } from '../utils/rules';
|
||||
|
||||
interface Props {
|
||||
rule: RulerRuleDTO;
|
||||
rule: RulerGrafanaRuleDTO;
|
||||
groupIdentifier: GrafanaRuleGroupIdentifier;
|
||||
/**
|
||||
* Method invoked after the request to change the paused state has completed
|
||||
@ -25,22 +25,16 @@ const MenuItemPauseRule = ({ rule, groupIdentifier, onPauseChange }: Props) => {
|
||||
const notifyApp = useAppNotification();
|
||||
const [pauseRule, updateState] = usePauseRuleInGroup();
|
||||
|
||||
const isPaused = isGrafanaRulerRule(rule) && isGrafanaRulerRulePaused(rule);
|
||||
const icon = isPaused ? 'play' : 'pause';
|
||||
const title = isPaused ? 'Resume evaluation' : 'Pause evaluation';
|
||||
const [icon, title] = isGrafanaRulerRulePaused(rule)
|
||||
? ['play' as const, 'Resume evaluation']
|
||||
: ['pause' as const, 'Pause evaluation'];
|
||||
|
||||
/**
|
||||
* Triggers API call to update the current rule to the new `is_paused` state
|
||||
*/
|
||||
const setRulePause = async (newIsPaused: boolean) => {
|
||||
if (!isGrafanaRulerRule(rule)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const ruleUID = rule.grafana_alert.uid;
|
||||
|
||||
await pauseRule.execute(groupIdentifier, ruleUID, newIsPaused);
|
||||
await pauseRule.execute(groupIdentifier, rule.grafana_alert.uid, newIsPaused);
|
||||
} catch (error) {
|
||||
notifyApp.error(`Failed to ${newIsPaused ? 'pause' : 'resume'} the rule: ${stringifyErrorLike(error)}`);
|
||||
return;
|
||||
@ -55,7 +49,7 @@ const MenuItemPauseRule = ({ rule, groupIdentifier, onPauseChange }: Props) => {
|
||||
icon={icon}
|
||||
disabled={isLoading(updateState)}
|
||||
onClick={() => {
|
||||
setRulePause(!isPaused);
|
||||
setRulePause(!rule.grafana_alert.is_paused);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -10,7 +10,7 @@ import { PromAlertingRuleState, RulerRuleDTO } from 'app/types/unified-alerting-
|
||||
import { AlertRuleAction, useRulerRuleAbility } from '../../hooks/useAbilities';
|
||||
import { createShareLink, isLocalDevEnv, isOpenSourceEdition } from '../../utils/misc';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { isAlertingRule } from '../../utils/rules';
|
||||
import { isAlertingRule, isGrafanaRulerRule } from '../../utils/rules';
|
||||
import { createRelativeUrl } from '../../utils/url';
|
||||
import { DeclareIncidentMenuItem } from '../bridges/DeclareIncidentButton';
|
||||
|
||||
@ -86,7 +86,7 @@ const AlertRuleMenu = ({
|
||||
|
||||
const menuItems = (
|
||||
<>
|
||||
{canPause && rulerRule && groupIdentifier.groupOrigin === 'grafana' && (
|
||||
{canPause && isGrafanaRulerRule(rulerRule) && groupIdentifier.groupOrigin === 'grafana' && (
|
||||
<MenuItemPauseRule rule={rulerRule} groupIdentifier={groupIdentifier} onPauseChange={onPauseChange} />
|
||||
)}
|
||||
{canSilence && <Menu.Item label="Silence notifications" icon="bell-slash" onClick={handleSilence} />}
|
||||
@ -131,15 +131,11 @@ const AlertRuleMenu = ({
|
||||
);
|
||||
};
|
||||
|
||||
function copyToClipboard(text: string) {
|
||||
navigator.clipboard?.writeText(text).then(() => {
|
||||
appEvents.emit(AppEvents.alertSuccess, ['URL copied to clipboard']);
|
||||
});
|
||||
interface ExportMenuItemProps {
|
||||
identifier: RuleIdentifier;
|
||||
}
|
||||
|
||||
type PropsWithIdentifier = { identifier: RuleIdentifier };
|
||||
|
||||
const ExportMenuItem = ({ identifier }: PropsWithIdentifier) => {
|
||||
const ExportMenuItem = ({ identifier }: ExportMenuItemProps) => {
|
||||
const returnTo = location.pathname + location.search;
|
||||
const url = createRelativeUrl(
|
||||
`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export`,
|
||||
@ -151,4 +147,10 @@ const ExportMenuItem = ({ identifier }: PropsWithIdentifier) => {
|
||||
return <Menu.Item key="with-modifications" label="With modifications" icon="file-edit-alt" url={url} />;
|
||||
};
|
||||
|
||||
function copyToClipboard(text: string) {
|
||||
navigator.clipboard?.writeText(text).then(() => {
|
||||
appEvents.emit(AppEvents.alertSuccess, ['URL copied to clipboard']);
|
||||
});
|
||||
}
|
||||
|
||||
export default AlertRuleMenu;
|
||||
|
@ -3,18 +3,21 @@ import { useCallback, useMemo, useState } from 'react';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { ConfirmModal } from '@grafana/ui';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { RuleGroupIdentifier, RuleGroupIdentifierV2 } from 'app/types/unified-alerting';
|
||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
import { EditableRuleIdentifier, RuleGroupIdentifierV2 } from 'app/types/unified-alerting';
|
||||
|
||||
import { shouldUsePrometheusRulesPrimary } from '../../featureToggles';
|
||||
import { useDeleteRuleFromGroup } from '../../hooks/ruleGroup/useDeleteRuleFromGroup';
|
||||
import { usePrometheusConsistencyCheck } from '../../hooks/usePrometheusConsistencyCheck';
|
||||
import { fetchPromAndRulerRulesAction, fetchRulerRulesAction } from '../../state/actions';
|
||||
import { fromRulerRuleAndRuleGroupIdentifier } from '../../utils/rule-id';
|
||||
import { ruleGroupIdentifierV2toV1 } from '../../utils/groupIdentifier';
|
||||
import { isCloudRuleIdentifier } from '../../utils/rules';
|
||||
|
||||
type DeleteModalHook = [JSX.Element, (rule: RulerRuleDTO, groupIdentifier: RuleGroupIdentifierV2) => void, () => void];
|
||||
type DeleteRuleInfo = { rule: RulerRuleDTO; groupIdentifier: RuleGroupIdentifierV2 } | undefined;
|
||||
type DeleteModalHook = [
|
||||
JSX.Element,
|
||||
(ruleIdentifier: EditableRuleIdentifier, groupIdentifier: RuleGroupIdentifierV2) => void,
|
||||
() => void,
|
||||
];
|
||||
type DeleteRuleInfo = { ruleIdentifier: EditableRuleIdentifier; groupIdentifier: RuleGroupIdentifierV2 } | undefined;
|
||||
|
||||
const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary();
|
||||
|
||||
@ -27,8 +30,8 @@ export const useDeleteModal = (redirectToListView = false): DeleteModalHook => {
|
||||
setRuleToDelete(undefined);
|
||||
}, []);
|
||||
|
||||
const showModal = useCallback((rule: RulerRuleDTO, groupIdentifier: RuleGroupIdentifierV2) => {
|
||||
setRuleToDelete({ rule, groupIdentifier });
|
||||
const showModal = useCallback((ruleIdentifier: EditableRuleIdentifier, groupIdentifier: RuleGroupIdentifierV2) => {
|
||||
setRuleToDelete({ ruleIdentifier, groupIdentifier });
|
||||
}, []);
|
||||
|
||||
const deleteRule = useCallback(async () => {
|
||||
@ -36,26 +39,22 @@ export const useDeleteModal = (redirectToListView = false): DeleteModalHook => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { rule, groupIdentifier } = ruleToDelete;
|
||||
const { ruleIdentifier, groupIdentifier } = ruleToDelete;
|
||||
|
||||
const groupIdentifierV1 = ruleGroupIdentifierV2toV1(groupIdentifier);
|
||||
const rulesSourceName = groupIdentifierV1.dataSourceName;
|
||||
|
||||
const groupIdentifierV1: RuleGroupIdentifier = {
|
||||
dataSourceName: groupIdentifier.rulesSource.name,
|
||||
namespaceName:
|
||||
'uid' in groupIdentifier.namespace ? groupIdentifier.namespace.uid : groupIdentifier.namespace.name,
|
||||
groupName: groupIdentifier.groupName,
|
||||
};
|
||||
const ruleIdentifier = fromRulerRuleAndRuleGroupIdentifier(groupIdentifierV1, rule);
|
||||
await deleteRuleFromGroup.execute(groupIdentifierV1, ruleIdentifier);
|
||||
|
||||
// refetch rules for this rules source
|
||||
// @TODO remove this when we moved everything to RTKQ – then the endpoint will simply invalidate the tags
|
||||
dispatch(fetchPromAndRulerRulesAction({ rulesSourceName: groupIdentifier.rulesSource.name }));
|
||||
dispatch(fetchPromAndRulerRulesAction({ rulesSourceName }));
|
||||
|
||||
if (prometheusRulesPrimary && isCloudRuleIdentifier(ruleIdentifier)) {
|
||||
await waitForRemoval(ruleIdentifier);
|
||||
} else {
|
||||
// Without this the delete popup will close and the user will still see the deleted rule
|
||||
await dispatch(fetchRulerRulesAction({ rulesSourceName: groupIdentifier.rulesSource.name }));
|
||||
await dispatch(fetchRulerRulesAction({ rulesSourceName }));
|
||||
}
|
||||
|
||||
dismissModal();
|
||||
|
@ -107,7 +107,8 @@ export const RuleActionsButtons = ({ compact, showViewButton, rule, rulesSource
|
||||
groupIdentifier={groupId}
|
||||
handleDelete={() => {
|
||||
if (rule.rulerRule) {
|
||||
showDeleteModal(rule.rulerRule, groupId);
|
||||
const editableRuleIdentifier = ruleId.fromRulerRuleAndGroupIdentifierV2(groupId, rule.rulerRule);
|
||||
showDeleteModal(editableRuleIdentifier, groupId);
|
||||
}
|
||||
}}
|
||||
handleSilence={() => setShowSilenceDrawer(true)}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { GrafanaRuleGroupIdentifier, RuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
|
||||
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
import { pauseRuleAction } from '../../reducers/ruler/ruleGroups';
|
||||
import { ruleGroupIdentifierV2toV1 } from '../../utils/groupIdentifier';
|
||||
import { useAsync } from '../useAsync';
|
||||
|
||||
import { useProduceNewRuleGroup } from './useProduceNewRuleGroup';
|
||||
@ -19,11 +20,8 @@ export function usePauseRuleInGroup() {
|
||||
const ruleResumedMessage = t('alerting.rules.resume-rule.success', 'Rule evaluation resumed');
|
||||
|
||||
return useAsync(async (ruleGroup: GrafanaRuleGroupIdentifier, uid: string, pause: boolean) => {
|
||||
const groupIdentifierV1: RuleGroupIdentifier = {
|
||||
dataSourceName: ruleGroup.rulesSource.name,
|
||||
namespaceName: ruleGroup.namespace.uid,
|
||||
groupName: ruleGroup.groupName,
|
||||
};
|
||||
const groupIdentifierV1 = ruleGroupIdentifierV2toV1(ruleGroup);
|
||||
|
||||
const action = pauseRuleAction({ uid, pause });
|
||||
const { newRuleGroupDefinition, rulerConfig } = await produceNewRuleGroup(groupIdentifierV1, action);
|
||||
|
||||
|
@ -20,6 +20,7 @@ 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 { isAdmin } from '../utils/misc';
|
||||
import { isFederatedRuleGroup, isGrafanaRecordingRule, isGrafanaRulerRule, isPluginProvidedRule } from '../utils/rules';
|
||||
|
||||
@ -242,7 +243,7 @@ export function useAllRulerRuleAbilities(
|
||||
rule: RulerRuleDTO | undefined,
|
||||
groupIdentifier: RuleGroupIdentifierV2
|
||||
): Abilities<AlertRuleAction> {
|
||||
const rulesSourceName = groupIdentifier.rulesSource.name;
|
||||
const rulesSourceName = getGroupOriginName(groupIdentifier);
|
||||
|
||||
const { isEditable, isRemovable, isRulerAvailable = false, loading } = useIsRuleEditable(rulesSourceName, rule);
|
||||
const [_, exportAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules);
|
||||
@ -443,9 +444,8 @@ function useCanSilence(rule?: RulerRuleDTO): [boolean, boolean] {
|
||||
const isGrafanaManagedRule = rule && isGrafanaRulerRule(rule);
|
||||
const isGrafanaRecording = rule && isGrafanaRecordingRule(rule);
|
||||
|
||||
const { currentData: amConfigStatus, isLoading } = useGetGrafanaAlertingConfigurationStatusQuery(undefined, {
|
||||
skip: !isGrafanaManagedRule || !rule,
|
||||
});
|
||||
const silenceSupported = useGrafanaRulesSilenceSupport();
|
||||
const canSilenceInFolder = useCanSilenceInFolder(folderUID);
|
||||
|
||||
if (!rule) {
|
||||
return [false, false];
|
||||
@ -453,24 +453,38 @@ function useCanSilence(rule?: RulerRuleDTO): [boolean, boolean] {
|
||||
|
||||
// we don't support silencing when the rule is not a Grafana managed alerting rule
|
||||
// we simply don't know what Alertmanager the ruler is sending alerts to
|
||||
if (!isGrafanaManagedRule || isGrafanaRecording || isLoading || folderIsLoading || !folder) {
|
||||
if (!isGrafanaManagedRule || isGrafanaRecording || folderIsLoading || !folder) {
|
||||
return [false, false];
|
||||
}
|
||||
|
||||
return [silenceSupported, canSilenceInFolder];
|
||||
}
|
||||
|
||||
function useCanSilenceInFolder(folderUID?: string) {
|
||||
const folderPermissions = useFolderPermissions(folderUID);
|
||||
|
||||
const hasFolderSilencePermission = folderPermissions[AccessControlAction.AlertingSilenceCreate] ?? false;
|
||||
const hasGlobalSilencePermission = ctx.hasPermission(AccessControlAction.AlertingInstanceCreate);
|
||||
|
||||
// User is permitted to silence if they either have the "global" permissions of "AlertingInstanceCreate",
|
||||
// or the folder specific access control of "AlertingSilenceCreate"
|
||||
const allowedToSilence = hasGlobalSilencePermission || hasFolderSilencePermission;
|
||||
return allowedToSilence;
|
||||
}
|
||||
|
||||
function useGrafanaRulesSilenceSupport() {
|
||||
const { currentData: amConfigStatus, isLoading } = useGetGrafanaAlertingConfigurationStatusQuery(undefined);
|
||||
|
||||
const interactsOnlyWithExternalAMs = amConfigStatus?.alertmanagersChoice === AlertmanagerChoice.External;
|
||||
const interactsWithAll = amConfigStatus?.alertmanagersChoice === AlertmanagerChoice.All;
|
||||
const silenceSupported = !interactsOnlyWithExternalAMs || interactsWithAll;
|
||||
|
||||
const { accessControl = {} } = folder;
|
||||
return isLoading ? false : silenceSupported;
|
||||
}
|
||||
|
||||
// User is permitted to silence if they either have the "global" permissions of "AlertingInstanceCreate",
|
||||
// or the folder specific access control of "AlertingSilenceCreate"
|
||||
const allowedToSilence = Boolean(
|
||||
ctx.hasPermission(AccessControlAction.AlertingInstanceCreate) ||
|
||||
accessControl[AccessControlAction.AlertingSilenceCreate]
|
||||
);
|
||||
|
||||
return [silenceSupported, allowedToSilence];
|
||||
function useFolderPermissions(folderUID?: string): Record<string, boolean> {
|
||||
const { folder } = useFolder(folderUID);
|
||||
return folder?.accessControl ?? {};
|
||||
}
|
||||
|
||||
// just a convenient function
|
||||
|
@ -9,7 +9,8 @@ import { getRulePluginOrigin, isAlertingRule, isRecordingRule } from '../utils/r
|
||||
import { createRelativeUrl } from '../utils/url';
|
||||
|
||||
import { AlertRuleListItem, RecordingRuleListItem, UnknownRuleListItem } from './components/AlertRuleListItem';
|
||||
import { ActionsLoader, RuleActionsButtons } from './components/RuleActionsButtons.V2';
|
||||
import { RuleActionsButtons } from './components/RuleActionsButtons.V2';
|
||||
import { RuleActionsSkeleton } from './components/RuleActionsSkeleton';
|
||||
|
||||
const { useDiscoverDsFeaturesQuery } = featureDiscoveryApi;
|
||||
const { useGetRuleGroupForNamespaceQuery } = alertRuleApi;
|
||||
@ -63,7 +64,7 @@ export const DataSourceRuleLoader = memo(function DataSourceRuleLoader({
|
||||
// 2.2 render provisioning badge and contact point metadata, etc.
|
||||
const actions = useMemo(() => {
|
||||
if (isLoading) {
|
||||
return <ActionsLoader />;
|
||||
return <RuleActionsSkeleton />;
|
||||
}
|
||||
|
||||
if (rulerRule) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { take, tap, withAbort } from 'ix/asynciterable/operators';
|
||||
import { useEffect, useRef, useState, useTransition } from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { Card, EmptyState, Stack, Text } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
@ -13,9 +12,7 @@ import { DataSourceRuleLoader } from './DataSourceRuleLoader';
|
||||
import { GrafanaRuleLoader } from './GrafanaRuleLoader';
|
||||
import LoadMoreHelper from './LoadMoreHelper';
|
||||
import { UnknownRuleListItem } from './components/AlertRuleListItem';
|
||||
import { ListItem } from './components/ListItem';
|
||||
import { ActionsLoader } from './components/RuleActionsButtons.V2';
|
||||
import { RuleListIcon } from './components/RuleListIcon';
|
||||
import { AlertRuleListItemLoader } from './components/AlertRuleListItemLoader';
|
||||
import {
|
||||
GrafanaRuleWithOrigin,
|
||||
PromRuleWithOrigin,
|
||||
@ -123,7 +120,7 @@ function FilterViewResults({ filterState }: FilterViewProps) {
|
||||
<GrafanaRuleLoader
|
||||
key={key}
|
||||
rule={rule}
|
||||
groupName={groupIdentifier.groupName}
|
||||
groupIdentifier={groupIdentifier}
|
||||
namespaceName={ruleWithOrigin.namespaceName}
|
||||
/>
|
||||
);
|
||||
@ -149,21 +146,11 @@ function FilterViewResults({ filterState }: FilterViewProps) {
|
||||
</Text>
|
||||
</Card>
|
||||
)}
|
||||
{!doneSearching && <LoadMoreHelper handleLoad={loadResultPage} />}
|
||||
{!doneSearching && !loading && <LoadMoreHelper handleLoad={loadResultPage} />}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const AlertRuleListItemLoader = () => (
|
||||
<ListItem
|
||||
title={<Skeleton width={64} />}
|
||||
icon={<RuleListIcon isPaused={false} />}
|
||||
description={<Skeleton width={256} />}
|
||||
actions={<ActionsLoader />}
|
||||
data-testid="alert-rule-list-item-loader"
|
||||
/>
|
||||
);
|
||||
|
||||
// simple helper function to detect the end of the source async iterable
|
||||
function onFinished<T>(fn: () => void) {
|
||||
return tap<T>(undefined, undefined, fn);
|
||||
|
@ -1,56 +1,70 @@
|
||||
import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
import { GrafanaPromRuleDTO, PromRuleType } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { alertRuleApi } from '../api/alertRuleApi';
|
||||
import { GrafanaRulesSource } from '../utils/datasource';
|
||||
import { createRelativeUrl } from '../utils/url';
|
||||
|
||||
import { AlertRuleListItem, RecordingRuleListItem, UnknownRuleListItem } from './components/AlertRuleListItem';
|
||||
import { AlertRuleListItemLoader, RulerRuleLoadingError } from './components/AlertRuleListItemLoader';
|
||||
import { RuleActionsButtons } from './components/RuleActionsButtons.V2';
|
||||
|
||||
const { useGetGrafanaRulerGroupQuery } = alertRuleApi;
|
||||
|
||||
interface GrafanaRuleLoaderProps {
|
||||
rule: GrafanaPromRuleDTO;
|
||||
groupName: string;
|
||||
groupIdentifier: GrafanaRuleGroupIdentifier;
|
||||
namespaceName: string;
|
||||
}
|
||||
|
||||
export function GrafanaRuleLoader({ rule, groupName, namespaceName }: GrafanaRuleLoaderProps) {
|
||||
const { folderUid } = rule;
|
||||
export function GrafanaRuleLoader({ rule, groupIdentifier, namespaceName }: GrafanaRuleLoaderProps) {
|
||||
const { data: rulerRuleGroup, isError } = useGetGrafanaRulerGroupQuery(groupIdentifier);
|
||||
|
||||
const rulerRule = rulerRuleGroup?.rules.find((rulerRule) => rulerRule.grafana_alert.uid === rule.uid);
|
||||
|
||||
if (!rulerRule) {
|
||||
if (isError) {
|
||||
return <RulerRuleLoadingError rule={rule} />;
|
||||
}
|
||||
|
||||
return <AlertRuleListItemLoader />;
|
||||
}
|
||||
|
||||
const {
|
||||
grafana_alert: { title, provenance, is_paused },
|
||||
annotations = {},
|
||||
labels = {},
|
||||
} = rulerRule;
|
||||
|
||||
const commonProps = {
|
||||
name: rule.name,
|
||||
name: title,
|
||||
rulesSource: GrafanaRulesSource,
|
||||
group: groupName,
|
||||
group: groupIdentifier.groupName,
|
||||
namespace: namespaceName,
|
||||
href: createRelativeUrl(`/alerting/grafana/${rule.uid}/view`),
|
||||
health: rule.health,
|
||||
error: rule.lastError,
|
||||
labels: rule.labels,
|
||||
labels: labels,
|
||||
isProvisioned: Boolean(provenance),
|
||||
isPaused: is_paused,
|
||||
application: 'grafana' as const,
|
||||
actions: <RuleActionsButtons rule={rulerRule} promRule={rule} groupIdentifier={groupIdentifier} compact />,
|
||||
};
|
||||
|
||||
if (rule.type === PromRuleType.Alerting) {
|
||||
return (
|
||||
<AlertRuleListItem
|
||||
{...commonProps}
|
||||
application="grafana"
|
||||
summary={rule.annotations?.summary}
|
||||
summary={annotations.summary}
|
||||
state={rule.state}
|
||||
isProvisioned={undefined}
|
||||
instancesCount={rule.alerts?.length}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (rule.type === PromRuleType.Recording) {
|
||||
return <RecordingRuleListItem {...commonProps} application="grafana" isProvisioned={undefined} />;
|
||||
return <RecordingRuleListItem {...commonProps} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<UnknownRuleListItem
|
||||
rule={rule}
|
||||
groupIdentifier={{
|
||||
rulesSource: GrafanaRulesSource,
|
||||
groupName,
|
||||
namespace: { uid: folderUid },
|
||||
groupOrigin: 'grafana',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <UnknownRuleListItem rule={rule} groupIdentifier={groupIdentifier} />;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { groupBy } from 'lodash';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { Icon, Stack, Text } from '@grafana/ui';
|
||||
import { GrafanaRulesSourceSymbol } from 'app/types/unified-alerting';
|
||||
import { GrafanaRuleGroupIdentifier, GrafanaRulesSourceSymbol } from 'app/types/unified-alerting';
|
||||
import { GrafanaPromRuleGroupDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { GrafanaRuleLoader } from './GrafanaRuleLoader';
|
||||
@ -83,10 +83,25 @@ interface GrafanaRuleGroupListItemProps {
|
||||
namespaceName: string;
|
||||
}
|
||||
export function GrafanaRuleGroupListItem({ group, namespaceName }: GrafanaRuleGroupListItemProps) {
|
||||
const groupIdentifier: GrafanaRuleGroupIdentifier = {
|
||||
groupName: group.name,
|
||||
namespace: {
|
||||
uid: group.folderUid,
|
||||
},
|
||||
groupOrigin: 'grafana',
|
||||
};
|
||||
|
||||
return (
|
||||
<ListGroup key={group.name} name={group.name} isOpen={false} actions={<RuleGroupActionsMenu />}>
|
||||
{group.rules.map((rule) => {
|
||||
return <GrafanaRuleLoader key={rule.uid} rule={rule} groupName={group.name} namespaceName={namespaceName} />;
|
||||
return (
|
||||
<GrafanaRuleLoader
|
||||
key={rule.uid}
|
||||
rule={rule}
|
||||
namespaceName={namespaceName}
|
||||
groupIdentifier={groupIdentifier}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ListGroup>
|
||||
);
|
||||
|
@ -16,7 +16,8 @@ import { hashRule } from '../utils/rule-id';
|
||||
import { getRulePluginOrigin, isAlertingRule, isGrafanaRulerRule } from '../utils/rules';
|
||||
|
||||
import { AlertRuleListItem } from './components/AlertRuleListItem';
|
||||
import { ActionsLoader, RuleActionsButtons } from './components/RuleActionsButtons.V2';
|
||||
import { RuleActionsButtons } from './components/RuleActionsButtons.V2';
|
||||
import { RuleActionsSkeleton } from './components/RuleActionsSkeleton';
|
||||
|
||||
interface Props {
|
||||
namespaces: CombinedRuleNamespace[];
|
||||
@ -124,7 +125,7 @@ const RulesByState = ({ state, rules }: { state: PromAlertingRuleState; rules: C
|
||||
rule.rulerRule ? (
|
||||
<RuleActionsButtons compact rule={rule.rulerRule} promRule={promRule} groupIdentifier={groupId} />
|
||||
) : (
|
||||
<ActionsLoader />
|
||||
<RuleActionsSkeleton />
|
||||
)
|
||||
}
|
||||
origin={originMeta}
|
||||
|
@ -5,13 +5,7 @@ import { ReactNode, useEffect } from 'react';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, Icon, Stack, Text, TextLink, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import {
|
||||
GrafanaRulesSourceSymbol,
|
||||
Rule,
|
||||
RuleGroupIdentifierV2,
|
||||
RuleHealth,
|
||||
RulesSourceIdentifier,
|
||||
} from 'app/types/unified-alerting';
|
||||
import { Rule, RuleGroupIdentifierV2, RuleHealth, RulesSourceIdentifier } from 'app/types/unified-alerting';
|
||||
import { Labels, PromAlertingRuleState, RulesSourceApplication } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { logError } from '../../Analytics';
|
||||
@ -19,6 +13,7 @@ import { MetaText } from '../../components/MetaText';
|
||||
import { ProvisioningBadge } from '../../components/Provisioning';
|
||||
import { PluginOriginBadge } from '../../plugins/PluginOriginBadge';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { getGroupOriginName } from '../../utils/groupIdentifier';
|
||||
import { labelsSize } from '../../utils/labels';
|
||||
import { createContactPointSearchLink } from '../../utils/misc';
|
||||
import { RulePluginOrigin } from '../../utils/rules';
|
||||
@ -268,12 +263,12 @@ export const UnknownRuleListItem = ({ rule, groupIdentifier }: UnknownRuleListIt
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
useEffect(() => {
|
||||
const { rulesSource, namespace, groupName } = groupIdentifier;
|
||||
const { namespace, groupName } = groupIdentifier;
|
||||
const ruleContext = {
|
||||
name: rule.name,
|
||||
groupName,
|
||||
namespace: JSON.stringify(namespace),
|
||||
rulesSource: rulesSource.uid === GrafanaRulesSourceSymbol ? GRAFANA_RULES_SOURCE_NAME : rulesSource.uid,
|
||||
rulesSource: getGroupOriginName(groupIdentifier),
|
||||
};
|
||||
logError(new Error('unknown rule type'), ruleContext);
|
||||
}, [rule, groupIdentifier]);
|
||||
|
@ -0,0 +1,30 @@
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { PromRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { ListItem } from './ListItem';
|
||||
import { RuleActionsSkeleton } from './RuleActionsSkeleton';
|
||||
import { RuleListIcon } from './RuleListIcon';
|
||||
|
||||
export function AlertRuleListItemLoader() {
|
||||
return (
|
||||
<ListItem
|
||||
title={<Skeleton width={64} />}
|
||||
icon={<RuleListIcon isPaused={false} />}
|
||||
description={<Skeleton width={256} />}
|
||||
actions={<RuleActionsSkeleton />}
|
||||
data-testid="alert-rule-list-item-loader"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function RulerRuleLoadingError({ rule }: { rule: PromRuleDTO }) {
|
||||
return (
|
||||
<ListItem
|
||||
title={rule.name}
|
||||
description={t('alerting.rule-list.rulerrule-loading-error', 'Failed to load the rule')}
|
||||
data-testid="ruler-rule-loading-error"
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,21 +1,15 @@
|
||||
import { useState } from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { LinkButton, Stack } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import AlertRuleMenu from 'app/features/alerting/unified/components/rule-viewer/AlertRuleMenu';
|
||||
import { useDeleteModal } from 'app/features/alerting/unified/components/rule-viewer/DeleteModal';
|
||||
import { RedirectToCloneRule } from 'app/features/alerting/unified/components/rules/CloneRule';
|
||||
import { INSTANCES_DISPLAY_LIMIT } from 'app/features/alerting/unified/components/rules/RuleDetails';
|
||||
import SilenceGrafanaRuleDrawer from 'app/features/alerting/unified/components/silences/SilenceGrafanaRuleDrawer';
|
||||
import { useRulesFilter } from 'app/features/alerting/unified/hooks/useFilteredRules';
|
||||
import { useDispatch } from 'app/types';
|
||||
import { Rule, RuleGroupIdentifierV2, RuleIdentifier } from 'app/types/unified-alerting';
|
||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { AlertRuleAction, useRulerRuleAbility } from '../../hooks/useAbilities';
|
||||
import { fetchPromAndRulerRulesAction } from '../../state/actions';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { isGrafanaAlertingRule, isGrafanaRulerRule } from '../../utils/rules';
|
||||
import { createRelativeUrl } from '../../utils/url';
|
||||
@ -34,8 +28,6 @@ interface Props {
|
||||
// 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
|
||||
export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }: Props) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const redirectToListView = compact ? false : true;
|
||||
const [deleteModal, showDeleteModal] = useDeleteModal(redirectToListView);
|
||||
|
||||
@ -45,8 +37,6 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }:
|
||||
{ identifier: RuleIdentifier; isProvisioned: boolean } | undefined
|
||||
>(undefined);
|
||||
|
||||
const { hasActiveFilters } = useRulesFilter();
|
||||
|
||||
const isProvisioned = isGrafanaRulerRule(rule) && Boolean(rule.grafana_alert.provenance);
|
||||
|
||||
const [editRuleSupported, editRuleAllowed] = useRulerRuleAbility(rule, groupIdentifier, AlertRuleAction.Update);
|
||||
@ -77,18 +67,9 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }:
|
||||
promRule={promRule}
|
||||
groupIdentifier={groupIdentifier}
|
||||
identifier={identifier}
|
||||
handleDelete={() => showDeleteModal(rule, groupIdentifier)}
|
||||
handleDelete={() => showDeleteModal(identifier, groupIdentifier)}
|
||||
handleSilence={() => setShowSilenceDrawer(true)}
|
||||
handleDuplicateRule={() => setRedirectToClone({ identifier, isProvisioned })}
|
||||
onPauseChange={() => {
|
||||
// Uses INSTANCES_DISPLAY_LIMIT + 1 here as exporting LIMIT_ALERTS from RuleList has the side effect
|
||||
// of breaking some unrelated tests in Policy.test.tsx due to mocking approach
|
||||
const limitAlerts = hasActiveFilters ? undefined : INSTANCES_DISPLAY_LIMIT + 1;
|
||||
// Trigger a re-fetch of the rules table
|
||||
// TODO: Migrate rules table functionality to RTK Query, so we instead rely
|
||||
// on tag invalidation (or optimistic cache updates) for this
|
||||
dispatch(fetchPromAndRulerRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME, limitAlerts }));
|
||||
}}
|
||||
/>
|
||||
{deleteModal}
|
||||
{isGrafanaAlertingRule(rule) && showSilenceDrawer && (
|
||||
@ -104,5 +85,3 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }:
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export const ActionsLoader = () => <Skeleton width={50} height={16} />;
|
||||
|
@ -0,0 +1,5 @@
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
export function RuleActionsSkeleton() {
|
||||
return <Skeleton width={50} height={16} />;
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { memo } from 'react';
|
||||
import type { RequireAtLeastOne } from 'type-fest';
|
||||
|
||||
import { Icon, type IconName, Text, Tooltip } from '@grafana/ui';
|
||||
@ -14,33 +15,34 @@ interface RuleListIconProps {
|
||||
isPaused?: boolean;
|
||||
}
|
||||
|
||||
const icons: Record<PromAlertingRuleState, IconName> = {
|
||||
[PromAlertingRuleState.Inactive]: 'check-circle',
|
||||
[PromAlertingRuleState.Pending]: 'circle',
|
||||
[PromAlertingRuleState.Firing]: 'exclamation-circle',
|
||||
};
|
||||
|
||||
const color: Record<PromAlertingRuleState, 'success' | 'error' | 'warning'> = {
|
||||
[PromAlertingRuleState.Inactive]: 'success',
|
||||
[PromAlertingRuleState.Pending]: 'warning',
|
||||
[PromAlertingRuleState.Firing]: 'error',
|
||||
};
|
||||
|
||||
const stateNames: Record<PromAlertingRuleState, string> = {
|
||||
[PromAlertingRuleState.Inactive]: 'Normal',
|
||||
[PromAlertingRuleState.Pending]: 'Pending',
|
||||
[PromAlertingRuleState.Firing]: 'Firing',
|
||||
};
|
||||
|
||||
/**
|
||||
* Make sure that the order of importance here matches the one we use in the StateBadge component for the detail view
|
||||
* This component is often rendered tens or hundreds of times in a single page, so it's performance is important
|
||||
*/
|
||||
export function RuleListIcon({
|
||||
export const RuleListIcon = memo(function RuleListIcon({
|
||||
state,
|
||||
health,
|
||||
recording = false,
|
||||
isPaused = false,
|
||||
}: RequireAtLeastOne<RuleListIconProps>) {
|
||||
const icons: Record<PromAlertingRuleState, IconName> = {
|
||||
[PromAlertingRuleState.Inactive]: 'check-circle',
|
||||
[PromAlertingRuleState.Pending]: 'circle',
|
||||
[PromAlertingRuleState.Firing]: 'exclamation-circle',
|
||||
};
|
||||
|
||||
const color: Record<PromAlertingRuleState, 'success' | 'error' | 'warning'> = {
|
||||
[PromAlertingRuleState.Inactive]: 'success',
|
||||
[PromAlertingRuleState.Pending]: 'warning',
|
||||
[PromAlertingRuleState.Firing]: 'error',
|
||||
};
|
||||
|
||||
const stateNames: Record<PromAlertingRuleState, string> = {
|
||||
[PromAlertingRuleState.Inactive]: 'Normal',
|
||||
[PromAlertingRuleState.Pending]: 'Pending',
|
||||
[PromAlertingRuleState.Firing]: 'Firing',
|
||||
};
|
||||
|
||||
let iconName: IconName = state ? icons[state] : 'circle';
|
||||
let iconColor: TextProps['color'] = state ? color[state] : 'secondary';
|
||||
let stateName: string = state ? stateNames[state] : 'unknown';
|
||||
@ -78,4 +80,4 @@ export function RuleListIcon({
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
import { RulesFilter } from '../../search/rulesSearchParser';
|
||||
import { labelsMatchMatchers } from '../../utils/alertmanager';
|
||||
import { Annotation } from '../../utils/constants';
|
||||
import { GrafanaRulesSource, getDatasourceAPIUid, getExternalRulesSources } from '../../utils/datasource';
|
||||
import { getDatasourceAPIUid, getExternalRulesSources } from '../../utils/datasource';
|
||||
import { parseMatcher } from '../../utils/matchers';
|
||||
import { isAlertingRule } from '../../utils/rules';
|
||||
|
||||
@ -109,7 +109,6 @@ function mapGrafanaRuleToRuleWithOrigin(
|
||||
return {
|
||||
rule,
|
||||
groupIdentifier: {
|
||||
rulesSource: GrafanaRulesSource,
|
||||
namespace: { uid: group.folderUid },
|
||||
groupName: group.name,
|
||||
groupOrigin: 'grafana',
|
||||
|
@ -0,0 +1,44 @@
|
||||
import { RuleGroupIdentifier, RuleGroupIdentifierV2 } from 'app/types/unified-alerting';
|
||||
|
||||
import { ruleGroupIdentifierV2toV1 } from './groupIdentifier';
|
||||
|
||||
describe('ruleGroupIdentifierV2toV1', () => {
|
||||
it('should convert grafana v2 rule group identifier to v1 format', () => {
|
||||
const identifier: RuleGroupIdentifierV2 = {
|
||||
groupName: 'group-1',
|
||||
namespace: {
|
||||
uid: 'uid123',
|
||||
},
|
||||
groupOrigin: 'grafana',
|
||||
};
|
||||
|
||||
const result = ruleGroupIdentifierV2toV1(identifier);
|
||||
expect(result).toStrictEqual<RuleGroupIdentifier>({
|
||||
dataSourceName: 'grafana',
|
||||
groupName: 'group-1',
|
||||
namespaceName: 'uid123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert data source v2 rule group identifier to v1 format', () => {
|
||||
const identifier: RuleGroupIdentifierV2 = {
|
||||
groupName: 'group-1',
|
||||
namespace: {
|
||||
name: 'namespace-1',
|
||||
},
|
||||
rulesSource: {
|
||||
uid: 'ds-uid123',
|
||||
name: 'ds-name',
|
||||
ruleSourceType: 'datasource',
|
||||
},
|
||||
groupOrigin: 'datasource',
|
||||
};
|
||||
|
||||
const result = ruleGroupIdentifierV2toV1(identifier);
|
||||
expect(result).toStrictEqual<RuleGroupIdentifier>({
|
||||
dataSourceName: 'ds-name',
|
||||
groupName: 'group-1',
|
||||
namespaceName: 'namespace-1',
|
||||
});
|
||||
});
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
import { CombinedRule, GrafanaRulesSourceSymbol, RuleGroupIdentifierV2 } from 'app/types/unified-alerting';
|
||||
import { CombinedRule, RuleGroupIdentifier, RuleGroupIdentifierV2 } from 'app/types/unified-alerting';
|
||||
|
||||
import { GRAFANA_RULES_SOURCE_NAME, getDatasourceAPIUid, getRulesSourceName, isGrafanaRulesSource } from './datasource';
|
||||
import { isGrafanaRulerRule } from './rules';
|
||||
@ -6,7 +6,6 @@ import { isGrafanaRulerRule } from './rules';
|
||||
function fromCombinedRule(rule: CombinedRule): RuleGroupIdentifierV2 {
|
||||
if (isGrafanaRulerRule(rule.rulerRule) && isGrafanaRulesSource(rule.namespace.rulesSource)) {
|
||||
return {
|
||||
rulesSource: { uid: GrafanaRulesSourceSymbol, name: GRAFANA_RULES_SOURCE_NAME, ruleSourceType: 'grafana' },
|
||||
namespace: { uid: rule.rulerRule.grafana_alert.namespace_uid },
|
||||
groupName: rule.group.name,
|
||||
groupOrigin: 'grafana',
|
||||
@ -23,6 +22,21 @@ function fromCombinedRule(rule: CombinedRule): RuleGroupIdentifierV2 {
|
||||
};
|
||||
}
|
||||
|
||||
export function getGroupOriginName(groupIdentifier: RuleGroupIdentifierV2) {
|
||||
return groupIdentifier.groupOrigin === 'grafana' ? GRAFANA_RULES_SOURCE_NAME : groupIdentifier.rulesSource.name;
|
||||
}
|
||||
|
||||
/** Helper function to convert RuleGroupIdentifier to RuleGroupIdentifierV2 */
|
||||
export function ruleGroupIdentifierV2toV1(groupIdentifier: RuleGroupIdentifierV2): RuleGroupIdentifier {
|
||||
const rulesSourceName = getGroupOriginName(groupIdentifier);
|
||||
|
||||
return {
|
||||
dataSourceName: rulesSourceName,
|
||||
namespaceName: 'uid' in groupIdentifier.namespace ? groupIdentifier.namespace.uid : groupIdentifier.namespace.name,
|
||||
groupName: groupIdentifier.groupName,
|
||||
};
|
||||
}
|
||||
|
||||
export const groupIdentifier = {
|
||||
fromCombinedRule,
|
||||
};
|
||||
|
@ -209,7 +209,6 @@ export interface DataSourceNamespaceIdentifier {
|
||||
}
|
||||
|
||||
export interface GrafanaRuleGroupIdentifier {
|
||||
rulesSource: GrafanaRulesSourceIdentifier;
|
||||
groupName: string;
|
||||
namespace: GrafanaNamespaceIdentifier;
|
||||
groupOrigin: 'grafana';
|
||||
@ -310,6 +309,7 @@ export interface StateHistoryItem {
|
||||
|
||||
export interface RulerDataSourceConfig {
|
||||
dataSourceName: string;
|
||||
dataSourceUid: string;
|
||||
apiVersion: 'legacy' | 'config';
|
||||
}
|
||||
|
||||
|
@ -506,7 +506,8 @@
|
||||
},
|
||||
"return-button": {
|
||||
"title": "Alert rules"
|
||||
}
|
||||
},
|
||||
"rulerrule-loading-error": "Failed to load the rule"
|
||||
},
|
||||
"rule-state": {
|
||||
"creating": "Creating",
|
||||
|
@ -506,7 +506,8 @@
|
||||
},
|
||||
"return-button": {
|
||||
"title": "Åľęřŧ řūľęş"
|
||||
}
|
||||
},
|
||||
"rulerrule-loading-error": "Fäįľęđ ŧő ľőäđ ŧĥę řūľę"
|
||||
},
|
||||
"rule-state": {
|
||||
"creating": "Cřęäŧįʼnģ",
|
||||
|
Loading…
Reference in New Issue
Block a user