From 5b228fd7fa4d58362bb110a0ce5bba8d6d676ff4 Mon Sep 17 00:00:00 2001 From: Alejandro Fraenkel Date: Mon, 12 Jan 2026 15:08:43 +0100 Subject: [PATCH] Revert "Remove Insights from navigation sidebar" This reverts commit 307cce059c887e33502f828463b53a0ea7194661. --- pkg/services/navtree/navtreeimpl/navtree.go | 37 +++++- .../navtreeimpl/navtree_alerting_test.go | 5 +- public/app/features/alerting/routes.tsx | 7 ++ .../unified/insights/InsightsPage.tsx | 44 +++++++ .../navigation/useInsightsNav.test.tsx | 118 ++++++++++++++++++ .../unified/navigation/useInsightsNav.ts | 79 ++++++++++++ 6 files changed, 287 insertions(+), 3 deletions(-) create mode 100644 public/app/features/alerting/unified/insights/InsightsPage.tsx create mode 100644 public/app/features/alerting/unified/navigation/useInsightsNav.test.tsx create mode 100644 public/app/features/alerting/unified/navigation/useInsightsNav.ts diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 9a05f87fbca..837cbe3ad81 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -563,7 +563,42 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na }) } - // 4. Settings (parent with tab: Settings) + // 4. Insights (parent with tabs: System Insights, Alert state history) + var insightsChildren []*navtree.NavLink + + // System Insights + if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(ac.ActionAlertingRuleExternalRead))) { + insightsChildren = append(insightsChildren, &navtree.NavLink{ + Text: "System Insights", SubTitle: "View system insights and analytics", Id: "insights-system", Url: s.cfg.AppSubURL + "/alerting/insights", Icon: "chart-line", + }) + } + + // Alert state history + //nolint:staticcheck // not yet migrated to OpenFeature + if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingCentralAlertHistory) { + if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead))) { + insightsChildren = append(insightsChildren, &navtree.NavLink{ + Text: "Alert state history", + SubTitle: "View a history of all alert events generated by your Grafana-managed alert rules. All alert events are displayed regardless of whether silences or mute timings are set.", + Id: "insights-history", + Url: s.cfg.AppSubURL + "/alerting/history", + Icon: "history", + }) + } + } + + if len(insightsChildren) > 0 { + alertChildNavs = append(alertChildNavs, &navtree.NavLink{ + Text: "Insights", + SubTitle: "Analytics and history for alerting", + Id: "insights", + Url: s.cfg.AppSubURL + "/alerting/insights", + Icon: "chart-line", + Children: insightsChildren, + }) + } + + // 5. Settings (parent with tab: Settings) if c.GetOrgRole() == org.RoleAdmin { settingsChildren := []*navtree.NavLink{ { diff --git a/pkg/services/navtree/navtreeimpl/navtree_alerting_test.go b/pkg/services/navtree/navtreeimpl/navtree_alerting_test.go index 8fe57ccb39f..96c7a223d49 100644 --- a/pkg/services/navtree/navtreeimpl/navtree_alerting_test.go +++ b/pkg/services/navtree/navtreeimpl/navtree_alerting_test.go @@ -118,7 +118,7 @@ func TestBuildAlertNavLinks_FeatureToggle(t *testing.T) { require.NotEmpty(t, navLink.Children) // Verify all expected parent items exist with children - expectedParents := []string{"alert-rules", "notification-config", "alerting-settings"} + expectedParents := []string{"alert-rules", "notification-config", "insights", "alerting-settings"} for _, parentId := range expectedParents { parent := findNavLink(navLink, parentId) require.NotNil(t, parent, "Should have %s parent in V2 navigation", parentId) @@ -170,7 +170,7 @@ func TestBuildAlertNavLinks_V2(t *testing.T) { require.NotEmpty(t, navLink.Children) // Verify all parent items exist with children - parentIds := []string{"alert-rules", "notification-config", "alerting-settings"} + parentIds := []string{"alert-rules", "notification-config", "insights", "alerting-settings"} for _, parentId := range parentIds { parent := findNavLink(navLink, parentId) require.NotNil(t, parent, "Should have parent %s in V2 navigation", parentId) @@ -189,6 +189,7 @@ func TestBuildAlertNavLinks_V2(t *testing.T) { }{ {"alert-rules", []string{"alert-rules-list", "alert-rules-recently-deleted"}}, {"notification-config", []string{"notification-config-contact-points", "notification-config-policies", "notification-config-templates", "notification-config-time-intervals"}}, + {"insights", []string{"insights-system", "insights-history"}}, } for _, tt := range tests { diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index 3d0e65dc2f2..e9e12a6b9fc 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -223,6 +223,13 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] { ) ), }, + { + path: '/alerting/insights', + roles: evaluateAccess([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "InsightsPage" */ 'app/features/alerting/unified/insights/InsightsPage') + ), + }, { path: '/alerting/recently-deleted/', roles: () => ['Admin'], diff --git a/public/app/features/alerting/unified/insights/InsightsPage.tsx b/public/app/features/alerting/unified/insights/InsightsPage.tsx new file mode 100644 index 00000000000..be48508d490 --- /dev/null +++ b/public/app/features/alerting/unified/insights/InsightsPage.tsx @@ -0,0 +1,44 @@ +import { useMemo } from 'react'; + +import { Trans, t } from '@grafana/i18n'; + +import { AlertingPageWrapper } from '../components/AlertingPageWrapper'; +import { getInsightsScenes, insightsIsAvailable } from '../home/Insights'; +import { useInsightsNav } from '../navigation/useInsightsNav'; +import { isLocalDevEnv } from '../utils/misc'; +import { withPageErrorBoundary } from '../withPageErrorBoundary'; + +function InsightsPage() { + const insightsEnabled = insightsIsAvailable() || isLocalDevEnv(); + const { navId, pageNav } = useInsightsNav(); + // Memoize the scene so it's only created once and properly initialized + const insightsScene = useMemo(() => getInsightsScenes(), []); + + if (!insightsEnabled) { + return ( + +
+ + Insights are not available. Please configure the required data sources. + +
+
+ ); + } + + return ( + + + + ); +} + +export default withPageErrorBoundary(InsightsPage); diff --git a/public/app/features/alerting/unified/navigation/useInsightsNav.test.tsx b/public/app/features/alerting/unified/navigation/useInsightsNav.test.tsx new file mode 100644 index 00000000000..35122d5c0e9 --- /dev/null +++ b/public/app/features/alerting/unified/navigation/useInsightsNav.test.tsx @@ -0,0 +1,118 @@ +import { renderHook } from '@testing-library/react'; +import { getWrapper } from 'test/test-utils'; + +import { config } from '@grafana/runtime'; +import { configureStore } from 'app/store/configureStore'; + +import { useInsightsNav } from './useInsightsNav'; + +describe('useInsightsNav', () => { + const mockNavIndex = { + insights: { + id: 'insights', + text: 'Insights', + url: '/alerting/insights', + }, + 'insights-system': { + id: 'insights-system', + text: 'System Insights', + url: '/alerting/insights', + }, + 'insights-history': { + id: 'insights-history', + text: 'Alert state history', + url: '/alerting/history', + }, + 'alerts-history': { + id: 'alerts-history', + text: 'History', + url: '/alerting/history', + }, + }; + + const defaultPreloadedState = { + navIndex: mockNavIndex, + }; + + beforeEach(() => { + config.featureToggles.alertingNavigationV2 = false; + }); + + it('should return legacy navId when feature flag is off', () => { + const wrapper = getWrapper({ + preloadedState: defaultPreloadedState, + renderWithRouter: true, + historyOptions: { + initialEntries: ['/alerting/history'], + }, + }); + + const { result } = renderHook(() => useInsightsNav(), { wrapper }); + + expect(result.current.navId).toBe('alerts-history'); + expect(result.current.pageNav).toBeUndefined(); + }); + + it('should return V2 navigation when feature flag is on', () => { + config.featureToggles.alertingNavigationV2 = true; + const store = configureStore(defaultPreloadedState); + const wrapper = getWrapper({ + store, + renderWithRouter: true, + historyOptions: { + initialEntries: ['/alerting/insights'], + }, + }); + + const { result } = renderHook(() => useInsightsNav(), { wrapper }); + + expect(result.current.navId).toBe('insights'); + expect(result.current.pageNav).toBeDefined(); + // eslint-disable-next-line testing-library/no-node-access + expect(result.current.pageNav?.children).toBeDefined(); + }); + + it('should set active tab based on current path', () => { + config.featureToggles.alertingNavigationV2 = true; + const store = configureStore(defaultPreloadedState); + const wrapper = getWrapper({ + store, + renderWithRouter: true, + historyOptions: { + initialEntries: ['/alerting/history'], + }, + }); + + const { result } = renderHook(() => useInsightsNav(), { wrapper }); + + // eslint-disable-next-line testing-library/no-node-access + const historyTab = result.current.pageNav?.children?.find((tab) => tab.id === 'insights-history'); + expect(historyTab?.active).toBe(true); + }); + + it('should filter tabs based on permissions', () => { + config.featureToggles.alertingNavigationV2 = true; + const limitedNavIndex = { + insights: mockNavIndex.insights, + 'insights-system': mockNavIndex['insights-system'], + // Missing 'insights-history' - user doesn't have permission + }; + const store = configureStore({ + navIndex: limitedNavIndex, + }); + const wrapper = getWrapper({ + store, + renderWithRouter: true, + historyOptions: { + initialEntries: ['/alerting/insights'], + }, + }); + + const { result } = renderHook(() => useInsightsNav(), { wrapper }); + + // eslint-disable-next-line testing-library/no-node-access + expect(result.current.pageNav?.children?.length).toBe(1); + // eslint-disable-next-line testing-library/no-node-access + expect(result.current.pageNav?.children?.[0].id).toBe('insights-system'); + }); +}); diff --git a/public/app/features/alerting/unified/navigation/useInsightsNav.ts b/public/app/features/alerting/unified/navigation/useInsightsNav.ts new file mode 100644 index 00000000000..7d888c30d5c --- /dev/null +++ b/public/app/features/alerting/unified/navigation/useInsightsNav.ts @@ -0,0 +1,79 @@ +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 { shouldUseAlertingNavigationV2 } from '../featureToggles'; + +export function useInsightsNav() { + const location = useLocation(); + const navIndex = useSelector((state) => state.navIndex); + const useV2Nav = shouldUseAlertingNavigationV2(); + + // If V2 navigation is not enabled, return legacy navId + if (!useV2Nav) { + if (location.pathname === '/alerting/history') { + return { + navId: 'alerts-history', + pageNav: undefined, + }; + } + // For insights page, it doesn't exist in legacy, so return undefined + return { + navId: undefined, + pageNav: undefined, + }; + } + + const insightsNav = navIndex.insights; + if (!insightsNav) { + // Fallback to legacy + if (location.pathname === '/alerting/history') { + return { + navId: 'alerts-history', + pageNav: undefined, + }; + } + return { + navId: undefined, + pageNav: undefined, + }; + } + + // All available tabs + const allTabs = [ + { + id: 'insights-system', + text: t('alerting.navigation.system-insights', 'System Insights'), + url: '/alerting/insights', + active: location.pathname === '/alerting/insights', + icon: 'chart-line', + parentItem: insightsNav, + }, + { + id: 'insights-history', + text: t('alerting.navigation.alert-state-history', 'Alert state history'), + url: '/alerting/history', + active: location.pathname === '/alerting/history', + icon: 'history', + parentItem: insightsNav, + }, + ].filter((tab) => { + // Filter based on permissions - if nav item doesn't exist, user doesn't have permission + const navItem = navIndex[tab.id]; + return navItem !== undefined; + }); + + // Create pageNav that represents the Insights page with tabs as children + const pageNav: NavModelItem = { + ...insightsNav, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + children: allTabs as NavModelItem[], + }; + + return { + navId: 'insights', + pageNav, + }; +}