kibana/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
Marco Liberati 62623cdab9
[Lens] New sorting feature for the datatable visualization (#84435)
Co-authored-by: Wylie Conlon <wylieconlon@gmail.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
2020-12-15 12:37:59 +01:00

336 lines
11 KiB
TypeScript

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect, useReducer, useState } from 'react';
import { CoreSetup, CoreStart } from 'kibana/public';
import { PaletteRegistry } from 'src/plugins/charts/public';
import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public';
import { Datasource, FramePublicAPI, Visualization } from '../../types';
import { reducer, getInitialState } from './state_management';
import { DataPanelWrapper } from './data_panel_wrapper';
import { ConfigPanelWrapper } from './config_panel';
import { FrameLayout } from './frame_layout';
import { SuggestionPanel } from './suggestion_panel';
import { WorkspacePanel } from './workspace_panel';
import { Document } from '../../persistence/saved_object_store';
import { RootDragDropProvider } from '../../drag_drop';
import { getSavedObjectFormat } from './save';
import { generateId } from '../../id_generator';
import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public';
import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public';
import { EditorFrameStartPlugins } from '../service';
import { initializeDatasources, createDatasourceLayers } from './state_helpers';
import { applyVisualizeFieldSuggestions } from './suggestion_helpers';
export interface EditorFrameProps {
doc?: Document;
datasourceMap: Record<string, Datasource>;
visualizationMap: Record<string, Visualization>;
initialDatasourceId: string | null;
initialVisualizationId: string | null;
ExpressionRenderer: ReactExpressionRendererType;
palettes: PaletteRegistry;
onError: (e: { message: string }) => void;
core: CoreSetup | CoreStart;
plugins: EditorFrameStartPlugins;
dateRange: {
fromDate: string;
toDate: string;
};
query: Query;
filters: Filter[];
savedQuery?: SavedQuery;
onChange: (arg: {
filterableIndexPatterns: string[];
doc: Document;
isSaveable: boolean;
}) => void;
showNoDataPopover: () => void;
initialContext?: VisualizeFieldContext;
}
export function EditorFrame(props: EditorFrameProps) {
const [state, dispatch] = useReducer(reducer, props, getInitialState);
const [visualizeTriggerFieldContext, setVisualizeTriggerFieldContext] = useState(
props.initialContext
);
const { onError } = props;
const activeVisualization =
state.visualization.activeId && props.visualizationMap[state.visualization.activeId];
const allLoaded = Object.values(state.datasourceStates).every(
({ isLoading }) => typeof isLoading === 'boolean' && !isLoading
);
// Initialize current datasource and all active datasources
useEffect(
() => {
// prevents executing dispatch on unmounted component
let isUnmounted = false;
if (!allLoaded) {
initializeDatasources(
props.datasourceMap,
state.datasourceStates,
props.doc?.references,
visualizeTriggerFieldContext
)
.then((result) => {
if (!isUnmounted) {
Object.entries(result).forEach(([datasourceId, { state: datasourceState }]) => {
dispatch({
type: 'UPDATE_DATASOURCE_STATE',
updater: datasourceState,
datasourceId,
});
});
}
})
.catch(onError);
}
return () => {
isUnmounted = true;
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[allLoaded, onError]
);
const datasourceLayers = createDatasourceLayers(props.datasourceMap, state.datasourceStates);
const framePublicAPI: FramePublicAPI = {
datasourceLayers,
activeData: state.activeData,
dateRange: props.dateRange,
query: props.query,
filters: props.filters,
availablePalettes: props.palettes,
addNewLayer() {
const newLayerId = generateId();
dispatch({
type: 'UPDATE_LAYER',
datasourceId: state.activeDatasourceId!,
layerId: newLayerId,
updater: props.datasourceMap[state.activeDatasourceId!].insertLayer,
});
return newLayerId;
},
removeLayers(layerIds: string[]) {
if (activeVisualization && activeVisualization.removeLayer && state.visualization.state) {
dispatch({
type: 'UPDATE_VISUALIZATION_STATE',
visualizationId: activeVisualization.id,
updater: layerIds.reduce(
(acc, layerId) =>
activeVisualization.removeLayer ? activeVisualization.removeLayer(acc, layerId) : acc,
state.visualization.state
),
});
}
layerIds.forEach((layerId) => {
const layerDatasourceId = Object.entries(props.datasourceMap).find(
([datasourceId, datasource]) =>
state.datasourceStates[datasourceId] &&
datasource.getLayers(state.datasourceStates[datasourceId].state).includes(layerId)
)![0];
dispatch({
type: 'UPDATE_LAYER',
layerId,
datasourceId: layerDatasourceId,
updater: props.datasourceMap[layerDatasourceId].removeLayer,
});
});
},
};
useEffect(
() => {
if (props.doc) {
dispatch({
type: 'VISUALIZATION_LOADED',
doc: {
...props.doc,
state: {
...props.doc.state,
visualization: props.doc.visualizationType
? props.visualizationMap[props.doc.visualizationType].initialize(
framePublicAPI,
props.doc.state.visualization
)
: props.doc.state.visualization,
},
},
});
} else {
dispatch({
type: 'RESET',
state: getInitialState(props),
});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[props.doc]
);
// Initialize visualization as soon as all datasources are ready
useEffect(
() => {
if (allLoaded && state.visualization.state === null && activeVisualization) {
const initialVisualizationState = activeVisualization.initialize(framePublicAPI);
dispatch({
type: 'UPDATE_VISUALIZATION_STATE',
visualizationId: activeVisualization.id,
updater: initialVisualizationState,
});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[allLoaded, activeVisualization, state.visualization.state]
);
// Get suggestions for visualize field when all datasources are ready
useEffect(() => {
if (allLoaded && visualizeTriggerFieldContext && !props.doc) {
applyVisualizeFieldSuggestions({
datasourceMap: props.datasourceMap,
datasourceStates: state.datasourceStates,
visualizationMap: props.visualizationMap,
activeVisualizationId: state.visualization.activeId,
visualizationState: state.visualization.state,
visualizeTriggerFieldContext,
dispatch,
});
setVisualizeTriggerFieldContext(undefined);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allLoaded]);
// The frame needs to call onChange every time its internal state changes
useEffect(
() => {
const activeDatasource =
state.activeDatasourceId && !state.datasourceStates[state.activeDatasourceId].isLoading
? props.datasourceMap[state.activeDatasourceId]
: undefined;
if (!activeDatasource || !activeVisualization) {
return;
}
props.onChange(
getSavedObjectFormat({
activeDatasources: Object.keys(state.datasourceStates).reduce(
(datasourceMap, datasourceId) => ({
...datasourceMap,
[datasourceId]: props.datasourceMap[datasourceId],
}),
{}
),
visualization: activeVisualization,
state,
framePublicAPI,
})
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
activeVisualization,
state.datasourceStates,
state.visualization,
state.activeData,
props.query,
props.dateRange,
props.filters,
props.savedQuery,
state.title,
]
);
return (
<RootDragDropProvider>
<FrameLayout
dataPanel={
<DataPanelWrapper
datasourceMap={props.datasourceMap}
activeDatasource={state.activeDatasourceId}
datasourceState={
state.activeDatasourceId
? state.datasourceStates[state.activeDatasourceId].state
: null
}
datasourceIsLoading={
state.activeDatasourceId
? state.datasourceStates[state.activeDatasourceId].isLoading
: true
}
dispatch={dispatch}
core={props.core}
query={props.query}
dateRange={props.dateRange}
filters={props.filters}
showNoDataPopover={props.showNoDataPopover}
/>
}
configPanel={
allLoaded && (
<ConfigPanelWrapper
activeDatasourceId={state.activeDatasourceId!}
datasourceMap={props.datasourceMap}
datasourceStates={state.datasourceStates}
visualizationMap={props.visualizationMap}
activeVisualizationId={state.visualization.activeId}
dispatch={dispatch}
visualizationState={state.visualization.state}
framePublicAPI={framePublicAPI}
core={props.core}
/>
)
}
workspacePanel={
allLoaded && (
<WorkspacePanel
title={state.title}
activeDatasourceId={state.activeDatasourceId}
activeVisualizationId={state.visualization.activeId}
datasourceMap={props.datasourceMap}
datasourceStates={state.datasourceStates}
framePublicAPI={framePublicAPI}
visualizationState={state.visualization.state}
visualizationMap={props.visualizationMap}
dispatch={dispatch}
ExpressionRenderer={props.ExpressionRenderer}
core={props.core}
plugins={props.plugins}
visualizeTriggerFieldContext={visualizeTriggerFieldContext}
/>
)
}
suggestionsPanel={
allLoaded && (
<SuggestionPanel
frame={framePublicAPI}
activeDatasourceId={state.activeDatasourceId}
activeVisualizationId={state.visualization.activeId}
datasourceMap={props.datasourceMap}
datasourceStates={state.datasourceStates}
visualizationState={state.visualization.state}
visualizationMap={props.visualizationMap}
dispatch={dispatch}
ExpressionRenderer={props.ExpressionRenderer}
stagedPreview={state.stagedPreview}
plugins={props.plugins}
/>
)
}
/>
</RootDragDropProvider>
);
}