diff --git a/packages/kbn-alerting-types/alert_type.ts b/packages/kbn-alerting-types/alert_type.ts
new file mode 100644
index 0000000000000..61746aaaa14f6
--- /dev/null
+++ b/packages/kbn-alerting-types/alert_type.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { TechnicalRuleDataFieldName } from '@kbn/rule-data-utils';
+
+export interface BasicFields {
+ _id: string;
+ _index: string;
+}
+
+export type Alert = BasicFields & {
+ [Property in TechnicalRuleDataFieldName]?: string[];
+} & {
+ [x: string]: unknown[];
+};
diff --git a/packages/kbn-alerting-types/index.ts b/packages/kbn-alerting-types/index.ts
index f260374c13183..6d63366f4343d 100644
--- a/packages/kbn-alerting-types/index.ts
+++ b/packages/kbn-alerting-types/index.ts
@@ -9,3 +9,4 @@
export * from './builtin_action_groups_types';
export * from './rule_type';
export * from './action_group_types';
+export * from './alert_type';
diff --git a/packages/kbn-alerting-types/tsconfig.json b/packages/kbn-alerting-types/tsconfig.json
index 049d2ef8d89b1..911e35551bbbd 100644
--- a/packages/kbn-alerting-types/tsconfig.json
+++ b/packages/kbn-alerting-types/tsconfig.json
@@ -18,5 +18,6 @@
"kbn_references": [
"@kbn/i18n",
"@kbn/licensing-plugin",
+ "@kbn/rule-data-utils"
]
}
diff --git a/packages/kbn-alerts-ui-shared/index.ts b/packages/kbn-alerts-ui-shared/index.ts
index 3dd1174da4129..25ef9bf8a66ca 100644
--- a/packages/kbn-alerts-ui-shared/index.ts
+++ b/packages/kbn-alerts-ui-shared/index.ts
@@ -15,3 +15,5 @@ export * from './src/alerts_search_bar/hooks';
export * from './src/alerts_search_bar/apis';
export { AlertsSearchBar } from './src/alerts_search_bar';
export type { AlertsSearchBarProps } from './src/alerts_search_bar/types';
+
+export * from './src/alert_fields_table';
diff --git a/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.test.tsx b/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.test.tsx
new file mode 100644
index 0000000000000..e930faa7acc0a
--- /dev/null
+++ b/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.test.tsx
@@ -0,0 +1,100 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { AlertFieldsTable, AlertFieldsTableProps } from '.';
+import { mount, ReactWrapper } from 'enzyme';
+
+describe('AlertFieldsTable', () => {
+ const defaultProps = {
+ alert: {
+ 'kibana.alert.status': ['active'],
+ 'kibana.alert.url': ['ALERT_URL'],
+ 'kibana.alert.evaluation.conditions': [
+ 'Number of matching documents is NOT greater than 1000',
+ ],
+ 'kibana.alert.rule.producer': ['stackAlerts'],
+ 'kibana.alert.reason.text': [
+ 'Document count is 0 in the last 5m in metrics-* data view. Alert when greater than 1000.',
+ ],
+ 'kibana.alert.rule.rule_type_id': ['.es-query'],
+ 'kibana.alert.evaluation.value': ['0'],
+ 'kibana.alert.instance.id': ['query matched'],
+ 'kibana.alert.flapping': [true],
+ 'kibana.alert.rule.name': ['Test rule'],
+ 'event.kind': ['signal'],
+ 'kibana.alert.title': ["rule 'Test rule' recovered"],
+ 'kibana.alert.workflow_status': ['open'],
+ 'kibana.alert.rule.uuid': ['313b0cfb-3455-41da-aff9-cee2bc9637f1'],
+ 'kibana.alert.time_range': [
+ {
+ gte: '1703241047013',
+ },
+ ],
+ 'kibana.alert.flapping_history': [false],
+ 'kibana.alert.reason': [
+ 'Document count is 0 in the last 5m in metrics-* data view. Alert when greater than 1000.',
+ ],
+ 'kibana.alert.rule.consumer': ['infrastructure'],
+ 'kibana.alert.action_group': ['query matched'],
+ 'kibana.alert.rule.category': ['Elasticsearch query'],
+ 'kibana.alert.start': ['2023-12-22T10:30:47.013Z'],
+ 'event.action': ['active'],
+ '@timestamp': ['2023-12-22T10:32:53.036Z'],
+ 'kibana.alert.duration.us': [126023000],
+ 'kibana.alert.rule.execution.uuid': ['ffcc5773-a4cc-48be-a265-9b16f85f4e62'],
+ 'kibana.alert.uuid': ['93377bf3-d837-425d-b63f-97a8a5ae8054'],
+ 'kibana.space_ids': ['default'],
+ 'kibana.version': ['8.13.0'],
+ 'kibana.alert.evaluation.threshold': [1000],
+ 'kibana.alert.rule.parameters': [
+ {
+ searchConfiguration: {
+ query: {
+ language: 'kuery',
+ query: '',
+ },
+ index: '8e29356e-9d83-4a89-a79d-7d096104f3f6',
+ },
+ timeField: '@timestamp',
+ searchType: 'searchSource',
+ timeWindowSize: 5,
+ timeWindowUnit: 'm',
+ threshold: [1000],
+ thresholdComparator: '>',
+ size: 100,
+ aggType: 'count',
+ groupBy: 'all',
+ termSize: 5,
+ excludeHitsFromPreviousRun: true,
+ },
+ ],
+ 'kibana.alert.rule.revision': [0],
+ _id: '93377bf3-d837-425d-b63f-97a8a5ae8054',
+ _index: '.internal.alerts-stack.alerts-default-000001',
+ },
+ } as unknown as AlertFieldsTableProps;
+ let wrapper: ReactWrapper;
+
+ beforeEach(async () => {
+ wrapper = mount();
+ });
+
+ it('should paginate the results', () => {
+ expect(wrapper.find('tbody tr')).toHaveLength(25);
+ wrapper.find(`[data-test-subj="pagination-button-next"]`).last().simulate('click');
+ expect(wrapper.find('tbody tr')).toHaveLength(8);
+ });
+
+ it('should filter the rows according to the search string', async () => {
+ wrapper
+ .find('input[type="search"]')
+ .simulate('keyup', { target: { value: 'kibana.alert.status' } });
+ expect(wrapper.find('tbody tr')).toHaveLength(1);
+ });
+});
diff --git a/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.tsx b/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.tsx
new file mode 100644
index 0000000000000..cf120a00e5aa2
--- /dev/null
+++ b/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.tsx
@@ -0,0 +1,114 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { i18n } from '@kbn/i18n';
+import {
+ EuiInMemoryTable,
+ EuiTabbedContent,
+ EuiTabbedContentProps,
+ useEuiOverflowScroll,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import React, { memo, useCallback, useMemo, useState } from 'react';
+import { Alert } from '@kbn/alerting-types';
+import { euiThemeVars } from '@kbn/ui-theme';
+
+export const search = {
+ box: {
+ incremental: true,
+ placeholder: i18n.translate('alertsUIShared.alertFieldsTable.filter.placeholder', {
+ defaultMessage: 'Filter by Field, Value, or Description...',
+ }),
+ schema: true,
+ },
+};
+
+const columns = [
+ {
+ field: 'key',
+ name: i18n.translate('alertsUIShared.alertFieldsTable.field', {
+ defaultMessage: 'Field',
+ }),
+ width: '30%',
+ },
+ {
+ field: 'value',
+ name: i18n.translate('alertsUIShared.alertFieldsTable.value', {
+ defaultMessage: 'Value',
+ }),
+ width: '70%',
+ },
+];
+
+export const ScrollableFlyoutTabbedContent = (props: EuiTabbedContentProps) => (
+
+);
+
+const COUNT_PER_PAGE_OPTIONS = [25, 50, 100];
+
+const useFieldBrowserPagination = () => {
+ const [pagination, setPagination] = useState<{ pageIndex: number }>({
+ pageIndex: 0,
+ });
+
+ const onTableChange = useCallback(({ page: { index } }: { page: { index: number } }) => {
+ setPagination({ pageIndex: index });
+ }, []);
+ const paginationTableProp = useMemo(
+ () => ({
+ ...pagination,
+ pageSizeOptions: COUNT_PER_PAGE_OPTIONS,
+ }),
+ [pagination]
+ );
+
+ return {
+ onTableChange,
+ paginationTableProp,
+ };
+};
+
+export interface AlertFieldsTableProps {
+ alert: Alert;
+}
+
+export const AlertFieldsTable = memo(({ alert }: AlertFieldsTableProps) => {
+ const { onTableChange, paginationTableProp } = useFieldBrowserPagination();
+ return (
+ ({
+ key,
+ value: Array.isArray(value) ? value?.[0] : value,
+ }))}
+ itemId="key"
+ columns={columns}
+ onTableChange={onTableChange}
+ pagination={paginationTableProp}
+ search={search}
+ css={css`
+ & .euiTableRow {
+ font-size: ${euiThemeVars.euiFontSizeXS};
+ font-family: ${euiThemeVars.euiCodeFontFamily};
+ }
+ `}
+ />
+ );
+});
diff --git a/packages/kbn-alerts-ui-shared/tsconfig.json b/packages/kbn-alerts-ui-shared/tsconfig.json
index a7af1fcec80cc..f2347b579acef 100644
--- a/packages/kbn-alerts-ui-shared/tsconfig.json
+++ b/packages/kbn-alerts-ui-shared/tsconfig.json
@@ -28,5 +28,6 @@
"@kbn/data-views-plugin",
"@kbn/unified-search-plugin",
"@kbn/es-query",
+ "@kbn/ui-theme",
]
}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/default_alerts_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/default_alerts_flyout.test.tsx
new file mode 100644
index 0000000000000..b0fae16cf365d
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/default_alerts_flyout.test.tsx
@@ -0,0 +1,135 @@
+/*
+ * 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 { waitFor } from '@testing-library/react';
+import { mount } from 'enzyme';
+import type { ReactWrapper } from 'enzyme';
+import React from 'react';
+
+import { getDefaultAlertFlyout } from './default_alerts_flyout';
+import { AlertsTableFlyoutBaseProps } from '../../../..';
+
+const columns = [
+ {
+ columnHeaderType: 'not-filtered',
+ displayAsText: 'Alert Status',
+ id: 'kibana.alert.status',
+ initialWidth: 110,
+ },
+ {
+ columnHeaderType: 'not-filtered',
+ displayAsText: 'Last updated',
+ id: '@timestamp',
+ initialWidth: 230,
+ schema: 'datetime',
+ },
+ {
+ columnHeaderType: 'not-filtered',
+ displayAsText: 'Duration',
+ id: 'kibana.alert.duration.us',
+ initialWidth: 116,
+ },
+ {
+ columnHeaderType: 'not-filtered',
+ displayAsText: 'Reason',
+ id: 'kibana.alert.reason',
+ linkField: '*',
+ },
+ {
+ columnHeaderType: 'not-filtered',
+ displayAsText: 'Maintenance windows',
+ id: 'kibana.alert.maintenance_window_ids',
+ schema: 'string',
+ initialWidth: 180,
+ },
+];
+
+const alert = {
+ _id: 'dc80788f-f869-4f14-bedb-950186c9d2f8',
+ _index: '.internal.alerts-stack.alerts-default-000001',
+ 'kibana.alert.status': ['active'],
+ 'kibana.alert.evaluation.conditions': ['Number of matching documents is NOT greater than 1000'],
+ 'kibana.alert.rule.producer': ['stackAlerts'],
+ 'kibana.alert.reason.text': [
+ 'Document count is 0 in the last 5m in metrics-* data view. Alert when greater than 1000.',
+ ],
+ 'kibana.alert.rule.rule_type_id': ['.es-query'],
+ 'kibana.alert.evaluation.value': ['0'],
+ 'kibana.alert.instance.id': ['query matched'],
+ 'kibana.alert.rule.name': ['Test rule'],
+ 'event.kind': ['signal'],
+ 'kibana.alert.title': ["rule 'Test rule' recovered"],
+ 'kibana.alert.workflow_status': ['open'],
+ 'kibana.alert.rule.uuid': ['TEST_RULE_UUID'],
+ 'kibana.alert.reason': [
+ 'Document count is 0 in the last 5m in metrics-* data view. Alert when greater than 1000.',
+ ],
+ 'kibana.alert.rule.consumer': ['infrastructure'],
+ 'kibana.alert.action_group': ['query matched'],
+ 'kibana.alert.rule.category': ['Elasticsearch query'],
+ 'event.action': ['active'],
+ '@timestamp': ['2023-12-22T09:23:08.244Z'],
+ 'kibana.alert.rule.execution.uuid': ['9ca6fe40-90c0-4e32-9772-025e4de79dd8'],
+ 'kibana.alert.uuid': ['dc80788f-f869-4f14-bedb-950186c9d2f8'],
+ 'kibana.space_ids': ['default'],
+ 'kibana.version': ['8.13.0'],
+} as unknown as AlertsTableFlyoutBaseProps['alert'];
+
+const tabsData = [
+ { name: 'Overview', subj: 'overviewTab' },
+ { name: 'Table', subj: 'tableTab' },
+];
+
+describe('DefaultAlertsFlyout', () => {
+ let wrapper: ReactWrapper;
+ beforeAll(async () => {
+ const { body: FlyoutBody } = getDefaultAlertFlyout(columns, (_columnId, value) => value)();
+ wrapper = mount() as ReactWrapper;
+ await waitFor(() => wrapper.update());
+ });
+
+ describe('tabs', () => {
+ tabsData.forEach(({ name: tab }) => {
+ test(`should render the ${tab} tab`, () => {
+ expect(
+ wrapper
+ .find('[data-test-subj="defaultAlertFlyoutTabs"]')
+ .find('[role="tablist"]')
+ .containsMatchingElement({tab})
+ ).toBeTruthy();
+ });
+ });
+
+ test('the Overview tab should be selected by default', () => {
+ expect(
+ wrapper
+ .find('[data-test-subj="defaultAlertFlyoutTabs"]')
+ .find('.euiTab-isSelected')
+ .first()
+ .text()
+ ).toEqual('Overview');
+ });
+
+ tabsData.forEach(({ subj, name }) => {
+ test(`should render the ${name} tab panel`, () => {
+ wrapper
+ .find('[data-test-subj="defaultAlertFlyoutTabs"]')
+ .find('[role="tablist"]')
+ .find(`[data-test-subj="${subj}"]`)
+ .first()
+ .simulate('click');
+ expect(
+ wrapper
+ .find('[data-test-subj="defaultAlertFlyoutTabs"]')
+ .find('[role="tabpanel"]')
+ .find(`[data-test-subj="${subj}Panel"]`)
+ .exists()
+ ).toBeTruthy();
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/default_alerts_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/default_alerts_flyout.tsx
index ab9b4d4813c18..ac6fd3e68970d 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/default_alerts_flyout.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/default_alerts_flyout.tsx
@@ -5,12 +5,20 @@
* 2.0.
*/
-import { get } from 'lodash';
-import React from 'react';
-import { type EuiDataGridColumn, EuiDescriptionList, EuiPanel, EuiTitle } from '@elastic/eui';
+import React, { useCallback, useMemo, useState } from 'react';
+import {
+ type EuiDataGridColumn,
+ EuiDescriptionList,
+ EuiPanel,
+ EuiTabbedContentTab,
+ EuiTitle,
+} from '@elastic/eui';
import { ALERT_RULE_NAME } from '@kbn/rule-data-utils';
-import { AlertsTableFlyoutBaseProps, AlertTableFlyoutComponent } from '../../../..';
+import { get } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import { ScrollableFlyoutTabbedContent, AlertFieldsTable } from '@kbn/alerts-ui-shared';
import { RegisterFormatter } from '../cells/render_cell_value';
+import { AlertsTableFlyoutBaseProps, AlertTableFlyoutComponent } from '../../../..';
const FlyoutHeader: AlertTableFlyoutComponent = ({ alert }: AlertsTableFlyoutBaseProps) => {
const name = alert[ALERT_RULE_NAME];
@@ -21,28 +29,83 @@ const FlyoutHeader: AlertTableFlyoutComponent = ({ alert }: AlertsTableFlyoutBas
);
};
+type TabId = 'overview' | 'table';
+
export const getDefaultAlertFlyout =
(columns: EuiDataGridColumn[], formatter: RegisterFormatter) => () => {
- const FlyoutBody: AlertTableFlyoutComponent = ({ alert }: AlertsTableFlyoutBaseProps) => (
-
- {
- const value = get(alert, column.id)?.[0];
-
- return {
- title: column.displayAsText as string,
- description: value != null ? formatter(column.id, value) : '—',
- };
- })}
- type="column"
- columnWidths={[1, 3]}
+ const FlyoutBody: AlertTableFlyoutComponent = ({ alert }: AlertsTableFlyoutBaseProps) => {
+ const overviewTab = useMemo(
+ () => ({
+ id: 'overview',
+ 'data-test-subj': 'overviewTab',
+ name: i18n.translate(
+ 'xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.overview',
+ {
+ defaultMessage: 'Overview',
+ }
+ ),
+ content: (
+
+ {
+ const value = get(alert, column.id)?.[0];
+
+ return {
+ title: column.displayAsText as string,
+ description: value != null ? formatter(column.id, value) : '—',
+ };
+ })}
+ type="column"
+ columnWidths={[1, 3]}
+ />
+
+ ),
+ }),
+ [alert]
+ );
+
+ const tableTab = useMemo(
+ () => ({
+ id: 'table',
+ 'data-test-subj': 'tableTab',
+ name: i18n.translate('xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.table', {
+ defaultMessage: 'Table',
+ }),
+ content: (
+
+
+
+ ),
+ }),
+ [alert]
+ );
+
+ const tabs = useMemo(() => [overviewTab, tableTab], [overviewTab, tableTab]);
+ const [selectedTabId, setSelectedTabId] = useState('overview');
+ const handleTabClick = useCallback(
+ (tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as TabId),
+ []
+ );
+
+ const selectedTab = useMemo(
+ () => tabs.find((tab) => tab.id === selectedTabId) ?? tabs[0],
+ [tabs, selectedTabId]
+ );
+
+ return (
+
-
- );
+ );
+ };
return {
- body: FlyoutBody,
header: FlyoutHeader,
+ body: FlyoutBody,
footer: null,
};
};