Alerting: move alert rule View/Edit/Delete action buttons to collapsed row (#57687)

* Move view edit and delete buttons to RulesTable

* Move tests for edit and delete buttons to a new test file for RulesTable

* Action buttons: Only show icon for non large screens, and add a Tooltip

* Remove buttons moved from the RuleDetailsActionButtons component

* Fix horizontal aligment for icons for non large devices
This commit is contained in:
Sonia Aguilar 2022-10-31 14:42:09 +01:00 committed by GitHub
parent ce38840f29
commit 30cb04f205
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 378 additions and 256 deletions

View File

@ -6,6 +6,7 @@ import { byTestId } from 'testing-library-selector';
import { DataSourceApi } from '@grafana/data';
import { locationService, setDataSourceSrv } from '@grafana/runtime';
import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { toggleOption } from 'app/features/variables/pickers/OptionsPicker/reducer';
@ -33,8 +34,10 @@ import * as ruleFormUtils from './utils/rule-form';
jest.mock('./api/prometheus');
jest.mock('./api/ruler');
jest.mock('../../../core/hooks/useMediaQueryChange');
jest.spyOn(config, 'getAllDataSources');
jest.spyOn(ruleActionButtons, 'matchesWidth').mockReturnValue(false);
const dataSources = {
prometheus: mockDataSource<PromOptions>({

View File

@ -8,6 +8,7 @@ import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector'
import { locationService, setDataSourceSrv, logInfo } from '@grafana/runtime';
import { contextSrv } from 'app/core/services/context_srv';
import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
import { configureStore } from 'app/store/configureStore';
import { AccessControlAction } from 'app/types';
import { PromAlertingRuleState, PromApplication } from 'app/types/unified-alerting-dto';
@ -37,6 +38,8 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
jest.mock('./api/buildInfo');
jest.mock('./api/prometheus');
jest.mock('./api/ruler');
jest.mock('../../../core/hooks/useMediaQueryChange');
jest.spyOn(ruleActionButtons, 'matchesWidth').mockReturnValue(false);
jest.mock('app/core/core', () => ({
appEvents: {
subscribe: () => {

View File

@ -0,0 +1,218 @@
import { css } from '@emotion/css';
import React, { FC, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { GrafanaTheme2, urlUtil } from '@grafana/data';
import { config } from '@grafana/runtime';
import {
Button,
ClipboardButton,
ConfirmModal,
HorizontalGroup,
LinkButton,
Tooltip,
useStyles2,
useTheme2,
} from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification';
import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange';
import { useDispatch } from 'app/types';
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
import { deleteRuleAction } from '../../state/actions';
import { getRulesSourceName, isCloudRulesSource } from '../../utils/datasource';
import { createViewLink } from '../../utils/misc';
import * as ruleId from '../../utils/rule-id';
import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
export const matchesWidth = (width: number) => window.matchMedia(`(max-width: ${width}px)`).matches;
interface Props {
rule: CombinedRule;
rulesSource: RulesSource;
}
function DontShowIfSmallDevice({ children }: { children: JSX.Element | string }) {
const theme = useTheme2();
const smBreakpoint = theme.breakpoints.values.xxl;
const [isSmallScreen, setIsSmallScreen] = useState(matchesWidth(smBreakpoint));
const style = useStyles2(getStyles);
useMediaQueryChange({
breakpoint: smBreakpoint,
onChange: (e) => {
setIsSmallScreen(e.matches);
},
});
if (isSmallScreen) {
return null;
} else {
return <div className={style.buttonText}>{children}</div>;
}
}
export const RuleActionsButtons: FC<Props> = ({ rule, rulesSource }) => {
const dispatch = useDispatch();
const location = useLocation();
const notifyApp = useAppNotification();
const style = useStyles2(getStyles);
const { namespace, group, rulerRule } = rule;
const [ruleToDelete, setRuleToDelete] = useState<CombinedRule>();
const rulesSourceName = getRulesSourceName(rulesSource);
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
const buttons: JSX.Element[] = [];
const isFederated = isFederatedRuleGroup(group);
const { isEditable, isRemovable } = useIsRuleEditable(rulesSourceName, rulerRule);
const returnTo = location.pathname + location.search;
const isViewMode = inViewMode(location.pathname);
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 = () => {
if (isCloudRulesSource(rulesSource)) {
const { appUrl, appSubUrl } = config;
const baseUrl = appSubUrl !== '' ? `${appUrl}${appSubUrl}/` : config.appUrl;
const ruleUrl = `${encodeURIComponent(rulesSource.name)}/${encodeURIComponent(rule.name)}`;
return `${baseUrl}alerting/${ruleUrl}/find`;
}
return window.location.href.split('?')[0];
};
if (!isViewMode) {
buttons.push(
<Tooltip placement="top" content={'View'}>
<LinkButton
className={style.button}
size="xs"
key="view"
variant="secondary"
icon="eye"
href={createViewLink(rulesSource, rule, returnTo)}
>
<DontShowIfSmallDevice>View</DontShowIfSmallDevice>
</LinkButton>
</Tooltip>
);
}
if (isEditable && rulerRule && !isFederated && !isProvisioned) {
const sourceName = getRulesSourceName(rulesSource);
const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule);
const editURL = urlUtil.renderUrl(
`${config.appSubUrl}/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`,
{
returnTo,
}
);
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>
);
}
buttons.push(
<Tooltip placement="top" content={'Edit'}>
<LinkButton className={style.button} size="xs" key="edit" variant="secondary" icon="pen" href={editURL}>
<DontShowIfSmallDevice>Edit</DontShowIfSmallDevice>
</LinkButton>
</Tooltip>
);
}
if (isRemovable && rulerRule && !isFederated && !isProvisioned) {
buttons.push(
<Tooltip placement="top" content={'Delete'}>
<Button
className={style.button}
size="xs"
type="button"
key="delete"
variant="secondary"
icon="trash-alt"
onClick={() => setRuleToDelete(rule)}
>
<DontShowIfSmallDevice>Delete</DontShowIfSmallDevice>
</Button>
</Tooltip>
);
}
if (buttons.length) {
return (
<>
<div className={style.wrapper}>
<HorizontalGroup width="auto">
{buttons.length ? buttons.map((button, index) => <div key={index}>{button}</div>) : <div />}
</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)}
/>
)}
</>
);
}
return null;
};
function inViewMode(pathname: string): boolean {
return pathname.endsWith('/view');
}
export const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css`
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
`,
button: css`
height: 24px;
font-size: ${theme.typography.size.sm};
svg {
margin-right: 0;
}
`,
buttonText: css`
margin-left: 8px;
`,
});

View File

@ -9,17 +9,10 @@ import { configureStore } from 'app/store/configureStore';
import { AccessControlAction } from 'app/types';
import { CombinedRule } from 'app/types/unified-alerting';
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
import { mockCombinedRule, mockDataSource, mockPromAlertingRule, mockRulerAlertingRule } from '../../mocks';
import { mockCombinedRule } from '../../mocks';
import { RuleDetails } from './RuleDetails';
jest.mock('../../hooks/useIsRuleEditable');
const mocks = {
useIsRuleEditable: jest.mocked(useIsRuleEditable),
};
const ui = {
actionButtons: {
edit: byRole('link', { name: 'Edit' }),
@ -31,51 +24,8 @@ const ui = {
jest.spyOn(contextSrv, 'accessControlEnabled').mockReturnValue(true);
describe('RuleDetails RBAC', () => {
describe('Grafana rules action buttons', () => {
describe('Grafana rules action buttons in details', () => {
const grafanaRule = getGrafanaRule({ name: 'Grafana' });
it('Should not render Edit button for users without the update permission', () => {
// Arrange
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false });
// Act
renderRuleDetails(grafanaRule);
// Assert
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
});
it('Should not render Delete button for users without the delete permission', () => {
// Arrange
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: false });
// Act
renderRuleDetails(grafanaRule);
// Assert
expect(ui.actionButtons.delete.query()).not.toBeInTheDocument();
});
it('Should render Edit button for users with the update permission', () => {
// Arrange
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
// Act
renderRuleDetails(grafanaRule);
// Assert
expect(ui.actionButtons.edit.query()).toBeInTheDocument();
});
it('Should render Delete button for users with the delete permission', () => {
// Arrange
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
// Act
renderRuleDetails(grafanaRule);
// Assert
expect(ui.actionButtons.delete.query()).toBeInTheDocument();
});
it('Should not render Silence button for users wihout the instance create permission', () => {
// Arrange
@ -101,53 +51,6 @@ describe('RuleDetails RBAC', () => {
expect(ui.actionButtons.silence.query()).toBeInTheDocument();
});
});
describe('Cloud rules action buttons', () => {
const cloudRule = getCloudRule({ name: 'Cloud' });
it('Should not render Edit button for users without the update permission', () => {
// Arrange
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false });
// Act
renderRuleDetails(cloudRule);
// Assert
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
});
it('Should not render Delete button for users without the delete permission', () => {
// Arrange
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: false });
// Act
renderRuleDetails(cloudRule);
// Assert
expect(ui.actionButtons.delete.query()).not.toBeInTheDocument();
});
it('Should render Edit button for users with the update permission', () => {
// Arrange
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
// Act
renderRuleDetails(cloudRule);
// Assert
expect(ui.actionButtons.edit.query()).toBeInTheDocument();
});
it('Should render Delete button for users with the delete permission', () => {
// Arrange
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
// Act
renderRuleDetails(cloudRule);
// Assert
expect(ui.actionButtons.delete.query()).toBeInTheDocument();
});
});
});
function renderRuleDetails(rule: CombinedRule) {
@ -172,16 +75,3 @@ function getGrafanaRule(override?: Partial<CombinedRule>) {
...override,
});
}
function getCloudRule(override?: Partial<CombinedRule>) {
return mockCombinedRule({
namespace: {
groups: [],
name: 'Cortex',
rulesSource: mockDataSource(),
},
promRule: mockPromAlertingRule(),
rulerRule: mockRulerAlertingRule(),
...override,
});
}

