From f26f95889bf3e71c2ddc438698aee9183fa03aa6 Mon Sep 17 00:00:00 2001 From: Ester Marti Date: Mon, 23 Aug 2021 16:38:00 +0200 Subject: [PATCH] Add global state to stack monitoring react app --- .../application/global_state_context.tsx | 55 +++++++++++++++++++ .../monitoring/public/application/index.tsx | 35 +++++++----- .../application/preserve_query_history.ts | 38 +++++++++++++ .../plugins/monitoring/public/legacy_shims.ts | 4 ++ x-pack/plugins/monitoring/public/url_state.ts | 26 ++++++--- 5 files changed, 135 insertions(+), 23 deletions(-) create mode 100644 x-pack/plugins/monitoring/public/application/global_state_context.tsx create mode 100644 x-pack/plugins/monitoring/public/application/preserve_query_history.ts diff --git a/x-pack/plugins/monitoring/public/application/global_state_context.tsx b/x-pack/plugins/monitoring/public/application/global_state_context.tsx new file mode 100644 index 0000000000000..2581d4232489f --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/global_state_context.tsx @@ -0,0 +1,55 @@ +/* + * 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 React, { createContext } from 'react'; +import { GlobalState } from '../url_state'; +import { MonitoringStartPluginDependencies } from '../types'; + +export const GlobalStateContext = createContext({}); + +interface GlobalStateProviderProps { + query: MonitoringStartPluginDependencies['data']['query']; + toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts']; + children: React.ReactNode; +} + +export const GlobalStateProvider = ({ query, toasts, children }: GlobalStateProviderProps) => { + const fakeRootScope: Partial = { + $on: ( + name: string, + listener: (event: ng.IAngularEvent, ...args: any[]) => any + ): (() => void) => () => {}, + $applyAsync: () => {}, + }; + + const fakeAngularLocation: Partial = { + search: () => { + return {} as any; + }, + replace: () => { + return {} as any; + }, + }; + + const localState: { [key: string]: unknown } = {}; + const state = new GlobalState(query, toasts, fakeRootScope, fakeAngularLocation, localState); + + const initialState: any = state.getState(); + for (const key in initialState) { + if (!initialState.hasOwnProperty(key)) { + continue; + } + localState[key] = initialState[key]; + } + + localState.save = () => { + const newState = { ...localState }; + delete newState.save; + state.setState(newState); + }; + + return {children}; +}; diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx index a0c9afd73f0ce..763507dc495c9 100644 --- a/x-pack/plugins/monitoring/public/application/index.tsx +++ b/x-pack/plugins/monitoring/public/application/index.tsx @@ -8,10 +8,12 @@ import { CoreStart, AppMountParameters } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; -import { Route, Switch, Redirect, HashRouter } from 'react-router-dom'; +import { Route, Switch, Redirect, Router } from 'react-router-dom'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { LoadingPage } from './pages/loading_page'; import { MonitoringStartPluginDependencies } from '../types'; +import { GlobalStateProvider } from './global_state_context'; +import { createPreserveQueryHistory } from './preserve_query_history'; export const renderApp = ( core: CoreStart, @@ -29,21 +31,26 @@ const MonitoringApp: React.FC<{ core: CoreStart; plugins: MonitoringStartPluginDependencies; }> = ({ core, plugins }) => { + const history = createPreserveQueryHistory(); + return ( - - - - - - - - - + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/monitoring/public/application/preserve_query_history.ts b/x-pack/plugins/monitoring/public/application/preserve_query_history.ts new file mode 100644 index 0000000000000..9e7858cf6e849 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/preserve_query_history.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { History, createHashHistory, LocationDescriptor, LocationDescriptorObject } from 'history'; + +function preserveQueryParameters( + history: History, + location: LocationDescriptorObject +): LocationDescriptorObject { + location.search = history.location.search; + return location; +} + +function createLocationDescriptorObject( + location: LocationDescriptor, + state?: History.LocationState +): LocationDescriptorObject { + return typeof location === 'string' ? { pathname: location, state } : location; +} + +export function createPreserveQueryHistory() { + const history = createHashHistory({ hashType: 'slash' }); + const oldPush = history.push; + const oldReplace = history.replace; + history.push = (path: LocationDescriptor, state?: History.LocationState) => + oldPush.apply(history, [ + preserveQueryParameters(history, createLocationDescriptorObject(path, state)), + ]); + history.replace = (path: LocationDescriptor, state?: History.LocationState) => + oldReplace.apply(history, [ + preserveQueryParameters(history, createLocationDescriptorObject(path, state)), + ]); + return history; +} diff --git a/x-pack/plugins/monitoring/public/legacy_shims.ts b/x-pack/plugins/monitoring/public/legacy_shims.ts index 1d897b710d7fa..fe754a965e3f1 100644 --- a/x-pack/plugins/monitoring/public/legacy_shims.ts +++ b/x-pack/plugins/monitoring/public/legacy_shims.ts @@ -147,4 +147,8 @@ export class Legacy { } return Legacy._shims; } + + public static isInitializated(): boolean { + return Boolean(Legacy._shims); + } } diff --git a/x-pack/plugins/monitoring/public/url_state.ts b/x-pack/plugins/monitoring/public/url_state.ts index f490654d579ae..c87230e92d1b1 100644 --- a/x-pack/plugins/monitoring/public/url_state.ts +++ b/x-pack/plugins/monitoring/public/url_state.ts @@ -74,8 +74,8 @@ export class GlobalState { constructor( queryService: MonitoringStartPluginDependencies['data']['query'], toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts'], - rootScope: ng.IRootScopeService, - ngLocation: ng.ILocationService, + rootScope: Partial, + ngLocation: Partial, externalState: RawObject ) { this.timefilterRef = queryService.timefilter.timefilter; @@ -102,11 +102,16 @@ export class GlobalState { this.stateContainerChangeSub = this.stateContainer.state$.subscribe(() => { this.lastAssignedState = this.getState(); if (!this.stateContainer.get() && this.lastKnownGlobalState) { - ngLocation.search(`${GLOBAL_STATE_KEY}=${this.lastKnownGlobalState}`).replace(); + ngLocation.search?.(`${GLOBAL_STATE_KEY}=${this.lastKnownGlobalState}`).replace(); } - Legacy.shims.breadcrumbs.update(); + + // TODO: check if this is not needed after https://github.com/elastic/kibana/pull/109132 is merged + if (Legacy.isInitializated()) { + Legacy.shims.breadcrumbs.update(); + } + this.syncExternalState(externalState); - rootScope.$applyAsync(); + rootScope.$applyAsync?.(); }); this.syncQueryStateWithUrlManager = syncQueryStateWithUrl(queryService, this.stateStorage); @@ -114,7 +119,7 @@ export class GlobalState { this.startHashSync(rootScope, ngLocation); this.lastAssignedState = this.getState(); - rootScope.$on('$destroy', () => this.destroy()); + rootScope.$on?.('$destroy', () => this.destroy()); } private syncExternalState(externalState: { [key: string]: unknown }) { @@ -131,15 +136,18 @@ export class GlobalState { } } - private startHashSync(rootScope: ng.IRootScopeService, ngLocation: ng.ILocationService) { - rootScope.$on( + private startHashSync( + rootScope: Partial, + ngLocation: Partial + ) { + rootScope.$on?.( '$routeChangeStart', (_: { preventDefault: () => void }, newState: Route, oldState: Route) => { const currentGlobalState = oldState?.params?._g; const nextGlobalState = newState?.params?._g; if (!nextGlobalState && currentGlobalState && typeof currentGlobalState === 'string') { newState.params._g = currentGlobalState; - ngLocation.search(`${GLOBAL_STATE_KEY}=${currentGlobalState}`).replace(); + ngLocation.search?.(`${GLOBAL_STATE_KEY}=${currentGlobalState}`).replace(); } this.lastKnownGlobalState = (nextGlobalState || currentGlobalState) as string; }