Alerting: Show converted alertmanager configurations in the UI (#109027)

This commit is contained in:
Alexander Akhmetov
2025-08-19 10:04:05 +02:00
committed by GitHub
parent bbf01a6383
commit 1e6719f571
17 changed files with 246 additions and 58 deletions
@@ -9,7 +9,7 @@ import { MATCHER_ALERT_RULE_UID } from 'app/features/alerting/unified/utils/cons
import { parseQueryParamMatchers } from 'app/features/alerting/unified/utils/matchers';
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';
import { GrafanaAlertmanagerWarning } from './components/GrafanaAlertmanagerWarning';
import { SilencesEditor } from './components/silences/SilencesEditor';
import { useAlertmanager } from './state/AlertmanagerContext';
import { withPageErrorBoundary } from './withPageErrorBoundary';
@@ -27,7 +27,7 @@ const SilencesEditorComponent = () => {
return (
<>
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={selectedAlertmanager} />
<GrafanaAlertmanagerWarning currentAlertmanager={selectedAlertmanager} />
<SilencesEditor
formValues={formValues}
alertManagerSourceName={selectedAlertmanager}
@@ -10,7 +10,7 @@ import { NotificationPoliciesList } from 'app/features/alerting/unified/componen
import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alerting/unified/hooks/useAbilities';
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';
import { GrafanaAlertmanagerWarning } from './components/GrafanaAlertmanagerWarning';
import { TimeIntervalsTable } from './components/mute-timings/MuteTimingsTable';
import { useAlertmanager } from './state/AlertmanagerContext';
import { withPageErrorBoundary } from './withPageErrorBoundary';
@@ -48,7 +48,7 @@ const NotificationPoliciesTabs = () => {
return (
<>
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={selectedAlertmanager} />
<GrafanaAlertmanagerWarning currentAlertmanager={selectedAlertmanager} />
<TabsBar>
{policiesSupported && canSeePoliciesTab && (
<Tab
@@ -30,6 +30,7 @@ export async function fetchAlertManagerConfig(alertManagerSourceName: string): P
alertmanager_config: result.data.alertmanager_config ?? {},
last_applied: result.data.last_applied,
id: result.data.id,
extra_config: result.data.extra_config,
};
} catch (e) {
// if no config has been uploaded to grafana, it returns error instead of latest config
@@ -191,6 +191,7 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
data: {
alertmanager_config: status.config,
template_files: {},
extra_config: undefined,
},
}))
);
@@ -207,6 +208,7 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
alertmanager_config: {},
template_files: {},
template_file_provenances: {},
extra_config: undefined,
};
const lazyConfigInitSupported = alertmanagerFeatures?.lazyConfigInit ?? false;
@@ -244,6 +246,7 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
template_file_provenances: result.template_file_provenances,
last_applied: result.last_applied,
id: result.id,
extra_config: result.extra_config,
}));
}
@@ -6,6 +6,7 @@ import { t } from '@grafana/i18n';
import { InlineField, Select, SelectMenuOptions, useStyles2 } from '@grafana/ui';
import { useAlertmanager } from '../state/AlertmanagerContext';
import { isExtraConfig } from '../utils/alertmanager/extraConfigs';
import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
interface Props {
@@ -13,7 +14,11 @@ interface Props {
}
function getAlertManagerLabel(alertManager: AlertManagerDataSource) {
return alertManager.name === GRAFANA_RULES_SOURCE_NAME ? 'Grafana' : alertManager.name;
if (alertManager.name === GRAFANA_RULES_SOURCE_NAME) {
return 'Grafana';
}
return alertManager.displayName || alertManager.name;
}
export const AlertManagerPicker = ({ disabled = false }: Props) => {
@@ -21,12 +26,50 @@ export const AlertManagerPicker = ({ disabled = false }: Props) => {
const { selectedAlertmanager, availableAlertManagers, setSelectedAlertmanager } = useAlertmanager();
const options = useMemo(() => {
return availableAlertManagers.map<SelectableValue<string>>((ds) => ({
label: getAlertManagerLabel(ds),
value: ds.name,
imgUrl: ds.imgUrl,
meta: ds.meta,
}));
// Group alertmanagers
const grafanaAM = availableAlertManagers.find((am) => am.name === GRAFANA_RULES_SOURCE_NAME);
const extraConfig = availableAlertManagers.find((am) => isExtraConfig(am.name));
const datasourceAMs = availableAlertManagers.filter(
(am) => am.name !== GRAFANA_RULES_SOURCE_NAME && !isExtraConfig(am.name)
);
const groupedOptions: Array<SelectableValue<string> | { label: string; options: Array<SelectableValue<string>> }> =
[];
// Add Grafana alertmanager first
if (grafanaAM) {
groupedOptions.push({
label: getAlertManagerLabel(grafanaAM),
value: grafanaAM.name,
imgUrl: grafanaAM.imgUrl,
meta: grafanaAM.meta,
});
}
// Add extra config (single merged configuration)
if (extraConfig) {
groupedOptions.push({
label: getAlertManagerLabel(extraConfig),
value: extraConfig.name,
imgUrl: extraConfig.imgUrl,
meta: extraConfig.meta,
});
}
// Add external alertmanagers
if (datasourceAMs.length > 0) {
groupedOptions.push({
label: t('alerting.alert-manager-picker.external-alertmanagers-group', 'External Alertmanagers'),
options: datasourceAMs.map((ds) => ({
label: getAlertManagerLabel(ds),
value: ds.name,
imgUrl: ds.imgUrl,
meta: ds.meta,
})),
});
}
return groupedOptions;
}, [availableAlertManagers]);
const isDisabled = disabled || options.length === 1;
@@ -61,12 +104,22 @@ const getStyles = (theme: GrafanaTheme2) => ({
field: css({
margin: 0,
}),
optionContent: css({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
whiteSpace: 'pre-line',
}),
});
// custom option that overwrites the default "white-space: nowrap" for Alertmanager names that are really long
const CustomOption = (props: ComponentProps<typeof SelectMenuOptions>) => (
<SelectMenuOptions
{...props}
renderOptionLabel={({ label }) => <div style={{ whiteSpace: 'pre-line' }}>{label}</div>}
/>
);
const CustomOption = (props: ComponentProps<typeof SelectMenuOptions>) => {
const styles = useStyles2(getStyles);
return (
<SelectMenuOptions
{...props}
renderOptionLabel={({ label }) => <div className={styles.optionContent}>{label}</div>}
/>
);
};
@@ -10,10 +10,10 @@ import { AlertmanagerChoice } from '../../../../plugins/datasource/alertmanager/
import { grantUserPermissions } from '../mocks';
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import { GrafanaAlertmanagerDeliveryWarning } from './GrafanaAlertmanagerDeliveryWarning';
import { GrafanaAlertmanagerWarning } from './GrafanaAlertmanagerWarning';
setupMswServer();
describe('GrafanaAlertmanagerDeliveryWarning', () => {
describe('GrafanaAlertmanagerWarning', () => {
beforeEach(() => {
grantUserPermissions([AccessControlAction.AlertingNotificationsRead]);
});
@@ -21,9 +21,7 @@ describe('GrafanaAlertmanagerDeliveryWarning', () => {
it('Should not render when the datasource is not Grafana', () => {
setAlertmanagerChoices(AlertmanagerChoice.External, 0);
const { container } = renderWithStore(
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager="custom-alertmanager" />
);
const { container } = renderWithStore(<GrafanaAlertmanagerWarning currentAlertmanager="custom-alertmanager" />);
expect(container).toBeEmptyDOMElement();
});
@@ -31,7 +29,7 @@ describe('GrafanaAlertmanagerDeliveryWarning', () => {
it('Should render warning when the datasource is Grafana and using external AM', async () => {
setAlertmanagerChoices(AlertmanagerChoice.External, 1);
renderWithStore(<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />);
renderWithStore(<GrafanaAlertmanagerWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />);
expect(await screen.findByText('Grafana alerts are not delivered to Grafana Alertmanager')).toBeVisible();
});
@@ -39,7 +37,7 @@ describe('GrafanaAlertmanagerDeliveryWarning', () => {
it('Should render warning when the datasource is Grafana and using All AM', async () => {
setAlertmanagerChoices(AlertmanagerChoice.All, 1);
renderWithStore(<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />);
renderWithStore(<GrafanaAlertmanagerWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />);
expect(await screen.findByText('You have additional Alertmanagers to configure')).toBeVisible();
});
@@ -48,7 +46,7 @@ describe('GrafanaAlertmanagerDeliveryWarning', () => {
setAlertmanagerChoices(AlertmanagerChoice.Internal, 1);
const { container } = renderWithStore(
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />
<GrafanaAlertmanagerWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />
);
await waitFor(() => {
@@ -60,7 +58,7 @@ describe('GrafanaAlertmanagerDeliveryWarning', () => {
setAlertmanagerChoices(AlertmanagerChoice.All, 0);
const { container } = renderWithStore(
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />
<GrafanaAlertmanagerWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />
);
await waitFor(() => {
@@ -7,13 +7,23 @@ import { Alert, useStyles2 } from '@grafana/ui';
import { AlertmanagerChoice } from '../../../../plugins/datasource/alertmanager/types';
import { alertmanagerApi } from '../api/alertmanagerApi';
import { AlertingAction, useAlertingAbility } from '../hooks/useAbilities';
import { isExtraConfig } from '../utils/alertmanager/extraConfigs';
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
interface GrafanaAlertmanagerDeliveryWarningProps {
interface GrafanaAlertmanagerWarningProps {
currentAlertmanager: string;
}
export function GrafanaAlertmanagerDeliveryWarning({ currentAlertmanager }: GrafanaAlertmanagerDeliveryWarningProps) {
export function GrafanaAlertmanagerWarning({ currentAlertmanager }: GrafanaAlertmanagerWarningProps) {
return (
<>
<GrafanaExternalAlertmanagerConfigWarning currentAlertmanager={currentAlertmanager} />
<GrafanaExtraConfigWarning currentAlertmanager={currentAlertmanager} />
</>
);
}
function GrafanaExternalAlertmanagerConfigWarning({ currentAlertmanager }: GrafanaAlertmanagerWarningProps) {
const styles = useStyles2(getStyles);
const externalAlertmanager = currentAlertmanager !== GRAFANA_RULES_SOURCE_NAME;
@@ -87,6 +97,23 @@ export function GrafanaAlertmanagerDeliveryWarning({ currentAlertmanager }: Graf
return null;
}
function GrafanaExtraConfigWarning({ currentAlertmanager }: GrafanaAlertmanagerWarningProps) {
const isSelectedExtraConfig = currentAlertmanager && isExtraConfig(currentAlertmanager);
if (!isSelectedExtraConfig) {
return null;
}
return (
<Alert title={t('alerting.alert-manager-picker.extra-config-warning.title', 'Imported configuration')}>
<Trans i18nKey="alerting.alert-manager-picker.extra-config-warning.content">
This shows the merged configuration of Grafana alertmanager with imported configurations. This merged view is
read-only in the UI.
</Trans>
</Alert>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
adminHint: css({
fontSize: theme.typography.bodySmall.fontSize,
@@ -23,10 +23,11 @@ import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbili
import { usePagination } from '../../hooks/usePagination';
import { useURLSearchParams } from '../../hooks/useURLSearchParams';
import { useAlertmanager } from '../../state/AlertmanagerContext';
import { isExtraConfig } from '../../utils/alertmanager/extraConfigs';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { withPageErrorBoundary } from '../../withPageErrorBoundary';
import { AlertmanagerPageWrapper } from '../AlertingPageWrapper';
import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning';
import { GrafanaAlertmanagerWarning } from '../GrafanaAlertmanagerWarning';
import { ContactPoint } from './ContactPoint';
import { NotificationTemplates } from './NotificationTemplates';
@@ -140,8 +141,12 @@ const ContactPointsTab = () => {
) : (
<ContactPointsList contactPoints={contactPoints} search={search} pageSize={DEFAULT_PAGE_SIZE} />
)}
{/* Grafana manager Alertmanager does not support global config, Mimir and Cortex do */}
{!isGrafanaManagedAlertmanager && <GlobalConfigAlert alertManagerName={selectedAlertmanager!} />}
{/* Extra configs also don't support global config */}
{!isGrafanaManagedAlertmanager && !isExtraConfig(selectedAlertmanager!) && (
<GlobalConfigAlert alertManagerName={selectedAlertmanager!} />
)}
{ExportDrawer}
</>
);
@@ -220,7 +225,7 @@ export const ContactPointsPageContents = () => {
return (
<>
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={selectedAlertmanager!} />
<GrafanaAlertmanagerWarning currentAlertmanager={selectedAlertmanager!} />
<Stack direction="column">
<TabsBar>
{showContactPointsTab && (
@@ -39,7 +39,7 @@ import { matcherFieldToMatcher } from '../../utils/alertmanager';
import { makeAMLink } from '../../utils/misc';
import { withPageErrorBoundary } from '../../withPageErrorBoundary';
import { AlertmanagerPageWrapper } from '../AlertingPageWrapper';
import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning';
import { GrafanaAlertmanagerWarning } from '../GrafanaAlertmanagerWarning';
import MatchersField from './MatchersField';
import { SilencePeriod } from './SilencePeriod';
@@ -118,7 +118,7 @@ const ExistingSilenceEditor = () => {
return (
<>
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={alertManagerSourceName} />
<GrafanaAlertmanagerWarning currentAlertmanager={alertManagerSourceName} />
<SilencesEditor ruleUid={ruleUid} formValues={defaultValues} alertManagerSourceName={alertManagerSourceName} />
</>
);
@@ -30,7 +30,7 @@ import { withPageErrorBoundary } from '../../withPageErrorBoundary';
import { AlertmanagerPageWrapper } from '../AlertingPageWrapper';
import { Authorize } from '../Authorize';
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning';
import { GrafanaAlertmanagerWarning } from '../GrafanaAlertmanagerWarning';
import { Matchers } from './Matchers';
import { NoSilencesSplash } from './NoSilencesCTA';
@@ -146,7 +146,7 @@ const SilencesTable = () => {
return (
<div data-testid="silences-table">
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={alertManagerSourceName} />
<GrafanaAlertmanagerWarning currentAlertmanager={alertManagerSourceName} />
{!!silences.length && (
<Stack direction="column">
<SilencesFilter />
@@ -1,9 +1,11 @@
import { renderHook } from '@testing-library/react';
import * as React from 'react';
import { Provider } from 'react-redux';
import { locationService } from '@grafana/runtime';
import store from 'app/core/store';
import { AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
import { configureStore } from 'app/store/configureStore';
import * as useAlertManagerSources from '../hooks/useAlertManagerSources';
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY } from '../utils/constants';
@@ -21,14 +23,21 @@ const externalAmMimir: AlertManagerDataSource = {
imgUrl: '',
};
function getProviderWrapper() {
const reduxStore = configureStore();
return ({ children }: React.PropsWithChildren) => (
<Provider store={reduxStore}>
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
</Provider>
);
}
describe('useAlertmanager', () => {
it('Should return undefined alert manager name when there are no available alert managers', () => {
jest
.spyOn(useAlertManagerSources, 'useAlertManagersByPermission')
.mockReturnValueOnce({ availableExternalDataSources: [], availableInternalDataSources: [] });
const wrapper = ({ children }: React.PropsWithChildren) => (
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
);
const wrapper = getProviderWrapper();
const { result } = renderHook(() => useAlertmanager(), { wrapper });
expect(result.current.selectedAlertmanager).toBe(undefined);
@@ -40,9 +49,7 @@ describe('useAlertmanager', () => {
availableInternalDataSources: [{ name: GRAFANA_RULES_SOURCE_NAME, imgUrl: '', hasConfigurationAPI: true }],
});
const wrapper = ({ children }: React.PropsWithChildren) => (
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
);
const wrapper = getProviderWrapper();
const { result } = renderHook(() => useAlertmanager(), { wrapper });
expect(result.current.selectedAlertmanager).toBe(GRAFANA_RULES_SOURCE_NAME);
@@ -55,9 +62,7 @@ describe('useAlertmanager', () => {
locationService.push({ search: `alertmanager=${externalAmProm.name}` });
const wrapper = ({ children }: React.PropsWithChildren) => (
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
);
const wrapper = getProviderWrapper();
const { result } = renderHook(() => useAlertmanager(), { wrapper });
expect(result.current.selectedAlertmanager).toBe(externalAmProm.name);
@@ -70,9 +75,7 @@ describe('useAlertmanager', () => {
locationService.push({ search: `alertmanager=Not available external AM` });
const wrapper = ({ children }: React.PropsWithChildren) => (
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
);
const wrapper = getProviderWrapper();
const { result } = renderHook(() => useAlertmanager(), { wrapper });
expect(result.current.selectedAlertmanager).toBe(undefined);
@@ -83,9 +86,7 @@ describe('useAlertmanager', () => {
.spyOn(useAlertManagerSources, 'useAlertManagersByPermission')
.mockReturnValueOnce({ availableExternalDataSources: [externalAmProm], availableInternalDataSources: [] });
const wrapper = ({ children }: React.PropsWithChildren) => (
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
);
const wrapper = getProviderWrapper();
store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, externalAmProm.name);
locationService.push({ search: '' });
@@ -101,9 +102,7 @@ describe('useAlertmanager', () => {
locationService.push({ search: `alertmanager=${externalAmProm.name}` });
const wrapper = ({ children }: React.PropsWithChildren) => (
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
);
const wrapper = getProviderWrapper();
store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, externalAmMimir.name);
@@ -1,10 +1,13 @@
import * as React from 'react';
import { locationService } from '@grafana/runtime';
import { config, locationService } from '@grafana/runtime';
import store from 'app/core/store';
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
import grafanaIconSvg from 'img/grafana_icon.svg';
import { useAlertManagersByPermission } from '../hooks/useAlertManagerSources';
import { useAlertmanagerConfig } from '../hooks/useAlertmanagerConfig';
import { EXTRA_CONFIG_UID, isExtraConfig } from '../utils/alertmanager/extraConfigs';
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from '../utils/constants';
import {
AlertManagerDataSource,
@@ -33,9 +36,46 @@ const AlertmanagerProvider = ({ children, accessType, alertmanagerSourceName }:
const queryParams = locationService.getSearch();
const updateQueryParams = locationService.partial;
const allAvailableAlertManagers = useAlertManagersByPermission(accessType);
const availableAlertManagers = allAvailableAlertManagers.availableInternalDataSources.concat(
allAvailableAlertManagers.availableExternalDataSources
);
const isExtraConfigEnabled = config.featureToggles.alertingImportAlertmanagerUI ?? false;
const { data: grafanaConfig } = useAlertmanagerConfig(isExtraConfigEnabled ? GRAFANA_RULES_SOURCE_NAME : undefined, {
refetchOnFocus: false,
refetchOnReconnect: false,
});
const hasExtraConfigs = Boolean(grafanaConfig?.extra_config && grafanaConfig.extra_config.length > 0);
const availableAlertManagers = React.useMemo(() => {
const regularAlertManagers = allAvailableAlertManagers.availableInternalDataSources.concat(
allAvailableAlertManagers.availableExternalDataSources
);
const extraConfigDataSource: AlertManagerDataSource[] =
isExtraConfigEnabled && hasExtraConfigs
? [
{
name: EXTRA_CONFIG_UID,
displayName: 'Grafana (imported)',
imgUrl: grafanaIconSvg,
hasConfigurationAPI: false,
handleGrafanaManagedAlerts: true,
},
]
: [];
// list in order: Grafana -> Extra Config -> Extra Alertmanagers
const grafanaAlertmanager = regularAlertManagers.find((am) => am.name === GRAFANA_RULES_SOURCE_NAME);
const datasourceAlertmanagers = regularAlertManagers.filter((am) => am.name !== GRAFANA_RULES_SOURCE_NAME);
const orderedAlertManagers: AlertManagerDataSource[] = [];
if (grafanaAlertmanager) {
orderedAlertManagers.push(grafanaAlertmanager);
}
orderedAlertManagers.push(...extraConfigDataSource);
orderedAlertManagers.push(...datasourceAlertmanagers);
return orderedAlertManagers;
}, [allAvailableAlertManagers, isExtraConfigEnabled, hasExtraConfigs]);
const updateSelectedAlertmanager = React.useCallback(
(selectedAlertManager: string) => {
@@ -73,7 +113,16 @@ const AlertmanagerProvider = ({ children, accessType, alertmanagerSourceName }:
? desiredAlertmanager
: undefined;
const selectedAlertmanagerConfig = getAlertmanagerDataSourceByName(selectedAlertmanager)?.jsonData;
const selectedAlertmanagerConfig = React.useMemo(() => {
if (selectedAlertmanager && isExtraConfig(selectedAlertmanager)) {
const config: AlertManagerDataSourceJsonData = {
implementation: AlertManagerImplementation.prometheus,
handleGrafanaManagedAlerts: true,
};
return config;
}
return getAlertmanagerDataSourceByName(selectedAlertmanager)?.jsonData;
}, [selectedAlertmanager]);
// determine if we're dealing with an Alertmanager data source that supports the ruler API
const isGrafanaAlertmanager = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME;
@@ -0,0 +1,24 @@
import { EXTRA_CONFIG_UID, isExtraConfig } from './extraConfigs';
describe('extraConfigs utilities', () => {
describe('isExtraConfig', () => {
it('should return true for the exact extra config UID', () => {
expect(isExtraConfig(EXTRA_CONFIG_UID)).toBe(true);
});
it('should return false for non-extra config names', () => {
expect(isExtraConfig('grafana')).toBe(false);
expect(isExtraConfig('prometheus-am')).toBe(false);
expect(isExtraConfig('regular-alertmanager')).toBe(false);
expect(isExtraConfig('')).toBe(false);
expect(isExtraConfig('~grafana-converted-extra-config-test')).toBe(false); // old pattern
expect(isExtraConfig('~grafana-with-extra-config-suffix')).toBe(false); // with suffix
});
});
describe('EXTRA_CONFIG_UID constant', () => {
it('should have the correct UID value', () => {
expect(EXTRA_CONFIG_UID).toBe('~grafana-with-extra-config');
});
});
});
@@ -0,0 +1,15 @@
export const EXTRA_CONFIG_UID = '~grafana-with-extra-config';
export interface ExtraConfiguration {
identifier: string;
source?: string;
createdAt?: string;
}
export interface AlertingConfigResponse {
extra_config?: ExtraConfiguration[];
}
export function isExtraConfig(name: string): boolean {
return name === EXTRA_CONFIG_UID;
}
@@ -27,6 +27,7 @@ import { useAlertManagersByPermission } from '../hooks/useAlertManagerSources';
import { isAlertManagerWithConfigAPI } from '../state/AlertmanagerContext';
import { instancesPermissions, notificationsPermissions, silencesPermissions } from './access-control';
import { isExtraConfig } from './alertmanager/extraConfigs';
import { getAllDataSources } from './config';
import { isGrafanaRuleIdentifier } from './rules';
@@ -52,6 +53,7 @@ export enum DataSourceType {
export interface AlertManagerDataSource {
name: string;
displayName?: string;
imgUrl: string;
meta?: DataSourceInstanceSettings['meta'];
hasConfigurationAPI?: boolean;
@@ -300,6 +302,11 @@ export function getDatasourceAPIUid(dataSourceName: string) {
if (dataSourceName === GRAFANA_RULES_SOURCE_NAME) {
return GRAFANA_RULES_SOURCE_NAME;
}
if (isExtraConfig(dataSourceName)) {
return dataSourceName;
}
const ds = getDataSourceByName(dataSourceName);
if (!ds) {
throw new Error(`Datasource "${dataSourceName}" not found`);
@@ -1,6 +1,7 @@
//DOCS: https://prometheus.io/docs/alerting/latest/configuration/
import { DataSourceJsonData, WithAccessControlMetadata } from '@grafana/data';
import { IoK8SApimachineryPkgApisMetaV1ObjectMeta } from 'app/features/alerting/unified/openapi/receiversApi.gen';
import { ExtraConfiguration } from 'app/features/alerting/unified/utils/alertmanager/extraConfigs';
export const ROUTES_META_SYMBOL = Symbol('routes_metadata');
@@ -11,6 +12,7 @@ export type AlertManagerCortexConfig = {
template_file_provenances?: Record<string, string>;
last_applied?: string;
id?: number;
extra_config?: ExtraConfiguration[];
};
export type TLSConfig = {
+5
View File
@@ -493,6 +493,11 @@
"title-muting-grouping-and-timings": "Muting, grouping, and timings"
},
"alert-manager-picker": {
"external-alertmanagers-group": "External Alertmanagers",
"extra-config-warning": {
"content": "This shows the merged configuration of Grafana alertmanager with imported configurations. This merged view is read-only in the UI.",
"title": "Imported configuration"
},
"noOptionsMessage-no-datasources-found": "No datasources found"
},
"alert-menu": {