mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<RuleDetailsMatchingInstances promRule={rule.promRule} />
|
<RuleDetailsMatchingInstances rule={rule} />
|
||||||
</div>
|
</div>
|
||||||
</RuleViewerLayoutContent>
|
</RuleViewerLayoutContent>
|
||||||
{!isFederatedRule && data && Object.keys(data).length > 0 && (
|
{!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 { Label, RadioButtonGroup } from '@grafana/ui';
|
||||||
import { GrafanaAlertState } from 'app/types/unified-alerting-dto';
|
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
export type InstanceStateFilter = GrafanaAlertState | PromAlertingRuleState.Pending | PromAlertingRuleState.Firing;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
stateFilter?: GrafanaAlertState;
|
filterType: 'grafana' | 'prometheus';
|
||||||
onStateFilterChange: (value: GrafanaAlertState | undefined) => void;
|
stateFilter?: InstanceStateFilter;
|
||||||
|
onStateFilterChange: (value?: InstanceStateFilter) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlertInstanceStateFilter = ({ className, onStateFilterChange, stateFilter }: Props) => {
|
const grafanaOptions = Object.values(GrafanaAlertState).map((value) => ({
|
||||||
const stateOptions = Object.values(GrafanaAlertState).map((value) => ({
|
|
||||||
label: value,
|
label: value,
|
||||||
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 (
|
return (
|
||||||
<div className={className}>
|
<div className={className} data-testid="alert-instance-state-filter">
|
||||||
<Label>State</Label>
|
<Label>State</Label>
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
options={stateOptions}
|
options={stateOptions}
|
||||||
|
@ -21,7 +21,6 @@ interface Props {
|
|||||||
export const RuleDetails: FC<Props> = ({ rule }) => {
|
export const RuleDetails: FC<Props> = ({ rule }) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const {
|
const {
|
||||||
promRule,
|
|
||||||
namespace: { rulesSource },
|
namespace: { rulesSource },
|
||||||
} = rule;
|
} = rule;
|
||||||
|
|
||||||
@ -44,7 +43,7 @@ export const RuleDetails: FC<Props> = ({ rule }) => {
|
|||||||
<RuleDetailsDataSources rulesSource={rulesSource} rule={rule} />
|
<RuleDetailsDataSources rulesSource={rulesSource} rule={rule} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<RuleDetailsMatchingInstances promRule={promRule} />
|
<RuleDetailsMatchingInstances rule={rule} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -53,6 +52,7 @@ export const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
wrapper: css`
|
wrapper: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
${theme.breakpoints.down('md')} {
|
${theme.breakpoints.down('md')} {
|
||||||
flex-direction: column;
|
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 { GrafanaTheme } from '@grafana/data';
|
||||||
import { useStyles } from '@grafana/ui';
|
import { useStyles } from '@grafana/ui';
|
||||||
import { MatcherFilter } from 'app/features/alerting/unified/components/alert-groups/MatcherFilter';
|
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 { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
|
||||||
import { sortAlerts } from 'app/features/alerting/unified/utils/misc';
|
import { sortAlerts } from 'app/features/alerting/unified/utils/misc';
|
||||||
import { SortOrder } from 'app/plugins/panel/alertlist/types';
|
import { SortOrder } from 'app/plugins/panel/alertlist/types';
|
||||||
import { Alert, Rule } from 'app/types/unified-alerting';
|
import { Alert, CombinedRule } from 'app/types/unified-alerting';
|
||||||
import { GrafanaAlertState, mapStateWithReasonToBaseState } from 'app/types/unified-alerting-dto';
|
import { mapStateWithReasonToBaseState } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
import { GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from '../../utils/datasource';
|
||||||
import { isAlertingRule } from '../../utils/rules';
|
import { isAlertingRule } from '../../utils/rules';
|
||||||
import { DetailsField } from '../DetailsField';
|
import { DetailsField } from '../DetailsField';
|
||||||
|
|
||||||
import { AlertInstancesTable } from './AlertInstancesTable';
|
import { AlertInstancesTable } from './AlertInstancesTable';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
promRule?: Rule;
|
rule: CombinedRule;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
|
export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
|
||||||
const { promRule } = props;
|
const {
|
||||||
|
rule: { promRule, namespace },
|
||||||
|
} = props;
|
||||||
|
|
||||||
const [queryString, setQueryString] = useState<string>();
|
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
|
// 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));
|
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 styles = useStyles(getStyles);
|
||||||
|
|
||||||
|
const stateFilterType = isGrafanaRulesSource(namespace.rulesSource) ? GRAFANA_RULES_SOURCE_NAME : 'prometheus';
|
||||||
|
|
||||||
const alerts = useMemo(
|
const alerts = useMemo(
|
||||||
(): Alert[] =>
|
(): Alert[] =>
|
||||||
isAlertingRule(promRule) && promRule.alerts?.length
|
isAlertingRule(promRule) && promRule.alerts?.length
|
||||||
@ -56,6 +64,7 @@ export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
|
|||||||
/>
|
/>
|
||||||
<AlertInstanceStateFilter
|
<AlertInstanceStateFilter
|
||||||
className={styles.rowChild}
|
className={styles.rowChild}
|
||||||
|
filterType={stateFilterType}
|
||||||
stateFilter={alertState}
|
stateFilter={alertState}
|
||||||
onStateFilterChange={setAlertState}
|
onStateFilterChange={setAlertState}
|
||||||
/>
|
/>
|
||||||
@ -69,7 +78,7 @@ export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
|
|||||||
|
|
||||||
function filterAlerts(
|
function filterAlerts(
|
||||||
alertInstanceLabel: string | undefined,
|
alertInstanceLabel: string | undefined,
|
||||||
alertInstanceState: GrafanaAlertState | undefined,
|
alertInstanceState: InstanceStateFilter | undefined,
|
||||||
alerts: Alert[]
|
alerts: Alert[]
|
||||||
): Alert[] {
|
): Alert[] {
|
||||||
let filteredAlerts = [...alerts];
|
let filteredAlerts = [...alerts];
|
||||||
|
Loading…
Reference in New Issue
Block a user