diff --git a/.betterer.results b/.betterer.results
index 8509dde739a7..a9aa3e106a94 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -2011,22 +2011,10 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "5"],
[0, 0, 0, "Styles should be written using objects.", "6"]
],
- "public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx:5381": [
- [0, 0, 0, "Styles should be written using objects.", "0"]
- ],
"public/app/features/alerting/unified/components/rules/RuleConfigStatus.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
],
- "public/app/features/alerting/unified/components/rules/RuleDetails.tsx:5381": [
- [0, 0, 0, "Styles should be written using objects.", "0"],
- [0, 0, 0, "Styles should be written using objects.", "1"],
- [0, 0, 0, "Styles should be written using objects.", "2"]
- ],
- "public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx:5381": [
- [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"],
- [0, 0, 0, "Styles should be written using objects.", "1"]
- ],
"public/app/features/alerting/unified/components/rules/RuleDetailsAnnotations.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx
index 7cac56a2518e..85f00b549ff2 100644
--- a/public/app/features/alerting/unified/RuleList.test.tsx
+++ b/public/app/features/alerting/unified/RuleList.test.tsx
@@ -155,7 +155,7 @@ const ui = {
paused: byText(/^Paused/),
},
actionButtons: {
- more: byRole('button', { name: 'More' }),
+ more: byRole('button', { name: /more-actions/ }),
},
moreActionItems: {
pause: byRole('menuitem', { name: /pause evaluation/i }),
diff --git a/public/app/features/alerting/unified/TODO.md b/public/app/features/alerting/unified/TODO.md
index 86e608115480..749ed2e388bc 100644
--- a/public/app/features/alerting/unified/TODO.md
+++ b/public/app/features/alerting/unified/TODO.md
@@ -17,7 +17,6 @@ If the item needs more rationale and you feel like a single sentence is inedequa
## Refactoring
- Get rid of "+ Add new" in drop-downs : Let's see if is there a way we can make it work with ``
-- There is a lot of overlap between `RuleActionButtons` and `RuleDetailsActionButtons`. As these components contain a lot of logic it would be nice to extract that logic into hooks
- Create a shared timings form that can be used in both `EditDefaultPolicyForm.tsx` and `EditNotificationPolicyForm.tsx`
## Testing
diff --git a/public/app/features/alerting/unified/components/rule-viewer/Actions.tsx b/public/app/features/alerting/unified/components/rule-viewer/Actions.tsx
deleted file mode 100644
index ff9d296d4170..000000000000
--- a/public/app/features/alerting/unified/components/rule-viewer/Actions.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import React from 'react';
-
-import { AppEvents } from '@grafana/data';
-import { Dropdown, LinkButton, Menu } from '@grafana/ui';
-import appEvents from 'app/core/app_events';
-import MenuItemPauseRule from 'app/features/alerting/unified/components/MenuItemPauseRule';
-import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting';
-
-import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities';
-import { useRulePluginLinkExtension } from '../../plugins/useRulePluginLinkExtensions';
-import { createShareLink, isLocalDevEnv, isOpenSourceEdition, makeRuleBasedSilenceLink } from '../../utils/misc';
-import * as ruleId from '../../utils/rule-id';
-import { createUrl } from '../../utils/url';
-import MoreButton from '../MoreButton';
-import { DeclareIncidentMenuItem } from '../bridges/DeclareIncidentButton';
-
-import { useAlertRule } from './RuleContext';
-
-interface Props {
- handleDelete: (rule: CombinedRule) => void;
- handleDuplicateRule: (identifier: RuleIdentifier) => void;
-}
-
-export const useAlertRulePageActions = ({ handleDelete, handleDuplicateRule }: Props) => {
- const { rule, identifier } = useAlertRule();
- const rulePluginLinkExtension = useRulePluginLinkExtension(rule);
-
- // check all abilities and permissions
- const [editSupported, editAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Update);
- const canEdit = editSupported && editAllowed;
-
- const [deleteSupported, deleteAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Delete);
- const canDelete = deleteSupported && deleteAllowed;
-
- const [duplicateSupported, duplicateAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Duplicate);
- const canDuplicate = duplicateSupported && duplicateAllowed;
-
- const [silenceSupported, silenceAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Silence);
- const canSilence = silenceSupported && silenceAllowed;
-
- const [exportSupported, exportAllowed] = useAlertRuleAbility(rule, AlertRuleAction.ModifyExport);
- const canExport = exportSupported && exportAllowed;
-
- /**
- * Since Incident isn't available as an open-source product we shouldn't show it for Open-Source licenced editions of Grafana.
- * We should show it in development mode
- */
- const shouldShowDeclareIncidentButton = !isOpenSourceEdition() || isLocalDevEnv();
- const shareUrl = createShareLink(rule.namespace.rulesSource, rule);
-
- return [
- canEdit && ,
-
- {canEdit && }
- {canSilence && (
-
- )}
- {shouldShowDeclareIncidentButton && }
- {canDuplicate && handleDuplicateRule(identifier)} />}
-
- copyToClipboard(shareUrl)} />
- {canExport && (
- ]}
- />
- )}
- {rulePluginLinkExtension.length > 0 && (
- <>
-
- {rulePluginLinkExtension.map((extension) => (
-
- ))}
- >
- )}
- {canDelete && (
- <>
-
- handleDelete(rule)} />
- >
- )}
-
- }
- >
-
- ,
- ];
-};
-
-function copyToClipboard(text: string) {
- navigator.clipboard?.writeText(text).then(() => {
- appEvents.emit(AppEvents.alertSuccess, ['URL copied to clipboard']);
- });
-}
-
-type PropsWithIdentifier = { identifier: RuleIdentifier };
-
-const ExportMenuItem = ({ identifier }: PropsWithIdentifier) => {
- const returnTo = location.pathname + location.search;
- const url = createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export`, {
- returnTo,
- });
-
- return
;
-};
-
-const EditButton = ({ identifier }: PropsWithIdentifier) => {
- const returnTo = location.pathname + location.search;
- const ruleIdentifier = ruleId.stringifyIdentifier(identifier);
- const editURL = createUrl(`/alerting/${encodeURIComponent(ruleIdentifier)}/edit`, { returnTo });
-
- return (
-
- Edit
-
- );
-};
diff --git a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx
new file mode 100644
index 000000000000..b08c731d6f9c
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx
@@ -0,0 +1,139 @@
+import React from 'react';
+
+import { AppEvents } from '@grafana/data';
+import { ComponentSize, Dropdown, Menu } from '@grafana/ui';
+import appEvents from 'app/core/app_events';
+import MenuItemPauseRule from 'app/features/alerting/unified/components/MenuItemPauseRule';
+import MoreButton from 'app/features/alerting/unified/components/MoreButton';
+import { useRulePluginLinkExtension } from 'app/features/alerting/unified/plugins/useRulePluginLinkExtensions';
+import { isAlertingRule } from 'app/features/alerting/unified/utils/rules';
+import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting';
+import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
+
+import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities';
+import { createShareLink, isLocalDevEnv, isOpenSourceEdition, makeRuleBasedSilenceLink } from '../../utils/misc';
+import * as ruleId from '../../utils/rule-id';
+import { createUrl } from '../../utils/url';
+import { DeclareIncidentMenuItem } from '../bridges/DeclareIncidentButton';
+
+interface Props {
+ rule: CombinedRule;
+ identifier: RuleIdentifier;
+ showCopyLinkButton?: boolean;
+ handleDelete: (rule: CombinedRule) => void;
+ handleDuplicateRule: (identifier: RuleIdentifier) => void;
+ onPauseChange?: () => void;
+ buttonSize?: ComponentSize;
+ hideLabels?: boolean;
+}
+
+/**
+ * Get a list of menu items + divider elements for rendering in an alert rule's
+ * dropdown menu
+ */
+const AlertRuleMenu = ({
+ rule,
+ identifier,
+ showCopyLinkButton,
+ handleDelete,
+ handleDuplicateRule,
+ onPauseChange,
+ buttonSize,
+}: Props) => {
+ // check all abilities and permissions
+ const [pauseSupported, pauseAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Pause);
+ const canPause = pauseSupported && pauseAllowed;
+
+ const [deleteSupported, deleteAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Delete);
+ const canDelete = deleteSupported && deleteAllowed;
+
+ const [duplicateSupported, duplicateAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Duplicate);
+ const canDuplicate = duplicateSupported && duplicateAllowed;
+
+ const [silenceSupported, silenceAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Silence);
+ const canSilence = silenceSupported && silenceAllowed;
+
+ const [exportSupported, exportAllowed] = useAlertRuleAbility(rule, AlertRuleAction.ModifyExport);
+ const canExport = exportSupported && exportAllowed;
+
+ const ruleExtensionLinks = useRulePluginLinkExtension(rule);
+
+ const extensionsAvailable = ruleExtensionLinks.length > 0;
+
+ /**
+ * Since Incident isn't available as an open-source product we shouldn't show it for Open-Source licenced editions of Grafana.
+ * We should show it in development mode
+ */
+ const shouldShowDeclareIncidentButton =
+ (!isOpenSourceEdition() || isLocalDevEnv()) &&
+ isAlertingRule(rule.promRule) &&
+ rule.promRule.state === PromAlertingRuleState.Firing;
+ const shareUrl = createShareLink(rule.namespace.rulesSource, rule);
+
+ const showDivider =
+ [canPause, canSilence, shouldShowDeclareIncidentButton, canDuplicate].some(Boolean) &&
+ [showCopyLinkButton, canExport].some(Boolean);
+
+ const menuItems = (
+ <>
+ {canPause && }
+ {canSilence && (
+
+ )}
+ {shouldShowDeclareIncidentButton && }
+ {canDuplicate && handleDuplicateRule(identifier)} />}
+ {showDivider && }
+ {shareUrl && copyToClipboard(shareUrl)} />}
+ {canExport && (
+ ]}
+ />
+ )}
+ {extensionsAvailable && (
+ <>
+
+ {ruleExtensionLinks.map((extension) => (
+
+ ))}
+ >
+ )}
+ {canDelete && (
+ <>
+
+ handleDelete(rule)} />
+ >
+ )}
+ >
+ );
+
+ return (
+ {menuItems}}>
+
+
+ );
+};
+
+function copyToClipboard(text: string) {
+ navigator.clipboard?.writeText(text).then(() => {
+ appEvents.emit(AppEvents.alertSuccess, ['URL copied to clipboard']);
+ });
+}
+
+type PropsWithIdentifier = { identifier: RuleIdentifier };
+
+const ExportMenuItem = ({ identifier }: PropsWithIdentifier) => {
+ const returnTo = location.pathname + location.search;
+ const url = createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export`, {
+ returnTo,
+ });
+
+ return ;
+};
+
+export default AlertRuleMenu;
diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx
index 65d5453fee0b..3f660cc17a2e 100644
--- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx
+++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx
@@ -46,15 +46,15 @@ const ELEMENTS = {
delete: byRole('menuitem', { name: /Delete/i }),
},
pluginActions: {
- sloDashboard: byRole('link', { name: /SLO dashboard/i }),
+ sloDashboard: byRole('menuitem', { name: /SLO dashboard/i }),
declareIncident: byRole('link', { name: /Declare incident/i }),
- assertsWorkbench: byRole('link', { name: /Open workbench/i }),
+ assertsWorkbench: byRole('menuitem', { name: /Open workbench/i }),
},
},
},
};
-const { apiHandlers: pluginApiHandlers } = setupPlugins(plugins.slo, plugins.incident, plugins.asserts);
+const { apiHandlers: pluginApiHandlers } = setupPlugins(plugins);
const server = createMockGrafanaServer(...pluginApiHandlers);
diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx
index ed317f4b9873..be5acf4370ee 100644
--- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx
+++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx
@@ -7,6 +7,7 @@ import { Alert, LinkButton, Stack, TabContent, Text, TextLink, useStyles2 } from
import { PageInfoItem } from 'app/core/components/Page/types';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedRule';
+import { RuleActionsButtons } from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
import { CombinedRule, RuleHealth, RuleIdentifier } from 'app/types/unified-alerting';
import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto';
@@ -31,8 +32,6 @@ import { WithReturnButton } from '../WithReturnButton';
import { decodeGrafanaNamespace } from '../expressions/util';
import { RedirectToCloneRule } from '../rules/CloneRule';
-import { useAlertRulePageActions } from './Actions';
-import { useDeleteModal } from './DeleteModal';
import { FederatedRuleWarning } from './FederatedRuleWarning';
import PausedBadge from './PausedBadge';
import { useAlertRule } from './RuleContext';
@@ -60,12 +59,6 @@ const RuleViewer = () => {
// of duplicating provisioned alert rules
const [duplicateRuleIdentifier, setDuplicateRuleIdentifier] = useState();
- const [deleteModal, showDeleteModal] = useDeleteModal();
- const actions = useAlertRulePageActions({
- handleDuplicateRule: setDuplicateRuleIdentifier,
- handleDelete: showDeleteModal,
- });
-
const { annotations, promRule } = rule;
const hasError = isErrorHealth(rule.promRule?.health);
@@ -95,7 +88,7 @@ const RuleViewer = () => {
ruleOrigin={ruleOrigin}
/>
)}
- actions={actions}
+ actions={}
info={createMetadata(rule)}
subTitle={
@@ -128,7 +121,6 @@ const RuleViewer = () => {
{activeTab === ActiveTab.Details && }
- {deleteModal}
{duplicateRuleIdentifier && (
{
+ grantUserPermissions([
+ AccessControlAction.AlertingRuleCreate,
+ AccessControlAction.AlertingRuleRead,
+ AccessControlAction.AlertingRuleUpdate,
+ AccessControlAction.AlertingRuleDelete,
+ AccessControlAction.AlertingInstanceCreate,
+ ]);
+ mockContextSrv.hasPermissionInMetadata.mockImplementation(() => true);
+ mockContextSrv.hasPermission.mockImplementation(() => true);
+};
+const grantNoPermissions = () => {
+ grantUserPermissions([]);
+ mockContextSrv.hasPermissionInMetadata.mockImplementation(() => false);
+ mockContextSrv.hasPermission.mockImplementation(() => false);
+};
+
+const getMenuContents = async () => {
+ await screen.findByRole('menu');
+ const allMenuItems = screen.queryAllByRole('menuitem').map((el) => el.textContent);
+ const allLinkItems = screen.queryAllByRole('link').map((el) => el.textContent);
+
+ return [...allMenuItems, ...allLinkItems];
+};
+
+setPluginExtensionsHook(() => ({
+ extensions: [],
+ isLoading: false,
+}));
+
+describe('RuleActionsButtons', () => {
+ it('renders correct options for grafana managed rule', async () => {
+ const user = userEvent.setup();
+ grantAllPermissions();
+ const mockRule = getGrafanaRule();
+
+ render();
+
+ await user.click(await ui.moreButton.find());
+
+ expect(await getMenuContents()).toMatchSnapshot();
+ });
+
+ it('renders correct options for Cloud rule', async () => {
+ const user = userEvent.setup();
+ grantAllPermissions();
+ const mockRule = getCloudRule();
+ const dataSource = mockDataSource({ id: 1 });
+
+ const defaultState = configureStore().getState();
+ render(, {
+ preloadedState: produce(defaultState, (store) => {
+ store.unifiedAlerting.dataSources[dataSource.name] = {
+ loading: false,
+ dispatched: true,
+ result: {
+ id: 'test-ds',
+ name: dataSource.name,
+ rulerConfig: {
+ dataSourceName: dataSource.name,
+ apiVersion: 'config',
+ },
+ },
+ };
+ }),
+ });
+
+ await user.click(await ui.moreButton.find());
+
+ expect(await getMenuContents()).toMatchSnapshot();
+ });
+
+ it('renders minimal "More" menu when appropriate', async () => {
+ const user = userEvent.setup();
+ grantNoPermissions();
+
+ const mockRule = getGrafanaRule({ promRule: mockPromAlertingRule({ state: PromAlertingRuleState.Inactive }) });
+
+ render();
+
+ await user.click(await ui.moreButton.find());
+
+ expect(await getMenuContents()).toMatchSnapshot();
+ });
+
+ it('does not allow deletion when rule is provisioned', async () => {
+ const user = userEvent.setup();
+ grantAllPermissions();
+ const mockRule = getGrafanaRule({ rulerRule: mockGrafanaRulerRule({ provenance: 'file' }) });
+
+ render();
+
+ await user.click(await ui.moreButton.find());
+
+ expect(screen.queryByText(/delete/i)).not.toBeInTheDocument();
+ });
+});
diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx
index 484910ad4bb9..b16add8fd932 100644
--- a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx
@@ -1,33 +1,20 @@
-import { css } from '@emotion/css';
-import { uniqueId } from 'lodash';
+import { css, cx } from '@emotion/css';
import React, { useState } from 'react';
import { useLocation } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data';
-import {
- Button,
- ClipboardButton,
- ConfirmModal,
- Dropdown,
- Icon,
- LinkButton,
- Menu,
- Tooltip,
- useStyles2,
- Stack,
-} from '@grafana/ui';
-import { useAppNotification } from 'app/core/copy/appNotification';
-import MenuItemPauseRule from 'app/features/alerting/unified/components/MenuItemPauseRule';
+import { LinkButton, useStyles2, Stack } from '@grafana/ui';
+import AlertRuleMenu from 'app/features/alerting/unified/components/rule-viewer/AlertRuleMenu';
+import { useDeleteModal } from 'app/features/alerting/unified/components/rule-viewer/DeleteModal';
import { INSTANCES_DISPLAY_LIMIT } from 'app/features/alerting/unified/components/rules/RuleDetails';
import { useRulesFilter } from 'app/features/alerting/unified/hooks/useFilteredRules';
import { useDispatch } from 'app/types';
import { CombinedRule, RuleIdentifier, RulesSource } from 'app/types/unified-alerting';
import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities';
-import { useRulePluginLinkExtension } from '../../plugins/useRulePluginLinkExtensions';
-import { deleteRuleAction, fetchAllPromAndRulerRulesAction } from '../../state/actions';
-import { getRulesSourceName } from '../../utils/datasource';
-import { createShareLink, createViewLink } from '../../utils/misc';
+import { fetchPromAndRulerRulesAction } from '../../state/actions';
+import { GRAFANA_RULES_SOURCE_NAME, getRulesSourceName } from '../../utils/datasource';
+import { createViewLink } from '../../utils/misc';
import * as ruleId from '../../utils/rule-id';
import { isGrafanaRulerRule } from '../../utils/rules';
import { createUrl } from '../../utils/url';
@@ -39,230 +26,124 @@ export const matchesWidth = (width: number) => window.matchMedia(`(max-width: ${
interface Props {
rule: CombinedRule;
rulesSource: RulesSource;
+ /**
+ * Should we show the buttons in a "compact" state?
+ * i.e. without text and using smaller button sizes
+ */
+ compact?: boolean;
+ showViewButton?: boolean;
+ showCopyLinkButton?: boolean;
}
-export const RuleActionsButtons = ({ rule, rulesSource }: Props) => {
+/**
+ * **Action** buttons to show for an alert rule - e.g. "View", "Edit", "More..."
+ */
+export const RuleActionsButtons = ({ compact, showViewButton, showCopyLinkButton, rule, rulesSource }: Props) => {
const dispatch = useDispatch();
const location = useLocation();
- const notifyApp = useAppNotification();
const style = useStyles2(getStyles);
+ const [deleteModal, showDeleteModal] = useDeleteModal();
const [redirectToClone, setRedirectToClone] = useState<
{ identifier: RuleIdentifier; isProvisioned: boolean } | undefined
>(undefined);
const { namespace, group, rulerRule } = rule;
- const [ruleToDelete, setRuleToDelete] = useState();
const { hasActiveFilters } = useRulesFilter();
const returnTo = location.pathname + location.search;
- const isViewMode = inViewMode(location.pathname);
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
- const ruleExtensionLinks = useRulePluginLinkExtension(rule);
-
const [editRuleSupported, editRuleAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Update);
- const [deleteRuleSupported, deleteRuleAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Delete);
- const [duplicateRuleSupported, duplicateRuleAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Duplicate);
- const [modifyExportSupported, modifyExportAllowed] = useAlertRuleAbility(rule, AlertRuleAction.ModifyExport);
const canEditRule = editRuleSupported && editRuleAllowed;
- const canDeleteRule = deleteRuleSupported && deleteRuleAllowed;
- const canDuplicateRule = duplicateRuleSupported && duplicateRuleAllowed;
- const canModifyExport = modifyExportSupported && modifyExportAllowed;
const buttons: JSX.Element[] = [];
- const moreActions: JSX.Element[] = [];
- const deleteRule = () => {
- if (ruleToDelete && ruleToDelete.rulerRule) {
- const identifier = ruleId.fromRulerRule(
- getRulesSourceName(ruleToDelete.namespace.rulesSource),
- ruleToDelete.namespace.name,
- ruleToDelete.group.name,
- ruleToDelete.rulerRule
- );
-
- dispatch(deleteRuleAction(identifier, { navigateTo: isViewMode ? '/alerting/list' : undefined }));
- setRuleToDelete(undefined);
- }
- };
-
- const buildShareUrl = () => createShareLink(rulesSource, rule);
+ const buttonClasses = cx({ [style.compactButton]: compact });
+ const buttonSize = compact ? 'sm' : 'md';
const sourceName = getRulesSourceName(rulesSource);
- if (!isViewMode) {
+ const identifier = ruleId.fromCombinedRule(sourceName, rule);
+
+ if (showViewButton) {
buttons.push(
-
-
-
+
+ {!compact && 'View'}
+
);
}
- if (rulerRule) {
+ if (rulerRule && canEditRule) {
const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule);
- if (canEditRule) {
- const editURL = createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`, {
- returnTo,
- });
+ const editURL = createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`, {
+ returnTo,
+ });
- buttons.push(
-
-
-
- );
+ buttons.push(
+
+ {!compact && 'Edit'}
+
+ );
+ }
- moreActions.push(
- {
- // Uses INSTANCES_DISPLAY_LIMIT + 1 here as exporting LIMIT_ALERTS from RuleList has the side effect
- // of breaking some unrelated tests in Policy.test.tsx due to mocking approach
- const limitAlerts = hasActiveFilters ? undefined : INSTANCES_DISPLAY_LIMIT + 1;
- // Trigger a re-fetch of the rules table
- // TODO: Migrate rules table functionality to RTK Query, so we instead rely
- // on tag invalidation (or optimistic cache updates) for this
- dispatch(fetchAllPromAndRulerRulesAction(false, { limitAlerts }));
- }}
+ return (
+
+ {buttons}
+ showDeleteModal(rule)}
+ handleDuplicateRule={() => setRedirectToClone({ identifier, isProvisioned })}
+ onPauseChange={() => {
+ // Uses INSTANCES_DISPLAY_LIMIT + 1 here as exporting LIMIT_ALERTS from RuleList has the side effect
+ // of breaking some unrelated tests in Policy.test.tsx due to mocking approach
+ const limitAlerts = hasActiveFilters ? undefined : INSTANCES_DISPLAY_LIMIT + 1;
+ // Trigger a re-fetch of the rules table
+ // TODO: Migrate rules table functionality to RTK Query, so we instead rely
+ // on tag invalidation (or optimistic cache updates) for this
+ dispatch(fetchPromAndRulerRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME, limitAlerts }));
+ }}
+ />
+ {deleteModal}
+ {redirectToClone?.identifier && (
+ setRedirectToClone(undefined)}
/>
- );
- }
-
- if (isViewMode) {
- buttons.push(
- {
- notifyApp.error('Error while copying URL', copiedText);
- }}
- className={style.button}
- size="sm"
- getText={buildShareUrl}
- >
- Copy link to rule
-
- );
- }
-
- if (canDuplicateRule) {
- moreActions.push(
- setRedirectToClone({ identifier, isProvisioned })} />
- );
- }
-
- if (canModifyExport) {
- moreActions.push(
-
- );
- }
- }
-
- if (ruleExtensionLinks.length > 0) {
- moreActions.push(
- ,
- ...ruleExtensionLinks.map((extension) => (
-
- ))
- );
- }
-
- if (rulerRule && canDeleteRule) {
- moreActions.push(
- ,
- setRuleToDelete(rule)} />
- );
- }
-
- if (buttons.length || moreActions.length) {
- return (
- <>
-
- {buttons.map((button, index) => (
- {button}
- ))}
- {moreActions.length > 0 && (
-
- {moreActions.map((action) => (
- {action}
- ))}
-
- }
- >
-
-
- )}
-
- {!!ruleToDelete && (
-
-
- Deleting "{ruleToDelete.name}" will permanently remove it from your alert
- rule list.
-
- Are you sure you want to delete this rule?
-
- }
- confirmText="Yes, delete"
- icon="exclamation-triangle"
- onConfirm={deleteRule}
- onDismiss={() => setRuleToDelete(undefined)}
- />
- )}
-
- {redirectToClone && (
- setRedirectToClone(undefined)}
- />
- )}
- >
- );
- }
-
- return null;
+ )}
+
+ );
};
-function inViewMode(pathname: string): boolean {
- return pathname.endsWith('/view');
-}
-
-export const getStyles = (theme: GrafanaTheme2) => ({
- button: css`
- padding: 0 ${theme.spacing(2)};
- `,
+const getStyles = (theme: GrafanaTheme2) => ({
+ compactButton: css({
+ padding: `0 ${theme.spacing(2)}`,
+ }),
});
diff --git a/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx b/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx
index 9f34c8c11c52..596e63b6bc38 100644
--- a/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx
@@ -9,10 +9,8 @@ import { byRole } from 'testing-library-selector';
import { PluginExtensionTypes } from '@grafana/data';
import { usePluginLinkExtensions, setBackendSrv } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv';
-import { contextSrv } from 'app/core/services/context_srv';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { configureStore } from 'app/store/configureStore';
-import { AccessControlAction } from 'app/types';
import { CombinedRule } from 'app/types/unified-alerting';
import { AlertmanagersChoiceResponse } from '../../api/alertmanagerApi';
@@ -113,32 +111,6 @@ describe('RuleDetails RBAC', () => {
expect(ui.actionButtons.delete.query()).not.toBeInTheDocument();
await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' }));
});
-
- it('Should not render Silence button for users wihout the instance create permission', async () => {
- // Arrange
- jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
-
- // Act
- renderRuleDetails(grafanaRule);
-
- // Assert
- expect(ui.actionButtons.silence.query()).not.toBeInTheDocument();
- await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' }));
- });
-
- it('Should render Silence button for users with the instance create permissions', async () => {
- // Arrange
- jest
- .spyOn(contextSrv, 'hasPermission')
- .mockImplementation((action) => action === AccessControlAction.AlertingInstanceCreate);
-
- // Act
- renderRuleDetails(grafanaRule);
-
- // Assert
- expect(await ui.actionButtons.silence.find()).toBeInTheDocument();
- await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' }));
- });
});
describe('Cloud rules action buttons', () => {
diff --git a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
index 3e42e25f1465..5023e5d08533 100644
--- a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
@@ -12,8 +12,8 @@ import { isNullDate } from '../../utils/time';
import { AlertLabels } from '../AlertLabels';
import { DetailsField } from '../DetailsField';
-import { RuleDetailsActionButtons } from './RuleDetailsActionButtons';
import { RuleDetailsAnnotations } from './RuleDetailsAnnotations';
+import RuleDetailsButtons from './RuleDetailsButtons';
import { RuleDetailsDataSources } from './RuleDetailsDataSources';
import { RuleDetailsExpression } from './RuleDetailsExpression';
import { RuleDetailsMatchingInstances } from './RuleDetailsMatchingInstances';
@@ -37,7 +37,7 @@ export const RuleDetails = ({ rule }: Props) => {
return (
-
+
{
}
@@ -111,21 +111,21 @@ const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) =>
};
export const getStyles = (theme: GrafanaTheme2) => ({
- wrapper: css`
- display: flex;
- flex-direction: row;
+ wrapper: css({
+ display: 'flex',
+ flexDirection: 'row',
- ${theme.breakpoints.down('md')} {
- flex-direction: column;
- }
- `,
- leftSide: css`
- flex: 1;
- `,
- rightSide: css`
- ${theme.breakpoints.up('md')} {
- padding-left: 90px;
- width: 300px;
- }
- `,
+ [theme.breakpoints.down('md')]: {
+ flexDirection: 'column',
+ },
+ }),
+ leftSide: css({
+ flex: '1',
+ }),
+ rightSide: css({
+ [theme.breakpoints.up('md')]: {
+ paddingLeft: '90px',
+ width: '300px',
+ },
+ }),
});
diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx
deleted file mode 100644
index a9a6014380f7..000000000000
--- a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx
+++ /dev/null
@@ -1,257 +0,0 @@
-import { css } from '@emotion/css';
-import { uniqueId } from 'lodash';
-import React, { Fragment, useState } from 'react';
-
-import { GrafanaTheme2, textUtil } from '@grafana/data';
-import { config, useReturnToPrevious } from '@grafana/runtime';
-import { Button, ConfirmModal, Dropdown, HorizontalGroup, Icon, LinkButton, Menu, useStyles2 } from '@grafana/ui';
-import { useDispatch } from 'app/types';
-import { CombinedRule, RuleIdentifier, RulesSource } from 'app/types/unified-alerting';
-import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
-
-import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities';
-import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
-import { deleteRuleAction } from '../../state/actions';
-import { getAlertmanagerByUid } from '../../utils/alertmanager';
-import { Annotation } from '../../utils/constants';
-import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
-import {
- createExploreLink,
- createShareLink,
- isLocalDevEnv,
- isOpenSourceEdition,
- makeRuleBasedSilenceLink,
-} from '../../utils/misc';
-import * as ruleId from '../../utils/rule-id';
-import { isAlertingRule, isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
-import { DeclareIncidentButton } from '../bridges/DeclareIncidentButton';
-
-import { RedirectToCloneRule } from './CloneRule';
-
-interface Props {
- rule: CombinedRule;
- rulesSource: RulesSource;
-}
-
-export const RuleDetailsActionButtons = ({ rule, rulesSource }: Props) => {
- const style = useStyles2(getStyles);
- const { group } = rule;
- const { StateHistoryModal, showStateHistoryModal } = useStateHistoryModal();
- const dispatch = useDispatch();
-
- const setReturnToPrevious = useReturnToPrevious();
-
- const [ruleToDelete, setRuleToDelete] = useState
();
- const [redirectToClone, setRedirectToClone] = useState<
- { identifier: RuleIdentifier; isProvisioned: boolean } | undefined
- >(undefined);
-
- const alertmanagerSourceName = isGrafanaRulesSource(rulesSource)
- ? rulesSource
- : getAlertmanagerByUid(rulesSource.jsonData.alertmanagerUid)?.name;
-
- const [silenceSupported, silenceAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Silence);
- const [exploreSupported, exploreAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Explore);
-
- const buttons: JSX.Element[] = [];
- const rightButtons: JSX.Element[] = [];
- const moreActionsButtons: React.ReactElement[] = [];
-
- const deleteRule = () => {
- if (ruleToDelete && ruleToDelete.rulerRule) {
- const identifier = ruleId.fromRulerRule(
- getRulesSourceName(ruleToDelete.namespace.rulesSource),
- ruleToDelete.namespace.name,
- ruleToDelete.group.name,
- ruleToDelete.rulerRule
- );
-
- dispatch(deleteRuleAction(identifier, { navigateTo: undefined }));
- setRuleToDelete(undefined);
- }
- };
-
- const isFederated = isFederatedRuleGroup(group);
-
- const isFiringRule = isAlertingRule(rule.promRule) && rule.promRule.state === PromAlertingRuleState.Firing;
-
- const canSilence = silenceSupported && silenceAllowed && alertmanagerSourceName;
-
- const buildShareUrl = () => createShareLink(rulesSource, rule);
-
- // explore does not support grafana rule queries atm
- // neither do "federated rules"
- if (isCloudRulesSource(rulesSource) && exploreSupported && exploreAllowed && !isFederated) {
- buttons.push(
-
- See graph
-
- );
- }
- if (rule.annotations[Annotation.runbookURL]) {
- buttons.push(
-
- View runbook
-
- );
- }
- if (rule.annotations[Annotation.dashboardUID]) {
- const dashboardUID = rule.annotations[Annotation.dashboardUID];
- const isReturnToPreviousEnabled = config.featureToggles.returnToPrevious;
- if (dashboardUID) {
- buttons.push(
- {
- setReturnToPrevious(rule.name);
- }}
- >
- Go to dashboard
-
- );
- const panelId = rule.annotations[Annotation.panelID];
- if (panelId) {
- buttons.push(
- {
- setReturnToPrevious(rule.name);
- }}
- >
- Go to panel
-
- );
- }
- }
- }
-
- if (canSilence) {
- buttons.push(
-
- Silence
-
- );
- }
-
- if (isGrafanaRulerRule(rule.rulerRule)) {
- buttons.push(
-
-
- {StateHistoryModal}
-
- );
- }
-
- if (isFiringRule && shouldShowDeclareIncidentButton()) {
- buttons.push(
-
-
-
- );
- }
-
- if (buttons.length || rightButtons.length || moreActionsButtons.length) {
- return (
- <>
-
-
{buttons.length ? buttons : }
-
- {rightButtons.length && rightButtons}
- {moreActionsButtons.length && (
-
- {moreActionsButtons.map((action) => (
- {action}
- ))}
-
- }
- >
-
-
- )}
-
-
- {!!ruleToDelete && (
- setRuleToDelete(undefined)}
- />
- )}
- {redirectToClone && (
- setRedirectToClone(undefined)}
- />
- )}
- >
- );
- }
-
- return null;
-};
-
-/**
- * Since Incident isn't available as an open-source product we shouldn't show it for Open-Source licenced editions of Grafana.
- * We should show it in development mode
- */
-function shouldShowDeclareIncidentButton() {
- return !isOpenSourceEdition() || isLocalDevEnv();
-}
-
-export const getStyles = (theme: GrafanaTheme2) => ({
- wrapper: css`
- padding: 0 0 ${theme.spacing(2)} 0;
- gap: ${theme.spacing(1)};
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- flex-wrap: wrap;
- border-bottom: solid 1px ${theme.colors.border.medium};
- `,
-});
diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsButtons.tsx
new file mode 100644
index 000000000000..2008d401a772
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rules/RuleDetailsButtons.tsx
@@ -0,0 +1,127 @@
+import React, { Fragment } from 'react';
+
+import { textUtil } from '@grafana/data';
+import { config, useReturnToPrevious } from '@grafana/runtime';
+import { Button, LinkButton, Stack } from '@grafana/ui';
+import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
+
+import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities';
+import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
+import { Annotation } from '../../utils/constants';
+import { isCloudRulesSource } from '../../utils/datasource';
+import { createExploreLink } from '../../utils/misc';
+import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
+
+interface Props {
+ rule: CombinedRule;
+ rulesSource: RulesSource;
+}
+
+/**
+ * Buttons to display on an expanded alert rule in the list view
+ *
+ * e.g. "Show state history", "Go to dashboard"
+ *
+ * Shouldn't include *actions* for the alert rule, just navigation items
+ */
+const RuleDetailsButtons = ({ rule, rulesSource }: Props) => {
+ const { group } = rule;
+ const { StateHistoryModal, showStateHistoryModal } = useStateHistoryModal();
+
+ const setReturnToPrevious = useReturnToPrevious();
+
+ const [exploreSupported, exploreAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Explore);
+
+ const buttons: JSX.Element[] = [];
+
+ const isFederated = isFederatedRuleGroup(group);
+
+ // explore does not support grafana rule queries atm
+ // neither do "federated rules"
+ if (isCloudRulesSource(rulesSource) && exploreSupported && exploreAllowed && !isFederated) {
+ buttons.push(
+
+ See graph
+
+ );
+ }
+ if (rule.annotations[Annotation.runbookURL]) {
+ buttons.push(
+
+ View runbook
+
+ );
+ }
+ if (rule.annotations[Annotation.dashboardUID]) {
+ const dashboardUID = rule.annotations[Annotation.dashboardUID];
+ const isReturnToPreviousEnabled = config.featureToggles.returnToPrevious;
+ if (dashboardUID) {
+ buttons.push(
+ {
+ setReturnToPrevious(rule.name);
+ }}
+ >
+ Go to dashboard
+
+ );
+ const panelId = rule.annotations[Annotation.panelID];
+ if (panelId) {
+ buttons.push(
+ {
+ setReturnToPrevious(rule.name);
+ }}
+ >
+ Go to panel
+
+ );
+ }
+ }
+ }
+
+ if (isGrafanaRulerRule(rule.rulerRule)) {
+ buttons.push(
+
+
+ {StateHistoryModal}
+
+ );
+ }
+
+ return buttons.length ? {buttons} : null;
+};
+
+export default RuleDetailsButtons;
diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx
index 7f29dcc73ac2..d64896332553 100644
--- a/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx
+++ b/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx
@@ -6,11 +6,12 @@ import { MemoryRouter } from 'react-router-dom';
import { byRole } from 'testing-library-selector';
import { setPluginExtensionsHook } from '@grafana/runtime';
+import { mockApi, setupMswServer } from 'app/features/alerting/unified/mockApi';
import { configureStore } from 'app/store/configureStore';
import { CombinedRule } from 'app/types/unified-alerting';
import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities';
-import { getCloudRule, getGrafanaRule } from '../../mocks';
+import { getCloudRule, getGrafanaRule, getMockPluginMeta } from '../../mocks';
import { RulesTable } from './RulesTable';
@@ -29,7 +30,7 @@ const ui = {
actionButtons: {
edit: byRole('link', { name: 'Edit' }),
view: byRole('link', { name: 'View' }),
- more: byRole('button', { name: 'More' }),
+ more: byRole('button', { name: /more-actions/i }),
},
moreActionItems: {
delete: byRole('menuitem', { name: 'Delete' }),
@@ -49,8 +50,14 @@ function renderRulesTable(rule: CombinedRule) {
}
const user = userEvent.setup();
+const server = setupMswServer();
describe('RulesTable RBAC', () => {
+ beforeEach(() => {
+ mockApi(server).plugins.getPluginSettings({
+ ...getMockPluginMeta('grafana-incident-app', 'Grafana Incident'),
+ });
+ });
describe('Grafana rules action buttons', () => {
const grafanaRule = getGrafanaRule({ name: 'Grafana' });
diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.tsx
index e1ae729a4f8d..0a40744e898c 100644
--- a/public/app/features/alerting/unified/components/rules/RulesTable.tsx
+++ b/public/app/features/alerting/unified/components/rules/RulesTable.tsx
@@ -266,7 +266,7 @@ function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean, showNe
label: 'Actions',
// eslint-disable-next-line react/display-name
renderCell: ({ data: rule }) => {
- return ;
+ return ;
},
size: '200px',
});
diff --git a/public/app/features/alerting/unified/components/rules/__snapshots__/RuleActionsButtons.test.tsx.snap b/public/app/features/alerting/unified/components/rules/__snapshots__/RuleActionsButtons.test.tsx.snap
new file mode 100644
index 000000000000..7e8ec05632eb
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rules/__snapshots__/RuleActionsButtons.test.tsx.snap
@@ -0,0 +1,28 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RuleActionsButtons renders correct options for Cloud rule 1`] = `
+[
+ "Duplicate",
+ "Copy link",
+ "Delete",
+ "Declare incident",
+]
+`;
+
+exports[`RuleActionsButtons renders correct options for grafana managed rule 1`] = `
+[
+ "Pause evaluation",
+ "Duplicate",
+ "Copy link",
+ "Export",
+ "Delete",
+ "Silence notifications",
+ "Declare incident",
+]
+`;
+
+exports[`RuleActionsButtons renders minimal "More" menu when appropriate 1`] = `
+[
+ "Copy link",
+]
+`;
diff --git a/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap b/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap
index 2b0417df5654..02137bf67ebf 100644
--- a/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap
+++ b/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap
@@ -18,6 +18,10 @@ exports[`AlertRule abilities should report no permissions while we are loading d
false,
false,
],
+ "pause-alert-rule": [
+ false,
+ false,
+ ],
"silence-alert-rule": [
false,
false,
@@ -51,6 +55,10 @@ exports[`AlertRule abilities should report that all actions are supported for a
true,
false,
],
+ "pause-alert-rule": [
+ true,
+ false,
+ ],
"silence-alert-rule": [
true,
false,
diff --git a/public/app/features/alerting/unified/hooks/useAbilities.ts b/public/app/features/alerting/unified/hooks/useAbilities.ts
index 14ffa869fc5d..9ba6999239c2 100644
--- a/public/app/features/alerting/unified/hooks/useAbilities.ts
+++ b/public/app/features/alerting/unified/hooks/useAbilities.ts
@@ -69,6 +69,7 @@ export enum AlertRuleAction {
Explore = 'explore-alert-rule',
Silence = 'silence-alert-rule',
ModifyExport = 'modify-export-rule',
+ Pause = 'pause-alert-rule',
}
// this enum lists all of the actions we can perform within alerting in general, not linked to a specific
@@ -178,6 +179,7 @@ export function useAllAlertRuleAbilities(rule: CombinedRule): Abilities PluginMeta = (id, name) => {
+ return {
+ name,
+ id,
+ type: PluginType.app,
+ module: `plugins/${id}/module`,
+ baseUrl: `public/plugins/${id}`,
+ info: {
+ author: { name: 'Grafana Labs' },
+ description: name,
+ updated: '',
+ version: '',
+ links: [],
+ logos: {
+ small: '',
+ large: '',
+ },
+ screenshots: [],
},
- screenshots: [],
- },
+ };
};
-export const labelsPluginMetaMock: PluginMeta = {
- name: 'Grafana IRM Labels',
- id: 'grafana-labels-app',
- type: PluginType.app,
- module: 'plugins/grafana-labels-app/module',
- baseUrl: 'public/plugins/grafana-labels-app',
- info: {
- author: { name: 'Grafana Labs' },
- description: '',
- updated: '',
- version: '',
- links: [],
- logos: {
- small: '',
- large: '',
- },
- screenshots: [],
- },
-};
+export const labelsPluginMetaMock = getMockPluginMeta('grafana-labels-app', 'Grafana IRM Labels');
+export const onCallPluginMetaMock = getMockPluginMeta('grafana-oncall-app', 'Grafana OnCall');
diff --git a/public/app/features/alerting/unified/mocks/folders.ts b/public/app/features/alerting/unified/mocks/folders.ts
new file mode 100644
index 000000000000..9b20e2d826f2
--- /dev/null
+++ b/public/app/features/alerting/unified/mocks/folders.ts
@@ -0,0 +1,6 @@
+import { HttpResponse, http } from 'msw';
+
+import { mockFolder } from 'app/features/alerting/unified/mocks';
+
+export const folderHandler = (response = mockFolder()) =>
+ http.get(`/api/folders/:folderUid`, () => HttpResponse.json(response));
diff --git a/public/app/features/alerting/unified/mocks/plugins.ts b/public/app/features/alerting/unified/mocks/plugins.ts
index 1a97fc39f501..54d0038f7576 100644
--- a/public/app/features/alerting/unified/mocks/plugins.ts
+++ b/public/app/features/alerting/unified/mocks/plugins.ts
@@ -1,10 +1,12 @@
import { http, HttpResponse } from 'msw';
import { PluginMeta } from '@grafana/data';
+import { plugins } from 'app/features/alerting/unified/testSetup/plugins';
-export const pluginsHandler = (pluginsRegistry: Map) =>
- http.get<{ pluginId: string }>(`/api/plugins/:pluginId/settings`, ({ params: { pluginId } }) =>
- pluginsRegistry.has(pluginId)
- ? HttpResponse.json(pluginsRegistry.get(pluginId)!)
- : HttpResponse.json({ message: 'Plugin not found, no installed plugin with that id' }, { status: 404 })
- );
+export const pluginsHandler = (pluginsArray: PluginMeta[] = plugins) =>
+ http.get<{ pluginId: string }>(`/api/plugins/:pluginId/settings`, ({ params: { pluginId } }) => {
+ const matchingPlugin = pluginsArray.find((plugin) => plugin.id === pluginId);
+ return matchingPlugin
+ ? HttpResponse.json(matchingPlugin)
+ : HttpResponse.json({ message: 'Plugin not found, no installed plugin with that id' }, { status: 404 });
+ });
diff --git a/public/app/features/alerting/unified/mocks/server/handlers.ts b/public/app/features/alerting/unified/mocks/server/handlers.ts
index 7fa5949fdc7d..99871367855f 100644
--- a/public/app/features/alerting/unified/mocks/server/handlers.ts
+++ b/public/app/features/alerting/unified/mocks/server/handlers.ts
@@ -7,6 +7,8 @@ import {
alertmanagerChoiceHandler,
} from 'app/features/alerting/unified/mocks/alertmanagerApi';
import { datasourceBuildInfoHandler } from 'app/features/alerting/unified/mocks/datasources';
+import { folderHandler } from 'app/features/alerting/unified/mocks/folders';
+import { pluginsHandler } from 'app/features/alerting/unified/mocks/plugins';
import {
silenceCreateHandler,
silenceGetHandler,
@@ -20,6 +22,10 @@ const allHandlers = [
alertmanagerChoiceHandler(),
alertmanagerAlertsListHandler(),
+ folderHandler(),
+
+ pluginsHandler(),
+
silencesListHandler(),
silenceGetHandler(),
silenceCreateHandler(),
diff --git a/public/app/features/alerting/unified/testSetup/plugins.ts b/public/app/features/alerting/unified/testSetup/plugins.ts
index 1ef1b4fae875..9cd8041afdf2 100644
--- a/public/app/features/alerting/unified/testSetup/plugins.ts
+++ b/public/app/features/alerting/unified/testSetup/plugins.ts
@@ -5,11 +5,8 @@ import { config } from '@grafana/runtime';
import { pluginsHandler } from '../mocks/plugins';
-export function setupPlugins(...plugins: PluginMeta[]): { apiHandlers: RequestHandler[] } {
- const pluginsRegistry = new Map();
- plugins.forEach((plugin) => pluginsRegistry.set(plugin.id, plugin));
-
- pluginsRegistry.forEach((plugin) => {
+export function setupPlugins(plugins: PluginMeta[]): { apiHandlers: RequestHandler[] } {
+ plugins.forEach((plugin) => {
config.apps[plugin.id] = {
id: plugin.id,
path: plugin.baseUrl,
@@ -20,12 +17,12 @@ export function setupPlugins(...plugins: PluginMeta[]): { apiHandlers: RequestHa
});
return {
- apiHandlers: [pluginsHandler(pluginsRegistry)],
+ apiHandlers: [pluginsHandler(plugins)],
};
}
-export const plugins: Record = {
- slo: {
+export const plugins: PluginMeta[] = [
+ {
id: 'grafana-slo-app',
name: 'SLO dashboard',
type: PluginType.app,
@@ -48,7 +45,7 @@ export const plugins: Record = {
module: 'public/plugins/grafana-slo-app/module.js',
baseUrl: 'public/plugins/grafana-slo-app',
},
- incident: {
+ {
id: 'grafana-incident-app',
name: 'Incident management',
type: PluginType.app,
@@ -71,7 +68,7 @@ export const plugins: Record = {
module: 'public/plugins/grafana-incident-app/module.js',
baseUrl: 'public/plugins/grafana-incident-app',
},
- asserts: {
+ {
id: 'grafana-asserts-app',
name: 'Asserts',
type: PluginType.app,
@@ -94,4 +91,4 @@ export const plugins: Record = {
module: 'public/plugins/grafana-asserts-app/module.js',
baseUrl: 'public/plugins/grafana-asserts-app',
},
-};
+];
diff --git a/public/app/features/alerting/unified/utils/misc.ts b/public/app/features/alerting/unified/utils/misc.ts
index 8032c2f44875..9106eb4a0220 100644
--- a/public/app/features/alerting/unified/utils/misc.ts
+++ b/public/app/features/alerting/unified/utils/misc.ts
@@ -5,7 +5,7 @@ import { GrafanaEdition } from '@grafana/data/src/types/config';
import { config, isFetchError } from '@grafana/runtime';
import { DataSourceRef } from '@grafana/schema';
import { escapePathSeparators } from 'app/features/alerting/unified/utils/rule-id';
-import { alertInstanceKey } from 'app/features/alerting/unified/utils/rules';
+import { alertInstanceKey, isGrafanaRulerRule } from 'app/features/alerting/unified/utils/rules';
import { SortOrder } from 'app/plugins/panel/alertlist/types';
import { Alert, CombinedRule, FilterState, RulesSource, SilenceFilterState } from 'app/types/unified-alerting';
import {
@@ -54,14 +54,16 @@ export function createMuteTimingLink(muteTimingName: string, alertManagerSourceN
});
}
-export function createShareLink(ruleSource: RulesSource, rule: CombinedRule): string {
+export function createShareLink(ruleSource: RulesSource, rule: CombinedRule): string | undefined {
if (isCloudRulesSource(ruleSource)) {
return createAbsoluteUrl(
`/alerting/${encodeURIComponent(ruleSource.name)}/${encodeURIComponent(escapePathSeparators(rule.name))}/find`
);
+ } else if (isGrafanaRulerRule(rule.rulerRule)) {
+ return createUrl(`/alerting/grafana/${rule.rulerRule.grafana_alert.uid}/view`);
}
- return window.location.href.split('?')[0];
+ return;
}
export function arrayToRecord(items: Array<{ key: string; value: string }>): Record {