From ffa8253ab4e8655d4699dd2097a2a082ce3b2ca5 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Mon, 18 Jul 2022 16:22:52 -0400 Subject: [PATCH] Track Input Output and componentState in redux for options list, range slider, and control group --- .../control_types/options_list/types.ts | 4 +- .../component/control_frame_component.tsx | 8 +-- .../component/control_group_component.tsx | 14 +++-- .../component/control_group_sortable_item.tsx | 11 ++-- .../control_group/editor/edit_control.tsx | 6 +- .../embeddable/control_group_container.tsx | 61 +++++++++++-------- .../state/control_group_reducers.ts | 12 ++-- .../controls/public/control_group/types.ts | 3 +- .../options_list/options_list_component.tsx | 14 +++-- .../options_list/options_list_embeddable.tsx | 14 +++-- .../options_list_popover_component.tsx | 16 ++--- .../options_list/options_list_reducers.ts | 35 ++++++----- .../control_types/options_list/types.ts | 8 ++- .../range_slider/range_slider_embeddable.tsx | 34 +++++------ .../range_slider/range_slider_popover.tsx | 36 +++++++++-- .../range_slider/range_slider_reducers.ts | 27 +++----- .../control_types/range_slider/types.ts | 5 +- .../control_types/time_slider/time_slider.tsx | 4 +- .../time_slider/time_slider_reducers.ts | 2 +- .../public/services/kibana/options_list.ts | 2 +- src/plugins/controls/public/types.ts | 4 +- .../hooks/use_dashboard_app_state.ts | 12 ++-- .../lib/sync_dashboard_data_views.ts | 57 +++++++++-------- .../public/lib/embeddables/embeddable.tsx | 1 - .../clean_redux_embeddable_state.ts | 49 +++++++++++++++ .../create_redux_embeddable_tools.tsx | 35 ++++------- .../sync_redux_embeddable.ts | 44 ++++++++++--- .../public/redux_embeddables/types.ts | 12 ++-- 28 files changed, 320 insertions(+), 210 deletions(-) create mode 100644 src/plugins/presentation_util/public/redux_embeddables/clean_redux_embeddable_state.ts diff --git a/src/plugins/controls/common/control_types/options_list/types.ts b/src/plugins/controls/common/control_types/options_list/types.ts index f93646b3ab2d38..46e4aab854d0d8 100644 --- a/src/plugins/controls/common/control_types/options_list/types.ts +++ b/src/plugins/controls/common/control_types/options_list/types.ts @@ -7,7 +7,7 @@ */ import type { Filter, Query, BoolQuery, TimeRange } from '@kbn/es-query'; -import { FieldSpec, DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import { FieldSpec, DataView } from '@kbn/data-views-plugin/common'; import { DataControlInput } from '../../types'; @@ -19,7 +19,7 @@ export interface OptionsListEmbeddableInput extends DataControlInput { singleSelect?: boolean; } -export type OptionsListField = DataViewField & { +export type OptionsListField = FieldSpec & { textFieldName?: string; parentFieldName?: string; childFieldName?: string; diff --git a/src/plugins/controls/public/control_group/component/control_frame_component.tsx b/src/plugins/controls/public/control_group/component/control_frame_component.tsx index 4a4550f54d5193..2dd3bafa87da78 100644 --- a/src/plugins/controls/public/control_group/component/control_frame_component.tsx +++ b/src/plugins/controls/public/control_group/component/control_frame_component.tsx @@ -19,7 +19,7 @@ import { import { useReduxContainerContext } from '@kbn/presentation-util-plugin/public'; import { ErrorEmbeddable } from '@kbn/embeddable-plugin/public'; -import { ControlGroupInput } from '../types'; +import { ControlGroupReduxState } from '../types'; import { pluginServices } from '../../services'; import { EditControlButton } from '../editor/edit_control'; import { ControlGroupStrings } from '../control_group_strings'; @@ -42,11 +42,11 @@ export const ControlFrame = ({ const [hasFatalError, setHasFatalError] = useState(false); const { - useEmbeddableSelector, + useEmbeddableSelector: select, containerActions: { untilEmbeddableLoaded, removeEmbeddable }, - } = useReduxContainerContext(); + } = useReduxContainerContext(); - const { controlStyle } = useEmbeddableSelector((state) => state); + const controlStyle = select((state) => state.explicitInput.controlStyle); // Controls Services Context const { overlays } = pluginServices.getHooks(); diff --git a/src/plugins/controls/public/control_group/component/control_group_component.tsx b/src/plugins/controls/public/control_group/component/control_group_component.tsx index 0d7eb4a4dd962a..4ce46ca9f023d5 100644 --- a/src/plugins/controls/public/control_group/component/control_group_component.tsx +++ b/src/plugins/controls/public/control_group/component/control_group_component.tsx @@ -28,27 +28,31 @@ import { useSensors, LayoutMeasuringStrategy, } from '@dnd-kit/core'; + import { ViewMode } from '@kbn/embeddable-plugin/public'; import { useReduxContainerContext } from '@kbn/presentation-util-plugin/public'; -import { ControlGroupInput } from '../types'; + +import { ControlGroupReduxState } from '../types'; import { controlGroupReducers } from '../state/control_group_reducers'; import { ControlClone, SortableControl } from './control_group_sortable_item'; export const ControlGroup = () => { // Redux embeddable container Context const reduxContainerContext = useReduxContainerContext< - ControlGroupInput, + ControlGroupReduxState, typeof controlGroupReducers >(); const { - useEmbeddableSelector, - useEmbeddableDispatch, actions: { setControlOrders }, + useEmbeddableSelector: select, + useEmbeddableDispatch, } = reduxContainerContext; const dispatch = useEmbeddableDispatch(); // current state - const { panels, viewMode, controlStyle } = useEmbeddableSelector((state) => state); + const panels = select((state) => state.explicitInput.panels); + const viewMode = select((state) => state.explicitInput.viewMode); + const controlStyle = select((state) => state.explicitInput.controlStyle); const isEditable = viewMode === ViewMode.EDIT; diff --git a/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx b/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx index e523cecb708964..43907b95a893a4 100644 --- a/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx +++ b/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx @@ -13,8 +13,8 @@ import { CSS } from '@dnd-kit/utilities'; import classNames from 'classnames'; import { useReduxContainerContext } from '@kbn/presentation-util-plugin/public'; -import { ControlGroupInput } from '../types'; import { ControlFrame, ControlFrameProps } from './control_frame_component'; +import { ControlGroupReduxState } from '../types'; import { ControlGroupStrings } from '../control_group_strings'; interface DragInfo { @@ -67,8 +67,8 @@ const SortableControlInner = forwardRef< dragHandleRef ) => { const { isOver, isDragging, draggingIndex, index } = dragInfo; - const { useEmbeddableSelector } = useReduxContainerContext(); - const { panels } = useEmbeddableSelector((state) => state); + const { useEmbeddableSelector } = useReduxContainerContext(); + const panels = useEmbeddableSelector((state) => state.explicitInput.panels); const grow = panels[embeddableId].grow; const width = panels[embeddableId].width; @@ -119,8 +119,9 @@ const SortableControlInner = forwardRef< * can be quite cumbersome. */ export const ControlClone = ({ draggingId }: { draggingId: string }) => { - const { useEmbeddableSelector } = useReduxContainerContext(); - const { panels, controlStyle } = useEmbeddableSelector((state) => state); + const { useEmbeddableSelector: select } = useReduxContainerContext(); + const panels = select((state) => state.explicitInput.panels); + const controlStyle = select((state) => state.explicitInput.controlStyle); const width = panels[draggingId].width; const title = panels[draggingId].explicitInput.title; diff --git a/src/plugins/controls/public/control_group/editor/edit_control.tsx b/src/plugins/controls/public/control_group/editor/edit_control.tsx index 12ceab4f69645d..28ece063fcce3f 100644 --- a/src/plugins/controls/public/control_group/editor/edit_control.tsx +++ b/src/plugins/controls/public/control_group/editor/edit_control.tsx @@ -14,7 +14,7 @@ import { OverlayRef } from '@kbn/core/public'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { EmbeddableFactoryNotFoundError } from '@kbn/embeddable-plugin/public'; import { useReduxContainerContext } from '@kbn/presentation-util-plugin/public'; -import { ControlGroupInput } from '../types'; +import { ControlGroupReduxState } from '../types'; import { ControlEditor } from './control_editor'; import { pluginServices } from '../../services'; import { ControlGroupStrings } from '../control_group_strings'; @@ -41,7 +41,7 @@ export const EditControlButton = ({ embeddableId }: { embeddableId: string }) => // Redux embeddable container Context const reduxContainerContext = useReduxContainerContext< - ControlGroupInput, + ControlGroupReduxState, typeof controlGroupReducers >(); const { @@ -53,7 +53,7 @@ export const EditControlButton = ({ embeddableId }: { embeddableId: string }) => const dispatch = useEmbeddableDispatch(); // current state - const { panels } = useEmbeddableSelector((state) => state); + const panels = useEmbeddableSelector((state) => state.explicitInput.panels); // keep up to date ref of latest panel state for comparison when closing editor. const latestPanelState = useRef(panels[embeddableId]); diff --git a/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx b/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx index f5a6d244b8ff6c..7ba3915c6d3865 100644 --- a/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx +++ b/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx @@ -6,37 +6,34 @@ * Side Public License, v 1. */ +import { + map, + skip, + switchMap, + catchError, + debounceTime, + distinctUntilChanged, +} from 'rxjs/operators'; import React from 'react'; -import { uniqBy } from 'lodash'; import ReactDOM from 'react-dom'; import deepEqual from 'fast-deep-equal'; import { Filter, uniqFilters } from '@kbn/es-query'; import { EMPTY, merge, pipe, Subject, Subscription } from 'rxjs'; import { EuiContextMenuPanel } from '@elastic/eui'; -import { - distinctUntilChanged, - debounceTime, - catchError, - switchMap, - map, - skip, - mapTo, -} from 'rxjs/operators'; import { - withSuspense, - LazyReduxEmbeddableWrapper, - ReduxEmbeddableWrapperPropsWithChildren, + ReduxEmbeddableTools, + createReduxEmbeddableTools, SolutionToolbarPopover, } from '@kbn/presentation-util-plugin/public'; import { OverlayRef } from '@kbn/core/public'; -import { DataView } from '@kbn/data-views-plugin/public'; +import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { Container, EmbeddableFactory } from '@kbn/embeddable-plugin/public'; -import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { ControlGroupInput, ControlGroupOutput, + ControlGroupReduxState, ControlPanelState, ControlsPanels, CONTROL_GROUP_TYPE, @@ -54,10 +51,6 @@ import { controlGroupReducers } from '../state/control_group_reducers'; import { ControlEmbeddable, ControlInput, ControlOutput } from '../../types'; import { CreateControlButton, CreateControlButtonTypes } from '../editor/create_control'; -const ControlGroupReduxWrapper = withSuspense< - ReduxEmbeddableWrapperPropsWithChildren ->(LazyReduxEmbeddableWrapper); - let flyoutRef: OverlayRef | undefined; export const setFlyoutRef = (newRef: OverlayRef | undefined) => { flyoutRef = newRef; @@ -77,6 +70,11 @@ export class ControlGroupContainer extends Container< private relevantDataViewId?: string; private lastUsedDataViewId?: string; + private reduxEmbeddableTools: ReduxEmbeddableTools< + ControlGroupReduxState, + typeof controlGroupReducers + >; + public setLastUsedDataViewId = (lastUsedDataViewId: string) => { this.lastUsedDataViewId = lastUsedDataViewId; }; @@ -165,7 +163,7 @@ export class ControlGroupContainer extends Container< constructor(initialInput: ControlGroupInput, parent?: Container) { super( initialInput, - { embeddableLoaded: {} }, + { dataViewIds: [], loading: false, embeddableLoaded: {}, filters: [] }, pluginServices.getServices().controls.getControlFactory, parent, ControlGroupChainingSystems[initialInput.chainingSystem]?.getContainerSettings(initialInput) @@ -173,6 +171,15 @@ export class ControlGroupContainer extends Container< this.recalculateFilters$ = new Subject(); + // build redux embeddable tools + this.reduxEmbeddableTools = createReduxEmbeddableTools< + ControlGroupReduxState, + typeof controlGroupReducers + >({ + embeddable: this, + reducers: controlGroupReducers, + }); + // when all children are ready setup subscriptions this.untilReady().then(() => { this.recalculateDataViews(); @@ -215,7 +222,7 @@ export class ControlGroupContainer extends Container< .pipe( // Embeddables often throw errors into their output streams. catchError(() => EMPTY), - mapTo(childId) + map(() => childId) ) ) ) @@ -261,12 +268,12 @@ export class ControlGroupContainer extends Container< }; private recalculateDataViews = () => { - const allDataViews: DataView[] = []; + const allDataViewIds: Set = new Set(); Object.values(this.children).map((child) => { - const childOutput = child.getOutput() as ControlOutput; - allDataViews.push(...(childOutput.dataViews ?? [])); + const dataViewId = (child.getOutput() as ControlOutput).dataViewId; + if (dataViewId) allDataViewIds.add(dataViewId); }); - this.updateOutput({ dataViews: uniqBy(allDataViews, 'id') }); + this.updateOutput({ dataViewIds: Array.from(allDataViewIds) }); }; protected createNewPanelState( @@ -354,10 +361,11 @@ export class ControlGroupContainer extends Container< } this.domNode = dom; const ControlsServicesProvider = pluginServices.getContextProvider(); + const { Wrapper: ControlGroupReduxWrapper } = this.reduxEmbeddableTools; ReactDOM.render( - + @@ -370,6 +378,7 @@ export class ControlGroupContainer extends Container< super.destroy(); this.closeAllFlyouts(); this.subscriptions.unsubscribe(); + this.reduxEmbeddableTools.cleanup(); if (this.domNode) ReactDOM.unmountComponentAtNode(this.domNode); } } diff --git a/src/plugins/controls/public/control_group/state/control_group_reducers.ts b/src/plugins/controls/public/control_group/state/control_group_reducers.ts index cfc93818bc1d8b..ff1b2e501dabe4 100644 --- a/src/plugins/controls/public/control_group/state/control_group_reducers.ts +++ b/src/plugins/controls/public/control_group/state/control_group_reducers.ts @@ -17,38 +17,38 @@ export const controlGroupReducers = { state: WritableDraft, action: PayloadAction ) => { - state.input.controlStyle = action.payload; + state.explicitInput.controlStyle = action.payload; }, setDefaultControlWidth: ( state: WritableDraft, action: PayloadAction ) => { - state.input.defaultControlWidth = action.payload; + state.explicitInput.defaultControlWidth = action.payload; }, setDefaultControlGrow: ( state: WritableDraft, action: PayloadAction ) => { - state.input.defaultControlGrow = action.payload; + state.explicitInput.defaultControlGrow = action.payload; }, setControlWidth: ( state: WritableDraft, action: PayloadAction<{ width: ControlWidth; embeddableId: string }> ) => { - state.input.panels[action.payload.embeddableId].width = action.payload.width; + state.explicitInput.panels[action.payload.embeddableId].width = action.payload.width; }, setControlGrow: ( state: WritableDraft, action: PayloadAction<{ grow: boolean; embeddableId: string }> ) => { - state.input.panels[action.payload.embeddableId].grow = action.payload.grow; + state.explicitInput.panels[action.payload.embeddableId].grow = action.payload.grow; }, setControlOrders: ( state: WritableDraft, action: PayloadAction<{ ids: string[] }> ) => { action.payload.ids.forEach((id, index) => { - state.input.panels[id].order = index; + state.explicitInput.panels[id].order = index; }); }, }; diff --git a/src/plugins/controls/public/control_group/types.ts b/src/plugins/controls/public/control_group/types.ts index e3e871abee57e0..8304c9dd0d0958 100644 --- a/src/plugins/controls/public/control_group/types.ts +++ b/src/plugins/controls/public/control_group/types.ts @@ -11,7 +11,8 @@ import { ReduxEmbeddableState } from '@kbn/presentation-util-plugin/public'; import { ControlGroupInput } from '../../common/control_group/types'; import { CommonControlOutput } from '../types'; -export type ControlGroupOutput = ContainerOutput & CommonControlOutput; +export type ControlGroupOutput = ContainerOutput & + Omit & { dataViewIds: string[] }; // public only - redux embeddable state type export type ControlGroupReduxState = ReduxEmbeddableState; diff --git a/src/plugins/controls/public/control_types/options_list/options_list_component.tsx b/src/plugins/controls/public/control_types/options_list/options_list_component.tsx index 191dfbd4256321..aa406c34097a1a 100644 --- a/src/plugins/controls/public/control_types/options_list/options_list_component.tsx +++ b/src/plugins/controls/public/control_types/options_list/options_list_component.tsx @@ -40,13 +40,15 @@ export const OptionsListComponent = ({ const dispatch = useEmbeddableDispatch(); // Select current state from Redux using multiple selectors to avoid rerenders. - const invalidSelections = select((state) => state.componentState?.invalidSelections); - const validSelections = select((state) => state.componentState?.validSelections); - const selectedOptions = select((state) => state.input.selectedOptions); - const controlStyle = select((state) => state.input.controlStyle); - const singleSelect = select((state) => state.input.singleSelect); + const invalidSelections = select((state) => state.componentState.invalidSelections); + const validSelections = select((state) => state.componentState.validSelections); + + const selectedOptions = select((state) => state.explicitInput.selectedOptions); + const controlStyle = select((state) => state.explicitInput.controlStyle); + const singleSelect = select((state) => state.explicitInput.singleSelect); + const id = select((state) => state.explicitInput.id); + const loading = select((state) => state.output.loading); - const id = select((state) => state.input.id); // debounce loading state so loading doesn't flash when user types const [buttonLoading, setButtonLoading] = useState(true); diff --git a/src/plugins/controls/public/control_types/options_list/options_list_embeddable.tsx b/src/plugins/controls/public/control_types/options_list/options_list_embeddable.tsx index ba9d47f9a8588b..6dceaa26dea9b1 100644 --- a/src/plugins/controls/public/control_types/options_list/options_list_embeddable.tsx +++ b/src/plugins/controls/public/control_types/options_list/options_list_embeddable.tsx @@ -196,11 +196,11 @@ export class OptionsListEmbeddable extends Embeddable state.componentState?.invalidSelections); - const totalCardinality = select((state) => state.componentState?.totalCardinality); - const availableOptions = select((state) => state.componentState?.availableOptions); - const selectedOptions = select((state) => state.input.selectedOptions); - const singleSelect = select((state) => state.input.singleSelect); - const field = select((state) => state.componentState?.field); + const invalidSelections = select((state) => state.componentState.invalidSelections); + const totalCardinality = select((state) => state.componentState.totalCardinality); + const availableOptions = select((state) => state.componentState.availableOptions); + const field = select((state) => state.componentState.field); + + const selectedOptions = select((state) => state.explicitInput.selectedOptions); + const singleSelect = select((state) => state.explicitInput.singleSelect); + const title = select((state) => state.explicitInput.title); + const loading = select((state) => state.output.loading); - const title = select((state) => state.input.title); // track selectedOptions and invalidSelections in sets for more efficient lookup const selectedOptionsSet = useMemo(() => new Set(selectedOptions), [selectedOptions]); diff --git a/src/plugins/controls/public/control_types/options_list/options_list_reducers.ts b/src/plugins/controls/public/control_types/options_list/options_list_reducers.ts index 24ebb727f17456..0099d30ac2144c 100644 --- a/src/plugins/controls/public/control_types/options_list/options_list_reducers.ts +++ b/src/plugins/controls/public/control_types/options_list/options_list_reducers.ts @@ -5,22 +5,21 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import { DataView } from '@kbn/data-views-plugin/common'; -import { Filter } from '@kbn/es-query'; import { PayloadAction } from '@reduxjs/toolkit'; import { WritableDraft } from 'immer/dist/types/types-external'; +import { Filter } from '@kbn/es-query'; + import { OptionsListField, OptionsListReduxState, OptionsListComponentState } from './types'; export const optionsListReducers = { deselectOption: (state: WritableDraft, action: PayloadAction) => { - if (!state.input.selectedOptions) return; - const itemIndex = state.input.selectedOptions.indexOf(action.payload); + if (!state.explicitInput.selectedOptions) return; + const itemIndex = state.explicitInput.selectedOptions.indexOf(action.payload); if (itemIndex !== -1) { - const newSelections = [...state.input.selectedOptions]; + const newSelections = [...state.explicitInput.selectedOptions]; newSelections.splice(itemIndex, 1); - state.input.selectedOptions = newSelections; + state.explicitInput.selectedOptions = newSelections; } }, deselectOptions: ( @@ -28,27 +27,27 @@ export const optionsListReducers = { action: PayloadAction ) => { for (const optionToDeselect of action.payload) { - if (!state.input.selectedOptions) return; - const itemIndex = state.input.selectedOptions.indexOf(optionToDeselect); + if (!state.explicitInput.selectedOptions) return; + const itemIndex = state.explicitInput.selectedOptions.indexOf(optionToDeselect); if (itemIndex !== -1) { - const newSelections = [...state.input.selectedOptions]; + const newSelections = [...state.explicitInput.selectedOptions]; newSelections.splice(itemIndex, 1); - state.input.selectedOptions = newSelections; + state.explicitInput.selectedOptions = newSelections; } } }, selectOption: (state: WritableDraft, action: PayloadAction) => { - if (!state.input.selectedOptions) state.input.selectedOptions = []; - state.input.selectedOptions?.push(action.payload); + if (!state.explicitInput.selectedOptions) state.explicitInput.selectedOptions = []; + state.explicitInput.selectedOptions?.push(action.payload); }, replaceSelection: ( state: WritableDraft, action: PayloadAction ) => { - state.input.selectedOptions = [action.payload]; + state.explicitInput.selectedOptions = [action.payload]; }, clearSelections: (state: WritableDraft) => { - if (state.input.selectedOptions) state.input.selectedOptions = []; + if (state.explicitInput.selectedOptions) state.explicitInput.selectedOptions = []; }, clearValidAndInvalidSelections: (state: WritableDraft) => { state.componentState.invalidSelections = []; @@ -88,10 +87,10 @@ export const optionsListReducers = { ) => { state.output.filters = action.payload; }, - setDataView: ( + setDataViewId: ( state: WritableDraft, - action: PayloadAction + action: PayloadAction ) => { - state.output.dataViews = action.payload ? [action.payload] : []; + state.output.dataViewId = action.payload; }, }; diff --git a/src/plugins/controls/public/control_types/options_list/types.ts b/src/plugins/controls/public/control_types/options_list/types.ts index 1b3a7e1baa2e11..b682e79ab0bbee 100644 --- a/src/plugins/controls/public/control_types/options_list/types.ts +++ b/src/plugins/controls/public/control_types/options_list/types.ts @@ -7,16 +7,18 @@ */ import { ReduxEmbeddableState } from '@kbn/presentation-util-plugin/public'; -import { DataViewField } from '@kbn/data-views-plugin/common'; import { ControlOutput } from '../../types'; -import { OptionsListEmbeddableInput } from '../../../common/control_types/options_list/types'; +import { + OptionsListEmbeddableInput, + OptionsListField, +} from '../../../common/control_types/options_list/types'; export * from '../../../common/control_types/options_list/types'; // Component state is only used by public components. export interface OptionsListComponentState { - field?: DataViewField; + field?: OptionsListField; totalCardinality?: number; availableOptions?: string[]; invalidSelections?: string[]; diff --git a/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable.tsx b/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable.tsx index 64620e4a671d6c..e4dfa9d406b950 100644 --- a/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable.tsx +++ b/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable.tsx @@ -153,10 +153,10 @@ export class RangeSliderEmbeddable extends Embeddable { dispatch(setLoading(false)); dispatch(setIsInvalid(!ignoreParentSettings?.ignoreValidations && hasEitherSelection)); - dispatch(setDataView(dataView)); + dispatch(setDataViewId(dataView.id)); dispatch(publishFilters([])); }); return; @@ -376,7 +370,7 @@ export class RangeSliderEmbeddable extends Embeddable { dispatch(setLoading(false)); dispatch(setIsInvalid(true)); - dispatch(setDataView(dataView)); + dispatch(setDataViewId(dataView.id)); dispatch(publishFilters([])); }); return; @@ -386,7 +380,7 @@ export class RangeSliderEmbeddable extends Embeddable { dispatch(setLoading(false)); dispatch(setIsInvalid(false)); - dispatch(setDataView(dataView)); + dispatch(setDataViewId(dataView.id)); dispatch(publishFilters([rangeFilter])); }); }; @@ -398,6 +392,7 @@ export class RangeSliderEmbeddable extends Embeddable { super.destroy(); this.subscriptions.unsubscribe(); + this.reduxEmbeddableTools.cleanup(); }; public render = (node: HTMLElement) => { @@ -406,11 +401,14 @@ export class RangeSliderEmbeddable extends Embeddable - - - + + + + + , node ); diff --git a/src/plugins/controls/public/control_types/range_slider/range_slider_popover.tsx b/src/plugins/controls/public/control_types/range_slider/range_slider_popover.tsx index 7d7bbdd0908fa3..4e674e752edaa0 100644 --- a/src/plugins/controls/public/control_types/range_slider/range_slider_popover.tsx +++ b/src/plugins/controls/public/control_types/range_slider/range_slider_popover.tsx @@ -18,19 +18,26 @@ import { EuiFlexItem, EuiDualRange, } from '@elastic/eui'; -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public'; import { RangeSliderStrings } from './range_slider_strings'; import { RangeSliderReduxState, RangeValue } from './types'; import { rangeSliderReducers } from './range_slider_reducers'; +import { pluginServices } from '../../services'; const INVALID_CLASS = 'rangeSliderAnchor__fieldNumber--invalid'; export const RangeSliderPopover = () => { + // Controls Services Context + const { dataViews } = pluginServices.getHooks(); + const { get: getDataViewById } = dataViews.useService(); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [rangeSliderMin, setRangeSliderMin] = useState(-Infinity); const [rangeSliderMax, setRangeSliderMax] = useState(Infinity); + const [fieldFormatter, setFieldFormatter] = useState(() => (toFormat: string) => toFormat); + const rangeRef = useRef(null); const { @@ -41,14 +48,31 @@ export const RangeSliderPopover = () => { const dispatch = useEmbeddableDispatch(); // Select current state from Redux using multiple selectors to avoid rerenders. - const id = select((state) => state.input.id); - const value = select((state) => state.input.value); - const title = select((state) => state.input.title); const min = select((state) => state.componentState.min); const max = select((state) => state.componentState.max); - const isLoading = select((state) => state.output.loading); const isInvalid = select((state) => state.componentState.isInvalid); - const fieldFormatter = select((state) => state.componentState.fieldFormatter); + const fieldSpec = select((state) => state.componentState.field); + + const id = select((state) => state.explicitInput.id); + const value = select((state) => state.explicitInput.value) ?? ['', '']; + const title = select((state) => state.explicitInput.title); + + const isLoading = select((state) => state.output.loading); + const dataViewId = select((state) => state.output.dataViewId); + + // derive field formatter from fieldSpec and dataViewId + useEffect(() => { + (async () => { + if (!dataViewId || !fieldSpec) return; + // dataViews are cached, and should always be available without having to hit ES. + const dataView = await getDataViewById(dataViewId); + setFieldFormatter( + () => + dataView?.getFormatterForField(fieldSpec).getConverterFor('text') ?? + ((toFormat: string) => toFormat) + ); + })(); + }, [fieldSpec, dataViewId, getDataViewById]); let errorMessage = ''; let helpText = ''; diff --git a/src/plugins/controls/public/control_types/range_slider/range_slider_reducers.ts b/src/plugins/controls/public/control_types/range_slider/range_slider_reducers.ts index bc9ac9e9c5f575..5bcf55770998fa 100644 --- a/src/plugins/controls/public/control_types/range_slider/range_slider_reducers.ts +++ b/src/plugins/controls/public/control_types/range_slider/range_slider_reducers.ts @@ -9,16 +9,14 @@ import { Filter } from '@kbn/es-query'; import { PayloadAction } from '@reduxjs/toolkit'; import { WritableDraft } from 'immer/dist/types/types-external'; -import { DataViewField, DataView } from '@kbn/data-views-plugin/common'; +import { FieldSpec } from '@kbn/data-views-plugin/common'; -import { RangeSliderComponentState, RangeSliderReduxState, RangeValue } from './types'; +import { RangeSliderReduxState, RangeValue } from './types'; -export const getDefaultComponentState = () => ({ +export const getDefaultComponentState = (): RangeSliderReduxState['componentState'] => ({ min: '', max: '', - loading: true, isInvalid: false, - fieldFormatter: (value: string) => value, }); export const rangeSliderReducers = { @@ -26,24 +24,19 @@ export const rangeSliderReducers = { state: WritableDraft, action: PayloadAction ) => { - state.input.value = action.payload; + state.explicitInput.value = action.payload; }, - setFieldAndFormatter: ( + setField: ( state: WritableDraft, - action: PayloadAction<{ - field?: DataViewField; - fieldFormatter?: RangeSliderComponentState['fieldFormatter']; - }> + action: PayloadAction ) => { - state.componentState.field = action.payload.field; - state.componentState.fieldFormatter = - action.payload.fieldFormatter ?? ((value: string) => value); + state.componentState.field = action.payload; }, - setDataView: ( + setDataViewId: ( state: WritableDraft, - action: PayloadAction + action: PayloadAction ) => { - state.output.dataViews = action.payload ? [action.payload] : []; + state.output.dataViewId = action.payload; }, setLoading: (state: WritableDraft, action: PayloadAction) => { state.output.loading = action.payload; diff --git a/src/plugins/controls/public/control_types/range_slider/types.ts b/src/plugins/controls/public/control_types/range_slider/types.ts index c82fd82ac50bdc..390d91de08b884 100644 --- a/src/plugins/controls/public/control_types/range_slider/types.ts +++ b/src/plugins/controls/public/control_types/range_slider/types.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { DataViewField } from '@kbn/data-views-plugin/common'; +import { FieldSpec } from '@kbn/data-views-plugin/common'; import type { ReduxEmbeddableState } from '@kbn/presentation-util-plugin/public'; import { RangeSliderEmbeddableInput } from '../../../common/control_types/range_slider/types'; @@ -14,8 +14,7 @@ import { ControlOutput } from '../../types'; // Component state is only used by public components. export interface RangeSliderComponentState { - field?: DataViewField; - fieldFormatter: (value: string) => string; + field?: FieldSpec; min: string; max: string; isInvalid?: boolean; diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider.tsx b/src/plugins/controls/public/control_types/time_slider/time_slider.tsx index e72a5245cb95ff..0b519406ccf8d6 100644 --- a/src/plugins/controls/public/control_types/time_slider/time_slider.tsx +++ b/src/plugins/controls/public/control_types/time_slider/time_slider.tsx @@ -38,8 +38,8 @@ export const TimeSlider: FC = ({ const dispatch = useEmbeddableDispatch(); const availableRange = select((state) => state.componentState.range); - const value = select((state) => state.input.value); - const id = select((state) => state.input.id); + const value = select((state) => state.explicitInput.value); + const id = select((state) => state.explicitInput.id); const { min, max } = availableRange ? availableRange diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider_reducers.ts b/src/plugins/controls/public/control_types/time_slider/time_slider_reducers.ts index e2481b359d5134..95b8d87dc902e5 100644 --- a/src/plugins/controls/public/control_types/time_slider/time_slider_reducers.ts +++ b/src/plugins/controls/public/control_types/time_slider/time_slider_reducers.ts @@ -15,6 +15,6 @@ export const timeSliderReducers = { state: WritableDraft, action: PayloadAction<[number | null, number | null]> ) => { - state.input.value = action.payload; + state.explicitInput.value = action.payload; }, }; diff --git a/src/plugins/controls/public/services/kibana/options_list.ts b/src/plugins/controls/public/services/kibana/options_list.ts index aa3f28e24ab166..4ff603f1af23f2 100644 --- a/src/plugins/controls/public/services/kibana/options_list.ts +++ b/src/plugins/controls/public/services/kibana/options_list.ts @@ -86,7 +86,7 @@ class OptionsListService implements ControlsOptionsListService { ...passThroughProps, filters: esFilters, fieldName: field.name, - fieldSpec: field.toSpec?.(), + fieldSpec: field, textFieldName: (field as OptionsListField).textFieldName, }; }; diff --git a/src/plugins/controls/public/types.ts b/src/plugins/controls/public/types.ts index 71436fa9926e0e..79db062136fd0a 100644 --- a/src/plugins/controls/public/types.ts +++ b/src/plugins/controls/public/types.ts @@ -16,14 +16,14 @@ import { IEmbeddable, } from '@kbn/embeddable-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { DataView, DataViewField, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { DataViewField, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { ControlInput } from '../common/types'; import { ControlsService } from './services/controls'; export interface CommonControlOutput { filters?: Filter[]; - dataViews?: DataView[]; + dataViewId?: string; } export type ControlOutput = EmbeddableOutput & CommonControlOutput; diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts index 28b3c3cf373144..494a355324df1a 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts @@ -14,7 +14,6 @@ import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs'; import { DashboardConstants } from '../..'; import { ViewMode } from '../../services/embeddable'; import { useKibana } from '../../services/kibana_react'; -import { DataView } from '../../services/data_views'; import { getNewDashboardTitle } from '../../dashboard_strings'; import { IKbnUrlStateStorage } from '../../services/kibana_utils'; import { setDashboardState, useDashboardDispatch, useDashboardSelector } from '../state'; @@ -252,11 +251,14 @@ export const useDashboardAppState = ({ const dataViewsSubscription = syncDashboardDataViews({ dashboardContainer, dataViews: dashboardBuildContext.dataViews, - onUpdateDataViews: (newDataViews: DataView[]) => { - if (newDataViews.length > 0 && newDataViews[0].id) { - dashboardContainer.controlGroup?.setRelevantDataViewId(newDataViews[0].id); + onUpdateDataViews: async (newDataViewIds: string[]) => { + if (newDataViewIds?.[0]) { + dashboardContainer.controlGroup?.setRelevantDataViewId(newDataViewIds[0]); } - setDashboardAppState((s) => ({ ...s, dataViews: newDataViews })); + + // fetch all data views. These should be cached locally at this time so we will not need to query ES. + const allDataViews = await Promise.all(newDataViewIds.map((id) => dataViews.get(id))); + setDashboardAppState((s) => ({ ...s, dataViews: allDataViews })); }, }); diff --git a/src/plugins/dashboard/public/application/lib/sync_dashboard_data_views.ts b/src/plugins/dashboard/public/application/lib/sync_dashboard_data_views.ts index 63cecaa76fb2f9..0d55ee26ab6ed1 100644 --- a/src/plugins/dashboard/public/application/lib/sync_dashboard_data_views.ts +++ b/src/plugins/dashboard/public/application/lib/sync_dashboard_data_views.ts @@ -6,10 +6,9 @@ * Side Public License, v 1. */ -import { uniqBy } from 'lodash'; import deepEqual from 'fast-deep-equal'; import { Observable, pipe, combineLatest } from 'rxjs'; -import { distinctUntilChanged, switchMap, filter, mapTo, map } from 'rxjs/operators'; +import { distinctUntilChanged, switchMap, filter, map } from 'rxjs/operators'; import { DashboardContainer } from '..'; import { isErrorEmbeddable } from '../../services/embeddable'; @@ -19,7 +18,7 @@ import { DataView } from '../../services/data_views'; interface SyncDashboardDataViewsProps { dashboardContainer: DashboardContainer; dataViews: DataViewsContract; - onUpdateDataViews: (newDataViews: DataView[]) => void; + onUpdateDataViews: (newDataViewIds: string[]) => void; } export const syncDashboardDataViews = ({ @@ -29,54 +28,59 @@ export const syncDashboardDataViews = ({ }: SyncDashboardDataViewsProps) => { const updateDataViewsOperator = pipe( filter((container: DashboardContainer) => !!container && !isErrorEmbeddable(container)), - map((container: DashboardContainer): DataView[] | undefined => { - let panelDataViews: DataView[] = []; + map((container: DashboardContainer): string[] | undefined => { + const panelDataViewIds: Set = new Set(); Object.values(container.getChildIds()).forEach((id) => { const embeddableInstance = container.getChild(id); if (isErrorEmbeddable(embeddableInstance)) return; - const embeddableDataViews = ( + + /** + * TODO - this assumes that all embeddables which communicate data views do so via an `indexPatterns` key on their output. + * This should be replaced with a more generic, interface based method where an embeddable can communicate a data view ID. + */ + const childPanelDataViews = ( embeddableInstance.getOutput() as { indexPatterns: DataView[] } ).indexPatterns; - if (!embeddableDataViews) return; - panelDataViews.push(...embeddableDataViews); + if (!childPanelDataViews) return; + childPanelDataViews.forEach((dataView) => { + if (dataView.id) panelDataViewIds.add(dataView.id); + }); }); if (container.controlGroup) { - panelDataViews.push(...(container.controlGroup.getOutput().dataViews ?? [])); + const controlGroupDataViewIds = container.controlGroup.getOutput().dataViewIds; + controlGroupDataViewIds?.forEach((dataViewId) => panelDataViewIds.add(dataViewId)); } - panelDataViews = uniqBy(panelDataViews, 'id'); /** * If no index patterns have been returned yet, and there is at least one embeddable which * hasn't yet loaded, defer the loading of the default index pattern by returning undefined. */ if ( - panelDataViews.length === 0 && + panelDataViewIds.size === 0 && Object.keys(container.getOutput().embeddableLoaded).length > 0 && Object.values(container.getOutput().embeddableLoaded).some((value) => value === false) ) { return; } - return panelDataViews; + return Array.from(panelDataViewIds); }), - distinctUntilChanged((a, b) => - deepEqual( - a?.map((ip) => ip && ip.id), - b?.map((ip) => ip && ip.id) - ) - ), + distinctUntilChanged((a, b) => deepEqual(a, b)), + // using switchMap for previous task cancellation - switchMap((panelDataViews?: DataView[]) => { + switchMap((allDataViewIds?: string[]) => { return new Observable((observer) => { - if (!panelDataViews) return; - if (panelDataViews.length > 0) { + if (!allDataViewIds) return; + if (allDataViewIds.length > 0) { if (observer.closed) return; - onUpdateDataViews(panelDataViews); + onUpdateDataViews(allDataViewIds); observer.complete(); } else { - dataViews.getDefault().then((defaultDataView) => { + dataViews.getDefaultId().then((defaultDataViewId) => { if (observer.closed) return; - onUpdateDataViews([defaultDataView as DataView]); + if (defaultDataViewId) { + onUpdateDataViews([defaultDataViewId]); + } observer.complete(); }); } @@ -89,6 +93,9 @@ export const syncDashboardDataViews = ({ dataViewSources.push(dashboardContainer.controlGroup.getOutput$()); return combineLatest(dataViewSources) - .pipe(mapTo(dashboardContainer), updateDataViewsOperator) + .pipe( + map(() => dashboardContainer), + updateDataViewsOperator + ) .subscribe(); }; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index 001cb98afa6c13..7c50634665e50e 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -78,7 +78,6 @@ export abstract class Embeddable< this.parentSubscription = Rx.merge(parent.getInput$(), parent.getOutput$()).subscribe(() => { // Make sure this panel hasn't been removed immediately after it was added, but before it finished loading. if (!parent.getInput().panels[this.id]) return; - const newInput = parent.getInputForChild(this.id); this.onResetInput(newInput); }); diff --git a/src/plugins/presentation_util/public/redux_embeddables/clean_redux_embeddable_state.ts b/src/plugins/presentation_util/public/redux_embeddables/clean_redux_embeddable_state.ts new file mode 100644 index 00000000000000..0ab78a0663e843 --- /dev/null +++ b/src/plugins/presentation_util/public/redux_embeddables/clean_redux_embeddable_state.ts @@ -0,0 +1,49 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Filter } from '@kbn/es-query'; +import { EmbeddableInput } from '@kbn/embeddable-plugin/public'; + +import { ReduxEmbeddableState } from './types'; + +// TODO: Make filters serializable so we don't need special treatment for them. +type InputWithFilters = Partial & { filters: Filter[] }; +export const stateContainsFilters = ( + state: Partial +): state is InputWithFilters => { + if ((state as InputWithFilters).filters) return true; + return false; +}; + +export const cleanFiltersForSerialize = (filters: Filter[]): Filter[] => { + return filters.map((filter) => { + if (filter.meta.value) delete filter.meta.value; + return filter; + }); +}; + +export const cleanInputForRedux = < + ReduxEmbeddableStateType extends ReduxEmbeddableState = ReduxEmbeddableState +>( + explicitInput: ReduxEmbeddableStateType['explicitInput'] +) => { + if (stateContainsFilters(explicitInput)) { + explicitInput.filters = cleanFiltersForSerialize(explicitInput.filters); + } + return explicitInput; +}; + +export const cleanStateForRedux = < + ReduxEmbeddableStateType extends ReduxEmbeddableState = ReduxEmbeddableState +>( + state: ReduxEmbeddableStateType +) => { + // clean explicit input + state.explicitInput = cleanInputForRedux(state.explicitInput); + return state; +}; diff --git a/src/plugins/presentation_util/public/redux_embeddables/create_redux_embeddable_tools.tsx b/src/plugins/presentation_util/public/redux_embeddables/create_redux_embeddable_tools.tsx index 579e8b1d7e7ed9..f6400d1424ffe0 100644 --- a/src/plugins/presentation_util/public/redux_embeddables/create_redux_embeddable_tools.tsx +++ b/src/plugins/presentation_util/public/redux_embeddables/create_redux_embeddable_tools.tsx @@ -13,11 +13,11 @@ import { PayloadAction, SliceCaseReducers, } from '@reduxjs/toolkit'; -import { Filter } from '@kbn/es-query'; import React, { ReactNode, PropsWithChildren } from 'react'; -import { EmbeddableInput, IEmbeddable } from '@kbn/embeddable-plugin/public'; import { Provider, TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import { IEmbeddable } from '@kbn/embeddable-plugin/public'; + import { EmbeddableReducers, ReduxEmbeddableTools, @@ -27,19 +27,7 @@ import { } from './types'; import { syncReduxEmbeddable } from './sync_redux_embeddable'; import { EmbeddableReduxContext } from './use_redux_embeddable_context'; - -// TODO: Make filters serializable so we don't need special treatment for them. -type InputWithFilters = Partial & { filters: Filter[] }; -const stateContainsFilters = (state: Partial): state is InputWithFilters => { - if ((state as InputWithFilters).filters) return true; - return false; -}; -const cleanFiltersForSerialize = (filters: Filter[]): Filter[] => { - return filters.map((filter) => { - if (filter.meta.value) delete filter.meta.value; - return filter; - }); -}; +import { cleanStateForRedux } from './clean_redux_embeddable_state'; export const createReduxEmbeddableTools = < ReduxEmbeddableStateType extends ReduxEmbeddableState = ReduxEmbeddableState, @@ -50,7 +38,10 @@ export const createReduxEmbeddableTools = < syncSettings, initialComponentState, }: { - embeddable: IEmbeddable; + embeddable: IEmbeddable< + ReduxEmbeddableStateType['explicitInput'], + ReduxEmbeddableStateType['output'] + >; initialComponentState?: ReduxEmbeddableStateType['componentState']; syncSettings?: ReduxEmbeddableSyncSettings; reducers: ReducerType; @@ -59,9 +50,9 @@ export const createReduxEmbeddableTools = < const genericReducers = { updateEmbeddableReduxInput: ( state: Draft, - action: PayloadAction> + action: PayloadAction> ) => { - state.input = { ...state.input, ...action.payload }; + state.explicitInput = { ...state.explicitInput, ...action.payload }; }, updateEmbeddableReduxOutput: ( state: Draft, @@ -72,15 +63,13 @@ export const createReduxEmbeddableTools = < }; // create initial state from Embeddable - const initialState: ReduxEmbeddableStateType = { - input: embeddable.getInput(), + let initialState: ReduxEmbeddableStateType = { output: embeddable.getOutput(), componentState: initialComponentState ?? {}, + explicitInput: embeddable.getExplicitInput(), } as ReduxEmbeddableStateType; - if (stateContainsFilters(initialState.input)) { - initialState.input.filters = cleanFiltersForSerialize(initialState.input.filters); - } + initialState = cleanStateForRedux(initialState); // create slice out of reducers and embeddable initial state. const slice = createSlice>({ diff --git a/src/plugins/presentation_util/public/redux_embeddables/sync_redux_embeddable.ts b/src/plugins/presentation_util/public/redux_embeddables/sync_redux_embeddable.ts index 05bdb03b0020d2..1bc2f1040aefd7 100644 --- a/src/plugins/presentation_util/public/redux_embeddables/sync_redux_embeddable.ts +++ b/src/plugins/presentation_util/public/redux_embeddables/sync_redux_embeddable.ts @@ -11,6 +11,9 @@ import deepEqual from 'fast-deep-equal'; import { IEmbeddable } from '@kbn/embeddable-plugin/public'; import { EnhancedStore } from '@reduxjs/toolkit'; import { ReduxEmbeddableContext, ReduxEmbeddableState, ReduxEmbeddableSyncSettings } from './types'; +import { cleanInputForRedux } from './clean_redux_embeddable_state'; + +type Writeable = { -readonly [P in keyof T]: T[P] }; export const syncReduxEmbeddable = < ReduxEmbeddableStateType extends ReduxEmbeddableState = ReduxEmbeddableState @@ -22,17 +25,23 @@ export const syncReduxEmbeddable = < }: { settings?: ReduxEmbeddableSyncSettings; store: EnhancedStore; - embeddable: IEmbeddable; + embeddable: IEmbeddable< + ReduxEmbeddableStateType['explicitInput'], + ReduxEmbeddableStateType['output'] + >; actions: ReduxEmbeddableContext['actions']; }) => { if (settings?.disableSync) { return; } + let embeddableToReduxInProgress = false; + let reduxToEmbeddableInProgress = false; + const { isInputEqual: inputEqualityCheck, isOutputEqual: outputEqualityCheck } = settings ?? {}; const inputEqual = ( - inputA: ReduxEmbeddableStateType['input'], - inputB: ReduxEmbeddableStateType['input'] + inputA: Partial, + inputB: Partial ) => (inputEqualityCheck ? inputEqualityCheck(inputA, inputB) : deepEqual(inputA, inputB)); const outputEqual = ( outputA: ReduxEmbeddableStateType['output'], @@ -41,9 +50,11 @@ export const syncReduxEmbeddable = < // when the redux store changes, diff, and push updates to the embeddable input or to the output. const unsubscribeFromStore = store.subscribe(() => { + if (embeddableToReduxInProgress) return; + reduxToEmbeddableInProgress = true; const reduxState = store.getState(); - if (!inputEqual(reduxState.input, embeddable.getInput())) { - embeddable.updateInput(reduxState.input); + if (!inputEqual(reduxState.explicitInput, embeddable.getExplicitInput())) { + embeddable.updateInput(reduxState.explicitInput); } if (!outputEqual(reduxState.output, embeddable.getOutput())) { // updating output is usually not accessible from outside of the embeddable. @@ -54,22 +65,37 @@ export const syncReduxEmbeddable = < } ).updateOutput(reduxState.output); } + reduxToEmbeddableInProgress = false; }); // when the embeddable input changes, diff and dispatch to the redux store - const inputSubscription = embeddable.getInput$().subscribe((embeddableInput) => { - const reduxState = store.getState(); - if (!inputEqual(reduxState.input, embeddableInput)) { - store.dispatch(actions.updateEmbeddableReduxInput(embeddableInput)); + const inputSubscription = embeddable.getInput$().subscribe(() => { + if (reduxToEmbeddableInProgress) return; + embeddableToReduxInProgress = true; + const { explicitInput: reduxExplicitInput } = store.getState(); + + // store only explicit input in the store + const embeddableExplictiInput = embeddable.getExplicitInput() as Writeable< + ReduxEmbeddableStateType['explicitInput'] + >; + + if (!inputEqual(reduxExplicitInput, embeddableExplictiInput)) { + store.dispatch( + actions.updateEmbeddableReduxInput(cleanInputForRedux(embeddableExplictiInput)) + ); } + embeddableToReduxInProgress = false; }); // when the embeddable output changes, diff and dispatch to the redux store const outputSubscription = embeddable.getOutput$().subscribe((embeddableOutput) => { + if (reduxToEmbeddableInProgress) return; + embeddableToReduxInProgress = true; const reduxState = store.getState(); if (!outputEqual(reduxState.output, embeddableOutput)) { store.dispatch(actions.updateEmbeddableReduxOutput(embeddableOutput)); } + embeddableToReduxInProgress = false; }); return () => { unsubscribeFromStore(); diff --git a/src/plugins/presentation_util/public/redux_embeddables/types.ts b/src/plugins/presentation_util/public/redux_embeddables/types.ts index 7b576344f31eba..10c94daa9dcb50 100644 --- a/src/plugins/presentation_util/public/redux_embeddables/types.ts +++ b/src/plugins/presentation_util/public/redux_embeddables/types.ts @@ -23,12 +23,12 @@ export interface ReduxEmbeddableSyncSettings< > { disableSync: boolean; isInputEqual?: ( - a: ReduxEmbeddableStateType['input'], - b: ReduxEmbeddableStateType['input'] + a: Partial, + b: Partial ) => boolean; isOutputEqual?: ( - a: ReduxEmbeddableStateType['output'], - b: ReduxEmbeddableStateType['output'] + a: Partial, + b: Partial ) => boolean; } @@ -57,7 +57,7 @@ export interface ReduxEmbeddableState< OutputType extends EmbeddableOutput = EmbeddableOutput, StateType extends unknown = unknown > { - input: InputType; + explicitInput: InputType; output: OutputType; componentState: StateType; } @@ -91,7 +91,7 @@ export interface ReduxEmbeddableContext< } & { // Generic reducers to interact with embeddable Input and Output. updateEmbeddableReduxInput: ActionCreatorWithPayload< - Partial + Partial >; updateEmbeddableReduxOutput: ActionCreatorWithPayload< Partial