+
toggle()}
+ isOpen={open}
description={description}
name={name}
metaRight={metaRight}
actions={actions}
/>
- {isOpen && {children}
}
+ {open && {children}
}
);
};
-const GroupHeader = (props: GroupProps) => {
+type GroupHeaderProps = GroupProps & {
+ onToggle: () => void;
+};
+
+const GroupHeader = (props: GroupHeaderProps) => {
const { name, description, metaRight = null, actions = null, isOpen = false, onToggle } = props;
const styles = useStyles2(getStyles);
@@ -50,9 +54,9 @@ const GroupHeader = (props: GroupProps) => {
return (
-
+
@@ -76,7 +80,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
flexDirection: 'column',
}),
headerWrapper: css({
- padding: `${theme.spacing(1)} ${theme.spacing(1.5)}`,
+ padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`,
background: theme.colors.background.secondary,
diff --git a/public/app/features/alerting/unified/rule-list/components/ListSection.tsx b/public/app/features/alerting/unified/rule-list/components/ListSection.tsx
index 1e9739f81b6..6a2cd961d69 100644
--- a/public/app/features/alerting/unified/rule-list/components/ListSection.tsx
+++ b/public/app/features/alerting/unified/rule-list/components/ListSection.tsx
@@ -30,7 +30,7 @@ export const ListSection = ({
-
+
({
border: `solid 1px ${theme.colors.border.weak}`,
borderBottom: 'none',
- marginLeft: theme.spacing(3),
-
- '&:before': {
- content: "''",
- position: 'absolute',
- height: '100%',
-
- borderLeft: `solid 1px ${theme.colors.border.weak}`,
-
- marginTop: 0,
- marginLeft: `-${theme.spacing(2.5)}`,
- },
+ marginLeft: theme.spacing(1.5),
}),
wrapper: css({
display: 'flex',
@@ -88,7 +77,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`,
background: theme.colors.background.secondary,
-
border: `solid 1px ${theme.colors.border.weak}`,
borderRadius: theme.shape.radius.default,
}),
diff --git a/public/app/features/alerting/unified/rule-list/components/Namespace.tsx b/public/app/features/alerting/unified/rule-list/components/Namespace.tsx
index ff620eeee93..8d4f325e3b8 100644
--- a/public/app/features/alerting/unified/rule-list/components/Namespace.tsx
+++ b/public/app/features/alerting/unified/rule-list/components/Namespace.tsx
@@ -21,7 +21,7 @@ const Namespace = ({ children, name, href, application }: NamespaceProps) => {
-
+
{href ? (
{
+export const DataSourceIcon = ({ application }: NamespaceIconProps) => {
switch (application) {
case PromApplication.Prometheus:
return (
@@ -64,11 +64,11 @@ const NamespaceIcon = ({ application }: NamespaceIconProps) => {
return (
);
- case 'loki':
+ case 'Loki':
return
;
case 'grafana':
default:
- return ;
+ return ;
}
};
@@ -101,10 +101,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
namespaceTitle: css({
padding: `${theme.spacing(1)} ${theme.spacing(1.5)}`,
- background: theme.colors.background.secondary,
+ // background: theme.colors.background.secondary,
- border: `solid 1px ${theme.colors.border.weak}`,
- borderRadius: theme.shape.radius.default,
+ // border: `solid 1px ${theme.colors.border.weak}`,
+ // borderRadius: theme.shape.radius.default,
}),
});
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
new file mode 100644
index 00000000000..e560b464a09
--- /dev/null
+++ b/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx
@@ -0,0 +1,114 @@
+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 { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext';
+import { useDispatch } from 'app/types';
+import { Rule, RuleGroupIdentifier, 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';
+
+interface Props {
+ rule: RulerRuleDTO;
+ promRule: Rule;
+ groupIdentifier: RuleGroupIdentifier;
+ /**
+ * 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
+export const RuleActionsButtons = ({ compact, rule, promRule, groupIdentifier }: Props) => {
+ const dispatch = useDispatch();
+
+ const redirectToListView = compact ? false : true;
+ const [deleteModal, showDeleteModal] = useDeleteModal(redirectToListView);
+
+ const [showSilenceDrawer, setShowSilenceDrawer] = useState(false);
+
+ const [redirectToClone, setRedirectToClone] = useState<
+ { identifier: RuleIdentifier; isProvisioned: boolean } | undefined
+ >(undefined);
+
+ const { namespaceName, groupName, dataSourceName } = groupIdentifier;
+ const { hasActiveFilters } = useRulesFilter();
+
+ const isProvisioned = isGrafanaRulerRule(rule) && Boolean(rule.grafana_alert.provenance);
+
+ const [editRuleSupported, editRuleAllowed] = useRulerRuleAbility(rule, groupIdentifier, AlertRuleAction.Update);
+
+ const canEditRule = editRuleSupported && editRuleAllowed;
+
+ const buttons: JSX.Element[] = [];
+ const buttonSize = compact ? 'sm' : 'md';
+
+ const identifier = ruleId.fromRulerRule(dataSourceName, namespaceName, groupName, rule);
+
+ if (canEditRule) {
+ const identifier = ruleId.fromRulerRule(dataSourceName, namespaceName, groupName, rule);
+
+ const editURL = createRelativeUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`);
+
+ buttons.push(
+
+ Edit
+
+ );
+ }
+
+ return (
+
+ {buttons}
+ showDeleteModal(rule, 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 && (
+
+ setShowSilenceDrawer(false)} />
+
+ )}
+ {redirectToClone?.identifier && (
+ setRedirectToClone(undefined)}
+ />
+ )}
+
+ );
+};
+
+export const ActionsLoader = () => ;
diff --git a/public/app/features/alerting/unified/rule-list/components/RuleGroup.tsx b/public/app/features/alerting/unified/rule-list/components/RuleGroup.tsx
index 00a1108eba7..fb8a036128f 100644
--- a/public/app/features/alerting/unified/rule-list/components/RuleGroup.tsx
+++ b/public/app/features/alerting/unified/rule-list/components/RuleGroup.tsx
@@ -10,7 +10,7 @@ import { usePagination } from '../../hooks/usePagination';
import { isAlertingRule } from '../../utils/rules';
import { AlertRuleListItem } from './AlertRuleListItem';
-import EvaluationGroup from './EvaluationGroup';
+import { EvaluationGroup } from './EvaluationGroup';
import { SkeletonListItem } from './ListItem';
interface EvaluationGroupLoaderProps {
@@ -75,6 +75,16 @@ export const EvaluationGroupLoader = ({
);
};
+export const LoadingIndicator = () => {
+ const [ref, { width }] = useMeasure();
+
+ return (
+
+
+
+ );
+};
+
const GroupLoadingIndicator = () => {
const [ref, { width }] = useMeasure();
diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts
index 78ad5a54e2f..7c05181fd53 100644
--- a/public/app/features/alerting/unified/state/actions.ts
+++ b/public/app/features/alerting/unified/state/actions.ts
@@ -10,18 +10,12 @@ import {
Receiver,
TestReceiversAlert,
} from 'app/plugins/datasource/alertmanager/types';
-import { FolderDTO, StoreState, ThunkResult } from 'app/types';
-import {
- PromBasedDataSource,
- RuleIdentifier,
- RuleNamespace,
- RulerDataSourceConfig,
- StateHistoryItem,
-} from 'app/types/unified-alerting';
-import { PromApplication, RulerRuleDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
+import { FolderDTO, ThunkResult } from 'app/types';
+import { RuleIdentifier, RuleNamespace, StateHistoryItem } from 'app/types/unified-alerting';
+import { RulerRuleDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
import { backendSrv } from '../../../../core/services/backend_srv';
-import { withPerformanceLogging, withPromRulesMetadataLogging, withRulerRulesMetadataLogging } from '../Analytics';
+import { withPromRulesMetadataLogging, withRulerRulesMetadataLogging } from '../Analytics';
import {
deleteAlertManagerConfig,
fetchAlertGroups,
@@ -30,43 +24,16 @@ import {
} from '../api/alertmanager';
import { alertmanagerApi } from '../api/alertmanagerApi';
import { fetchAnnotations } from '../api/annotations';
-import { discoverFeatures } from '../api/buildInfo';
+import { featureDiscoveryApi } from '../api/featureDiscoveryApi';
import { FetchPromRulesFilter, fetchRules } from '../api/prometheus';
import { FetchRulerRulesFilter, fetchRulerRules } from '../api/ruler';
import { addDefaultsToAlertmanagerConfig } from '../utils/alertmanager';
-import { GRAFANA_RULES_SOURCE_NAME, getAllRulesSourceNames, getRulesDataSource } from '../utils/datasource';
+import { getAllRulesSourceNames } from '../utils/datasource';
import { makeAMLink } from '../utils/misc';
-import { AsyncRequestMapSlice, withAppEvents, withSerializedError } from '../utils/redux';
+import { withAppEvents, withSerializedError } from '../utils/redux';
import { getAlertInfo } from '../utils/rules';
import { safeParsePrometheusDuration } from '../utils/time';
-function getDataSourceConfig(getState: () => unknown, rulesSourceName: string) {
- const dataSources = (getState() as StoreState).unifiedAlerting.dataSources;
- const dsConfig = dataSources[rulesSourceName]?.result;
- const dsError = dataSources[rulesSourceName]?.error;
-
- // @TODO use aggregateError but add support for it in "stringifyErrorLike"
- if (!dsConfig) {
- const error = new Error(`Data source configuration is not available for "${rulesSourceName}" data source`);
- if (dsError) {
- error.cause = dsError;
- }
-
- throw error;
- }
-
- return dsConfig;
-}
-
-export function getDataSourceRulerConfig(getState: () => unknown, rulesSourceName: string) {
- const dsConfig = getDataSourceConfig(getState, rulesSourceName);
- if (!dsConfig.rulerConfig) {
- throw new Error(`Ruler API is not available for ${rulesSourceName}`);
- }
-
- return dsConfig.rulerConfig;
-}
-
export const fetchPromRulesAction = createAsyncThunk(
'unifiedalerting/fetchPromRules',
async (
@@ -87,8 +54,6 @@ export const fetchPromRulesAction = createAsyncThunk(
},
thunkAPI
): Promise => {
- await thunkAPI.dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName }));
-
const fetchRulesWithLogging = withPromRulesMetadataLogging('unifiedalerting/fetchPromRules', fetchRules, {
dataSourceName: rulesSourceName,
thunk: 'unifiedalerting/fetchPromRules',
@@ -112,8 +77,13 @@ export const fetchRulerRulesAction = createAsyncThunk(
},
{ dispatch, getState }
): Promise => {
- await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName }));
- const rulerConfig = getDataSourceRulerConfig(getState, rulesSourceName);
+ const { data: dsFeatures } = await dispatch(
+ featureDiscoveryApi.endpoints.discoverDsFeatures.initiate({ rulesSourceName })
+ );
+
+ if (!dsFeatures?.rulerConfig) {
+ return null;
+ }
const fetchRulerRulesWithLogging = withRulerRulesMetadataLogging(
'unifiedalerting/fetchRulerRules',
@@ -124,7 +94,7 @@ export const fetchRulerRulesAction = createAsyncThunk(
}
);
- return await withSerializedError(fetchRulerRulesWithLogging(rulerConfig, filter));
+ return await withSerializedError(fetchRulerRulesWithLogging(dsFeatures.rulerConfig, filter));
}
);
@@ -143,89 +113,18 @@ export function fetchPromAndRulerRulesAction({
matcher?: Matcher[];
state?: string[];
}): ThunkResult> {
- return async (dispatch, getState) => {
- await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName }));
- const dsConfig = getDataSourceConfig(getState, rulesSourceName);
-
- await dispatch(fetchPromRulesAction({ rulesSourceName, identifier, filter, limitAlerts, matcher, state }));
- if (dsConfig.rulerConfig) {
- await dispatch(fetchRulerRulesAction({ rulesSourceName }));
- }
- };
-}
-
-// TODO: memoize this or move to RTK Query so we can cache results!
-export function fetchAllPromBuildInfoAction(): ThunkResult> {
return async (dispatch) => {
- const allRequests = getAllRulesSourceNames().map((rulesSourceName) =>
- dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName }))
+ const { data: dsFeatures } = await dispatch(
+ featureDiscoveryApi.endpoints.discoverDsFeatures.initiate({ rulesSourceName })
);
- await Promise.allSettled(allRequests);
+ await Promise.all([
+ dispatch(fetchPromRulesAction({ rulesSourceName, identifier, filter, limitAlerts, matcher, state })),
+ dsFeatures?.rulerConfig ? dispatch(fetchRulerRulesAction({ rulesSourceName })) : Promise.resolve(),
+ ]);
};
}
-export const fetchRulesSourceBuildInfoAction = createAsyncThunk(
- 'unifiedalerting/fetchPromBuildinfo',
- async ({ rulesSourceName }: { rulesSourceName: string }): Promise => {
- return withSerializedError(
- (async (): Promise => {
- if (rulesSourceName === GRAFANA_RULES_SOURCE_NAME) {
- return {
- name: GRAFANA_RULES_SOURCE_NAME,
- id: GRAFANA_RULES_SOURCE_NAME,
- rulerConfig: {
- dataSourceName: GRAFANA_RULES_SOURCE_NAME,
- apiVersion: 'legacy',
- },
- };
- }
-
- const ds = getRulesDataSource(rulesSourceName);
- if (!ds) {
- throw new Error(`Missing data source configuration for ${rulesSourceName}`);
- }
-
- const { id, name } = ds;
-
- const discoverFeaturesWithLogging = withPerformanceLogging(
- 'unifiedalerting/fetchPromBuildinfo',
- discoverFeatures,
- {
- dataSourceName: rulesSourceName,
- thunk: 'unifiedalerting/fetchPromBuildinfo',
- }
- );
-
- const buildInfo = await discoverFeaturesWithLogging(name);
-
- const rulerConfig: RulerDataSourceConfig | undefined = buildInfo.features.rulerApiEnabled
- ? {
- dataSourceName: name,
- apiVersion: buildInfo.application === PromApplication.Cortex ? 'legacy' : 'config',
- }
- : undefined;
-
- return {
- name: name,
- id: id,
- rulerConfig,
- };
- })()
- );
- },
- {
- condition: ({ rulesSourceName }, { getState }) => {
- const dataSources: AsyncRequestMapSlice = (getState() as StoreState).unifiedAlerting
- .dataSources;
- const hasLoaded = Boolean(dataSources[rulesSourceName]?.result);
- const hasError = Boolean(dataSources[rulesSourceName]?.error);
-
- return !(hasLoaded || hasError);
- },
- }
-);
-
interface FetchPromRulesRulesActionProps {
filter?: FetchPromRulesFilter;
limitAlerts?: number;
@@ -242,18 +141,18 @@ export function fetchAllPromAndRulerRulesAction(
await Promise.allSettled(
getAllRulesSourceNames().map(async (rulesSourceName) => {
- await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName }));
+ const { data: dsFeatures } = await dispatch(
+ featureDiscoveryApi.endpoints.discoverDsFeatures.initiate({ rulesSourceName })
+ );
- const { promRules, rulerRules, dataSources } = getStore().unifiedAlerting;
- const dataSourceConfig = dataSources[rulesSourceName].result;
+ const { promRules, rulerRules } = getStore().unifiedAlerting;
- if (!dataSourceConfig) {
+ if (!dsFeatures) {
return;
}
const shouldLoadProm = force || !promRules[rulesSourceName]?.loading;
- const shouldLoadRuler =
- (force || !rulerRules[rulesSourceName]?.loading) && Boolean(dataSourceConfig.rulerConfig);
+ const shouldLoadRuler = (force || !rulerRules[rulesSourceName]?.loading) && Boolean(dsFeatures?.rulerConfig);
await Promise.allSettled([
shouldLoadProm && dispatch(fetchPromRulesAction({ rulesSourceName, ...options })),
diff --git a/public/app/features/alerting/unified/state/reducers.ts b/public/app/features/alerting/unified/state/reducers.ts
index d12154135d1..19153a3bee1 100644
--- a/public/app/features/alerting/unified/state/reducers.ts
+++ b/public/app/features/alerting/unified/state/reducers.ts
@@ -9,17 +9,11 @@ import {
fetchGrafanaAnnotationsAction,
fetchPromRulesAction,
fetchRulerRulesAction,
- fetchRulesSourceBuildInfoAction,
testReceiversAction,
updateAlertManagerConfigAction,
} from './actions';
export const reducer = combineReducers({
- dataSources: createAsyncMapSlice(
- 'dataSources',
- fetchRulesSourceBuildInfoAction,
- ({ rulesSourceName }) => rulesSourceName
- ).reducer,
promRules: createAsyncMapSlice('promRules', fetchPromRulesAction, ({ rulesSourceName }) => rulesSourceName).reducer,
rulerRules: createAsyncMapSlice('rulerRules', fetchRulerRulesAction, ({ rulesSourceName }) => rulesSourceName)
.reducer,
diff --git a/public/app/features/alerting/unified/utils/datasource.ts b/public/app/features/alerting/unified/utils/datasource.ts
index 64275f04169..7ad7aa6aadc 100644
--- a/public/app/features/alerting/unified/utils/datasource.ts
+++ b/public/app/features/alerting/unified/utils/datasource.ts
@@ -8,7 +8,6 @@ import {
} from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import { RulesSource } from 'app/types/unified-alerting';
-import { PromApplication, RulesSourceApplication } from 'app/types/unified-alerting-dto';
import { alertmanagerApi } from '../api/alertmanagerApi';
import { PERMISSIONS_CONTACT_POINTS } from '../components/contact-points/permissions';
@@ -22,6 +21,8 @@ import { getAllDataSources } from './config';
export const GRAFANA_RULES_SOURCE_NAME = 'grafana';
export const GRAFANA_DATASOURCE_NAME = '-- Grafana --';
+export type RulesSourceIdentifier = { rulesSourceName: string } | { uid: string };
+
export enum DataSourceType {
Alertmanager = 'alertmanager',
Loki = 'loki',
@@ -39,12 +40,15 @@ export interface AlertManagerDataSource {
export const RulesDataSourceTypes: string[] = [DataSourceType.Loki, DataSourceType.Prometheus];
export function getRulesDataSources() {
- if (!contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalRead)) {
+ const hasReadPermission = contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalRead);
+ const hasWritePermission = contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalWrite);
+ if (!hasReadPermission && !hasWritePermission) {
return [];
}
return getAllDataSources()
- .filter((ds) => RulesDataSourceTypes.includes(ds.type) && ds.jsonData.manageAlerts !== false)
+ .filter((ds) => RulesDataSourceTypes.includes(ds.type))
+ .filter((ds) => isDataSourceManagingAlerts(ds))
.sort((a, b) => a.name.localeCompare(b.name));
}
@@ -56,6 +60,10 @@ export function getRulesDataSource(rulesSourceName: string) {
return getRulesDataSources().find((x) => x.name === rulesSourceName);
}
+export function getRulesDataSourceByUID(uid: string) {
+ return getRulesDataSources().find((x) => x.uid === uid);
+}
+
export function getAlertManagerDataSources() {
return getAllDataSources()
.filter(isAlertmanagerDataSourceInstance)
@@ -203,7 +211,7 @@ export function getAllRulesSources(): RulesSource[] {
const availableRulesSources: RulesSource[] = getRulesDataSources();
if (contextSrv.hasPermission(AccessControlAction.AlertingRuleRead)) {
- availableRulesSources.push(GRAFANA_RULES_SOURCE_NAME);
+ availableRulesSources.unshift(GRAFANA_RULES_SOURCE_NAME);
}
return availableRulesSources;
@@ -242,6 +250,10 @@ export function getDataSourceByName(name: string): DataSourceInstanceSettings source.name === name);
}
+export function getDataSourceByUid(dsUid: string): DataSourceInstanceSettings | undefined {
+ return getAllDataSources().find((source) => source.uid === dsUid);
+}
+
export function getAlertmanagerDataSourceByName(name: string) {
return getAllDataSources()
.filter(isAlertmanagerDataSourceInstance)
@@ -277,6 +289,22 @@ export function getDatasourceAPIUid(dataSourceName: string) {
return ds.uid;
}
+export function getDataSourceUID(rulesSourceIdentifier: RulesSourceIdentifier) {
+ if ('uid' in rulesSourceIdentifier) {
+ return rulesSourceIdentifier.uid;
+ }
+
+ if (rulesSourceIdentifier.rulesSourceName === GRAFANA_RULES_SOURCE_NAME) {
+ return GRAFANA_RULES_SOURCE_NAME;
+ }
+
+ const ds = getRulesDataSource(rulesSourceIdentifier.rulesSourceName);
+ if (!ds) {
+ return undefined;
+ }
+ return ds.uid;
+}
+
export function getFirstCompatibleDataSource(): DataSourceInstanceSettings | undefined {
return getDataSourceSrv().getList({ alerting: true })[0];
}
@@ -291,20 +319,3 @@ export function getDefaultOrFirstCompatibleDataSource(): DataSourceInstanceSetti
export function isDataSourceManagingAlerts(ds: DataSourceInstanceSettings) {
return ds.jsonData.manageAlerts !== false; //if this prop is undefined it defaults to true
}
-
-export function getApplicationFromRulesSource(rulesSource: RulesSource): RulesSourceApplication {
- if (isGrafanaRulesSource(rulesSource)) {
- return 'grafana';
- }
-
- // @TODO use buildinfo
- if ('prometheusType' in rulesSource.jsonData) {
- return rulesSource.jsonData?.prometheusType ?? PromApplication.Prometheus;
- }
-
- if (rulesSource.type === 'loki') {
- return 'loki';
- }
-
- return PromApplication.Prometheus; // assume Prometheus if nothing matches
-}
diff --git a/public/app/features/alerting/unified/utils/labels.ts b/public/app/features/alerting/unified/utils/labels.ts
index e76d600c400..492f25d83f0 100644
--- a/public/app/features/alerting/unified/utils/labels.ts
+++ b/public/app/features/alerting/unified/utils/labels.ts
@@ -1,3 +1,5 @@
+import { isEmpty } from 'lodash';
+
import { Labels } from '../../../../types/unified-alerting-dto';
import { Label } from '../components/rules/state-history/common';
@@ -35,7 +37,11 @@ export function arrayKeyValuesToObject(
export const GRAFANA_ORIGIN_LABEL = '__grafana_origin';
-export function labelsSize(labels: Labels) {
+export function labelsSize(labels?: Labels) {
+ if (isEmpty(labels)) {
+ return 0;
+ }
+
return Object.keys(labels).filter((key) => !isPrivateLabelKey(key)).length;
}
diff --git a/public/app/features/alerting/unified/utils/misc.ts b/public/app/features/alerting/unified/utils/misc.ts
index 04998ff66c8..e9479df2405 100644
--- a/public/app/features/alerting/unified/utils/misc.ts
+++ b/public/app/features/alerting/unified/utils/misc.ts
@@ -6,9 +6,21 @@ import { config, isFetchError } from '@grafana/runtime';
import { DataSourceRef } from '@grafana/schema';
import { contextSrv } from 'app/core/services/context_srv';
import { escapePathSeparators } from 'app/features/alerting/unified/utils/rule-id';
-import { alertInstanceKey, isGrafanaRulerRule } from 'app/features/alerting/unified/utils/rules';
+import {
+ alertInstanceKey,
+ isCloudRuleIdentifier,
+ isGrafanaRuleIdentifier,
+ isPrometheusRuleIdentifier,
+} from 'app/features/alerting/unified/utils/rules';
import { SortOrder } from 'app/plugins/panel/alertlist/types';
-import { Alert, CombinedRule, FilterState, RulesSource, SilenceFilterState } from 'app/types/unified-alerting';
+import {
+ Alert,
+ CombinedRule,
+ FilterState,
+ RuleIdentifier,
+ RulesSource,
+ SilenceFilterState,
+} from 'app/types/unified-alerting';
import {
GrafanaAlertState,
PromAlertingRuleState,
@@ -16,7 +28,7 @@ import {
} from 'app/types/unified-alerting-dto';
import { ALERTMANAGER_NAME_QUERY_KEY } from './constants';
-import { getRulesSourceName, isCloudRulesSource } from './datasource';
+import { getRulesSourceName } from './datasource';
import { getMatcherQueryParams } from './matchers';
import * as ruleId from './rule-id';
import { createAbsoluteUrl, createRelativeUrl } from './url';
@@ -67,13 +79,13 @@ export function createMuteTimingLink(muteTimingName: string, alertManagerSourceN
});
}
-export function createShareLink(ruleSource: RulesSource, rule: CombinedRule): string | undefined {
- if (isCloudRulesSource(ruleSource)) {
+export function createShareLink(ruleIdentifier: RuleIdentifier): string | undefined {
+ if (isCloudRuleIdentifier(ruleIdentifier) || isPrometheusRuleIdentifier(ruleIdentifier)) {
return createAbsoluteUrl(
- `/alerting/${encodeURIComponent(ruleSource.name)}/${encodeURIComponent(escapePathSeparators(rule.name))}/find`
+ `/alerting/${encodeURIComponent(ruleIdentifier.ruleSourceName)}/${encodeURIComponent(escapePathSeparators(ruleIdentifier.ruleName))}/find`
);
- } else if (isGrafanaRulerRule(rule.rulerRule)) {
- return createAbsoluteUrl(`/alerting/grafana/${rule.rulerRule.grafana_alert.uid}/view`);
+ } else if (isGrafanaRuleIdentifier(ruleIdentifier)) {
+ return createAbsoluteUrl(`/alerting/grafana/${ruleIdentifier.uid}/view`);
}
return;
diff --git a/public/app/features/alerting/unified/utils/rules.test.ts b/public/app/features/alerting/unified/utils/rules.test.ts
index fb1477d038c..4b4036c40d2 100644
--- a/public/app/features/alerting/unified/utils/rules.test.ts
+++ b/public/app/features/alerting/unified/utils/rules.test.ts
@@ -7,6 +7,7 @@ import {
mockCombinedRule,
mockCombinedRuleGroup,
mockGrafanaRulerRule,
+ mockPromAlertingRule,
mockRuleWithLocation,
mockRulerAlertingRule,
} from '../mocks';
@@ -20,21 +21,21 @@ import {
describe('getRuleOrigin', () => {
it('returns undefined when no origin label is present', () => {
- const rule = mockCombinedRule({
+ const rule = mockPromAlertingRule({
labels: {},
});
expect(getRulePluginOrigin(rule)).toBeUndefined();
});
it('returns undefined when origin label does not match expected format', () => {
- const rule = mockCombinedRule({
+ const rule = mockPromAlertingRule({
labels: { [GRAFANA_ORIGIN_LABEL]: 'invalid_format' },
});
expect(getRulePluginOrigin(rule)).toBeUndefined();
});
it('returns undefined when plugin is not installed', () => {
- const rule = mockCombinedRule({
+ const rule = mockPromAlertingRule({
labels: { [GRAFANA_ORIGIN_LABEL]: 'plugin/uninstalled_plugin' },
});
expect(getRulePluginOrigin(rule)).toBeUndefined();
@@ -64,7 +65,7 @@ describe('getRuleOrigin', () => {
},
},
};
- const rule = mockCombinedRule({
+ const rule = mockPromAlertingRule({
labels: { [GRAFANA_ORIGIN_LABEL]: 'plugin/installed_plugin' },
});
expect(getRulePluginOrigin(rule)).toEqual({ pluginId: 'installed_plugin' });
diff --git a/public/app/features/alerting/unified/utils/rules.ts b/public/app/features/alerting/unified/utils/rules.ts
index 6e18da1826a..fd372acdda4 100644
--- a/public/app/features/alerting/unified/utils/rules.ts
+++ b/public/app/features/alerting/unified/utils/rules.ts
@@ -176,10 +176,12 @@ export interface RulePluginOrigin {
pluginId: string;
}
-export function getRulePluginOrigin(rule: CombinedRule): RulePluginOrigin | undefined {
- // com.grafana.origin=plugin/
- // Prom and Mimir do not support dots in label names 😔
- const origin = rule.labels[GRAFANA_ORIGIN_LABEL];
+export function getRulePluginOrigin(rule?: Rule | RulerRuleDTO): RulePluginOrigin | undefined {
+ if (!rule) {
+ return undefined;
+ }
+
+ const origin = rule.labels?.[GRAFANA_ORIGIN_LABEL];
if (!origin) {
return undefined;
}
@@ -203,7 +205,7 @@ function isPluginInstalled(pluginId: string) {
return Boolean(config.apps[pluginId]);
}
-export function isPluginProvidedRule(rule: CombinedRule): boolean {
+export function isPluginProvidedRule(rule?: Rule | RulerRuleDTO): boolean {
return Boolean(getRulePluginOrigin(rule));
}
diff --git a/public/app/types/unified-alerting-dto.ts b/public/app/types/unified-alerting-dto.ts
index bf149896c12..3edbb586d21 100644
--- a/public/app/types/unified-alerting-dto.ts
+++ b/public/app/types/unified-alerting-dto.ts
@@ -77,7 +77,7 @@ export enum PromApplication {
Thanos = 'Thanos',
}
-export type RulesSourceApplication = PromApplication | 'loki' | 'grafana';
+export type RulesSourceApplication = PromApplication | 'Loki' | 'grafana';
export interface PromBuildInfoResponse {
data: {
@@ -96,7 +96,7 @@ export interface PromBuildInfoResponse {
}
export interface PromApiFeatures {
- application?: PromApplication;
+ application: RulesSourceApplication;
features: {
rulerApiEnabled: boolean;
};
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 2d0005e46dc..eb90b340ad5 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -280,6 +280,10 @@
"success": "Successfully updated rule group"
}
},
+ "rule-list": {
+ "configure-datasource": "Configure",
+ "new-alert-rule": "New alert rule"
+ },
"rule-state": {
"creating": "Creating",
"deleting": "Deleting",
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index 4be0ea99904..8950fe5a584 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -280,6 +280,10 @@
"success": "Ŝūččęşşƒūľľy ūpđäŧęđ řūľę ģřőūp"
}
},
+ "rule-list": {
+ "configure-datasource": "Cőʼnƒįģūřę",
+ "new-alert-rule": "Ńęŵ äľęřŧ řūľę"
+ },
"rule-state": {
"creating": "Cřęäŧįʼnģ",
"deleting": "Đęľęŧįʼnģ",