diff --git a/public/app/features/alerting/unified/RuleViewer.tsx b/public/app/features/alerting/unified/RuleViewer.tsx
index 647795709f7..6d46a2932cc 100644
--- a/public/app/features/alerting/unified/RuleViewer.tsx
+++ b/public/app/features/alerting/unified/RuleViewer.tsx
@@ -175,7 +175,7 @@ export function RuleViewer({ match }: RuleViewerProps) {
-
+
{!isFederatedRule && data && Object.keys(data).length > 0 && (
diff --git a/public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx b/public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx
index 4cbc939d434..ada64a58b58 100644
--- a/public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx
+++ b/public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx
@@ -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 (
-
+
= ({ rule }) => {
const styles = useStyles2(getStyles);
const {
- promRule,
namespace: { rulesSource },
} = rule;
@@ -44,7 +43,7 @@ export const RuleDetails: FC = ({ rule }) => {
-
+
);
};
@@ -53,6 +52,7 @@ export const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css`
display: flex;
flex-direction: row;
+
${theme.breakpoints.down('md')} {
flex-direction: column;
}
diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx
new file mode 100644
index 00000000000..5d608dd51dd
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx
@@ -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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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',
+ };
+}
diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx
index aa095a1d0e9..7cae0e0b74b 100644
--- a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx
@@ -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();
- const [alertState, setAlertState] = useState();
+ const [alertState, setAlertState] = useState();
// This key is used to force a rerender on the inputs when the filters are cleared
const [filterKey] = useState(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 {
/>
@@ -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];