diff --git a/conf/defaults.ini b/conf/defaults.ini
index cfc5c43ef61..1cefee3488a 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -2072,7 +2072,7 @@ license_path =
# will take precedence over toggles in the `enable` list.
# enable = feature1,feature2
-enable = alertingNavigationV2
+enable = alertingNavigationV2, alertingTriage, alertingListViewV2, alertingFilterV2, alertingCentralAlertHistory, alertingSavedSearches
# Some features are enabled by default, see:
# https://grafana.com/docs/grafana/next/setup-grafana/configure-grafana/feature-toggles/
diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx
index 274fa97bd0d..e9e12a6b9fc 100644
--- a/public/app/features/alerting/routes.tsx
+++ b/public/app/features/alerting/routes.tsx
@@ -56,6 +56,17 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
)
),
},
+ {
+ path: '/alerting/time-intervals',
+ roles: evaluateAccess([
+ AccessControlAction.AlertingNotificationsRead,
+ AccessControlAction.AlertingNotificationsExternalRead,
+ ...PERMISSIONS_TIME_INTERVALS_READ,
+ ]),
+ component: importAlertingComponent(
+ () => import(/* webpackChunkName: "TimeIntervalsPage" */ 'app/features/alerting/unified/TimeIntervalsPage')
+ ),
+ },
{
path: '/alerting/routes/mute-timing/new',
roles: evaluateAccess([
diff --git a/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx b/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx
index 90bfe88ad4b..e29be82b38b 100644
--- a/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx
+++ b/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx
@@ -1,6 +1,6 @@
import { produce } from 'immer';
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
-import { render, screen, userEvent, within } from 'test/test-utils';
+import { render, screen, testWithFeatureToggles, userEvent, within } from 'test/test-utils';
import { byLabelText, byRole, byTestId } from 'testing-library-selector';
import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList';
@@ -140,6 +140,39 @@ const getRootRoute = async () => {
};
describe('NotificationPolicies', () => {
+ describe('V2 Navigation Mode', () => {
+ testWithFeatureToggles({ enable: ['alertingNavigationV2'] });
+
+ beforeEach(() => {
+ setupDataSources(dataSources.am);
+ grantUserPermissions([
+ AccessControlAction.AlertingNotificationsRead,
+ AccessControlAction.AlertingNotificationsWrite,
+ ...PERMISSIONS_NOTIFICATION_POLICIES,
+ ]);
+ });
+
+ it('shows only notification policies without internal tabs', async () => {
+ renderNotificationPolicies();
+
+ // Should show notification policies directly
+ expect(await ui.rootRouteContainer.find()).toBeInTheDocument();
+
+ // Should not have tabs
+ expect(screen.queryByRole('tab')).not.toBeInTheDocument();
+ });
+
+ it('does not show time intervals tab in V2 mode', async () => {
+ renderNotificationPolicies();
+
+ // Should show notification policies
+ expect(await ui.rootRouteContainer.find()).toBeInTheDocument();
+
+ // Should not show time intervals tab
+ expect(screen.queryByText(/time intervals/i)).not.toBeInTheDocument();
+ });
+ });
+
// combobox hack :/
beforeAll(() => {
const mockGetBoundingClientRect = jest.fn(() => ({
diff --git a/public/app/features/alerting/unified/NotificationPoliciesPage.tsx b/public/app/features/alerting/unified/NotificationPoliciesPage.tsx
index 98768e5d87e..fef0a347b97 100644
--- a/public/app/features/alerting/unified/NotificationPoliciesPage.tsx
+++ b/public/app/features/alerting/unified/NotificationPoliciesPage.tsx
@@ -12,6 +12,7 @@ import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alertin
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
import { GrafanaAlertmanagerWarning } from './components/GrafanaAlertmanagerWarning';
import { TimeIntervalsTable } from './components/mute-timings/MuteTimingsTable';
+import { shouldUseAlertingNavigationV2 } from './featureToggles';
import { useNotificationConfigNav } from './navigation/useNotificationConfigNav';
import { useAlertmanager } from './state/AlertmanagerContext';
import { withPageErrorBoundary } from './withPageErrorBoundary';
@@ -107,8 +108,30 @@ function getActiveTabFromUrl(queryParams: UrlQueryMap, defaultTab: ActiveTab): Q
};
}
+const NotificationPoliciesContent = () => {
+ const { selectedAlertmanager = '' } = useAlertmanager();
+ return (
+ <>
+
+
+ >
+ );
+};
+
function NotificationPoliciesPage() {
+ const useV2Nav = shouldUseAlertingNavigationV2();
const { navId, pageNav } = useNotificationConfigNav();
+
+ // In V2 mode, show only notification policies (no internal tabs)
+ if (useV2Nav) {
+ return (
+
+
+
+ );
+ }
+
+ // Legacy mode: Show internal tabs (backward compatible)
return (
diff --git a/public/app/features/alerting/unified/Templates.tsx b/public/app/features/alerting/unified/Templates.tsx
index 74b23cfcc1f..569cc430e22 100644
--- a/public/app/features/alerting/unified/Templates.tsx
+++ b/public/app/features/alerting/unified/Templates.tsx
@@ -1,13 +1,56 @@
import { Route, Routes } from 'react-router-dom-v5-compat';
+import { Trans } from '@grafana/i18n';
+import { LinkButton, Stack, Text } from '@grafana/ui';
+
+import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
import DuplicateMessageTemplate from './components/contact-points/DuplicateMessageTemplate';
import EditMessageTemplate from './components/contact-points/EditMessageTemplate';
import NewMessageTemplate from './components/contact-points/NewMessageTemplate';
+import { NotificationTemplates } from './components/contact-points/NotificationTemplates';
+import { shouldUseAlertingNavigationV2 } from './featureToggles';
+import { AlertmanagerAction, useAlertmanagerAbility } from './hooks/useAbilities';
+import { useNotificationConfigNav } from './navigation/useNotificationConfigNav';
import { withPageErrorBoundary } from './withPageErrorBoundary';
-function NotificationTemplates() {
+const TemplatesList = () => {
+ const [createTemplateSupported, createTemplateAllowed] = useAlertmanagerAbility(
+ AlertmanagerAction.CreateNotificationTemplate
+ );
+
+ return (
+ <>
+
+
+
+ Create notification templates to customize your notifications.
+
+
+ {createTemplateSupported && (
+
+
+ Add notification template group
+
+
+ )}
+
+
+ >
+ );
+};
+
+function NotificationTemplatesRoutes() {
+ const useV2Nav = shouldUseAlertingNavigationV2();
+
return (
+ {/* In V2 mode, show templates list on base route */}
+ {useV2Nav && } />}
} />
} />
} />
@@ -15,4 +58,21 @@ function NotificationTemplates() {
);
}
-export default withPageErrorBoundary(NotificationTemplates);
+function NotificationTemplatesPage() {
+ const useV2Nav = shouldUseAlertingNavigationV2();
+ const { navId, pageNav } = useNotificationConfigNav();
+
+ // In V2 mode, wrap with page wrapper for proper navigation
+ if (useV2Nav) {
+ return (
+
+
+
+ );
+ }
+
+ // In legacy mode, just render routes (templates are accessed via ContactPoints page tabs)
+ return ;
+}
+
+export default withPageErrorBoundary(NotificationTemplatesPage);
diff --git a/public/app/features/alerting/unified/TimeIntervalsPage.test.tsx b/public/app/features/alerting/unified/TimeIntervalsPage.test.tsx
new file mode 100644
index 00000000000..e00f609fbb4
--- /dev/null
+++ b/public/app/features/alerting/unified/TimeIntervalsPage.test.tsx
@@ -0,0 +1,53 @@
+import { render, screen, testWithFeatureToggles } from 'test/test-utils';
+
+import { AccessControlAction } from 'app/types/accessControl';
+
+import TimeIntervalsPage from './TimeIntervalsPage';
+import { setupMswServer } from './mockApi';
+import { grantUserPermissions, mockDataSource } from './mocks';
+import { setupDataSources } from './testSetup/datasources';
+import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
+
+setupMswServer();
+
+const alertManager = mockDataSource({
+ name: 'Alertmanager',
+ type: DataSourceType.Alertmanager,
+});
+
+describe('TimeIntervalsPage', () => {
+ describe('V2 Navigation Mode', () => {
+ testWithFeatureToggles({ enable: ['alertingNavigationV2'] });
+
+ beforeEach(() => {
+ setupDataSources(alertManager);
+ grantUserPermissions([
+ AccessControlAction.AlertingNotificationsRead,
+ AccessControlAction.AlertingNotificationsTimeIntervalsRead,
+ ]);
+ });
+
+ it('renders time intervals table', async () => {
+ render(, {
+ historyOptions: {
+ initialEntries: ['/alerting/time-intervals'],
+ },
+ });
+
+ // Should show time intervals content
+ expect(await screen.findByText(/time intervals/i)).toBeInTheDocument();
+ });
+
+ it('returns null in legacy mode', () => {
+ // This test verifies that the component returns null when V2 is disabled
+ // The feature toggle is controlled by testWithFeatureToggles, so we test it separately
+ const { container } = render(, {
+ historyOptions: {
+ initialEntries: ['/alerting/time-intervals'],
+ },
+ });
+ // In V2 mode (enabled by testWithFeatureToggles), it should render content
+ expect(container).not.toBeEmptyDOMElement();
+ });
+ });
+});
diff --git a/public/app/features/alerting/unified/TimeIntervalsPage.tsx b/public/app/features/alerting/unified/TimeIntervalsPage.tsx
new file mode 100644
index 00000000000..4674ac6699f
--- /dev/null
+++ b/public/app/features/alerting/unified/TimeIntervalsPage.tsx
@@ -0,0 +1,26 @@
+import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
+import { GrafanaAlertmanagerWarning } from './components/GrafanaAlertmanagerWarning';
+import { TimeIntervalsTable } from './components/mute-timings/MuteTimingsTable';
+import { shouldUseAlertingNavigationV2 } from './featureToggles';
+import { useNotificationConfigNav } from './navigation/useNotificationConfigNav';
+import { withPageErrorBoundary } from './withPageErrorBoundary';
+
+function TimeIntervalsPage() {
+ const useV2Nav = shouldUseAlertingNavigationV2();
+ const { navId, pageNav } = useNotificationConfigNav();
+
+ // In V2 mode, wrap with page wrapper for proper navigation
+ if (useV2Nav) {
+ return (
+
+
+
+
+ );
+ }
+
+ // Legacy mode: not used (handled by NotificationPoliciesPage)
+ return null;
+}
+
+export default withPageErrorBoundary(TimeIntervalsPage);
diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx
index db39d474957..d75aef6e52e 100644
--- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx
+++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx
@@ -1,6 +1,14 @@
import { MemoryHistoryBuildOptions } from 'history';
import { ComponentProps, ReactNode } from 'react';
-import { render, screen, userEvent, waitFor, waitForElementToBeRemoved, within } from 'test/test-utils';
+import {
+ render,
+ screen,
+ testWithFeatureToggles,
+ userEvent,
+ waitFor,
+ waitForElementToBeRemoved,
+ within,
+} from 'test/test-utils';
import { selectors } from '@grafana/e2e-selectors';
import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants';
@@ -170,6 +178,30 @@ describe('contact points', () => {
});
});
+ describe('V2 Navigation Mode', () => {
+ testWithFeatureToggles({ enable: ['alertingNavigationV2'] });
+
+ test('shows only contact points without internal tabs', async () => {
+ renderWithProvider();
+
+ // Should show contact points directly
+ expect(await screen.findByText(/create contact point/i)).toBeInTheDocument();
+
+ // Should not have tabs
+ expect(screen.queryByRole('tab')).not.toBeInTheDocument();
+ });
+
+ test('does not show templates tab in V2 mode', async () => {
+ renderWithProvider();
+
+ // Should show contact points
+ expect(await screen.findByText(/create contact point/i)).toBeInTheDocument();
+
+ // Should not show templates tab
+ expect(screen.queryByText(/notification templates/i)).not.toBeInTheDocument();
+ });
+ });
+
describe('templates tab', () => {
it('does not show a warning for a "misconfigured" template', async () => {
renderWithProvider(
diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx
index 59fdb5e7140..123c4fbd1a5 100644
--- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx
+++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx
@@ -19,6 +19,7 @@ import { shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils';
import { makeAMLink, stringifyErrorLike } from 'app/features/alerting/unified/utils/misc';
import { AccessControlAction } from 'app/types/accessControl';
+import { shouldUseAlertingNavigationV2 } from '../../featureToggles';
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
import { usePagination } from '../../hooks/usePagination';
import { useURLSearchParams } from '../../hooks/useURLSearchParams';
@@ -202,6 +203,9 @@ const useTabQueryParam = (defaultTab: ActiveTab) => {
export const ContactPointsPageContents = () => {
const { selectedAlertmanager } = useAlertmanager();
+ const useV2Nav = shouldUseAlertingNavigationV2();
+
+ // All hooks must be called unconditionally before any early returns
const [, canViewContactPoints] = useAlertmanagerAbility(AlertmanagerAction.ViewContactPoint);
const [, canCreateContactPoints] = useAlertmanagerAbility(AlertmanagerAction.CreateContactPoint);
const [, showTemplatesTab] = useAlertmanagerAbility(AlertmanagerAction.ViewNotificationTemplate);
@@ -221,6 +225,19 @@ export const ContactPointsPageContents = () => {
alertmanager: selectedAlertmanager!,
});
+ // In V2 navigation mode, show only contact points (no internal tabs)
+ // Templates are accessible via the sidebar navigation
+ if (useV2Nav) {
+ return (
+ <>
+
+
+ >
+ );
+ }
+
+ // Legacy mode: Show internal tabs (backward compatible)
+
const showingContactPoints = activeTab === ActiveTab.ContactPoints;
const showNotificationTemplates = activeTab === ActiveTab.NotificationTemplates;
diff --git a/public/app/features/alerting/unified/navigation/useNotificationConfigNav.ts b/public/app/features/alerting/unified/navigation/useNotificationConfigNav.ts
index 34b006a7eff..ccf8e1424b3 100644
--- a/public/app/features/alerting/unified/navigation/useNotificationConfigNav.ts
+++ b/public/app/features/alerting/unified/navigation/useNotificationConfigNav.ts
@@ -52,8 +52,11 @@ export function useNotificationConfigNav() {
};
}
- // Check if we're on the routes page with time_intervals tab
- const isTimeIntervalsTab = location.pathname === '/alerting/routes' && location.search.includes('tab=time_intervals');
+ // Check if we're on the time intervals page
+ // In V2 mode, check for dedicated route; in legacy mode, check for query param
+ const isTimeIntervalsTab = useV2Nav
+ ? location.pathname === '/alerting/time-intervals'
+ : location.pathname === '/alerting/routes' && location.search.includes('tab=time_intervals');
// All available tabs
const allTabs: NavModelItem[] = [
@@ -84,7 +87,7 @@ export function useNotificationConfigNav() {
{
id: 'notification-config-time-intervals',
text: t('alerting.navigation.time-intervals', 'Time intervals'),
- url: '/alerting/routes?tab=time_intervals',
+ url: useV2Nav ? '/alerting/time-intervals' : '/alerting/routes?tab=time_intervals',
active: isTimeIntervalsTab,
icon: 'clock-nine',
parentItem: notificationConfigNav,