Alerting: Add Alerting menu in getPanelMenu (#76618)

* Add Alerting menu in getPanelMenu

* Add translations

* Allow alert tab, heart icon in all panel types, and not show warning in DashobardPicker panels

* Fix tests

* Move alerting submenu under 'More...' item

* Move create alert menu item to More... without submenu

* Update translations

* Revert "Allow alert tab, heart icon in all panel types, and not show warning in DashobardPicker panels"

This reverts commit 225da3f60e.

* Revert allowing alert tab and health icon for all panel types

* use onCreateAlert method name in onClick instead of new function

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>

* Move getAlertingMenuAvailability method to a /features/alerting folder and rename it to getCreateAlertInMenuAvailability

* Use onCreate direclty instead of a new method

* Make getCreateAlertInMenuAvailability to return a boolean

---------

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
Sonia Aguilar 2023-10-18 14:13:38 +02:00 committed by GitHub
parent a851750b1c
commit 1de65bb384
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 141 additions and 32 deletions

View File

@ -296,39 +296,39 @@ describe('AnnotationsField', function () {
expect(annotationKeyElements[1]).toHaveTextContent('Panel ID'); expect(annotationKeyElements[1]).toHaveTextContent('Panel ID');
expect(annotationValueElements[1]).toHaveTextContent('3'); 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(<FormWrapper formValues={{ annotations: [] }} />);
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(<FormWrapper formValues={{ annotations: [] }} />);
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) { function mockGetDashboardResponse(dashboard: DashboardDTO) {
server.use( server.use(
rest.get(`/api/dashboards/uid/${dashboard.dashboard.uid}`, (req, res, ctx) => rest.get(`/api/dashboards/uid/${dashboard.dashboard.uid}`, (req, res, ctx) =>

View File

@ -1,7 +1,8 @@
import { getConfig } from 'app/core/config';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { AccessControlAction } from 'app/types'; import { AccessControlAction } from 'app/types';
import { isGrafanaRulesSource } from './datasource'; import { GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from './datasource';
type RulesSourceType = 'grafana' | 'external'; type RulesSourceType = 'grafana' | 'external';
@ -128,3 +129,12 @@ export function getRulesAccess() {
contextSrv.hasPermission(provisioningPermissions.readSecrets), 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;
}

View File

@ -12,8 +12,10 @@ import {
} from '@grafana/data'; } from '@grafana/data';
import { AngularComponent, getPluginLinkExtensions } from '@grafana/runtime'; import { AngularComponent, getPluginLinkExtensions } from '@grafana/runtime';
import config from 'app/core/config'; import config from 'app/core/config';
import { grantUserPermissions } from 'app/features/alerting/unified/mocks';
import * as actions from 'app/features/explore/state/main'; import * as actions from 'app/features/explore/state/main';
import { setStore } from 'app/store/store'; import { setStore } from 'app/store/store';
import { AccessControlAction } from 'app/types';
import { PanelModel } from '../state'; import { PanelModel } from '../state';
import { createDashboardModelFixture } from '../state/__fixtures__/dashboardFixtures'; import { createDashboardModelFixture } from '../state/__fixtures__/dashboardFixtures';
@ -23,6 +25,7 @@ import { getPanelMenu } from './getPanelMenu';
jest.mock('app/core/services/context_srv', () => ({ jest.mock('app/core/services/context_srv', () => ({
contextSrv: { contextSrv: {
hasAccessToExplore: () => true, hasAccessToExplore: () => true,
hasPermission: jest.fn(),
}, },
})); }));
@ -38,6 +41,8 @@ describe('getPanelMenu()', () => {
beforeEach(() => { beforeEach(() => {
getPluginLinkExtensionsMock.mockRestore(); getPluginLinkExtensionsMock.mockRestore();
getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] }); getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] });
grantUserPermissions([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleUpdate]);
config.unifiedAlertingEnabled = false;
}); });
it('should return the correct panel menu items', () => { it('should return the correct panel menu items', () => {
@ -619,4 +624,57 @@ describe('getPanelMenu()', () => {
expect(windowOpen).toHaveBeenLastCalledWith(`${testSubUrl}${testUrl}`); 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();
});
});
}); });

View File

@ -3,14 +3,16 @@ import {
PanelMenuItem, PanelMenuItem,
PluginExtensionLink, PluginExtensionLink,
PluginExtensionPoints, PluginExtensionPoints,
urlUtil,
type PluginExtensionPanelContext, type PluginExtensionPanelContext,
} from '@grafana/data'; } 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 { PanelCtrl } from 'app/angular/panel/panel_ctrl';
import config from 'app/core/config'; import config from 'app/core/config';
import { t } from 'app/core/internationalization'; import { t } from 'app/core/internationalization';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { getExploreUrl } from 'app/core/utils/explore'; 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 { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { import {
@ -28,6 +30,7 @@ import { truncateTitle } from 'app/features/plugins/extensions/utils';
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
import { store } from 'app/store/store'; import { store } from 'app/store/store';
import { getCreateAlertInMenuAvailability } from '../../alerting/unified/utils/access-control';
import { navigateToExplore } from '../../explore/state/main'; import { navigateToExplore } from '../../explore/state/main';
import { getTimeSrv } from '../services/TimeSrv'; import { getTimeSrv } from '../services/TimeSrv';
@ -202,8 +205,27 @@ export function getPanelMenu(
subMenu: inspectMenu, 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 subMenu: PanelMenuItem[] = [];
const canEdit = dashboard.canEditPanel(panel); const canEdit = dashboard.canEditPanel(panel);
const isCreateAlertMenuOptionAvailable = getCreateAlertInMenuAvailability();
if (!(panel.isViewing || panel.isEditing)) { if (!(panel.isViewing || panel.isEditing)) {
if (canEdit) { if (canEdit) {
subMenu.push({ 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 // add old angular panel options
if (angularComponent) { if (angularComponent) {
const scope = angularComponent.getScope(); const scope = angularComponent.getScope();
@ -273,6 +302,12 @@ export function getPanelMenu(
// When editing hide most actions // When editing hide most actions
if (panel.isEditing) { if (panel.isEditing) {
subMenu.length = 0; 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) { if (canEdit && panel.plugin && !panel.plugin.meta.skipDataQuery) {

View File

@ -773,6 +773,7 @@
"panel": { "panel": {
"header-menu": { "header-menu": {
"copy": "Kopieren", "copy": "Kopieren",
"create-alert": "",
"create-library-panel": "Bibliotheksleiste erstellen", "create-library-panel": "Bibliotheksleiste erstellen",
"duplicate": "Duplikat", "duplicate": "Duplikat",
"edit": "Bearbeiten", "edit": "Bearbeiten",

View File

@ -773,6 +773,7 @@
"panel": { "panel": {
"header-menu": { "header-menu": {
"copy": "Copy", "copy": "Copy",
"create-alert": "Create alert",
"create-library-panel": "Create library panel", "create-library-panel": "Create library panel",
"duplicate": "Duplicate", "duplicate": "Duplicate",
"edit": "Edit", "edit": "Edit",

View File

@ -779,6 +779,7 @@
"panel": { "panel": {
"header-menu": { "header-menu": {
"copy": "Copiar", "copy": "Copiar",
"create-alert": "",
"create-library-panel": "Crear panel de librería", "create-library-panel": "Crear panel de librería",
"duplicate": "Duplicar", "duplicate": "Duplicar",
"edit": "Editar", "edit": "Editar",

View File

@ -779,6 +779,7 @@
"panel": { "panel": {
"header-menu": { "header-menu": {
"copy": "Copier", "copy": "Copier",
"create-alert": "",
"create-library-panel": "Créer un panneau Bibliothèque", "create-library-panel": "Créer un panneau Bibliothèque",
"duplicate": "Dupliquer", "duplicate": "Dupliquer",
"edit": "Modifier", "edit": "Modifier",

View File

@ -773,6 +773,7 @@
"panel": { "panel": {
"header-menu": { "header-menu": {
"copy": "Cőpy", "copy": "Cőpy",
"create-alert": "Cřęäŧę äľęřŧ",
"create-library-panel": "Cřęäŧę ľįþřäřy päʼnęľ", "create-library-panel": "Cřęäŧę ľįþřäřy päʼnęľ",
"duplicate": "Đūpľįčäŧę", "duplicate": "Đūpľįčäŧę",
"edit": "Ēđįŧ", "edit": "Ēđįŧ",

View File

@ -767,6 +767,7 @@
"panel": { "panel": {
"header-menu": { "header-menu": {
"copy": "复制", "copy": "复制",
"create-alert": "",
"create-library-panel": "创建库面板", "create-library-panel": "创建库面板",
"duplicate": "复制", "duplicate": "复制",
"edit": "编辑", "edit": "编辑",