mirror of
https://github.com/grafana/grafana.git
synced 2025-02-09 23:16:16 -06:00
Alerting: Fix alert instances filtering for prom rules (#50850)
This commit is contained in:
parent
18c3456d13
commit
87bf0f4315
@ -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 && (
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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',
|
||||
};
|
||||
}
|
@ -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];
|
||||
|
Loading…
Reference in New Issue
Block a user