diff --git a/x-pack/platform/plugins/shared/streams/server/routes/internal/streams/management/route.ts b/x-pack/platform/plugins/shared/streams/server/routes/internal/streams/management/route.ts index 85cc0c8bd07ff..bd6f2369f8ca9 100644 --- a/x-pack/platform/plugins/shared/streams/server/routes/internal/streams/management/route.ts +++ b/x-pack/platform/plugins/shared/streams/server/routes/internal/streams/management/route.ts @@ -5,13 +5,7 @@ * 2.0. */ -import { - SampleDocument, - Streams, - conditionSchema, - conditionToQueryDsl, - getConditionFields, -} from '@kbn/streams-schema'; +import { Streams } from '@kbn/streams-schema'; import { z } from '@kbn/zod'; import { STREAMS_API_PRIVILEGES } from '../../../../../common/constants'; import { SecurityError } from '../../../../lib/streams/errors/security_error'; @@ -23,86 +17,6 @@ import { } from '../../../../lib/streams/stream_crud'; import { createServerRoute } from '../../../create_server_route'; -export const sampleStreamRoute = createServerRoute({ - endpoint: 'POST /internal/streams/{name}/_sample', - options: { - access: 'internal', - }, - security: { - authz: { - requiredPrivileges: [STREAMS_API_PRIVILEGES.read], - }, - }, - params: z.object({ - path: z.object({ name: z.string() }), - body: z.object({ - if: z.optional(conditionSchema), - start: z.optional(z.number()), - end: z.optional(z.number()), - size: z.optional(z.number()), - }), - }), - handler: async ({ params, request, getScopedClients }) => { - const { scopedClusterClient } = await getScopedClients({ request }); - - const { read } = await checkAccess({ name: params.path.name, scopedClusterClient }); - - if (!read) { - throw new SecurityError(`Cannot read stream ${params.path.name}, insufficient privileges`); - } - - const { if: condition, start, end, size } = params.body; - const searchBody = { - query: { - bool: { - must: [ - condition ? conditionToQueryDsl(condition) : { match_all: {} }, - { - range: { - '@timestamp': { - gte: start, - lte: end, - format: 'epoch_millis', - }, - }, - }, - ], - }, - }, - // Conditions could be using fields which are not indexed or they could use it with other types than they are eventually mapped as. - // Because of this we can't rely on mapped fields to draw a sample, instead we need to use runtime fields to simulate what happens during - // ingest in the painless condition checks. - // This is less efficient than it could be - in some cases, these fields _are_ indexed with the right type and we could use them directly. - // This can be optimized in the future. - runtime_mappings: condition - ? Object.fromEntries( - getConditionFields(condition).map((field) => [ - field.name, - { type: field.type === 'string' ? ('keyword' as const) : ('double' as const) }, - ]) - ) - : undefined, - sort: [ - { - '@timestamp': { - order: 'desc' as const, - }, - }, - ], - terminate_after: size, - track_total_hits: false, - size, - }; - const results = await scopedClusterClient.asCurrentUser.search({ - index: params.path.name, - allow_no_indices: true, - ...searchBody, - }); - - return { documents: results.hits.hits.map((hit) => hit._source) as SampleDocument[] }; - }, -}); - export const unmanagedAssetDetailsRoute = createServerRoute({ endpoint: 'GET /internal/streams/{name}/_unmanaged_assets', options: { @@ -148,6 +62,5 @@ export const unmanagedAssetDetailsRoute = createServerRoute({ }); export const internalManagementRoutes = { - ...sampleStreamRoute, ...unmanagedAssetDetailsRoute, }; diff --git a/x-pack/platform/plugins/shared/streams/server/routes/internal/streams/processing/simulation_handler.ts b/x-pack/platform/plugins/shared/streams/server/routes/internal/streams/processing/simulation_handler.ts index 2db375e257a13..3bace1f84a0ee 100644 --- a/x-pack/platform/plugins/shared/streams/server/routes/internal/streams/processing/simulation_handler.ts +++ b/x-pack/platform/plugins/shared/streams/server/routes/internal/streams/processing/simulation_handler.ts @@ -535,9 +535,9 @@ const extractProcessorMetrics = ({ return { detected_fields, errors, - failed_rate: parseFloat(failureRate.toFixed(2)), - skipped_rate: parseFloat(skippedRate.toFixed(2)), - parsed_rate: parseFloat(parsedRate.toFixed(2)), + failed_rate: parseFloat(failureRate.toFixed(3)), + skipped_rate: parseFloat(skippedRate.toFixed(3)), + parsed_rate: parseFloat(parsedRate.toFixed(3)), }; }); }; @@ -718,10 +718,10 @@ const prepareSimulationResponse = async ( documents: docReports, processors_metrics: processorsMetrics, documents_metrics: { - failed_rate: parseFloat(failureRate.toFixed(2)), - partially_parsed_rate: parseFloat(partiallyParsedRate.toFixed(2)), - skipped_rate: parseFloat(skippedRate.toFixed(2)), - parsed_rate: parseFloat(parsedRate.toFixed(2)), + failed_rate: parseFloat(failureRate.toFixed(3)), + partially_parsed_rate: parseFloat(partiallyParsedRate.toFixed(3)), + skipped_rate: parseFloat(skippedRate.toFixed(3)), + parsed_rate: parseFloat(parsedRate.toFixed(3)), }, is_non_additive_simulation: isNotAdditiveSimulation, }; diff --git a/x-pack/platform/plugins/shared/streams_app/common/url_schema/common.ts b/x-pack/platform/plugins/shared/streams_app/common/url_schema/common.ts new file mode 100644 index 0000000000000..c774158faa168 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/common/url_schema/common.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const ENRICHMENT_URL_STATE_KEY = 'pageState'; diff --git a/x-pack/platform/plugins/shared/streams_app/common/url_schema/enrichment_url_schema.ts b/x-pack/platform/plugins/shared/streams_app/common/url_schema/enrichment_url_schema.ts new file mode 100644 index 0000000000000..4b3029224336a --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/common/url_schema/enrichment_url_schema.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Filter, TimeRange } from '@kbn/es-query'; +import { SampleDocument, sampleDocument } from '@kbn/streams-schema/src/shared/record_types'; +import { z } from '@kbn/zod'; + +/** + * Base interface for all data source types with common properties + */ +export interface BaseDataSource { + enabled: boolean; + name?: string; +} + +/** + * Base schema for common data source properties + */ +const baseDataSourceSchema = z.object({ + enabled: z.boolean(), + name: z.string().optional(), +}) satisfies z.Schema; + +/** + * Random samples data source that retrieves data from the stream index + */ +export interface RandomSamplesDataSource extends BaseDataSource { + type: 'random-samples'; +} + +const randomSamplesDataSourceSchema = baseDataSourceSchema.extend({ + type: z.literal('random-samples'), +}) satisfies z.Schema; + +/** + * KQL samples data source that retrieves data based on KQL query + */ +export interface KqlSamplesDataSource extends BaseDataSource { + type: 'kql-samples'; + query: { + language: string; + query: string; + }; + filters?: Filter[]; + timeRange: TimeRange; +} + +const kqlSamplesDataSourceSchema = baseDataSourceSchema.extend({ + type: z.literal('kql-samples'), + query: z.object({ + language: z.string(), + query: z.string(), + }), + filters: z.array(z.any()).optional(), + timeRange: z.object({ + from: z.string(), + to: z.string(), + }), +}) satisfies z.Schema; + +/** + * Custom samples data source with user-provided documents + */ +export interface CustomSamplesDataSource extends BaseDataSource { + type: 'custom-samples'; + documents: SampleDocument[]; +} + +export const customSamplesDataSourceDocumentsSchema = z.array(sampleDocument); + +export const customSamplesDataSourceSchema = baseDataSourceSchema.extend({ + type: z.literal('custom-samples'), + documents: customSamplesDataSourceDocumentsSchema, +}) satisfies z.Schema; + +/** + * Union type of all possible data source types + */ +export type EnrichmentDataSource = + | RandomSamplesDataSource + | KqlSamplesDataSource + | CustomSamplesDataSource; + +/** + * Schema for validating enrichment data sources + */ +const enrichmentDataSourceSchema = z.union([ + randomSamplesDataSourceSchema, + kqlSamplesDataSourceSchema, + customSamplesDataSourceSchema, +]) satisfies z.Schema; + +/** + * URL state for enrichment configuration + */ +export interface EnrichmentUrlState { + v: 1; + dataSources: EnrichmentDataSource[]; +} + +/** + * Schema for validating enrichment URL state + */ +export const enrichmentUrlSchema = z.object({ + v: z.literal(1), + dataSources: z.array(enrichmentDataSourceSchema), +}) satisfies z.Schema; diff --git a/x-pack/platform/plugins/shared/streams_app/common/url_schema/index.ts b/x-pack/platform/plugins/shared/streams_app/common/url_schema/index.ts new file mode 100644 index 0000000000000..4f523f7b4c380 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/common/url_schema/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ENRICHMENT_URL_STATE_KEY } from './common'; +export * from './enrichment_url_schema'; diff --git a/x-pack/platform/plugins/shared/streams_app/kibana.jsonc b/x-pack/platform/plugins/shared/streams_app/kibana.jsonc index 8242467462580..fcbc0f273a318 100644 --- a/x-pack/platform/plugins/shared/streams_app/kibana.jsonc +++ b/x-pack/platform/plugins/shared/streams_app/kibana.jsonc @@ -28,6 +28,6 @@ "streams", "unifiedSearch" ], - "requiredBundles": ["kibanaReact"] + "requiredBundles": ["kibanaReact", "kibanaUtils"] } } diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/app_root/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/app_root/index.tsx index 8290edac235d2..0fbcfca6c49ed 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/app_root/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/app_root/index.tsx @@ -17,6 +17,7 @@ import { StreamsAppContextProvider } from '../streams_app_context_provider'; import { streamsAppRouter } from '../../routes/config'; import { StreamsAppStartDependencies } from '../../types'; import { StreamsAppServices } from '../../services/types'; +import { KbnUrlStateStorageFromRouterProvider } from '../../util/kbn_url_state_context'; export function AppRoot({ coreStart, @@ -46,9 +47,11 @@ export function AppRoot({ - - - + + + + + diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/preview_table/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/preview_table/index.tsx index 0ed80dd75b3a7..298469fbe41c5 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/preview_table/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/preview_table/index.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiDataGrid, EuiDataGridRowHeightsOptions } from '@elastic/eui'; +import { EuiDataGrid, EuiDataGridProps, EuiDataGridRowHeightsOptions } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SampleDocument } from '@kbn/streams-schema'; import React, { useMemo } from 'react'; @@ -12,6 +12,7 @@ import React, { useMemo } from 'react'; export function PreviewTable({ documents, displayColumns, + height, renderCellValue, rowHeightsOptions, toolbarVisibility = false, @@ -20,6 +21,7 @@ export function PreviewTable({ }: { documents: SampleDocument[]; displayColumns?: string[]; + height?: EuiDataGridProps['height']; renderCellValue?: (doc: SampleDocument, columnId: string) => React.ReactNode | undefined; rowHeightsOptions?: EuiDataGridRowHeightsOptions; toolbarVisibility?: boolean; @@ -97,6 +99,7 @@ export function PreviewTable({ setVisibleColumns: setVisibleColumns || (() => {}), canDragAndDropColumns: false, }} + height={height} toolbarVisibility={toolbarVisibility} rowCount={documents.length} rowHeightsOptions={rowHeightsOptions} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/add_data_sources_context_menu.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/add_data_sources_context_menu.tsx new file mode 100644 index 0000000000000..175a6aa640ee5 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/add_data_sources_context_menu.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton, EuiContextMenu, EuiPopover } from '@elastic/eui'; +import { useBoolean } from '@kbn/react-hooks'; +import { DATA_SOURCES_I18N } from './translations'; +import { + defaultCustomSamplesDataSource, + defaultKqlSamplesDataSource, +} from '../state_management/stream_enrichment_state_machine/utils'; +import { useStreamEnrichmentEvents } from '../state_management/stream_enrichment_state_machine'; + +export const AddDataSourcesContextMenu = () => { + const { addDataSource } = useStreamEnrichmentEvents(); + + const [isOpen, { toggle: toggleMenu, off: closeMenu }] = useBoolean(); + + return ( + + {DATA_SOURCES_I18N.contextMenu.addDataSource} + + } + isOpen={isOpen} + closePopover={closeMenu} + panelPaddingSize="none" + anchorPosition="downLeft" + > + { + addDataSource(defaultKqlSamplesDataSource); + closeMenu(); + }, + }, + { + name: DATA_SOURCES_I18N.contextMenu.addCustomSamples, + icon: 'visText', + onClick: () => { + addDataSource(defaultCustomSamplesDataSource); + closeMenu(); + }, + }, + ], + }, + ]} + /> + + ); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/custom_samples_data_source_card.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/custom_samples_data_source_card.tsx new file mode 100644 index 0000000000000..683ef2996395c --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/custom_samples_data_source_card.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiCallOut, EuiSpacer, EuiFormRow } from '@elastic/eui'; +import { CodeEditor } from '@kbn/code-editor'; +import { isSchema } from '@kbn/streams-schema'; +import { customSamplesDataSourceDocumentsSchema } from '../../../../../common/url_schema'; +import { + DataSourceActorRef, + useDataSourceSelector, +} from '../state_management/data_source_state_machine'; +import { CustomSamplesDataSourceWithUIAttributes } from '../types'; +import { deserializeJson, serializeXJson } from '../helpers'; +import { DataSourceCard } from './data_source_card'; +import { NameField } from './name_field'; +import { DATA_SOURCES_I18N } from './translations'; + +interface CustomSamplesDataSourceCardProps { + readonly dataSourceRef: DataSourceActorRef; +} + +export const CustomSamplesDataSourceCard = ({ + dataSourceRef, +}: CustomSamplesDataSourceCardProps) => { + const dataSource = useDataSourceSelector( + dataSourceRef, + (snapshot) => snapshot.context.dataSource as CustomSamplesDataSourceWithUIAttributes + ); + + const isDisabled = useDataSourceSelector(dataSourceRef, (snapshot) => + snapshot.matches('disabled') + ); + + const handleChange = (params: Partial) => { + dataSourceRef.send({ type: 'dataSource.change', dataSource: { ...dataSource, ...params } }); + }; + + const editorValue = useMemo( + () => serializeXJson(dataSource.documents, '[]'), + [dataSource.documents] + ); + const handleEditorChange = (value: string) => { + const documents = deserializeJson(value); + if (isSchema(customSamplesDataSourceDocumentsSchema, documents)) { + handleChange({ documents }); + } + }; + + return ( + + + + handleChange({ name: event.target.value })} + value={dataSource.name} + disabled={isDisabled} + /> + + + + + + ); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/data_source.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/data_source.tsx new file mode 100644 index 0000000000000..2d7405421d9a4 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/data_source.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + DataSourceActorRef, + useDataSourceSelector, +} from '../state_management/data_source_state_machine'; +import { RandomSamplesDataSourceCard } from './random_samples_data_source_card'; +import { KqlSamplesDataSourceCard } from './kql_samples_data_source_card'; +import { CustomSamplesDataSourceCard } from './custom_samples_data_source_card'; + +interface DataSourceProps { + readonly dataSourceRef: DataSourceActorRef; +} + +export const DataSource = ({ dataSourceRef }: DataSourceProps) => { + const dataSourceType = useDataSourceSelector( + dataSourceRef, + (snapshot) => snapshot.context.dataSource.type + ); + + switch (dataSourceType) { + case 'random-samples': + return ; + case 'kql-samples': + return ; + case 'custom-samples': + return ; + default: + return null; + } +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/data_source_card.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/data_source_card.tsx new file mode 100644 index 0000000000000..651ae3b5bcfca --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/data_source_card.tsx @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { PropsWithChildren } from 'react'; +import { + EuiCheckableCard, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiBadge, + EuiButtonIcon, + EuiText, + EuiAccordion, + EuiSpacer, + EuiProgress, + EuiEmptyPrompt, +} from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import { flattenObjectNestedLast } from '@kbn/object-utils'; +import { FlattenRecord } from '@kbn/streams-schema'; +import { useDiscardConfirm } from '../../../../hooks/use_discard_confirm'; +import { + DataSourceActorRef, + useDataSourceSelector, +} from '../state_management/data_source_state_machine'; +import { AssetImage } from '../../../asset_image'; +import { PreviewTable } from '../../preview_table'; +import { DATA_SOURCES_I18N } from './translations'; + +interface DataSourceCardProps { + readonly dataSourceRef: DataSourceActorRef; + readonly title?: string; + readonly subtitle?: string; + readonly isPreviewVisible?: boolean; +} + +export const DataSourceCard = ({ + children, + dataSourceRef, + title, + subtitle, + isPreviewVisible, +}: PropsWithChildren) => { + const dataSourceState = useDataSourceSelector(dataSourceRef, (snapshot) => snapshot); + + const { data: previewDocs, dataSource } = dataSourceState.context; + + const canDeleteDataSource = dataSourceState.can({ type: 'dataSource.delete' }); + const isEnabled = dataSourceState.matches('enabled'); + const isLoading = + dataSourceState.matches({ enabled: 'loadingData' }) || + dataSourceState.matches({ enabled: 'debouncingChanges' }); + + const toggleActivity = () => dataSourceRef.send({ type: 'dataSource.toggleActivity' }); + + const deleteDataSource = useDiscardConfirm( + () => dataSourceRef.send({ type: 'dataSource.delete' }), + { + title: DATA_SOURCES_I18N.dataSourceCard.delete.title, + message: DATA_SOURCES_I18N.dataSourceCard.delete.message, + cancelButtonText: DATA_SOURCES_I18N.dataSourceCard.delete.cancelButtonText, + confirmButtonText: DATA_SOURCES_I18N.dataSourceCard.delete.confirmButtonText, + } + ); + + return ( + + + +

{title ?? dataSource.type}

+
+ + + {isEnabled + ? DATA_SOURCES_I18N.dataSourceCard.enabled + : DATA_SOURCES_I18N.dataSourceCard.disabled} + + + + {canDeleteDataSource && ( + + )} +
+ + {subtitle} + + + } + checkableType="checkbox" + onChange={toggleActivity} + checked={isEnabled} + > + {children} + + + {isLoading && } + {isEmpty(previewDocs) ? ( + } + titleSize="xs" + title={

{DATA_SOURCES_I18N.dataSourceCard.noDataPreview}

} + /> + ) : ( + + )} +
+
+ ); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/index.tsx new file mode 100644 index 0000000000000..2650de8736f6c --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/index.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiFlyoutResizable, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiSpacer, + EuiText, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { useStreamEnrichmentSelector } from '../state_management/stream_enrichment_state_machine'; +import { DATA_SOURCES_I18N } from './translations'; +import { AddDataSourcesContextMenu } from './add_data_sources_context_menu'; +import { DataSource } from './data_source'; + +interface DataSourcesFlyoutProps { + onClose: () => void; +} + +export const DataSourcesFlyout = ({ onClose }: DataSourcesFlyoutProps) => { + const dataSourcesActorRefs = useStreamEnrichmentSelector( + (snapshot) => snapshot.context.dataSourcesRefs + ); + + return ( + + + +

{DATA_SOURCES_I18N.flyout.title}

+
+ + + {DATA_SOURCES_I18N.flyout.subtitle} + +
+ + } + > + + +

{DATA_SOURCES_I18N.flyout.infoTitle}

+
+ +
+ + + {dataSourcesActorRefs.map((dataSourceRef) => ( + + + + ))} + +
+
+ ); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/kql_samples_data_source_card.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/kql_samples_data_source_card.tsx new file mode 100644 index 0000000000000..355101a5007a0 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/kql_samples_data_source_card.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import useAsync from 'react-use/lib/useAsync'; +import { Query, TimeRange } from '@kbn/es-query'; +import { useKibana } from '../../../../hooks/use_kibana'; +import { useStreamEnrichmentSelector } from '../state_management/stream_enrichment_state_machine'; +import { + DataSourceActorRef, + useDataSourceSelector, +} from '../state_management/data_source_state_machine'; +import { KqlSamplesDataSourceWithUIAttributes } from '../types'; +import { UncontrolledStreamsAppSearchBar } from '../../../streams_app_search_bar/uncontrolled_streams_app_bar'; +import { DataSourceCard } from './data_source_card'; +import { NameField } from './name_field'; +import { DATA_SOURCES_I18N } from './translations'; + +interface KqlSamplesDataSourceCardProps { + readonly dataSourceRef: DataSourceActorRef; +} + +export const KqlSamplesDataSourceCard = ({ dataSourceRef }: KqlSamplesDataSourceCardProps) => { + const { data } = useKibana().dependencies.start; + + const definition = useStreamEnrichmentSelector((state) => state.context.definition); + const dataSource = useDataSourceSelector( + dataSourceRef, + (snapshot) => snapshot.context.dataSource as KqlSamplesDataSourceWithUIAttributes + ); + + const isDisabled = useDataSourceSelector(dataSourceRef, (snapshot) => + snapshot.matches('disabled') + ); + + const { value: streamDataView } = useAsync(() => + data.dataViews.create({ + title: definition.stream.name, + timeFieldName: '@timestamp', + }) + ); + + const handleChange = (params: Partial) => { + dataSourceRef.send({ type: 'dataSource.change', dataSource: { ...dataSource, ...params } }); + }; + + const handleQueryChange = ({ query, dateRange }: { query?: Query; dateRange: TimeRange }) => + handleChange({ + query: query as KqlSamplesDataSourceWithUIAttributes['query'], + timeRange: dateRange, + }); + + return ( + + handleChange({ name: event.target.value })} + value={dataSource.name} + disabled={isDisabled} + /> + + {streamDataView && ( + <> + handleChange({ filters })} + onQuerySubmit={handleQueryChange} + query={dataSource.query} + showDatePicker + showFilterBar + showQueryInput + /> + + + )} + + ); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/name_field.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/name_field.tsx new file mode 100644 index 0000000000000..4a8c60f2d0f8a --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/name_field.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFormRow, EuiFieldText, EuiFieldTextProps } from '@elastic/eui'; +import { DATA_SOURCES_I18N } from './translations'; + +export const NameField = (props: Omit) => { + return ( + + + + ); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/random_samples_data_source_card.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/random_samples_data_source_card.tsx new file mode 100644 index 0000000000000..f6dc968fba02d --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/random_samples_data_source_card.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { DataSourceActorRef } from '../state_management/data_source_state_machine'; +import { DataSourceCard } from './data_source_card'; +import { DATA_SOURCES_I18N } from './translations'; + +interface RandomSamplesDataSourceCardProps { + readonly dataSourceRef: DataSourceActorRef; +} + +export const RandomSamplesDataSourceCard = ({ + dataSourceRef, +}: RandomSamplesDataSourceCardProps) => { + return ( + + + + + ); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/translations.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/translations.tsx new file mode 100644 index 0000000000000..ed3e69b9ed71f --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_flyout/translations.tsx @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiCode } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; + +export const DATA_SOURCES_I18N = { + flyout: { + title: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.title', + { defaultMessage: 'Manage simulation data sources' } + ), + subtitle: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.subtitle', + { + defaultMessage: + 'Configure data sources for simulation and testing. Each data source provides sample data for your analysis.', + } + ), + infoDescription: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.infoDescription', + { + defaultMessage: + 'Active data sources will be used for simulation. You can toggle data sources on/off without removing them.', + } + ), + infoTitle: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.infoTitle', + { defaultMessage: 'Data sources' } + ), + }, + contextMenu: { + addDataSource: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.addDataSource.menu', + { defaultMessage: 'Add data source' } + ), + addKqlDataSource: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.addDataSource.menu.addKqlDataSource', + { defaultMessage: 'Add KQL search samples' } + ), + addCustomSamples: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.addDataSource.menu.addCustomSamples', + { defaultMessage: 'Add custom docs samples' } + ), + }, + dataSourceCard: { + enabled: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.dataSourceCard.enabled', + { defaultMessage: 'Enabled' } + ), + disabled: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.dataSourceCard.disabled', + { defaultMessage: 'Disabled' } + ), + deleteDataSourceLabel: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.dataSourceCard.deleteDataSourceLabel', + { defaultMessage: 'Delete data source' } + ), + dataPreviewAccordionLabel: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.dataSourceCard.dataPreviewAccordion.label', + { defaultMessage: 'Data preview' } + ), + noDataPreview: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.dataSourceCard.dataPreviewAccordion.noData', + { defaultMessage: 'No documents to preview available' } + ), + delete: { + title: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.dataSourceCard.delete.title', + { defaultMessage: 'Remove sample data source?' } + ), + message: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.dataSourceCard.delete.message', + { defaultMessage: 'Removed sample data source will need to be reconfigured.' } + ), + cancelButtonText: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.dataSourceCard.delete.cancelButtonText', + { defaultMessage: 'Cancel' } + ), + confirmButtonText: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.dataSourceCard.delete.confirmButtonText', + { defaultMessage: 'Delete' } + ), + }, + }, + randomSamples: { + name: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSources.randomSamples.name', + { defaultMessage: 'Random samples from stream' } + ), + subtitle: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSources.randomSamples.subtitle', + { defaultMessage: 'Automatically samples random data from the stream.' } + ), + callout: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSources.randomSamples.callout', + { + defaultMessage: + 'The random samples data source cannot be deleted to guarantee available samples for the simulation. You can still disable it if you want to focus on samples from other data sources.', + } + ), + }, + kqlDataSource: { + defaultName: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.kqlDataSource.defaultName', + { defaultMessage: 'KQL search samples' } + ), + subtitle: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.kqlDataSource.subtitle', + { defaultMessage: 'Sample data using KQL query syntax.' } + ), + }, + customSamples: { + defaultName: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.customSamples.defaultName', + { defaultMessage: 'Custom samples' } + ), + subtitle: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.customSamples.subtitle', + { defaultMessage: 'Manually defined sample documents.' } + ), + callout: i18n.translate('xpack.streams.enrichment.dataSources.customSamples.callout', { + defaultMessage: + 'The custom samples will not be persisted. They will be lost when you leave the processing page.', + }), + label: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.customSamples.label', + { defaultMessage: 'Documents' } + ), + helpText: ( + + {JSON.stringify([ + { '@timestamp': '2025-06-17T12:00:00Z', message: 'Sample log message' }, + ])} + + ), + }} + /> + ), + }, + nameField: { + label: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.nameField.label', + { defaultMessage: 'Name' } + ), + helpText: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.dataSourcesFlyout.nameField.helpText', + { defaultMessage: 'Describe what samples the data source loads.' } + ), + }, +} as const; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_list.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_list.tsx new file mode 100644 index 0000000000000..0afc40b55a08c --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/data_sources_list.tsx @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiCheckbox, + EuiPanel, + EuiText, + EuiToolTip, + EuiFlexGroup, + EuiFlexItem, + EuiNotificationBadge, + EuiButtonEmpty, + useEuiTheme, + EuiButtonIcon, +} from '@elastic/eui'; +import { dynamic } from '@kbn/shared-ux-utility'; +import { css } from '@emotion/react'; +import { + useStreamEnrichmentEvents, + useStreamEnrichmentSelector, +} from './state_management/stream_enrichment_state_machine'; +import { + DataSourceActorRef, + useDataSourceSelector, +} from './state_management/data_source_state_machine'; + +const DataSourcesFlyout = dynamic(() => + import('./data_sources_flyout').then((mod) => ({ default: mod.DataSourcesFlyout })) +); + +const VISIBLE_DATA_SOURCES_LIMIT = 2; +const DATA_SOURCE_CARD_MAX_WIDTH = 160; + +const manageDataSourcesLabel = i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.openDataSourcesManagement.label', + { defaultMessage: 'Manage data sources' } +); + +export const DataSourcesList = () => { + const { closeDataSourcesManagement, openDataSourcesManagement } = useStreamEnrichmentEvents(); + + const isManagingDataSources = useStreamEnrichmentSelector((state) => + state.matches({ ready: { enrichment: { managingDataSources: 'open' } } }) + ); + const dataSourcesRefs = useStreamEnrichmentSelector((state) => state.context.dataSourcesRefs); + + const visibleDataSourcesRefs = dataSourcesRefs.slice(0, VISIBLE_DATA_SOURCES_LIMIT); + const hiddenDataSourcesRefs = dataSourcesRefs.slice(VISIBLE_DATA_SOURCES_LIMIT); + const hasHiddenDataSources = hiddenDataSourcesRefs.length > 0; + + return ( + + {visibleDataSourcesRefs.map((dataSourceRef) => ( + + + + ))} + + + + {hasHiddenDataSources ? ( + + + +{hiddenDataSourcesRefs.length} + + + ) : ( + + )} + + + + {isManagingDataSources && } + + ); +}; + +interface DataSourceListItemProps { + readonly dataSourceRef: DataSourceActorRef; +} + +const DataSourceListItem = ({ dataSourceRef }: DataSourceListItemProps) => { + const { euiTheme } = useEuiTheme(); + const dataSourceState = useDataSourceSelector(dataSourceRef, (snapshot) => snapshot); + + const isEnabled = dataSourceState.matches('enabled'); + const toggleActivity = () => { + dataSourceRef.send({ type: 'dataSource.toggleActivity' }); + }; + + const content = ( + + + {dataSourceState.context.dataSource.name || dataSourceState.context.dataSource.type} + + + ({dataSourceState.context.data.length}) + + + ); + + return ( + + + + ); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/detected_fields_editor.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/detected_fields_editor.tsx index 5c23d2b177fe7..c595dd90044a5 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/detected_fields_editor.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/detected_fields_editor.tsx @@ -13,17 +13,21 @@ import { Streams } from '@kbn/streams-schema'; import { AssetImage } from '../../asset_image'; import { SchemaEditor } from '../schema_editor'; import { SchemaField } from '../schema_editor/types'; -import { useStreamEnrichmentEvents } from './state_management/stream_enrichment_state_machine'; +import { + useStreamEnrichmentEvents, + useStreamEnrichmentSelector, +} from './state_management/stream_enrichment_state_machine'; interface DetectedFieldsEditorProps { - definition: Streams.ingest.all.GetResponse; detectedFields: SchemaField[]; } -export const DetectedFieldsEditor = ({ definition, detectedFields }: DetectedFieldsEditorProps) => { +export const DetectedFieldsEditor = ({ detectedFields }: DetectedFieldsEditorProps) => { const { euiTheme } = useEuiTheme(); const { mapField, unmapField } = useStreamEnrichmentEvents(); + + const definition = useStreamEnrichmentSelector((state) => state.context.definition); const isWiredStream = Streams.WiredStream.GetResponse.is(definition); const hasFields = detectedFields.length > 0; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/page_content.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/page_content.tsx index 2fdb015791fd8..125ef71a85427 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/page_content.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/page_content.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { DragDropContextProps, EuiAccordion, @@ -25,8 +25,7 @@ import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt'; import { css } from '@emotion/react'; import { isEmpty } from 'lodash'; import { FormattedMessage } from '@kbn/i18n-react'; -import { BehaviorSubject } from 'rxjs'; -import { useTimefilter } from '../../../hooks/use_timefilter'; +import { useKbnUrlStateStorageFromRouterContext } from '../../../util/kbn_url_state_context'; import { useKibana } from '../../../hooks/use_kibana'; import { DraggableProcessorListItem } from './processors_list'; import { SortableList } from './sortable_list'; @@ -37,7 +36,7 @@ import { StreamEnrichmentContextProvider, useSimulatorSelector, useStreamEnrichmentEvents, - useStreamsEnrichmentSelector, + useStreamEnrichmentSelector, } from './state_management/stream_enrichment_state_machine'; const MemoSimulationPlayground = React.memo(SimulationPlayground); @@ -50,35 +49,20 @@ interface StreamDetailEnrichmentContentProps { export function StreamDetailEnrichmentContent(props: StreamDetailEnrichmentContentProps) { const { core, dependencies } = useKibana(); const { + data, streams: { streamsRepositoryClient }, } = dependencies.start; - const timefilterHook = useTimefilter(); - - const timeState$ = useMemo(() => { - const subject = new BehaviorSubject(timefilterHook.timeState); - return subject; - // No need to ever recreate this observable, as we subscribe to it in the - // useEffect below. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - const subscription = timefilterHook.timeState$.subscribe((value) => - timeState$.next(value.timeState) - ); - return () => { - subscription.unsubscribe(); - }; - }, [timeState$, timefilterHook.timeState$]); + const urlStateStorageContainer = useKbnUrlStateStorageFromRouterContext(); return ( @@ -90,18 +74,15 @@ export function StreamDetailEnrichmentContentImpl() { const { resetChanges, saveChanges } = useStreamEnrichmentEvents(); - const hasChanges = useStreamsEnrichmentSelector((state) => state.can({ type: 'stream.update' })); - const canManage = useStreamsEnrichmentSelector( + const isReady = useStreamEnrichmentSelector((state) => state.matches('ready')); + const hasChanges = useStreamEnrichmentSelector((state) => state.can({ type: 'stream.update' })); + const canManage = useStreamEnrichmentSelector( (state) => state.context.definition.privileges.manage ); - const isSavingChanges = useStreamsEnrichmentSelector((state) => + const isSavingChanges = useStreamEnrichmentSelector((state) => state.matches({ ready: { stream: 'updating' } }) ); - const isInitializing = useStreamsEnrichmentSelector((state) => { - return !state.matches('ready'); - }); - useUnsavedChangesPrompt({ hasUnsavedChanges: hasChanges, history: appParams.history, @@ -110,7 +91,7 @@ export function StreamDetailEnrichmentContentImpl() { openConfirm: core.overlays.openConfirm, }); - if (isInitializing) { + if (!isReady) { return null; } @@ -166,9 +147,9 @@ const ProcessorsEditor = React.memo(() => { const { euiTheme } = useEuiTheme(); const { reorderProcessors } = useStreamEnrichmentEvents(); - const definition = useStreamsEnrichmentSelector((state) => state.context.definition); + const definition = useStreamEnrichmentSelector((state) => state.context.definition); - const processorsRefs = useStreamsEnrichmentSelector((state) => + const processorsRefs = useStreamEnrichmentSelector((state) => state.context.processorsRefs.filter((processorRef) => processorRef.getSnapshot().matches('configured') ) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processor_outcome_preview.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processor_outcome_preview.tsx index ea825fa8194f3..95ba80d333bb2 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processor_outcome_preview.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processor_outcome_preview.tsx @@ -7,25 +7,24 @@ import React, { useMemo } from 'react'; import { - EuiFlexGroup, EuiFilterButton, EuiFilterGroup, EuiEmptyPrompt, - EuiFlexItem, EuiSpacer, EuiProgress, + EuiFlexItem, + EuiFlexGroup, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import { Sample } from '@kbn/grok-ui'; import { GrokProcessorDefinition } from '@kbn/streams-schema'; -import { StreamsAppSearchBar } from '../../streams_app_search_bar'; import { PreviewTable } from '../preview_table'; import { AssetImage } from '../../asset_image'; import { useSimulatorSelector, useStreamEnrichmentEvents, - useStreamsEnrichmentSelector, + useStreamEnrichmentSelector, } from './state_management/stream_enrichment_state_machine'; import { PreviewDocsFilterOption, @@ -39,16 +38,13 @@ import { WithUIAttributes } from './types'; export const ProcessorOutcomePreview = () => { const isLoading = useSimulatorSelector( - (state) => - state.matches('debouncingChanges') || - state.matches('loadingSamples') || - state.matches('runningSimulation') + (state) => state.matches('debouncingChanges') || state.matches('runningSimulation') ); return ( <> - + @@ -56,15 +52,16 @@ export const ProcessorOutcomePreview = () => { ); }; + const formatter = new Intl.NumberFormat('en-US', { style: 'percent', - maximumFractionDigits: 0, + maximumFractionDigits: 1, }); const formatRateToPercentage = (rate?: number) => (rate ? formatter.format(rate) : undefined) as any; // This is a workaround for the type error, since the numFilters & numActiveFilters props are defined as number | undefined -const OutcomeControls = () => { +const PreviewDocumentsGroupBy = () => { const { changePreviewDocsFilter } = useStreamEnrichmentEvents(); const previewDocsFilter = useSimulatorSelector((state) => state.context.previewDocsFilter); @@ -134,7 +131,6 @@ const OutcomeControls = () => { {previewDocsFilterOptions.outcome_filter_failed.label} - ); }; @@ -171,11 +167,11 @@ const OutcomePreviewTable = () => { return Array.from(fields); }, [previewDocuments]); - const draftProcessor = useStreamsEnrichmentSelector((snapshot) => + const draftProcessor = useStreamEnrichmentSelector((snapshot) => selectDraftProcessor(snapshot.context) ); - const grokCollection = useStreamsEnrichmentSelector( + const grokCollection = useStreamEnrichmentSelector( (machineState) => machineState.context.grokCollection ); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/date/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/date/index.tsx index 10c87b769f100..c930d2c8b6a49 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/date/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/date/index.tsx @@ -28,7 +28,7 @@ import { DateFormatsField } from './date_formats_field'; import { selectPreviewDocuments } from '../../state_management/simulation_state_machine/selectors'; import { - useStreamsEnrichmentSelector, + useStreamEnrichmentSelector, useSimulatorSelector, } from '../../state_management/stream_enrichment_state_machine'; import { DateFormState } from '../../types'; @@ -43,7 +43,7 @@ export const DateProcessorForm = () => { const form = useFormContext(); - const definition = useStreamsEnrichmentSelector((snapshot) => snapshot.context.definition); + const definition = useStreamEnrichmentSelector((snapshot) => snapshot.context.definition); const previewDocuments = useSimulatorSelector((snapshot) => selectPreviewDocuments(snapshot.context) ); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/grok/grok_custom_sample_data.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/grok/grok_custom_sample_data.tsx index 37161023c15f6..e4c0cae3817ff 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/grok/grok_custom_sample_data.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/grok/grok_custom_sample_data.tsx @@ -9,10 +9,10 @@ import { DraftGrokExpression, SampleInput } from '@kbn/grok-ui'; import React, { useState } from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; import { GrokFormState } from '../../types'; -import { useStreamsEnrichmentSelector } from '../../state_management/stream_enrichment_state_machine'; +import { useStreamEnrichmentSelector } from '../../state_management/stream_enrichment_state_machine'; export const CustomSampleData = () => { - const grokCollection = useStreamsEnrichmentSelector( + const grokCollection = useStreamEnrichmentSelector( (machineState) => machineState.context.grokCollection ); const [sample, setSample] = useState(''); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/grok/grok_patterns_editor.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/grok/grok_patterns_editor.tsx index 9b10a81528d1a..66b1277ff9190 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/grok/grok_patterns_editor.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/grok/grok_patterns_editor.tsx @@ -34,7 +34,7 @@ import { DraftGrokExpression, GrokCollection } from '@kbn/grok-ui'; import { Expression } from '@kbn/grok-ui'; import useDebounce from 'react-use/lib/useDebounce'; import useObservable from 'react-use/lib/useObservable'; -import { useStreamsEnrichmentSelector } from '../../state_management/stream_enrichment_state_machine'; +import { useStreamEnrichmentSelector } from '../../state_management/stream_enrichment_state_machine'; import { SortableList } from '../../sortable_list'; import { GrokPatternSuggestion } from './grok_pattern_suggestion'; import { GeneratePatternButton, AdditionalChargesCallout } from './generate_pattern_button'; @@ -52,7 +52,7 @@ export const GrokPatternsEditor = () => { setValue, } = useFormContext(); - const grokCollection = useStreamsEnrichmentSelector( + const grokCollection = useStreamEnrichmentSelector( (machineState) => machineState.context.grokCollection ); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/index.tsx index 3b9bf2e47fd65..0aea77d14d3b3 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/index.tsx @@ -43,7 +43,7 @@ import { import { ProcessorErrors, ProcessorMetricBadges } from './processor_metrics'; import { useStreamEnrichmentEvents, - useStreamsEnrichmentSelector, + useStreamEnrichmentSelector, useSimulatorSelector, StreamEnrichmentContextType, useGetStreamEnrichmentState, @@ -60,7 +60,7 @@ export function AddProcessorPanel() { const { addProcessor } = useStreamEnrichmentEvents(); - const processorRef = useStreamsEnrichmentSelector((state) => + const processorRef = useStreamEnrichmentSelector((state) => state.context.processorsRefs.find((p) => p.getSnapshot().matches('draft')) ); const processorMetrics = useSimulatorSelector( @@ -68,7 +68,7 @@ export function AddProcessorPanel() { ); const getEnrichmentState = useGetStreamEnrichmentState(); - const grokCollection = useStreamsEnrichmentSelector((state) => state.context.grokCollection); + const grokCollection = useStreamEnrichmentSelector((state) => state.context.grokCollection); const isOpen = Boolean(processorRef); const defaultValuesGetter = useCallback( @@ -235,8 +235,9 @@ export function EditProcessorPanel({ processorRef, processorMetrics }: EditProce const { euiTheme } = useEuiTheme(); const state = useSelector(processorRef, (s) => s); const getEnrichmentState = useGetStreamEnrichmentState(); - const grokCollection = useStreamsEnrichmentSelector((_state) => _state.context.grokCollection); - const canEdit = useStreamsEnrichmentSelector((s) => s.context.definition.privileges.simulate); + + const canEdit = useStreamEnrichmentSelector((s) => s.context.definition.privileges.simulate); + const grokCollection = useStreamEnrichmentSelector((_state) => _state.context.grokCollection); const previousProcessor = state.context.previousProcessor; const processor = state.context.processor; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/processor_metrics.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/processor_metrics.tsx index e4bb046a3197d..98f37c7d90046 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/processor_metrics.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/processor_metrics.tsx @@ -26,7 +26,7 @@ type ProcessorMetricBadgesProps = ProcessorMetrics; const formatter = new Intl.NumberFormat('en-US', { style: 'percent', - maximumFractionDigits: 0, + maximumFractionDigits: 1, }); export const ProcessorMetricBadges = ({ diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/processor_type_selector.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/processor_type_selector.tsx index 01f4a5f602b16..b39e4c25303d3 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/processor_type_selector.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/processor_type_selector.tsx @@ -17,7 +17,7 @@ import { ProcessorFormState } from '../types'; import { configDrivenProcessors } from './config_driven'; import { useGetStreamEnrichmentState } from '../state_management/stream_enrichment_state_machine'; import { selectPreviewDocuments } from '../state_management/simulation_state_machine/selectors'; -import { useStreamsEnrichmentSelector } from '../state_management/stream_enrichment_state_machine'; +import { useStreamEnrichmentSelector } from '../state_management/stream_enrichment_state_machine'; interface TAvailableProcessor { type: ProcessorType; @@ -42,7 +42,7 @@ export const ProcessorTypeSelector = ({ const processorType = useWatch<{ type: ProcessorType }>({ name: 'type' }); - const grokCollection = useStreamsEnrichmentSelector((state) => state.context.grokCollection); + const grokCollection = useStreamEnrichmentSelector((state) => state.context.grokCollection); const handleChange = (type: ProcessorType) => { const formState = getDefaultFormStateByType( diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/simulation_playground.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/simulation_playground.tsx index 636ba58d07249..6ec08c913c1ca 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/simulation_playground.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/simulation_playground.tsx @@ -8,6 +8,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { + EuiButtonIcon, + EuiFlexGroup, EuiFlexItem, EuiNotificationBadge, EuiProgress, @@ -19,19 +21,21 @@ import { ProcessorOutcomePreview } from './processor_outcome_preview'; import { useSimulatorSelector, useStreamEnrichmentEvents, - useStreamsEnrichmentSelector, + useStreamEnrichmentSelector, } from './state_management/stream_enrichment_state_machine'; import { DetectedFieldsEditor } from './detected_fields_editor'; +import { DataSourcesList } from './data_sources_list'; export const SimulationPlayground = () => { - const { viewSimulationPreviewData, viewSimulationDetectedFields } = useStreamEnrichmentEvents(); + const { refreshSimulation, viewSimulationPreviewData, viewSimulationDetectedFields } = + useStreamEnrichmentEvents(); - const isViewingDataPreview = useStreamsEnrichmentSelector((state) => + const isViewingDataPreview = useStreamEnrichmentSelector((state) => state.matches({ ready: { enrichment: { displayingSimulation: 'viewDataPreview' } }, }) ); - const isViewingDetectedFields = useStreamsEnrichmentSelector((state) => + const isViewingDetectedFields = useStreamEnrichmentSelector((state) => state.matches({ ready: { enrichment: { displayingSimulation: 'viewDetectedFields' } }, }) @@ -39,46 +43,56 @@ export const SimulationPlayground = () => { const detectedFields = useSimulatorSelector((state) => state.context.detectedSchemaFields); const isLoading = useSimulatorSelector( - (state) => - state.matches('debouncingChanges') || - state.matches('loadingSamples') || - state.matches('runningSimulation') + (state) => state.matches('debouncingChanges') || state.matches('runningSimulation') ); - const definition = useStreamsEnrichmentSelector((state) => state.context.definition); - return ( <> - - - {i18n.translate( - 'xpack.streams.streamDetailView.managementTab.enrichment.simulationPlayground.dataPreview', - { defaultMessage: 'Data preview' } - )} - - 0 ? ( - {detectedFields.length} - ) : undefined - } - > - {i18n.translate( - 'xpack.streams.streamDetailView.managementTab.enrichment.simulationPlayground.detectedFields', - { defaultMessage: 'Detected fields' } - )} - - - {isLoading && } + + + + + } + > + {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.simulationPlayground.dataPreview', + { defaultMessage: 'Data preview' } + )} + + 0 ? ( + {detectedFields.length} + ) : undefined + } + > + {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.simulationPlayground.detectedFields', + { defaultMessage: 'Detected fields' } + )} + + + + + + + {isLoading && } + {isViewingDataPreview && } - {isViewingDetectedFields && ( - - )} + {isViewingDetectedFields && } ); }; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/data_source_state_machine/data_collector_actor.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/data_source_state_machine/data_collector_actor.ts new file mode 100644 index 0000000000000..db7678c59c9ef --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/data_source_state_machine/data_collector_actor.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { SampleDocument } from '@kbn/streams-schema'; +import { ErrorActorEvent, fromObservable } from 'xstate5'; +import type { errors as esErrors } from '@elastic/elasticsearch'; +import { Filter, Query, TimeRange, buildEsQuery } from '@kbn/es-query'; +import { Observable, filter, map, of } from 'rxjs'; +import { isRunningResponse } from '@kbn/data-plugin/common'; +import { IEsSearchResponse } from '@kbn/search-types'; +import { pick } from 'lodash'; +import { getFormattedError } from '../../../../../util/errors'; +import { DataSourceMachineDeps } from './types'; +import { EnrichmentDataSourceWithUIAttributes } from '../../types'; + +export interface SamplesFetchInput { + dataSource: EnrichmentDataSourceWithUIAttributes; + streamName: string; +} + +interface SearchParamsOptions { + filters?: Filter[]; + index: string; + query?: Query; + size?: number; + timeRange?: TimeRange; +} + +interface CollectKqlDataParams extends SearchParamsOptions { + data: DataSourceMachineDeps['data']; +} + +type CollectorParams = Pick; + +/** + * Creates a data collector actor that fetches sample documents based on the data source type + */ +export function createDataCollectorActor({ data }: Pick) { + return fromObservable(({ input }) => { + const { dataSource, streamName } = input; + return getDataCollectorForDataSource(dataSource)({ data, index: streamName }); + }); +} + +/** + * Returns the appropriate data collector function based on the data source type + */ +function getDataCollectorForDataSource(dataSource: EnrichmentDataSourceWithUIAttributes) { + if (dataSource.type === 'random-samples') { + return (args: CollectorParams) => collectKqlData(args); + } + if (dataSource.type === 'kql-samples') { + return (args: CollectorParams) => + collectKqlData({ ...args, ...pick(dataSource, ['filters', 'query', 'timeRange']) }); + } + if (dataSource.type === 'custom-samples') { + return () => of(dataSource.documents); + } + return () => of([]); +} + +/** + * Core function to collect data using KQL + */ +function collectKqlData({ + data, + ...searchParams +}: CollectKqlDataParams): Observable { + const abortController = new AbortController(); + const params = buildSamplesSearchParams(searchParams); + + return new Observable((observer) => { + const subscription = data.search + .search({ params }, { abortSignal: abortController.signal }) + .pipe(filter(isValidSearchResult), map(extractDocumentsFromResult)) + .subscribe(observer); + + return () => { + abortController.abort(); + subscription.unsubscribe(); + }; + }); +} + +/** + * Validates if the search result contains hits + */ +function isValidSearchResult(result: IEsSearchResponse): boolean { + return !isRunningResponse(result) && result.rawResponse.hits?.hits !== undefined; +} + +/** + * Extracts documents from search result + */ +function extractDocumentsFromResult(result: IEsSearchResponse): SampleDocument[] { + return result.rawResponse.hits.hits.map((doc) => doc._source); +} + +/** + * Builds search parameters for Elasticsearch query + */ +function buildSamplesSearchParams({ + filters, + index, + query, + size = 100, + timeRange, +}: SearchParamsOptions) { + const queryDefinition = buildEsQuery({ title: index, fields: [] }, query ?? [], filters ?? []); + addTimeRangeToQuery(queryDefinition, timeRange); + + return { + index, + allow_no_indices: true, + query: queryDefinition, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + size, + terminate_after: size, + track_total_hits: false, + }; +} + +/** + * Adds time range to the query definition if provided + */ +function addTimeRangeToQuery(queryDefinition: any, timeRange?: TimeRange): void { + if (timeRange) { + queryDefinition.bool.must.unshift({ + range: { + '@timestamp': { + gte: timeRange.from, + lte: timeRange.to, + }, + }, + }); + } +} + +/** + * Creates a notifier for data collection failures + */ +export function createDataCollectionFailureNofitier({ + toasts, +}: { + toasts: DataSourceMachineDeps['toasts']; +}) { + return (params: { event: unknown }) => { + const event = params.event as ErrorActorEvent; + const error = getFormattedError(event.error); + toasts.addError(error, { + title: i18n.translate('xpack.streams.enrichment.dataSources.dataCollectionError', { + defaultMessage: 'An issue occurred retrieving documents from the data source.', + }), + toastMessage: error.message, + }); + }; +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/data_source_state_machine/data_source_state_machine.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/data_source_state_machine/data_source_state_machine.ts new file mode 100644 index 0000000000000..14eb8e621b341 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/data_source_state_machine/data_source_state_machine.ts @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + ActorRefFrom, + MachineImplementationsFrom, + SnapshotFrom, + assertEvent, + assign, + sendTo, + setup, +} from 'xstate5'; +import { SampleDocument } from '@kbn/streams-schema'; +import { getPlaceholderFor } from '@kbn/xstate-utils'; +import { isEqual, omit } from 'lodash'; +import { + DataSourceInput, + DataSourceContext, + DataSourceEvent, + DataSourceMachineDeps, + DataSourceToParentEvent, +} from './types'; +import { + createDataCollectionFailureNofitier, + createDataCollectorActor, +} from './data_collector_actor'; +import { EnrichmentDataSourceWithUIAttributes } from '../../types'; + +export type DataSourceActorRef = ActorRefFrom; +export type DataSourceActorSnapshot = SnapshotFrom; + +export const dataSourceMachine = setup({ + types: { + input: {} as DataSourceInput, + context: {} as DataSourceContext, + events: {} as DataSourceEvent, + }, + actors: { + collectData: getPlaceholderFor(createDataCollectorActor), + }, + delays: { + dataSourceChangeDebounceTime: 800, + }, + actions: { + notifyDataCollectionFailure: getPlaceholderFor(createDataCollectionFailureNofitier), + storeDataSource: assign( + ({ context }, params: { dataSource: EnrichmentDataSourceWithUIAttributes }) => ({ + dataSource: { ...params.dataSource, id: context.dataSource.id }, + }) + ), + storeData: assign((_, params: { data: SampleDocument[] }) => ({ data: params.data })), + toggleDataSourceActivity: assign(({ context }) => ({ + dataSource: { ...context.dataSource, enabled: !context.dataSource.enabled }, + })), + notifyParent: sendTo( + ({ context }) => context.parentRef, + ({ context }, params: { eventType: DataSourceToParentEvent['type'] }) => ({ + type: params.eventType, + id: context.dataSource.id, + }) + ), + }, + guards: { + isEnabled: ({ context }) => context.dataSource.enabled, + isDeletable: ({ context }) => context.dataSource.type !== 'random-samples', // We don't allow deleting the random-sample source to always have a data source available + isValidData: (_, params: { data?: SampleDocument[] }) => Array.isArray(params.data), + shouldCollectData: ({ context, event }) => { + assertEvent(event, 'dataSource.change'); + /** + * Determines if the dataSource update contains substantive changes. + * Ignores cosmetic changes like name updates that don't affect functionality. + */ + const ignoredProps = ['name']; + return !isEqual(omit(context.dataSource, ignoredProps), omit(event.dataSource, ignoredProps)); + }, + }, +}).createMachine({ + /** @xstate-layout N4IgpgJg5mDOIC5QQIYBcUGUD2BXATgMZgDEqGOBxAdBGADZhpgDaADALqKgAO2sASzQDsAO24gAHogCcMgMzUZAdlUAmNcpkBWACwBGfQBoQAT0S6AbIrYAOW5pn7ry2-rbKAvp5PkseIlI-SkDqNGwoKEYAQUJhADchU3YuJBA+QWExCWkENQNqZQ8ZNQUithldE3MEXTVLamttfRk3S202Uo9vX3R-KiC+kJpwyJi4gUS0ZP1U3n4hEXE03OtbajV5W11leTr9eWU1aot6xvlm1ucO0p6QYICaOmZ8AFsBUQ+oEhSJDMXsitEABafRqahsfS2eRsSz6ZTaaEIzYnBCI5TUeQySH6XQXEpwrw+e5DR5gWhMMBvD5fH6zP4LLLLUC5ZzrXH2WwySw6KHaZRVMyIRENLRqbSVWGWIpqO4PAbUMCiFAAI0YEDIpIV+DAADMdbAABa-NL-Jk5RDuNjUDm6Wwi3YVfTaVFIiH83TNTryUpqfRyrWhJWq9Waihk6iEQ0oUQwE3zTJLC1ojqFZS49MeXYdF1ChAlfQ2uoVG5aOQyAPhhXBtWQMP9UJRmNx+mmxlJoEIK3aG2tBE7O0HPGogtFtQlzq2SsNmg19UUlV4USEL4AYWjsbgJEksAwzGoKF1LwAFPLAuvm2AACJgRe4ZdgAAqAleYAAlPXhuS55AF0uV7GF6brA8bpO2gIsog8jQRCVh4jC456LYyiWKi7gOEo8jwkhKjuK405foqyq1hA1D0NgKAQF8V59CQVL4Ng+DUDw9DoLqjGvLQfSrtg9CMHEjGxOE+CgWaHaQQg0I2to1gIZ0zjSsoaFsDBbCdCplwwgc3jEqI2B0PAaRnsQDKJhBUggnC1C2Gp0qWDsOibChqLQTINoXOOyFFPClgERGzxUu8nyxqZALMhZCCgjY8KuDJGZOChxx5p6hbtNoFzpR0dTjrofnVsR6qheanZRdZtkoQ52hORKqIytQ2genCMKqPy2h5UGBW-gIECMEV4kRc6GJYh44oOLsliaGhGjgnYpTws4nTtESvRVh1Ia-nQd7LmuG4wIZCZhcmE1uc6dQ7DJRSejIykCpiDhsJ6dq6E41jtbOnWkeRlHUX0fXmbkey9gomxVQ9iWCjUJTWnizSGItMnbG95JUbA60QH94UA9YGyGPaOzOs6kIQ8K9pFkTHQoQ42nEsZyMMJS6NtmZmOWip6ytRcWJ2viqF5jJ1rDWCmhsIilQVjpQA */ + id: 'dataSource', + context: ({ input }) => ({ + parentRef: input.parentRef, + dataSource: input.dataSource, + streamName: input.streamName, + data: [], + }), + initial: 'determining', + on: { + 'dataSource.delete': { + guard: 'isDeletable', + target: '.deleted', + }, + 'dataSource.toggleActivity': [ + { + guard: 'isEnabled', + target: '.disabled', + actions: [ + { type: 'toggleDataSourceActivity' }, + { type: 'notifyParent', params: { eventType: 'dataSource.change' } }, + ], + }, + { + target: '.enabled', + actions: [ + { type: 'toggleDataSourceActivity' }, + { type: 'notifyParent', params: { eventType: 'dataSource.change' } }, + ], + }, + ], + }, + states: { + determining: { + always: [{ target: 'enabled', guard: 'isEnabled' }, { target: 'disabled' }], + }, + enabled: { + initial: 'loadingData', + on: { + 'dataSource.refresh': '.loadingData', + 'dataSource.change': [ + { + guard: 'shouldCollectData', + target: '.debouncingChanges', + reenter: true, + actions: [ + { type: 'storeDataSource', params: ({ event }) => event }, + { type: 'notifyParent', params: { eventType: 'dataSource.change' } }, + ], + }, + { + actions: [ + { type: 'storeDataSource', params: ({ event }) => event }, + { type: 'notifyParent', params: { eventType: 'dataSource.change' } }, + ], + }, + ], + }, + exit: [{ type: 'notifyParent', params: { eventType: 'dataSource.dataChange' } }], + states: { + idle: {}, + debouncingChanges: { + after: { + dataSourceChangeDebounceTime: 'loadingData', + }, + }, + loadingData: { + invoke: { + id: 'dataCollectorActor', + src: 'collectData', + input: ({ context }) => ({ + dataSource: context.dataSource, + streamName: context.streamName, + }), + onSnapshot: { + guard: { + type: 'isValidData', + params: ({ event }) => ({ data: event.snapshot.context }), + }, + target: 'idle', + actions: [ + { + type: 'storeData', + params: ({ event }) => ({ data: event.snapshot.context ?? [] }), + }, + { type: 'notifyParent', params: { eventType: 'dataSource.dataChange' } }, + ], + }, + onError: { + target: 'idle', + actions: [ + { type: 'storeData', params: () => ({ data: [] }) }, + { type: 'notifyParent', params: { eventType: 'dataSource.dataChange' } }, + { type: 'notifyDataCollectionFailure' }, + ], + }, + }, + }, + }, + }, + disabled: {}, + deleted: { + id: 'deleted', + type: 'final', + entry: [{ type: 'notifyParent', params: { eventType: 'dataSource.delete' } }], + }, + }, +}); + +export const createDataSourceMachineImplementations = ({ + data, + toasts, +}: DataSourceMachineDeps): MachineImplementationsFrom => ({ + actors: { + collectData: createDataCollectorActor({ data }), + }, + actions: { + notifyDataCollectionFailure: createDataCollectionFailureNofitier({ toasts }), + }, +}); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/data_source_state_machine/index.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/data_source_state_machine/index.ts new file mode 100644 index 0000000000000..0f3aa79ae271b --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/data_source_state_machine/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './data_source_state_machine'; +export * from './types'; +export * from './use_data_source_selector'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/data_source_state_machine/types.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/data_source_state_machine/types.ts new file mode 100644 index 0000000000000..928ddad6483c5 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/data_source_state_machine/types.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ActorRef, Snapshot } from 'xstate5'; +import { IToasts } from '@kbn/core/public'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { SampleDocument } from '@kbn/streams-schema'; +import { EnrichmentDataSourceWithUIAttributes } from '../../types'; + +export interface DataSourceMachineDeps { + data: DataPublicPluginStart; + toasts: IToasts; +} + +export type DataSourceToParentEvent = + | { type: 'dataSource.change'; id: string } + | { type: 'dataSource.dataChange'; id: string } + | { type: 'dataSource.delete'; id: string }; + +export interface DataSourceInput { + parentRef: DataSourceParentActor; + streamName: string; + dataSource: EnrichmentDataSourceWithUIAttributes; +} + +export type DataSourceParentActor = ActorRef, DataSourceToParentEvent>; + +export interface DataSourceContext { + parentRef: DataSourceParentActor; + streamName: string; + dataSource: EnrichmentDataSourceWithUIAttributes; + data: SampleDocument[]; +} + +export type DataSourceEvent = + | { type: 'dataSource.change'; dataSource: EnrichmentDataSourceWithUIAttributes } + | { type: 'dataSource.delete' } + | { type: 'dataSource.refresh' } + | { type: 'dataSource.toggleActivity' }; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/data_source_state_machine/use_data_source_selector.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/data_source_state_machine/use_data_source_selector.ts new file mode 100644 index 0000000000000..3f051394dfcf9 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/data_source_state_machine/use_data_source_selector.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useSelector } from '@xstate5/react'; +import { DataSourceActorRef, DataSourceActorSnapshot } from './data_source_state_machine'; + +export function useDataSourceSelector( + actorRef: DataSourceActorRef, + selector: (snapshot: DataSourceActorSnapshot) => T +): T { + return useSelector(actorRef, selector); +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/processor_state_machine/types.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/processor_state_machine/types.ts index a27163df2fbdb..a97165a53e8e7 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/processor_state_machine/types.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/processor_state_machine/types.ts @@ -13,7 +13,8 @@ import { ProcessorDefinitionWithUIAttributes } from '../../types'; export type ProcessorToParentEvent = | { type: 'processor.change'; id: string } | { type: 'processor.delete'; id: string } - | { type: 'processor.stage' }; + | { type: 'processor.stage' } + | { type: 'processor.update' }; export interface ProcessorInput { parentRef: ProcessorParentActor; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/index.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/index.ts index 2e33f97d56749..3233f81e88bbd 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/index.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -export * from './preview_docs_filter'; +export * from './simulation_documents_search'; export * from './simulation_state_machine'; export * from './types'; export * from './utils'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/samples_fetcher_actor.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/samples_fetcher_actor.ts deleted file mode 100644 index fa7f43a192e24..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/samples_fetcher_actor.ts +++ /dev/null @@ -1,57 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { Condition, SampleDocument } from '@kbn/streams-schema'; -import { fromPromise, ErrorActorEvent } from 'xstate5'; -import type { errors as esErrors } from '@elastic/elasticsearch'; -import { SimulationMachineDeps } from './types'; - -export interface SamplesFetchInput { - condition?: Condition; - streamName: string; -} - -export function createSamplesFetchActor({ - streamsRepositoryClient, - timeState$, -}: Pick) { - return fromPromise(async ({ input, signal }) => { - const { asAbsoluteTimeRange } = timeState$.getValue(); - const samplesBody = await streamsRepositoryClient.fetch( - 'POST /internal/streams/{name}/_sample', - { - signal, - params: { - path: { name: input.streamName }, - body: { - if: input.condition, - start: new Date(asAbsoluteTimeRange.from).getTime(), - end: new Date(asAbsoluteTimeRange.to).getTime(), - size: 100, - }, - }, - } - ); - - return samplesBody.documents; - }); -} - -export function createSamplesFetchFailureNofitier({ - toasts, -}: Pick) { - return (params: { event: unknown }) => { - const event = params.event as ErrorActorEvent; - toasts.addError(new Error(event.error.body.message), { - title: i18n.translate('xpack.streams.enrichment.simulation.samplesFetchError', { - defaultMessage: 'An issue occurred retrieving samples.', - }), - toastMessage: event.error.body.message, - }); - }; -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/preview_docs_filter.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/simulation_documents_search.ts similarity index 100% rename from x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/preview_docs_filter.ts rename to x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/simulation_documents_search.ts diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/simulation_state_machine.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/simulation_state_machine.ts index 3eb41fe80caf1..4cd8a70daf1b2 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/simulation_state_machine.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/simulation_state_machine.ts @@ -4,14 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { - ActorRefFrom, - MachineImplementationsFrom, - SnapshotFrom, - assign, - fromEventObservable, - setup, -} from 'xstate5'; +import { ActorRefFrom, MachineImplementationsFrom, SnapshotFrom, assign, setup } from 'xstate5'; import { getPlaceholderFor } from '@kbn/xstate-utils'; import { FlattenRecord, @@ -19,10 +12,8 @@ import { isSchema, processorDefinitionSchema, } from '@kbn/streams-schema'; -import { isEmpty, isEqual } from 'lodash'; +import { isEmpty } from 'lodash'; import { flattenObjectNestedLast } from '@kbn/object-utils'; -import { BehaviorSubject, map } from 'rxjs'; -import { TimeState } from '@kbn/es-query'; import { ProcessorDefinitionWithUIAttributes } from '../../types'; import { processorConverter } from '../../utils'; import { @@ -32,21 +23,12 @@ import { Simulation, SimulationMachineDeps, } from './types'; -import { PreviewDocsFilterOption } from './preview_docs_filter'; -import { - createSamplesFetchActor, - createSamplesFetchFailureNofitier, -} from './samples_fetcher_actor'; +import { PreviewDocsFilterOption } from './simulation_documents_search'; import { createSimulationRunnerActor, createSimulationRunFailureNofitier, } from './simulation_runner_actor'; -import { - composeSamplingCondition, - getSchemaFieldsFromSimulation, - mapField, - unmapField, -} from './utils'; +import { getSchemaFieldsFromSimulation, mapField, unmapField } from './utils'; import { MappedSchemaField } from '../../../schema_editor/types'; export type SimulationActorRef = ActorRefFrom; @@ -69,14 +51,10 @@ export const simulationMachine = setup({ events: {} as SimulationEvent, }, actors: { - fetchSamples: getPlaceholderFor(createSamplesFetchActor), runSimulation: getPlaceholderFor(createSimulationRunnerActor), - subscribeTimeUpdates: getPlaceholderFor(createTimeUpdatesActor), }, actions: { - notifySamplesFetchFailure: getPlaceholderFor(createSamplesFetchFailureNofitier), notifySimulationRunFailure: getPlaceholderFor(createSimulationRunFailureNofitier), - storeTimeUpdated: getPlaceholderFor(createSimulationRunFailureNofitier), storePreviewDocsFilter: assign((_, params: { filter: PreviewDocsFilterOption }) => ({ previewDocsFilter: params.filter, })), @@ -104,9 +82,6 @@ export const simulationMachine = setup({ storePreviewColumnsOrder: assign(({ context }, params: { columns: string[] }) => ({ previewColumnsOrder: params.columns, })), - deriveSamplingCondition: assign(({ context }) => ({ - samplingCondition: composeSamplingCondition(context.processors), - })), deriveDetectedSchemaFields: assign(({ context }) => ({ detectedSchemaFields: context.simulation ? getSchemaFieldsFromSimulation( @@ -122,36 +97,31 @@ export const simulationMachine = setup({ unmapField: assign(({ context }, params: { fieldName: string }) => ({ detectedSchemaFields: unmapField(context.detectedSchemaFields, params.fieldName), })), - resetSimulation: assign({ + resetSimulationOutcome: assign({ processors: [], detectedSchemaFields: [], explicitlyEnabledPreviewColumns: [], explicitlyDisabledPreviewColumns: [], previewColumnsOrder: [], simulation: undefined, - samplingCondition: composeSamplingCondition([]), previewDocsFilter: 'outcome_filter_all', }), + resetProcessors: assign({ processors: [] }), + resetSamples: assign({ samples: [] }), }, delays: { - debounceTime: 800, + processorChangeDebounceTime: 800, }, guards: { - canSimulate: ({ context }, params: ProcessorEventParams) => - hasSamples(context.samples) && hasValidProcessors(params.processors), + canSimulate: ({ context }) => + hasSamples(context.samples) && hasValidProcessors(context.processors), hasProcessors: (_, params: ProcessorEventParams) => !isEmpty(params.processors), - hasSamples: ({ context }) => hasSamples(context.samples), - hasValidProcessors: (_, params: ProcessorEventParams) => hasValidProcessors(params.processors), - shouldRefetchSamples: ({ context }) => - Boolean( - context.samplingCondition && - !isEqual(context.samplingCondition, composeSamplingCondition(context.processors)) - ), + '!hasSamples': (_, params: { samples: SampleDocument[] }) => !hasSamples(params.samples), }, }).createMachine({ - /** @xstate-layout N4IgpgJg5mDOIC5SwJYFsCuAbAhgFxQHsA7AYgnzACUdiYA6DABwrzAG0AGAXUVCcKoCJPiAAeiAIwB2AGwAWetICsnTgA5Js2cuUBOZQCYANCACeiALSTOh+rfUBmWdL3TpzzkYC+306kxcYTIA7HwiYnoAYwALWhgABQAnMAA3FDAAdwARQijYADEULDYkrl4kEAEhCNEJBFlHSXplFXVDPRl1ZVlNUwsES2VJR3oRw0dDeXUXR04ZX390MODSUKCI+hTYMDxy0WqUYLrEKdcx9T15ecNJPVlO6X6rQ3V6SddDdzd5K-llRyLEDrcIkUhMJJ5OCwQhJWD0ABU+0qh2OlXqkhmikc8kkhmU6hm+n08meg3xzU4-3aUy0kmmyiBINWEKhsBhSWitCiYCwyP4giOtXRpxGynoBhmhkMsipLlJ5isb2U13u8mk6qmRl0TOWGzBrJ57Nh0TidA4PAOgrRoHqhL07w6elpBL0GjJlkcDoBvWkcr0k0ahl1gVBZEN0JNEF5uwtFQFNREIoQ0oU9GmOlusr9KrJNkULlkmLdk2l6g0IZWEXBkKNHPo0awsfYknjVWtwttpzmdnkuNubk6KscZL7dkL8g+-3V0hllf1kRQxCFOCwKAAXkuoKR+e3E8QTggRo43r9NGOs6m87J6L0epxnZcFPIOvOw-Qlyu15u6DvW1b90PMVmnUdwvC8GxnSuMkXCUSCvE6B4mimN9ggbMAACNCAwYgoi3ABhM0YFgGs2XrWJ4jjAChSTLsEF+aQJXuTgNT0J9Og9HF7FVV4Az9RwCVkVDNmjLCcLwuhCMokixFgPBKHoHAADNSgAClE7DcLAAAVdAwAASjWPV3w08SCKIuBd1RTtxCkWwQJfRxpFAjwNUmPNOBvGQix0dUNHuV5hJIdCxNw8zpNIWT5LYRSVLAJJ1MwzSeV0tADKM0M0NMsLJIs2AWzbazaNso8WJvbR-leTRhlkExFRTXp7G0bQOmkcZXCEvxgWMtCsEIHAIC3ABlHA0CYJsSIgEgwA-YhUkIABrGbYFG8a4AKXZYnigBBKI8FhKyO2KjEVE4JrGnmRwvSuiYPLsVRJ1eB9XAEq4gsiPqBuG1aJtIeLIU5cb8CU2E0HoFaxomja8C2pJdv2spLRRI6D2TQx5jeSQeh6X4XxkBUBhse6qUmctB1e+R3sU9l4oIOghp6iIqDAABHDAUBSNLiDwEjDsA5N6SaehbhYr08SlOqBiLM7CU8kYri+Nr1CpnAaaSOmoAZzKmdZ9nObAbnef-ZH+bowWzvslVZVuFVMRgnRhZmOR3AZS6qaSHDl3pxmwSm4gZqXealvBn3iCoT2dr2g6kYTGjUbouQb1UVQ5g8IwVFkUdJiUUDdCcAE3Wmd3PeG0O-qSAH6CBvAQaSMHmSZiO4ajxHCpRw9nbGDx5kHbQvT0MkCUYrHrnkHR3C0QEgWIQho3gSoG+Kor45KvsHT0ZjWPYyROJPFpqSxhRug1Tgp6WbXgs-AhVw3LdqJtEqxRvOZBJUMeRnUAmlRvNr+-+XRMTlmkFTFAEAmz3xsvUQs9BP7qjxP8DeFIyQakUAhF2fxdAbyptlCSUApLmnnrHB+doRhKF6P8ekWhuh23qpILG6YaqT0QqfZ0VNPqDXpj9OAEDjpSF6M0CYk41StC9EYD01wmq2FlAGB4rw+yUy6ovSIqsdjq1LhfMOusOZgC5jzHhK8MQPAdNoDebhpTox6E8eqXxRi3BGM5U+E8R7F2IF7TWod9FAVsGdAk-9nSunlIPboYw5jZjmE5ekytfDeCAA */ + /** @xstate-layout N4IgpgJg5mDOIC5SwJYFsCuAbAhgFxQHsA7AYlU1wJIDoBjACx2JgAUAnMANxTAHcAIoTqwAYiix4w7ANoAGALqJQAB0KpqxZSAAeiAGwBmOTQDsAJnP65AFnMAOAIw2598wBoQAT0SOArFY09vqmjmGGNvZy+vY2AL5xnhTY+ERkyVRpNJywYHjySkggahpp2noIALSWNEYAnHX2fnX6djHRNp4+CI76zjRycobNFo729sOmCUnoKZrks5m0nHRgKFxgAPqwOGgqWHAF2iUomuWI1ea1hg1NLW1RrV2+poamNC6N5n5+hs4u8USIAyqRIC0ooOI2TAq3WWx2ewOsBkjkKqnUpzKRQqfhs7zq5j+hn0dUihkM5kczwQrT8NEsch++gC9xsfmmwMWkPBcyyKzWG22u32h3MaOKGLO2IuNXqjWarSsj063l8wRMTmsv1iAICHJB8xU7GEcFghHYsBoACojkUTlLQBVLtdbgqHh1qc5KQM6q9hrZzHi7PquYbjatYGb2PRmKssLb0aUSOcqrKbvL7kqPaqen5HO8frY-NFHHI6sNDCGIWGTZHzfQmCwwAmJUmtNKqnZHEFTAFHFZGhEbL7qTZ+pZ9MzYsFxvpzFXeWCjbWozQIGADlIW-asY6LjcbDRjKXeuY6qWKXVqbT6eZGYr7HVi8GgQa0qRlxHV+vN83UcdJV3XQZSuOU7kVdonhzRwn3eAd-EMR9zFMYJ9AXJYoRQCADh5DCaAAM14LAIEtNAcBUbdAOTDtBwGQkbGJQkjG+a8JmuRlA0aYcbnsdDIRoLCcLfWhCI3EiaAwYgyIoxQALbFNaLkejGOJCk-GpFCTFcYl7FCfsfgaPjNDXMAACNCEkugUBYABhRsYFgUgdFgPB8DAGgcHwqR2AACk-U1zTs5gYAEMyLOIVYABV0DAABKXD+PXczLOsqAgqbZFZLtKj2z3BBFOUudVJY6CbneG5GV6Nw3jeKZX1DLIcEjaQCBYAAlMAAEcMBQTg0DAYg8Ecyj5I7UsGnpCJkMiX0YLnT1-HeGJTBsGwjCGH5AyMxrmvYVqoA67rerAfrBuG-9stGvKwjCGgfjW4YGKUlDDGvN52NxRDkIJX1tuWSTiFSgBlBqwQgEh3OsrhCAAa3c4TiDagHpAAQToPBzRGzFqLy4d7BoMYVvLZDhhHHN7jMJw7y9AldLqP6oXYAHgdBshpGNaN9nwfDzTQGgEaR4hiFR9HMayxNsdy4CEDxgndO4kmRmpXTuwpR4KUGUlCQSIFiEIdd4CKBG5MllNKjWuoez7AcJlWsnunzcqLFGKx82sAkGYE7CwBNh1paMfG5DGVxTDqaIzz+akrEPXTkOD30-GZJ9PaS8KrNs+y4F9oCKiKmgw4JSdVv8RxhmpAI6WZIY7HJL6+k9prcj21LDp6vqBqG7Ocel893jkImmn7sc8XscviXz5xGVMUwy0QtlPaZoWWerICd27iown0AZcRJBik7ZUecyQj4z2YomZ7LHW4iAA */ id: 'simulation', - context: ({ input, self, spawn }) => ({ + context: ({ input }) => ({ detectedSchemaFields: [], previewDocsFilter: 'outcome_filter_all', previewDocuments: [], @@ -160,23 +130,36 @@ export const simulationMachine = setup({ previewColumnsOrder: [], processors: input.processors, samples: [], - samplingCondition: composeSamplingCondition(input.processors), streamName: input.streamName, }), - initial: 'loadingSamples', - invoke: { - id: 'subscribeTimeUpdatesActor', - src: 'subscribeTimeUpdates', - }, + initial: 'idle', on: { - 'dateRange.update': '.loadingSamples', 'simulation.changePreviewDocsFilter': { actions: [{ type: 'storePreviewDocsFilter', params: ({ event }) => event }], }, 'simulation.reset': { target: '.idle', - actions: [{ type: 'resetSimulation' }], + actions: [{ type: 'resetSimulationOutcome' }, { type: 'resetProcessors' }], }, + 'simulation.receive_samples': [ + { + guard: { type: '!hasSamples', params: ({ event }) => event }, + target: '.idle', + actions: [{ type: 'resetSimulationOutcome' }, { type: 'resetSamples' }], + }, + { + guard: { + type: 'hasProcessors', + params: ({ context }) => ({ processors: context.processors }), + }, + target: '.assertingRequirements', + actions: [{ type: 'storeSamples', params: ({ event }) => event }], + }, + { + target: '.idle', + actions: [{ type: 'storeSamples', params: ({ event }) => event }], + }, + ], 'previewColumns.updateExplicitlyEnabledColumns': { actions: [ { @@ -206,15 +189,17 @@ export const simulationMachine = setup({ }, // Handle adding/reordering processors 'processors.*': { - target: '.assertingSimulationRequirements', + target: '.assertingRequirements', actions: [{ type: 'storeProcessors', params: ({ event }) => event }], }, 'processor.cancel': { - target: '.assertingSimulationRequirements', + target: '.assertingRequirements', actions: [{ type: 'storeProcessors', params: ({ event }) => event }], }, 'processor.change': { target: '.debouncingChanges', + reenter: true, + description: 'Re-enter debouncing state and reinitialize the delayed processing.', actions: [{ type: 'storeProcessors', params: ({ event }) => event }], }, 'processor.delete': [ @@ -223,12 +208,12 @@ export const simulationMachine = setup({ type: 'hasProcessors', params: ({ event }) => ({ processors: event.processors }), }, - target: '.assertingSimulationRequirements', + target: '.assertingRequirements', actions: [{ type: 'storeProcessors', params: ({ event }) => event }], }, { target: '.idle', - actions: [{ type: 'resetSimulation' }], + actions: [{ type: 'resetSimulationOutcome' }, { type: 'resetProcessors' }], }, ], }, @@ -236,80 +221,24 @@ export const simulationMachine = setup({ idle: { on: { 'simulation.fields.map': { - target: 'assertingSimulationRequirements', + target: 'assertingRequirements', actions: [{ type: 'mapField', params: ({ event }) => event }], }, 'simulation.fields.unmap': { - target: 'assertingSimulationRequirements', + target: 'assertingRequirements', actions: [{ type: 'unmapField', params: ({ event }) => event }], }, }, }, debouncingChanges: { - on: { - 'processor.change': { - target: 'debouncingChanges', - actions: [{ type: 'storeProcessors', params: ({ event }) => event }], - description: 'Re-enter debouncing state and reinitialize the delayed processing.', - reenter: true, - }, - }, after: { - debounceTime: [ - { - guard: 'shouldRefetchSamples', - target: 'loadingSamples', - actions: [{ type: 'deriveSamplingCondition' }], - }, - { target: 'assertingSimulationRequirements' }, - ], + processorChangeDebounceTime: 'assertingRequirements', }, }, - loadingSamples: { - invoke: { - id: 'samplesFetcherActor', - src: 'fetchSamples', - input: ({ context }) => ({ - condition: context.samplingCondition, - streamName: context.streamName, - }), - onDone: [ - { - guard: { - type: 'hasProcessors', - params: ({ context }) => ({ processors: context.processors }), - }, - target: 'assertingSimulationRequirements', - actions: [{ type: 'storeSamples', params: ({ event }) => ({ samples: event.output }) }], - }, - { - target: 'idle', - actions: [{ type: 'storeSamples', params: ({ event }) => ({ samples: event.output }) }], - }, - ], - onError: { - target: 'idle', - actions: [ - { type: 'storeSamples', params: () => ({ samples: [] }) }, - { type: 'notifySamplesFetchFailure' }, - ], - }, - }, - }, - - assertingSimulationRequirements: { - always: [ - { - guard: { - type: 'canSimulate', - params: ({ context }) => ({ processors: context.processors }), - }, - target: 'runningSimulation', - }, - { target: 'idle' }, - ], + assertingRequirements: { + always: [{ guard: 'canSimulate', target: 'runningSimulation' }, { target: 'idle' }], }, runningSimulation: { @@ -341,19 +270,11 @@ export const simulationMachine = setup({ export const createSimulationMachineImplementations = ({ streamsRepositoryClient, toasts, - timeState$, }: SimulationMachineDeps): MachineImplementationsFrom => ({ actors: { - fetchSamples: createSamplesFetchActor({ streamsRepositoryClient, timeState$ }), runSimulation: createSimulationRunnerActor({ streamsRepositoryClient }), - subscribeTimeUpdates: createTimeUpdatesActor({ timeState$ }), }, actions: { - notifySamplesFetchFailure: createSamplesFetchFailureNofitier({ toasts }), notifySimulationRunFailure: createSimulationRunFailureNofitier({ toasts }), }, }); - -function createTimeUpdatesActor({ timeState$ }: { timeState$: BehaviorSubject }) { - return fromEventObservable(() => timeState$.pipe(map(() => ({ type: 'dateRange.update' })))); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/types.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/types.ts index b5baae8391998..234e194673883 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/types.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/types.ts @@ -5,20 +5,20 @@ * 2.0. */ -import { Condition, FlattenRecord, SampleDocument } from '@kbn/streams-schema'; +import { FlattenRecord, SampleDocument } from '@kbn/streams-schema'; import { APIReturnType, StreamsRepositoryClient } from '@kbn/streams-plugin/public/api'; import { IToasts } from '@kbn/core/public'; -import { BehaviorSubject } from 'rxjs'; -import { TimeState } from '@kbn/es-query'; +import { Query } from '@kbn/es-query'; +import { DataPublicPluginStart, QueryState } from '@kbn/data-plugin/public'; import { ProcessorDefinitionWithUIAttributes } from '../../types'; -import { PreviewDocsFilterOption } from './preview_docs_filter'; +import { PreviewDocsFilterOption } from './simulation_documents_search'; import { MappedSchemaField, SchemaField } from '../../../schema_editor/types'; export type Simulation = APIReturnType<'POST /internal/streams/{name}/processing/_simulate'>; export type DetectedField = Simulation['detected_fields'][number]; export interface SimulationMachineDeps { - timeState$: BehaviorSubject; + data: DataPublicPluginStart; streamsRepositoryClient: StreamsRepositoryClient; toasts: IToasts; } @@ -26,13 +26,19 @@ export interface SimulationMachineDeps { export type ProcessorMetrics = Simulation['processors_metrics'][keyof Simulation['processors_metrics']]; +export interface SimulationSearchParams extends Required { + query: Query; +} + export interface SimulationInput { processors: ProcessorDefinitionWithUIAttributes[]; streamName: string; } export type SimulationEvent = - | { type: 'dateRange.update' } + | { type: 'previewColumns.updateExplicitlyEnabledColumns'; columns: string[] } + | { type: 'previewColumns.updateExplicitlyDisabledColumns'; columns: string[] } + | { type: 'previewColumns.order'; columns: string[] } | { type: 'processors.add'; processors: ProcessorDefinitionWithUIAttributes[] } | { type: 'processor.cancel'; processors: ProcessorDefinitionWithUIAttributes[] } | { type: 'processor.change'; processors: ProcessorDefinitionWithUIAttributes[] } @@ -41,9 +47,7 @@ export type SimulationEvent = | { type: 'simulation.fields.map'; field: MappedSchemaField } | { type: 'simulation.fields.unmap'; fieldName: string } | { type: 'simulation.reset' } - | { type: 'previewColumns.updateExplicitlyEnabledColumns'; columns: string[] } - | { type: 'previewColumns.updateExplicitlyDisabledColumns'; columns: string[] } - | { type: 'previewColumns.order'; columns: string[] }; + | { type: 'simulation.receive_samples'; samples: SampleDocument[] }; export interface SimulationContext { detectedSchemaFields: SchemaField[]; @@ -54,7 +58,6 @@ export interface SimulationContext { previewColumnsOrder: string[]; processors: ProcessorDefinitionWithUIAttributes[]; samples: SampleDocument[]; - samplingCondition?: Condition; simulation?: Simulation; streamName: string; } diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/utils.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/utils.ts index ad054e65538b1..1309779c19e8c 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/utils.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/utils.ts @@ -5,36 +5,14 @@ * 2.0. */ -import { Condition, FieldDefinition, UnaryOperator, getProcessorConfig } from '@kbn/streams-schema'; -import { isEmpty, uniq } from 'lodash'; -import { ALWAYS_CONDITION } from '../../../../../util/condition'; +import { FieldDefinition, getProcessorConfig } from '@kbn/streams-schema'; +import { uniq } from 'lodash'; import { ProcessorDefinitionWithUIAttributes } from '../../types'; -import { PreviewDocsFilterOption } from './preview_docs_filter'; +import { PreviewDocsFilterOption } from './simulation_documents_search'; import { DetectedField, Simulation } from './types'; import { MappedSchemaField, SchemaField, isSchemaFieldTyped } from '../../../schema_editor/types'; import { convertToFieldDefinitionConfig } from '../../../schema_editor/utils'; -export function composeSamplingCondition( - processors: ProcessorDefinitionWithUIAttributes[] -): Condition | undefined { - if (isEmpty(processors)) { - return undefined; - } - - const uniqueFields = uniq(getSourceFields(processors)); - - if (isEmpty(uniqueFields)) { - return ALWAYS_CONDITION; - } - - const conditions = uniqueFields.map((field) => ({ - field, - operator: 'exists' as UnaryOperator, - })); - - return { or: conditions }; -} - export function getSourceFields(processors: ProcessorDefinitionWithUIAttributes[]): string[] { return processors .map((processor) => { diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/selectors.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/selectors.ts index 4522ff14eed32..d08aacc4cf7f2 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/selectors.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/selectors.ts @@ -10,8 +10,8 @@ import { StreamEnrichmentContextType } from './types'; /** * Selects the processor marked as the draft processor. */ -export const selectDraftProcessor = (context: StreamEnrichmentContextType | undefined) => { - const draft = context?.processorsRefs?.find((p) => p.getSnapshot().matches('draft')); +export const selectDraftProcessor = (context: StreamEnrichmentContextType) => { + const draft = context.processorsRefs.find((p) => p.getSnapshot().matches('draft')); return { processor: draft?.getSnapshot().context.processor, resources: draft?.getSnapshot().context.resources, diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/setup_grok_collection_actor.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/setup_grok_collection_actor.ts index 050d02399d1a2..90740cae7e12d 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/setup_grok_collection_actor.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/setup_grok_collection_actor.ts @@ -9,7 +9,7 @@ import type { GrokCollection } from '@kbn/grok-ui'; import { fromPromise } from 'xstate5'; export function setupGrokCollectionActor() { - return fromPromise(async ({ input, signal }) => { + return fromPromise(async ({ input }) => { await input.grokCollection.setup(); }); } diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/stream_enrichment_state_machine.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/stream_enrichment_state_machine.ts index f0ae8ac2ddde1..aa3c867aa7f78 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/stream_enrichment_state_machine.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/stream_enrichment_state_machine.ts @@ -15,11 +15,13 @@ import { and, ActorRefFrom, raise, + cancel, } from 'xstate5'; import { getPlaceholderFor } from '@kbn/xstate-utils'; import { isRootStreamDefinition, Streams } from '@kbn/streams-schema'; import { htmlIdGenerator } from '@elastic/eui'; import { GrokCollection } from '@kbn/grok-ui'; +import { EnrichmentDataSource, EnrichmentUrlState } from '../../../../../../common/url_schema'; import { StreamEnrichmentContextType, StreamEnrichmentEvent, @@ -39,7 +41,20 @@ import { createSimulationMachineImplementations, } from '../simulation_state_machine'; import { processorMachine, ProcessorActorRef } from '../processor_state_machine'; -import { getConfiguredProcessors, getStagedProcessors, getUpsertWiredFields } from './utils'; +import { + defaultEnrichmentUrlState, + getConfiguredProcessors, + getDataSourcesSamples, + getDataSourcesUrlState, + getStagedProcessors, + getUpsertWiredFields, + spawnDataSource, +} from './utils'; +import { createUrlInitializerActor, createUrlSyncAction } from './url_state_actor'; +import { + createDataSourceMachineImplementations, + dataSourceMachine, +} from '../data_source_state_machine'; import { setupGrokCollectionActor } from './setup_grok_collection_actor'; const createId = htmlIdGenerator(); @@ -53,7 +68,9 @@ export const streamEnrichmentMachine = setup({ events: {} as StreamEnrichmentEvent, }, actors: { + initializeUrl: getPlaceholderFor(createUrlInitializerActor), upsertStream: getPlaceholderFor(createUpsertStreamActor), + dataSourceMachine: getPlaceholderFor(() => dataSourceMachine), setupGrokCollection: getPlaceholderFor(setupGrokCollectionActor), processorMachine: getPlaceholderFor(() => processorMachine), simulationMachine: getPlaceholderFor(() => simulationMachine), @@ -73,30 +90,35 @@ export const streamEnrichmentMachine = setup({ notifyUpsertStreamSuccess: getPlaceholderFor(createUpsertStreamSuccessNofitier), notifyUpsertStreamFailure: getPlaceholderFor(createUpsertStreamFailureNofitier), refreshDefinition: () => {}, + /* URL state actions */ + storeUrlState: assign((_, params: { urlState: EnrichmentUrlState }) => ({ + urlState: params.urlState, + })), + syncUrlState: getPlaceholderFor(createUrlSyncAction), storeDefinition: assign((_, params: { definition: Streams.ingest.all.GetResponse }) => ({ definition: params.definition, })), - - stopProcessors: ({ context }) => context.processorsRefs.forEach(stopChild), - setupProcessors: assign( - ({ self, spawn }, params: { definition: Streams.ingest.all.GetResponse }) => { - const processorsRefs = params.definition.stream.ingest.processing.map((proc) => { - const processor = processorConverter.toUIDefinition(proc); - return spawn('processorMachine', { - id: processor.id, - input: { - parentRef: self, - processor, - }, - }); + /* Processors actions */ + setupProcessors: assign(({ context, self, spawn }) => { + // Clean-up pre-existing processors + context.processorsRefs.forEach(stopChild); + // Setup processors from the stream definition + const processorsRefs = context.definition.stream.ingest.processing.map((proc) => { + const processor = processorConverter.toUIDefinition(proc); + return spawn('processorMachine', { + id: processor.id, + input: { + parentRef: self, + processor, + }, }); + }); - return { - initialProcessorsRefs: processorsRefs, - processorsRefs, - }; - } - ), + return { + initialProcessorsRefs: processorsRefs, + processorsRefs, + }; + }), addProcessor: assign( ( { context, spawn, self }, @@ -117,7 +139,6 @@ export const streamEnrichmentMachine = setup({ }; } ), - stopProcessor: stopChild((_, params: { id: string }) => params.id), deleteProcessor: assign(({ context }, params: { id: string }) => ({ processorsRefs: context.processorsRefs.filter((proc) => proc.id !== params.id), })), @@ -127,13 +148,42 @@ export const streamEnrichmentMachine = setup({ reassignProcessors: assign(({ context }) => ({ processorsRefs: [...context.processorsRefs], })), - forwardProcessorsEventToSimulator: sendTo( + /* Data sources actions */ + setupDataSources: assign((assignArgs) => ({ + dataSourcesRefs: assignArgs.context.urlState.dataSources.map((dataSource) => + spawnDataSource(dataSource, assignArgs) + ), + })), + addDataSource: assign((assignArgs, { dataSource }: { dataSource: EnrichmentDataSource }) => { + const newDataSourceRef = spawnDataSource(dataSource, assignArgs); + + return { + dataSourcesRefs: [newDataSourceRef, ...assignArgs.context.dataSourcesRefs], + }; + }), + deleteDataSource: assign(({ context }, params: { id: string }) => ({ + dataSourcesRefs: context.dataSourcesRefs.filter((proc) => proc.id !== params.id), + })), + refreshDataSources: ({ context }) => { + context.dataSourcesRefs.forEach((dataSourceRef) => + dataSourceRef.send({ type: 'dataSource.refresh' }) + ); + }, + sendProcessorsEventToSimulator: sendTo( 'simulator', ({ context }, params: { type: StreamEnrichmentEvent['type'] }) => ({ type: params.type, processors: getStagedProcessors(context), }) ), + sendDataSourcesSamplesToSimulator: sendTo( + 'simulator', + ({ context }) => ({ + type: 'simulation.receive_samples', + samples: getDataSourcesSamples(context), + }), + { delay: 800, id: 'send-samples-to-simulator' } + ), sendResetEventToSimulator: sendTo('simulator', { type: 'simulation.reset' }), updateGrokCollectionCustomPatterns: assign(({ context }, params: { id: string }) => { const processorRefContext = context.processorsRefs @@ -141,7 +191,7 @@ export const streamEnrichmentMachine = setup({ ?.getSnapshot().context; if (processorRefContext && isGrokProcessor(processorRefContext.processor)) { context.grokCollection.setCustomPatterns( - processorRefContext?.processor.grok.pattern_definitions ?? {} + processorRefContext.processor.grok.pattern_definitions ?? {} ); } return { grokCollection: context.grokCollection }; @@ -182,28 +232,40 @@ export const streamEnrichmentMachine = setup({ return processorRef.getSnapshot().matches('draft'); }, isRootStream: ({ context }) => isRootStreamDefinition(context.definition.stream), - isWiredStream: ({ context }) => Streams.WiredStream.GetResponse.is(context.definition), }, }).createMachine({ - /** @xstate-layout N4IgpgJg5mDOIC5RgHYCcCWBjAFgZQBc0wBDAWwDoMUMCMSAbDAL2qgGIBtABgF1FQABwD2sWhmEoBIAB6IAtAA4ATAE4K3AGzdlizQBYAjPoCsi7voA0IAJ4KAzIoomT3cwHYdixfferFAL4B1qiYuITE5FQ0dIwsbFyG-EggImJ0ktJyCErGzpqGqoaa7u7KmkWG1nYIyiYUytwmFX7mjZqK+kEh6Nj4RKSUkRA27LADUcRYYBgAbpA8yUKi4pkp2fJ13BSObib6Ksr6+o3u1QrHFO6Kqi6q+h0PBcrd4L3hE0OkIxTjkZQYCAMMBjT4UYiwMAERbSNKrKTrRDKPL6biqVTKTE6AyKExnWyIQz2PImZH7Y76Rz2VSvUJ9CKDcHfGy-MGA4Gg-4UACugggJAIYBhKThGQRoA23mUV3cRJMGMaWiK5wQxl0FBOJnsakMWmJD1p736XOGLL+jN5-LoKA4EEkYGis2EAGsHbzIWgCAzyABBLAEYRoYXLdIScWyBQuexXezcNyqdz6VSaeymFVE1TR-RlZSy3GGYp4w1hY2M02srmWgUJMBoNCBiiCBgCgBmgco7trXs+foDQb4sJWYqyDgLFEzufs2k0zWuePTqM044O2nMqh01y6wTeJe9XxIPzpuDIqAIFAgGFgTZINjYAAV69NYLBA7B2IJH3AX2hYBQDxBg1SIcwxHHJOm2bhSn2TxjmUex7BMFUp3cDVzCnApiR8SCTGLekwXLI8cBPFAzwvK9m1vG0H2EJ9vzfD8aK-V8mUDCBa0A0UQMRHJjiXLUmmpA5KQqQx8RqMwnG4QozDjbUzGKXCPhNZkKEI4jSMva9KKgajaNfd9P2fBs2OBQUOOAtYJQcfYdmg1wkzjbhqSQmzZOudxmiaAxDBw7dCL3JkDxZNTT3PTSKPvQy6IMxijLQVkSBgczQ0siMcmubZ-F8DzikaSlFBVeVo2gjpCk8dzAj8o0AoIo11LC8ibzYPAMDIblmzFGK9J-CgACpkvhUDUSueUigqfY0SkzQVXRPjvFlPF-BMYwtx6Xd8JUkKSIarTmta9rq0kLqmPi-qBxFCzw2yYaPPRYpblRdciRVTpoxW1c-EMNQ0UU0tJk2urQrI3abRatqOrDChZgwMAAHcABEBRIB8wGhuGxn2iHJChmGEahMB-UgAAxGGGAgWABuHbj5BcaVdSwtFTHKE4ZtuVDdQQ5oVFXX6aoBkt6uBiLQcxw6UBxuHEYIZHiDR2GMfBsWKFwEgbTAFG5fhmjYBJhhBX7JYgJSq6FB8fQKG8WNCng65iXsF6imcXMikcUlmkKXmNqC1TAe2oWmpFxWxQlvHBUJiASbAMm3zEIPIc1pGNdxymuKsnJ9mlMabj8eD7hudMtEMHZ3G1ODY2ze5fO3FBhDY+AUn8z5B2N0D5F1NQrgqOCvrlRRjBVNvtQaG5mljdcEPsT2uWocQ4lYG1m8G6n-CL9wu+JZEEL7+2CR47ZzDcRmNzqeDfLWvDlKCxeqbT+QEycS2pMzXwfGe3fdSXNyXALOpF0nqr1qXx+Oacg19U5pTvrGGUcoFRxhEumOCKEEylGxK0MwU8ywqRAQCIEYAwGpQ2JoOC44iGzTRCoL6Vh35yQ1KUdyGdFDuAwf9b22CeR8mrAvC6LdqYGHULKDyWoDCOFzAVd+Lh6hJlRFJbMahkyrR3BfTB3stoEHwSbHImZ95YWti-O2Kpyh8ScmvNe64mb-3PkpZRh5fYaUatpXSJ164hiXrfAokibglGTGibUjDCo6AtlzLQTkfK3DPooqxLCbECyBuFAOUAwYHRvkbVxECpQWx0c-W2b8ageRQhuPwiYiidANAApRUTgq2J2sLBJotg4J2lknOG6jW6lCXPBFa2ZZIVGmrvdEHiOYuA6OUNwzD9zRL6ILOJ2lElY3FprfG4dI7RxadTTE5tn5FUMHoTEahWYDKckM7mWgxlMhfAweYEAABKwhhDdn+KstxRxaGlFEo8PQLNd7yBOBoVwfh3ZTgsH4IIQQgA */ + /** @xstate-layout N4IgpgJg5mDOIC5RgHYCcCWBjAFgZQBc0wBDAWwDoMUMCMSAbDAL2qkOPIGIBtABgC6iUAAcA9rFoYxKYSAAeiALQB2PgDYKAJgCMATnXqV6rQFYt6gMw6ANCACeyvXpXa1AFh1qV7gBy+tLQBfILtUTFwOUkpqKUYWNijuHh0hJBBxSToZOUUEJXVfV2dLPj13Sz0dSp1TO0d8vQsKXy8td3ctMqKVFRCw9Gx8ImiqGjp41hQoADE0MTIAVTQGLgBXFbG4pmZIfjTRCSkc9LyCvT4KdSbrPj9C3xN65R93FvVTS3U+FUstUr0-XAg0iI3IFFgYAIaxEAHF5gBrADCYgYDDAWGyKC4EBkYDGADcxAj8ZDoXDESi0Risfs5JljrJTspKlormVOi5dLUXM98qYDBQ9KZ3GVTHxTB8-jogeEhklKGSYfDiVT0ZjpNiwGh5mgKCIGCQCAAzMRoRVQ5WU1Hq2mCelHLG5ZSilp8PhFLRNDl8Cx1BwvDRCnS6QouCU6O6ykHDTiUTgQexcWBg+MYsAYAl7e3pBlO5kIQw6CjucwBdxGPg6HSePmqawUSz+FSmLxN8qVaMRWOjBP2CGpqgQdHJwfEMl03OOzXOhDlPQUd3i4XqHm+dx8prFdcGFy+Swtnxd+Vj0iJgdxocjlOXmEQI1gSeHLIzgtKfwL0qt3zunwS-d8tWWi+BQvQVl4xgSoY6jHqCl59heox3kabA4nihLEviMKQmgBAKgAgpiZpPhk04nKAZySpcFg1BofBfP8KiAWUlgtLoVHiiKEGwT24IITeSEiPedDTFw2q6vqhommalDYdqeGpoRBDETmz6MrOSjWCBFYfJKVQGEY6iAZGrheiovgGK8nxRqEwLdgqFAIXKuBkKgBDrJssD2CgWAkXmr4UYgAQgVoKhdIU7iWWFm6seYIpehUHrXF4PEOU5MauSg7nCSQeBiBsWD4rgJDTI+qmkS+5EKEFZRCuUwXugE5RaHWFZChKoq1GYFS6Klp4kOezk4Jl2VGrl+VoIVFA5UiOAlTAflkUygUIMYbKFIEooqCGrZ6L4dbhVcFZlLpJitBWfXwWe-ZDSN00YLABokPYiQYGQaxSZqXAiPMhWwLAZqwBQA0QItlXLdVCCWF8VztOKniRntul8sYbqhRKTZdJUBiXb210ULdbn3Y9hovdMeBvR9KEyN9v1wADaBA8QZoQNqYPqQWpSuP4q7XKK3yPBuAYIKFmjev4pbmfR6juLjfH44TWXE09ZPsJTn00z9Yh-QzFDFaV7P5it1iseZvimHpagHk8wtKIKwo6d0Fg-h8ctpgNN0ZUTEAPSrr3vRr2JazrZp63NBupA64OztWxbXMBJ06Od-iWAdMORc15Qy62+iWG7jkK17Ss+yTz3+1TWK09r9Oh6z6IEGVBwVRzK1KLorFVpYHQhjofPtAdVglnF-iRnc9EWPn6XdndJd++T6vU0HdP-aHKYkAt5X+VVlFdBQsc-I8ljw02B2RUKkHuGolkhsBk+F9P3u+6T5eB1XId6shDeGwFkPvtDQpdArF0TiVgjK23aAuFw3wPTmThsYO+HsCZFwIMrZ+88A6L2TAvLEBdjTjhwN-beiBAhCjOtYAwDEQzrj5F3EC9UDwVF6AEWopgEGDWQagsu6CK6agoASDAYAADuAARMaAAFYg-ChFYIwTgqRIioQ0kgDMARDAICwEIRDSi5gKCfC9BcX0JgLh6EAl6c+5txR3DUMBPQedbJDTSvfIYM8n5cLVrI3h8jREEBIBIsA8iZE8JkGHeaYA-FeO1rAFRDAG5oE0RpfcphGw-DUOuJO5sVAmOFs0X08V9wm1KPROxAx7L9XYQ-YurjVYUw8cErxijMTKNUeowJgc+ECJEeIyRHT4lvgtsUSKIYwodQ0P6Bo+gQq6Eih0ecR9ehsM9hUlBs80HuKCSgdpQjhENIbhAFRYA1GwFaYvCgxpmlAwAFS9JWpGUCmSZaikCJKcUhhmKuHdPoXo5glwQIWUgpZFAyAlXXmwbx40CpwD1gwCQkAcRjTyhCoGYgRCoAALLApgCNa5v98m6PbkUa4B4NCp2FtM7QtRD7CjNvRWW9iYyOMQYrFBQKUAgumGChFk1IXItQHCnxnK-pQphei1lmK3LYsop0PeR8LKSlXPRcwfIyXsUpaYalXc-lMsBRi0F8KJqCp5diHKArIUgwlS8MKjZahGF+CkmlfIzolkyeUToXpqzm01RwllbKoAcv1dylFRq9UQumgcxR5r+S90XJ4PwndSidBJQ0EwSSj61FdeBfJIRbIoDEKzeA6QHGpiji3X+kZ-5fiTr+UUpgAh1kjP4Nw9FD6BEqEUfOsQJg7ESEWqc0c3x7WLGURG7R-jQyrHWZwC5vmVC+MYUsZh23jHoF26YcwFjLAYMWo2v9UklgsNcKwYV-hJzrI8S4wprUsWmRdOlpTLxKgpKqG0NIf5by0S6Fse8ih6Uir0YCxg6y-GLFUJ5-xjE-hlLek8l4oDWmpBqGQMwSAYAYBsMAW6f55BHaBG4vcDwiisUxW2u7PCBDuP4LoJkFkYaIY0c2yTvxVv-Im5QOch5mBNjLVcSM-kCXIDR99jQDwMcrT8atAFha2JAn8NQhLGoPN44ODAw50O9pLWcKwlxIqFF7sKMjvdmJmDYt4bj21dCAig3BPGiC+OySEihaYAmNIfE0GYaorw9xeFMRbFoPULhDPaPuT1SynP9obFzVJnh-BqqyQ0TS4ohRxXnAZb4rtLO8XduU5xj9S7VOwa+paCT9zJN6B6KLGTYvKHrU60srQDx7U8NtYL2XKm5ZficrxXT-EdNC63XomgWyrmhuZaoBgWrCxDJA8yFt3SX19BZYpdloPWayy5HLc81ltPqQ3Rpezzm9d-k8xsNbgKBCMBAsZiBJvmJm1Y+bfR0sMtW8NIm3qoC6v5f6-Nalt1nBHni6oBLbGyZY1DUKLRe75MPQEL4zW1tKzex98FXKgZYGhZCCAB3KIuEXO6D4FkygXEuwgTwKbe7XAyT4CwD2SnLfloyr1Or2XBpRxQQ1WPWNVD3l6NVooqhqBDHySUbwWzaaMNWZwNladWflgDBgWYIAACUxBiAUnGDn+RKiXBFAYLwVYXag6UKPS4pWmy+jCntT4WaghAA */ id: 'enrichStream', context: ({ input }) => ({ definition: input.definition, + dataSourcesRefs: [], grokCollection: new GrokCollection(), initialProcessorsRefs: [], processorsRefs: [], + urlState: defaultEnrichmentUrlState, }), - initial: 'initializing', + initial: 'initializingStream', states: { - initializing: { + initializingStream: { always: [ - { - target: 'resolvedRootStream', - guard: 'isRootStream', - }, - { target: 'setupGrokCollection' }, + { target: 'resolvedRootStream', guard: 'isRootStream' }, + { target: 'initializingFromUrl' }, ], }, + initializingFromUrl: { + invoke: { + src: 'initializeUrl', + }, + on: { + 'url.initialized': { + actions: [ + { type: 'storeUrlState', params: ({ event }) => event }, + { type: 'syncUrlState' }, + ], + target: 'setupGrokCollection', + }, + }, + }, setupGrokCollection: { invoke: { id: 'setupGrokCollection', @@ -211,22 +273,15 @@ export const streamEnrichmentMachine = setup({ input: ({ context }) => ({ grokCollection: context.grokCollection, }), - onDone: { - target: 'ready', - }, - onError: { - target: 'grokCollectionFailure', - }, + onDone: 'ready', + onError: 'grokCollectionFailure', }, }, grokCollectionFailure: {}, ready: { id: 'ready', type: 'parallel', - entry: [ - { type: 'stopProcessors' }, - { type: 'setupProcessors', params: ({ context }) => ({ definition: context.definition }) }, - ], + entry: [{ type: 'setupProcessors' }, { type: 'setupDataSources' }], on: { 'stream.received': { target: '#ready', @@ -279,59 +334,86 @@ export const streamEnrichmentMachine = setup({ }, enrichment: { type: 'parallel', + on: { + 'url.sync': { + actions: [ + { + type: 'storeUrlState', + params: ({ context }) => ({ + urlState: { v: 1, dataSources: getDataSourcesUrlState(context) }, + }), + }, + { type: 'syncUrlState' }, + ], + }, + 'dataSource.change': { + actions: raise({ type: 'url.sync' }), + }, + 'dataSource.dataChange': { + actions: [ + cancel('send-samples-to-simulator'), // Debounce samples sent to simulator on multiple data sources retrieval + { type: 'sendDataSourcesSamplesToSimulator' }, + ], + }, + }, states: { - displayingProcessors: { + displayingSimulation: { + initial: 'viewDataPreview', + entry: [{ type: 'spawnSimulationMachine' }], on: { 'processors.add': { guard: '!hasPendingDraft', - actions: [{ type: 'addProcessor', params: ({ event }) => event }], + actions: [ + { type: 'addProcessor', params: ({ event }) => event }, + { type: 'sendProcessorsEventToSimulator', params: ({ event }) => event }, + ], }, 'processors.reorder': { guard: 'hasMultipleProcessors', - actions: [{ type: 'reorderProcessors', params: ({ event }) => event }], + actions: [ + { type: 'reorderProcessors', params: ({ event }) => event }, + { type: 'sendProcessorsEventToSimulator', params: ({ event }) => event }, + ], }, + 'processor.change': [ + { + guard: { type: 'isStagedProcessor', params: ({ event }) => event }, + actions: [ + { type: 'sendProcessorsEventToSimulator', params: ({ event }) => event }, + ], + }, + { + guard: { type: 'isDraftProcessor', params: ({ event }) => event }, + actions: [ + { + type: 'updateGrokCollectionCustomPatterns', + params: ({ event }) => event, + }, + { type: 'sendProcessorsEventToSimulator', params: ({ event }) => event }, + ], + }, + ], 'processor.delete': { actions: [ - { type: 'stopProcessor', params: ({ event }) => event }, + stopChild(({ event }) => event.id), { type: 'deleteProcessor', params: ({ event }) => event }, + { type: 'sendProcessorsEventToSimulator', params: ({ event }) => event }, ], }, 'processor.stage': { - actions: [{ type: 'reassignProcessors' }], - }, - 'processor.update': { - actions: [{ type: 'reassignProcessors' }], - }, - 'processor.change': { - guard: { type: 'isDraftProcessor', params: ({ event }) => event }, - actions: [ - { - type: 'updateGrokCollectionCustomPatterns', - params: ({ event }) => event, - }, - ], - }, - }, - }, - displayingSimulation: { - entry: [{ type: 'spawnSimulationMachine' }], - initial: 'viewDataPreview', - on: { - 'processor.change': { - guard: { type: 'isStagedProcessor', params: ({ event }) => event }, actions: [ - { type: 'forwardProcessorsEventToSimulator', params: ({ event }) => event }, + { type: 'reassignProcessors' }, + { type: 'sendProcessorsEventToSimulator', params: ({ event }) => event }, ], }, - 'processor.*': { + 'processor.update': { actions: [ - { type: 'forwardProcessorsEventToSimulator', params: ({ event }) => event }, + { type: 'reassignProcessors' }, + { type: 'sendProcessorsEventToSimulator', params: ({ event }) => event }, ], }, - 'processors.*': { - actions: [ - { type: 'forwardProcessorsEventToSimulator', params: ({ event }) => event }, - ], + 'simulation.refresh': { + actions: [{ type: 'refreshDataSources' }], }, }, states: { @@ -356,6 +438,34 @@ export const streamEnrichmentMachine = setup({ }, }, }, + managingDataSources: { + initial: 'closed', + states: { + closed: { + on: { + 'dataSources.openManagement': 'open', + }, + }, + open: { + on: { + 'dataSources.closeManagement': 'closed', + 'dataSources.add': { + actions: [ + { type: 'addDataSource', params: ({ event }) => event }, + raise({ type: 'url.sync' }), + ], + }, + 'dataSource.delete': { + actions: [ + stopChild(({ event }) => event.id), + { type: 'deleteDataSource', params: ({ event }) => event }, + raise({ type: 'url.sync' }), + ], + }, + }, + }, + }, + }, }, }, }, @@ -370,17 +480,22 @@ export const createStreamEnrichmentMachineImplementations = ({ refreshDefinition, streamsRepositoryClient, core, - timeState$, + data, + urlStateStorageContainer, }: StreamEnrichmentServiceDependencies): MachineImplementationsFrom< typeof streamEnrichmentMachine > => ({ actors: { + initializeUrl: createUrlInitializerActor({ core, urlStateStorageContainer }), upsertStream: createUpsertStreamActor({ streamsRepositoryClient }), setupGrokCollection: setupGrokCollectionActor(), processorMachine, + dataSourceMachine: dataSourceMachine.provide( + createDataSourceMachineImplementations({ data, toasts: core.notifications.toasts }) + ), simulationMachine: simulationMachine.provide( createSimulationMachineImplementations({ - timeState$, + data, streamsRepositoryClient, toasts: core.notifications.toasts, }) @@ -388,6 +503,7 @@ export const createStreamEnrichmentMachineImplementations = ({ }, actions: { refreshDefinition, + syncUrlState: createUrlSyncAction({ urlStateStorageContainer }), notifyUpsertStreamSuccess: createUpsertStreamSuccessNofitier({ toasts: core.notifications.toasts, }), diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/types.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/types.ts index 2e14aa6a063d0..aed9c831649a9 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/types.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/types.ts @@ -8,19 +8,22 @@ import { CoreStart } from '@kbn/core/public'; import { StreamsRepositoryClient } from '@kbn/streams-plugin/public/api'; import { Streams } from '@kbn/streams-schema'; -import { TimeState } from '@kbn/es-query'; -import { BehaviorSubject } from 'rxjs'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { GrokCollection } from '@kbn/grok-ui'; +import { EnrichmentDataSource, EnrichmentUrlState } from '../../../../../../common/url_schema'; import { ProcessorDefinitionWithUIAttributes } from '../../types'; import { ProcessorActorRef, ProcessorToParentEvent } from '../processor_state_machine'; import { PreviewDocsFilterOption, SimulationActorRef } from '../simulation_state_machine'; import { MappedSchemaField } from '../../../schema_editor/types'; +import { DataSourceActorRef, DataSourceToParentEvent } from '../data_source_state_machine'; export interface StreamEnrichmentServiceDependencies { refreshDefinition: () => void; streamsRepositoryClient: StreamsRepositoryClient; core: CoreStart; - timeState$: BehaviorSubject; + data: DataPublicPluginStart; + urlStateStorageContainer: IKbnUrlStateStorage; } export interface StreamEnrichmentInput { @@ -30,18 +33,25 @@ export interface StreamEnrichmentInput { export interface StreamEnrichmentContextType { definition: Streams.ingest.all.GetResponse; initialProcessorsRefs: ProcessorActorRef[]; + dataSourcesRefs: DataSourceActorRef[]; processorsRefs: ProcessorActorRef[]; grokCollection: GrokCollection; simulatorRef?: SimulationActorRef; + urlState: EnrichmentUrlState; } export type StreamEnrichmentEvent = + | DataSourceToParentEvent | ProcessorToParentEvent | { type: 'stream.received'; definition: Streams.ingest.all.GetResponse } | { type: 'stream.reset' } | { type: 'stream.update' } + | { type: 'simulation.refresh' } | { type: 'simulation.viewDataPreview' } | { type: 'simulation.viewDetectedFields' } + | { type: 'dataSources.add'; dataSource: EnrichmentDataSource } + | { type: 'dataSources.closeManagement' } + | { type: 'dataSources.openManagement' } | { type: 'simulation.changePreviewDocsFilter'; filter: PreviewDocsFilterOption } | { type: 'simulation.fields.map'; field: MappedSchemaField } | { type: 'simulation.fields.unmap'; fieldName: string } @@ -49,4 +59,6 @@ export type StreamEnrichmentEvent = | { type: 'previewColumns.updateExplicitlyDisabledColumns'; columns: string[] } | { type: 'previewColumns.order'; columns: string[] } | { type: 'processors.add'; processor: ProcessorDefinitionWithUIAttributes } - | { type: 'processors.reorder'; processorsRefs: ProcessorActorRef[] }; + | { type: 'processors.reorder'; processorsRefs: ProcessorActorRef[] } + | { type: 'url.initialized'; urlState: EnrichmentUrlState } + | { type: 'url.sync' }; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/url_state_actor.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/url_state_actor.ts new file mode 100644 index 0000000000000..be863d73acb18 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/url_state_actor.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fromCallback } from 'xstate5'; +import { withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; +import { + ENRICHMENT_URL_STATE_KEY, + EnrichmentDataSource, + enrichmentUrlSchema, +} from '../../../../../../common/url_schema'; +import { StreamEnrichmentContextType, StreamEnrichmentServiceDependencies } from './types'; +import { defaultEnrichmentUrlState, defaultRandomSamplesDataSource } from './utils'; + +export function createUrlInitializerActor({ + core, + urlStateStorageContainer, +}: Pick) { + return fromCallback(({ sendBack }) => { + const urlStateValues = + urlStateStorageContainer.get(ENRICHMENT_URL_STATE_KEY) ?? undefined; + + if (!urlStateValues) { + return sendBack({ + type: 'url.initialized', + urlState: defaultEnrichmentUrlState, + }); + } + + const urlState = enrichmentUrlSchema.safeParse(urlStateValues); + + if (urlState.success) { + // Always add default random samples data source + if (!hasDefaultRandomSamplesDataSource(urlState.data.dataSources)) { + urlState.data.dataSources.unshift(defaultRandomSamplesDataSource); + } + + sendBack({ + type: 'url.initialized', + urlState: urlState.data, + }); + } else { + withNotifyOnErrors(core.notifications.toasts).onGetError( + new Error('The default state will be used as fallback.') + ); + sendBack({ + type: 'url.initialized', + urlState: defaultEnrichmentUrlState, + }); + } + }); +} + +const hasDefaultRandomSamplesDataSource = (dataSources: EnrichmentDataSource[]) => { + return dataSources.some((dataSource) => dataSource.type === 'random-samples'); +}; + +export function createUrlSyncAction({ + urlStateStorageContainer, +}: Pick) { + return ({ context }: { context: StreamEnrichmentContextType }) => { + urlStateStorageContainer.set(ENRICHMENT_URL_STATE_KEY, context.urlState, { + replace: true, + }); + }; +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/use_stream_enrichment.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/use_stream_enrichment.tsx index db3fe9779bcdc..9223d6fe8f919 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/use_stream_enrichment.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/use_stream_enrichment.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import { createActorContext, useSelector } from '@xstate5/react'; import { createConsoleInspector } from '@kbn/xstate-utils'; +import { EnrichmentDataSource } from '../../../../../../common/url_schema'; import { streamEnrichmentMachine, createStreamEnrichmentMachineImplementations, @@ -23,7 +24,7 @@ const consoleInspector = createConsoleInspector(); const StreamEnrichmentContext = createActorContext(streamEnrichmentMachine); -export const useStreamsEnrichmentSelector = StreamEnrichmentContext.useSelector; +export const useStreamEnrichmentSelector = StreamEnrichmentContext.useSelector; export type StreamEnrichmentEvents = ReturnType; @@ -49,6 +50,9 @@ export const useStreamEnrichmentEvents = () => { saveChanges: () => { service.send({ type: 'stream.update' }); }, + refreshSimulation: () => { + service.send({ type: 'simulation.refresh' }); + }, viewSimulationPreviewData: () => { service.send({ type: 'simulation.viewDataPreview' }); }, @@ -64,6 +68,15 @@ export const useStreamEnrichmentEvents = () => { unmapField: (fieldName: string) => { service.send({ type: 'simulation.fields.unmap', fieldName }); }, + openDataSourcesManagement: () => { + service.send({ type: 'dataSources.openManagement' }); + }, + closeDataSourcesManagement: () => { + service.send({ type: 'dataSources.closeManagement' }); + }, + addDataSource: (dataSource: EnrichmentDataSource) => { + service.send({ type: 'dataSources.add', dataSource }); + }, setExplicitlyEnabledPreviewColumns: (columns: string[]) => { service.send({ type: 'previewColumns.updateExplicitlyEnabledColumns', @@ -145,7 +158,7 @@ const ListenForDefinitionChanges = ({ }; export const useSimulatorRef = () => { - return useStreamsEnrichmentSelector((state) => state.context.simulatorRef); + return useStreamEnrichmentSelector((state) => state.context.simulatorRef); }; export const useSimulatorSelector = (selector: (snapshot: SimulationActorSnapshot) => T): T => { diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/utils.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/utils.ts index 097dc21405701..9af146db04ba8 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/utils.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/stream_enrichment_state_machine/utils.ts @@ -6,12 +6,75 @@ */ import { FieldDefinition, Streams } from '@kbn/streams-schema'; +import { i18n } from '@kbn/i18n'; +import { AssignArgs } from 'xstate5'; import { StreamEnrichmentContextType } from './types'; import { convertToFieldDefinition, getMappedSchemaFields, getUnmappedSchemaFields, } from '../simulation_state_machine'; +import { + EnrichmentUrlState, + KqlSamplesDataSource, + RandomSamplesDataSource, + CustomSamplesDataSource, + EnrichmentDataSource, +} from '../../../../../../common/url_schema'; +import { dataSourceConverter } from '../../utils'; + +export const defaultRandomSamplesDataSource: RandomSamplesDataSource = { + type: 'random-samples', + name: i18n.translate('xpack.streams.enrichment.dataSources.randomSamples.defaultName', { + defaultMessage: 'Random samples', + }), + enabled: true, +}; + +export const defaultKqlSamplesDataSource: KqlSamplesDataSource = { + type: 'kql-samples', + name: '', + enabled: true, + timeRange: { + from: 'now-15m', + to: 'now', + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, +}; + +export const defaultCustomSamplesDataSource: CustomSamplesDataSource = { + type: 'custom-samples', + name: '', + enabled: true, + documents: [], +}; + +export const defaultEnrichmentUrlState: EnrichmentUrlState = { + v: 1, + dataSources: [defaultRandomSamplesDataSource], +}; + +export function getDataSourcesUrlState(context: StreamEnrichmentContextType) { + const dataSources = context.dataSourcesRefs.map( + (dataSourceRef) => dataSourceRef.getSnapshot().context.dataSource + ); + + return dataSources + .filter((dataSource) => dataSource.type !== 'custom-samples') // Custom samples are not stored in the URL + .map(dataSourceConverter.toUrlSchema); +} + +export function getDataSourcesSamples(context: StreamEnrichmentContextType) { + const dataSourcesSnapshots = context.dataSourcesRefs + .map((dataSourceRef) => dataSourceRef.getSnapshot()) + .filter((snapshot) => snapshot.matches('enabled')); + + return dataSourcesSnapshots.flatMap((snapshot) => snapshot.context.data); +} export function getStagedProcessors(context: StreamEnrichmentContextType) { return context.processorsRefs @@ -52,3 +115,20 @@ export function getUpsertWiredFields( return { ...originalFieldDefinition, ...simulationMappedFieldDefinition }; } + +export const spawnDataSource = >( + dataSource: EnrichmentDataSource, + assignArgs: TAssignArgs +) => { + const { spawn, context, self } = assignArgs; + const dataSourceWithUIAttributes = dataSourceConverter.toUIDefinition(dataSource); + + return spawn('dataSourceMachine', { + id: dataSourceWithUIAttributes.id, + input: { + parentRef: self, + streamName: context.definition.stream.name, + dataSource: dataSourceWithUIAttributes, + }, + }); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/types.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/types.ts index eb9e6bf5f94e2..40a29f27054cd 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/types.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/types.ts @@ -12,9 +12,9 @@ import { ProcessorDefinition, ProcessorTypeOf, } from '@kbn/streams-schema'; - import { ManualIngestPipelineProcessorConfig } from '@kbn/streams-schema'; import { DraftGrokExpression } from '@kbn/grok-ui'; +import { EnrichmentDataSource } from '../../../../common/url_schema'; import { ConfigDrivenProcessorFormState } from './processors/config_driven/types'; export type WithUIAttributes = T & { @@ -22,6 +22,9 @@ export type WithUIAttributes = T & { type: ProcessorTypeOf; }; +/** + * Processors' types + */ export type ProcessorDefinitionWithUIAttributes = WithUIAttributes; export type GrokFormState = Omit & { @@ -52,3 +55,25 @@ export type ExtractBooleanFields = NonNullable< }[keyof TInput] : never >; + +/** + * Data sources types + */ +export type EnrichmentDataSourceWithUIAttributes = EnrichmentDataSource & { + id: string; +}; + +export type RandomSamplesDataSourceWithUIAttributes = Extract< + EnrichmentDataSourceWithUIAttributes, + { type: 'random-samples' } +>; + +export type KqlSamplesDataSourceWithUIAttributes = Extract< + EnrichmentDataSourceWithUIAttributes, + { type: 'kql-samples' } +>; + +export type CustomSamplesDataSourceWithUIAttributes = Extract< + EnrichmentDataSourceWithUIAttributes, + { type: 'custom-samples' } +>; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/utils.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/utils.ts index 46637d1ce41f5..e5531eef46a67 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/utils.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/utils.ts @@ -15,8 +15,9 @@ import { getProcessorType, } from '@kbn/streams-schema'; import { htmlIdGenerator } from '@elastic/eui'; +import { countBy, isEmpty, mapValues, omit, orderBy } from 'lodash'; import { DraftGrokExpression } from '@kbn/grok-ui'; -import { isEmpty, mapValues, omit, countBy, orderBy } from 'lodash'; +import { EnrichmentDataSource } from '../../../../common/url_schema'; import { DissectFormState, ProcessorDefinitionWithUIAttributes, @@ -25,6 +26,7 @@ import { WithUIAttributes, DateFormState, ManualIngestPipelineFormState, + EnrichmentDataSourceWithUIAttributes, } from './types'; import { ALWAYS_CONDITION } from '../../../util/condition'; import { configDrivenProcessors } from './processors/config_driven'; @@ -315,7 +317,8 @@ export const isManualIngestPipelineJsonProcessor = export const isGrokProcessor = createProcessorGuardByType('grok'); const createId = htmlIdGenerator(); -const toUIDefinition = ( + +const processorToUIDefinition = ( processor: TProcessorDefinition ): ProcessorDefinitionWithUIAttributes => ({ id: createId(), @@ -323,12 +326,14 @@ const toUIDefinition = ( ...processor, }); -const toAPIDefinition = (processor: ProcessorDefinitionWithUIAttributes): ProcessorDefinition => { +const processorToAPIDefinition = ( + processor: ProcessorDefinitionWithUIAttributes +): ProcessorDefinition => { const { id, type, ...processorConfig } = processor; return processorConfig; }; -const toSimulateDefinition = ( +const processorToSimulateDefinition = ( processor: ProcessorDefinitionWithUIAttributes ): ProcessorDefinitionWithId => { const { type, ...processorConfig } = processor; @@ -336,7 +341,26 @@ const toSimulateDefinition = ( }; export const processorConverter = { - toAPIDefinition, - toSimulateDefinition, - toUIDefinition, + toAPIDefinition: processorToAPIDefinition, + toSimulateDefinition: processorToSimulateDefinition, + toUIDefinition: processorToUIDefinition, +}; + +const dataSourceToUIDefinition = ( + dataSource: TEnrichementDataSource +): EnrichmentDataSourceWithUIAttributes => ({ + id: createId(), + ...dataSource, +}); + +const dataSourceToUrlSchema = ( + dataSourceWithUIAttributes: EnrichmentDataSourceWithUIAttributes +): EnrichmentDataSource => { + const { id, ...dataSource } = dataSourceWithUIAttributes; + return dataSource; +}; + +export const dataSourceConverter = { + toUIDefinition: dataSourceToUIDefinition, + toUrlSchema: dataSourceToUrlSchema, }; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/index.tsx index a77d609a695ea..5aaed0ea279da 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/index.tsx @@ -4,25 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { DataView } from '@kbn/data-views-plugin/common'; import React from 'react'; import { isEqual } from 'lodash'; import { TimeRange } from '@kbn/es-query'; import { getAbsoluteTimeRange } from '@kbn/data-plugin/common'; +import { + UncontrolledStreamsAppSearchBar, + UncontrolledStreamsAppSearchBarProps, +} from './uncontrolled_streams_app_bar'; import { useTimefilter } from '../../hooks/use_timefilter'; -import { UncontrolledStreamsAppSearchBar } from './uncontrolled_streams_app_bar'; -export interface StreamsAppSearchBarProps { - query?: string; - onQueryChange?: (payload: { query: string }) => void; - onQuerySubmit?: (payload: { query: string }) => void; - placeholder?: string; - dataViews?: DataView[]; - showSubmitButton?: boolean; - showDatePicker?: boolean; -} +export type StreamsAppSearchBarProps = UncontrolledStreamsAppSearchBarProps; -// if the absolute time stays the same +// If the absolute time stays the same function needsRefresh(left: TimeRange, right: TimeRange) { const forceNow = new Date(); const leftAbsolute = getAbsoluteTimeRange(left, { forceNow }); @@ -31,15 +25,7 @@ function needsRefresh(left: TimeRange, right: TimeRange) { return isEqual(leftAbsolute, rightAbsolute); } -export function StreamsAppSearchBar({ - onQueryChange, - onQuerySubmit, - query, - placeholder, - dataViews, - showDatePicker = false, - showSubmitButton = true, -}: StreamsAppSearchBarProps) { +export function StreamsAppSearchBar({ onQuerySubmit, ...props }: StreamsAppSearchBarProps) { const { timeState, setTime, refresh } = useTimefilter(); function refreshOrSetTime(next: TimeRange) { @@ -52,21 +38,15 @@ export function StreamsAppSearchBar({ return ( { + onQuerySubmit={({ dateRange, query }, isUpdate) => { if (dateRange) { refreshOrSetTime(dateRange); } - onQuerySubmit?.({ query: nextQuery }); - }} - onQueryChange={({ query: nextQuery }) => { - onQueryChange?.({ query: nextQuery }); + onQuerySubmit?.({ dateRange, query }, isUpdate); }} - query={query} - showSubmitButton={showSubmitButton} - dateRangeFrom={showDatePicker ? timeState.timeRange.from : undefined} - dateRangeTo={showDatePicker ? timeState.timeRange.to : undefined} - placeholder={placeholder} - dataViews={dataViews} + dateRangeFrom={timeState.timeRange.from} + dateRangeTo={timeState.timeRange.to} + {...props} /> ); } diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/uncontrolled_streams_app_bar.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/uncontrolled_streams_app_bar.tsx index 962f502d32bac..b41014a717c49 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/uncontrolled_streams_app_bar.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/uncontrolled_streams_app_bar.tsx @@ -4,71 +4,26 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { TimeRange } from '@kbn/es-query'; -import { SearchBar } from '@kbn/unified-search-plugin/public'; -import React, { useMemo } from 'react'; -import type { DataView } from '@kbn/data-views-plugin/common'; +import React from 'react'; +import { StatefulSearchBarProps } from '@kbn/unified-search-plugin/public'; import { useKibana } from '../../hooks/use_kibana'; -export interface UncontrolledStreamsAppSearchBarProps { - query?: string; - dateRangeFrom?: string; - dateRangeTo?: string; - onQueryChange?: (payload: { dateRange?: TimeRange; query: string }) => void; - onQuerySubmit?: (payload: { dateRange?: TimeRange; query: string }, isUpdate?: boolean) => void; - onRefresh?: Required>['onRefresh']; - placeholder?: string; - dataViews?: DataView[]; - showSubmitButton?: boolean; -} - -export function UncontrolledStreamsAppSearchBar({ - dateRangeFrom, - dateRangeTo, - onQueryChange, - onQuerySubmit, - onRefresh, - query, - placeholder, - dataViews, - showSubmitButton = true, -}: UncontrolledStreamsAppSearchBarProps) { - const { - dependencies: { - start: { unifiedSearch }, - }, - } = useKibana(); - - const queryObj = useMemo(() => (query ? { query, language: 'kuery' } : undefined), [query]); +export type UncontrolledStreamsAppSearchBarProps = Omit; - const showQueryInput = query === undefined; +export function UncontrolledStreamsAppSearchBar(props: UncontrolledStreamsAppSearchBarProps) { + const { unifiedSearch } = useKibana().dependencies.start; return ( { - onQuerySubmit?.( - { dateRange, query: (nextQuery?.query as string | undefined) ?? '' }, - isUpdate - ); - }} - onQueryChange={({ dateRange, query: nextQuery }) => { - onQueryChange?.({ dateRange, query: (nextQuery?.query as string | undefined) ?? '' }); - }} - query={queryObj} - showQueryInput={showQueryInput} + showDatePicker={false} showFilterBar={false} showQueryMenu={false} - showDatePicker={Boolean(dateRangeFrom && dateRangeTo)} - showSubmitButton={showSubmitButton} + showQueryInput={false} submitButtonStyle="iconOnly" - dateRangeFrom={dateRangeFrom} - dateRangeTo={dateRangeTo} - onRefresh={onRefresh} displayStyle="inPage" disableQueryLanguageSwitcher - placeholder={placeholder} - indexPatterns={dataViews} + {...props} /> ); } diff --git a/x-pack/platform/plugins/shared/streams_app/public/util/kbn_url_state_context.ts b/x-pack/platform/plugins/shared/streams_app/public/util/kbn_url_state_context.ts new file mode 100644 index 0000000000000..25a9ae149640b --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/util/kbn_url_state_context.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; +import createContainer from 'constate'; +import { useState } from 'react'; +import { useKibana } from '../hooks/use_kibana'; + +const useKbnUrlStateStorageFromRouter = () => { + const { + appParams: { history }, + core: { + notifications: { toasts }, + uiSettings, + }, + } = useKibana(); + + const [urlStateStorage] = useState(() => + createKbnUrlStateStorage({ + history, + useHash: uiSettings.get('state:storeInSessionStorage'), + useHashQuery: false, + ...withNotifyOnErrors(toasts), + }) + ); + + return urlStateStorage; +}; + +export const [KbnUrlStateStorageFromRouterProvider, useKbnUrlStateStorageFromRouterContext] = + createContainer(useKbnUrlStateStorageFromRouter); diff --git a/x-pack/platform/plugins/shared/streams_app/tsconfig.json b/x-pack/platform/plugins/shared/streams_app/tsconfig.json index ca5c99649ce0c..28441aa4b15ae 100644 --- a/x-pack/platform/plugins/shared/streams_app/tsconfig.json +++ b/x-pack/platform/plugins/shared/streams_app/tsconfig.json @@ -71,6 +71,8 @@ "@kbn/charts-theme", "@kbn/grok-ui", "@kbn/scout", + "@kbn/zod", + "@kbn/kibana-utils-plugin", "@kbn/field-types", "@kbn/field-formats-plugin" ]