mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Fix rules deleting when reordering whilst filtered (#88221)
This commit is contained in:
parent
60f9817303
commit
ebdad80dfa
@ -1,9 +1,9 @@
|
|||||||
import { SerializedError } from '@reduxjs/toolkit';
|
import { SerializedError } from '@reduxjs/toolkit';
|
||||||
import { prettyDOM, render, screen, waitFor } from '@testing-library/react';
|
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { setupServer } from 'msw/node';
|
import { setupServer } from 'msw/node';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TestProvider } from 'test/helpers/TestProvider';
|
import { TestProvider } from 'test/helpers/TestProvider';
|
||||||
|
import { prettyDOM, render, screen, waitFor, within } from 'test/test-utils';
|
||||||
import { byRole, byTestId, byText } from 'testing-library-selector';
|
import { byRole, byTestId, byText } from 'testing-library-selector';
|
||||||
|
|
||||||
import { PluginExtensionTypes, PluginMeta } from '@grafana/data';
|
import { PluginExtensionTypes, PluginMeta } from '@grafana/data';
|
||||||
@ -632,6 +632,63 @@ describe('RuleList', () => {
|
|||||||
await waitFor(() => expect(ui.ruleGroup.get()).toHaveTextContent('group-2'));
|
await waitFor(() => expect(ui.ruleGroup.get()).toHaveTextContent('group-2'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses entire group when reordering after filtering', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]);
|
||||||
|
|
||||||
|
setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom }));
|
||||||
|
|
||||||
|
mocks.api.discoverFeatures.mockResolvedValue({
|
||||||
|
application: PromApplication.Cortex,
|
||||||
|
features: {
|
||||||
|
rulerApiEnabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mocks.api.fetchRulerRules.mockImplementation(() => Promise.resolve(someRulerRules));
|
||||||
|
mocks.api.fetchRules.mockImplementation((dataSourceName: string) => {
|
||||||
|
if (dataSourceName === GRAFANA_RULES_SOURCE_NAME) {
|
||||||
|
return Promise.resolve([
|
||||||
|
mockPromRuleNamespace({
|
||||||
|
name: 'foofolder',
|
||||||
|
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||||
|
groups: [
|
||||||
|
mockPromRuleGroup({
|
||||||
|
name: 'grafana-group',
|
||||||
|
rules: [
|
||||||
|
mockPromAlertingRule({
|
||||||
|
query: '[]',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
renderRuleList();
|
||||||
|
|
||||||
|
const [firstReorderButton] = await screen.findAllByLabelText(/reorder/i);
|
||||||
|
|
||||||
|
const filterInput = ui.rulesFilterInput.get();
|
||||||
|
await userEvent.type(filterInput, 'alert1a{Enter}');
|
||||||
|
|
||||||
|
await user.click(firstReorderButton);
|
||||||
|
|
||||||
|
const reorderDialog = await screen.findByRole('dialog');
|
||||||
|
|
||||||
|
const alertsInReorder = within(reorderDialog).getAllByTestId('reorder-alert-rule');
|
||||||
|
|
||||||
|
// We've filtered down to one rule, but the reorder dialog should still
|
||||||
|
// have everything in the group visible for reordering
|
||||||
|
// If this were not the case, rules could be deleted ⚠️
|
||||||
|
expect(alertsInReorder).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
describe('pausing rules', () => {
|
describe('pausing rules', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
grantUserPermissions([
|
grantUserPermissions([
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Badge, Icon, Modal, Tooltip, useStyles2 } from '@grafana/ui';
|
import { Badge, Icon, Modal, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
|
import { useCombinedRuleNamespaces } from 'app/features/alerting/unified/hooks/useCombinedRuleNamespaces';
|
||||||
import { dispatch } from 'app/store/store';
|
import { dispatch } from 'app/store/store';
|
||||||
import { CombinedRule, CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
|
import { CombinedRule, CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
@ -34,8 +35,18 @@ type CombinedRuleWithUID = { uid: string } & CombinedRule;
|
|||||||
|
|
||||||
export const ReorderCloudGroupModal = (props: ModalProps) => {
|
export const ReorderCloudGroupModal = (props: ModalProps) => {
|
||||||
const { group, namespace, onClose, folderUid } = props;
|
const { group, namespace, onClose, folderUid } = props;
|
||||||
|
|
||||||
|
// The list of rules might have been filtered before we get to this reordering modal
|
||||||
|
// We need to grab the full (unfiltered) list so we are able to reorder via the API without
|
||||||
|
// deleting any rules (as they otherwise would have been omitted from the payload)
|
||||||
|
const unfilteredNamespaces = useCombinedRuleNamespaces();
|
||||||
|
const matchedNamespace = unfilteredNamespaces.find(
|
||||||
|
(ns) => ns.rulesSource === namespace.rulesSource && ns.name === namespace.name
|
||||||
|
);
|
||||||
|
const matchedGroup = matchedNamespace?.groups.find((g) => g.name === group.name);
|
||||||
|
|
||||||
const [pending, setPending] = useState<boolean>(false);
|
const [pending, setPending] = useState<boolean>(false);
|
||||||
const [rulesList, setRulesList] = useState<CombinedRule[]>(group.rules);
|
const [rulesList, setRulesList] = useState<CombinedRule[]>(matchedGroup?.rules || []);
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
@ -129,6 +140,7 @@ const ListItem = ({ provided, rule, isClone = false, isDragging = false }: ListI
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
data-testid="reorder-alert-rule"
|
||||||
className={cx(styles.listItem, isClone && 'isClone', isDragging && 'isDragging')}
|
className={cx(styles.listItem, isClone && 'isClone', isDragging && 'isDragging')}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
|
@ -105,7 +105,6 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }:
|
|||||||
);
|
);
|
||||||
actionIcons.push(
|
actionIcons.push(
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
aria-label="re-order rules"
|
|
||||||
data-testid="reorder-group"
|
data-testid="reorder-group"
|
||||||
key="reorder"
|
key="reorder"
|
||||||
icon="exchange-alt"
|
icon="exchange-alt"
|
||||||
@ -181,11 +180,10 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }:
|
|||||||
);
|
);
|
||||||
actionIcons.push(
|
actionIcons.push(
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
aria-label="re-order rules"
|
|
||||||
data-testid="reorder-group"
|
data-testid="reorder-group"
|
||||||
key="reorder"
|
key="reorder"
|
||||||
icon="exchange-alt"
|
icon="exchange-alt"
|
||||||
tooltip="re-order rules"
|
tooltip="reorder rules"
|
||||||
className={styles.rotate90}
|
className={styles.rotate90}
|
||||||
onClick={() => setIsReorderingGroup(true)}
|
onClick={() => setIsReorderingGroup(true)}
|
||||||
/>
|
/>
|
||||||
|
@ -561,7 +561,10 @@ export const somePromRules = (dataSourceName = 'Prometheus'): RuleNamespace[] =>
|
|||||||
|
|
||||||
export const someRulerRules: RulerRulesConfigDTO = {
|
export const someRulerRules: RulerRulesConfigDTO = {
|
||||||
namespace1: [
|
namespace1: [
|
||||||
mockRulerRuleGroup({ name: 'group1', rules: [mockRulerAlertingRule({ alert: 'alert1' })] }),
|
mockRulerRuleGroup({
|
||||||
|
name: 'group1',
|
||||||
|
rules: [mockRulerAlertingRule({ alert: 'alert1' }), mockRulerAlertingRule({ alert: 'alert1a' })],
|
||||||
|
}),
|
||||||
mockRulerRuleGroup({ name: 'group2', rules: [mockRulerAlertingRule({ alert: 'alert2' })] }),
|
mockRulerRuleGroup({ name: 'group2', rules: [mockRulerAlertingRule({ alert: 'alert2' })] }),
|
||||||
],
|
],
|
||||||
namespace2: [mockRulerRuleGroup({ name: 'group3', rules: [mockRulerAlertingRule({ alert: 'alert3' })] })],
|
namespace2: [mockRulerRuleGroup({ name: 'group3', rules: [mockRulerAlertingRule({ alert: 'alert3' })] })],
|
||||||
|
@ -30,6 +30,7 @@ import {
|
|||||||
|
|
||||||
import { backendSrv } from '../../../../core/services/backend_srv';
|
import { backendSrv } from '../../../../core/services/backend_srv';
|
||||||
import {
|
import {
|
||||||
|
logError,
|
||||||
logInfo,
|
logInfo,
|
||||||
LogMessages,
|
LogMessages,
|
||||||
trackSwitchToPoliciesRouting,
|
trackSwitchToPoliciesRouting,
|
||||||
@ -852,6 +853,16 @@ export const updateRulesOrder = createAsyncThunk(
|
|||||||
throw new Error(`Group "${groupName}" not found.`);
|
throw new Error(`Group "${groupName}" not found.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We're unlikely to have this happen, as any user of this action should have already ensured
|
||||||
|
// that the entire group was fetched before sending a new order.
|
||||||
|
// But as a final safeguard we should fail if we somehow ended up here with a mismatched rules count
|
||||||
|
// This would indicate an accidental deletion of rules following a frontend bug
|
||||||
|
if (existingGroup.rules.length !== newRules.length) {
|
||||||
|
const err = new Error('Rules count mismatch. Please refresh the page and try again.');
|
||||||
|
logError(err, { namespaceName, groupName });
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
const payload: PostableRulerRuleGroupDTO = {
|
const payload: PostableRulerRuleGroupDTO = {
|
||||||
name: existingGroup.name,
|
name: existingGroup.name,
|
||||||
interval: existingGroup.interval,
|
interval: existingGroup.interval,
|
||||||
|
Loading…
Reference in New Issue
Block a user