mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
remove redux tools from presentation_utils plugin (#197891)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
5b6887dd3d
commit
fb885eaeff
17 changed files with 59 additions and 694 deletions
|
@ -39,8 +39,8 @@ import {
|
|||
connectToQueryState,
|
||||
syncGlobalQueryStateWithUrl,
|
||||
} from '@kbn/data-plugin/public';
|
||||
import { cleanFiltersForSerialize } from '@kbn/presentation-util-plugin/public';
|
||||
import moment, { Moment } from 'moment';
|
||||
import { cleanFiltersForSerialize } from '../utils/clean_filters_for_serialize';
|
||||
import { dataService } from '../services/kibana_services';
|
||||
import { DashboardCreationOptions, DashboardState } from './types';
|
||||
import { DEFAULT_DASHBOARD_INPUT, GLOBAL_STATE_STORAGE_KEY } from '../dashboard_constants';
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
*/
|
||||
|
||||
import type { ContainerOutput } from '@kbn/embeddable-plugin/public';
|
||||
import type { ReduxEmbeddableState } from '@kbn/presentation-util-plugin/public';
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
|
||||
import { ControlGroupRuntimeState } from '@kbn/controls-plugin/public';
|
||||
|
@ -19,11 +18,6 @@ export interface UnsavedPanelState {
|
|||
[key: string]: object | undefined;
|
||||
}
|
||||
|
||||
export type DashboardReduxState = ReduxEmbeddableState<
|
||||
DashboardContainerInput,
|
||||
DashboardContainerOutput
|
||||
>;
|
||||
|
||||
export type DashboardRedirect = (props: RedirectToProps) => void;
|
||||
export type RedirectToProps =
|
||||
| { destination: 'dashboard'; id?: string; useReplace?: boolean; editMode?: boolean }
|
||||
|
|
|
@ -14,8 +14,8 @@ import { injectSearchSourceReferences } from '@kbn/data-plugin/public';
|
|||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { Filter, Query } from '@kbn/es-query';
|
||||
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public';
|
||||
import { cleanFiltersForSerialize } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import { cleanFiltersForSerialize } from '../../../utils/clean_filters_for_serialize';
|
||||
import { getDashboardContentManagementCache } from '..';
|
||||
import { convertPanelsArrayToPanelMap, injectReferences } from '../../../../common';
|
||||
import type { DashboardGetIn, DashboardGetOut } from '../../../../server/content_management';
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { cleanFiltersForSerialize } from './clean_filters_for_serialize';
|
||||
|
||||
describe('cleanFiltersForSerialize', () => {
|
||||
test('should return an empty array if filters is not provided', () => {
|
||||
expect(cleanFiltersForSerialize()).toEqual([]);
|
||||
});
|
||||
|
||||
test('should remove "meta.value" property from each filter', () => {
|
||||
const filters: Filter[] = [
|
||||
{ query: { a: 'a' }, meta: { value: 'value1' } },
|
||||
{ query: { b: 'b' }, meta: { value: 'value2' } },
|
||||
];
|
||||
|
||||
const cleanedFilters = cleanFiltersForSerialize(filters);
|
||||
|
||||
expect(cleanedFilters[0]).toEqual({ query: { a: 'a' }, meta: {} });
|
||||
expect(cleanedFilters[1]).toEqual({ query: { b: 'b' }, meta: {} });
|
||||
});
|
||||
|
||||
test('should not fail if meta is missing from filters', () => {
|
||||
const filters: Filter[] = [{ query: { a: 'a' } }, { query: { b: 'b' } }] as unknown as Filter[];
|
||||
|
||||
const cleanedFilters = cleanFiltersForSerialize(filters as unknown as Filter[]);
|
||||
|
||||
expect(cleanedFilters[0]).toEqual({ query: { a: 'a' } });
|
||||
expect(cleanedFilters[1]).toEqual({ query: { b: 'b' } });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { Filter } from '@kbn/es-query';
|
||||
|
||||
export function cleanFiltersForSerialize(filters?: Filter[]): Filter[] {
|
||||
if (!filters) return [];
|
||||
return filters.map((filter) => {
|
||||
if (filter.meta?.value) delete filter.meta.value;
|
||||
return filter;
|
||||
});
|
||||
}
|
|
@ -8,51 +8,4 @@ tags: ['kibana', 'presentation', 'services']
|
|||
related: []
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas).
|
||||
|
||||
## Redux Embeddables
|
||||
|
||||
The Redux Embeddables system allows embeddable authors to interact with their embeddables in a standardized way using Redux toolkit. This wrapper abstracts away store and slice creation, and embeddable input sync. To use this system, a developer can use CreateReduxEmbeddableTools in the constructor of their embeddable, supplying a collection of reducers.
|
||||
|
||||
### Reducers
|
||||
|
||||
The reducer object expected by the ReduxEmbeddableWrapper is the same type as the reducers expected by [Redux Toolkit's CreateSlice](https://redux-toolkit.js.org/api/createslice).
|
||||
|
||||
<DocAccordion buttonContent="Reducers Example" initialIsOpen>
|
||||
```ts
|
||||
// my_embeddable_reducers.ts
|
||||
import { MyEmbeddableInput } from './my_embeddable';
|
||||
|
||||
export const myEmbeddableReducers = {
|
||||
setSpecialBoolean: (
|
||||
state: WritableDraft<MyEmbeddableInput>,
|
||||
action: PayloadAction<MyEmbeddableInput['specialBoolean']>
|
||||
) => {
|
||||
state.specialBoolean = action.payload;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
</DocAccordion>
|
||||
|
||||
### Accessing Actions and State
|
||||
|
||||
From components under the embeddable, actions, containerActions, and the current state of the redux store are accessed via the embeddable instance. You can pass the embeddable instance down as a prop, or use a context.
|
||||
|
||||
<DocAccordion buttonContent="Accessing Redux Embeddable Context" initialIsOpen>
|
||||
```ts
|
||||
// my_embeddable_component.tsx
|
||||
const MyEmbeddableComponent = ({ embeddableInstance }: { embeddableInstance: IEmbeddable }) => {
|
||||
// current state
|
||||
const specialBoolean = embeddableInstance.select((state) => state.specialBoolean);
|
||||
|
||||
// change specialBoolean after 5 seconds
|
||||
setTimeout(() => embeddableInstance.dispatch.setSpecialBoolean(false), 5000);
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
</DocAccordion>
|
||||
```
|
||||
|
|
|
@ -30,15 +30,6 @@ export {
|
|||
DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS,
|
||||
} from './components';
|
||||
|
||||
export {
|
||||
lazyLoadReduxToolsPackage,
|
||||
cleanFiltersForSerialize,
|
||||
type ReduxEmbeddableState,
|
||||
type ReduxEmbeddableTools,
|
||||
type ReduxTools,
|
||||
type ReduxToolsPackage,
|
||||
} from './redux_tools';
|
||||
|
||||
export type {
|
||||
ExpressionInputEditorRef,
|
||||
ExpressionInputProps,
|
||||
|
|
|
@ -8,9 +8,7 @@
|
|||
*/
|
||||
|
||||
import { PresentationUtilPluginStart } from './types';
|
||||
import { ReduxToolsPackage, registerExpressionsLanguage } from '.';
|
||||
import { createReduxEmbeddableTools } from './redux_tools/redux_embeddables/create_redux_embeddable_tools';
|
||||
import { createReduxTools } from './redux_tools/create_redux_tools';
|
||||
import { registerExpressionsLanguage } from '.';
|
||||
import { setStubKibanaServices } from './services/mocks';
|
||||
|
||||
const createStartContract = (): PresentationUtilPluginStart => {
|
||||
|
@ -31,14 +29,6 @@ export const presentationUtilPluginMock = {
|
|||
createStartContract,
|
||||
};
|
||||
|
||||
/**
|
||||
* A non async-imported version of the real redux embeddable tools package for mocking purposes.
|
||||
*/
|
||||
export const mockedReduxEmbeddablePackage: ReduxToolsPackage = {
|
||||
createReduxEmbeddableTools,
|
||||
createReduxTools,
|
||||
};
|
||||
|
||||
export * from './__stories__/fixtures/flights';
|
||||
export const setMockedPresentationUtilServices = () => {
|
||||
setStubKibanaServices();
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import {
|
||||
AnyAction,
|
||||
Middleware,
|
||||
createSlice,
|
||||
configureStore,
|
||||
SliceCaseReducers,
|
||||
CaseReducerActions,
|
||||
} from '@reduxjs/toolkit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { createContext } from 'react';
|
||||
import { createSelectorHook } from 'react-redux';
|
||||
|
||||
import { ReduxTools, ReduxToolsReducers, ReduxToolsSetters } from './types';
|
||||
|
||||
export const createReduxTools = <
|
||||
ReduxStateType extends unknown,
|
||||
ReducerType extends ReduxToolsReducers<ReduxStateType> = ReduxToolsReducers<ReduxStateType>
|
||||
>({
|
||||
reducers,
|
||||
additionalMiddleware,
|
||||
initialState,
|
||||
}: {
|
||||
additionalMiddleware?: Array<Middleware<AnyAction>>;
|
||||
initialState: ReduxStateType;
|
||||
reducers: ReducerType;
|
||||
}): ReduxTools<ReduxStateType, ReducerType> => {
|
||||
const id = uuidv4();
|
||||
|
||||
/**
|
||||
* Create slice out of reducers and embeddable initial state.
|
||||
*/
|
||||
const slice = createSlice<ReduxStateType, SliceCaseReducers<ReduxStateType>>({
|
||||
initialState,
|
||||
name: id,
|
||||
reducers,
|
||||
});
|
||||
|
||||
const store = configureStore({
|
||||
reducer: slice.reducer,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(...(additionalMiddleware ?? [])),
|
||||
});
|
||||
|
||||
/**
|
||||
* Create an object of setter functions by looping through the reducers, and creating a method that dispatches the related
|
||||
* action to the appropriate store.
|
||||
*/
|
||||
const dispatch: ReduxToolsSetters<ReduxStateType, ReducerType> = Object.keys(reducers).reduce(
|
||||
(acc, key: keyof ReducerType) => {
|
||||
const sliceAction =
|
||||
slice.actions[key as keyof CaseReducerActions<SliceCaseReducers<ReduxStateType>, string>];
|
||||
acc[key] = (payload) => store.dispatch(sliceAction(payload));
|
||||
return acc;
|
||||
},
|
||||
{} as ReduxToolsSetters<ReduxStateType, ReducerType>
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a selector which can be used by react components to get the latest state values and to re-render when state changes.
|
||||
*/
|
||||
const select = createSelectorHook(
|
||||
createContext({
|
||||
store,
|
||||
storeState: store.getState(),
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
store,
|
||||
select,
|
||||
dispatch,
|
||||
getState: store.getState,
|
||||
onStateChange: store.subscribe,
|
||||
};
|
||||
};
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { ReduxToolsPackage } from './types';
|
||||
|
||||
export type { ReduxEmbeddableState, ReduxEmbeddableTools } from './redux_embeddables/types';
|
||||
export { cleanFiltersForSerialize } from './redux_embeddables/clean_redux_embeddable_state';
|
||||
export type { ReduxToolsPackage, ReduxTools } from './types';
|
||||
|
||||
export const lazyLoadReduxToolsPackage = async (): Promise<ReduxToolsPackage> => {
|
||||
const { createReduxTools } = await import('./create_redux_tools');
|
||||
const { createReduxEmbeddableTools } = await import(
|
||||
'./redux_embeddables/create_redux_embeddable_tools'
|
||||
);
|
||||
return {
|
||||
createReduxTools,
|
||||
createReduxEmbeddableTools,
|
||||
};
|
||||
};
|
|
@ -1,129 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { EmbeddableInput } from '@kbn/embeddable-plugin/public';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import {
|
||||
cleanFiltersForSerialize,
|
||||
cleanInputForRedux,
|
||||
cleanStateForRedux,
|
||||
stateContainsFilters,
|
||||
} from './clean_redux_embeddable_state';
|
||||
|
||||
type InputWithFilters = Partial<EmbeddableInput> & { filters: Filter[] };
|
||||
|
||||
describe('stateContainsFilters', () => {
|
||||
test('should return true if state contains filters', () => {
|
||||
const explicitInput: InputWithFilters = {
|
||||
id: 'wat',
|
||||
filters: [{ query: {}, meta: {} }],
|
||||
};
|
||||
|
||||
expect(stateContainsFilters(explicitInput)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false if state does not contain filters', () => {
|
||||
const explicitInput: EmbeddableInput = {
|
||||
id: 'wat',
|
||||
};
|
||||
|
||||
expect(stateContainsFilters(explicitInput)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanFiltersForSerialize', () => {
|
||||
test('should return an empty array if filters is not provided', () => {
|
||||
expect(cleanFiltersForSerialize()).toEqual([]);
|
||||
});
|
||||
|
||||
test('should remove "meta.value" property from each filter', () => {
|
||||
const filters: Filter[] = [
|
||||
{ query: { a: 'a' }, meta: { value: 'value1' } },
|
||||
{ query: { b: 'b' }, meta: { value: 'value2' } },
|
||||
];
|
||||
|
||||
const cleanedFilters = cleanFiltersForSerialize(filters);
|
||||
|
||||
expect(cleanedFilters[0]).toEqual({ query: { a: 'a' }, meta: {} });
|
||||
expect(cleanedFilters[1]).toEqual({ query: { b: 'b' }, meta: {} });
|
||||
});
|
||||
|
||||
test('should not fail if meta is missing from filters', () => {
|
||||
const filters: Filter[] = [{ query: { a: 'a' } }, { query: { b: 'b' } }] as unknown as Filter[];
|
||||
|
||||
const cleanedFilters = cleanFiltersForSerialize(filters as unknown as Filter[]);
|
||||
|
||||
expect(cleanedFilters[0]).toEqual({ query: { a: 'a' } });
|
||||
expect(cleanedFilters[1]).toEqual({ query: { b: 'b' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanInputForRedux', () => {
|
||||
test('should clean filters to make explicit input serializable', () => {
|
||||
const explicitInput = {
|
||||
id: 'wat',
|
||||
filters: [
|
||||
{ query: { a: 'a' }, meta: { value: 'value1' } },
|
||||
{ query: { b: 'b' }, meta: { value: 'value2' } },
|
||||
],
|
||||
};
|
||||
|
||||
const cleanedInput = cleanInputForRedux(explicitInput) as InputWithFilters;
|
||||
|
||||
expect(cleanedInput.filters[0]).toEqual({ query: { a: 'a' }, meta: {} });
|
||||
expect(cleanedInput.filters[1]).toEqual({ query: { b: 'b' }, meta: {} });
|
||||
});
|
||||
|
||||
test('should not modify input if filters are not present', () => {
|
||||
const explicitInput = {
|
||||
id: 'wat',
|
||||
otherProp: 'value',
|
||||
};
|
||||
|
||||
const cleanedInput = cleanInputForRedux(explicitInput);
|
||||
|
||||
expect(cleanedInput).toEqual(explicitInput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanStateForRedux', () => {
|
||||
test('should clean explicitInput for serializable state', () => {
|
||||
const state = {
|
||||
output: {},
|
||||
componentState: {},
|
||||
explicitInput: {
|
||||
id: 'wat',
|
||||
filters: [
|
||||
{ query: { a: 'a' }, meta: { value: 'value1' } },
|
||||
{ query: { b: 'b' }, meta: { value: 'value2' } },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const cleanedState = cleanStateForRedux(state) as { explicitInput: InputWithFilters };
|
||||
|
||||
expect(cleanedState.explicitInput.filters[0]).toEqual({ query: { a: 'a' }, meta: {} });
|
||||
expect(cleanedState.explicitInput.filters[1]).toEqual({ query: { b: 'b' }, meta: {} });
|
||||
});
|
||||
|
||||
test('should not modify state if explicitInput filters are not present', () => {
|
||||
const state = {
|
||||
output: {},
|
||||
componentState: {},
|
||||
explicitInput: {
|
||||
id: 'wat',
|
||||
otherKey: 'value',
|
||||
},
|
||||
};
|
||||
|
||||
const cleanedState = cleanStateForRedux(state);
|
||||
|
||||
expect(cleanedState).toEqual(state);
|
||||
});
|
||||
});
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { 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<EmbeddableInput> & { filters: Filter[] };
|
||||
export const stateContainsFilters = (
|
||||
state: Partial<EmbeddableInput>
|
||||
): state is InputWithFilters => {
|
||||
if ((state as InputWithFilters).filters && (state as InputWithFilters).filters.length > 0)
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
export const cleanFiltersForSerialize = (filters?: Filter[]): Filter[] => {
|
||||
if (!filters) return [];
|
||||
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<ReduxEmbeddableStateType>(state.explicitInput);
|
||||
return state;
|
||||
};
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { Draft, AnyAction, Middleware, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { Embeddable } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import { ReduxToolsReducers } from '../types';
|
||||
import { createReduxTools } from '../create_redux_tools';
|
||||
import { syncReduxEmbeddable } from './sync_redux_embeddable';
|
||||
import { cleanStateForRedux } from './clean_redux_embeddable_state';
|
||||
import { ReduxEmbeddableTools, ReduxEmbeddableState, ReduxEmbeddableSyncSettings } from './types';
|
||||
|
||||
export const createReduxEmbeddableTools = <
|
||||
ReduxEmbeddableStateType extends ReduxEmbeddableState = ReduxEmbeddableState,
|
||||
ReducerType extends ReduxToolsReducers<ReduxEmbeddableStateType> = ReduxToolsReducers<ReduxEmbeddableStateType>
|
||||
>({
|
||||
reducers,
|
||||
embeddable,
|
||||
syncSettings,
|
||||
additionalMiddleware,
|
||||
initialComponentState,
|
||||
}: {
|
||||
embeddable: Embeddable<
|
||||
ReduxEmbeddableStateType['explicitInput'],
|
||||
ReduxEmbeddableStateType['output']
|
||||
>;
|
||||
additionalMiddleware?: Array<Middleware<AnyAction>>;
|
||||
initialComponentState?: ReduxEmbeddableStateType['componentState'];
|
||||
syncSettings?: ReduxEmbeddableSyncSettings;
|
||||
reducers: ReducerType;
|
||||
}): ReduxEmbeddableTools<ReduxEmbeddableStateType, ReducerType> => {
|
||||
/**
|
||||
* Build additional generic reducers to aid in embeddable syncing.
|
||||
*/
|
||||
const genericReducers = {
|
||||
replaceEmbeddableReduxInput: (
|
||||
state: Draft<ReduxEmbeddableStateType>,
|
||||
action: PayloadAction<ReduxEmbeddableStateType['explicitInput']>
|
||||
) => {
|
||||
state.explicitInput = action.payload;
|
||||
},
|
||||
replaceEmbeddableReduxOutput: (
|
||||
state: Draft<ReduxEmbeddableStateType>,
|
||||
action: PayloadAction<ReduxEmbeddableStateType['output']>
|
||||
) => {
|
||||
state.output = action.payload;
|
||||
},
|
||||
};
|
||||
const allReducers = { ...reducers, ...genericReducers };
|
||||
|
||||
/**
|
||||
* Create initial state from Embeddable.
|
||||
*/
|
||||
let initialState: ReduxEmbeddableStateType = {
|
||||
output: embeddable.getOutput(),
|
||||
componentState: initialComponentState ?? {},
|
||||
explicitInput: embeddable.getExplicitInput(),
|
||||
} as ReduxEmbeddableStateType;
|
||||
|
||||
initialState = cleanStateForRedux<ReduxEmbeddableStateType>(initialState);
|
||||
|
||||
const { dispatch, store, select, getState, onStateChange } = createReduxTools<
|
||||
ReduxEmbeddableStateType,
|
||||
typeof allReducers
|
||||
>({
|
||||
reducers: allReducers,
|
||||
additionalMiddleware,
|
||||
initialState,
|
||||
});
|
||||
|
||||
/**
|
||||
* Sync redux state with embeddable input and output observables. Eventually we can replace the input and output observables
|
||||
* with redux and remove this sync.
|
||||
*/
|
||||
const stopReduxEmbeddableSync = syncReduxEmbeddable<ReduxEmbeddableStateType>({
|
||||
replaceEmbeddableReduxInput: dispatch.replaceEmbeddableReduxInput,
|
||||
replaceEmbeddableReduxOutput: dispatch.replaceEmbeddableReduxOutput,
|
||||
settings: syncSettings,
|
||||
embeddable,
|
||||
store,
|
||||
});
|
||||
|
||||
return {
|
||||
store,
|
||||
select,
|
||||
dispatch,
|
||||
getState,
|
||||
onStateChange,
|
||||
cleanup: () => stopReduxEmbeddableSync?.(),
|
||||
};
|
||||
};
|
|
@ -1,107 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { EnhancedStore } from '@reduxjs/toolkit';
|
||||
|
||||
import { IEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import { cleanInputForRedux } from './clean_redux_embeddable_state';
|
||||
import { ReduxEmbeddableState, ReduxEmbeddableSyncSettings } from './types';
|
||||
|
||||
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||
|
||||
export const syncReduxEmbeddable = <
|
||||
ReduxEmbeddableStateType extends ReduxEmbeddableState = ReduxEmbeddableState
|
||||
>({
|
||||
store,
|
||||
settings,
|
||||
embeddable,
|
||||
replaceEmbeddableReduxInput,
|
||||
replaceEmbeddableReduxOutput,
|
||||
}: {
|
||||
settings?: ReduxEmbeddableSyncSettings;
|
||||
store: EnhancedStore<ReduxEmbeddableStateType>;
|
||||
replaceEmbeddableReduxInput: (input: ReduxEmbeddableStateType['explicitInput']) => void;
|
||||
replaceEmbeddableReduxOutput: (output: ReduxEmbeddableStateType['output']) => void;
|
||||
embeddable: IEmbeddable<
|
||||
ReduxEmbeddableStateType['explicitInput'],
|
||||
ReduxEmbeddableStateType['output']
|
||||
>;
|
||||
}) => {
|
||||
if (settings?.disableSync) {
|
||||
return;
|
||||
}
|
||||
|
||||
let embeddableToReduxInProgress = false;
|
||||
let reduxToEmbeddableInProgress = false;
|
||||
|
||||
const { isInputEqual: inputEqualityCheck, isOutputEqual: outputEqualityCheck } = settings ?? {};
|
||||
const inputEqual = (
|
||||
inputA: Partial<ReduxEmbeddableStateType['explicitInput']>,
|
||||
inputB: Partial<ReduxEmbeddableStateType['explicitInput']>
|
||||
) => (inputEqualityCheck ? inputEqualityCheck(inputA, inputB) : deepEqual(inputA, inputB));
|
||||
const outputEqual = (
|
||||
outputA: ReduxEmbeddableStateType['output'],
|
||||
outputB: ReduxEmbeddableStateType['output']
|
||||
) => (outputEqualityCheck ? outputEqualityCheck(outputA, outputB) : deepEqual(outputA, outputB));
|
||||
|
||||
// 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.explicitInput, embeddable.getExplicitInput())) {
|
||||
embeddable.updateInput(reduxState.explicitInput);
|
||||
}
|
||||
if (!outputEqual(reduxState.output, embeddable.getOutput())) {
|
||||
// updating output is usually not accessible from outside of the embeddable.
|
||||
// This redux sync utility is meant to be used from inside the embeddable, so we need to workaround the typescript error via casting.
|
||||
(
|
||||
embeddable as unknown as {
|
||||
updateOutput: (newOutput: ReduxEmbeddableStateType['output']) => void;
|
||||
}
|
||||
).updateOutput(reduxState.output);
|
||||
}
|
||||
reduxToEmbeddableInProgress = false;
|
||||
});
|
||||
|
||||
// when the embeddable input changes, diff and dispatch to the redux store
|
||||
const inputSubscription = embeddable.getInput$().subscribe(() => {
|
||||
if (reduxToEmbeddableInProgress) return;
|
||||
embeddableToReduxInProgress = true;
|
||||
const { explicitInput: reduxExplicitInput } = store.getState();
|
||||
|
||||
// store only explicit input in the store
|
||||
const embeddableExplictInput = embeddable.getExplicitInput() as Writeable<
|
||||
ReduxEmbeddableStateType['explicitInput']
|
||||
>;
|
||||
|
||||
if (!inputEqual(reduxExplicitInput, embeddableExplictInput)) {
|
||||
replaceEmbeddableReduxInput(cleanInputForRedux(embeddableExplictInput));
|
||||
}
|
||||
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)) {
|
||||
replaceEmbeddableReduxOutput(embeddableOutput);
|
||||
}
|
||||
embeddableToReduxInProgress = false;
|
||||
});
|
||||
return () => {
|
||||
unsubscribeFromStore();
|
||||
inputSubscription.unsubscribe();
|
||||
outputSubscription.unsubscribe();
|
||||
};
|
||||
};
|
|
@ -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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { EnhancedStore } from '@reduxjs/toolkit';
|
||||
import { EmbeddableInput, EmbeddableOutput } from '@kbn/embeddable-plugin/public';
|
||||
import { ReduxToolsReducers, ReduxToolsSelect, ReduxToolsSetters } from '../types';
|
||||
|
||||
export interface ReduxEmbeddableSyncSettings<
|
||||
ReduxEmbeddableStateType extends ReduxEmbeddableState = ReduxEmbeddableState
|
||||
> {
|
||||
disableSync: boolean;
|
||||
isInputEqual?: (
|
||||
a: Partial<ReduxEmbeddableStateType['explicitInput']>,
|
||||
b: Partial<ReduxEmbeddableStateType['explicitInput']>
|
||||
) => boolean;
|
||||
isOutputEqual?: (
|
||||
a: Partial<ReduxEmbeddableStateType['output']>,
|
||||
b: Partial<ReduxEmbeddableStateType['output']>
|
||||
) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The return type from createReduxEmbeddableTools. Contains tools to get state, select state for react components,
|
||||
* set state, and react to state changes.
|
||||
*/
|
||||
export interface ReduxEmbeddableTools<
|
||||
ReduxEmbeddableStateType extends ReduxEmbeddableState = ReduxEmbeddableState,
|
||||
ReducerType extends ReduxToolsReducers<ReduxEmbeddableStateType> = ReduxToolsReducers<ReduxEmbeddableStateType>
|
||||
> {
|
||||
cleanup: () => void;
|
||||
store: EnhancedStore<ReduxEmbeddableStateType>;
|
||||
select: ReduxToolsSelect<ReduxEmbeddableStateType>;
|
||||
getState: EnhancedStore<ReduxEmbeddableStateType>['getState'];
|
||||
dispatch: ReduxToolsSetters<ReduxEmbeddableStateType, ReducerType>;
|
||||
onStateChange: EnhancedStore<ReduxEmbeddableStateType>['subscribe'];
|
||||
}
|
||||
|
||||
/**
|
||||
* The Embeddable Redux store should contain Input, Output and State. Input is serialized and used to create the embeddable,
|
||||
* Output is used as a communication layer for state that the Embeddable creates, and State is used to store ephemeral state which needs
|
||||
* to be communicated between an embeddable and its inner React components.
|
||||
*/
|
||||
export interface ReduxEmbeddableState<
|
||||
InputType extends EmbeddableInput = EmbeddableInput,
|
||||
OutputType extends EmbeddableOutput = EmbeddableOutput,
|
||||
StateType extends unknown = unknown
|
||||
> {
|
||||
explicitInput: InputType;
|
||||
output: OutputType;
|
||||
componentState: StateType;
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { CaseReducer, PayloadAction, EnhancedStore } from '@reduxjs/toolkit';
|
||||
|
||||
/**
|
||||
* The Redux Tools Selector is a react redux selector function that can be used to grab values from the state, and to make a component
|
||||
* re-render when those values change.
|
||||
*/
|
||||
export type ReduxToolsSelect<ReduxStateType extends unknown> = <Selected extends unknown>(
|
||||
selector: (state: ReduxStateType) => Selected,
|
||||
equalityFn?: ((previous: Selected, next: Selected) => boolean) | undefined
|
||||
) => Selected;
|
||||
|
||||
/**
|
||||
* The Redux Embeddable Setters are a collection of functions which dispatch actions to the correct store.
|
||||
*/
|
||||
export type ReduxToolsSetters<
|
||||
ReduxStateType extends unknown,
|
||||
ReducerType extends ReduxToolsReducers<ReduxStateType> = ReduxToolsReducers<ReduxStateType>
|
||||
> = {
|
||||
[ReducerKey in keyof ReducerType]: (
|
||||
payload: Parameters<ReducerType[ReducerKey]>[1]['payload']
|
||||
) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* The return type from createReduxTools. Contains tools to get state, select state for react components,
|
||||
* set state, and react to state changes.
|
||||
*/
|
||||
export interface ReduxTools<
|
||||
ReduxStateType extends unknown,
|
||||
ReducerType extends ReduxToolsReducers<ReduxStateType> = ReduxToolsReducers<ReduxStateType>
|
||||
> {
|
||||
store: EnhancedStore<ReduxStateType>;
|
||||
select: ReduxToolsSelect<ReduxStateType>;
|
||||
getState: EnhancedStore<ReduxStateType>['getState'];
|
||||
onStateChange: EnhancedStore<ReduxStateType>['subscribe'];
|
||||
dispatch: ReduxToolsSetters<ReduxStateType, ReducerType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Redux Tools Reducers are the shape of the Raw reducers which will be passed into createSlice. These will be used to populate the actions
|
||||
* object which the tools will return.
|
||||
*/
|
||||
export interface ReduxToolsReducers<ReduxStateType extends unknown> {
|
||||
/**
|
||||
* PayloadAction of type any is strategic here because we want to allow payloads of any shape in generic reducers.
|
||||
* This type will be overridden to remove any and be type safe when returned by setupReduxEmbeddable.
|
||||
*/
|
||||
[key: string]: CaseReducer<ReduxStateType, PayloadAction<any>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The package type is lazily exported from presentation_util and should contain all methods needed to use the redux embeddable tools.
|
||||
*/
|
||||
export interface ReduxToolsPackage {
|
||||
createReduxTools: typeof import('./create_redux_tools')['createReduxTools'];
|
||||
createReduxEmbeddableTools: typeof import('./redux_embeddables/create_redux_embeddable_tools')['createReduxEmbeddableTools'];
|
||||
}
|
|
@ -22,7 +22,6 @@
|
|||
"@kbn/data-views-plugin",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/monaco",
|
||||
"@kbn/es-query",
|
||||
"@kbn/field-formats-plugin",
|
||||
"@kbn/interpreter",
|
||||
"@kbn/react-field",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue