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];