Alerting: Extensible Settings module (#107831)
* Add extension points to the Settings page * wip * Update Settings page routing to catch unmatched routes and redirect to alertmanager * Add settings page extension * Refactor settings navigation * Refactor settings navigation hook * Update import paths * Add Settings page tests for custom tabs * Update translations * Add a route explanation --------- Co-authored-by: Sonia Aguilar <soniaaguilarpeiron@gmail.com>
This commit is contained in:
co-authored by
Sonia Aguilar
parent
92121a1030
commit
89c09a14b8
@@ -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: () => <Navigate replace to="/alerting/admin/alertmanager" />,
|
||||
},
|
||||
{
|
||||
path: '/alerting/admin/alertmanager',
|
||||
roles: () => ['Admin'],
|
||||
component: importAlertingComponent(
|
||||
() => import(/* webpackChunkName: "AlertingSettings" */ 'app/features/alerting/unified/Settings')
|
||||
),
|
||||
|
||||
@@ -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(<SettingsPage />, {
|
||||
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(<SettingsPage />, {
|
||||
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(<SettingsPage />, {
|
||||
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(<SettingsPage />, {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<SettingsProvider>
|
||||
<SettingsContent />
|
||||
<AlertmanagerSettingsContent />
|
||||
</SettingsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsContent() {
|
||||
function AlertmanagerSettingsContent() {
|
||||
const [configurationDrawer, showConfiguration] = useEditConfigurationDrawer();
|
||||
const { isLoading } = useSettings();
|
||||
|
||||
const { navId, pageNav } = useSettingsPageNav();
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper
|
||||
navId="alerting-admin"
|
||||
navId={navId}
|
||||
isLoading={isLoading}
|
||||
pageNav={pageNav}
|
||||
actions={[
|
||||
<WithReturnButton
|
||||
key="add-alertmanager"
|
||||
@@ -54,4 +58,4 @@ function SettingsContent() {
|
||||
);
|
||||
}
|
||||
|
||||
export default withPageErrorBoundary(SettingsPage);
|
||||
export default withPageErrorBoundary(AlertmanagerSettingsPage);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useLocation } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { useSelector } from 'app/types/store';
|
||||
|
||||
type SettingsSectionUrl = `/alerting/admin/${string}`;
|
||||
type SettingsSectionNav = Pick<NavModelItem, 'id' | 'text' | 'icon'> & {
|
||||
url: SettingsSectionUrl;
|
||||
};
|
||||
|
||||
const settingsExtensions: Map<SettingsSectionUrl, { nav: SettingsSectionNav }> = 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();
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user