[maps] fix map embeddable using separate refresh interval timer from container (#97298)

* [maps] remove refresh interval timer from MapContainer

* tslint

* use redux store to determine when loading is finished

* tslint

* tslint

* tslint

* apm tslint

* security_solution tslint

* review feedback

* remove unused file

* tslint

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2021-06-07 12:31:49 -06:00 committed by GitHub
parent e63816577d
commit 7033741885
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 91 additions and 170 deletions

View file

@ -83,10 +83,6 @@ export function EmbeddedMapComponent() {
attributes: { title: '' },
id: uuid.v4(),
filters: mapFilters,
refreshConfig: {
value: 0,
pause: false,
},
viewMode: ViewMode.VIEW,
isLayerTOCOpen: false,
query: {

View file

@ -76,10 +76,6 @@ export function EmbeddedMapComponent({
attributes: { title: '' },
filters: [],
hidePanelTitles: true,
refreshConfig: {
value: 0,
pause: false,
},
viewMode: ViewMode.VIEW,
isLayerTOCOpen: false,
hideFilterActions: true,

View file

@ -24,11 +24,6 @@ export type MapQuery = Query & {
queryLastTriggeredAt?: string;
};
export type MapRefreshConfig = {
isPaused: boolean;
interval: number;
};
export type MapCenter = {
lat: number;
lon: number;

View file

@ -23,12 +23,10 @@ export const LAYER_DATA_LOAD_ERROR = 'LAYER_DATA_LOAD_ERROR';
export const UPDATE_SOURCE_DATA_REQUEST = 'UPDATE_SOURCE_DATA_REQUEST';
export const SET_JOINS = 'SET_JOINS';
export const SET_QUERY = 'SET_QUERY';
export const TRIGGER_REFRESH_TIMER = 'TRIGGER_REFRESH_TIMER';
export const UPDATE_LAYER_PROP = 'UPDATE_LAYER_PROP';
export const UPDATE_LAYER_STYLE = 'UPDATE_LAYER_STYLE';
export const SET_LAYER_STYLE_META = 'SET_LAYER_STYLE_META';
export const UPDATE_SOURCE_PROP = 'UPDATE_SOURCE_PROP';
export const SET_REFRESH_CONFIG = 'SET_REFRESH_CONFIG';
export const SET_MOUSE_COORDINATES = 'SET_MOUSE_COORDINATES';
export const CLEAR_MOUSE_COORDINATES = 'CLEAR_MOUSE_COORDINATES';
export const SET_GOTO = 'SET_GOTO';

View file

@ -38,10 +38,8 @@ import {
SET_MOUSE_COORDINATES,
SET_OPEN_TOOLTIPS,
SET_QUERY,
SET_REFRESH_CONFIG,
SET_SCROLL_ZOOM,
TRACK_MAP_SETTINGS,
TRIGGER_REFRESH_TIMER,
UPDATE_DRAW_STATE,
UPDATE_MAP_SETTING,
} from './map_action_constants';
@ -53,7 +51,6 @@ import {
MapCenter,
MapCenterAndZoom,
MapExtent,
MapRefreshConfig,
Timeslice,
} from '../../common/descriptor_types';
import { INITIAL_LOCATION } from '../../common/constants';
@ -276,7 +273,7 @@ export function setQuery({
queryLastTriggeredAt: forceRefresh ? generateQueryTimestamp() : prevTriggeredAt,
},
filters: filters ? filters : getFilters(getState()),
searchSessionId,
searchSessionId: searchSessionId ? searchSessionId : getSearchSessionId(getState()),
searchSessionMapBuffer,
};
@ -307,31 +304,6 @@ export function setQuery({
};
}
export function setRefreshConfig({ isPaused, interval }: MapRefreshConfig) {
return {
type: SET_REFRESH_CONFIG,
isPaused,
interval,
};
}
export function triggerRefreshTimer() {
return async (
dispatch: ThunkDispatch<MapStoreState, void, AnyAction>,
getState: () => MapStoreState
) => {
dispatch({
type: TRIGGER_REFRESH_TIMER,
});
if (getMapSettings(getState()).autoFitToDataBounds) {
dispatch(autoFitToBounds());
} else {
await dispatch(syncDataForAllLayers());
}
};
}
export function updateDrawState(drawState: DrawState | null) {
return (dispatch: Dispatch) => {
if (drawState !== null) {

View file

@ -10,11 +10,10 @@ import { ThunkDispatch } from 'redux-thunk';
import { connect } from 'react-redux';
import { MapContainer } from './map_container';
import { getFlyoutDisplay, getIsFullScreen } from '../../selectors/ui_selectors';
import { triggerRefreshTimer, cancelAllInFlightRequests, exitFullScreen } from '../../actions';
import { cancelAllInFlightRequests, exitFullScreen } from '../../actions';
import {
areLayersLoaded,
getLayerList,
getRefreshConfig,
getMapInitError,
getMapSettings,
getQueryableUniqueIndexPatternIds,
@ -27,7 +26,6 @@ function mapStateToProps(state: MapStoreState) {
areLayersLoaded: areLayersLoaded(state),
flyoutDisplay: getFlyoutDisplay(state),
isFullScreen: getIsFullScreen(state),
refreshConfig: getRefreshConfig(state),
mapInitError: getMapInitError(state),
indexPatternIds: getQueryableUniqueIndexPatternIds(state),
settings: getMapSettings(state),
@ -37,7 +35,6 @@ function mapStateToProps(state: MapStoreState) {
function mapDispatchToProps(dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) {
return {
triggerRefreshTimer: () => dispatch(triggerRefreshTimer()),
exitFullScreen: () => {
dispatch(exitFullScreen());
getCoreChrome().setIsVisible(true);

View file

@ -26,7 +26,6 @@ import { MapSettings } from '../../reducers/map';
import { MapSettingsPanel } from '../map_settings_panel';
import { registerLayerWizards } from '../../classes/layers/load_layer_wizards';
import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property';
import { MapRefreshConfig } from '../../../common/descriptor_types';
import { ILayer } from '../../classes/layers/layer';
const RENDER_COMPLETE_EVENT = 'renderComplete';
@ -43,9 +42,7 @@ export interface Props {
isFullScreen: boolean;
indexPatternIds: string[];
mapInitError: string | null | undefined;
refreshConfig: MapRefreshConfig;
renderTooltipContent?: RenderToolTipContent;
triggerRefreshTimer: () => void;
title?: string;
description?: string;
settings: MapSettings;
@ -62,9 +59,6 @@ interface State {
export class MapContainer extends Component<Props, State> {
private _isMounted: boolean = false;
private _isInitalLoadRenderTimerStarted: boolean = false;
private _refreshTimerId: number | null = null;
private _prevIsPaused: boolean | null = null;
private _prevInterval: number | null = null;
state: State = {
isInitialLoadRenderTimeoutComplete: false,
@ -75,14 +69,12 @@ export class MapContainer extends Component<Props, State> {
componentDidMount() {
this._isMounted = true;
this._setRefreshTimer();
this._loadShowFitToBoundsButton();
this._loadShowTimesliderButton();
registerLayerWizards();
}
componentDidUpdate() {
this._setRefreshTimer();
this._loadShowFitToBoundsButton();
this._loadShowTimesliderButton();
if (this.props.areLayersLoaded && !this._isInitalLoadRenderTimerStarted) {
@ -93,7 +85,6 @@ export class MapContainer extends Component<Props, State> {
componentWillUnmount() {
this._isMounted = false;
this._clearRefreshTimer();
this.props.cancelAllInFlightRequests();
}
@ -141,33 +132,6 @@ export class MapContainer extends Component<Props, State> {
}
}
_setRefreshTimer = () => {
const { isPaused, interval } = this.props.refreshConfig;
if (this._prevIsPaused === isPaused && this._prevInterval === interval) {
// refreshConfig is the same, nothing to do
return;
}
this._prevIsPaused = isPaused;
this._prevInterval = interval;
this._clearRefreshTimer();
if (!isPaused && interval > 0) {
this._refreshTimerId = window.setInterval(() => {
this.props.triggerRefreshTimer();
}, interval);
}
};
_clearRefreshTimer = () => {
if (this._refreshTimerId) {
window.clearInterval(this._refreshTimerId);
this._refreshTimerId = null;
}
};
// Mapbox does not provide any feedback when rendering is complete.
// Temporary solution is just to wait set period of time after data has loaded.
_startInitialLoadRenderTimer = () => {

View file

@ -26,14 +26,12 @@ import {
TimeRange,
Filter,
Query,
RefreshInterval,
} from '../../../../../src/plugins/data/public';
import { createExtentFilter } from '../../common/elasticsearch_util';
import {
replaceLayerList,
setMapSettings,
setQuery,
setRefreshConfig,
disableScrollZoom,
setReadOnly,
} from '../actions';
@ -106,7 +104,6 @@ export class MapEmbeddable
private _prevMapExtent?: MapExtent;
private _prevTimeRange?: TimeRange;
private _prevQuery?: Query;
private _prevRefreshConfig?: RefreshInterval;
private _prevFilters: Filter[] = [];
private _prevSyncColors?: boolean;
private _prevSearchSessionId?: string;
@ -171,9 +168,6 @@ export class MapEmbeddable
this._dispatchSetQuery({
forceRefresh: false,
});
if (this.input.refreshConfig) {
this._dispatchSetRefreshConfig(this.input.refreshConfig);
}
this._unsubscribeFromStore = this._savedMap.getStore().subscribe(() => {
this._handleStoreChanges();
@ -260,10 +254,6 @@ export class MapEmbeddable
});
}
if (this.input.refreshConfig && !_.isEqual(this.input.refreshConfig, this._prevRefreshConfig)) {
this._dispatchSetRefreshConfig(this.input.refreshConfig);
}
if (this.input.syncColors !== this._prevSyncColors) {
this._dispatchSetChartsPaletteServiceGetColor(this.input.syncColors);
}
@ -318,16 +308,6 @@ export class MapEmbeddable
);
}
_dispatchSetRefreshConfig(refreshConfig: RefreshInterval) {
this._prevRefreshConfig = refreshConfig;
this._savedMap.getStore().dispatch(
setRefreshConfig({
isPaused: refreshConfig.pause,
interval: refreshConfig.value,
})
);
}
async _dispatchSetChartsPaletteServiceGetColor(syncColors?: boolean) {
this._prevSyncColors = syncColors;
const chartsPaletteServiceGetColor = syncColors

View file

@ -11,7 +11,7 @@ import {
EmbeddableOutput,
SavedObjectEmbeddableInput,
} from '../../../../../src/plugins/embeddable/public';
import { RefreshInterval, Query, Filter, TimeRange } from '../../../../../src/plugins/data/common';
import { Query, Filter, TimeRange } from '../../../../../src/plugins/data/common';
import { MapCenterAndZoom, MapExtent } from '../../common/descriptor_types';
import { MapSavedObjectAttributes } from '../../common/map_saved_object_type';
import { MapSettings } from '../reducers/map';
@ -21,7 +21,6 @@ export interface MapEmbeddableConfig {
}
interface MapEmbeddableState {
refreshConfig?: RefreshInterval;
isLayerTOCOpen?: boolean;
openTOCDetails?: string[];
mapCenter?: MapCenterAndZoom;

View file

@ -27,8 +27,6 @@ import {
SET_LAYER_STYLE_META,
SET_JOINS,
UPDATE_SOURCE_PROP,
SET_REFRESH_CONFIG,
TRIGGER_REFRESH_TIMER,
SET_MOUSE_COORDINATES,
CLEAR_MOUSE_COORDINATES,
SET_GOTO,
@ -76,7 +74,6 @@ export const DEFAULT_MAP_STATE: MapState = {
timeslice: undefined,
query: undefined,
filters: [],
refreshConfig: undefined,
refreshTimerLastTriggeredAt: undefined,
drawState: undefined,
},
@ -239,26 +236,6 @@ export function map(state: MapState = DEFAULT_MAP_STATE, action: any) {
searchSessionMapBuffer,
},
};
case SET_REFRESH_CONFIG:
const { isPaused, interval } = action;
return {
...state,
mapState: {
...state.mapState,
refreshConfig: {
isPaused,
interval,
},
},
};
case TRIGGER_REFRESH_TIMER:
return {
...state,
mapState: {
...state.mapState,
refreshTimerLastTriggeredAt: new Date().toISOString(),
},
};
case SET_SELECTED_LAYER:
const selectedMatch = state.layerList.find((layer) => layer.id === action.selectedLayerId);
return { ...state, selectedLayerId: selectedMatch ? action.selectedLayerId : null };

View file

@ -14,7 +14,6 @@ import {
MapCenter,
MapExtent,
MapQuery,
MapRefreshConfig,
Timeslice,
TooltipState,
} from '../../../common/descriptor_types';
@ -35,7 +34,6 @@ export type MapContext = {
timeslice?: Timeslice;
query?: MapQuery;
filters: Filter[];
refreshConfig?: MapRefreshConfig;
refreshTimerLastTriggeredAt?: string;
drawState?: DrawState;
searchSessionId?: string;

View file

@ -15,15 +15,13 @@ import {
getFilters,
getQuery,
getQueryableUniqueIndexPatternIds,
getRefreshConfig,
getTimeFilters,
hasDirtyState,
} from '../../../selectors/map_selectors';
import { setQuery, setRefreshConfig, enableFullScreen, openMapSettings } from '../../../actions';
import { setQuery, enableFullScreen, openMapSettings } from '../../../actions';
import { FLYOUT_STATE } from '../../../reducers/ui';
import { getInspectorAdapters } from '../../../reducers/non_serializable_instances';
import { MapStoreState } from '../../../reducers/store';
import { MapRefreshConfig } from '../../../../common/descriptor_types';
function mapStateToProps(state: MapStoreState) {
return {
@ -33,7 +31,6 @@ function mapStateToProps(state: MapStoreState) {
inspectorAdapters: getInspectorAdapters(state),
nextIndexPatternIds: getQueryableUniqueIndexPatternIds(state),
flyoutDisplay: getFlyoutDisplay(state),
refreshConfig: getRefreshConfig(state),
filters: getFilters(state),
query: getQuery(state),
timeFilters: getTimeFilters(state),
@ -42,16 +39,18 @@ function mapStateToProps(state: MapStoreState) {
function mapDispatchToProps(dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) {
return {
dispatchSetQuery: ({
setQuery: ({
forceRefresh,
filters,
query,
timeFilters,
searchSessionId,
}: {
filters?: Filter[];
query?: Query;
timeFilters?: TimeRange;
forceRefresh?: boolean;
searchSessionId?: string;
}) => {
dispatch(
setQuery({
@ -59,11 +58,10 @@ function mapDispatchToProps(dispatch: ThunkDispatch<MapStoreState, void, AnyActi
query,
timeFilters,
forceRefresh,
searchSessionId,
})
);
},
setRefreshConfig: (refreshConfig: MapRefreshConfig) =>
dispatch(setRefreshConfig(refreshConfig)),
enableFullScreen: () => dispatch(enableFullScreen()),
openMapSettings: () => dispatch(openMapSettings()),
};

View file

@ -7,6 +7,7 @@
import React from 'react';
import _ from 'lodash';
import { finalize, switchMap, tap } from 'rxjs/operators';
import { i18n } from '@kbn/i18n';
import { AppLeaveAction, AppMountParameters } from 'kibana/public';
import { Adapters } from 'src/plugins/embeddable/public';
@ -16,6 +17,7 @@ import {
getCoreChrome,
getMapsCapabilities,
getNavigation,
getTimeFilter,
getToasts,
} from '../../../kibana_services';
import {
@ -39,7 +41,7 @@ import {
import { MapContainer } from '../../../connected_components/map_container';
import { getIndexPatternsFromIds } from '../../../index_pattern_util';
import { getTopNavConfig } from '../top_nav_config';
import { MapRefreshConfig, MapQuery } from '../../../../common/descriptor_types';
import { MapQuery } from '../../../../common/descriptor_types';
import { goToSpecifiedPath } from '../../../render_app';
import { MapSavedObjectAttributes } from '../../../../common/map_saved_object_type';
import { getExistingMapPath, APP_ID } from '../../../../common/constants';
@ -51,6 +53,12 @@ import {
unsavedChangesTitle,
unsavedChangesWarning,
} from '../saved_map';
import { waitUntilTimeLayersLoad$ } from './wait_until_time_layers_load';
interface MapRefreshConfig {
isPaused: boolean;
interval: number;
}
export interface Props {
savedMap: SavedMap;
@ -64,20 +72,20 @@ export interface Props {
openMapSettings: () => void;
inspectorAdapters: Adapters;
nextIndexPatternIds: string[];
dispatchSetQuery: ({
setQuery: ({
forceRefresh,
filters,
query,
timeFilters,
searchSessionId,
}: {
filters?: Filter[];
query?: Query;
timeFilters?: TimeRange;
forceRefresh?: boolean;
searchSessionId?: string;
}) => void;
timeFilters: TimeRange;
refreshConfig: MapRefreshConfig;
setRefreshConfig: (refreshConfig: MapRefreshConfig) => void;
isSaveDisabled: boolean;
query: MapQuery | undefined;
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
@ -87,9 +95,12 @@ export interface State {
initialized: boolean;
indexPatterns: IndexPattern[];
savedQuery?: SavedQuery;
isRefreshPaused: boolean;
refreshInterval: number;
}
export class MapApp extends React.Component<Props, State> {
_autoRefreshSubscription: Subscription | null = null;
_globalSyncUnsubscribe: (() => void) | null = null;
_globalSyncChangeMonitorSubscription: Subscription | null = null;
_appSyncUnsubscribe: (() => void) | null = null;
@ -102,12 +113,26 @@ export class MapApp extends React.Component<Props, State> {
this.state = {
indexPatterns: [],
initialized: false,
isRefreshPaused: true,
refreshInterval: 0,
};
}
componentDidMount() {
this._isMounted = true;
this._autoRefreshSubscription = getTimeFilter()
.getAutoRefreshFetch$()
.pipe(
tap(() => {
this.props.setQuery({ forceRefresh: true });
}),
switchMap((done) =>
waitUntilTimeLayersLoad$(this.props.savedMap.getStore()).pipe(finalize(done))
)
)
.subscribe();
this._globalSyncUnsubscribe = startGlobalStateSyncing();
this._appSyncUnsubscribe = startAppStateSyncing(this._appStateManager);
this._globalSyncChangeMonitorSubscription = getData().query.state$.subscribe(
@ -137,6 +162,9 @@ export class MapApp extends React.Component<Props, State> {
componentWillUnmount() {
this._isMounted = false;
if (this._autoRefreshSubscription) {
this._autoRefreshSubscription.unsubscribe();
}
if (this._globalSyncUnsubscribe) {
this._globalSyncUnsubscribe();
}
@ -198,7 +226,7 @@ export class MapApp extends React.Component<Props, State> {
filterManager.setFilters(filters);
}
this.props.dispatchSetQuery({
this.props.setQuery({
forceRefresh,
filters: filterManager.getFilters(),
query,
@ -264,14 +292,16 @@ export class MapApp extends React.Component<Props, State> {
});
};
// mapRefreshConfig: MapRefreshConfig
_onRefreshConfigChange(mapRefreshConfig: MapRefreshConfig) {
this.props.setRefreshConfig(mapRefreshConfig);
_onRefreshConfigChange({ isPaused, interval }: MapRefreshConfig) {
this.setState({
isRefreshPaused: isPaused,
refreshInterval: interval,
});
updateGlobalState(
{
refreshInterval: {
pause: mapRefreshConfig.isPaused,
value: mapRefreshConfig.interval,
pause: isPaused,
value: interval,
},
},
!this.state.initialized
@ -370,8 +400,8 @@ export class MapApp extends React.Component<Props, State> {
onFiltersUpdated={this._onFiltersChange}
dateRangeFrom={this.props.timeFilters.from}
dateRangeTo={this.props.timeFilters.to}
isRefreshPaused={this.props.refreshConfig.isPaused}
refreshInterval={this.props.refreshConfig.interval}
isRefreshPaused={this.state.isRefreshPaused}
refreshInterval={this.state.refreshInterval}
onRefreshChange={({
isPaused,
refreshInterval,

View file

@ -0,0 +1,36 @@
/*
* 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 { from } from 'rxjs';
import { debounceTime, first, switchMap } from 'rxjs/operators';
import { getLayerList } from '../../../selectors/map_selectors';
import { MapStore } from '../../../reducers/store';
export function waitUntilTimeLayersLoad$(store: MapStore) {
// @ts-expect-error
const reduxState$ = from(store);
return reduxState$.pipe(
debounceTime(300),
// using switchMap since switchMap will discard promise from previous state iterations in progress
switchMap(async (state) => {
const promises = getLayerList(state).map(async (layer) => {
return {
isFilteredByGlobalTime: await layer.isFilteredByGlobalTime(),
layer,
};
});
const layersWithMeta = await Promise.all(promises);
return layersWithMeta;
}),
first((layersWithMeta) => {
const areTimeLayersStillLoading = layersWithMeta
.filter(({ isFilteredByGlobalTime }) => isFilteredByGlobalTime)
.some(({ layer }) => layer.isLayerLoading());
return !areTimeLayersStillLoading;
})
);
}

View file

@ -16,7 +16,6 @@ import {
getMapZoom,
getMapCenter,
getLayerListRaw,
getRefreshConfig,
getQuery,
getFilters,
getMapSettings,
@ -39,6 +38,7 @@ import {
getToasts,
getIsAllowByValueEmbeddables,
getSavedObjectsTagging,
getTimeFilter,
} from '../../../kibana_services';
import { goToSpecifiedPath } from '../../../render_app';
import { LayerDescriptor } from '../../../../common/descriptor_types';
@ -394,7 +394,10 @@ export class SavedMap {
zoom: getMapZoom(state),
center: getMapCenter(state),
timeFilters: getTimeFilters(state),
refreshConfig: getRefreshConfig(state),
refreshConfig: {
isPaused: getTimeFilter().getRefreshInterval().pause,
interval: getTimeFilter().getRefreshInterval().value,
},
query: _.omit(getQuery(state), 'queryLastTriggeredAt'),
filters: getFilters(state),
settings: getMapSettings(state),

View file

@ -45,7 +45,6 @@ import {
MapCenter,
MapExtent,
MapQuery,
MapRefreshConfig,
TooltipState,
VectorLayerDescriptor,
} from '../../common/descriptor_types';
@ -201,18 +200,6 @@ export const isDrawingFilter = ({ map }: MapStoreState): boolean => {
return !!map.mapState.drawState;
};
export const getRefreshConfig = ({ map }: MapStoreState): MapRefreshConfig => {
if (map.mapState.refreshConfig) {
return map.mapState.refreshConfig;
}
const refreshInterval = getTimeFilter().getRefreshInterval();
return {
isPaused: refreshInterval.pause,
interval: refreshInterval.value,
};
};
export const getRefreshTimerLastTriggeredAt = ({ map }: MapStoreState): string | undefined =>
map.mapState.refreshTimerLastTriggeredAt;

View file

@ -77,10 +77,6 @@ export function MlEmbeddedMapComponent({
attributes: { title: '' },
filters: [],
hidePanelTitles: true,
refreshConfig: {
value: 0,
pause: false,
},
viewMode: ViewMode.VIEW,
isLayerTOCOpen: false,
hideFilterActions: true,

View file

@ -70,7 +70,6 @@ export const createEmbeddable = async (
filters,
hidePanelTitles: true,
query,
refreshConfig: { value: 0, pause: true },
timeRange: {
from: new Date(startDate).toISOString(),
to: new Date(endDate).toISOString(),