[8.x] 🌊 Streams: Selectors for derived samples (#213638) (#216611)

# Backport

This will backport the following commits from `main` to `8.x`:
- [🌊 Streams: Selectors for derived samples
(#213638)](https://github.com/elastic/kibana/pull/213638)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Joe
Reuter","email":"johannes.reuter@elastic.co"},"sourceCommit":{"committedDate":"2025-04-01T09:47:31Z","message":"🌊
Streams: Selectors for derived samples (#213638)\n\nSimplified massively
from first state and just plugging in reselect in\nplaces where that's
suitable (here to calculate the currently relevant\nsample
documents).\n\nAlso does a drive-by layout fix.\n\n~Introduces a new
xstate helper for derived data.~\n\n~In most cases, the actor and state
machine model of xstate is great,\nbut for derived data using pure
functions, the semantics of the\n`useMemo` hook with defined
dependencies is often easier to understand\nand eliminates the risk of
forgetting to update the derived data\ncorrectly in some
cases.~\n\n~It's about using the right tool for the right job - you
don't need to\nchoose between the dependency list of useMemo and the
actor model of\nxstate, you can use what fits the case, without
compromising\nperformance.~\n\n~This is the API:~\n```ts\nconst
myActorContext = withMemoizedSelectors(\n
createActorContext(myMachine),\n {\n derivedView: createSelector(\n [\n
(ctx: MyContextType) => {\n return ctx.dependency1;\n },\n (ctx:
MyContextType) =>\n ctx.dependency2,\n ],\n (dependency1, dependency2)
=> {\n return // expensive calculation only running when necessary\n }\n
),\n },\n (context) => (context.subMachine ? [context.subMachine] : [])
// optional subscribe to changes of submachines as well\n);\n\n\n// in
react use useMemoizedSelector hook\n// this will cause the component to
rerender if the selector is returning a new
value\nmyActorContext.useMemoizedSelector('derivedView')\n```\n\n~This
is using reselect to declare the dependencies similar to a
react\nuseMemo hook - the actual selector will only run if the
dependencies\nchange, leading to similar semantics as useMemo, with the
additional\nbenefit that if the value is used in multiple places, it's
still just\ncalculated once. The component calling
`withMemoizedSelectors` only\nre-renders if the value returned by the
selector changes. The selector\nitself only re-runs if one of the
declared dependencies changes.~\n\n~Everything is type-safe by capturing
the types of the reselect selector\nobject via inferred type param and
using it in the
`useMemoizedSelector`\ntype.~","sha":"c5e0b05454b124949de754556ec8ba5289445ab3","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:obs-ux-logs","backport:version","Feature:Streams","v9.1.0","v8.19.0"],"title":"🌊
Streams: Selectors for derived
samples","number":213638,"url":"https://github.com/elastic/kibana/pull/213638","mergeCommit":{"message":"🌊
Streams: Selectors for derived samples (#213638)\n\nSimplified massively
from first state and just plugging in reselect in\nplaces where that's
suitable (here to calculate the currently relevant\nsample
documents).\n\nAlso does a drive-by layout fix.\n\n~Introduces a new
xstate helper for derived data.~\n\n~In most cases, the actor and state
machine model of xstate is great,\nbut for derived data using pure
functions, the semantics of the\n`useMemo` hook with defined
dependencies is often easier to understand\nand eliminates the risk of
forgetting to update the derived data\ncorrectly in some
cases.~\n\n~It's about using the right tool for the right job - you
don't need to\nchoose between the dependency list of useMemo and the
actor model of\nxstate, you can use what fits the case, without
compromising\nperformance.~\n\n~This is the API:~\n```ts\nconst
myActorContext = withMemoizedSelectors(\n
createActorContext(myMachine),\n {\n derivedView: createSelector(\n [\n
(ctx: MyContextType) => {\n return ctx.dependency1;\n },\n (ctx:
MyContextType) =>\n ctx.dependency2,\n ],\n (dependency1, dependency2)
=> {\n return // expensive calculation only running when necessary\n }\n
),\n },\n (context) => (context.subMachine ? [context.subMachine] : [])
// optional subscribe to changes of submachines as well\n);\n\n\n// in
react use useMemoizedSelector hook\n// this will cause the component to
rerender if the selector is returning a new
value\nmyActorContext.useMemoizedSelector('derivedView')\n```\n\n~This
is using reselect to declare the dependencies similar to a
react\nuseMemo hook - the actual selector will only run if the
dependencies\nchange, leading to similar semantics as useMemo, with the
additional\nbenefit that if the value is used in multiple places, it's
still just\ncalculated once. The component calling
`withMemoizedSelectors` only\nre-renders if the value returned by the
selector changes. The selector\nitself only re-runs if one of the
declared dependencies changes.~\n\n~Everything is type-safe by capturing
the types of the reselect selector\nobject via inferred type param and
using it in the
`useMemoizedSelector`\ntype.~","sha":"c5e0b05454b124949de754556ec8ba5289445ab3"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/213638","number":213638,"mergeCommit":{"message":"🌊
Streams: Selectors for derived samples (#213638)\n\nSimplified massively
from first state and just plugging in reselect in\nplaces where that's
suitable (here to calculate the currently relevant\nsample
documents).\n\nAlso does a drive-by layout fix.\n\n~Introduces a new
xstate helper for derived data.~\n\n~In most cases, the actor and state
machine model of xstate is great,\nbut for derived data using pure
functions, the semantics of the\n`useMemo` hook with defined
dependencies is often easier to understand\nand eliminates the risk of
forgetting to update the derived data\ncorrectly in some
cases.~\n\n~It's about using the right tool for the right job - you
don't need to\nchoose between the dependency list of useMemo and the
actor model of\nxstate, you can use what fits the case, without
compromising\nperformance.~\n\n~This is the API:~\n```ts\nconst
myActorContext = withMemoizedSelectors(\n
createActorContext(myMachine),\n {\n derivedView: createSelector(\n [\n
(ctx: MyContextType) => {\n return ctx.dependency1;\n },\n (ctx:
MyContextType) =>\n ctx.dependency2,\n ],\n (dependency1, dependency2)
=> {\n return // expensive calculation only running when necessary\n }\n
),\n },\n (context) => (context.subMachine ? [context.subMachine] : [])
// optional subscribe to changes of submachines as well\n);\n\n\n// in
react use useMemoizedSelector hook\n// this will cause the component to
rerender if the selector is returning a new
value\nmyActorContext.useMemoizedSelector('derivedView')\n```\n\n~This
is using reselect to declare the dependencies similar to a
react\nuseMemo hook - the actual selector will only run if the
dependencies\nchange, leading to similar semantics as useMemo, with the
additional\nbenefit that if the value is used in multiple places, it's
still just\ncalculated once. The component calling
`withMemoizedSelectors` only\nre-renders if the value returned by the
selector changes. The selector\nitself only re-runs if one of the
declared dependencies changes.~\n\n~Everything is type-safe by capturing
the types of the reselect selector\nobject via inferred type param and
using it in the
`useMemoizedSelector`\ntype.~","sha":"c5e0b05454b124949de754556ec8ba5289445ab3"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Joe Reuter <johannes.reuter@elastic.co>
This commit is contained in:
Kibana Machine 2025-04-01 18:27:43 +02:00 committed by GitHub
parent 189a1eb14a
commit 442330ad94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 55 additions and 28 deletions

View file

@ -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),

View file

@ -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 && (
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexGroup
direction="column"
gutterSize="m"
// make sure the content is always filling the full width so the
// refresh button is rendered below in all cases
className={css`
width: 100%;
`}
>
{content}
</EuiFlexGroup>
)}
@ -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 (

View file

@ -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;
}

View file

@ -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
);
}
);

View file

@ -30,7 +30,7 @@ import {
createSimulationRunnerActor,
createSimulationRunFailureNofitier,
} from './simulation_runner_actor';
import { filterSimulationDocuments, composeSamplingCondition } from './utils';
import { composeSamplingCondition } from './utils';
export type SimulationActorRef = ActorRefFrom<typeof simulationMachine>;
export type SimulationActorSnapshot = SnapshotFrom<typeof simulationMachine>;
@ -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: {

View file

@ -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<typeof streamEnrichmentMachi
export const streamEnrichmentMachine = setup({
types: {
input: {} as StreamEnrichmentInput,
context: {} as StreamEnrichmentContext,
context: {} as StreamEnrichmentContextType,
events: {} as StreamEnrichmentEvent,
},
actors: {
@ -346,7 +346,7 @@ export const createStreamEnrichmentMachineImplementations = ({
},
});
function getStagedProcessors(context: StreamEnrichmentContext) {
function getStagedProcessors(context: StreamEnrichmentContextType) {
return context.processorsRefs
.map((proc) => proc.getSnapshot())
.filter((proc) => proc.context.isNew)

View file

@ -24,7 +24,7 @@ export interface StreamEnrichmentInput {
definition: IngestStreamGetResponse;
}
export interface StreamEnrichmentContext {
export interface StreamEnrichmentContextType {
definition: IngestStreamGetResponse;
initialProcessorsRefs: ProcessorActorRef[];
processorsRefs: ProcessorActorRef[];