Compare commits

...

2 Commits

Author SHA1 Message Date
Alejandro Fraenkel
32975dd0a8 chore(i18n): extract translations for alert rules navigation
Add translation strings for the new Alert Rules navigation tabs:
- alerting.navigation.alert-rules
- alerting.navigation.recently-deleted

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-14 13:15:50 +01:00
Alejandro Fraenkel
f7b7c53a09 feat(alerting): add Alert Rules tabs navigation with feature toggle
This change introduces Alert Rules tabs (list + recently deleted) when
the alertingNavigationV2 feature toggle is enabled, while maintaining
backward compatibility with the legacy navigation structure.

**Backend changes (navtree.go):**
- Add conditional navigation structure based on alertingNavigationV2 toggle
- When enabled: Create "alert-rules" parent with child nav items for permission checking
- Add "alert-rules-list" and "alert-rules-recently-deleted" child items
- Hide standalone "Recently deleted" menu item in V2 mode (shown as tab instead)
- When disabled: Keep legacy "alert-list" navigation structure

**Frontend changes:**
- Create useAlertRulesNav hook to manage Alert Rules navigation
- Hook checks feature toggle and returns appropriate navId and pageNav
- Filters tabs based on user permissions (via navIndex)
- Update RuleList.v1.tsx and RuleList.v2.tsx to use the hook
- Pass navId and pageNav to AlertingPageWrapper for proper navigation rendering

**Testing:**
- Go backend builds successfully with no errors
- Feature toggle alertingNavigationV2 exists and is properly configured

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-14 12:12:08 +01:00
5 changed files with 117 additions and 11 deletions

View File

@@ -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 {

View File

@@ -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,
};
}

View File

@@ -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
<AlertingPageWrapper
navId="alert-list"
navId={navId}
pageNav={pageNav}
isLoading={false}
renderTitle={(title) => <RuleListPageTitle title={title} />}
actions={<RuleListActionButtons hasAlertRulesCreated={hasAlertRulesCreated} />}

View File

@@ -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 (
<AlertingPageWrapper
navId="alert-list"
navId={navId}
pageNav={pageNav}
renderTitle={(title) => <RuleListPageTitle title={title} />}
isLoading={isApplying}
actions={<RuleListActions />}

View File

@@ -1929,6 +1929,10 @@
"select-group": "Select group",
"select-namespace": "Select namespace"
},
"navigation": {
"alert-rules": "Alert rules",
"recently-deleted": "Recently deleted"
},
"need-help-info": {
"need-help": "Need help?"
},