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": "编辑",