[Lens] moving store loading to middleware (#106872)

This commit is contained in:
Marta Bondyra 2021-08-03 18:37:15 +02:00 committed by GitHub
parent 91e64e0afa
commit bcb16c1b86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 506 additions and 436 deletions

View file

@ -70,6 +70,7 @@ const sessionIdSubject = new Subject<string>();
describe('Lens App', () => {
let defaultDoc: Document;
let defaultSavedObjectId: string;
const mockDatasource: DatasourceMock = createMockDatasource('testDatasource');
const mockDatasource2: DatasourceMock = createMockDatasource('testDatasource2');
const datasourceMap = {

View file

@ -6,24 +6,20 @@
*/
import React, { FC, useCallback } from 'react';
import { DeepPartial } from '@reduxjs/toolkit';
import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom';
import { History } from 'history';
import { render, unmountComponentAtNode } from 'react-dom';
import { i18n } from '@kbn/i18n';
import { DashboardFeatureFlagConfig } from 'src/plugins/dashboard/public';
import { Provider } from 'react-redux';
import { isEqual } from 'lodash';
import { EmbeddableEditorState } from 'src/plugins/embeddable/public';
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import { LensReportManager, setReportManager, trackUiEvent } from '../lens_ui_telemetry';
import { App } from './app';
import { Datasource, EditorFrameStart, Visualization } from '../types';
import { EditorFrameStart } from '../types';
import { addHelpMenuToAppChrome } from '../help_menu_util';
import { LensPluginStartDependencies } from '../plugin';
import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID } from '../../common';
@ -32,32 +28,18 @@ import {
LensByReferenceInput,
LensByValueInput,
} from '../embeddable/embeddable';
import {
ACTION_VISUALIZE_LENS_FIELD,
VisualizeFieldContext,
} from '../../../../../src/plugins/ui_actions/public';
import { ACTION_VISUALIZE_LENS_FIELD } from '../../../../../src/plugins/ui_actions/public';
import { LensAttributeService } from '../lens_attribute_service';
import { LensAppServices, RedirectToOriginProps, HistoryLocationState } from './types';
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
import {
makeConfigureStore,
navigateAway,
getPreloadedState,
LensRootStore,
setState,
LensAppState,
updateLayer,
updateVisualizationState,
loadInitial,
LensState,
} from '../state_management';
import { getPersistedDoc } from './save_modal_container';
import { getResolvedDateRange, getInitialDatasourceId } from '../utils';
import { initializeDatasources } from '../editor_frame_service/editor_frame';
import { generateId } from '../id_generator';
import {
getVisualizeFieldSuggestions,
switchToSuggestion,
} from '../editor_frame_service/editor_frame/suggestion_helpers';
import { getPreloadedState } from '../state_management/lens_slice';
export async function getLensServices(
coreStart: CoreStart,
@ -114,7 +96,7 @@ export async function mountApp(
const lensServices = await getLensServices(coreStart, startDependencies, attributeService);
const { stateTransfer, data, storage, dashboardFeatureFlag } = lensServices;
const { stateTransfer, data, storage } = lensServices;
const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID);
@ -183,37 +165,19 @@ export async function mountApp(
if (embeddableEditorIncomingState?.searchSessionId) {
data.search.session.continue(embeddableEditorIncomingState.searchSessionId);
}
const { datasourceMap, visualizationMap } = instance;
const storeDeps = {
lensServices,
datasourceMap,
visualizationMap,
embeddableEditorIncomingState,
initialContext,
};
const lensStore: LensRootStore = makeConfigureStore(storeDeps, {
lens: getPreloadedState(storeDeps),
} as DeepPartial<LensState>);
const initialDatasourceId = getInitialDatasourceId(datasourceMap);
const datasourceStates: LensAppState['datasourceStates'] = {};
if (initialDatasourceId) {
datasourceStates[initialDatasourceId] = {
state: null,
isLoading: true,
};
}
const preloadedState = getPreloadedState({
isLoading: true,
query: data.query.queryString.getQuery(),
// Do not use app-specific filters from previous app,
// only if Lens was opened with the intention to visualize a field (e.g. coming from Discover)
filters: !initialContext
? data.query.filterManager.getGlobalFilters()
: data.query.filterManager.getFilters(),
searchSessionId: data.search.session.getSessionId(),
resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter),
isLinkedToOriginatingApp: Boolean(embeddableEditorIncomingState?.originatingApp),
activeDatasourceId: initialDatasourceId,
datasourceStates,
visualization: {
state: null,
activeId: Object.keys(visualizationMap)[0] || null,
},
});
const lensStore: LensRootStore = makeConfigureStore(preloadedState, { data });
const EditorRenderer = React.memo(
(props: { id?: string; history: History<unknown>; editByValue?: boolean }) => {
const redirectCallback = useCallback(
@ -224,17 +188,7 @@ export async function mountApp(
);
trackUiEvent('loaded');
const initialInput = getInitialInput(props.id, props.editByValue);
loadInitialStore(
redirectCallback,
initialInput,
lensServices,
lensStore,
embeddableEditorIncomingState,
dashboardFeatureFlag,
datasourceMap,
visualizationMap,
initialContext
);
lensStore.dispatch(loadInitial({ redirectCallback, initialInput }));
return (
<Provider store={lensStore}>
@ -309,181 +263,3 @@ export async function mountApp(
lensStore.dispatch(navigateAway());
};
}
export function loadInitialStore(
redirectCallback: (savedObjectId?: string) => void,
initialInput: LensEmbeddableInput | undefined,
lensServices: LensAppServices,
lensStore: LensRootStore,
embeddableEditorIncomingState: EmbeddableEditorState | undefined,
dashboardFeatureFlag: DashboardFeatureFlagConfig,
datasourceMap: Record<string, Datasource>,
visualizationMap: Record<string, Visualization>,
initialContext?: VisualizeFieldContext
) {
const { attributeService, chrome, notifications, data } = lensServices;
const { persistedDoc } = lensStore.getState().lens;
if (
!initialInput ||
(attributeService.inputIsRefType(initialInput) &&
initialInput.savedObjectId === persistedDoc?.savedObjectId)
) {
return initializeDatasources(
datasourceMap,
lensStore.getState().lens.datasourceStates,
undefined,
initialContext,
{
isFullEditor: true,
}
)
.then((result) => {
const datasourceStates = Object.entries(result).reduce(
(state, [datasourceId, datasourceState]) => ({
...state,
[datasourceId]: {
...datasourceState,
isLoading: false,
},
}),
{}
);
lensStore.dispatch(
setState({
datasourceStates,
isLoading: false,
})
);
if (initialContext) {
const selectedSuggestion = getVisualizeFieldSuggestions({
datasourceMap,
datasourceStates,
visualizationMap,
activeVisualizationId: Object.keys(visualizationMap)[0] || null,
visualizationState: null,
visualizeTriggerFieldContext: initialContext,
});
if (selectedSuggestion) {
switchToSuggestion(lensStore.dispatch, selectedSuggestion, 'SWITCH_VISUALIZATION');
}
}
const activeDatasourceId = getInitialDatasourceId(datasourceMap);
const visualization = lensStore.getState().lens.visualization;
const activeVisualization =
visualization.activeId && visualizationMap[visualization.activeId];
if (visualization.state === null && activeVisualization) {
const newLayerId = generateId();
const initialVisualizationState = activeVisualization.initialize(() => newLayerId);
lensStore.dispatch(
updateLayer({
datasourceId: activeDatasourceId!,
layerId: newLayerId,
updater: datasourceMap[activeDatasourceId!].insertLayer,
})
);
lensStore.dispatch(
updateVisualizationState({
visualizationId: activeVisualization.id,
updater: initialVisualizationState,
})
);
}
})
.catch((e: { message: string }) => {
notifications.toasts.addDanger({
title: e.message,
});
redirectCallback();
});
}
getPersistedDoc({
initialInput,
attributeService,
data,
chrome,
notifications,
})
.then(
(doc) => {
if (doc) {
const currentSessionId = data.search.session.getSessionId();
const docDatasourceStates = Object.entries(doc.state.datasourceStates).reduce(
(stateMap, [datasourceId, datasourceState]) => ({
...stateMap,
[datasourceId]: {
isLoading: true,
state: datasourceState,
},
}),
{}
);
initializeDatasources(
datasourceMap,
docDatasourceStates,
doc.references,
initialContext,
{
isFullEditor: true,
}
)
.then((result) => {
const activeDatasourceId = getInitialDatasourceId(datasourceMap, doc);
lensStore.dispatch(
setState({
query: doc.state.query,
searchSessionId:
dashboardFeatureFlag.allowByValueEmbeddables &&
Boolean(embeddableEditorIncomingState?.originatingApp) &&
!(initialInput as LensByReferenceInput)?.savedObjectId &&
currentSessionId
? currentSessionId
: data.search.session.start(),
...(!isEqual(persistedDoc, doc) ? { persistedDoc: doc } : null),
activeDatasourceId,
visualization: {
activeId: doc.visualizationType,
state: doc.state.visualization,
},
datasourceStates: Object.entries(result).reduce(
(state, [datasourceId, datasourceState]) => ({
...state,
[datasourceId]: {
...datasourceState,
isLoading: false,
},
}),
{}
),
isLoading: false,
})
);
})
.catch((e: { message: string }) =>
notifications.toasts.addDanger({
title: e.message,
})
);
} else {
redirectCallback();
}
},
() => {
lensStore.dispatch(
setState({
isLoading: false,
})
);
redirectCallback();
}
)
.catch((e: { message: string }) =>
notifications.toasts.addDanger({
title: e.message,
})
);
}

View file

@ -34,7 +34,7 @@ import {
EmbeddableEditorState,
EmbeddableStateTransfer,
} from '../../../../../src/plugins/embeddable/public';
import { Datasource, EditorFrameInstance, Visualization } from '../types';
import { DatasourceMap, EditorFrameInstance, VisualizationMap } from '../types';
import { PresentationUtilPluginStart } from '../../../../../src/plugins/presentation_util/public';
export interface RedirectToOriginProps {
input?: LensEmbeddableInput;
@ -54,8 +54,8 @@ export interface LensAppProps {
// State passed in by the container which is used to determine the id of the Originating App.
incomingState?: EmbeddableEditorState;
datasourceMap: Record<string, Datasource>;
visualizationMap: Record<string, Visualization>;
datasourceMap: DatasourceMap;
visualizationMap: VisualizationMap;
}
export type RunSave = (
@ -82,7 +82,7 @@ export interface LensTopNavMenuProps {
indicateNoData: boolean;
setIsSaveModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
runSave: RunSave;
datasourceMap: Record<string, Datasource>;
datasourceMap: DatasourceMap;
title?: string;
}

View file

@ -27,11 +27,8 @@ import {
} from '../../../state_management';
export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) {
const activeVisualization = props.visualizationMap[props.activeVisualizationId || ''];
const { visualizationState } = props;
return activeVisualization && visualizationState ? (
<LayerPanels {...props} activeVisualization={activeVisualization} />
return props.activeVisualization && props.visualizationState ? (
<LayerPanels {...props} activeVisualization={props.activeVisualization} />
) : null;
});

View file

@ -8,17 +8,16 @@
import {
Visualization,
FramePublicAPI,
Datasource,
DatasourceDimensionEditorProps,
VisualizationDimensionGroupConfig,
DatasourceMap,
} from '../../../types';
export interface ConfigPanelWrapperProps {
activeDatasourceId: string;
visualizationState: unknown;
visualizationMap: Record<string, Visualization>;
activeVisualizationId: string | null;
activeVisualization: Visualization | null;
framePublicAPI: FramePublicAPI;
datasourceMap: Record<string, Datasource>;
datasourceMap: DatasourceMap;
datasourceStates: Record<
string,
{
@ -33,7 +32,7 @@ export interface ConfigPanelWrapperProps {
export interface LayerPanelProps {
activeDatasourceId: string;
visualizationState: unknown;
datasourceMap: Record<string, Datasource>;
datasourceMap: DatasourceMap;
activeVisualization: Visualization;
framePublicAPI: FramePublicAPI;
datasourceStates: Record<

View file

@ -13,7 +13,7 @@ import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } fr
import { createSelector } from '@reduxjs/toolkit';
import { NativeRenderer } from '../../native_renderer';
import { DragContext, DragDropIdentifier } from '../../drag_drop';
import { StateSetter, DatasourceDataPanelProps, Datasource } from '../../types';
import { StateSetter, DatasourceDataPanelProps, DatasourceMap } from '../../types';
import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public';
import {
switchDatasource,
@ -27,7 +27,7 @@ import { initializeDatasources } from './state_helpers';
interface DataPanelWrapperProps {
datasourceState: unknown;
datasourceMap: Record<string, Datasource>;
datasourceMap: DatasourceMap;
activeDatasource: string | null;
datasourceIsLoading: boolean;
showNoDataPopover: () => void;

View file

@ -8,7 +8,7 @@
import React, { useCallback, useRef, useMemo } from 'react';
import { CoreStart } from 'kibana/public';
import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public';
import { Datasource, FramePublicAPI, Visualization } from '../../types';
import { DatasourceMap, FramePublicAPI, VisualizationMap } from '../../types';
import { DataPanelWrapper } from './data_panel_wrapper';
import { ConfigPanelWrapper } from './config_panel';
import { FrameLayout } from './frame_layout';
@ -22,8 +22,8 @@ import { trackUiEvent } from '../../lens_ui_telemetry';
import { useLensSelector, useLensDispatch } from '../../state_management';
export interface EditorFrameProps {
datasourceMap: Record<string, Datasource>;
visualizationMap: Record<string, Visualization>;
datasourceMap: DatasourceMap;
visualizationMap: VisualizationMap;
ExpressionRenderer: ReactExpressionRendererType;
core: CoreStart;
plugins: EditorFrameStartPlugins;
@ -125,11 +125,12 @@ export function EditorFrame(props: EditorFrameProps) {
configPanel={
allLoaded && (
<ConfigPanelWrapper
activeVisualization={
visualization.activeId ? props.visualizationMap[visualization.activeId] : null
}
activeDatasourceId={activeDatasourceId!}
datasourceMap={props.datasourceMap}
datasourceStates={datasourceStates}
visualizationMap={props.visualizationMap}
activeVisualizationId={visualization.activeId}
visualizationState={visualization.state}
framePublicAPI={framePublicAPI}
core={props.core}

View file

@ -6,11 +6,11 @@
*/
import { Ast, fromExpression, ExpressionFunctionAST } from '@kbn/interpreter/common';
import { Visualization, Datasource, DatasourcePublicAPI } from '../../types';
import { Visualization, DatasourcePublicAPI, DatasourceMap } from '../../types';
export function prependDatasourceExpression(
visualizationExpression: Ast | string | null,
datasourceMap: Record<string, Datasource>,
datasourceMap: DatasourceMap,
datasourceStates: Record<
string,
{
@ -80,7 +80,7 @@ export function buildExpression({
description?: string;
visualization: Visualization | null;
visualizationState: unknown;
datasourceMap: Record<string, Datasource>;
datasourceMap: DatasourceMap;
datasourceStates: Record<
string,
{

View file

@ -10,11 +10,13 @@ import { Ast } from '@kbn/interpreter/common';
import memoizeOne from 'memoize-one';
import {
Datasource,
DatasourceMap,
DatasourcePublicAPI,
FramePublicAPI,
InitializationOptions,
Visualization,
VisualizationDimensionGroupConfig,
VisualizationMap,
} from '../../types';
import { buildExpression } from './expression_helpers';
import { Document } from '../../persistence/saved_object_store';
@ -28,7 +30,7 @@ import {
} from '../error_helper';
export async function initializeDatasources(
datasourceMap: Record<string, Datasource>,
datasourceMap: DatasourceMap,
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>,
references?: SavedObjectReference[],
initialContext?: VisualizeFieldContext,
@ -55,7 +57,7 @@ export async function initializeDatasources(
}
export const createDatasourceLayers = memoizeOne(function createDatasourceLayers(
datasourceMap: Record<string, Datasource>,
datasourceMap: DatasourceMap,
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>
) {
const datasourceLayers: Record<string, DatasourcePublicAPI> = {};
@ -78,7 +80,7 @@ export const createDatasourceLayers = memoizeOne(function createDatasourceLayers
export async function persistedStateToExpression(
datasources: Record<string, Datasource>,
visualizations: Record<string, Visualization>,
visualizations: VisualizationMap,
doc: Document
): Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }> {
const {

View file

@ -18,6 +18,8 @@ import {
TableSuggestion,
DatasourceSuggestion,
DatasourcePublicAPI,
DatasourceMap,
VisualizationMap,
} from '../../types';
import { DragDropIdentifier } from '../../drag_drop';
import { LensDispatch, selectSuggestion, switchVisualization } from '../../state_management';
@ -57,7 +59,7 @@ export function getSuggestions({
activeData,
mainPalette,
}: {
datasourceMap: Record<string, Datasource>;
datasourceMap: DatasourceMap;
datasourceStates: Record<
string,
{
@ -65,7 +67,7 @@ export function getSuggestions({
state: unknown;
}
>;
visualizationMap: Record<string, Visualization>;
visualizationMap: VisualizationMap;
activeVisualizationId: string | null;
subVisualizationId?: string;
visualizationState: unknown;
@ -140,7 +142,7 @@ export function getVisualizeFieldSuggestions({
visualizationState,
visualizeTriggerFieldContext,
}: {
datasourceMap: Record<string, Datasource>;
datasourceMap: DatasourceMap;
datasourceStates: Record<
string,
{
@ -148,7 +150,7 @@ export function getVisualizeFieldSuggestions({
state: unknown;
}
>;
visualizationMap: Record<string, Visualization>;
visualizationMap: VisualizationMap;
activeVisualizationId: string | null;
subVisualizationId?: string;
visualizationState: unknown;

View file

@ -25,7 +25,14 @@ import { Ast, toExpression } from '@kbn/interpreter/common';
import { i18n } from '@kbn/i18n';
import classNames from 'classnames';
import { ExecutionContextSearch } from 'src/plugins/data/public';
import { Datasource, Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types';
import {
Datasource,
Visualization,
FramePublicAPI,
DatasourcePublicAPI,
DatasourceMap,
VisualizationMap,
} from '../../types';
import { getSuggestions, switchToSuggestion } from './suggestion_helpers';
import {
ReactExpressionRendererProps,
@ -45,7 +52,7 @@ const MAX_SUGGESTIONS_DISPLAYED = 5;
export interface SuggestionPanelProps {
activeDatasourceId: string | null;
datasourceMap: Record<string, Datasource>;
datasourceMap: DatasourceMap;
datasourceStates: Record<
string,
{
@ -54,7 +61,7 @@ export interface SuggestionPanelProps {
}
>;
activeVisualizationId: string | null;
visualizationMap: Record<string, Visualization>;
visualizationMap: VisualizationMap;
visualizationState: unknown;
ExpressionRenderer: ReactExpressionRendererType;
frame: FramePublicAPI;

View file

@ -20,7 +20,13 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { Visualization, FramePublicAPI, Datasource, VisualizationType } from '../../../types';
import {
Visualization,
FramePublicAPI,
VisualizationType,
VisualizationMap,
DatasourceMap,
} from '../../../types';
import { getSuggestions, switchToSuggestion, Suggestion } from '../suggestion_helpers';
import { trackUiEvent } from '../../../lens_ui_telemetry';
import { ToolbarButton } from '../../../../../../../src/plugins/kibana_react/public';
@ -44,9 +50,9 @@ interface VisualizationSelection {
}
interface Props {
visualizationMap: Record<string, Visualization>;
framePublicAPI: FramePublicAPI;
datasourceMap: Record<string, Datasource>;
visualizationMap: VisualizationMap;
datasourceMap: DatasourceMap;
}
type SelectableEntry = EuiSelectableOption<{ value: string }>;
@ -55,7 +61,7 @@ function VisualizationSummary({
visualizationMap,
visualization,
}: {
visualizationMap: Record<string, Visualization>;
visualizationMap: VisualizationMap;
visualization: {
activeId: string | null;
state: unknown;

View file

@ -34,12 +34,12 @@ import {
ReactExpressionRendererType,
} from '../../../../../../../src/plugins/expressions/public';
import {
Datasource,
Visualization,
FramePublicAPI,
isLensBrushEvent,
isLensFilterEvent,
isLensEditEvent,
VisualizationMap,
DatasourceMap,
} from '../../../types';
import { DragDrop, DragContext, DragDropIdentifier } from '../../../drag_drop';
import { Suggestion, switchToSuggestion } from '../suggestion_helpers';
@ -62,10 +62,10 @@ import {
export interface WorkspacePanelProps {
activeVisualizationId: string | null;
visualizationMap: Record<string, Visualization>;
visualizationMap: VisualizationMap;
visualizationState: unknown;
activeDatasourceId: string | null;
datasourceMap: Record<string, Datasource>;
datasourceMap: DatasourceMap;
datasourceStates: Record<
string,
{

View file

@ -10,7 +10,7 @@ import './workspace_panel_wrapper.scss';
import React, { useCallback } from 'react';
import { EuiPageContent, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import classNames from 'classnames';
import { Datasource, FramePublicAPI, Visualization } from '../../../types';
import { DatasourceMap, FramePublicAPI, VisualizationMap } from '../../../types';
import { NativeRenderer } from '../../../native_renderer';
import { ChartSwitch } from './chart_switch';
import { WarningsPopover } from './warnings_popover';
@ -21,9 +21,9 @@ export interface WorkspacePanelWrapperProps {
children: React.ReactNode | React.ReactNode[];
framePublicAPI: FramePublicAPI;
visualizationState: unknown;
visualizationMap: Record<string, Visualization>;
visualizationMap: VisualizationMap;
visualizationId: string | null;
datasourceMap: Record<string, Datasource>;
datasourceMap: DatasourceMap;
datasourceStates: Record<
string,
{

View file

@ -16,6 +16,7 @@ import moment from 'moment';
import { Provider } from 'react-redux';
import { act } from 'react-dom/test-utils';
import { ReactExpressionRendererProps } from 'src/plugins/expressions/public';
import { DeepPartial } from '@reduxjs/toolkit';
import { LensPublicStart } from '.';
import { visualizationTypes } from './xy_visualization/types';
import { navigationPluginMock } from '../../../../src/plugins/navigation/public/mocks';
@ -35,7 +36,7 @@ import {
import { LensAttributeService } from './lens_attribute_service';
import { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/public';
import { makeConfigureStore, getPreloadedState, LensAppState } from './state_management/index';
import { makeConfigureStore, LensAppState, LensState } from './state_management/index';
import { getResolvedDateRange } from './utils';
import { presentationUtilPluginMock } from '../../../../src/plugins/presentation_util/public/mocks';
import { DatasourcePublicAPI, Datasource, Visualization, FramePublicAPI } from './types';
@ -82,6 +83,11 @@ export function createMockVisualization(): jest.Mocked<Visualization> {
};
}
const visualizationMap = {
vis: createMockVisualization(),
vis2: createMockVisualization(),
};
export type DatasourceMock = jest.Mocked<Datasource> & {
publicAPIMock: jest.Mocked<DatasourcePublicAPI>;
};
@ -126,6 +132,13 @@ export function createMockDatasource(id: string): DatasourceMock {
};
}
const mockDatasource: DatasourceMock = createMockDatasource('testDatasource');
const mockDatasource2: DatasourceMock = createMockDatasource('testDatasource2');
const datasourceMap = {
testDatasource2: mockDatasource2,
testDatasource: mockDatasource,
};
export function createExpressionRendererMock(): jest.Mock<
React.ReactElement,
[ReactExpressionRendererProps]
@ -401,17 +414,21 @@ export function makeLensStore({
data = mockDataPlugin();
}
const lensStore = makeConfigureStore(
getPreloadedState({
...defaultState,
searchSessionId: data.search.session.start(),
query: data.query.queryString.getQuery(),
filters: data.query.filterManager.getGlobalFilters(),
resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter),
...preloadedState,
}),
{
data,
}
lensServices: { ...makeDefaultServices(), data },
datasourceMap,
visualizationMap,
},
{
lens: {
...defaultState,
searchSessionId: data.search.session.start(),
query: data.query.queryString.getQuery(),
filters: data.query.filterManager.getGlobalFilters(),
resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter),
...preloadedState,
},
} as DeepPartial<LensState>
);
const origDispatch = lensStore.dispatch;

View file

@ -5,16 +5,14 @@
* 2.0.
*/
import { configureStore, DeepPartial, getDefaultMiddleware } from '@reduxjs/toolkit';
import { configureStore, getDefaultMiddleware, DeepPartial } from '@reduxjs/toolkit';
import logger from 'redux-logger';
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import { lensSlice, initialState } from './lens_slice';
import { lensSlice } from './lens_slice';
import { timeRangeMiddleware } from './time_range_middleware';
import { optimizingMiddleware } from './optimizing_middleware';
import { externalContextMiddleware } from './external_context_middleware';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import { LensAppState, LensState } from './types';
import { LensState, LensStoreDeps } from './types';
import { initMiddleware } from './init_middleware';
export * from './types';
export const reducer = {
@ -22,8 +20,9 @@ export const reducer = {
};
export const {
setState,
loadInitial,
navigateAway,
setState,
setSaveable,
onActiveDataChange,
updateState,
@ -38,29 +37,17 @@ export const {
setToggleFullscreen,
} = lensSlice.actions;
export const getPreloadedState = (initializedState: Partial<LensAppState>) => {
const state = {
lens: {
...initialState,
...initializedState,
},
} as DeepPartial<LensState>;
return state;
};
type PreloadedState = ReturnType<typeof getPreloadedState>;
export const makeConfigureStore = (
preloadedState: PreloadedState,
{ data }: { data: DataPublicPluginStart }
storeDeps: LensStoreDeps,
preloadedState: DeepPartial<LensState>
) => {
const middleware = [
...getDefaultMiddleware({
serializableCheck: false,
}),
initMiddleware(storeDeps),
optimizingMiddleware(),
timeRangeMiddleware(data),
externalContextMiddleware(data),
timeRangeMiddleware(storeDeps.lensServices.data),
];
if (process.env.NODE_ENV === 'development') middleware.push(logger);

View file

@ -0,0 +1,33 @@
/*
* 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 { Dispatch, MiddlewareAPI, PayloadAction } from '@reduxjs/toolkit';
import { LensStoreDeps } from '..';
import { lensSlice } from '../lens_slice';
import { loadInitial } from './load_initial';
import { subscribeToExternalContext } from './subscribe_to_external_context';
export const initMiddleware = (storeDeps: LensStoreDeps) => (store: MiddlewareAPI) => {
const unsubscribeFromExternalContext = subscribeToExternalContext(
storeDeps.lensServices.data,
store.getState,
store.dispatch
);
return (next: Dispatch) => (action: PayloadAction) => {
if (lensSlice.actions.loadInitial.match(action)) {
return loadInitial(
store,
storeDeps,
action.payload.redirectCallback,
action.payload.initialInput
);
} else if (lensSlice.actions.navigateAway.match(action)) {
return unsubscribeFromExternalContext();
}
next(action);
};
};

View file

@ -4,11 +4,17 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { makeDefaultServices, makeLensStore, defaultDoc, createMockVisualization } from '../mocks';
import { createMockDatasource, DatasourceMock } from '../mocks';
import {
makeDefaultServices,
makeLensStore,
defaultDoc,
createMockVisualization,
createMockDatasource,
DatasourceMock,
} from '../../mocks';
import { act } from 'react-dom/test-utils';
import { loadInitialStore } from './mounter';
import { LensEmbeddableInput } from '../embeddable/embeddable';
import { loadInitial } from './load_initial';
import { LensEmbeddableInput } from '../../embeddable';
const defaultSavedObjectId = '1234';
const preloadedState = {
@ -20,7 +26,6 @@ const preloadedState = {
};
describe('Mounter', () => {
const byValueFlag = { allowByValueEmbeddables: true };
const mockDatasource: DatasourceMock = createMockDatasource('testDatasource');
const mockDatasource2: DatasourceMock = createMockDatasource('testDatasource2');
const datasourceMap = {
@ -66,15 +71,15 @@ describe('Mounter', () => {
preloadedState,
});
await act(async () => {
await loadInitialStore(
redirectCallback,
undefined,
services,
await loadInitial(
lensStore,
undefined,
byValueFlag,
datasourceMap,
visualizationMap
{
lensServices: services,
datasourceMap,
visualizationMap,
},
redirectCallback,
({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput
);
});
expect(mockDatasource.initialize).toHaveBeenCalled();
@ -87,15 +92,14 @@ describe('Mounter', () => {
const lensStore = await makeLensStore({ data: services.data, preloadedState });
await act(async () => {
await loadInitialStore(
redirectCallback,
undefined,
services,
await loadInitial(
lensStore,
undefined,
byValueFlag,
datasourceMap,
visualizationMap
{
lensServices: services,
datasourceMap,
visualizationMap,
},
redirectCallback
);
});
expect(mockDatasource.initialize).toHaveBeenCalled();
@ -114,20 +118,19 @@ describe('Mounter', () => {
// it.skip('should pass the datasource api for each layer to the visualization', async () => {})
// it('displays errors from the frame in a toast', async () => {
describe('loadInitialStore', () => {
describe('loadInitial', () => {
it('does not load a document if there is no initial input', async () => {
const services = makeDefaultServices();
const redirectCallback = jest.fn();
const lensStore = makeLensStore({ data: services.data, preloadedState });
await loadInitialStore(
redirectCallback,
undefined,
services,
await loadInitial(
lensStore,
undefined,
byValueFlag,
datasourceMap,
visualizationMap
{
lensServices: services,
datasourceMap,
visualizationMap,
},
redirectCallback
);
expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled();
});
@ -139,15 +142,15 @@ describe('Mounter', () => {
const lensStore = await makeLensStore({ data: services.data, preloadedState });
await act(async () => {
await loadInitialStore(
redirectCallback,
{ savedObjectId: defaultSavedObjectId } as LensEmbeddableInput,
services,
await loadInitial(
lensStore,
undefined,
byValueFlag,
datasourceMap,
visualizationMap
{
lensServices: services,
datasourceMap,
visualizationMap,
},
redirectCallback,
({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput
);
});
@ -175,43 +178,43 @@ describe('Mounter', () => {
const lensStore = makeLensStore({ data: services.data, preloadedState });
await act(async () => {
await loadInitialStore(
redirectCallback,
{ savedObjectId: defaultSavedObjectId } as LensEmbeddableInput,
services,
await loadInitial(
lensStore,
undefined,
byValueFlag,
datasourceMap,
visualizationMap
{
lensServices: services,
datasourceMap,
visualizationMap,
},
redirectCallback,
({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput
);
});
await act(async () => {
await loadInitialStore(
redirectCallback,
{ savedObjectId: defaultSavedObjectId } as LensEmbeddableInput,
services,
await loadInitial(
lensStore,
undefined,
byValueFlag,
datasourceMap,
visualizationMap
{
lensServices: services,
datasourceMap,
visualizationMap,
},
redirectCallback,
({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput
);
});
expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1);
await act(async () => {
await loadInitialStore(
redirectCallback,
{ savedObjectId: '5678' } as LensEmbeddableInput,
services,
await loadInitial(
lensStore,
undefined,
byValueFlag,
datasourceMap,
visualizationMap
{
lensServices: services,
datasourceMap,
visualizationMap,
},
redirectCallback,
({ savedObjectId: '5678' } as unknown) as LensEmbeddableInput
);
});
@ -227,15 +230,15 @@ describe('Mounter', () => {
services.attributeService.unwrapAttributes = jest.fn().mockRejectedValue('failed to load');
await act(async () => {
await loadInitialStore(
redirectCallback,
{ savedObjectId: defaultSavedObjectId } as LensEmbeddableInput,
services,
await loadInitial(
lensStore,
undefined,
byValueFlag,
datasourceMap,
visualizationMap
{
lensServices: services,
datasourceMap,
visualizationMap,
},
redirectCallback,
({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput
);
});
expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({
@ -251,15 +254,15 @@ describe('Mounter', () => {
const services = makeDefaultServices();
const lensStore = makeLensStore({ data: services.data, preloadedState });
await act(async () => {
await loadInitialStore(
redirectCallback,
({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput,
services,
await loadInitial(
lensStore,
undefined,
byValueFlag,
datasourceMap,
visualizationMap
{
lensServices: services,
datasourceMap,
visualizationMap,
},
redirectCallback,
({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput
);
});

View file

@ -0,0 +1,199 @@
/*
* 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 { MiddlewareAPI } from '@reduxjs/toolkit';
import { isEqual } from 'lodash';
import { setState } from '..';
import { updateLayer, updateVisualizationState, LensStoreDeps } from '..';
import { LensEmbeddableInput, LensByReferenceInput } from '../../embeddable/embeddable';
import { getInitialDatasourceId } from '../../utils';
import { initializeDatasources } from '../../editor_frame_service/editor_frame';
import { generateId } from '../../id_generator';
import {
getVisualizeFieldSuggestions,
switchToSuggestion,
} from '../../editor_frame_service/editor_frame/suggestion_helpers';
import { getPersistedDoc } from '../../app_plugin/save_modal_container';
export function loadInitial(
store: MiddlewareAPI,
{
lensServices,
datasourceMap,
visualizationMap,
embeddableEditorIncomingState,
initialContext,
}: LensStoreDeps,
redirectCallback: (savedObjectId?: string) => void,
initialInput?: LensEmbeddableInput
) {
const { getState, dispatch } = store;
const { attributeService, chrome, notifications, data, dashboardFeatureFlag } = lensServices;
const { persistedDoc } = getState().lens;
if (
!initialInput ||
(attributeService.inputIsRefType(initialInput) &&
initialInput.savedObjectId === persistedDoc?.savedObjectId)
) {
return initializeDatasources(
datasourceMap,
getState().lens.datasourceStates,
undefined,
initialContext,
{
isFullEditor: true,
}
)
.then((result) => {
const datasourceStates = Object.entries(result).reduce(
(state, [datasourceId, datasourceState]) => ({
...state,
[datasourceId]: {
...datasourceState,
isLoading: false,
},
}),
{}
);
dispatch(
setState({
datasourceStates,
isLoading: false,
})
);
if (initialContext) {
const selectedSuggestion = getVisualizeFieldSuggestions({
datasourceMap,
datasourceStates,
visualizationMap,
activeVisualizationId: Object.keys(visualizationMap)[0] || null,
visualizationState: null,
visualizeTriggerFieldContext: initialContext,
});
if (selectedSuggestion) {
switchToSuggestion(dispatch, selectedSuggestion, 'SWITCH_VISUALIZATION');
}
}
const activeDatasourceId = getInitialDatasourceId(datasourceMap);
const visualization = getState().lens.visualization;
const activeVisualization =
visualization.activeId && visualizationMap[visualization.activeId];
if (visualization.state === null && activeVisualization) {
const newLayerId = generateId();
const initialVisualizationState = activeVisualization.initialize(() => newLayerId);
dispatch(
updateLayer({
datasourceId: activeDatasourceId!,
layerId: newLayerId,
updater: datasourceMap[activeDatasourceId!].insertLayer,
})
);
dispatch(
updateVisualizationState({
visualizationId: activeVisualization.id,
updater: initialVisualizationState,
})
);
}
})
.catch((e: { message: string }) => {
notifications.toasts.addDanger({
title: e.message,
});
redirectCallback();
});
}
getPersistedDoc({
initialInput,
attributeService,
data,
chrome,
notifications,
})
.then(
(doc) => {
if (doc) {
const currentSessionId = data.search.session.getSessionId();
const docDatasourceStates = Object.entries(doc.state.datasourceStates).reduce(
(stateMap, [datasourceId, datasourceState]) => ({
...stateMap,
[datasourceId]: {
isLoading: true,
state: datasourceState,
},
}),
{}
);
initializeDatasources(
datasourceMap,
docDatasourceStates,
doc.references,
initialContext,
{
isFullEditor: true,
}
)
.then((result) => {
const activeDatasourceId = getInitialDatasourceId(datasourceMap, doc);
dispatch(
setState({
query: doc.state.query,
searchSessionId:
dashboardFeatureFlag.allowByValueEmbeddables &&
Boolean(embeddableEditorIncomingState?.originatingApp) &&
!(initialInput as LensByReferenceInput)?.savedObjectId &&
currentSessionId
? currentSessionId
: data.search.session.start(),
...(!isEqual(persistedDoc, doc) ? { persistedDoc: doc } : null),
activeDatasourceId,
visualization: {
activeId: doc.visualizationType,
state: doc.state.visualization,
},
datasourceStates: Object.entries(result).reduce(
(state, [datasourceId, datasourceState]) => ({
...state,
[datasourceId]: {
...datasourceState,
isLoading: false,
},
}),
{}
),
isLoading: false,
})
);
})
.catch((e: { message: string }) =>
notifications.toasts.addDanger({
title: e.message,
})
);
} else {
redirectCallback();
}
},
() => {
dispatch(
setState({
isLoading: false,
})
);
redirectCallback();
}
)
.catch((e: { message: string }) =>
notifications.toasts.addDanger({
title: e.message,
})
);
}

View file

@ -7,34 +7,15 @@
import { delay, finalize, switchMap, tap } from 'rxjs/operators';
import { debounce, isEqual } from 'lodash';
import { Dispatch, MiddlewareAPI, PayloadAction } from '@reduxjs/toolkit';
import { trackUiEvent } from '../lens_ui_telemetry';
import { trackUiEvent } from '../../lens_ui_telemetry';
import {
waitUntilNextSessionCompletes$,
DataPublicPluginStart,
} from '../../../../../src/plugins/data/public';
import { setState, LensGetState, LensDispatch } from '.';
import { LensAppState } from './types';
import { getResolvedDateRange } from '../utils';
} from '../../../../../../src/plugins/data/public';
import { setState, LensGetState, LensDispatch } from '..';
import { getResolvedDateRange } from '../../utils';
export const externalContextMiddleware = (data: DataPublicPluginStart) => (
store: MiddlewareAPI
) => {
const unsubscribeFromExternalContext = subscribeToExternalContext(
data,
store.getState,
store.dispatch
);
return (next: Dispatch) => (action: PayloadAction<Partial<LensAppState>>) => {
if (action.type === 'lens/navigateAway') {
unsubscribeFromExternalContext();
}
next(action);
};
};
function subscribeToExternalContext(
export function subscribeToExternalContext(
data: DataPublicPluginStart,
getState: LensGetState,
dispatch: LensDispatch

View file

@ -6,8 +6,10 @@
*/
import { createSlice, current, PayloadAction } from '@reduxjs/toolkit';
import { LensEmbeddableInput } from '..';
import { TableInspectorAdapter } from '../editor_frame_service/types';
import { LensAppState } from './types';
import { getInitialDatasourceId, getResolvedDateRange } from '../utils';
import { LensAppState, LensStoreDeps } from './types';
export const initialState: LensAppState = {
searchSessionId: '',
@ -26,6 +28,44 @@ export const initialState: LensAppState = {
},
};
export const getPreloadedState = ({
lensServices: { data },
initialContext,
embeddableEditorIncomingState,
datasourceMap,
visualizationMap,
}: LensStoreDeps) => {
const initialDatasourceId = getInitialDatasourceId(datasourceMap);
const datasourceStates: LensAppState['datasourceStates'] = {};
if (initialDatasourceId) {
datasourceStates[initialDatasourceId] = {
state: null,
isLoading: true,
};
}
const state = {
...initialState,
isLoading: true,
query: data.query.queryString.getQuery(),
// Do not use app-specific filters from previous app,
// only if Lens was opened with the intention to visualize a field (e.g. coming from Discover)
filters: !initialContext
? data.query.filterManager.getGlobalFilters()
: data.query.filterManager.getFilters(),
searchSessionId: data.search.session.getSessionId(),
resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter),
isLinkedToOriginatingApp: Boolean(embeddableEditorIncomingState?.originatingApp),
activeDatasourceId: initialDatasourceId,
datasourceStates,
visualization: {
state: null as unknown,
activeId: Object.keys(visualizationMap)[0] || null,
},
};
return state;
};
export const lensSlice = createSlice({
name: 'lens',
initialState,
@ -254,6 +294,13 @@ export const lensSlice = createSlice({
};
},
navigateAway: (state) => state,
loadInitial: (
state,
payload: PayloadAction<{
initialInput?: LensEmbeddableInput;
redirectCallback: (savedObjectId?: string) => void;
}>
) => state,
},
});

View file

@ -5,14 +5,14 @@
* 2.0.
*/
import { Dispatch, MiddlewareAPI, PayloadAction } from '@reduxjs/toolkit';
import { Dispatch, MiddlewareAPI, Action } from '@reduxjs/toolkit';
import { isEqual } from 'lodash';
import { LensAppState } from './types';
import { lensSlice } from './lens_slice';
/** cancels updates to the store that don't change the state */
export const optimizingMiddleware = () => (store: MiddlewareAPI) => {
return (next: Dispatch) => (action: PayloadAction<Partial<LensAppState>>) => {
if (action.type === 'lens/onActiveDataChange') {
return (next: Dispatch) => (action: Action) => {
if (lensSlice.actions.onActiveDataChange.match(action)) {
if (isEqual(store.getState().lens.activeData, action.payload)) {
return;
}

View file

@ -5,11 +5,15 @@
* 2.0.
*/
import { VisualizeFieldContext } from 'src/plugins/ui_actions/public';
import { EmbeddableEditorState } from 'src/plugins/embeddable/public';
import { Filter, Query, SavedQuery } from '../../../../../src/plugins/data/public';
import { Document } from '../persistence';
import { TableInspectorAdapter } from '../editor_frame_service/types';
import { DateRange } from '../../common';
import { LensAppServices } from '../app_plugin/types';
import { DatasourceMap, VisualizationMap } from '../types';
export interface PreviewState {
visualization: {
@ -49,3 +53,11 @@ export type DispatchSetState = (
export interface LensState {
lens: LensAppState;
}
export interface LensStoreDeps {
lensServices: LensAppServices;
datasourceMap: DatasourceMap;
visualizationMap: VisualizationMap;
initialContext?: VisualizeFieldContext;
embeddableEditorIncomingState?: EmbeddableEditorState;
}

View file

@ -45,10 +45,13 @@ export interface EditorFrameProps {
showNoDataPopover: () => void;
}
export type VisualizationMap = Record<string, Visualization>;
export type DatasourceMap = Record<string, Datasource>;
export interface EditorFrameInstance {
EditorFrameContainer: (props: EditorFrameProps) => React.ReactElement;
datasourceMap: Record<string, Datasource>;
visualizationMap: Record<string, Visualization>;
datasourceMap: DatasourceMap;
visualizationMap: VisualizationMap;
}
export interface EditorFrameSetup {

View file

@ -13,7 +13,7 @@ import { SavedObjectReference } from 'kibana/public';
import { Filter, Query } from 'src/plugins/data/public';
import { uniq } from 'lodash';
import { Document } from './persistence/saved_object_store';
import { Datasource } from './types';
import { Datasource, DatasourceMap } from './types';
import { extractFilterReferences } from './persistence';
export function getVisualizeGeoFieldMessage(fieldType: string) {
@ -55,10 +55,7 @@ export function getActiveDatasourceIdFromDoc(doc?: Document) {
return firstDatasourceFromDoc || null;
}
export const getInitialDatasourceId = (
datasourceMap: Record<string, Datasource>,
doc?: Document
) => {
export const getInitialDatasourceId = (datasourceMap: DatasourceMap, doc?: Document) => {
return (doc && getActiveDatasourceIdFromDoc(doc)) || Object.keys(datasourceMap)[0] || null;
};