Alerting: Lotex alert groups removal (#45150)

* Add lotex group removal UI

* Connect UI to delete group action

* Add rules' refreshing after deletion of a group

* Improve confirmation message

* Add tests for RulesGroup

* Remove redundant check
This commit is contained in:
Konrad Lalik 2022-02-24 11:31:36 +01:00 committed by GitHub
parent bb5a39faef
commit d6c580e338
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 185 additions and 11 deletions

View File

@ -0,0 +1,111 @@
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { configureStore } from 'app/store/configureStore';
import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
import React from 'react';
import { Provider } from 'react-redux';
import { byTestId, byText } from 'testing-library-selector';
import { mockCombinedRule, mockDataSource } from '../../mocks';
import { RulesGroup } from './RulesGroup';
const hasRulerMock = jest.fn<boolean, any>();
jest.mock('../../hooks/useHasRuler', () => ({
useHasRuler: () => hasRulerMock,
}));
beforeEach(() => hasRulerMock.mockReset());
const ui = {
editGroupButton: byTestId('edit-group'),
deleteGroupButton: byTestId('delete-group'),
confirmDeleteModal: {
header: byText('Delete group'),
confirmButton: byText('Delete'),
},
};
describe('Rules group tests', () => {
const store = configureStore();
function renderRulesGroup(namespace: CombinedRuleNamespace, group: CombinedRuleGroup) {
return render(
<Provider store={store}>
<RulesGroup group={group} namespace={namespace} expandAll={false} />
</Provider>
);
}
describe('When the datasource is grafana', () => {
const group: CombinedRuleGroup = {
name: 'TestGroup',
rules: [mockCombinedRule()],
};
const namespace: CombinedRuleNamespace = {
name: 'TestNamespace',
rulesSource: 'grafana',
groups: [group],
};
it('Should hide delete and edit group buttons', () => {
// Act
renderRulesGroup(namespace, group);
// Assert
expect(ui.deleteGroupButton.query()).not.toBeInTheDocument();
expect(ui.editGroupButton.query()).not.toBeInTheDocument();
});
});
describe('When the datasource is not grafana', () => {
const group: CombinedRuleGroup = {
name: 'TestGroup',
rules: [mockCombinedRule()],
};
const namespace: CombinedRuleNamespace = {
name: 'TestNamespace',
rulesSource: mockDataSource(),
groups: [group],
};
it('When ruler enabled should display delete and edit group buttons', () => {
// Arrange
hasRulerMock.mockReturnValue(true);
// Act
renderRulesGroup(namespace, group);
// Assert
expect(hasRulerMock).toHaveBeenCalled();
expect(ui.deleteGroupButton.get()).toBeInTheDocument();
expect(ui.editGroupButton.get()).toBeInTheDocument();
});
it('When ruler disabled should hide delete and edit group buttons', () => {
// Arrange
hasRulerMock.mockReturnValue(false);
// Act
renderRulesGroup(namespace, group);
// Assert
expect(hasRulerMock).toHaveBeenCalled();
expect(ui.deleteGroupButton.query()).not.toBeInTheDocument();
expect(ui.editGroupButton.query()).not.toBeInTheDocument();
});
it('Delete button click should display confirmation modal', () => {
// Arrange
hasRulerMock.mockReturnValue(true);
// Act
renderRulesGroup(namespace, group);
userEvent.click(ui.deleteGroupButton.get());
// Assert
expect(ui.confirmDeleteModal.header.get()).toBeInTheDocument();
expect(ui.confirmDeleteModal.confirmButton.get()).toBeInTheDocument();
});
});
});

View File

@ -1,18 +1,21 @@
import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
import React, { FC, useState, useEffect } from 'react';
import { HorizontalGroup, Icon, Spinner, Tooltip, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { ConfirmModal, HorizontalGroup, Icon, Spinner, Tooltip, useStyles2 } from '@grafana/ui';
import kbn from 'app/core/utils/kbn';
import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
import pluralize from 'pluralize';
import React, { FC, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useFolder } from '../../hooks/useFolder';
import { useHasRuler } from '../../hooks/useHasRuler';
import { deleteRulesGroupAction } from '../../state/actions';
import { GRAFANA_RULES_SOURCE_NAME, isCloudRulesSource } from '../../utils/datasource';
import { isGrafanaRulerRule } from '../../utils/rules';
import { CollapseToggle } from '../CollapseToggle';
import { RulesTable } from './RulesTable';
import { GRAFANA_RULES_SOURCE_NAME, isCloudRulesSource } from '../../utils/datasource';
import { ActionIcon } from './ActionIcon';
import { useHasRuler } from '../../hooks/useHasRuler';
import kbn from 'app/core/utils/kbn';
import { useFolder } from '../../hooks/useFolder';
import { RuleStats } from './RuleStats';
import { EditCloudGroupModal } from './EditCloudGroupModal';
import { RulesTable } from './RulesTable';
import { RuleStats } from './RuleStats';
interface Props {
namespace: CombinedRuleNamespace;
@ -22,9 +25,11 @@ interface Props {
export const RulesGroup: FC<Props> = React.memo(({ group, namespace, expandAll }) => {
const { rulesSource } = namespace;
const dispatch = useDispatch();
const styles = useStyles2(getStyles);
const [isEditingGroup, setIsEditingGroup] = useState(false);
const [isDeletingGroup, setIsDeletingGroup] = useState(false);
const [isCollapsed, setIsCollapsed] = useState(!expandAll);
useEffect(() => {
@ -39,6 +44,11 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace, expandAll }
// group "is deleting" if rules source has ruler, but this group has no rules that are in ruler
const isDeleting = hasRuler(rulesSource) && !group.rules.find((rule) => !!rule.rulerRule);
const deleteGroup = () => {
dispatch(deleteRulesGroupAction(namespace, group));
setIsDeletingGroup(false);
};
const actionIcons: React.ReactNode[] = [];
// for grafana, link to folder views
@ -88,6 +98,17 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace, expandAll }
onClick={() => setIsEditingGroup(true)}
/>
);
actionIcons.push(
<ActionIcon
aria-label="delete rule group"
data-testid="delete-group"
key="delete-group"
icon="trash-alt"
tooltip="delete rule group"
onClick={() => setIsDeletingGroup(true)}
/>
);
}
const groupName = isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name;
@ -129,6 +150,22 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace, expandAll }
{isEditingGroup && (
<EditCloudGroupModal group={group} namespace={namespace} onClose={() => setIsEditingGroup(false)} />
)}
<ConfirmModal
isOpen={isDeletingGroup}
title="Delete group"
body={
<div>
Deleting this group will permanently remove the group
<br />
and {group.rules.length} alert {pluralize('rule', group.rules.length)} belonging to it.
<br />
Are you sure you want to delete this group?
</div>
}
onConfirm={deleteGroup}
onDismiss={() => setIsDeletingGroup(false)}
confirmText="Delete"
/>
</div>
);
});

