diff --git a/public/app/features/alerting/unified/api/alertRuleApi.ts b/public/app/features/alerting/unified/api/alertRuleApi.ts index 329b3fbe79d..f483df956e7 100644 --- a/public/app/features/alerting/unified/api/alertRuleApi.ts +++ b/public/app/features/alerting/unified/api/alertRuleApi.ts @@ -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, 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), diff --git a/public/app/features/alerting/unified/api/featureDiscoveryApi.ts b/public/app/features/alerting/unified/api/featureDiscoveryApi.ts index 82e55c1875a..a0d40e6c874 100644 --- a/public/app/features/alerting/unified/api/featureDiscoveryApi.ts +++ b/public/app/features/alerting/unified/api/featureDiscoveryApi.ts @@ -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; diff --git a/public/app/features/alerting/unified/api/ruler.test.ts b/public/app/features/alerting/unified/api/ruler.test.ts index a4f69e49ee4..350c267268d 100644 --- a/public/app/features/alerting/unified/api/ruler.test.ts +++ b/public/app/features/alerting/unified/api/ruler.test.ts @@ -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'); diff --git a/public/app/features/alerting/unified/api/ruler.ts b/public/app/features/alerting/unified/api/ruler.ts index bcda1b7df74..b2a1e8761ae 100644 --- a/public/app/features/alerting/unified/api/ruler.ts +++ b/public/app/features/alerting/unified/api/ruler.ts @@ -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`; } diff --git a/public/app/features/alerting/unified/components/MenuItemPauseRule.tsx b/public/app/features/alerting/unified/components/MenuItemPauseRule.tsx index e6d170b0506..c2c800c08e2 100644 --- a/public/app/features/alerting/unified/components/MenuItemPauseRule.tsx +++ b/public/app/features/alerting/unified/components/MenuItemPauseRule.tsx @@ -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); }} /> ); diff --git a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx index e5502ca14da..d8d7641b2ce 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx @@ -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' && ( )} {canSilence && } @@ -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 ; }; +function copyToClipboard(text: string) { + navigator.clipboard?.writeText(text).then(() => { + appEvents.emit(AppEvents.alertSuccess, ['URL copied to clipboard']); + }); +} + export default AlertRuleMenu; diff --git a/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx b/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx index 6586d1c9088..c408a4fc37e 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx @@ -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(); diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx index 9efe62efaf4..9f7d4710520 100644 --- a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx @@ -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)} diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/usePauseAlertRule.ts b/public/app/features/alerting/unified/hooks/ruleGroup/usePauseAlertRule.ts index 2788f8d7011..9f06bf38bfd 100644 --- a/public/app/features/alerting/unified/hooks/ruleGroup/usePauseAlertRule.ts +++ b/public/app/features/alerting/unified/hooks/ruleGroup/usePauseAlertRule.ts @@ -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); diff --git a/public/app/features/alerting/unified/hooks/useAbilities.ts b/public/app/features/alerting/unified/hooks/useAbilities.ts index 27f0fa0e178..f7a623a2c64 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.ts +++ b/public/app/features/alerting/unified/hooks/useAbilities.ts @@ -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 { - 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 { + const { folder } = useFolder(folderUID); + return folder?.accessControl ?? {}; } // just a convenient function diff --git a/public/app/features/alerting/unified/rule-list/DataSourceRuleLoader.tsx b/public/app/features/alerting/unified/rule-list/DataSourceRuleLoader.tsx index 234d241bcdc..d041b42566d 100644 --- a/public/app/features/alerting/unified/rule-list/DataSourceRuleLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/DataSourceRuleLoader.tsx @@ -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 ; + return ; } if (rulerRule) { diff --git a/public/app/features/alerting/unified/rule-list/FilterView.tsx b/public/app/features/alerting/unified/rule-list/FilterView.tsx index 41414d88461..25358a57390 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.tsx @@ -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) { ); @@ -149,21 +146,11 @@ function FilterViewResults({ filterState }: FilterViewProps) { )} - {!doneSearching && } + {!doneSearching && !loading && } ); } -const AlertRuleListItemLoader = () => ( - } - icon={} - description={} - actions={} - data-testid="alert-rule-list-item-loader" - /> -); - // simple helper function to detect the end of the source async iterable function onFinished(fn: () => void) { return tap(undefined, undefined, fn); diff --git a/public/app/features/alerting/unified/rule-list/GrafanaRuleLoader.tsx b/public/app/features/alerting/unified/rule-list/GrafanaRuleLoader.tsx index a572e29148f..f4d127feaa5 100644 --- a/public/app/features/alerting/unified/rule-list/GrafanaRuleLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/GrafanaRuleLoader.tsx @@ -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 ; + } + + return ; + } + + 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: , }; if (rule.type === PromRuleType.Alerting) { return ( ); } if (rule.type === PromRuleType.Recording) { - return ; + return ; } - return ( - - ); + return ; } diff --git a/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx b/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx index 3f9cd0814cd..f9daf26d6b6 100644 --- a/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx @@ -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 ( }> {group.rules.map((rule) => { - return ; + return ( + + ); })} ); diff --git a/public/app/features/alerting/unified/rule-list/StateView.tsx b/public/app/features/alerting/unified/rule-list/StateView.tsx index 6f87188fb79..d68edb6d3d0 100644 --- a/public/app/features/alerting/unified/rule-list/StateView.tsx +++ b/public/app/features/alerting/unified/rule-list/StateView.tsx @@ -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 ? ( ) : ( - + ) } origin={originMeta} diff --git a/public/app/features/alerting/unified/rule-list/components/AlertRuleListItem.tsx b/public/app/features/alerting/unified/rule-list/components/AlertRuleListItem.tsx index be4e51eaed3..38b6c50d2db 100644 --- a/public/app/features/alerting/unified/rule-list/components/AlertRuleListItem.tsx +++ b/public/app/features/alerting/unified/rule-list/components/AlertRuleListItem.tsx @@ -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]); diff --git a/public/app/features/alerting/unified/rule-list/components/AlertRuleListItemLoader.tsx b/public/app/features/alerting/unified/rule-list/components/AlertRuleListItemLoader.tsx new file mode 100644 index 00000000000..ad0db11991d --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/components/AlertRuleListItemLoader.tsx @@ -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 ( + } + icon={} + description={} + actions={} + data-testid="alert-rule-list-item-loader" + /> + ); +} + +export function RulerRuleLoadingError({ rule }: { rule: PromRuleDTO }) { + return ( + + ); +} diff --git a/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx b/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx index 259f3038081..5c27ea48476 100644 --- a/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx +++ b/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx @@ -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 }: ); } - -export const ActionsLoader = () => ; diff --git a/public/app/features/alerting/unified/rule-list/components/RuleActionsSkeleton.tsx b/public/app/features/alerting/unified/rule-list/components/RuleActionsSkeleton.tsx new file mode 100644 index 00000000000..cd4ef723bdd --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/components/RuleActionsSkeleton.tsx @@ -0,0 +1,5 @@ +import Skeleton from 'react-loading-skeleton'; + +export function RuleActionsSkeleton() { + return ; +} diff --git a/public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx b/public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx index 936a0d113c4..cb543108054 100644 --- a/public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx +++ b/public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx @@ -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.Inactive]: 'check-circle', + [PromAlertingRuleState.Pending]: 'circle', + [PromAlertingRuleState.Firing]: 'exclamation-circle', +}; + +const color: Record = { + [PromAlertingRuleState.Inactive]: 'success', + [PromAlertingRuleState.Pending]: 'warning', + [PromAlertingRuleState.Firing]: 'error', +}; + +const stateNames: Record = { + [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) { - const icons: Record = { - [PromAlertingRuleState.Inactive]: 'check-circle', - [PromAlertingRuleState.Pending]: 'circle', - [PromAlertingRuleState.Firing]: 'exclamation-circle', - }; - - const color: Record = { - [PromAlertingRuleState.Inactive]: 'success', - [PromAlertingRuleState.Pending]: 'warning', - [PromAlertingRuleState.Firing]: 'error', - }; - - const stateNames: Record = { - [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({ ); -} +}); diff --git a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts index 69eac1b63a7..ddbfea8e216 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts @@ -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', diff --git a/public/app/features/alerting/unified/utils/groupIdentifier.test.tsx b/public/app/features/alerting/unified/utils/groupIdentifier.test.tsx new file mode 100644 index 00000000000..4a9846de785 --- /dev/null +++ b/public/app/features/alerting/unified/utils/groupIdentifier.test.tsx @@ -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({ + 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({ + dataSourceName: 'ds-name', + groupName: 'group-1', + namespaceName: 'namespace-1', + }); + }); +}); diff --git a/public/app/features/alerting/unified/utils/groupIdentifier.ts b/public/app/features/alerting/unified/utils/groupIdentifier.ts index 1474b421c31..61ae10d49ab 100644 --- a/public/app/features/alerting/unified/utils/groupIdentifier.ts +++ b/public/app/features/alerting/unified/utils/groupIdentifier.ts @@ -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, }; diff --git a/public/app/types/unified-alerting.ts b/public/app/types/unified-alerting.ts index 0e956d2cacf..32ce7621f8f 100644 --- a/public/app/types/unified-alerting.ts +++ b/public/app/types/unified-alerting.ts @@ -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'; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 9f656cef575..6181dc87999 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -506,7 +506,8 @@ }, "return-button": { "title": "Alert rules" - } + }, + "rulerrule-loading-error": "Failed to load the rule" }, "rule-state": { "creating": "Creating", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 17ac962fc40..e5fff220ff6 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -506,7 +506,8 @@ }, "return-button": { "title": "Åľęřŧ řūľęş" - } + }, + "rulerrule-loading-error": "Fäįľęđ ŧő ľőäđ ŧĥę řūľę" }, "rule-state": { "creating": "Cřęäŧįʼnģ",