From 5974e4b5ce4a9aef130e62a14a445f9e346867a7 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 1 Apr 2025 11:47:31 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=8A=20Streams:=20Selectors=20for=20der?= =?UTF-8?q?ived=20samples=20(#213638)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplified massively from first state and just plugging in reselect in places where that's suitable (here to calculate the currently relevant sample documents). Also does a drive-by layout fix. ~Introduces a new xstate helper for derived data.~ ~In most cases, the actor and state machine model of xstate is great, but for derived data using pure functions, the semantics of the `useMemo` hook with defined dependencies is often easier to understand and eliminates the risk of forgetting to update the derived data correctly in some cases.~ ~It's about using the right tool for the right job - you don't need to choose between the dependency list of useMemo and the actor model of xstate, you can use what fits the case, without compromising performance.~ ~This is the API:~ ```ts const myActorContext = withMemoizedSelectors( createActorContext(myMachine), { derivedView: createSelector( [ (ctx: MyContextType) => { return ctx.dependency1; }, (ctx: MyContextType) => ctx.dependency2, ], (dependency1, dependency2) => { return // expensive calculation only running when necessary } ), }, (context) => (context.subMachine ? [context.subMachine] : []) // optional subscribe to changes of submachines as well ); // in react use useMemoizedSelector hook // this will cause the component to rerender if the selector is returning a new value myActorContext.useMemoizedSelector('derivedView') ``` ~This is using reselect to declare the dependencies similar to a react useMemo hook - the actual selector will only run if the dependencies change, leading to similar semantics as useMemo, with the additional benefit that if the value is used in multiple places, it's still just calculated once. The component calling `withMemoizedSelectors` only re-renders if the value returned by the selector changes. The selector itself only re-runs if one of the declared dependencies changes.~ ~Everything is type-safe by capturing the types of the reselect selector object via inferred type param and using it in the `useMemoizedSelector` type.~ (cherry picked from commit c5e0b05454b124949de754556ec8ba5289445ab3) --- .../processor_outcome_preview.tsx | 5 +++- .../processors/grok/grok_ai_suggestions.tsx | 16 ++++++++++-- .../processors/index.tsx | 4 +-- .../simulation_state_machine/selectors.ts | 26 +++++++++++++++++++ .../simulation_state_machine.ts | 24 ++++------------- .../stream_enrichment_state_machine.ts | 6 ++--- .../stream_enrichment_state_machine/types.ts | 2 +- 7 files changed, 55 insertions(+), 28 deletions(-) create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/selectors.ts 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 d217520452219..4f75bb6936830 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 @@ -30,6 +30,7 @@ import { getTableColumns, previewDocsFilterOptions, } from './state_management/simulation_state_machine'; +import { selectPreviewDocuments } from './state_management/simulation_state_machine/selectors'; export const ProcessorOutcomePreview = () => { const isLoading = useSimulatorSelector( @@ -140,7 +141,9 @@ const OutcomePreviewTable = () => { const processors = useSimulatorSelector((state) => state.context.processors); const detectedFields = useSimulatorSelector((state) => state.context.simulation?.detected_fields); const previewDocsFilter = useSimulatorSelector((state) => state.context.previewDocsFilter); - const previewDocuments = useSimulatorSelector((state) => state.context.previewDocuments); + const previewDocuments = useSimulatorSelector((snapshot) => + selectPreviewDocuments(snapshot.context) + ); const previewColumns = useMemo( () => getTableColumns(processors, detectedFields ?? [], previewDocsFilter), diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/grok/grok_ai_suggestions.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/grok/grok_ai_suggestions.tsx index d71afb16c849b..6df68970463ef 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/grok/grok_ai_suggestions.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/grok/grok_ai_suggestions.tsx @@ -29,10 +29,12 @@ import type { FindActionResult } from '@kbn/actions-plugin/server'; import { UseGenAIConnectorsResult } from '@kbn/observability-ai-assistant-plugin/public/hooks/use_genai_connectors'; import { useAbortController, useBoolean } from '@kbn/react-hooks'; import useObservable from 'react-use/lib/useObservable'; +import { css } from '@emotion/css'; import { useStreamDetail } from '../../../../../hooks/use_stream_detail'; import { useKibana } from '../../../../../hooks/use_kibana'; import { GrokFormState, ProcessorFormState } from '../../types'; import { useSimulatorSelector } from '../../state_management/stream_enrichment_state_machine'; +import { selectPreviewDocuments } from '../../state_management/simulation_state_machine/selectors'; const RefreshButton = ({ generatePatterns, @@ -353,7 +355,15 @@ function InnerGrokAiSuggestions({ return ( <> {content != null && ( - + {content} )} @@ -379,7 +389,9 @@ export function GrokAiSuggestions() { } = useKibana(); const { enabled: isAiEnabled, couldBeEnabled } = useAiEnabled(); const { definition } = useStreamDetail(); - const previewDocuments = useSimulatorSelector((state) => state.context.previewDocuments); + const previewDocuments = useSimulatorSelector((snapshot) => + selectPreviewDocuments(snapshot.context) + ); if (!isAiEnabled && couldBeEnabled) { return ( 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 29fcb7dc99aaa..1c3b7133868c3 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 @@ -44,7 +44,7 @@ import { useStreamEnrichmentEvents, useStreamsEnrichmentSelector, useSimulatorSelector, - StreamEnrichmentContext, + StreamEnrichmentContextType, } from '../state_management/stream_enrichment_state_machine'; import { ProcessorMetrics } from '../state_management/simulation_state_machine'; import { DateProcessorForm } from './date'; @@ -194,7 +194,7 @@ const createDraftProcessorFromForm = ( }; export interface EditProcessorPanelProps { - processorRef: StreamEnrichmentContext['processorsRefs'][number]; + processorRef: StreamEnrichmentContextType['processorsRefs'][number]; processorMetrics?: ProcessorMetrics; } diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/selectors.ts b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/selectors.ts new file mode 100644 index 0000000000000..e35e379ce0afd --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/state_management/simulation_state_machine/selectors.ts @@ -0,0 +1,26 @@ +/* + * 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 { createSelector } from 'reselect'; +import { filterSimulationDocuments } from './utils'; +import { SimulationActorSnapshot } from './simulation_state_machine'; + +const EMPTY_ARRAY: [] = []; + +export const selectPreviewDocuments = createSelector( + [ + (snapshot: SimulationActorSnapshot['context']) => snapshot.samples, + (snapshot: SimulationActorSnapshot['context']) => snapshot.previewDocsFilter, + (snapshot: SimulationActorSnapshot['context']) => snapshot.simulation?.documents, + ], + (samples, previewDocsFilter, documents) => { + return ( + (previewDocsFilter && documents + ? filterSimulationDocuments(documents, previewDocsFilter) + : samples) || EMPTY_ARRAY + ); + } +); 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 c250e35ef2c4c..eb343e452db8a 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 @@ -30,7 +30,7 @@ import { createSimulationRunnerActor, createSimulationRunFailureNofitier, } from './simulation_runner_actor'; -import { filterSimulationDocuments, composeSamplingCondition } from './utils'; +import { composeSamplingCondition } from './utils'; export type SimulationActorRef = ActorRefFrom; export type SimulationActorSnapshot = SnapshotFrom; @@ -72,13 +72,6 @@ export const simulationMachine = setup({ storeSimulation: assign((_, params: { simulation: Simulation | undefined }) => ({ simulation: params.simulation, })), - derivePreviewDocuments: assign(({ context }) => { - return { - previewDocuments: context.simulation - ? filterSimulationDocuments(context.simulation.documents, context.previewDocsFilter) - : context.samples, - }; - }), deriveSamplingCondition: assign(({ context }) => ({ samplingCondition: composeSamplingCondition(context.processors), })), @@ -125,14 +118,11 @@ export const simulationMachine = setup({ on: { 'dateRange.update': '.loadingSamples', 'simulation.changePreviewDocsFilter': { - actions: [ - { type: 'storePreviewDocsFilter', params: ({ event }) => event }, - { type: 'derivePreviewDocuments' }, - ], + actions: [{ type: 'storePreviewDocsFilter', params: ({ event }) => event }], }, 'simulation.reset': { target: '.idle', - actions: [{ type: 'resetSimulation' }, { type: 'derivePreviewDocuments' }], + actions: [{ type: 'resetSimulation' }], }, // Handle adding/reordering processors 'processors.*': { @@ -158,7 +148,7 @@ export const simulationMachine = setup({ }, { target: '.idle', - actions: [{ type: 'resetSimulation' }, { type: 'derivePreviewDocuments' }], + actions: [{ type: 'resetSimulation' }], }, ], }, @@ -210,10 +200,7 @@ export const simulationMachine = setup({ }), onDone: { target: 'assertingSimulationRequirements', - actions: [ - { type: 'storeSamples', params: ({ event }) => ({ samples: event.output }) }, - { type: 'derivePreviewDocuments' }, - ], + actions: [{ type: 'storeSamples', params: ({ event }) => ({ samples: event.output }) }], }, onError: { target: 'idle', @@ -251,7 +238,6 @@ export const simulationMachine = setup({ target: 'idle', actions: [ { type: 'storeSimulation', params: ({ event }) => ({ simulation: event.output }) }, - { type: 'derivePreviewDocuments' }, ], }, onError: { 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 f814dc958d837..5b9190735687c 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 @@ -23,7 +23,7 @@ import { } from '@kbn/streams-schema'; import { htmlIdGenerator } from '@elastic/eui'; import { - StreamEnrichmentContext, + StreamEnrichmentContextType, StreamEnrichmentEvent, StreamEnrichmentInput, StreamEnrichmentServiceDependencies, @@ -49,7 +49,7 @@ export type StreamEnrichmentActorRef = ActorRefFrom proc.getSnapshot()) .filter((proc) => proc.context.isNew) 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 c335d4e63820c..c20dc0631e5d5 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 @@ -24,7 +24,7 @@ export interface StreamEnrichmentInput { definition: IngestStreamGetResponse; } -export interface StreamEnrichmentContext { +export interface StreamEnrichmentContextType { definition: IngestStreamGetResponse; initialProcessorsRefs: ProcessorActorRef[]; processorsRefs: ProcessorActorRef[];