diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index a1ef1aab1ef..9be53a01c6f 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -446,9 +446,32 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na } if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(ac.ActionAlertingRuleExternalRead))) { - alertChildNavs = append(alertChildNavs, &navtree.NavLink{ - Text: "Alert rules", SubTitle: "Rules that determine whether an alert will fire", Id: "alert-list", Url: s.cfg.AppSubURL + "/alerting/list", Icon: "list-ul", - }) + //nolint:staticcheck // not yet migrated to OpenFeature + if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingNavigationV2) { + // New navigation: Alert rules parent (tabs managed on frontend) + alertChildNavs = append(alertChildNavs, &navtree.NavLink{ + Text: "Alert rules", SubTitle: "Rules that determine whether an alert will fire", Id: "alert-rules", Url: s.cfg.AppSubURL + "/alerting/list", Icon: "list-ul", + }) + + // Add child nav items for permission checking (tabs will be rendered by frontend) + // Alert rules list tab + alertChildNavs = append(alertChildNavs, &navtree.NavLink{ + Text: "Alert rules", Id: "alert-rules-list", Url: s.cfg.AppSubURL + "/alerting/list", + }) + + // Recently deleted tab (check additional feature flags) + //nolint:staticcheck // not yet migrated to OpenFeature + if c.GetOrgRole() == org.RoleAdmin && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertRuleRestore) && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingRuleRecoverDeleted) { + alertChildNavs = append(alertChildNavs, &navtree.NavLink{ + Text: "Recently deleted", Id: "alert-rules-recently-deleted", Url: s.cfg.AppSubURL + "/alerting/recently-deleted", + }) + } + } else { + // Legacy navigation + alertChildNavs = append(alertChildNavs, &navtree.NavLink{ + Text: "Alert rules", SubTitle: "Rules that determine whether an alert will fire", Id: "alert-list", Url: s.cfg.AppSubURL + "/alerting/list", Icon: "list-ul", + }) + } } contactPointsPerms := []ac.Evaluator{ @@ -508,12 +531,16 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na } //nolint:staticcheck // not yet migrated to OpenFeature if c.GetOrgRole() == org.RoleAdmin && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertRuleRestore) && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingRuleRecoverDeleted) { - alertChildNavs = append(alertChildNavs, &navtree.NavLink{ - Text: "Recently deleted", - SubTitle: "Any items listed here for more than 30 days will be automatically deleted.", - Id: "alerts/recently-deleted", - Url: s.cfg.AppSubURL + "/alerting/recently-deleted", - }) + // Only show as standalone item in legacy navigation (V2 shows it as a tab under Alert rules) + //nolint:staticcheck // not yet migrated to OpenFeature + if !s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingNavigationV2) { + alertChildNavs = append(alertChildNavs, &navtree.NavLink{ + Text: "Recently deleted", + SubTitle: "Any items listed here for more than 30 days will be automatically deleted.", + Id: "alerts/recently-deleted", + Url: s.cfg.AppSubURL + "/alerting/recently-deleted", + }) + } } if c.GetOrgRole() == org.RoleAdmin { diff --git a/public/app/features/alerting/unified/navigation/useAlertRulesNav.ts b/public/app/features/alerting/unified/navigation/useAlertRulesNav.ts new file mode 100644 index 00000000000..a3f0a560904 --- /dev/null +++ b/public/app/features/alerting/unified/navigation/useAlertRulesNav.ts @@ -0,0 +1,69 @@ +import { useLocation } from 'react-router-dom-v5-compat'; + +import { NavModelItem } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; +import { useSelector } from 'app/types/store'; + +export function useAlertRulesNav() { + const location = useLocation(); + const navIndex = useSelector((state) => state.navIndex); + + // Check if V2 navigation is enabled + const useV2Nav = config.featureToggles.alertingNavigationV2; + + if (!useV2Nav) { + // Legacy navigation: return simple navId + return { + navId: 'alert-list', + pageNav: undefined, + }; + } + + // V2 Navigation: Create tabs structure + const alertRulesNav = navIndex['alert-rules']; + + if (!alertRulesNav) { + // Fallback to legacy if nav item doesn't exist + return { + navId: 'alert-list', + pageNav: undefined, + }; + } + + // All available tabs + const allTabs = [ + { + id: 'alert-rules-list', + text: t('alerting.navigation.alert-rules', 'Alert rules'), + url: '/alerting/list', + active: location.pathname === '/alerting/list', + icon: 'list-ul', + parentItem: alertRulesNav, + }, + { + id: 'alert-rules-recently-deleted', + text: t('alerting.navigation.recently-deleted', 'Recently deleted'), + url: '/alerting/recently-deleted', + active: location.pathname === '/alerting/recently-deleted', + icon: 'history', + parentItem: alertRulesNav, + }, + ].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 Alert rules page with tabs as children + const pageNav: NavModelItem = { + ...alertRulesNav, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + children: allTabs as NavModelItem[], + }; + + return { + navId: 'alert-rules', + pageNav, + }; +} diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx index bec19c1cc99..2ecb5ae182a 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx @@ -20,6 +20,7 @@ import { shouldUsePrometheusRulesPrimary } from '../featureToggles'; import { useCombinedRuleNamespaces } from '../hooks/useCombinedRuleNamespaces'; import { useFilteredRules, useRulesFilter } from '../hooks/useFilteredRules'; import { useUnifiedAlertingSelector } from '../hooks/useUnifiedAlertingSelector'; +import { useAlertRulesNav } from '../navigation/useAlertRulesNav'; import { fetchAllPromAndRulerRulesAction, fetchAllPromRulesAction, fetchRulerRulesAction } from '../state/actions'; import { RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants'; import { GRAFANA_RULES_SOURCE_NAME, getAllRulesSourceNames } from '../utils/datasource'; @@ -38,6 +39,7 @@ const LIMIT_ALERTS = INSTANCES_DISPLAY_LIMIT + 1; const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary(); const RuleListV1 = () => { + const { navId, pageNav } = useAlertRulesNav(); const dispatch = useDispatch(); const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []); const [expandAll, setExpandAll] = useState(false); @@ -119,7 +121,8 @@ const RuleListV1 = () => { // We don't want to show the Loading... indicator for the whole page. // We show separate indicators for Grafana-managed and Cloud rules } actions={} diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx index b90663200ce..4c50aff808b 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx @@ -13,6 +13,7 @@ import { useListViewMode } from '../components/rules/Filter/RulesViewModeSelecto import { AIAlertRuleButtonComponent } from '../enterprise-components/AI/AIGenAlertRuleButton/addAIAlertRuleButton'; import { AlertingAction, useAlertingAbility } from '../hooks/useAbilities'; import { useRulesFilter } from '../hooks/useFilteredRules'; +import { useAlertRulesNav } from '../navigation/useAlertRulesNav'; import { getRulesDataSources } from '../utils/datasource'; import { FilterView } from './FilterView'; @@ -123,10 +124,12 @@ export function RuleListActions() { export default function RuleListPage() { const { isApplying } = useApplyDefaultSearch(); + const { navId, pageNav } = useAlertRulesNav(); return ( } isLoading={isApplying} actions={}