mirror of
https://github.com/grafana/grafana.git
synced 2026-08-26 13:27:30 -05:00
Alerting: unify alert rule actions between list and detail view (#86071)
* Add mock method for getting a plugin * Update tests to find "more" button via label * Remove test for Silence action in rule details * Unify alert rule actions to pull from same place * Restore behaviour of only showing incident button when firing * Fix identifier and pause permission/logic * Remove TODO comment related to refactor * Update snapshot for useAbilities * Undo optional param * Rename alert rule menu hook to component * Refactor hook to component * Rename Rule action buttons component * Chore: update style syntax for RuleDetails * Add tests for refactored alert rule menu * Only re-fetch Grafana managed alerts after pausing/resuming * Remove console log and check for extensions * Improve share rule generation of GMA rules * Rename component * Update action * Refactor plugins and fix tests * lint --------- Co-authored-by: Konrad Lalik <konrad.lalik@grafana.com> Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
co-authored by
Konrad Lalik
Gilles De Mey
parent
93519f70ca
commit
b0f6913ef6
@@ -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"]
|
||||
],
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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 `<Select allowCustomValue />`
|
||||
- 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
|
||||
|
||||
@@ -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 && <EditButton key="edit-action" identifier={identifier} />,
|
||||
<Dropdown
|
||||
key="more-actions"
|
||||
overlay={
|
||||
<Menu>
|
||||
{canEdit && <MenuItemPauseRule rule={rule} />}
|
||||
{canSilence && (
|
||||
<Menu.Item
|
||||
label="Silence notifications"
|
||||
icon="bell-slash"
|
||||
url={makeRuleBasedSilenceLink(identifier.ruleSourceName, rule)}
|
||||
/>
|
||||
)}
|
||||
{shouldShowDeclareIncidentButton && <DeclareIncidentMenuItem title={rule.name} url={''} />}
|
||||
{canDuplicate && <Menu.Item label="Duplicate" icon="copy" onClick={() => handleDuplicateRule(identifier)} />}
|
||||
<Menu.Divider />
|
||||
<Menu.Item label="Copy link" icon="share-alt" onClick={() => copyToClipboard(shareUrl)} />
|
||||
{canExport && (
|
||||
<Menu.Item
|
||||
label="Export"
|
||||
icon="download-alt"
|
||||
childItems={[<ExportMenuItem key="export-with-modifications" identifier={identifier} />]}
|
||||
/>
|
||||
)}
|
||||
{rulePluginLinkExtension.length > 0 && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
{rulePluginLinkExtension.map((extension) => (
|
||||
<Menu.Item
|
||||
key={extension.id}
|
||||
label={extension.title}
|
||||
icon={extension.icon}
|
||||
onClick={extension.onClick}
|
||||
url={extension.path}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{canDelete && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Item label="Delete" icon="trash-alt" destructive onClick={() => handleDelete(rule)} />
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<MoreButton size="md" />
|
||||
</Dropdown>,
|
||||
];
|
||||
};
|
||||
|
||||
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 <Menu.Item key="with-modifications" label="With modifications" icon="file-edit-alt" url={url} />;
|
||||
};
|
||||
|
||||
const EditButton = ({ identifier }: PropsWithIdentifier) => {
|
||||
const returnTo = location.pathname + location.search;
|
||||
const ruleIdentifier = ruleId.stringifyIdentifier(identifier);
|
||||
const editURL = createUrl(`/alerting/${encodeURIComponent(ruleIdentifier)}/edit`, { returnTo });
|
||||
|
||||
return (
|
||||
<LinkButton variant="secondary" icon="pen" href={editURL}>
|
||||
Edit
|
||||
</LinkButton>
|
||||
);
|
||||
};
|
||||
@@ -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 && <MenuItemPauseRule rule={rule} onPauseChange={onPauseChange} />}
|
||||
{canSilence && (
|
||||
<Menu.Item
|
||||
label="Silence notifications"
|
||||
icon="bell-slash"
|
||||
url={makeRuleBasedSilenceLink(identifier.ruleSourceName, rule)}
|
||||
/>
|
||||
)}
|
||||
{shouldShowDeclareIncidentButton && <DeclareIncidentMenuItem title={rule.name} url={''} />}
|
||||
{canDuplicate && <Menu.Item label="Duplicate" icon="copy" onClick={() => handleDuplicateRule(identifier)} />}
|
||||
{showDivider && <Menu.Divider />}
|
||||
{shareUrl && <Menu.Item label="Copy link" icon="share-alt" onClick={() => copyToClipboard(shareUrl)} />}
|
||||
{canExport && (
|
||||
<Menu.Item
|
||||
label="Export"
|
||||
icon="download-alt"
|
||||
childItems={[<ExportMenuItem key="export-with-modifications" identifier={identifier} />]}
|
||||
/>
|
||||
)}
|
||||
{extensionsAvailable && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
{ruleExtensionLinks.map((extension) => (
|
||||
<Menu.Item key={extension.id} label={extension.title} icon={extension.icon} onClick={extension.onClick} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{canDelete && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Item label="Delete" icon="trash-alt" destructive onClick={() => handleDelete(rule)} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown overlay={<Menu>{menuItems}</Menu>}>
|
||||
<MoreButton size={buttonSize} />
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
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 <Menu.Item key="with-modifications" label="With modifications" icon="file-edit-alt" url={url} />;
|
||||
};
|
||||
|
||||
export default AlertRuleMenu;
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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<RuleIdentifier>();
|
||||
|
||||
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={<RuleActionsButtons rule={rule} showCopyLinkButton rulesSource={rule.namespace.rulesSource} />}
|
||||
info={createMetadata(rule)}
|
||||
subTitle={
|
||||
<Stack direction="column">
|
||||
@@ -128,7 +121,6 @@ const RuleViewer = () => {
|
||||
{activeTab === ActiveTab.Details && <Details rule={rule} />}
|
||||
</TabContent>
|
||||
</Stack>
|
||||
{deleteModal}
|
||||
{duplicateRuleIdentifier && (
|
||||
<RedirectToCloneRule
|
||||
redirectTo={true}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { produce } from 'immer';
|
||||
import React from 'react';
|
||||
import { render, screen, userEvent } from 'test/test-utils';
|
||||
import { byLabelText } from 'testing-library-selector';
|
||||
|
||||
import { setPluginExtensionsHook } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { RuleActionsButtons } from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
|
||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import {
|
||||
getCloudRule,
|
||||
getGrafanaRule,
|
||||
grantUserPermissions,
|
||||
mockDataSource,
|
||||
mockGrafanaRulerRule,
|
||||
mockPromAlertingRule,
|
||||
} from 'app/features/alerting/unified/mocks';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||
|
||||
setupMswServer();
|
||||
jest.mock('app/core/services/context_srv');
|
||||
const mockContextSrv = jest.mocked(contextSrv);
|
||||
|
||||
const ui = {
|
||||
moreButton: byLabelText('more-actions'),
|
||||
};
|
||||
|
||||
const grantAllPermissions = () => {
|
||||
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(<RuleActionsButtons rule={mockRule} rulesSource="grafana" showCopyLinkButton />);
|
||||
|
||||
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(<RuleActionsButtons rule={mockRule} rulesSource={dataSource} />, {
|
||||
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(<RuleActionsButtons rule={mockRule} rulesSource="grafana" />);
|
||||
|
||||
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(<RuleActionsButtons rule={mockRule} rulesSource="grafana" />);
|
||||
|
||||
await user.click(await ui.moreButton.find());
|
||||
|
||||
expect(screen.queryByText(/delete/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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<CombinedRule>();
|
||||
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(
|
||||
<Tooltip placement="top" content={'View'}>
|
||||
<LinkButton
|
||||
className={style.button}
|
||||
title="View"
|
||||
size="sm"
|
||||
key="view"
|
||||
variant="secondary"
|
||||
icon="eye"
|
||||
href={createViewLink(rulesSource, rule, returnTo)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<LinkButton
|
||||
tooltip={compact ? 'View' : undefined}
|
||||
tooltipPlacement="top"
|
||||
className={buttonClasses}
|
||||
title={'View'}
|
||||
size={buttonSize}
|
||||
key="view"
|
||||
variant="secondary"
|
||||
icon="eye"
|
||||
href={createViewLink(rulesSource, rule, returnTo)}
|
||||
>
|
||||
{!compact && 'View'}
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
<Tooltip placement="top" content={'Edit'}>
|
||||
<LinkButton
|
||||
title="Edit"
|
||||
className={style.button}
|
||||
size="sm"
|
||||
key="edit"
|
||||
variant="secondary"
|
||||
icon="pen"
|
||||
href={editURL}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
buttons.push(
|
||||
<LinkButton
|
||||
tooltip={compact ? 'Edit' : undefined}
|
||||
tooltipPlacement="top"
|
||||
title={'Edit'}
|
||||
className={buttonClasses}
|
||||
size={buttonSize}
|
||||
key="edit"
|
||||
variant="secondary"
|
||||
icon="pen"
|
||||
href={editURL}
|
||||
>
|
||||
{!compact && 'Edit'}
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
||||
moreActions.push(
|
||||
<MenuItemPauseRule
|
||||
rule={rule}
|
||||
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(fetchAllPromAndRulerRulesAction(false, { limitAlerts }));
|
||||
}}
|
||||
return (
|
||||
<Stack gap={1}>
|
||||
{buttons}
|
||||
<AlertRuleMenu
|
||||
buttonSize={buttonSize}
|
||||
rule={rule}
|
||||
identifier={identifier}
|
||||
showCopyLinkButton={showCopyLinkButton}
|
||||
handleDelete={() => 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 && (
|
||||
<RedirectToCloneRule
|
||||
identifier={redirectToClone.identifier}
|
||||
isProvisioned={redirectToClone.isProvisioned}
|
||||
onDismiss={() => setRedirectToClone(undefined)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isViewMode) {
|
||||
buttons.push(
|
||||
<ClipboardButton
|
||||
key="copy"
|
||||
icon="copy"
|
||||
onClipboardError={(copiedText) => {
|
||||
notifyApp.error('Error while copying URL', copiedText);
|
||||
}}
|
||||
className={style.button}
|
||||
size="sm"
|
||||
getText={buildShareUrl}
|
||||
>
|
||||
Copy link to rule
|
||||
</ClipboardButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (canDuplicateRule) {
|
||||
moreActions.push(
|
||||
<Menu.Item label="Duplicate" icon="copy" onClick={() => setRedirectToClone({ identifier, isProvisioned })} />
|
||||
);
|
||||
}
|
||||
|
||||
if (canModifyExport) {
|
||||
moreActions.push(
|
||||
<Menu.Item
|
||||
label="Modify export"
|
||||
icon="edit"
|
||||
url={createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export`, {
|
||||
returnTo: location.pathname + location.search,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (ruleExtensionLinks.length > 0) {
|
||||
moreActions.push(
|
||||
<Menu.Divider />,
|
||||
...ruleExtensionLinks.map((extension) => (
|
||||
<Menu.Item key={extension.id} label={extension.title} icon={extension.icon} onClick={extension.onClick} />
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
if (rulerRule && canDeleteRule) {
|
||||
moreActions.push(
|
||||
<Menu.Divider />,
|
||||
<Menu.Item label="Delete" icon="trash-alt" destructive onClick={() => setRuleToDelete(rule)} />
|
||||
);
|
||||
}
|
||||
|
||||
if (buttons.length || moreActions.length) {
|
||||
return (
|
||||
<>
|
||||
<Stack gap={1}>
|
||||
{buttons.map((button, index) => (
|
||||
<React.Fragment key={index}>{button}</React.Fragment>
|
||||
))}
|
||||
{moreActions.length > 0 && (
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
{moreActions.map((action) => (
|
||||
<React.Fragment key={uniqueId('action_')}>{action}</React.Fragment>
|
||||
))}
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button variant="secondary" size="sm">
|
||||
More
|
||||
<Icon name="angle-down" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
</Stack>
|
||||
{!!ruleToDelete && (
|
||||
<ConfirmModal
|
||||
isOpen={true}
|
||||
title="Delete rule"
|
||||
body={
|
||||
<div>
|
||||
<p>
|
||||
Deleting "<strong>{ruleToDelete.name}</strong>" will permanently remove it from your alert
|
||||
rule list.
|
||||
</p>
|
||||
<p>Are you sure you want to delete this rule?</p>
|
||||
</div>
|
||||
}
|
||||
confirmText="Yes, delete"
|
||||
icon="exclamation-triangle"
|
||||
onConfirm={deleteRule}
|
||||
onDismiss={() => setRuleToDelete(undefined)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{redirectToClone && (
|
||||
<RedirectToCloneRule
|
||||
identifier={redirectToClone.identifier}
|
||||
isProvisioned={redirectToClone.isProvisioned}
|
||||
onDismiss={() => setRedirectToClone(undefined)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
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)}`,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
<RuleDetailsActionButtons rule={rule} rulesSource={rulesSource} />
|
||||
<RuleDetailsButtons rule={rule} rulesSource={rulesSource} />
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.leftSide}>
|
||||
{<EvaluationBehaviorSummary rule={rule} />}
|
||||
@@ -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',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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<CombinedRule>();
|
||||
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(
|
||||
<LinkButton
|
||||
size="sm"
|
||||
key="explore"
|
||||
variant="primary"
|
||||
icon="chart-line"
|
||||
target="_blank"
|
||||
href={createExploreLink(rulesSource, rule.query)}
|
||||
>
|
||||
See graph
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
if (rule.annotations[Annotation.runbookURL]) {
|
||||
buttons.push(
|
||||
<LinkButton
|
||||
size="sm"
|
||||
key="runbook"
|
||||
variant="primary"
|
||||
icon="book"
|
||||
target="_blank"
|
||||
href={textUtil.sanitizeUrl(rule.annotations[Annotation.runbookURL])}
|
||||
>
|
||||
View runbook
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
if (rule.annotations[Annotation.dashboardUID]) {
|
||||
const dashboardUID = rule.annotations[Annotation.dashboardUID];
|
||||
const isReturnToPreviousEnabled = config.featureToggles.returnToPrevious;
|
||||
if (dashboardUID) {
|
||||
buttons.push(
|
||||
<LinkButton
|
||||
size="sm"
|
||||
key="dashboard"
|
||||
variant="primary"
|
||||
icon="apps"
|
||||
target={isReturnToPreviousEnabled ? undefined : '_blank'}
|
||||
href={`d/${encodeURIComponent(dashboardUID)}`}
|
||||
onClick={() => {
|
||||
setReturnToPrevious(rule.name);
|
||||
}}
|
||||
>
|
||||
Go to dashboard
|
||||
</LinkButton>
|
||||
);
|
||||
const panelId = rule.annotations[Annotation.panelID];
|
||||
if (panelId) {
|
||||
buttons.push(
|
||||
<LinkButton
|
||||
size="sm"
|
||||
key="panel"
|
||||
variant="primary"
|
||||
icon="apps"
|
||||
target={isReturnToPreviousEnabled ? undefined : '_blank'}
|
||||
href={`d/${encodeURIComponent(dashboardUID)}?viewPanel=${encodeURIComponent(panelId)}`}
|
||||
onClick={() => {
|
||||
setReturnToPrevious(rule.name);
|
||||
}}
|
||||
>
|
||||
Go to panel
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (canSilence) {
|
||||
buttons.push(
|
||||
<LinkButton
|
||||
size="sm"
|
||||
key="silence"
|
||||
icon="bell-slash"
|
||||
target="_blank"
|
||||
href={makeRuleBasedSilenceLink(alertmanagerSourceName, rule)}
|
||||
>
|
||||
Silence
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (isGrafanaRulerRule(rule.rulerRule)) {
|
||||
buttons.push(
|
||||
<Fragment key="history">
|
||||
<Button
|
||||
size="sm"
|
||||
icon="history"
|
||||
onClick={() => isGrafanaRulerRule(rule.rulerRule) && showStateHistoryModal(rule.rulerRule)}
|
||||
>
|
||||
Show state history
|
||||
</Button>
|
||||
{StateHistoryModal}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (isFiringRule && shouldShowDeclareIncidentButton()) {
|
||||
buttons.push(
|
||||
<Fragment key="declare-incident">
|
||||
<DeclareIncidentButton title={rule.name} url={buildShareUrl()} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (buttons.length || rightButtons.length || moreActionsButtons.length) {
|
||||
return (
|
||||
<>
|
||||
<div className={style.wrapper}>
|
||||
<HorizontalGroup width="auto">{buttons.length ? buttons : <div />}</HorizontalGroup>
|
||||
<HorizontalGroup width="auto">
|
||||
{rightButtons.length && rightButtons}
|
||||
{moreActionsButtons.length && (
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
{moreActionsButtons.map((action) => (
|
||||
<React.Fragment key={uniqueId('action_')}>{action}</React.Fragment>
|
||||
))}
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button variant="secondary" size="sm">
|
||||
More
|
||||
<Icon name="angle-down" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
{!!ruleToDelete && (
|
||||
<ConfirmModal
|
||||
isOpen={true}
|
||||
title="Delete rule"
|
||||
body="Deleting this rule 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 && (
|
||||
<RedirectToCloneRule
|
||||
identifier={redirectToClone.identifier}
|
||||
isProvisioned={redirectToClone.isProvisioned}
|
||||
onDismiss={() => 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};
|
||||
`,
|
||||
});
|
||||
@@ -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(
|
||||
<LinkButton
|
||||
size="sm"
|
||||
key="explore"
|
||||
variant="primary"
|
||||
icon="chart-line"
|
||||
target="_blank"
|
||||
href={createExploreLink(rulesSource, rule.query)}
|
||||
>
|
||||
See graph
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
if (rule.annotations[Annotation.runbookURL]) {
|
||||
buttons.push(
|
||||
<LinkButton
|
||||
size="sm"
|
||||
key="runbook"
|
||||
variant="primary"
|
||||
icon="book"
|
||||
target="_blank"
|
||||
href={textUtil.sanitizeUrl(rule.annotations[Annotation.runbookURL])}
|
||||
>
|
||||
View runbook
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
if (rule.annotations[Annotation.dashboardUID]) {
|
||||
const dashboardUID = rule.annotations[Annotation.dashboardUID];
|
||||
const isReturnToPreviousEnabled = config.featureToggles.returnToPrevious;
|
||||
if (dashboardUID) {
|
||||
buttons.push(
|
||||
<LinkButton
|
||||
size="sm"
|
||||
key="dashboard"
|
||||
variant="primary"
|
||||
icon="apps"
|
||||
target={isReturnToPreviousEnabled ? undefined : '_blank'}
|
||||
href={`d/${encodeURIComponent(dashboardUID)}`}
|
||||
onClick={() => {
|
||||
setReturnToPrevious(rule.name);
|
||||
}}
|
||||
>
|
||||
Go to dashboard
|
||||
</LinkButton>
|
||||
);
|
||||
const panelId = rule.annotations[Annotation.panelID];
|
||||
if (panelId) {
|
||||
buttons.push(
|
||||
<LinkButton
|
||||
size="sm"
|
||||
key="panel"
|
||||
variant="primary"
|
||||
icon="apps"
|
||||
target={isReturnToPreviousEnabled ? undefined : '_blank'}
|
||||
href={`d/${encodeURIComponent(dashboardUID)}?viewPanel=${encodeURIComponent(panelId)}`}
|
||||
onClick={() => {
|
||||
setReturnToPrevious(rule.name);
|
||||
}}
|
||||
>
|
||||
Go to panel
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isGrafanaRulerRule(rule.rulerRule)) {
|
||||
buttons.push(
|
||||
<Fragment key="history">
|
||||
<Button
|
||||
size="sm"
|
||||
icon="history"
|
||||
onClick={() => isGrafanaRulerRule(rule.rulerRule) && showStateHistoryModal(rule.rulerRule)}
|
||||
>
|
||||
Show state history
|
||||
</Button>
|
||||
{StateHistoryModal}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return buttons.length ? <Stack>{buttons}</Stack> : null;
|
||||
};
|
||||
|
||||
export default RuleDetailsButtons;
|
||||
@@ -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' });
|
||||
|
||||
|
||||
@@ -266,7 +266,7 @@ function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean, showNe
|
||||
label: 'Actions',
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({ data: rule }) => {
|
||||
return <RuleActionsButtons rule={rule} rulesSource={rule.namespace.rulesSource} />;
|
||||
return <RuleActionsButtons compact showViewButton rule={rule} rulesSource={rule.namespace.rulesSource} />;
|
||||
},
|
||||
size: '200px',
|
||||
});
|
||||
|
||||
+28
@@ -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",
|
||||
]
|
||||
`;
|
||||
@@ -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,
|
||||
|
||||
@@ -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<AlertRul
|
||||
[AlertRuleAction.Explore]: toAbility(AlwaysSupported, AccessControlAction.DataSourcesExplore),
|
||||
[AlertRuleAction.Silence]: canSilence,
|
||||
[AlertRuleAction.ModifyExport]: [isGrafanaManagedAlertRule, exportAllowed],
|
||||
[AlertRuleAction.Pause]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false],
|
||||
};
|
||||
|
||||
return abilities;
|
||||
|
||||
@@ -755,42 +755,27 @@ export function mockDashboardDto(
|
||||
};
|
||||
}
|
||||
|
||||
export const onCallPluginMetaMock: PluginMeta = {
|
||||
name: 'Grafana OnCall',
|
||||
id: 'grafana-oncall-app',
|
||||
type: PluginType.app,
|
||||
module: 'plugins/grafana-oncall-app/module',
|
||||
baseUrl: 'public/plugins/grafana-oncall-app',
|
||||
info: {
|
||||
author: { name: 'Grafana Labs' },
|
||||
description: 'Grafana OnCall',
|
||||
updated: '',
|
||||
version: '',
|
||||
links: [],
|
||||
logos: {
|
||||
small: '',
|
||||
large: '',
|
||||
export const getMockPluginMeta: (id: string, name: string) => 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');
|
||||
|
||||
@@ -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));
|
||||
@@ -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<string, PluginMeta>) =>
|
||||
http.get<{ pluginId: string }>(`/api/plugins/:pluginId/settings`, ({ params: { pluginId } }) =>
|
||||
pluginsRegistry.has(pluginId)
|
||||
? HttpResponse.json<PluginMeta>(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<PluginMeta>(matchingPlugin)
|
||||
: HttpResponse.json({ message: 'Plugin not found, no installed plugin with that id' }, { status: 404 });
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<string, PluginMeta>();
|
||||
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<string, PluginMeta> = {
|
||||
slo: {
|
||||
export const plugins: PluginMeta[] = [
|
||||
{
|
||||
id: 'grafana-slo-app',
|
||||
name: 'SLO dashboard',
|
||||
type: PluginType.app,
|
||||
@@ -48,7 +45,7 @@ export const plugins: Record<string, PluginMeta> = {
|
||||
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<string, PluginMeta> = {
|
||||
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<string, PluginMeta> = {
|
||||
module: 'public/plugins/grafana-asserts-app/module.js',
|
||||
baseUrl: 'public/plugins/grafana-asserts-app',
|
||||
},
|
||||
};
|
||||
];
|
||||
|
||||
@@ -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<string, string> {
|
||||
|
||||
Reference in New Issue
Block a user