Dashboard Scene: Fix permissions and error handling in creating alert from panel (#94521)

* Dashboard Scene: Add better error handling when creating alert from panel menu

* Add missing permission to createNewAlert and add unit test
pull/94709/head
Alexa V 7 months ago committed by GitHub
parent e746f55126
commit 00af0afe52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 121
      public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx
  2. 37
      public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx

@ -6,9 +6,10 @@ import {
PluginExtensionTypes,
getDefaultTimeRange,
toDataFrame,
urlUtil,
} from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { getPluginLinkExtensions, locationService } from '@grafana/runtime';
import { config, getPluginLinkExtensions, locationService } from '@grafana/runtime';
import {
LocalValueVariable,
SceneQueryRunner,
@ -19,6 +20,10 @@ import {
} from '@grafana/scenes';
import { contextSrv } from 'app/core/services/context_srv';
import { GetExploreUrlArguments } from 'app/core/utils/explore';
import { grantUserPermissions } from 'app/features/alerting/unified/mocks';
import { scenesPanelToRuleFormValues } from 'app/features/alerting/unified/utils/rule-form';
import * as storeModule from 'app/store/store';
import { AccessControlAction } from 'app/types';
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
@ -30,6 +35,7 @@ import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutMana
const mocks = {
contextSrv: jest.mocked(contextSrv),
getExploreUrl: jest.fn(),
notifyApp: jest.fn(),
};
jest.mock('app/core/utils/explore', () => ({
@ -47,6 +53,10 @@ jest.mock('@grafana/runtime', () => ({
getPluginLinkExtensions: jest.fn(),
}));
jest.mock('app/store/store', () => ({
dispatch: jest.fn(),
}));
const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions);
describe('panelMenuBehavior', () => {
@ -102,6 +112,9 @@ describe('panelMenuBehavior', () => {
mocks.contextSrv.hasAccessToExplore.mockReturnValue(true);
mocks.getExploreUrl.mockReturnValue(Promise.resolve('/explore'));
config.unifiedAlertingEnabled = true;
grantUserPermissions([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleUpdate]);
menu.activate();
await new Promise((r) => setTimeout(r, 1));
@ -548,6 +561,112 @@ describe('panelMenuBehavior', () => {
expect(menu.state.items?.[0].text).toBe('Explore');
});
});
describe('onCreateAlert', () => {
beforeEach(() => {
jest.spyOn(storeModule, 'dispatch').mockImplementation(() => {});
jest.spyOn(locationService, 'push').mockImplementation(() => {});
jest.spyOn(urlUtil, 'renderUrl').mockImplementation((url, params) => `${url}?${JSON.stringify(params)}`);
});
it('should navigate to alert creation page on success', async () => {
const { menu, panel } = await buildTestScene({});
const mockFormValues = { someKey: 'someValue' };
config.unifiedAlertingEnabled = true;
grantUserPermissions([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleUpdate]);
jest
.spyOn(require('app/features/alerting/unified/utils/rule-form'), 'scenesPanelToRuleFormValues')
.mockResolvedValue(mockFormValues);
// activate the menu
menu.activate();
// wait for the menu to be activated
await new Promise((r) => setTimeout(r, 1));
// use userEvent mechanism to click the menu item
const moreMenu = menu.state.items?.find((i) => i.text === 'More...')?.subMenu;
const alertMenuItem = moreMenu?.find((i) => i.text === 'New alert rule')?.onClick;
expect(alertMenuItem).toBeDefined();
alertMenuItem?.({} as React.MouseEvent);
expect(scenesPanelToRuleFormValues).toHaveBeenCalledWith(panel);
});
it('should show error notification on failure', async () => {
const { menu, panel } = await buildTestScene({});
const mockError = new Error('Test error');
jest
.spyOn(require('app/features/alerting/unified/utils/rule-form'), 'scenesPanelToRuleFormValues')
.mockRejectedValue(mockError);
// Don't make notifyApp throw an error, just mock it
menu.activate();
await new Promise((r) => setTimeout(r, 1));
const moreMenu = menu.state.items?.find((i) => i.text === 'More...')?.subMenu;
const alertMenuItem = moreMenu?.find((i) => i.text === 'New alert rule')?.onClick;
expect(alertMenuItem).toBeDefined();
await alertMenuItem?.({} as React.MouseEvent);
await new Promise((r) => setTimeout(r, 0));
expect(scenesPanelToRuleFormValues).toHaveBeenCalledWith(panel);
});
it('should render "New alert rule" menu item when user has permissions to read and update alerts', async () => {
const { menu } = await buildTestScene({});
config.unifiedAlertingEnabled = true;
grantUserPermissions([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleUpdate]);
menu.activate();
await new Promise((r) => setTimeout(r, 1));
const moreMenu = menu.state.items?.find((i) => i.text === 'More...')?.subMenu;
expect(moreMenu?.find((i) => i.text === 'New alert rule')).toBeDefined();
});
it('should not contain "New alert rule" menu item when user does not have permissions to read and update alerts', async () => {
const { menu } = await buildTestScene({});
config.unifiedAlertingEnabled = true;
grantUserPermissions([AccessControlAction.AlertingRuleRead]);
menu.activate();
await new Promise((r) => setTimeout(r, 1));
const moreMenu = menu.state.items?.find((i) => i.text === 'More...')?.subMenu;
expect(moreMenu?.find((i) => i.text === 'New alert rule')).toBeUndefined();
});
it('should not contain "New alert rule" menu item when unifiedAlertingEnabled is false', async () => {
const { menu } = await buildTestScene({});
config.unifiedAlertingEnabled = false;
menu.activate();
await new Promise((r) => setTimeout(r, 1));
const moreMenu = menu.state.items?.find((i) => i.text === 'More...')?.subMenu;
expect(moreMenu?.find((i) => i.text === 'New alert rule')).toBeUndefined();
});
it('should not contain "New alert rule" menu item when user does not have permissions to read and update alerts', async () => {
const { menu } = await buildTestScene({});
config.unifiedAlertingEnabled = true;
grantUserPermissions([AccessControlAction.AlertingRuleRead]);
menu.activate();
await new Promise((r) => setTimeout(r, 1));
const moreMenu = menu.state.items?.find((i) => i.text === 'More...')?.subMenu;
const alertMenuItem = moreMenu?.find((i) => i.text === 'New alert rule')?.onClick;
expect(alertMenuItem).toBeUndefined();
});
afterEach(() => {
jest.restoreAllMocks();
});
});
});
interface SceneOptions {

@ -12,14 +12,19 @@ import { config, getPluginLinkExtensions, locationService } from '@grafana/runti
import { LocalValueVariable, sceneGraph, SceneGridRow, VizPanel, VizPanelMenu } from '@grafana/scenes';
import { DataQuery, OptionsWithLegend } from '@grafana/schema';
import appEvents from 'app/core/app_events';
import { createErrorNotification } from 'app/core/copy/appNotification';
import { t } from 'app/core/internationalization';
import { notifyApp } from 'app/core/reducers/appNotification';
import { contextSrv } from 'app/core/services/context_srv';
import { getMessageFromError } from 'app/core/utils/errors';
import { getCreateAlertInMenuAvailability } from 'app/features/alerting/unified/utils/access-control';
import { scenesPanelToRuleFormValues } from 'app/features/alerting/unified/utils/rule-form';
import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils';
import { InspectTab } from 'app/features/inspector/types';
import { getScenePanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils';
import { addDataTrailPanelAction } from 'app/features/trails/Integrations/dashboardIntegration';
import { dispatch } from 'app/store/store';
import { ShowConfirmModalEvent } from 'app/types/events';
import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
@ -214,11 +219,15 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
}
}
moreSubMenu.push({
text: t('panel.header-menu.new-alert-rule', `New alert rule`),
iconClassName: 'bell',
onClick: (e) => onCreateAlert(panel),
});
const isCreateAlertMenuOptionAvailable = getCreateAlertInMenuAvailability();
if (isCreateAlertMenuOptionAvailable) {
moreSubMenu.push({
text: t('panel.header-menu.new-alert-rule', `New alert rule`),
iconClassName: 'bell',
onClick: (e) => onCreateAlert(panel),
});
}
if (hasLegendOptions(panel.state.options) && !isEditingPanel) {
moreSubMenu.push({
@ -482,12 +491,18 @@ export function onRemovePanel(dashboard: DashboardScene, panel: VizPanel) {
}
const onCreateAlert = async (panel: VizPanel) => {
const formValues = await scenesPanelToRuleFormValues(panel);
const ruleFormUrl = urlUtil.renderUrl('/alerting/new', {
defaults: JSON.stringify(formValues),
returnTo: location.pathname + location.search,
});
locationService.push(ruleFormUrl);
try {
const formValues = await scenesPanelToRuleFormValues(panel);
const ruleFormUrl = urlUtil.renderUrl('/alerting/new', {
defaults: JSON.stringify(formValues),
returnTo: location.pathname + location.search,
});
locationService.push(ruleFormUrl);
} catch (err) {
const message = `Error getting rule values from the panel: ${getMessageFromError(err)}`;
dispatch(notifyApp(createErrorNotification(message)));
return;
}
};
export function toggleVizPanelLegend(vizPanel: VizPanel): void {

Loading…
Cancel
Save