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(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) {
server.use(
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 { 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;
}

View File

@ -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();
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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