diff --git a/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/index.ts b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/index.ts
new file mode 100644
index 0000000000000..b2b4e144952e0
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { renderRuleStats } from './rule_stats';
+export type { RuleStatsState } from './types';
diff --git a/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.test.tsx b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.test.tsx
new file mode 100644
index 0000000000000..6f2edf5d0b1b6
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.test.tsx
@@ -0,0 +1,199 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { renderRuleStats } from './rule_stats';
+import { render, screen } from '@testing-library/react';
+
+const RULES_PAGE_LINK = '/app/observability/alerts/rules';
+const STAT_CLASS = 'euiStat';
+const STAT_TITLE_PRIMARY_CLASS = 'euiStat__title--primary';
+const STAT_BUTTON_CLASS = 'euiButtonEmpty';
+
+describe('Rule stats', () => {
+ test('renders all rule stats', async () => {
+ const stats = renderRuleStats(
+ {
+ total: 11,
+ disabled: 0,
+ muted: 0,
+ error: 0,
+ snoozed: 0,
+ },
+ RULES_PAGE_LINK,
+ false
+ );
+ expect(stats.length).toEqual(6);
+ });
+ test('disabled stat is not clickable, when there are no disabled rules', async () => {
+ const stats = renderRuleStats(
+ {
+ total: 11,
+ disabled: 0,
+ muted: 0,
+ error: 0,
+ snoozed: 0,
+ },
+ RULES_PAGE_LINK,
+ false
+ );
+ const { findByText, container } = render(stats[4]);
+ const disabledElement = await findByText('Disabled');
+ expect(disabledElement).toBeInTheDocument();
+ expect(container.getElementsByClassName(STAT_CLASS).length).toBe(1);
+ expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(0);
+ expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(0);
+ });
+
+ test('disabled stat is clickable, when there are disabled rules', async () => {
+ const stats = renderRuleStats(
+ {
+ total: 11,
+ disabled: 1,
+ muted: 0,
+ error: 0,
+ snoozed: 0,
+ },
+ RULES_PAGE_LINK,
+ false
+ );
+ const { container } = render(stats[4]);
+ expect(screen.getByText('Disabled').closest('a')).toHaveAttribute(
+ 'href',
+ `${RULES_PAGE_LINK}?_a=(lastResponse:!(),status:!(disabled))`
+ );
+
+ expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(1);
+ });
+
+ test('disabled stat count is link-colored, when there are disabled rules', async () => {
+ const stats = renderRuleStats(
+ {
+ total: 11,
+ disabled: 1,
+ muted: 0,
+ error: 0,
+ snoozed: 0,
+ },
+ RULES_PAGE_LINK,
+ false
+ );
+ const { container } = render(stats[4]);
+ expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(1);
+ });
+
+ test('snoozed stat is not clickable, when there are no snoozed rules', async () => {
+ const stats = renderRuleStats(
+ {
+ total: 11,
+ disabled: 0,
+ muted: 0,
+ error: 0,
+ snoozed: 0,
+ },
+ RULES_PAGE_LINK,
+ false
+ );
+ const { findByText, container } = render(stats[3]);
+ const snoozedElement = await findByText('Snoozed');
+ expect(snoozedElement).toBeInTheDocument();
+ expect(container.getElementsByClassName(STAT_CLASS).length).toBe(1);
+ expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(0);
+ expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(0);
+ });
+
+ test('snoozed stat is clickable, when there are snoozed rules', () => {
+ const stats = renderRuleStats(
+ {
+ total: 11,
+ disabled: 0,
+ muted: 1,
+ error: 0,
+ snoozed: 1,
+ },
+ RULES_PAGE_LINK,
+ false
+ );
+ const { container } = render(stats[3]);
+ expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(1);
+ expect(screen.getByText('Snoozed').closest('a')).toHaveAttribute(
+ 'href',
+ `${RULES_PAGE_LINK}?_a=(lastResponse:!(),status:!(snoozed))`
+ );
+ });
+
+ test('snoozed stat count is link-colored, when there are snoozed rules', async () => {
+ const stats = renderRuleStats(
+ {
+ total: 11,
+ disabled: 0,
+ muted: 1,
+ error: 0,
+ snoozed: 1,
+ },
+ RULES_PAGE_LINK,
+ false
+ );
+ const { container } = render(stats[3]);
+ expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(1);
+ });
+
+ test('errors stat is not clickable, when there are no error rules', async () => {
+ const stats = renderRuleStats(
+ {
+ total: 11,
+ disabled: 0,
+ muted: 0,
+ error: 0,
+ snoozed: 0,
+ },
+ RULES_PAGE_LINK,
+ false
+ );
+ const { findByText, container } = render(stats[2]);
+ const errorsElement = await findByText('Errors');
+ expect(errorsElement).toBeInTheDocument();
+ expect(container.getElementsByClassName(STAT_CLASS).length).toBe(1);
+ expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(0);
+ expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(0);
+ });
+
+ test('errors stat is clickable, when there are error rules', () => {
+ const stats = renderRuleStats(
+ {
+ total: 11,
+ disabled: 0,
+ muted: 0,
+ error: 2,
+ snoozed: 0,
+ },
+ RULES_PAGE_LINK,
+ false
+ );
+ const { container } = render(stats[2]);
+ expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(1);
+ expect(screen.getByText('Errors').closest('a')).toHaveAttribute(
+ 'href',
+ `${RULES_PAGE_LINK}?_a=(lastResponse:!(error),status:!())`
+ );
+ });
+
+ test('errors stat count is link-colored, when there are error rules', () => {
+ const stats = renderRuleStats(
+ {
+ total: 11,
+ disabled: 0,
+ muted: 0,
+ error: 2,
+ snoozed: 0,
+ },
+ RULES_PAGE_LINK,
+ false
+ );
+ const { container } = render(stats[2]);
+ expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(1);
+ });
+});
diff --git a/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.tsx b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.tsx
new file mode 100644
index 0000000000000..62c520c7b7442
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.tsx
@@ -0,0 +1,156 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { EuiButtonEmpty, EuiStat } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { euiStyled } from '@kbn/kibana-react-plugin/common';
+
+interface RuleStatsState {
+ total: number;
+ disabled: number;
+ muted: number;
+ error: number;
+ snoozed: number;
+}
+type StatType = 'disabled' | 'snoozed' | 'error';
+
+const Divider = euiStyled.div`
+ border-right: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
+ height: 100%;
+`;
+
+const StyledStat = euiStyled(EuiStat)`
+ .euiText {
+ line-height: 1;
+ }
+`;
+
+const ConditionalWrap = ({
+ condition,
+ wrap,
+ children,
+}: {
+ condition: boolean;
+ wrap: (wrappedChildren: React.ReactNode) => JSX.Element;
+ children: JSX.Element;
+}): JSX.Element => (condition ? wrap(children) : children);
+
+export const renderRuleStats = (
+ ruleStats: RuleStatsState,
+ manageRulesHref: string,
+ ruleStatsLoading: boolean
+) => {
+ const createRuleStatsLink = (stats: RuleStatsState, statType: StatType) => {
+ const count = stats[statType];
+ let statsLink = `${manageRulesHref}?_a=(lastResponse:!(),status:!())`;
+ if (count > 0) {
+ switch (statType) {
+ case 'error':
+ statsLink = `${manageRulesHref}?_a=(lastResponse:!(error),status:!())`;
+ break;
+ case 'snoozed':
+ case 'disabled':
+ statsLink = `${manageRulesHref}?_a=(lastResponse:!(),status:!(${statType}))`;
+ break;
+ default:
+ break;
+ }
+ }
+ return statsLink;
+ };
+
+ const disabledStatsComponent = (
+ 0}
+ wrap={(wrappedChildren) => (
+
+ {wrappedChildren}
+
+ )}
+ >
+ 0 ? 'primary' : ''}
+ titleSize="xs"
+ isLoading={ruleStatsLoading}
+ data-test-subj="statDisabled"
+ />
+
+ );
+
+ const snoozedStatsComponent = (
+ 0}
+ wrap={(wrappedChildren) => (
+
+ {wrappedChildren}
+
+ )}
+ >
+ 0 ? 'primary' : ''}
+ titleSize="xs"
+ isLoading={ruleStatsLoading}
+ data-test-subj="statMuted"
+ />
+
+ );
+
+ const errorStatsComponent = (
+ 0}
+ wrap={(wrappedChildren) => (
+
+ {wrappedChildren}
+
+ )}
+ >
+ 0 ? 'primary' : ''}
+ titleSize="xs"
+ isLoading={ruleStatsLoading}
+ data-test-subj="statErrors"
+ />
+
+ );
+ return [
+ ,
+ disabledStatsComponent,
+ snoozedStatsComponent,
+ errorStatsComponent,
+ ,
+
+ {i18n.translate('xpack.observability.alerts.manageRulesButtonLabel', {
+ defaultMessage: 'Manage Rules',
+ })}
+ ,
+ ].reverse();
+};
diff --git a/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/types.ts b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/types.ts
new file mode 100644
index 0000000000000..87ff668ebf87f
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/types.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface RuleStatsState {
+ total: number;
+ disabled: number;
+ muted: number;
+ error: number;
+ snoozed: number;
+}
+
+export type StatType = 'disabled' | 'snoozed' | 'error';
diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx
index e99a3195d0f30..2fe114771c329 100644
--- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx
+++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx
@@ -5,15 +5,13 @@
* 2.0.
*/
-import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { DataViewBase } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import useAsync from 'react-use/lib/useAsync';
import { ALERT_STATUS, AlertStatus } from '@kbn/rule-data-utils';
-
-import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { loadRuleAggregations } from '@kbn/triggers-actions-ui-plugin/public';
import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common/parse_technical_fields';
@@ -38,6 +36,7 @@ import {
} from '../state_container';
import './styles.scss';
import { AlertsStatusFilter, AlertsDisclaimer, AlertsSearchBar } from '../../components';
+import { renderRuleStats } from '../../components/rule_stats';
import { ObservabilityAppServices } from '../../../../application/types';
import { OBSERVABILITY_RULE_TYPES } from '../../../rules/config';
@@ -57,11 +56,6 @@ export interface TopAlert {
active: boolean;
}
-const Divider = euiStyled.div`
- border-right: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
- height: 100%;
-`;
-
const regExpEscape = (str: string) => str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
const NO_INDEX_PATTERNS: DataViewBase[] = [];
const BASE_ALERT_REGEX = new RegExp(`\\s*${regExpEscape(ALERT_STATUS)}\\s*:\\s*"(.*?|\\*?)"`);
@@ -251,54 +245,7 @@ function AlertsPage() {
>
),
- rightSideItems: [
- ,
- ,
- ,
- ,
- ,
-
- {i18n.translate('xpack.observability.alerts.manageRulesButtonLabel', {
- defaultMessage: 'Manage Rules',
- })}
- ,
- ].reverse(),
+ rightSideItems: renderRuleStats(ruleStats, manageRulesHref, ruleStatsLoading),
}}
>