Alerting: Fix alert instances filtering for prom rules (#50850)

This commit is contained in:
Konrad Lalik 2022-06-20 15:37:05 +02:00 committed by GitHub
parent 18c3456d13
commit 87bf0f4315
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 170 additions and 21 deletions

View File

@ -175,7 +175,7 @@ export function RuleViewer({ match }: RuleViewerProps) {
</div>
</div>
<div>
<RuleDetailsMatchingInstances promRule={rule.promRule} />
<RuleDetailsMatchingInstances rule={rule} />
</div>
</RuleViewerLayoutContent>
{!isFederatedRule && data && Object.keys(data).length > 0 && (

View File

@ -1,22 +1,34 @@
import React from 'react';
import { capitalize } from 'lodash';
import React, { useMemo } from 'react';
import { RadioButtonGroup, Label } from '@grafana/ui';
import { GrafanaAlertState } from 'app/types/unified-alerting-dto';
import { Label, RadioButtonGroup } from '@grafana/ui';
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
export type InstanceStateFilter = GrafanaAlertState | PromAlertingRuleState.Pending | PromAlertingRuleState.Firing;
interface Props {
className?: string;
stateFilter?: GrafanaAlertState;
onStateFilterChange: (value: GrafanaAlertState | undefined) => void;
filterType: 'grafana' | 'prometheus';
stateFilter?: InstanceStateFilter;
onStateFilterChange: (value?: InstanceStateFilter) => void;
}
export const AlertInstanceStateFilter = ({ className, onStateFilterChange, stateFilter }: Props) => {
const stateOptions = Object.values(GrafanaAlertState).map((value) => ({
label: value,
value,
}));
const grafanaOptions = Object.values(GrafanaAlertState).map((value) => ({
label: value,
value,
}));
const promOptionValues = [PromAlertingRuleState.Firing, PromAlertingRuleState.Pending] as const;
const promOptions = promOptionValues.map((state) => ({
label: capitalize(state),
value: state,
}));
export const AlertInstanceStateFilter = ({ className, onStateFilterChange, stateFilter, filterType }: Props) => {
const stateOptions = useMemo(() => (filterType === 'grafana' ? grafanaOptions : promOptions), [filterType]);
return (
<div className={className}>
<div className={className} data-testid="alert-instance-state-filter">
<Label>State</Label>
<RadioButtonGroup
options={stateOptions}

View File

@ -21,7 +21,6 @@ interface Props {
export const RuleDetails: FC<Props> = ({ rule }) => {
const styles = useStyles2(getStyles);
const {
promRule,
namespace: { rulesSource },
} = rule;
@ -44,7 +43,7 @@ export const RuleDetails: FC<Props> = ({ rule }) => {
<RuleDetailsDataSources rulesSource={rulesSource} rule={rule} />
</div>
</div>
<RuleDetailsMatchingInstances promRule={promRule} />
<RuleDetailsMatchingInstances rule={rule} />
</div>
);
};
@ -53,6 +52,7 @@ export const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css`
display: flex;
flex-direction: row;
${theme.breakpoints.down('md')} {
flex-direction: column;
}

View File

@ -0,0 +1,128 @@
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { byLabelText, byRole, byTestId } from 'testing-library-selector';
import { CombinedRuleNamespace } from '../../../../../types/unified-alerting';
import { GrafanaAlertState, PromAlertingRuleState } from '../../../../../types/unified-alerting-dto';
import { mockCombinedRule, mockDataSource, mockPromAlert, mockPromAlertingRule } from '../../mocks';
import { alertStateToReadable } from '../../utils/rules';
import { RuleDetailsMatchingInstances } from './RuleDetailsMatchingInstances';
const ui = {
stateFilter: byTestId('alert-instance-state-filter'),
stateButton: byRole('radio'),
grafanaStateButton: {
normal: byLabelText('Normal'),
alerting: byLabelText('Alerting'),
pending: byLabelText('Pending'),
noData: byLabelText('NoData'),
error: byLabelText('Error'),
},
cloudStateButton: {
firing: byLabelText('Firing'),
pending: byLabelText('Pending'),
},
instanceRow: byTestId('row'),
};
describe('RuleDetailsMatchingInstances', () => {
describe('Filtering', () => {
it('For Grafana Managed rules instances filter should contain five states', () => {
const rule = mockCombinedRule();
render(<RuleDetailsMatchingInstances rule={rule} />);
const stateFilter = ui.stateFilter.get();
expect(stateFilter).toBeInTheDocument();
const stateButtons = ui.stateButton.getAll(stateFilter);
expect(stateButtons).toHaveLength(5);
expect(ui.grafanaStateButton.normal.get(stateFilter)).toBeInTheDocument();
expect(ui.grafanaStateButton.alerting.get(stateFilter)).toBeInTheDocument();
expect(ui.grafanaStateButton.pending.get(stateFilter)).toBeInTheDocument();
expect(ui.grafanaStateButton.noData.get(stateFilter)).toBeInTheDocument();
expect(ui.grafanaStateButton.error.get(stateFilter)).toBeInTheDocument();
});
it.each(Object.values(GrafanaAlertState))('Should filter grafana rules by %s state', async (state) => {
const rule = mockCombinedRule({
promRule: mockPromAlertingRule({
alerts: [
mockPromAlert({ state: GrafanaAlertState.Normal }),
mockPromAlert({ state: GrafanaAlertState.Alerting }),
mockPromAlert({ state: GrafanaAlertState.Pending }),
mockPromAlert({ state: GrafanaAlertState.NoData }),
mockPromAlert({ state: GrafanaAlertState.Error }),
],
}),
});
const buttons = {
[GrafanaAlertState.Normal]: ui.grafanaStateButton.normal,
[GrafanaAlertState.Alerting]: ui.grafanaStateButton.alerting,
[GrafanaAlertState.Pending]: ui.grafanaStateButton.pending,
[GrafanaAlertState.NoData]: ui.grafanaStateButton.noData,
[GrafanaAlertState.Error]: ui.grafanaStateButton.error,
};
render(<RuleDetailsMatchingInstances rule={rule} />);
await userEvent.click(buttons[state].get());
expect(ui.instanceRow.getAll()).toHaveLength(1);
expect(ui.instanceRow.get()).toHaveTextContent(alertStateToReadable(state));
});
it('For Cloud rules instances filter should contain two states', () => {
const rule = mockCombinedRule({
namespace: mockPromNamespace(),
});
render(<RuleDetailsMatchingInstances rule={rule} />);
const stateFilter = ui.stateFilter.get();
expect(stateFilter).toBeInTheDocument();
const stateButtons = ui.stateButton.getAll(stateFilter);
expect(stateButtons).toHaveLength(2);
expect(ui.cloudStateButton.firing.get(stateFilter)).toBeInTheDocument();
expect(ui.cloudStateButton.pending.get(stateFilter)).toBeInTheDocument();
});
it.each([PromAlertingRuleState.Pending, PromAlertingRuleState.Firing] as const)(
'Should filter cloud rules by %s state',
async (state) => {
const rule = mockCombinedRule({
namespace: mockPromNamespace(),
promRule: mockPromAlertingRule({
alerts: [
mockPromAlert({ state: PromAlertingRuleState.Firing }),
mockPromAlert({ state: PromAlertingRuleState.Pending }),
],
}),
});
render(<RuleDetailsMatchingInstances rule={rule} />);
await userEvent.click(ui.cloudStateButton[state].get());
expect(ui.instanceRow.getAll()).toHaveLength(1);
expect(ui.instanceRow.get()).toHaveTextContent(alertStateToReadable(state));
}
);
});
});
function mockPromNamespace(): CombinedRuleNamespace {
return {
rulesSource: mockDataSource(),
groups: [{ name: 'Prom rules group', rules: [] }],
name: 'Prometheus-test',
};
}

View File

@ -4,27 +4,33 @@ import React, { useMemo, useState } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { useStyles } from '@grafana/ui';
import { MatcherFilter } from 'app/features/alerting/unified/components/alert-groups/MatcherFilter';
import { AlertInstanceStateFilter } from 'app/features/alerting/unified/components/rules/AlertInstanceStateFilter';
import {
AlertInstanceStateFilter,
InstanceStateFilter,
} from 'app/features/alerting/unified/components/rules/AlertInstanceStateFilter';
import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
import { sortAlerts } from 'app/features/alerting/unified/utils/misc';
import { SortOrder } from 'app/plugins/panel/alertlist/types';
import { Alert, Rule } from 'app/types/unified-alerting';
import { GrafanaAlertState, mapStateWithReasonToBaseState } from 'app/types/unified-alerting-dto';
import { Alert, CombinedRule } from 'app/types/unified-alerting';
import { mapStateWithReasonToBaseState } from 'app/types/unified-alerting-dto';
import { GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from '../../utils/datasource';
import { isAlertingRule } from '../../utils/rules';
import { DetailsField } from '../DetailsField';
import { AlertInstancesTable } from './AlertInstancesTable';
type Props = {
promRule?: Rule;
rule: CombinedRule;
};
export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
const { promRule } = props;
const {
rule: { promRule, namespace },
} = props;
const [queryString, setQueryString] = useState<string>();
const [alertState, setAlertState] = useState<GrafanaAlertState>();
const [alertState, setAlertState] = useState<InstanceStateFilter>();
// This key is used to force a rerender on the inputs when the filters are cleared
const [filterKey] = useState<number>(Math.floor(Math.random() * 100));
@ -32,6 +38,8 @@ export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
const styles = useStyles(getStyles);
const stateFilterType = isGrafanaRulesSource(namespace.rulesSource) ? GRAFANA_RULES_SOURCE_NAME : 'prometheus';
const alerts = useMemo(
(): Alert[] =>
isAlertingRule(promRule) && promRule.alerts?.length
@ -56,6 +64,7 @@ export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
/>
<AlertInstanceStateFilter
className={styles.rowChild}
filterType={stateFilterType}
stateFilter={alertState}
onStateFilterChange={setAlertState}
/>
@ -69,7 +78,7 @@ export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
function filterAlerts(
alertInstanceLabel: string | undefined,
alertInstanceState: GrafanaAlertState | undefined,
alertInstanceState: InstanceStateFilter | undefined,
alerts: Alert[]
): Alert[] {
let filteredAlerts = [...alerts];