diff --git a/src/platform/plugins/shared/workflows_management/kibana.jsonc b/src/platform/plugins/shared/workflows_management/kibana.jsonc
index 6b862250f9b10..2d47dddf5db17 100644
--- a/src/platform/plugins/shared/workflows_management/kibana.jsonc
+++ b/src/platform/plugins/shared/workflows_management/kibana.jsonc
@@ -30,6 +30,8 @@
"fieldFormats",
"unifiedSearch",
"embeddable",
+ "controls",
+ "uiActions",
"licensing"
],
"optionalPlugins": ["alerting", "serverless", "cloud", "inbox"],
diff --git a/src/platform/plugins/shared/workflows_management/moon.yml b/src/platform/plugins/shared/workflows_management/moon.yml
index 6d5c110ce54e8..9019cbc51887d 100644
--- a/src/platform/plugins/shared/workflows_management/moon.yml
+++ b/src/platform/plugins/shared/workflows_management/moon.yml
@@ -113,6 +113,7 @@ dependsOn:
- '@kbn/inbox-plugin'
- '@kbn/inbox-common'
- '@kbn/datemath'
+ - '@kbn/presentation-publishing'
tags:
- plugin
- prod
diff --git a/src/platform/plugins/shared/workflows_management/public/mocks.ts b/src/platform/plugins/shared/workflows_management/public/mocks.ts
index 1872eae4f629c..c4e997433ebc4 100644
--- a/src/platform/plugins/shared/workflows_management/public/mocks.ts
+++ b/src/platform/plugins/shared/workflows_management/public/mocks.ts
@@ -21,6 +21,7 @@ import { QueryClient } from '@kbn/react-query';
import { serverlessMock } from '@kbn/serverless/public/mocks';
import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks';
import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks';
+import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
import { workflowsExtensionsMock } from '@kbn/workflows-extensions/public/mocks';
import { createAvailabilityServiceMock } from './common/lib/availability/mock';
@@ -39,6 +40,7 @@ export const createStartServicesMock = () => ({
data: dataPluginMock.createStartContract(),
spaces: spacesPluginMock.createStartContract(),
triggersActionsUi: triggersActionsUiMock.createStart(),
+ uiActions: uiActionsPluginMock.createStartContract(),
workflowsExtensions: workflowsExtensionsMock.createStart(),
licensing: licensingMock.createStart(),
cloud: cloudMock.createStart(),
diff --git a/src/platform/plugins/shared/workflows_management/public/pages/executions/executions_page.test.tsx b/src/platform/plugins/shared/workflows_management/public/pages/executions/executions_page.test.tsx
index 4d3eb435905a8..8686b1bbde147 100644
--- a/src/platform/plugins/shared/workflows_management/public/pages/executions/executions_page.test.tsx
+++ b/src/platform/plugins/shared/workflows_management/public/pages/executions/executions_page.test.tsx
@@ -7,21 +7,60 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import { render, screen } from '@testing-library/react';
+import { render, screen, waitFor } from '@testing-library/react';
import React from 'react';
+import { of } from 'rxjs';
+import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
import { WorkflowExecutionsPage } from './executions_page';
import { createStartServicesMock } from '../../mocks';
import { getTestProvider } from '../../shared/mocks/test_providers';
+jest.mock('@kbn/alerts-ui-shared/src/alert_filter_controls', () => ({
+ AlertFilterControls: () =>
,
+}));
+
+jest.mock('@kbn/unified-data-table', () => {
+ const actual = jest.requireActual('@kbn/unified-data-table');
+ return {
+ ...actual,
+ UnifiedDataTable: () => ,
+ };
+});
+
+jest.mock('@kbn/cell-actions', () => ({
+ CellActionsProvider: ({ children }: { children: React.ReactNode }) => <>{children}>,
+}));
+
describe('WorkflowExecutionsPage', () => {
- it('renders the executions shell and stub grid', async () => {
+ it('renders the executions page with search, filters, and table', async () => {
const services = createStartServicesMock();
services.workflowsManagement.globalExecutionsView.enabled = true;
+ services.spaces.getActiveSpace = jest.fn().mockResolvedValue({ id: 'default' });
+ const SearchBarStub = () => ;
+ services.unifiedSearch.ui.SearchBar = SearchBarStub;
+
+ jest.mocked(searchSourceInstanceMock.fetch$).mockReturnValue(
+ of({
+ rawResponse: {
+ hits: {
+ hits: [],
+ total: { value: 0, relation: 'eq' },
+ },
+ },
+ }) as unknown as ReturnType
+ );
render(, { wrapper: getTestProvider({ services }) });
expect(screen.getByTestId('workflowExecutionsPage')).toBeInTheDocument();
- expect(screen.getByTestId('workflowExecutionsSearchFilterScaffold')).toBeInTheDocument();
- await screen.findByTestId('discoverDocTable');
+ expect(screen.getByTestId('workflowExecutionsPageContent')).toBeInTheDocument();
+ expect(screen.getByTestId('searchBarStub')).toBeInTheDocument();
+ await waitFor(() => {
+ expect(screen.getByTestId('workflowExecutionsFilters')).toBeInTheDocument();
+ });
+ expect(screen.getByTestId('alertFilterControlsStub')).toBeInTheDocument();
+ await waitFor(() => {
+ expect(screen.getByTestId('workflowExecutionsTableEmpty')).toBeInTheDocument();
+ });
});
});
diff --git a/src/platform/plugins/shared/workflows_management/public/pages/executions/executions_page.tsx b/src/platform/plugins/shared/workflows_management/public/pages/executions/executions_page.tsx
index 953e26898f236..d4157312cf5f6 100644
--- a/src/platform/plugins/shared/workflows_management/public/pages/executions/executions_page.tsx
+++ b/src/platform/plugins/shared/workflows_management/public/pages/executions/executions_page.tsx
@@ -7,13 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import { EuiPageTemplate, EuiSpacer, EuiText, useEuiTheme } from '@elastic/eui';
+import { EuiPageTemplate, EuiScreenReaderOnly, EuiText, useEuiTheme } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n-react';
-import { WorkflowExecutionsFilterSearchScaffold } from './workflow_executions_filter_search_scaffold';
-import { WorkflowExecutionsStubDataGrid } from './workflow_executions_stub_data_grid';
-import { useKibana } from '../../hooks/use_kibana';
+import { WorkflowExecutionsPageContent } from './workflow_executions_page_content';
import { useWorkflowsBreadcrumbs } from '../../hooks/use_workflow_breadcrumbs/use_workflow_breadcrumbs';
const executionsPageTitle = i18n.translate('workflowsManagement.executionsPage.pageTitle', {
@@ -23,13 +20,11 @@ const executionsPageTitle = i18n.translate('workflowsManagement.executionsPage.p
const executionsPageDescription = i18n.translate(
'workflowsManagement.executionsPage.pageDescription',
{
- defaultMessage:
- 'Recent workflow executions for your space. Data below is static until the list is connected to Elasticsearch.',
+ defaultMessage: 'Browse and filter workflow executions across your space.',
}
);
export function WorkflowExecutionsPage() {
- const services = useKibana().services;
const { euiTheme } = useEuiTheme();
useWorkflowsBreadcrumbs(executionsPageTitle);
@@ -41,23 +36,15 @@ export function WorkflowExecutionsPage() {
data-test-subj="workflowExecutionsPage"
>
+
+ {executionsPageTitle}
+
{executionsPageDescription}
-
-
-
-
-
-
-
-
-
+
);
diff --git a/src/platform/plugins/shared/workflows_management/public/pages/executions/static_execution_rows.ts b/src/platform/plugins/shared/workflows_management/public/pages/executions/static_execution_rows.ts
deleted file mode 100644
index 0ee371324fc3e..0000000000000
--- a/src/platform/plugins/shared/workflows_management/public/pages/executions/static_execution_rows.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-export interface WorkflowExecutionListStubRow {
- readonly '@timestamp': string;
- readonly execution_id: string;
- readonly workflow_name: string;
- readonly status: string;
- readonly trigger_type: string;
-}
-
-export const WORKFLOW_EXECUTIONS_STUB_DATA_VIEW_TITLE = 'workflows-executions-stub';
-
-export const STATIC_WORKFLOW_EXECUTION_ROWS: WorkflowExecutionListStubRow[] = [
- {
- '@timestamp': '2026-05-01T10:15:00.000Z',
- execution_id: 'exec-001',
- workflow_name: 'Notify on-call',
- status: 'completed',
- trigger_type: 'manual',
- },
- {
- '@timestamp': '2026-05-01T10:42:22.000Z',
- execution_id: 'exec-002',
- workflow_name: 'Index enrichment',
- status: 'running',
- trigger_type: 'index',
- },
- {
- '@timestamp': '2026-05-02T08:03:11.000Z',
- execution_id: 'exec-003',
- workflow_name: 'Weekly report',
- status: 'failed',
- trigger_type: 'alert',
- },
-];
diff --git a/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_execution_detail_flyout.tsx b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_execution_detail_flyout.tsx
new file mode 100644
index 0000000000000..0b3804e6b7377
--- /dev/null
+++ b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_execution_detail_flyout.tsx
@@ -0,0 +1,152 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import {
+ EuiCodeBlock,
+ EuiDescriptionList,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutHeader,
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+ useGeneratedHtmlId,
+} from '@elastic/eui';
+import React, { useMemo } from 'react';
+import type { DataTableRecord } from '@kbn/discover-utils/types';
+import { i18n } from '@kbn/i18n';
+
+export interface WorkflowExecutionDetailFlyoutProps {
+ hit: DataTableRecord;
+ onClose: () => void;
+}
+
+const formatValue = (value: unknown): string => {
+ if (value == null) {
+ return '\u2014';
+ }
+ if (Array.isArray(value)) {
+ return value.length === 1 ? formatValue(value[0]) : value.map(formatValue).join(', ');
+ }
+ if (typeof value === 'object') {
+ return JSON.stringify(value);
+ }
+ return String(value);
+};
+
+const SUMMARY_FIELDS: ReadonlyArray<{ field: string; label: string }> = [
+ {
+ field: 'id',
+ label: i18n.translate('workflowsManagement.executionsPage.flyoutFieldId', {
+ defaultMessage: 'Execution ID',
+ }),
+ },
+ {
+ field: 'workflowId',
+ label: i18n.translate('workflowsManagement.executionsPage.flyoutFieldWorkflow', {
+ defaultMessage: 'Workflow',
+ }),
+ },
+ {
+ field: 'status',
+ label: i18n.translate('workflowsManagement.executionsPage.flyoutFieldStatus', {
+ defaultMessage: 'Status',
+ }),
+ },
+ {
+ field: 'startedAt',
+ label: i18n.translate('workflowsManagement.executionsPage.flyoutFieldStarted', {
+ defaultMessage: 'Started at',
+ }),
+ },
+ {
+ field: 'finishedAt',
+ label: i18n.translate('workflowsManagement.executionsPage.flyoutFieldFinished', {
+ defaultMessage: 'Finished at',
+ }),
+ },
+ {
+ field: 'triggeredBy',
+ label: i18n.translate('workflowsManagement.executionsPage.flyoutFieldTriggeredBy', {
+ defaultMessage: 'Triggered by',
+ }),
+ },
+ {
+ field: 'executedBy',
+ label: i18n.translate('workflowsManagement.executionsPage.flyoutFieldExecutedBy', {
+ defaultMessage: 'Executed by',
+ }),
+ },
+];
+
+export const WorkflowExecutionDetailFlyout = React.memo(
+ ({ hit, onClose }) => {
+ const flyoutTitleId = useGeneratedHtmlId({ prefix: 'workflowExecutionDetailFlyoutTitle' });
+
+ const summary = useMemo(
+ () =>
+ SUMMARY_FIELDS.map(({ field, label }) => ({
+ title: label,
+ description: formatValue(hit.flattened[field]),
+ })),
+ [hit.flattened]
+ );
+
+ const rawJson = useMemo(
+ () => JSON.stringify(hit.raw?._source ?? hit.flattened, null, 2),
+ [hit.flattened, hit.raw]
+ );
+
+ return (
+
+
+
+
+ {i18n.translate('workflowsManagement.executionsPage.flyoutTitle', {
+ defaultMessage: 'Execution details',
+ })}
+
+
+
+
+ {hit.flattened.id ? formatValue(hit.flattened.id) : hit.id}
+
+
+
+
+
+
+
+ {i18n.translate('workflowsManagement.executionsPage.flyoutRawJsonTitle', {
+ defaultMessage: 'Raw document',
+ })}
+
+
+
+
+ {rawJson}
+
+
+
+ );
+ }
+);
+WorkflowExecutionDetailFlyout.displayName = 'WorkflowExecutionDetailFlyout';
diff --git a/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_data_view.ts b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_data_view.ts
new file mode 100644
index 0000000000000..51e64d30cdf81
--- /dev/null
+++ b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_data_view.ts
@@ -0,0 +1,77 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { DataViewSpec, FieldSpec } from '@kbn/data-views-plugin/common';
+import { DataView } from '@kbn/data-views-plugin/common';
+import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
+import { WORKFLOWS_EXECUTIONS_INDEX } from '../../../common';
+
+export const WORKFLOW_EXECUTIONS_DATA_VIEW_ID = 'workflows-executions-adhoc';
+
+export const WORKFLOW_EXECUTIONS_DATA_VIEW_SPEC: DataViewSpec = {
+ id: WORKFLOW_EXECUTIONS_DATA_VIEW_ID,
+ title: WORKFLOWS_EXECUTIONS_INDEX,
+ timeFieldName: 'startedAt',
+};
+
+const keywordField = (name: string): FieldSpec => ({
+ name,
+ type: 'string',
+ esTypes: ['keyword'],
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ scripted: false,
+});
+
+const dateField = (name: string): FieldSpec => ({
+ name,
+ type: 'date',
+ esTypes: ['date'],
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ scripted: false,
+});
+
+const booleanField = (name: string): FieldSpec => ({
+ name,
+ type: 'boolean',
+ esTypes: ['boolean'],
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ scripted: false,
+});
+
+export function createWorkflowExecutionsDataView(fieldFormats: FieldFormatsStart): DataView {
+ const fields: Record = {
+ startedAt: dateField('startedAt'),
+ createdAt: dateField('createdAt'),
+ finishedAt: dateField('finishedAt'),
+ id: keywordField('id'),
+ workflowId: keywordField('workflowId'),
+ status: keywordField('status'),
+ triggeredBy: keywordField('triggeredBy'),
+ executedBy: keywordField('executedBy'),
+ createdBy: keywordField('createdBy'),
+ isTestRun: booleanField('isTestRun'),
+ spaceId: keywordField('spaceId'),
+ };
+
+ return new DataView({
+ spec: {
+ ...WORKFLOW_EXECUTIONS_DATA_VIEW_SPEC,
+ allowNoIndex: true,
+ fields,
+ },
+ fieldFormats,
+ metaFields: ['_id', '_type', '_source'],
+ });
+}
diff --git a/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_filter_search_scaffold.tsx b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_filter_search_scaffold.tsx
deleted file mode 100644
index 651943b039fb6..0000000000000
--- a/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_filter_search_scaffold.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import {
- EuiFieldSearch,
- EuiFilterButton,
- EuiFilterGroup,
- EuiFlexGroup,
- EuiFlexItem,
- EuiScreenReaderOnly,
-} from '@elastic/eui';
-import { css } from '@emotion/react';
-import React from 'react';
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n-react';
-
-const searchBarWrapperCss = css`
- min-width: 200px;
- & .euiPopover {
- display: block;
- }
-`;
-
-const searchAriaLabel = i18n.translate(
- 'workflowsManagement.executionsPage.searchScaffoldAriaLabel',
- {
- defaultMessage: 'Search executions (not yet available)',
- }
-);
-
-const searchPlaceholder = i18n.translate(
- 'workflowsManagement.executionsPage.searchScaffoldPlaceholder',
- {
- defaultMessage: 'Execution ID / workflow name',
- }
-);
-
-const filterStatusLabel = i18n.translate(
- 'workflowsManagement.executionsPage.filterScaffoldStatus',
- {
- defaultMessage: 'Status',
- }
-);
-
-const filterWorkflowLabel = i18n.translate(
- 'workflowsManagement.executionsPage.filterScaffoldWorkflow',
- {
- defaultMessage: 'Workflow',
- }
-);
-
-const filterTimeRangeLabel = i18n.translate(
- 'workflowsManagement.executionsPage.filterScaffoldTimeRange',
- {
- defaultMessage: 'Time range',
- }
-);
-
-export const WorkflowExecutionsFilterSearchScaffold = React.memo(() => (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {filterStatusLabel}
-
-
-
-
-
-
- {filterWorkflowLabel}
-
-
-
-
-
-
- {filterTimeRangeLabel}
-
-
-
-
-
-));
-WorkflowExecutionsFilterSearchScaffold.displayName = 'WorkflowExecutionsFilterSearchScaffold';
diff --git a/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_filters.tsx b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_filters.tsx
new file mode 100644
index 0000000000000..8ba4b8c2159db
--- /dev/null
+++ b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_filters.tsx
@@ -0,0 +1,105 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import React, { useCallback, useMemo } from 'react';
+import { useHistory, useLocation } from 'react-router-dom';
+import { AlertFilterControls } from '@kbn/alerts-ui-shared/src/alert_filter_controls';
+import type { FilterControlConfig } from '@kbn/alerts-ui-shared/src/alert_filter_controls/types';
+import type { Filter, Query, TimeRange } from '@kbn/es-query';
+import { createKbnUrlStateStorage, Storage } from '@kbn/kibana-utils-plugin/public';
+import { convertCamelCasedKeysToSnakeCase } from '@kbn/presentation-publishing';
+import {
+ WORKFLOW_EXECUTIONS_DATA_VIEW_ID,
+ WORKFLOW_EXECUTIONS_DATA_VIEW_SPEC,
+} from './workflow_executions_data_view';
+import {
+ DEFAULT_EXECUTION_PAGE_FILTERS,
+ EXECUTION_FILTERS_STORAGE_KEY,
+ EXECUTION_FILTERS_URL_PARAM_KEY,
+} from './workflow_executions_page_constants';
+import { useKibana } from '../../hooks/use_kibana';
+import { useSpaceId } from '../../hooks/use_space_id';
+
+export interface WorkflowExecutionsFiltersProps {
+ filters: Filter[];
+ query?: Query;
+ timeRange: TimeRange;
+ onFiltersChange: (filters: Filter[]) => void;
+}
+
+export const WorkflowExecutionsFilters = React.memo(
+ ({ filters, query, timeRange, onFiltersChange }) => {
+ const { http, notifications, dataViews } = useKibana().services;
+ const spaceId = useSpaceId();
+ const history = useHistory();
+ const location = useLocation();
+
+ const urlStorage = useMemo(
+ () =>
+ createKbnUrlStateStorage({
+ history,
+ useHash: false,
+ useHashQuery: false,
+ }),
+ [history]
+ );
+
+ const persisted = urlStorage.get(
+ EXECUTION_FILTERS_URL_PARAM_KEY
+ );
+ const controlsUrlState = persisted
+ ? persisted.map(convertCamelCasedKeysToSnakeCase)
+ : undefined;
+
+ const setControlsUrlState = useCallback(
+ (next: FilterControlConfig[]) => {
+ urlStorage.set(EXECUTION_FILTERS_URL_PARAM_KEY, next);
+ },
+ [urlStorage]
+ );
+
+ const services = useMemo(
+ () => ({
+ http,
+ notifications,
+ dataViews,
+ storage: Storage,
+ }),
+ [http, notifications, dataViews]
+ );
+
+ if (!spaceId) {
+ return null;
+ }
+
+ return (
+
+ );
+ }
+);
+WorkflowExecutionsFilters.displayName = 'WorkflowExecutionsFilters';
diff --git a/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_page_constants.ts b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_page_constants.ts
new file mode 100644
index 0000000000000..e343dea771606
--- /dev/null
+++ b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_page_constants.ts
@@ -0,0 +1,43 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { FilterControlConfig } from '@kbn/alerts-ui-shared/src/alert_filter_controls/types';
+import { i18n } from '@kbn/i18n';
+
+export const EXECUTION_FILTERS_URL_PARAM_KEY = 'workflowsExecutionsPageFilters' as const;
+export const EXECUTION_FILTERS_STORAGE_KEY = 'workflows.executions.pageFilters' as const;
+
+export const DEFAULT_EXECUTION_PAGE_FILTERS: FilterControlConfig[] = [
+ {
+ title: i18n.translate('workflowsManagement.executionsPage.filterStatus', {
+ defaultMessage: 'Status',
+ }),
+ field_name: 'status',
+ persist: true,
+ },
+ {
+ title: i18n.translate('workflowsManagement.executionsPage.filterWorkflow', {
+ defaultMessage: 'Workflow',
+ }),
+ field_name: 'workflowId',
+ persist: true,
+ },
+ {
+ title: i18n.translate('workflowsManagement.executionsPage.filterExecutedBy', {
+ defaultMessage: 'Executed by',
+ }),
+ field_name: 'executedBy',
+ },
+ {
+ title: i18n.translate('workflowsManagement.executionsPage.filterTrigger', {
+ defaultMessage: 'Trigger',
+ }),
+ field_name: 'triggeredBy',
+ },
+];
diff --git a/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_page_content.tsx b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_page_content.tsx
new file mode 100644
index 0000000000000..5aef1227e8225
--- /dev/null
+++ b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_page_content.tsx
@@ -0,0 +1,102 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
+import React, { useCallback, useMemo, useState } from 'react';
+import type { Filter, Query, TimeRange } from '@kbn/es-query';
+import { createWorkflowExecutionsDataView } from './workflow_executions_data_view';
+import { WorkflowExecutionsFilters } from './workflow_executions_filters';
+import { WorkflowExecutionsSearchBar } from './workflow_executions_search_bar';
+import { WorkflowExecutionsTable } from './workflow_executions_table';
+import { useKibana } from '../../hooks/use_kibana';
+import { useSpaceId } from '../../hooks/use_space_id';
+
+const DEFAULT_TIME_RANGE: TimeRange = {
+ from: 'now-24h',
+ to: 'now',
+};
+
+const DEFAULT_QUERY: Query = {
+ query: '',
+ language: 'kuery',
+};
+
+const EMPTY_FILTERS: Filter[] = [];
+
+export const WorkflowExecutionsPageContent = React.memo(() => {
+ const { fieldFormats } = useKibana().services;
+ const spaceId = useSpaceId();
+ const dataView = useMemo(() => createWorkflowExecutionsDataView(fieldFormats), [fieldFormats]);
+
+ const [query, setQuery] = useState(DEFAULT_QUERY);
+ const [submittedQuery, setSubmittedQuery] = useState(DEFAULT_QUERY);
+ const [timeRange, setTimeRange] = useState(DEFAULT_TIME_RANGE);
+ const [controlFilters, setControlFilters] = useState(EMPTY_FILTERS);
+ const [searchBarFilters, setSearchBarFilters] = useState(EMPTY_FILTERS);
+
+ const combinedFilters = useMemo(
+ () => [...controlFilters, ...searchBarFilters],
+ [controlFilters, searchBarFilters]
+ );
+
+ const handleQueryChange = useCallback(
+ ({ query: nextQuery, dateRange }: { query?: Query; dateRange: TimeRange }) => {
+ if (nextQuery) {
+ setQuery(nextQuery);
+ }
+ setTimeRange(dateRange);
+ },
+ []
+ );
+
+ const handleQuerySubmit = useCallback(
+ ({ query: nextQuery, dateRange }: { query?: Query; dateRange: TimeRange }) => {
+ if (nextQuery) {
+ setQuery(nextQuery);
+ setSubmittedQuery(nextQuery);
+ }
+ setTimeRange(dateRange);
+ },
+ []
+ );
+
+ return (
+
+
+
+
+
+
+
+ {spaceId ? (
+
+ ) : null}
+
+ );
+});
+WorkflowExecutionsPageContent.displayName = 'WorkflowExecutionsPageContent';
diff --git a/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_search_bar.tsx b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_search_bar.tsx
new file mode 100644
index 0000000000000..8f78d7d2a31b4
--- /dev/null
+++ b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_search_bar.tsx
@@ -0,0 +1,59 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import React from 'react';
+import type { DataView } from '@kbn/data-views-plugin/common';
+import type { Filter, Query, TimeRange } from '@kbn/es-query';
+import { i18n } from '@kbn/i18n';
+import { useKibana } from '../../hooks/use_kibana';
+
+export interface WorkflowExecutionsSearchBarProps {
+ dataView: DataView;
+ query: Query;
+ timeRange: TimeRange;
+ filters: Filter[];
+ onQueryChange: (payload: { query?: Query; dateRange: TimeRange }) => void;
+ onQuerySubmit: (payload: { query?: Query; dateRange: TimeRange }) => void;
+ onFiltersUpdated: (filters: Filter[]) => void;
+}
+
+export const WorkflowExecutionsSearchBar = React.memo(
+ ({ dataView, query, timeRange, filters, onQueryChange, onQuerySubmit, onFiltersUpdated }) => {
+ const { unifiedSearch } = useKibana().services;
+ const { SearchBar } = unifiedSearch.ui;
+
+ return (
+
+
+
+ );
+ }
+);
+WorkflowExecutionsSearchBar.displayName = 'WorkflowExecutionsSearchBar';
diff --git a/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_search_query.test.ts b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_search_query.test.ts
new file mode 100644
index 0000000000000..1d8b4f86a3d51
--- /dev/null
+++ b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_search_query.test.ts
@@ -0,0 +1,80 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import {
+ buildWorkflowExecutionsSearchFilters,
+ getWorkflowExecutionsFetchErrorMessage,
+ isWorkflowExecutionsIndexNotFoundError,
+} from './workflow_executions_search_query';
+
+describe('workflow_executions_search_query', () => {
+ describe('buildWorkflowExecutionsSearchFilters', () => {
+ it('includes user filters, space, step-run exclusion, and time range', () => {
+ const userFilters = [
+ {
+ meta: { alias: 'status filter', disabled: false },
+ query: { term: { status: 'completed' } },
+ },
+ ];
+
+ const filters = buildWorkflowExecutionsSearchFilters({
+ spaceId: 'default',
+ timeRange: { from: 'now-24h', to: 'now' },
+ timeField: 'startedAt',
+ userFilters,
+ });
+
+ expect(filters).toHaveLength(4);
+ expect(filters[0]).toBe(userFilters[0]);
+ expect(filters[1].query).toEqual({
+ bool: {
+ should: [
+ { term: { spaceId: 'default' } },
+ { bool: { must_not: { exists: { field: 'spaceId' } } } },
+ ],
+ minimum_should_match: 1,
+ },
+ });
+ expect(filters[2].query).toEqual({
+ bool: {
+ must_not: { exists: { field: 'stepId' } },
+ },
+ });
+ expect(filters[3].query).toEqual({
+ range: {
+ startedAt: {
+ gte: 'now-24h',
+ lte: 'now',
+ format: 'strict_date_optional_time',
+ },
+ },
+ });
+ });
+ });
+
+ describe('isWorkflowExecutionsIndexNotFoundError', () => {
+ it('returns true for index_not_found_exception', () => {
+ const error = {
+ body: { error: { type: 'index_not_found_exception', reason: 'missing' } },
+ };
+
+ expect(isWorkflowExecutionsIndexNotFoundError(error)).toBe(true);
+ });
+
+ it('returns false for other errors', () => {
+ expect(isWorkflowExecutionsIndexNotFoundError(new Error('other'))).toBe(false);
+ });
+ });
+
+ describe('getWorkflowExecutionsFetchErrorMessage', () => {
+ it('returns a generic message', () => {
+ expect(getWorkflowExecutionsFetchErrorMessage()).toBe('Failed to load executions');
+ });
+ });
+});
diff --git a/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_search_query.ts b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_search_query.ts
new file mode 100644
index 0000000000000..7eda56c60b1b0
--- /dev/null
+++ b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_search_query.ts
@@ -0,0 +1,83 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import type { Filter, TimeRange } from '@kbn/es-query';
+import { i18n } from '@kbn/i18n';
+
+interface ElasticsearchErrorBody {
+ error?: {
+ type?: string;
+ };
+}
+
+const isElasticsearchResponseError = (error: unknown): error is { body: ElasticsearchErrorBody } =>
+ typeof error === 'object' &&
+ error !== null &&
+ 'body' in error &&
+ typeof (error as { body?: unknown }).body === 'object';
+
+const buildSpaceFilterQuery = (spaceId: string): QueryDslQueryContainer => ({
+ bool: {
+ should: [{ term: { spaceId } }, { bool: { must_not: { exists: { field: 'spaceId' } } } }],
+ minimum_should_match: 1,
+ },
+});
+
+const buildOmitStepRunsFilterQuery = (): QueryDslQueryContainer => ({
+ bool: {
+ must_not: { exists: { field: 'stepId' } },
+ },
+});
+
+const buildTimeRangeFilter = (timeRange: TimeRange, timeField: string): Filter => ({
+ query: {
+ range: {
+ [timeField]: {
+ gte: timeRange.from,
+ lte: timeRange.to,
+ format: 'strict_date_optional_time',
+ },
+ },
+ },
+ meta: {
+ type: 'custom',
+ },
+});
+
+export const buildWorkflowExecutionsSearchFilters = ({
+ spaceId,
+ timeRange,
+ timeField,
+ userFilters,
+}: {
+ spaceId: string;
+ timeRange: TimeRange;
+ timeField: string;
+ userFilters: Filter[];
+}): Filter[] => [
+ ...userFilters,
+ {
+ meta: { type: 'custom', disabled: false },
+ query: buildSpaceFilterQuery(spaceId),
+ },
+ {
+ meta: { type: 'custom', disabled: false },
+ query: buildOmitStepRunsFilterQuery(),
+ },
+ buildTimeRangeFilter(timeRange, timeField),
+];
+
+export const isWorkflowExecutionsIndexNotFoundError = (error: unknown): boolean =>
+ isElasticsearchResponseError(error) && error.body.error?.type === 'index_not_found_exception';
+
+export const getWorkflowExecutionsFetchErrorMessage = (): string =>
+ i18n.translate('workflowsManagement.executionsPage.fetchError', {
+ defaultMessage: 'Failed to load executions',
+ });
diff --git a/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_stub_data_grid.tsx b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_stub_data_grid.tsx
deleted file mode 100644
index 5bee2079c4eb5..0000000000000
--- a/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_stub_data_grid.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import { css } from '@emotion/react';
-import React, { useCallback, useMemo, useState } from 'react';
-import { CellActionsProvider } from '@kbn/cell-actions';
-import { DataView, type FieldSpec } from '@kbn/data-views-plugin/common';
-import type { DataTableRecord } from '@kbn/discover-utils/types';
-import { Storage } from '@kbn/kibana-utils-plugin/public';
-import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
-import { DataLoadingState, type SortOrder, UnifiedDataTable } from '@kbn/unified-data-table';
-import {
- STATIC_WORKFLOW_EXECUTION_ROWS,
- WORKFLOW_EXECUTIONS_STUB_DATA_VIEW_TITLE,
- type WorkflowExecutionListStubRow,
-} from './static_execution_rows';
-import type { WorkflowsServices } from '../../types';
-
-const INITIAL_COLUMNS: string[] = [
- '@timestamp',
- 'workflow_name',
- 'status',
- 'execution_id',
- 'trigger_type',
-];
-
-const sortOrder: SortOrder[] = [];
-
-const noopGetTriggerCompatibleActions: UiActionsStart['getTriggerCompatibleActions'] =
- async () => [];
-
-function stringField(name: string): FieldSpec {
- return {
- name,
- type: 'string',
- esTypes: ['keyword'],
- searchable: true,
- aggregatable: true,
- readFromDocValues: true,
- scripted: false,
- };
-}
-
-function dateField(name: string): FieldSpec {
- return {
- name,
- type: 'date',
- esTypes: ['date'],
- searchable: true,
- aggregatable: true,
- readFromDocValues: true,
- scripted: false,
- };
-}
-
-function createStubExecutionsDataView(fieldFormats: WorkflowsServices['fieldFormats']): DataView {
- const fields: Record = {
- '@timestamp': dateField('@timestamp'),
- execution_id: stringField('execution_id'),
- workflow_name: stringField('workflow_name'),
- status: stringField('status'),
- trigger_type: stringField('trigger_type'),
- };
-
- return new DataView({
- spec: {
- title: WORKFLOW_EXECUTIONS_STUB_DATA_VIEW_TITLE,
- allowNoIndex: true,
- timeFieldName: '@timestamp',
- fields,
- },
- fieldFormats,
- metaFields: ['_id', '_type', '_source'],
- });
-}
-
-function rowsToDataTableRecords(rows: WorkflowExecutionListStubRow[]): DataTableRecord[] {
- return rows.map(
- (row) =>
- ({
- id: row.execution_id,
- raw: row,
- flattened: row,
- } as unknown as DataTableRecord)
- );
-}
-
-const ROWS_PER_PAGE_OPTIONS = [10, 25];
-const DEFAULT_ROWS_PER_PAGE = 10;
-
-export interface WorkflowExecutionsStubDataGridProps {
- services: WorkflowsServices;
-}
-
-export const WorkflowExecutionsStubDataGrid = React.memo(
- ({ services }) => {
- const { data, fieldFormats, notifications, theme, uiSettings } = services;
-
- const dataView = useMemo(() => createStubExecutionsDataView(fieldFormats), [fieldFormats]);
-
- const [columns, setColumns] = useState(INITIAL_COLUMNS);
- const [rowsPerPage, setRowsPerPage] = useState(DEFAULT_ROWS_PER_PAGE);
-
- const rows = useMemo(() => rowsToDataTableRecords(STATIC_WORKFLOW_EXECUTION_ROWS), []);
-
- const unifiedTableServices = useMemo(
- () => ({
- data,
- theme,
- uiSettings,
- toastNotifications: notifications.toasts,
- fieldFormats,
- storage: new Storage(localStorage),
- }),
- [data, fieldFormats, notifications.toasts, theme, uiSettings]
- );
-
- const onSetColumns = useCallback((next: string[]) => {
- setColumns(next);
- }, []);
-
- return (
-
-
-
- );
- }
-);
-WorkflowExecutionsStubDataGrid.displayName = 'WorkflowExecutionsStubDataGrid';
diff --git a/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_table.test.tsx b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_table.test.tsx
new file mode 100644
index 0000000000000..1dc9f13660e20
--- /dev/null
+++ b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_table.test.tsx
@@ -0,0 +1,152 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { errors } from '@elastic/elasticsearch';
+import { render, screen, waitFor } from '@testing-library/react';
+import React from 'react';
+import { of, throwError } from 'rxjs';
+import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
+import { createWorkflowExecutionsDataView } from './workflow_executions_data_view';
+import { WorkflowExecutionsTable } from './workflow_executions_table';
+import { createStartServicesMock } from '../../mocks';
+import { getTestProvider } from '../../shared/mocks/test_providers';
+
+jest.mock('@kbn/unified-data-table', () => {
+ const actual = jest.requireActual('@kbn/unified-data-table');
+ return {
+ ...actual,
+ UnifiedDataTable: () => ,
+ };
+});
+
+jest.mock('@kbn/cell-actions', () => ({
+ CellActionsProvider: ({ children }: { children: React.ReactNode }) => <>{children}>,
+}));
+
+describe('WorkflowExecutionsTable', () => {
+ const defaultQuery = { query: '', language: 'kuery' as const };
+ const defaultTimeRange = { from: 'now-24h', to: 'now' };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.mocked(searchSourceInstanceMock.fetch$).mockReturnValue(
+ of({
+ rawResponse: {
+ hits: {
+ hits: [],
+ total: { value: 0, relation: 'eq' },
+ },
+ },
+ }) as unknown as ReturnType
+ );
+ });
+
+ it('queries with space scoping and step-run exclusion filters', async () => {
+ const services = createStartServicesMock();
+ const dataView = createWorkflowExecutionsDataView(services.fieldFormats);
+
+ render(
+ ,
+ { wrapper: getTestProvider({ services }) }
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('workflowExecutionsTableEmpty')).toBeInTheDocument();
+ });
+
+ const filterCalls = jest
+ .mocked(searchSourceInstanceMock.setField)
+ .mock.calls.filter(([field]) => field === 'filter');
+ expect(filterCalls.length).toBeGreaterThan(0);
+
+ const searchFilters = filterCalls[filterCalls.length - 1][1] as Array<{ query: unknown }>;
+ expect(searchFilters).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ query: {
+ bool: {
+ should: [
+ { term: { spaceId: 'my-space' } },
+ { bool: { must_not: { exists: { field: 'spaceId' } } } },
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ }),
+ expect.objectContaining({
+ query: {
+ bool: {
+ must_not: { exists: { field: 'stepId' } },
+ },
+ },
+ }),
+ ])
+ );
+ });
+
+ it('shows empty state when the executions index does not exist', async () => {
+ const services = createStartServicesMock();
+ const dataView = createWorkflowExecutionsDataView(services.fieldFormats);
+ const indexNotFoundError = new errors.ResponseError({
+ statusCode: 404,
+ body: { error: { type: 'index_not_found_exception', reason: 'missing' } },
+ } as ConstructorParameters[0]);
+
+ jest
+ .mocked(searchSourceInstanceMock.fetch$)
+ .mockReturnValue(throwError(() => indexNotFoundError));
+
+ render(
+ ,
+ { wrapper: getTestProvider({ services }) }
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('workflowExecutionsTableEmpty')).toBeInTheDocument();
+ });
+ expect(screen.queryByTestId('workflowExecutionsTableError')).not.toBeInTheDocument();
+ });
+
+ it('shows a generic error prompt for non-index errors', async () => {
+ const services = createStartServicesMock();
+ const dataView = createWorkflowExecutionsDataView(services.fieldFormats);
+
+ jest
+ .mocked(searchSourceInstanceMock.fetch$)
+ .mockReturnValue(throwError(() => new Error('cluster unavailable')));
+
+ render(
+ ,
+ { wrapper: getTestProvider({ services }) }
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('workflowExecutionsTableError')).toBeInTheDocument();
+ });
+ expect(screen.getByText('Failed to load executions')).toBeInTheDocument();
+ });
+});
diff --git a/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_table.tsx b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_table.tsx
new file mode 100644
index 0000000000000..37bc4d009c8b7
--- /dev/null
+++ b/src/platform/plugins/shared/workflows_management/public/pages/executions/workflow_executions_table.tsx
@@ -0,0 +1,360 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import {
+ EuiButtonEmpty,
+ EuiCallOut,
+ EuiEmptyPrompt,
+ EuiPanel,
+ EuiSkeletonText,
+ EuiTablePagination,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { take } from 'rxjs';
+import { CellActionsProvider } from '@kbn/cell-actions';
+import { SortDirection } from '@kbn/data-plugin/public';
+import type { DataView } from '@kbn/data-views-plugin/common';
+import { buildDataTableRecordList } from '@kbn/discover-utils';
+import type { DataTableRecord, EsHitRecord } from '@kbn/discover-utils/types';
+import type { Filter, Query, TimeRange } from '@kbn/es-query';
+import type { ESSearchResponse, SearchHit } from '@kbn/es-types';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n-react';
+import {
+ DataLoadingState,
+ type SortOrder,
+ UnifiedDataTable,
+ type UnifiedDataTableSettings,
+} from '@kbn/unified-data-table';
+import type { EsWorkflowExecution } from '@kbn/workflows';
+import { WorkflowExecutionDetailFlyout } from './workflow_execution_detail_flyout';
+import {
+ buildWorkflowExecutionsSearchFilters,
+ getWorkflowExecutionsFetchErrorMessage,
+ isWorkflowExecutionsIndexNotFoundError,
+} from './workflow_executions_search_query';
+import { useKibana } from '../../hooks/use_kibana';
+
+const DEFAULT_COLUMNS = ['workflowId', 'status', 'id', 'triggeredBy', 'executedBy'] as const;
+const DEFAULT_PAGE_SIZE = 25;
+const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
+const DEFAULT_SORT: SortOrder[] = [['startedAt', 'desc']];
+
+const gridStyleOverride = {
+ border: 'all' as const,
+ header: 'shade' as const,
+ stripes: false,
+};
+
+const tableContainerCss = css`
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+ min-height: 0;
+`;
+
+const gridWrapperCss = css`
+ flex: 1 1 auto;
+`;
+
+export interface WorkflowExecutionsTableProps {
+ dataView: DataView;
+ query: Query;
+ filters: Filter[];
+ timeRange: TimeRange;
+ spaceId: string;
+}
+
+export const WorkflowExecutionsTable = React.memo(
+ ({ dataView, query, filters, timeRange, spaceId }) => {
+ const {
+ data: dataService,
+ fieldFormats,
+ notifications: { toasts },
+ storage,
+ theme,
+ uiActions,
+ uiSettings,
+ } = useKibana().services;
+
+ const [hits, setHits] = useState([]);
+ const [total, setTotal] = useState(0);
+ const [loadingState, setLoadingState] = useState(DataLoadingState.loading);
+ const [error, setError] = useState(null);
+
+ const [pageIndex, setPageIndex] = useState(0);
+ const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
+ const [sort, setSort] = useState(DEFAULT_SORT);
+ const [visibleColumns, setVisibleColumns] = useState(Array.from(DEFAULT_COLUMNS));
+ const [gridSettings, setGridSettings] = useState({});
+ const [expandedDoc, setExpandedDoc] = useState();
+ const [retryToken, setRetryToken] = useState(0);
+ const timeFrom = timeRange.from;
+ const timeTo = timeRange.to;
+
+ useEffect(() => {
+ let cancelled = false;
+
+ const fetchExecutions = async () => {
+ setLoadingState(DataLoadingState.loading);
+ setError(null);
+
+ try {
+ const searchSource = await dataService.search.searchSource.create();
+ const timeField = dataView.timeFieldName ?? 'startedAt';
+ const searchFilters = buildWorkflowExecutionsSearchFilters({
+ spaceId,
+ timeRange: { from: timeFrom, to: timeTo },
+ timeField,
+ userFilters: filters,
+ });
+
+ searchSource.setField('index', dataView);
+
+ if (query?.query) {
+ searchSource.setField('query', query);
+ }
+
+ searchSource.setField('filter', searchFilters);
+ searchSource.setField('from', pageIndex * pageSize);
+ searchSource.setField('size', pageSize);
+ searchSource.setField(
+ 'sort',
+ sort.map(([field, direction]) => ({
+ [field]: {
+ order: direction === 'asc' ? SortDirection.asc : SortDirection.desc,
+ },
+ }))
+ );
+ searchSource.setField('trackTotalHits', true);
+
+ const response = await searchSource.fetch$().pipe(take(1)).toPromise();
+
+ if (cancelled) {
+ return;
+ }
+
+ const rawResponse = response?.rawResponse as
+ | ESSearchResponse
+ | undefined;
+ const responseHits = (rawResponse?.hits?.hits ?? []).filter(
+ (hit: SearchHit): hit is SearchHit =>
+ hit._source != null
+ ) as unknown as EsHitRecord[];
+ const totalHits = rawResponse?.hits?.total;
+ const totalCount =
+ typeof totalHits === 'number' ? totalHits : totalHits?.value ?? responseHits.length;
+
+ setHits(responseHits);
+ setTotal(totalCount);
+ setLoadingState(DataLoadingState.loaded);
+ } catch (err) {
+ if (cancelled) {
+ return;
+ }
+
+ if (isWorkflowExecutionsIndexNotFoundError(err)) {
+ setHits([]);
+ setTotal(0);
+ setError(null);
+ setLoadingState(DataLoadingState.loaded);
+ return;
+ }
+
+ setError(getWorkflowExecutionsFetchErrorMessage());
+ setHits([]);
+ setTotal(0);
+ setLoadingState(DataLoadingState.loaded);
+ }
+ };
+
+ fetchExecutions();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [
+ dataService.search.searchSource,
+ dataView,
+ filters,
+ pageIndex,
+ pageSize,
+ query,
+ retryToken,
+ sort,
+ spaceId,
+ timeFrom,
+ timeTo,
+ ]);
+
+ useEffect(() => {
+ setPageIndex(0);
+ setExpandedDoc(undefined);
+ }, [query, filters, spaceId, timeFrom, timeTo]);
+
+ const handleRetry = useCallback(() => {
+ setRetryToken((n) => n + 1);
+ }, []);
+
+ const rows = useMemo(
+ () => buildDataTableRecordList({ records: hits, dataView }),
+ [hits, dataView]
+ );
+
+ const services = useMemo(
+ () => ({
+ theme,
+ fieldFormats,
+ uiSettings,
+ toastNotifications: toasts,
+ storage,
+ data: dataService,
+ }),
+ [dataService, fieldFormats, storage, theme, toasts, uiSettings]
+ );
+
+ const handleSort = useCallback((nextSort: string[][]) => {
+ setSort(nextSort.length === 0 ? DEFAULT_SORT : (nextSort as SortOrder[]));
+ setPageIndex(0);
+ }, []);
+
+ const handleSetColumns = useCallback((nextColumns: string[]) => {
+ setVisibleColumns(nextColumns);
+ }, []);
+
+ const handleResize = useCallback((resized: { columnId: string; width: number | undefined }) => {
+ setGridSettings((prev) => ({
+ ...prev,
+ columns: {
+ ...prev.columns,
+ [resized.columnId]: {
+ ...prev.columns?.[resized.columnId],
+ width: resized.width,
+ },
+ },
+ }));
+ }, []);
+
+ const handlePageChange = useCallback((nextPageIndex: number) => {
+ setPageIndex(nextPageIndex);
+ }, []);
+
+ const handlePageSizeChange = useCallback((nextPageSize: number) => {
+ setPageSize(nextPageSize);
+ setPageIndex(0);
+ }, []);
+
+ const handleCloseFlyout = useCallback(() => {
+ setExpandedDoc(undefined);
+ }, []);
+
+ const renderDocumentView = useCallback(
+ (hit: DataTableRecord) => (
+
+ ),
+ [handleCloseFlyout]
+ );
+
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
+
+ if (error) {
+ return (
+
+
+
+ }
+ body={{error}
}
+ actions={
+
+
+
+ }
+ />
+ );
+ }
+
+ if (loadingState === DataLoadingState.loading && rows.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ if (rows.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ );
+ }
+);
+WorkflowExecutionsTable.displayName = 'WorkflowExecutionsTable';
diff --git a/src/platform/plugins/shared/workflows_management/public/types.ts b/src/platform/plugins/shared/workflows_management/public/types.ts
index d335c27df3c88..cf9f6017ed8a6 100644
--- a/src/platform/plugins/shared/workflows_management/public/types.ts
+++ b/src/platform/plugins/shared/workflows_management/public/types.ts
@@ -24,6 +24,7 @@ import type {
TriggersAndActionsUIPublicPluginSetup,
TriggersAndActionsUIPublicPluginStart,
} from '@kbn/triggers-actions-ui-plugin/public';
+import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { WorkflowsExtensionsPublicPluginStart } from '@kbn/workflows-extensions/public';
import type {
@@ -79,6 +80,7 @@ export interface WorkflowsPublicPluginStartDependencies {
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
workflowsExtensions: WorkflowsExtensionsPublicPluginStart;
licensing: LicensingPluginStart;
+ uiActions: UiActionsStart;
cloud?: CloudStart;
}
diff --git a/src/platform/plugins/shared/workflows_management/tsconfig.json b/src/platform/plugins/shared/workflows_management/tsconfig.json
index 1adf499f9fc03..c4afd09f6e06c 100644
--- a/src/platform/plugins/shared/workflows_management/tsconfig.json
+++ b/src/platform/plugins/shared/workflows_management/tsconfig.json
@@ -110,7 +110,8 @@
"@kbn/esql-utils",
"@kbn/inbox-plugin",
"@kbn/inbox-common",
- "@kbn/datemath"
+ "@kbn/datemath",
+ "@kbn/presentation-publishing"
],
"exclude": ["target/**/*"]
}