View File

@ -1,23 +1,17 @@
import { css } from '@emotion/css';
import React, { FC, Fragment, useState } from 'react';
import { useLocation } from 'react-router-dom';
import React, { FC, Fragment } from 'react';
import { GrafanaTheme2, urlUtil } from '@grafana/data';
import { config } from '@grafana/runtime';
import { Button, ClipboardButton, ConfirmModal, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { AccessControlAction, useDispatch } from 'app/types';
import { AccessControlAction } from 'app/types';
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
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, createViewLink, makeRuleBasedSilenceLink } from '../../utils/misc';
import * as ruleId from '../../utils/rule-id';
import { isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
import { createExploreLink, makeRuleBasedSilenceLink } from '../../utils/misc';
import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
interface Props {
@ -26,60 +20,25 @@ interface Props {
}
export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
const dispatch = useDispatch();
const location = useLocation();
const notifyApp = useAppNotification();
const style = useStyles2(getStyles);
const { namespace, group, rulerRule } = rule;
const [ruleToDelete, setRuleToDelete] = useState<CombinedRule>();
const { group } = rule;
const alertId = isGrafanaRulerRule(rule.rulerRule) ? rule.rulerRule.grafana_alert.id ?? '' : '';
const { StateHistoryModal, showStateHistoryModal } = useStateHistoryModal(alertId);
const alertmanagerSourceName = isGrafanaRulesSource(rulesSource)
? rulesSource
: getAlertmanagerByUid(rulesSource.jsonData.alertmanagerUid)?.name;
const rulesSourceName = getRulesSourceName(rulesSource);
const hasExplorePermission = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore);
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
const leftButtons: JSX.Element[] = [];
const rightButtons: JSX.Element[] = [];
const buttons: JSX.Element[] = [];
const isFederated = isFederatedRuleGroup(group);
const { isEditable, isRemovable } = useIsRuleEditable(rulesSourceName, rulerRule);
const returnTo = location.pathname + location.search;
const isViewMode = inViewMode(location.pathname);
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 = () => {
if (isCloudRulesSource(rulesSource)) {
const { appUrl, appSubUrl } = config;
const baseUrl = appSubUrl !== '' ? `${appUrl}${appSubUrl}/` : config.appUrl;
const ruleUrl = `${encodeURIComponent(rulesSource.name)}/${encodeURIComponent(rule.name)}`;
return `${baseUrl}alerting/${ruleUrl}/find`;
}
return window.location.href.split('?')[0];
};
// explore does not support grafana rule queries atm
// neither do "federated rules"
if (isCloudRulesSource(rulesSource) && hasExplorePermission && !isFederated) {
leftButtons.push(
buttons.push(
<LinkButton
className={style.button}
size="xs"
@ -94,7 +53,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
);
}
if (rule.annotations[Annotation.runbookURL]) {
leftButtons.push(
buttons.push(
<LinkButton
className={style.button}
size="xs"
@ -111,7 +70,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
if (rule.annotations[Annotation.dashboardUID]) {
const dashboardUID = rule.annotations[Annotation.dashboardUID];
if (dashboardUID) {
leftButtons.push(
buttons.push(
<LinkButton
className={style.button}
size="xs"
@ -126,7 +85,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
);
const panelId = rule.annotations[Annotation.panelID];
if (panelId) {
leftButtons.push(
buttons.push(
<LinkButton
className={style.button}
size="xs"
@ -144,7 +103,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
}
if (alertmanagerSourceName && contextSrv.hasAccess(AccessControlAction.AlertingInstanceCreate, contextSrv.isEditor)) {
leftButtons.push(
buttons.push(
<LinkButton
className={style.button}
size="xs"
@ -159,7 +118,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
}
if (alertId) {
leftButtons.push(
buttons.push(
<Fragment key="history">
<Button className={style.button} size="xs" icon="history" onClick={() => showStateHistoryModal()}>
Show state history
@ -169,101 +128,17 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
);
}
if (!isViewMode) {
rightButtons.push(
<LinkButton
className={style.button}
size="xs"
key="view"
variant="secondary"
icon="eye"
href={createViewLink(rulesSource, rule, returnTo)}
>
View
</LinkButton>
);
}
if (isEditable && rulerRule && !isFederated && !isProvisioned) {
const sourceName = getRulesSourceName(rulesSource);
const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule);
const editURL = urlUtil.renderUrl(
`${config.appSubUrl}/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`,
{
returnTo,
}
);
if (isViewMode) {
rightButtons.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>
);
}
rightButtons.push(
<LinkButton className={style.button} size="xs" key="edit" variant="secondary" icon="pen" href={editURL}>
Edit
</LinkButton>
);
}
if (isRemovable && rulerRule && !isFederated && !isProvisioned) {
rightButtons.push(
<Button
className={style.button}
size="xs"
type="button"
key="delete"
variant="secondary"
icon="trash-alt"
onClick={() => setRuleToDelete(rule)}
>
Delete
</Button>
);
}
if (leftButtons.length || rightButtons.length) {
if (buttons.length) {
return (
<>
<div className={style.wrapper}>
<HorizontalGroup width="auto">{leftButtons.length ? leftButtons : <div />}</HorizontalGroup>
<HorizontalGroup width="auto">{rightButtons.length ? rightButtons : <div />}</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)}
/>
)}
</>
<div className={style.wrapper}>
<HorizontalGroup width="auto">{buttons.length ? buttons : <div />}</HorizontalGroup>
</div>
);
}
return null;
};
function inViewMode(pathname: string): boolean {
return pathname.endsWith('/view');
}
export const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css`
padding: ${theme.spacing(2)} 0;

View File

@ -0,0 +1,122 @@
import { render } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { byRole } from 'testing-library-selector';
import { contextSrv } from 'app/core/services/context_srv';
import { configureStore } from 'app/store/configureStore';
import { CombinedRule } from 'app/types/unified-alerting';
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
import { mockCombinedRule, mockDataSource, mockPromAlertingRule, mockRulerAlertingRule } from '../../mocks';
import { RulesTable } from './RulesTable';
jest.mock('../../hooks/useIsRuleEditable');
const mocks = {
useIsRuleEditable: jest.mocked(useIsRuleEditable),
};
const ui = {
actionButtons: {
edit: byRole('link', { name: 'Edit' }),
view: byRole('link', { name: 'View' }),
delete: byRole('button', { name: 'Delete' }),
},
};
jest.spyOn(contextSrv, 'accessControlEnabled').mockReturnValue(true);
function renderRulesTable(rule: CombinedRule) {
const store = configureStore();
render(
<Provider store={store}>
<MemoryRouter>
<RulesTable rules={[rule]} />
</MemoryRouter>
</Provider>
);
}
function getGrafanaRule(override?: Partial<CombinedRule>) {
return mockCombinedRule({
namespace: {
groups: [],
name: 'Grafana',
rulesSource: 'grafana',
},
...override,
});
}
function getCloudRule(override?: Partial<CombinedRule>) {
return mockCombinedRule({
namespace: {
groups: [],
name: 'Cortex',
rulesSource: mockDataSource(),
},
promRule: mockPromAlertingRule(),
rulerRule: mockRulerAlertingRule(),
...override,
});
}
describe('RulesTable RBAC', () => {
describe('Grafana rules action buttons', () => {
const grafanaRule = getGrafanaRule({ name: 'Grafana' });
it('Should not render Edit button for users without the update permission', () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false });
renderRulesTable(grafanaRule);
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
});
it('Should not render Delete button for users without the delete permission', () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: false });
renderRulesTable(grafanaRule);
expect(ui.actionButtons.delete.query()).not.toBeInTheDocument();
});
it('Should render Edit button for users with the update permission', () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
renderRulesTable(grafanaRule);
expect(ui.actionButtons.edit.query()).toBeInTheDocument();
});
it('Should render Delete button for users with the delete permission', () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
renderRulesTable(grafanaRule);
expect(ui.actionButtons.delete.query()).toBeInTheDocument();
});
});
describe('Cloud rules action buttons', () => {
const cloudRule = getCloudRule({ name: 'Cloud' });
it('Should not render Edit button for users without the update permission', () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false });
renderRulesTable(cloudRule);
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
});
it('Should not render Delete button for users without the delete permission', () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: false });
renderRulesTable(cloudRule);
expect(ui.actionButtons.delete.query()).not.toBeInTheDocument();
});
it('Should render Edit button for users with the update permission', () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
renderRulesTable(cloudRule);
expect(ui.actionButtons.edit.query()).toBeInTheDocument();
});
it('Should render Delete button for users with the delete permission', () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
renderRulesTable(cloudRule);
expect(ui.actionButtons.delete.query()).toBeInTheDocument();
});
});
});

View File

@ -15,6 +15,7 @@ import { ProvisioningBadge } from '../Provisioning';
import { RuleLocation } from '../RuleLocation';
import { Tokenize } from '../Tokenize';
import { RuleActionsButtons } from './RuleActionsButtons';
import { RuleConfigStatus } from './RuleConfigStatus';
import { RuleDetails } from './RuleDetails';
import { RuleHealth } from './RuleHealth';
@ -188,6 +189,16 @@ function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean) {
size: 5,
});
}
columns.push({
id: 'actions',
label: 'Actions',
// eslint-disable-next-line react/display-name
renderCell: ({ data: rule }) => {
return <RuleActionsButtons rule={rule} rulesSource={rule.namespace.rulesSource} />;
},
size: '290px',
});
return columns;
}, [hasRuler, rulerRulesLoaded, showSummaryColumn, showGroupColumn]);
}, [showSummaryColumn, showGroupColumn, hasRuler, rulerRulesLoaded]);
}