diff --git a/public/app/features/dashboard/utils/getPanelMenu.test.ts b/public/app/features/dashboard/utils/getPanelMenu.test.ts index 2afb76ce4d4..e3fa26d9682 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.test.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.test.ts @@ -12,8 +12,10 @@ import { } from '@grafana/data'; import { AngularComponent, getPluginLinkExtensions } from '@grafana/runtime'; import config from 'app/core/config'; +import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import * as actions from 'app/features/explore/state/main'; import { setStore } from 'app/store/store'; +import { AccessControlAction } from 'app/types'; import { PanelModel } from '../state'; import { createDashboardModelFixture } from '../state/__fixtures__/dashboardFixtures'; @@ -23,6 +25,7 @@ import { getPanelMenu } from './getPanelMenu'; jest.mock('app/core/services/context_srv', () => ({ contextSrv: { hasAccessToExplore: () => true, + hasPermission: jest.fn(), }, })); @@ -38,6 +41,8 @@ describe('getPanelMenu()', () => { beforeEach(() => { getPluginLinkExtensionsMock.mockRestore(); getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] }); + grantUserPermissions([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleUpdate]); + config.unifiedAlertingEnabled = false; }); it('should return the correct panel menu items', () => { @@ -619,4 +624,62 @@ describe('getPanelMenu()', () => { expect(windowOpen).toHaveBeenLastCalledWith(`${testSubUrl}${testUrl}`); }); }); + describe('Alerting menu', () => { + it('should render alerting Menu with correct sub menu if user has permissions to read and update alerts ', () => { + const panel = new PanelModel({}); + + const dashboard = createDashboardModelFixture({}); + config.unifiedAlertingEnabled = true; + grantUserPermissions([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleUpdate]); + const menuItems = getPanelMenu(dashboard, panel); + const alertingSubMenu = menuItems.find((i) => i.text === 'Alerting')?.subMenu; + + expect(alertingSubMenu).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + text: 'View all alert rules', + }), + expect.objectContaining({ + text: 'Create alert rule from this panel', + }), + ]) + ); + }); + + it('should not render create alert submenu item in the Alerting subMenu, if user does not have permissions to update alerts ', () => { + const panel = new PanelModel({}); + const dashboard = createDashboardModelFixture({}); + + grantUserPermissions([AccessControlAction.AlertingRuleRead]); + config.unifiedAlertingEnabled = true; + + const menuItems = getPanelMenu(dashboard, panel); + + const alertingSubMenu = menuItems.find((i) => i.text === 'Alerting')?.subMenu; + + expect(alertingSubMenu).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + text: 'View all alert rules', + }), + expect.not.objectContaining({ + text: 'Create alert rule from this panel', + }), + ]) + ); + }); + it('should not render Alerting in menu, if user does not have permissions to read update alerts ', () => { + const panel = new PanelModel({}); + + const dashboard = createDashboardModelFixture({}); + grantUserPermissions([]); + config.unifiedAlertingEnabled = true; + + const menuItems = getPanelMenu(dashboard, panel); + + const alertingSubMenu = menuItems.find((i) => i.text === 'Alerting')?.subMenu; + + expect(alertingSubMenu).toBeUndefined(); + }); + }); }); diff --git a/public/app/features/dashboard/utils/getPanelMenu.ts b/public/app/features/dashboard/utils/getPanelMenu.ts index f5725cc1f36..3ea0f581840 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.ts @@ -3,14 +3,18 @@ import { PanelMenuItem, PluginExtensionLink, PluginExtensionPoints, + urlUtil, type PluginExtensionPanelContext, } from '@grafana/data'; -import { AngularComponent, locationService, reportInteraction, getPluginLinkExtensions } from '@grafana/runtime'; +import { AngularComponent, getPluginLinkExtensions, locationService, reportInteraction } from '@grafana/runtime'; import { PanelCtrl } from 'app/angular/panel/panel_ctrl'; -import config from 'app/core/config'; +import config, { getConfig } from 'app/core/config'; import { t } from 'app/core/internationalization'; import { contextSrv } from 'app/core/services/context_srv'; import { getExploreUrl } from 'app/core/utils/explore'; +import { getRulesPermissions } from 'app/features/alerting/unified/utils/access-control'; +import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; +import { panelToRuleFormValues } from 'app/features/alerting/unified/utils/rule-form'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { @@ -202,6 +206,77 @@ export function getPanelMenu( subMenu: inspectMenu, }); + const createAlert = async () => { + const formValues = await panelToRuleFormValues(panel, dashboard); + + const ruleFormUrl = urlUtil.renderUrl('/alerting/new', { + defaults: JSON.stringify(formValues), + returnTo: location.pathname + location.search, + }); + + locationService.push(ruleFormUrl); + }; + + const navigateToAlertListView = async () => { + const alertListUrl = urlUtil.renderUrl('/alerting/list', { + returnTo: location.pathname + location.search, + }); + + locationService.push(alertListUrl); + }; + + const onNavigateToAlertListView = (event: React.MouseEvent) => { + event.preventDefault(); + navigateToAlertListView(); + reportInteraction('dashboards_panelheader_menu', { item: 'create-alert' }); + }; + + const onCreateAlert = (event: React.MouseEvent) => { + event.preventDefault(); + createAlert(); + reportInteraction('dashboards_panelheader_menu', { item: 'create-alert' }); + }; + + const { unifiedAlertingEnabled } = getConfig(); + const hasRuleReadPermissions = contextSrv.hasPermission(getRulesPermissions(GRAFANA_RULES_SOURCE_NAME).read); + const hasRuleUpdatePermissions = contextSrv.hasPermission(getRulesPermissions(GRAFANA_RULES_SOURCE_NAME).update); + const isAlertingAvailableForRead = unifiedAlertingEnabled && hasRuleReadPermissions; + + const alertingMenuAvailable = isAlertingAvailableForRead; + if (alertingMenuAvailable) { + // prepare submenu depending on permissions + const subMenu: PanelMenuItem[] = []; + if (hasRuleUpdatePermissions) { + subMenu.push({ + text: t('panel.header-menu.create-alert', `Create alert rule from this panel`), + onClick: (e: React.MouseEvent) => onCreateAlert(e), + }); + } + subMenu.push({ + text: t('panel.header-menu.view-alerts', `View all alert rules`), + onClick: (e: React.MouseEvent) => onNavigateToAlertListView(e), + }); + + menu.push({ + type: 'submenu', + text: t('panel.header-menu.alerting', `Alerting`), + iconClassName: 'bell', + onClick: (e: React.MouseEvent) => { + const currentTarget = e.currentTarget; + const target = e.target; + + if ( + target === currentTarget || + (target instanceof HTMLElement && target.closest('[role="menuitem"]') === currentTarget) + ) { + onInspectPanel(); + } + }, + shortcut: 'a', + subMenu: subMenu, + }); + } + const subMenu: PanelMenuItem[] = []; const canEdit = dashboard.canEditPanel(panel); if (!(panel.isViewing || panel.isEditing)) {