View File

@ -12,7 +12,14 @@ import {
TestReceiversAlert,
} from 'app/plugins/datasource/alertmanager/types';
import { FolderDTO, NotifierDTO, ThunkResult } from 'app/types';
import { RuleIdentifier, RuleNamespace, RuleWithLocation, StateHistoryItem } from 'app/types/unified-alerting';
import {
CombinedRuleGroup,
CombinedRuleNamespace,
RuleIdentifier,
RuleNamespace,
RuleWithLocation,
StateHistoryItem,
} from 'app/types/unified-alerting';
import {
PostableRulerRuleGroupDTO,
RulerGrafanaRuleDTO,
@ -49,6 +56,7 @@ import {
import { RuleFormType, RuleFormValues } from '../types/rule-form';
import {
getAllRulesSourceNames,
getRulesSourceName,
GRAFANA_RULES_SOURCE_NAME,
isGrafanaRulesSource,
isVanillaPrometheusAlertManagerDataSource,
@ -263,6 +271,24 @@ async function deleteRule(ruleWithLocation: RuleWithLocation): Promise<void> {
});
}
export function deleteRulesGroupAction(
namespace: CombinedRuleNamespace,
ruleGroup: CombinedRuleGroup
): ThunkResult<void> {
return async (dispatch) => {
withAppEvents(
(async () => {
const sourceName = getRulesSourceName(namespace.rulesSource);
await deleteRulerRulesGroup(sourceName, namespace.name, ruleGroup.name);
dispatch(fetchRulerRulesAction({ rulesSourceName: sourceName }));
dispatch(fetchPromRulesAction({ rulesSourceName: sourceName }));
})(),
{ successMessage: 'Group deleted' }
);
};
}
export function deleteRuleAction(
ruleIdentifier: RuleIdentifier,
options: { navigateTo?: string } = {}