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/**/*"] }