From 30cb04f205d6ffb8ba7771cdec1727c704b0b9f3 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Mon, 31 Oct 2022 14:42:09 +0100 Subject: [PATCH] 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 --- .../unified/PanelAlertTabContent.test.tsx | 3 + .../alerting/unified/RuleList.test.tsx | 3 + .../components/rules/RuleActionsButtons.tsx | 218 ++++++++++++++++++ .../components/rules/RuleDetails.test.tsx | 114 +-------- .../rules/RuleDetailsActionButtons.tsx | 161 ++----------- .../components/rules/RulesTable.test.tsx | 122 ++++++++++ .../unified/components/rules/RulesTable.tsx | 13 +- 7 files changed, 378 insertions(+), 256 deletions(-) create mode 100644 public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx create mode 100644 public/app/features/alerting/unified/components/rules/RulesTable.test.tsx diff --git a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx index 4fc0d82a73f..41762d22a02 100644 --- a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx +++ b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx @@ -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({ diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index 76be42c9363..3620d5faec4 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -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: () => { diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx new file mode 100644 index 00000000000..0531ac03270 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx @@ -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
{children}
; + } +} + +export const RuleActionsButtons: FC = ({ rule, rulesSource }) => { + const dispatch = useDispatch(); + const location = useLocation(); + const notifyApp = useAppNotification(); + const style = useStyles2(getStyles); + const { namespace, group, rulerRule } = rule; + const [ruleToDelete, setRuleToDelete] = useState(); + + 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( + + + View + + + ); + } + + 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( + { + notifyApp.error('Error while copying URL', copiedText); + }} + className={style.button} + size="sm" + getText={buildShareUrl} + > + Copy link to rule + + ); + } + + buttons.push( + + + Edit + + + ); + } + + if (isRemovable && rulerRule && !isFederated && !isProvisioned) { + buttons.push( + + + + ); + } + + if (buttons.length) { + return ( + <> +
+ + {buttons.length ? buttons.map((button, index) =>
{button}
) :
} + +
+ {!!ruleToDelete && ( + 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; + `, +}); 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 7fad7526137..f0267f90617 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx @@ -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) { ...override, }); } - -function getCloudRule(override?: Partial) { - return mockCombinedRule({ - namespace: { - groups: [], - name: 'Cortex', - rulesSource: mockDataSource(), - }, - promRule: mockPromAlertingRule(), - rulerRule: mockRulerAlertingRule(), - ...override, - }); -} diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx index 4ef5a98f33f..00439bb23bd 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx @@ -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 = ({ rule, rulesSource }) => { - const dispatch = useDispatch(); - const location = useLocation(); - const notifyApp = useAppNotification(); const style = useStyles2(getStyles); - const { namespace, group, rulerRule } = rule; - const [ruleToDelete, setRuleToDelete] = useState(); + 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( = ({ rule, rulesSource }) => { ); } if (rule.annotations[Annotation.runbookURL]) { - leftButtons.push( + buttons.push( = ({ rule, rulesSource }) => { if (rule.annotations[Annotation.dashboardUID]) { const dashboardUID = rule.annotations[Annotation.dashboardUID]; if (dashboardUID) { - leftButtons.push( + buttons.push( = ({ rule, rulesSource }) => { ); const panelId = rule.annotations[Annotation.panelID]; if (panelId) { - leftButtons.push( + buttons.push( = ({ rule, rulesSource }) => { } if (alertmanagerSourceName && contextSrv.hasAccess(AccessControlAction.AlertingInstanceCreate, contextSrv.isEditor)) { - leftButtons.push( + buttons.push( = ({ rule, rulesSource }) => { } if (alertId) { - leftButtons.push( + buttons.push( - ); - } - - if (leftButtons.length || rightButtons.length) { + if (buttons.length) { return ( - <> -
- {leftButtons.length ? leftButtons :
} - {rightButtons.length ? rightButtons :
} -
- {!!ruleToDelete && ( - setRuleToDelete(undefined)} - /> - )} - +
+ {buttons.length ? buttons :
} +
); } return null; }; -function inViewMode(pathname: string): boolean { - return pathname.endsWith('/view'); -} - export const getStyles = (theme: GrafanaTheme2) => ({ wrapper: css` padding: ${theme.spacing(2)} 0; diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx new file mode 100644 index 00000000000..31b0241fb46 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx @@ -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( + + + + + + ); +} + +function getGrafanaRule(override?: Partial) { + return mockCombinedRule({ + namespace: { + groups: [], + name: 'Grafana', + rulesSource: 'grafana', + }, + ...override, + }); +} + +function getCloudRule(override?: Partial) { + 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(); + }); + }); +}); diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.tsx index a6771e9f47c..affa11b32f2 100644 --- a/public/app/features/alerting/unified/components/rules/RulesTable.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesTable.tsx @@ -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 ; + }, + size: '290px', + }); + return columns; - }, [hasRuler, rulerRulesLoaded, showSummaryColumn, showGroupColumn]); + }, [showSummaryColumn, showGroupColumn, hasRuler, rulerRulesLoaded]); }