diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx
index ced0d8d394f..e51fd7cc27e 100644
--- a/public/app/features/alerting/routes.tsx
+++ b/public/app/features/alerting/routes.tsx
@@ -314,8 +314,16 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
),
},
{
+ // This route is for backward compatibility
+ // Previously we had a single admin page containing only the alertmanager settings
+ // We now have a separate route for the alertmanager settings and other settings can be added as extensions
path: '/alerting/admin',
roles: () => ['Admin'],
+ component: () => ,
+ },
+ {
+ path: '/alerting/admin/alertmanager',
+ roles: () => ['Admin'],
component: importAlertingComponent(
() => import(/* webpackChunkName: "AlertingSettings" */ 'app/features/alerting/unified/Settings')
),
diff --git a/public/app/features/alerting/unified/Settings.test.tsx b/public/app/features/alerting/unified/Settings.test.tsx
index 0d362c74b45..cba6f0a0308 100644
--- a/public/app/features/alerting/unified/Settings.test.tsx
+++ b/public/app/features/alerting/unified/Settings.test.tsx
@@ -7,6 +7,7 @@ import DataSourcesResponse from './components/settings/mocks/api/datasources.jso
import { setupGrafanaManagedServer, withExternalOnlySetting } from './components/settings/mocks/server';
import { setupMswServer } from './mockApi';
import { grantUserRole } from './mocks';
+import { addSettingsSection, clearSettingsExtensions } from './settings/extensions';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
@@ -35,12 +36,23 @@ const ui = {
versionsTab: byRole('tab', { name: /versions/i }),
provisionedBadge: byText(/^Provisioned$/),
+
+ // New selectors for extension tabs
+ alertmanagerTab: byRole('tab', { name: 'Alert managers' }),
+ enrichmentTab: byRole('tab', { name: 'Enrichment' }),
+ notificationsTab: byRole('tab', { name: 'Notifications' }),
+ customTab: (name: string) => byRole('tab', { name }),
};
describe('Alerting settings', () => {
beforeEach(() => {
grantUserRole('ServerAdmin');
setupGrafanaManagedServer(server);
+ clearSettingsExtensions();
+ });
+
+ afterEach(() => {
+ clearSettingsExtensions();
});
it('should render the page with Built-in only enabled, others disabled', async () => {
@@ -131,4 +143,118 @@ describe('Alerting settings', () => {
expect(enableButton).not.toBeInTheDocument();
expect(disableButton).not.toBeInTheDocument();
});
+
+ it('should display additional tabs when settings extensions are registered', async () => {
+ // Register extensions before rendering
+ addSettingsSection({
+ id: 'enrichment',
+ text: 'Enrichment',
+ url: '/alerting/admin/enrichment',
+ icon: 'star',
+ });
+
+ addSettingsSection({
+ id: 'notifications',
+ text: 'Notifications',
+ url: '/alerting/admin/notifications',
+ icon: 'bell',
+ });
+
+ render(, {
+ historyOptions: {
+ initialEntries: ['/alerting/admin/alertmanager'],
+ },
+ });
+
+ // Wait for the page to load
+ await waitFor(() => expect(ui.builtInAlertmanagerSection.get()).toBeInTheDocument());
+
+ // Check that the extension tabs are visible
+ expect(ui.enrichmentTab.get()).toBeInTheDocument();
+ expect(ui.notificationsTab.get()).toBeInTheDocument();
+ });
+
+ it('should correctly show active state for extension tabs based on route', async () => {
+ // Register an extension
+ addSettingsSection({
+ id: 'enrichment',
+ text: 'Enrichment',
+ url: '/alerting/admin/enrichment',
+ icon: 'star',
+ });
+
+ // Render with the extension route as active
+ render(, {
+ historyOptions: {
+ initialEntries: ['/alerting/admin/enrichment'],
+ },
+ });
+
+ // Wait for the page to load
+ await waitFor(() => expect(ui.builtInAlertmanagerSection.get()).toBeInTheDocument());
+
+ // Check that the extension tab is visible and active
+ const enrichmentTab = ui.enrichmentTab.get();
+ expect(enrichmentTab).toBeInTheDocument();
+ expect(enrichmentTab).toHaveAttribute('aria-selected', 'true');
+
+ // Check that the default alertmanager tab is not active
+ const alertmanagerTab = ui.alertmanagerTab.get();
+ expect(alertmanagerTab).toHaveAttribute('aria-selected', 'false');
+ });
+
+ it('should handle multiple extensions correctly', async () => {
+ // Register multiple extensions
+ addSettingsSection({
+ id: 'enrichment',
+ text: 'Enrichment',
+ url: '/alerting/admin/enrichment',
+ icon: 'star',
+ });
+
+ addSettingsSection({
+ id: 'notifications',
+ text: 'Notifications',
+ url: '/alerting/admin/notifications',
+ icon: 'bell',
+ });
+
+ addSettingsSection({
+ id: 'custom-settings',
+ text: 'Custom Settings',
+ url: '/alerting/admin/custom',
+ icon: 'cog',
+ });
+
+ render(, {
+ historyOptions: {
+ initialEntries: ['/alerting/admin/alertmanager'],
+ },
+ });
+
+ // Wait for the page to load
+ await waitFor(() => expect(ui.builtInAlertmanagerSection.get()).toBeInTheDocument());
+
+ // Check that all tabs are visible
+ expect(ui.alertmanagerTab.get()).toBeInTheDocument();
+ expect(ui.enrichmentTab.get()).toBeInTheDocument();
+ expect(ui.notificationsTab.get()).toBeInTheDocument();
+ expect(ui.customTab('Custom Settings').get()).toBeInTheDocument();
+ });
+
+ it('should not show extension tabs when no extensions are registered', async () => {
+ render(, {
+ historyOptions: {
+ initialEntries: ['/alerting/admin/alertmanager'],
+ },
+ });
+
+ // Wait for the page to load
+ await waitFor(() => expect(ui.builtInAlertmanagerSection.get()).toBeInTheDocument());
+
+ // Check that only the default alertmanager tab is visible
+ expect(ui.alertmanagerTab.get()).toBeInTheDocument();
+ expect(ui.enrichmentTab.query()).not.toBeInTheDocument();
+ expect(ui.notificationsTab.query()).not.toBeInTheDocument();
+ });
});
diff --git a/public/app/features/alerting/unified/Settings.tsx b/public/app/features/alerting/unified/Settings.tsx
index e1a1eea1679..f09e9036fd3 100644
--- a/public/app/features/alerting/unified/Settings.tsx
+++ b/public/app/features/alerting/unified/Settings.tsx
@@ -7,24 +7,28 @@ import { useEditConfigurationDrawer } from './components/settings/ConfigurationD
import { ExternalAlertmanagers } from './components/settings/ExternalAlertmanagers';
import InternalAlertmanager from './components/settings/InternalAlertmanager';
import { SettingsProvider, useSettings } from './components/settings/SettingsContext';
+import { useSettingsPageNav } from './settings/navigation';
import { withPageErrorBoundary } from './withPageErrorBoundary';
-function SettingsPage() {
+function AlertmanagerSettingsPage() {
return (
-
+
);
}
-function SettingsContent() {
+function AlertmanagerSettingsContent() {
const [configurationDrawer, showConfiguration] = useEditConfigurationDrawer();
const { isLoading } = useSettings();
+ const { navId, pageNav } = useSettingsPageNav();
+
return (
& {
+ url: SettingsSectionUrl;
+};
+
+const settingsExtensions: Map = new Map();
+
+/**
+ * Registers a new settings section that will appear as a tab in the alerting settings page.
+ * @param pageNav - The navigation configuration for the settings section
+ */
+export function addSettingsSection(pageNav: SettingsSectionNav) {
+ if (settingsExtensions.has(pageNav.url)) {
+ console.warn('Unable to add settings page, PageNav must have an unique url');
+ return;
+ }
+ settingsExtensions.set(pageNav.url, { nav: pageNav });
+}
+
+/**
+ * Returns the navigation configuration for all settings extensions.
+ */
+export function useSettingsExtensionsNav(): NavModelItem[] {
+ const location = useLocation();
+
+ const navIndex = useSelector((state) => state.navIndex);
+ const settingsNav = navIndex['alerting-admin'];
+
+ // Build extension tabs from settingsExtensions
+ const extensionTabs: NavModelItem[] = Array.from(settingsExtensions.entries()).map(([url, { nav }]) => ({
+ ...nav,
+ active: location.pathname === url,
+ url: url,
+ parentItem: settingsNav,
+ }));
+
+ return extensionTabs;
+}
+
+/**
+ * ONLY USE FOR TESTING. Clears all settings extensions.
+ */
+export function clearSettingsExtensions() {
+ settingsExtensions.clear();
+}
diff --git a/public/app/features/alerting/unified/settings/navigation.test.ts b/public/app/features/alerting/unified/settings/navigation.test.ts
new file mode 100644
index 00000000000..c4b4c4678ef
--- /dev/null
+++ b/public/app/features/alerting/unified/settings/navigation.test.ts
@@ -0,0 +1,111 @@
+import { renderHook } from '@testing-library/react';
+import { getWrapper } from 'test/test-utils';
+
+import { addSettingsSection, clearSettingsExtensions } from './extensions';
+import { useSettingsPageNav } from './navigation';
+
+describe('useSettingsPageNav', () => {
+ const mockNavIndex = {
+ 'alerting-admin': {
+ id: 'alerting-admin',
+ text: 'Settings',
+ url: '/alerting/admin',
+ },
+ };
+
+ const defaultPreloadedState = {
+ navIndex: mockNavIndex,
+ };
+
+ it('should return settings page nav with alertmanager child when no extensions are present', () => {
+ const wrapper = getWrapper({
+ preloadedState: defaultPreloadedState,
+ renderWithRouter: true,
+ historyOptions: {
+ initialEntries: ['/alerting/admin/alertmanager'],
+ },
+ });
+
+ const { result } = renderHook(() => useSettingsPageNav(), { wrapper });
+
+ expect(result.current.navId).toBe('alerting');
+
+ // Check the structure
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(result.current.pageNav.children).toHaveLength(1);
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(result.current.pageNav.children).toEqual([
+ expect.objectContaining({
+ id: 'alertmanager',
+ text: 'Alert managers',
+ url: '/alerting/admin/alertmanager',
+ active: true,
+ icon: 'cloud',
+ parentItem: undefined,
+ }),
+ ]);
+ });
+
+ it('should include extensions when added via addSettingsSection', () => {
+ // Clear any existing extensions
+ clearSettingsExtensions();
+
+ // Add two extensions
+ addSettingsSection({
+ id: 'enrichment',
+ text: 'Enrichment',
+ url: '/alerting/admin/enrichment',
+ icon: 'star',
+ });
+
+ addSettingsSection({
+ id: 'notifications',
+ text: 'Notifications',
+ url: '/alerting/admin/notifications',
+ icon: 'bell',
+ });
+
+ const wrapper = getWrapper({
+ preloadedState: defaultPreloadedState,
+ renderWithRouter: true,
+ historyOptions: {
+ initialEntries: ['/alerting/admin/enrichment'],
+ },
+ });
+
+ const { result } = renderHook(() => useSettingsPageNav(), { wrapper });
+
+ expect(result.current.navId).toBe('alerting');
+
+ // Should have 3 children: alertmanager + 2 extensions
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(result.current.pageNav.children).toHaveLength(3);
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(result.current.pageNav.children).toEqual([
+ expect.objectContaining({
+ id: 'alertmanager',
+ text: 'Alert managers',
+ url: '/alerting/admin/alertmanager',
+ active: false,
+ icon: 'cloud',
+ parentItem: undefined,
+ }),
+ expect.objectContaining({
+ id: 'enrichment',
+ text: 'Enrichment',
+ url: '/alerting/admin/enrichment',
+ active: true,
+ icon: 'star',
+ parentItem: undefined,
+ }),
+ expect.objectContaining({
+ id: 'notifications',
+ text: 'Notifications',
+ url: '/alerting/admin/notifications',
+ active: false,
+ icon: 'bell',
+ parentItem: undefined,
+ }),
+ ]);
+ });
+});
diff --git a/public/app/features/alerting/unified/settings/navigation.ts b/public/app/features/alerting/unified/settings/navigation.ts
new file mode 100644
index 00000000000..85513269e1f
--- /dev/null
+++ b/public/app/features/alerting/unified/settings/navigation.ts
@@ -0,0 +1,40 @@
+import { useLocation } from 'react-router-dom-v5-compat';
+
+import { NavModelItem } from '@grafana/data';
+import { t } from '@grafana/i18n';
+import { useSelector } from 'app/types/store';
+
+import { useSettingsExtensionsNav } from './extensions';
+
+export function useSettingsPageNav() {
+ const location = useLocation();
+
+ const navIndex = useSelector((state) => state.navIndex);
+ const settingsNav = navIndex['alerting-admin'];
+
+ const extensionTabs = useSettingsExtensionsNav();
+
+ // All available tabs including the main alertmanager tab
+ const allTabs: NavModelItem[] = [
+ {
+ id: 'alertmanager',
+ text: t('alerting.settings.tabs.alert-managers', 'Alert managers'),
+ url: '/alerting/admin/alertmanager',
+ active: location.pathname === '/alerting/admin/alertmanager',
+ icon: 'cloud',
+ parentItem: settingsNav,
+ },
+ ...extensionTabs,
+ ];
+
+ // Create pageNav that represents the Settings page with tabs as children
+ const pageNav: NavModelItem = {
+ ...settingsNav,
+ children: allTabs,
+ };
+
+ return {
+ navId: 'alerting',
+ pageNav,
+ };
+}
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index ff096665d92..deaf63f3127 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -2650,6 +2650,11 @@
"selecting-data-source-tooltip": {
"tooltip-content": "Not finding the data source you want? Some data sources are not supported for alerting. Click on the icon for more information."
},
+ "settings": {
+ "tabs": {
+ "alert-managers": "Alert managers"
+ }
+ },
"settings-content": {
"add-new-alertmanager": "Add new Alertmanager",
"builtin-alertmanager": "Built-in Alertmanager",