diff --git a/public/app/features/alerting/unified/components/rule-editor/AnnotationsField.test.tsx b/public/app/features/alerting/unified/components/rule-editor/AnnotationsField.test.tsx index 5ca9a009cc2..c9b25fe59b5 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AnnotationsField.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AnnotationsField.test.tsx @@ -296,39 +296,39 @@ describe('AnnotationsField', function () { expect(annotationKeyElements[1]).toHaveTextContent('Panel ID'); expect(annotationValueElements[1]).toHaveTextContent('3'); }); - - it('should render warning icon for panels of type other than graph and timeseries', async function () { - mockSearchApiResponse(server, [ - mockDashboardSearchItem({ title: 'My dashboard', uid: 'dash-test-uid', type: DashboardSearchItemType.DashDB }), - ]); - - mockGetDashboardResponse( - mockDashboardDto({ - title: 'My dashboard', - uid: 'dash-test-uid', - panels: [ - { id: 1, title: 'First panel', type: 'bar' }, - { id: 2, title: 'Second panel', type: 'graph' }, - ], - }) - ); - - const user = userEvent.setup(); - - render(); - - const { dialog } = ui.dashboardPicker; - - await user.click(ui.setDashboardButton.get()); - await user.click(await findByTitle(dialog.get(), 'My dashboard')); - - const warnedPanel = await findByRole(dialog.get(), 'button', { name: /First panel/ }); - - expect(getByTestId(warnedPanel, 'warning-icon')).toBeInTheDocument(); - }); }); }); +it('should render warning icon for panels of type other than graph and timeseries', async function () { + mockSearchApiResponse(server, [ + mockDashboardSearchItem({ title: 'My dashboard', uid: 'dash-test-uid', type: DashboardSearchItemType.DashDB }), + ]); + + mockGetDashboardResponse( + mockDashboardDto({ + title: 'My dashboard', + uid: 'dash-test-uid', + panels: [ + { id: 1, title: 'First panel', type: 'bar' }, + { id: 2, title: 'Second panel', type: 'graph' }, + ], + }) + ); + + const user = userEvent.setup(); + + render(); + + const { dialog } = ui.dashboardPicker; + + await user.click(ui.setDashboardButton.get()); + await user.click(await findByTitle(dialog.get(), 'My dashboard')); + + const warnedPanel = await findByRole(dialog.get(), 'button', { name: /First panel/ }); + + expect(getByTestId(warnedPanel, 'warning-icon')).toBeInTheDocument(); +}); + function mockGetDashboardResponse(dashboard: DashboardDTO) { server.use( rest.get(`/api/dashboards/uid/${dashboard.dashboard.uid}`, (req, res, ctx) => diff --git a/public/app/features/alerting/unified/utils/access-control.ts b/public/app/features/alerting/unified/utils/access-control.ts index d71134a4659..46ca371882c 100644 --- a/public/app/features/alerting/unified/utils/access-control.ts +++ b/public/app/features/alerting/unified/utils/access-control.ts @@ -1,7 +1,8 @@ +import { getConfig } from 'app/core/config'; import { contextSrv } from 'app/core/services/context_srv'; import { AccessControlAction } from 'app/types'; -import { isGrafanaRulesSource } from './datasource'; +import { GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from './datasource'; type RulesSourceType = 'grafana' | 'external'; @@ -128,3 +129,12 @@ export function getRulesAccess() { contextSrv.hasPermission(provisioningPermissions.readSecrets), }; } + +export function getCreateAlertInMenuAvailability() { + 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; + + return isAlertingAvailableForRead && hasRuleUpdatePermissions; +} diff --git a/public/app/features/dashboard/utils/getPanelMenu.test.ts b/public/app/features/dashboard/utils/getPanelMenu.test.ts index 2afb76ce4d4..ad194684d10 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,57 @@ describe('getPanelMenu()', () => { expect(windowOpen).toHaveBeenLastCalledWith(`${testSubUrl}${testUrl}`); }); }); + describe('Alerting menu', () => { + it('should render Create alert menu item 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 moreSubMenu = menuItems.find((i) => i.text === 'More...')?.subMenu; + + expect(moreSubMenu).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + text: 'Create alert', + }), + ]) + ); + }); + + it('should not render Create alert menu item, 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 moreSubMenu = menuItems.find((i) => i.text === 'More...')?.subMenu; + + expect(moreSubMenu).toEqual( + expect.arrayContaining([ + expect.not.objectContaining({ + text: 'Create alert', + }), + ]) + ); + }); + it('should not render Create alert menu item, 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 moreSubMenu = menuItems.find((i) => i.text === 'More...')?.subMenu; + const createAlertOption = moreSubMenu?.find((i) => i.text === 'Create alert')?.subMenu; + + expect(createAlertOption).toBeUndefined(); + }); + }); }); diff --git a/public/app/features/dashboard/utils/getPanelMenu.ts b/public/app/features/dashboard/utils/getPanelMenu.ts index f5725cc1f36..0894368211d 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.ts @@ -3,14 +3,16 @@ 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 { t } from 'app/core/internationalization'; import { contextSrv } from 'app/core/services/context_srv'; import { getExploreUrl } from 'app/core/utils/explore'; +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 { @@ -28,6 +30,7 @@ import { truncateTitle } from 'app/features/plugins/extensions/utils'; import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; import { store } from 'app/store/store'; +import { getCreateAlertInMenuAvailability } from '../../alerting/unified/utils/access-control'; import { navigateToExplore } from '../../explore/state/main'; import { getTimeSrv } from '../services/TimeSrv'; @@ -202,8 +205,27 @@ 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 onCreateAlert = (event: React.MouseEvent) => { + event.preventDefault(); + createAlert(); + reportInteraction('dashboards_panelheader_menu', { item: 'create-alert' }); + }; + const subMenu: PanelMenuItem[] = []; const canEdit = dashboard.canEditPanel(panel); + const isCreateAlertMenuOptionAvailable = getCreateAlertInMenuAvailability(); + if (!(panel.isViewing || panel.isEditing)) { if (canEdit) { subMenu.push({ @@ -237,6 +259,13 @@ export function getPanelMenu( } } + if (isCreateAlertMenuOptionAvailable) { + subMenu.push({ + text: t('panel.header-menu.create-alert', `Create alert`), + onClick: onCreateAlert, + }); + } + // add old angular panel options if (angularComponent) { const scope = angularComponent.getScope(); @@ -273,6 +302,12 @@ export function getPanelMenu( // When editing hide most actions if (panel.isEditing) { subMenu.length = 0; + if (isCreateAlertMenuOptionAvailable) { + subMenu.push({ + text: t('panel.header-menu.create-alert', `Create alert`), + onClick: onCreateAlert, + }); + } } if (canEdit && panel.plugin && !panel.plugin.meta.skipDataQuery) { diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index fe5d7e7981d..d2bd547ab4d 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -773,6 +773,7 @@ "panel": { "header-menu": { "copy": "Kopieren", + "create-alert": "", "create-library-panel": "Bibliotheksleiste erstellen", "duplicate": "Duplikat", "edit": "Bearbeiten", diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 29c575e4c4b..d0369489aa9 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -773,6 +773,7 @@ "panel": { "header-menu": { "copy": "Copy", + "create-alert": "Create alert", "create-library-panel": "Create library panel", "duplicate": "Duplicate", "edit": "Edit", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index ca9fd8b7530..d80df14b05f 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -779,6 +779,7 @@ "panel": { "header-menu": { "copy": "Copiar", + "create-alert": "", "create-library-panel": "Crear panel de librería", "duplicate": "Duplicar", "edit": "Editar", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index e887ae1378b..f703114f1ff 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -779,6 +779,7 @@ "panel": { "header-menu": { "copy": "Copier", + "create-alert": "", "create-library-panel": "Créer un panneau Bibliothèque", "duplicate": "Dupliquer", "edit": "Modifier", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index c1860822a00..172d2a21114 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -773,6 +773,7 @@ "panel": { "header-menu": { "copy": "Cőpy", + "create-alert": "Cřęäŧę äľęřŧ", "create-library-panel": "Cřęäŧę ľįþřäřy päʼnęľ", "duplicate": "Đūpľįčäŧę", "edit": "Ēđįŧ", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 8f7810c189e..9e5da4eb6d7 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -767,6 +767,7 @@ "panel": { "header-menu": { "copy": "复制", + "create-alert": "", "create-library-panel": "创建库面板", "duplicate": "复制", "edit": "编辑",