[Maps] Code split Maps app (#64594)

Code-split the maps-plugin to reduce the initial `maps.plugin.js` size.

There were two main code dependencies in the plugin initialization that were the root cause of the large bundle size.

- `GisMap` wraps the entire application UX, including the add-layer-wizard. The layer wizards only need to be available there. This PR moves the `load_layer_wizard` dependency from the plugin-initialization to the `GisMap` component. 
- The `MapEmbeddableFactory` needs to be registered at plugin-initialization. However, this module imports a lot of core-application code. By code-splitting here, we avoid pulling in the entire Maps-app in the main bundle.

This also  introduces a lazy-initialization of the `GisMap` itself as an additional split to further reduce size of the bundles.
This commit is contained in:
Thomas Neirynck 2020-05-04 16:48:31 -04:00 committed by GitHub
parent f126e6130b
commit 7bf7174a2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 139 additions and 54 deletions

View file

@ -9,7 +9,11 @@ import { Filter } from 'src/plugins/data/public';
import { RenderToolTipContent } from '../../layers/tooltips/tooltip_property';
export const GisMap: React.ComponentType<{
declare const GisMap: React.ComponentType<{
addFilters: ((filters: Filter[]) => void) | null;
renderTooltipContent?: RenderToolTipContent;
}>;
export { GisMap };
// eslint-disable-next-line import/no-default-export
export default GisMap;

View file

@ -5,7 +5,7 @@
*/
import { connect } from 'react-redux';
import { GisMap } from './view';
import { GisMap as UnconnectedGisMap } from './view';
import { exitFullScreen } from '../../actions/ui_actions';
import { getFlyoutDisplay, getIsFullScreen } from '../../selectors/ui_selectors';
import { triggerRefreshTimer, cancelAllInFlightRequests } from '../../actions/map_actions';
@ -42,5 +42,6 @@ function mapDispatchToProps(dispatch) {
};
}
const connectedGisMap = connect(mapStateToProps, mapDispatchToProps)(GisMap);
export { connectedGisMap as GisMap };
const connectedGisMap = connect(mapStateToProps, mapDispatchToProps)(UnconnectedGisMap);
export { connectedGisMap as GisMap }; // GisMap is pulled in by name by the Maps-app itself
export default connectedGisMap; //lazy-loading in the embeddable requires default export

View file

@ -22,6 +22,7 @@ import { i18n } from '@kbn/i18n';
import uuid from 'uuid/v4';
import { FLYOUT_STATE } from '../../reducers/ui';
import { MapSettingsPanel } from '../map_settings_panel';
import { registerLayerWizards } from '../../layers/load_layer_wizards';
const RENDER_COMPLETE_EVENT = 'renderComplete';
@ -36,6 +37,7 @@ export class GisMap extends Component {
this._isMounted = true;
this._isInitalLoadRenderTimerStarted = false;
this._setRefreshTimer();
registerLayerWizards();
}
componentDidUpdate() {

View file

@ -40,7 +40,6 @@ function mapStateToProps(state = {}) {
scrollZoom: getScrollZoom(state),
disableInteractive: isInteractiveDisabled(state),
disableTooltipControl: isTooltipControlDisabled(state),
disableTooltipControl: isTooltipControlDisabled(state),
hideViewControl: isViewControlHidden(state),
};
}

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
// These are map-dependencies of the embeddable.
// By lazy-loading these, the Maps-app can register the embeddable when the plugin mounts, without actually pulling all the code.
// @ts-ignore
export * from '../../angular/services/gis_map_saved_object_loader';
export * from '../map_embeddable';
export * from '../../kibana_services';
export * from '../../reducers/store';
export * from '../../actions/map_actions';
export * from '../../selectors/map_selectors';
export * from '../../angular/get_initial_layers';
export * from './../merge_input_with_saved_map';

View file

@ -5,16 +5,16 @@
*/
import _ from 'lodash';
import React from 'react';
import React, { Suspense, lazy } from 'react';
import { Provider } from 'react-redux';
import { render, unmountComponentAtNode } from 'react-dom';
import 'mapbox-gl/dist/mapbox-gl.css';
import { Subscription } from 'rxjs';
import { Unsubscribe } from 'redux';
import { EuiLoadingSpinner } from '@elastic/eui';
import {
Embeddable,
IContainer,
EmbeddableInput,
EmbeddableOutput,
} from '../../../../../src/plugins/embeddable/public';
import { APPLY_FILTER_TRIGGER } from '../../../../../src/plugins/ui_actions/public';
@ -26,7 +26,6 @@ import {
Query,
RefreshInterval,
} from '../../../../../src/plugins/data/public';
import { GisMap } from '../connected_components/gis_map';
import { createMapStore, MapStore } from '../reducers/store';
import { MapSettings } from '../reducers/map';
import {
@ -43,7 +42,6 @@ import {
setHiddenLayers,
setMapSettings,
} from '../actions/map_actions';
import { MapCenterAndZoom } from '../../common/descriptor_types';
import { setReadOnly, setIsLayerTOCOpen, setOpenTOCDetails } from '../actions/ui_actions';
import { getIsLayerTOCOpen, getOpenTOCDetails } from '../selectors/ui_selectors';
import {
@ -56,36 +54,14 @@ import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants';
import { RenderToolTipContent } from '../layers/tooltips/tooltip_property';
import { getUiActions, getCoreI18n } from '../kibana_services';
interface MapEmbeddableConfig {
editUrl?: string;
indexPatterns: IIndexPattern[];
editable: boolean;
title?: string;
layerList: unknown[];
settings?: MapSettings;
}
export interface MapEmbeddableInput extends EmbeddableInput {
timeRange?: TimeRange;
filters: Filter[];
query?: Query;
refreshConfig: RefreshInterval;
isLayerTOCOpen: boolean;
openTOCDetails?: string[];
disableTooltipControl?: boolean;
disableInteractive?: boolean;
hideToolbarOverlay?: boolean;
hideLayerControl?: boolean;
hideViewControl?: boolean;
mapCenter?: MapCenterAndZoom;
hiddenLayers?: string[];
hideFilterActions?: boolean;
}
import { MapEmbeddableInput, MapEmbeddableConfig } from './types';
export { MapEmbeddableInput, MapEmbeddableConfig };
export interface MapEmbeddableOutput extends EmbeddableOutput {
indexPatterns: IIndexPattern[];
}
const GisMap = lazy(() => import('../connected_components/gis_map'));
export class MapEmbeddable extends Embeddable<MapEmbeddableInput, MapEmbeddableOutput> {
type = MAP_SAVED_OBJECT_TYPE;
@ -254,10 +230,12 @@ export class MapEmbeddable extends Embeddable<MapEmbeddableInput, MapEmbeddableO
render(
<Provider store={this._store}>
<I18nContext>
<GisMap
addFilters={this.input.hideFilterActions ? null : this.addFilters}
renderTooltipContent={this._renderTooltipContent}
/>
<Suspense fallback={<EuiLoadingSpinner />}>
<GisMap
addFilters={this.input.hideFilterActions ? null : this.addFilters}
renderTooltipContent={this._renderTooltipContent}
/>
</Suspense>
</I18nContext>
</Provider>,
this._domNode

View file

@ -6,24 +6,67 @@
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { AnyAction } from 'redux';
import { IIndexPattern } from 'src/plugins/data/public';
// @ts-ignore
import { getMapsSavedObjectLoader } from '../angular/services/gis_map_saved_object_loader';
import { MapEmbeddable, MapEmbeddableInput } from './map_embeddable';
import { getIndexPatternService, getHttp, getMapsCapabilities } from '../kibana_services';
import {
Embeddable,
EmbeddableFactoryDefinition,
IContainer,
} from '../../../../../src/plugins/embeddable/public';
import { createMapPath, MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants';
import { createMapStore } from '../reducers/store';
import { addLayerWithoutDataSync } from '../actions/map_actions';
import { getQueryableUniqueIndexPatternIds } from '../selectors/map_selectors';
import { getInitialLayers } from '../angular/get_initial_layers';
import { mergeInputWithSavedMap } from './merge_input_with_saved_map';
import '../index.scss';
import { createMapPath, MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants';
import { MapStore, MapStoreState } from '../reducers/store';
import { MapEmbeddableConfig, MapEmbeddableInput } from './types';
import { MapEmbeddableOutput } from './map_embeddable';
import { RenderToolTipContent } from '../layers/tooltips/tooltip_property';
import { EventHandlers } from '../reducers/non_serializable_instances';
let whenModulesLoadedPromise: Promise<boolean>;
let getMapsSavedObjectLoader: any;
let MapEmbeddable: new (
config: MapEmbeddableConfig,
initialInput: MapEmbeddableInput,
parent?: IContainer,
renderTooltipContent?: RenderToolTipContent,
eventHandlers?: EventHandlers
) => Embeddable<MapEmbeddableInput, MapEmbeddableOutput>;
let getIndexPatternService: () => {
get: (id: string) => IIndexPattern | undefined;
};
let getHttp: () => any;
let getMapsCapabilities: () => any;
let createMapStore: () => MapStore;
let addLayerWithoutDataSync: (layerDescriptor: unknown) => AnyAction;
let getQueryableUniqueIndexPatternIds: (state: MapStoreState) => string[];
let getInitialLayers: (layerListJSON?: string, initialLayers?: unknown[]) => unknown[];
let mergeInputWithSavedMap: any;
async function waitForMapDependencies(): Promise<boolean> {
if (typeof whenModulesLoadedPromise !== 'undefined') {
return whenModulesLoadedPromise;
}
whenModulesLoadedPromise = new Promise(async resolve => {
({
// @ts-ignore
getMapsSavedObjectLoader,
getQueryableUniqueIndexPatternIds,
MapEmbeddable,
getIndexPatternService,
getHttp,
getMapsCapabilities,
createMapStore,
addLayerWithoutDataSync,
getInitialLayers,
mergeInputWithSavedMap,
} = await import('./lazy'));
resolve(true);
});
return whenModulesLoadedPromise;
}
export class MapEmbeddableFactory implements EmbeddableFactoryDefinition {
type = MAP_SAVED_OBJECT_TYPE;
@ -36,6 +79,7 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition {
};
async isEditable() {
await waitForMapDependencies();
return getMapsCapabilities().save as boolean;
}
@ -53,7 +97,7 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition {
async _getIndexPatterns(layerList: unknown[]): Promise<IIndexPattern[]> {
// Need to extract layerList from store to get queryable index pattern ids
const store = createMapStore();
let queryableIndexPatternIds;
let queryableIndexPatternIds: string[];
try {
layerList.forEach((layerDescriptor: unknown) => {
store.dispatch(addLayerWithoutDataSync(layerDescriptor));
@ -69,6 +113,7 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition {
const promises = queryableIndexPatternIds.map(async indexPatternId => {
try {
// @ts-ignore
return await getIndexPatternService().get(indexPatternId);
} catch (error) {
// Unable to load index pattern, better to not throw error so map embeddable can render
@ -90,6 +135,7 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition {
input: MapEmbeddableInput,
parent?: IContainer
) => {
await waitForMapDependencies();
const savedMap = await this._fetchSavedMap(savedObjectId);
const layerList = getInitialLayers(savedMap.layerListJSON);
const indexPatterns = await this._getIndexPatterns(layerList);
@ -129,6 +175,7 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition {
};
create = async (input: MapEmbeddableInput, parent?: IContainer) => {
await waitForMapDependencies();
const layerList = getInitialLayers();
const indexPatterns = await this._getIndexPatterns(layerList);

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { IIndexPattern } from '../../../../../src/plugins/data/common/index_patterns';
import { MapSettings } from '../reducers/map';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { EmbeddableInput } from '../../../../../src/plugins/embeddable/public/lib/embeddables';
import { Filter, Query, RefreshInterval, TimeRange } from '../../../../../src/plugins/data/common';
import { MapCenterAndZoom } from '../../common/descriptor_types';
export interface MapEmbeddableConfig {
editUrl?: string;
indexPatterns: IIndexPattern[];
editable: boolean;
title?: string;
layerList: unknown[];
settings?: MapSettings;
}
export interface MapEmbeddableInput extends EmbeddableInput {
timeRange?: TimeRange;
filters: Filter[];
query?: Query;
refreshConfig: RefreshInterval;
isLayerTOCOpen: boolean;
openTOCDetails?: string[];
disableTooltipControl?: boolean;
disableInteractive?: boolean;
hideToolbarOverlay?: boolean;
hideLayerControl?: boolean;
hideViewControl?: boolean;
mapCenter?: MapCenterAndZoom;
hiddenLayers?: string[];
hideFilterActions?: boolean;
}

View file

@ -36,11 +36,10 @@ import {
import { featureCatalogueEntry } from './feature_catalogue_entry';
// @ts-ignore
import { getMapsVisTypeAlias } from './maps_vis_type_alias';
import { registerLayerWizards } from './layers/load_layer_wizards';
import { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
import { VisualizationsSetup } from '../../../../src/plugins/visualizations/public';
import { MAP_SAVED_OBJECT_TYPE } from '../common/constants';
import { MapEmbeddableFactory } from './embeddable';
import { MapEmbeddableFactory } from './embeddable/map_embeddable_factory';
import { EmbeddableSetup } from '../../../../src/plugins/embeddable/public';
export interface MapsPluginSetupDependencies {
@ -85,7 +84,6 @@ export const bindStartCoreAndPlugins = (core: CoreStart, plugins: any) => {
setUiActions(plugins.uiActions);
setNavigation(plugins.navigation);
setCoreI18n(core.i18n);
registerLayerWizards();
};
/**