mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
a851750b1c
commit
1de65bb384
@ -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) =>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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) {
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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": "Ēđįŧ",
|
||||||
|
@ -767,6 +767,7 @@
|
|||||||
"panel": {
|
"panel": {
|
||||||
"header-menu": {
|
"header-menu": {
|
||||||
"copy": "复制",
|
"copy": "复制",
|
||||||
|
"create-alert": "",
|
||||||
"create-library-panel": "创建库面板",
|
"create-library-panel": "创建库面板",
|
||||||
"duplicate": "复制",
|
"duplicate": "复制",
|
||||||
"edit": "编辑",
|
"edit": "编辑",
|
||||||
|
Loading…
Reference in New Issue
Block a user