mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Fix missing edit/delete/copy link in alert view (#60874)
Fix missing edit/delete/copy link in alert view
This commit is contained in:
@@ -2,17 +2,33 @@ import { act, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { byRole } from 'testing-library-selector';
|
||||
|
||||
import { DataSourceJsonData, PluginMeta } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { RuleViewer } from './RuleViewer';
|
||||
import { useCombinedRule } from './hooks/useCombinedRule';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||
import { useIsRuleEditable } from './hooks/useIsRuleEditable';
|
||||
import { getCloudRule, getGrafanaRule } from './mocks';
|
||||
|
||||
const mockGrafanaRule = getGrafanaRule({ name: 'Test alert' });
|
||||
const mockCloudRule = getCloudRule({ name: 'cloud test alert' });
|
||||
const mockRoute: GrafanaRouteComponentProps<{ id?: string; sourceName?: string }> = {
|
||||
route: {
|
||||
path: '/',
|
||||
component: RuleViewer,
|
||||
},
|
||||
queryParams: { returnTo: '/alerting/list' },
|
||||
match: { params: { id: 'test1', sourceName: 'grafana' }, isExact: false, url: 'asdf', path: '' },
|
||||
history: locationService.getHistory(),
|
||||
location: { pathname: '', hash: '', search: '', state: '' },
|
||||
staticContext: {},
|
||||
};
|
||||
|
||||
jest.mock('./hooks/useCombinedRule');
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
@@ -40,6 +56,20 @@ const renderRuleViewer = () => {
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const ui = {
|
||||
actionButtons: {
|
||||
edit: byRole('link', { name: /edit/i }),
|
||||
delete: byRole('button', { name: /delete/i }),
|
||||
silence: byRole('link', { name: 'Silence' }),
|
||||
},
|
||||
};
|
||||
jest.mock('./hooks/useIsRuleEditable');
|
||||
|
||||
const mocks = {
|
||||
useIsRuleEditable: jest.mocked(useIsRuleEditable),
|
||||
};
|
||||
|
||||
describe('RuleViewer', () => {
|
||||
let mockCombinedRule: jest.MockedFn<typeof useCombinedRule>;
|
||||
|
||||
@@ -59,6 +89,7 @@ describe('RuleViewer', () => {
|
||||
requestId: 'A',
|
||||
error: undefined,
|
||||
});
|
||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false });
|
||||
await renderRuleViewer();
|
||||
|
||||
expect(screen.getByText(/view rule/i)).toBeInTheDocument();
|
||||
@@ -73,82 +104,142 @@ describe('RuleViewer', () => {
|
||||
requestId: 'A',
|
||||
error: undefined,
|
||||
});
|
||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false });
|
||||
await renderRuleViewer();
|
||||
expect(screen.getByText(/view rule/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/cloud test alert/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const mockGrafanaRule = {
|
||||
name: 'Test alert',
|
||||
query: 'up',
|
||||
labels: {},
|
||||
annotations: {},
|
||||
group: {
|
||||
name: 'Prom up alert',
|
||||
rules: [],
|
||||
},
|
||||
namespace: {
|
||||
rulesSource: GRAFANA_RULES_SOURCE_NAME,
|
||||
name: 'Alerts',
|
||||
groups: [],
|
||||
},
|
||||
rulerRule: {
|
||||
for: '',
|
||||
annotations: {},
|
||||
labels: {},
|
||||
grafana_alert: {
|
||||
condition: 'B',
|
||||
exec_err_state: GrafanaAlertStateDecision.Alerting,
|
||||
namespace_id: 11,
|
||||
namespace_uid: 'namespaceuid123',
|
||||
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||
title: 'Test alert',
|
||||
uid: 'asdf23',
|
||||
data: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
describe('RuleDetails RBAC', () => {
|
||||
describe('Grafana rules action buttons in details', () => {
|
||||
let mockCombinedRule: jest.MockedFn<typeof useCombinedRule>;
|
||||
|
||||
const mockCloudRule = {
|
||||
name: 'Cloud test alert',
|
||||
labels: {},
|
||||
query: 'up == 0',
|
||||
annotations: {},
|
||||
group: {
|
||||
name: 'test',
|
||||
rules: [],
|
||||
},
|
||||
promRule: {
|
||||
health: 'ok',
|
||||
name: 'cloud up alert',
|
||||
query: 'up == 0',
|
||||
type: 'alerting',
|
||||
},
|
||||
namespace: {
|
||||
name: 'prom test alerts',
|
||||
groups: [],
|
||||
rulesSource: {
|
||||
name: 'prom test',
|
||||
type: 'prometheus',
|
||||
uid: 'asdf23',
|
||||
id: 1,
|
||||
meta: {} as PluginMeta,
|
||||
jsonData: {} as DataSourceJsonData,
|
||||
access: 'proxy',
|
||||
readOnly: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
beforeEach(() => {
|
||||
mockCombinedRule = jest.mocked(useCombinedRule);
|
||||
});
|
||||
|
||||
const mockRoute: GrafanaRouteComponentProps<{ id?: string; sourceName?: string }> = {
|
||||
route: {
|
||||
path: '/',
|
||||
component: RuleViewer,
|
||||
},
|
||||
queryParams: { returnTo: '/alerting/list' },
|
||||
match: { params: { id: 'test1', sourceName: 'grafana' }, isExact: false, url: 'asdf', path: '' },
|
||||
history: locationService.getHistory(),
|
||||
location: { pathname: '', hash: '', search: '', state: '' },
|
||||
staticContext: {},
|
||||
};
|
||||
afterEach(() => {
|
||||
mockCombinedRule.mockReset();
|
||||
});
|
||||
it('Should render Edit button for users with the update permission', async () => {
|
||||
// Arrange
|
||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
|
||||
mockCombinedRule.mockReturnValue({
|
||||
result: mockGrafanaRule as CombinedRule,
|
||||
loading: false,
|
||||
dispatched: true,
|
||||
requestId: 'A',
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
// Act
|
||||
await renderRuleViewer();
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.edit.get()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render Delete button for users with the delete permission', async () => {
|
||||
// Arrange
|
||||
mockCombinedRule.mockReturnValue({
|
||||
result: mockGrafanaRule as CombinedRule,
|
||||
loading: false,
|
||||
dispatched: true,
|
||||
requestId: 'A',
|
||||
error: undefined,
|
||||
});
|
||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
|
||||
|
||||
// Act
|
||||
await renderRuleViewer();
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.delete.get()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should not render Silence button for users wihout the instance create permission', async () => {
|
||||
// Arrange
|
||||
mockCombinedRule.mockReturnValue({
|
||||
result: mockGrafanaRule as CombinedRule,
|
||||
loading: false,
|
||||
dispatched: true,
|
||||
requestId: 'A',
|
||||
error: undefined,
|
||||
});
|
||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
|
||||
|
||||
// Act
|
||||
await renderRuleViewer();
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.silence.query()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render Silence button for users with the instance create permissions', async () => {
|
||||
// Arrange
|
||||
mockCombinedRule.mockReturnValue({
|
||||
result: mockGrafanaRule as CombinedRule,
|
||||
loading: false,
|
||||
dispatched: true,
|
||||
requestId: 'A',
|
||||
error: undefined,
|
||||
});
|
||||
jest
|
||||
.spyOn(contextSrv, 'hasPermission')
|
||||
.mockImplementation((action) => action === AccessControlAction.AlertingInstanceCreate);
|
||||
|
||||
// Act
|
||||
await renderRuleViewer();
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.silence.query()).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
describe('Cloud rules action buttons', () => {
|
||||
let mockCombinedRule: jest.MockedFn<typeof useCombinedRule>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCombinedRule = jest.mocked(useCombinedRule);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockCombinedRule.mockReset();
|
||||
});
|
||||
it('Should render edit button for users with the update permission', async () => {
|
||||
// Arrange
|
||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
|
||||
mockCombinedRule.mockReturnValue({
|
||||
result: mockCloudRule as CombinedRule,
|
||||
loading: false,
|
||||
dispatched: true,
|
||||
requestId: 'A',
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
// Act
|
||||
await renderRuleViewer();
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.edit.query()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render Delete button for users with the delete permission', async () => {
|
||||
// Arrange
|
||||
mockCombinedRule.mockReturnValue({
|
||||
result: mockCloudRule as CombinedRule,
|
||||
loading: false,
|
||||
dispatched: true,
|
||||
requestId: 'A',
|
||||
error: undefined,
|
||||
});
|
||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
|
||||
|
||||
// Act
|
||||
await renderRuleViewer();
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.delete.query()).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -170,7 +170,7 @@ export function RuleViewer({ match }: RuleViewerProps) {
|
||||
<Icon name="bell" size="lg" /> {rule.name}
|
||||
</h4>
|
||||
<RuleState rule={rule} isCreating={false} isDeleting={false} />
|
||||
<RuleDetailsActionButtons rule={rule} rulesSource={rulesSource} />
|
||||
<RuleDetailsActionButtons rule={rule} rulesSource={rulesSource} isViewMode={true} />
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
<div className={styles.leftSide}>
|
||||
|
||||
@@ -9,24 +9,57 @@ import { configureStore } from 'app/store/configureStore';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
|
||||
import { mockCombinedRule } from '../../mocks';
|
||||
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
|
||||
import { getCloudRule, getGrafanaRule } from '../../mocks';
|
||||
|
||||
import { RuleDetails } from './RuleDetails';
|
||||
|
||||
jest.mock('../../hooks/useIsRuleEditable');
|
||||
|
||||
const mocks = {
|
||||
useIsRuleEditable: jest.mocked(useIsRuleEditable),
|
||||
};
|
||||
|
||||
const ui = {
|
||||
actionButtons: {
|
||||
edit: byRole('link', { name: 'Edit' }),
|
||||
delete: byRole('button', { name: 'Delete' }),
|
||||
edit: byRole('link', { name: /edit/i }),
|
||||
delete: byRole('button', { name: /delete/i }),
|
||||
silence: byRole('link', { name: 'Silence' }),
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(contextSrv, 'accessControlEnabled').mockReturnValue(true);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('RuleDetails RBAC', () => {
|
||||
describe('Grafana rules action buttons in details', () => {
|
||||
const grafanaRule = getGrafanaRule({ name: 'Grafana' });
|
||||
|
||||
it('Should not 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()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should not 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()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should not render Silence button for users wihout the instance create permission', () => {
|
||||
// Arrange
|
||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
|
||||
@@ -51,6 +84,31 @@ 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 with the update permission', () => {
|
||||
// Arrange
|
||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
|
||||
|
||||
// Act
|
||||
renderRuleDetails(cloudRule);
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should not 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()).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function renderRuleDetails(rule: CombinedRule) {
|
||||
@@ -64,14 +122,3 @@ function renderRuleDetails(rule: CombinedRule) {
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function getGrafanaRule(override?: Partial<CombinedRule>) {
|
||||
return mockCombinedRule({
|
||||
namespace: {
|
||||
groups: [],
|
||||
name: 'Grafana',
|
||||
rulesSource: 'grafana',
|
||||
},
|
||||
...override,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export const RuleDetails: FC<Props> = ({ rule }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RuleDetailsActionButtons rule={rule} rulesSource={rulesSource} />
|
||||
<RuleDetailsActionButtons rule={rule} rulesSource={rulesSource} isViewMode={false} />
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.leftSide}>
|
||||
{<EvaluationBehaviorSummary rule={rule} />}
|
||||
|
||||
@@ -1,29 +1,41 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { FC, Fragment } from 'react';
|
||||
import React, { FC, Fragment, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { GrafanaTheme2, textUtil } from '@grafana/data';
|
||||
import { Button, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { GrafanaTheme2, textUtil, 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 { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { AccessControlAction, useDispatch } 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 { isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
||||
import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
||||
import { createExploreLink, makeRuleBasedSilenceLink } from '../../utils/misc';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
|
||||
|
||||
interface Props {
|
||||
rule: CombinedRule;
|
||||
rulesSource: RulesSource;
|
||||
isViewMode: boolean;
|
||||
}
|
||||
|
||||
export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
|
||||
export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewMode }) => {
|
||||
const style = useStyles2(getStyles);
|
||||
const { group } = rule;
|
||||
const { namespace, group, rulerRule } = rule;
|
||||
const alertId = isGrafanaRulerRule(rule.rulerRule) ? rule.rulerRule.grafana_alert.id ?? '' : '';
|
||||
const { StateHistoryModal, showStateHistoryModal } = useStateHistoryModal(alertId);
|
||||
const dispatch = useDispatch();
|
||||
const location = useLocation();
|
||||
const notifyApp = useAppNotification();
|
||||
|
||||
const [ruleToDelete, setRuleToDelete] = useState<CombinedRule>();
|
||||
|
||||
const alertmanagerSourceName = isGrafanaRulesSource(rulesSource)
|
||||
? rulesSource
|
||||
@@ -32,9 +44,39 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
|
||||
const hasExplorePermission = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore);
|
||||
|
||||
const buttons: JSX.Element[] = [];
|
||||
const rightButtons: 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 = () => {
|
||||
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];
|
||||
};
|
||||
|
||||
const isFederated = isFederatedRuleGroup(group);
|
||||
const rulesSourceName = getRulesSourceName(rulesSource);
|
||||
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
|
||||
|
||||
const { isEditable, isRemovable } = useIsRuleEditable(rulesSourceName, rulerRule);
|
||||
|
||||
const returnTo = location.pathname + location.search;
|
||||
// explore does not support grafana rule queries atm
|
||||
// neither do "federated rules"
|
||||
if (isCloudRulesSource(rulesSource) && hasExplorePermission && !isFederated) {
|
||||
@@ -128,14 +170,77 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (buttons.length) {
|
||||
return (
|
||||
<div className={style.wrapper}>
|
||||
<HorizontalGroup width="auto">{buttons.length ? buttons : <div />}</HorizontalGroup>
|
||||
</div>
|
||||
);
|
||||
if (isViewMode) {
|
||||
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,
|
||||
}
|
||||
);
|
||||
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 (buttons.length || rightButtons.length) {
|
||||
return (
|
||||
<>
|
||||
<div className={style.wrapper}>
|
||||
<HorizontalGroup width="auto">{buttons.length ? buttons : <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)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -150,7 +255,6 @@ export const getStyles = (theme: GrafanaTheme2) => ({
|
||||
`,
|
||||
button: css`
|
||||
height: 24px;
|
||||
margin-top: ${theme.spacing(1)};
|
||||
font-size: ${theme.typography.size.sm};
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ 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 { getCloudRule, getGrafanaRule } from '../../mocks';
|
||||
|
||||
import { RulesTable } from './RulesTable';
|
||||
|
||||
@@ -41,30 +41,6 @@ function renderRulesTable(rule: CombinedRule) {
|
||||
);
|
||||
}
|
||||
|
||||
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' });
|
||||
|
||||
@@ -516,3 +516,26 @@ export function mockStore(recipe: (state: StoreState) => void) {
|
||||
|
||||
return configureStore(produce(defaultState, recipe));
|
||||
}
|
||||
|
||||
export function getGrafanaRule(override?: Partial<CombinedRule>) {
|
||||
return mockCombinedRule({
|
||||
namespace: {
|
||||
groups: [],
|
||||
name: 'Grafana',
|
||||
rulesSource: 'grafana',
|
||||
},
|
||||
...override,
|
||||
});
|
||||
}
|
||||
export function getCloudRule(override?: Partial<CombinedRule>) {
|
||||
return mockCombinedRule({
|
||||
namespace: {
|
||||
groups: [],
|
||||
name: 'Cortex',
|
||||
rulesSource: mockDataSource(),
|
||||
},
|
||||
promRule: mockPromAlertingRule(),
|
||||
rulerRule: mockRulerAlertingRule(),
|
||||
...override,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user