From 9c922a078c79390692a8787ba60d15e6fb02269b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ester=20Mart=C3=AD=20Vilaseca?= Date: Thu, 26 Aug 2021 11:03:21 +0200 Subject: [PATCH] [Stack monitoring] Add global state context and routeInit component (#109790) * Add global state to stack monitoring react app * Add type for state * Add some todos * Add route_init migration Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../application/global_state_context.tsx | 66 +++++++++++++++++ .../public/application/hooks/use_clusters.ts | 5 +- .../monitoring/public/application/index.tsx | 51 +++++++++---- .../public/application/pages/loading_page.tsx | 2 +- .../application/preserve_query_history.ts | 38 ++++++++++ .../public/application/route_init.tsx | 71 +++++++++++++++++++ .../plugins/monitoring/public/legacy_shims.ts | 4 ++ .../public/lib/get_cluster_from_clusters.d.ts | 12 ++++ x-pack/plugins/monitoring/public/url_state.ts | 27 ++++--- 9 files changed, 249 insertions(+), 27 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 create mode 100644 x-pack/plugins/monitoring/public/application/route_init.tsx create mode 100644 x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.d.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 000000000000..e6e18e279bba --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/global_state_context.tsx @@ -0,0 +1,66 @@ +/* + * 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'; + +interface GlobalStateProviderProps { + query: MonitoringStartPluginDependencies['data']['query']; + toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts']; + children: React.ReactNode; +} + +interface State { + cluster_uuid?: string; +} + +export const GlobalStateContext = createContext({} as State); + +export const GlobalStateProvider = ({ query, toasts, children }: GlobalStateProviderProps) => { + // TODO: remove fakeAngularRootScope and fakeAngularLocation when angular is removed + const fakeAngularRootScope: 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, + fakeAngularRootScope, + 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/hooks/use_clusters.ts b/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts index 49f6464b2ce3..b970d8c84b5b 100644 --- a/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts +++ b/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts @@ -8,8 +8,7 @@ import { useState, useEffect } from 'react'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants'; -export function useClusters(codePaths?: string[], fetchAllClusters?: boolean, ccs?: any) { - const clusterUuid = fetchAllClusters ? null : ''; +export function useClusters(clusterUuid?: string | null, ccs?: any, codePaths?: string[]) { const { services } = useKibana<{ data: any }>(); const bounds = services.data?.query.timefilter.timefilter.getBounds(); @@ -43,7 +42,7 @@ export function useClusters(codePaths?: string[], fetchAllClusters?: boolean, cc } catch (err) { // TODO: handle errors } finally { - setLoaded(null); + setLoaded(true); } }; diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx index a0c9afd73f0c..ed74d342f7a8 100644 --- a/x-pack/plugins/monitoring/public/application/index.tsx +++ b/x-pack/plugins/monitoring/public/application/index.tsx @@ -8,10 +8,13 @@ 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'; +import { RouteInit } from './route_init'; export const renderApp = ( core: CoreStart, @@ -29,21 +32,37 @@ const MonitoringApp: React.FC<{ core: CoreStart; plugins: MonitoringStartPluginDependencies; }> = ({ core, plugins }) => { + const history = createPreserveQueryHistory(); + return ( - - - - - - - - - + + + + + + + + + + + + ); }; @@ -59,3 +78,7 @@ const Home: React.FC<{}> = () => { const ClusterOverview: React.FC<{}> = () => { return
Cluster overview page
; }; + +const License: React.FC<{}> = () => { + return
License page
; +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/loading_page.tsx b/x-pack/plugins/monitoring/public/application/pages/loading_page.tsx index 4bd09f73ac75..d5c1bcf80c23 100644 --- a/x-pack/plugins/monitoring/public/application/pages/loading_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/loading_page.tsx @@ -16,7 +16,7 @@ import { CODE_PATH_ELASTICSEARCH } from '../../../common/constants'; const CODE_PATHS = [CODE_PATH_ELASTICSEARCH]; export const LoadingPage = () => { - const { clusters, loaded } = useClusters(CODE_PATHS, true); + const { clusters, loaded } = useClusters(null, undefined, CODE_PATHS); const title = i18n.translate('xpack.monitoring.loading.pageTitle', { defaultMessage: 'Loading', }); 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 000000000000..9e7858cf6e84 --- /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/application/route_init.tsx b/x-pack/plugins/monitoring/public/application/route_init.tsx new file mode 100644 index 000000000000..cf3b0c6646d0 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/route_init.tsx @@ -0,0 +1,71 @@ +/* + * 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, { useContext } from 'react'; +import { Route, Redirect, useLocation } from 'react-router-dom'; +import { useClusters } from './hooks/use_clusters'; +import { GlobalStateContext } from './global_state_context'; +import { getClusterFromClusters } from '../lib/get_cluster_from_clusters'; + +interface RouteInitProps { + path: string; + component: React.ComponentType; + codePaths: string[]; + fetchAllClusters: boolean; + unsetGlobalState?: boolean; +} + +export const RouteInit: React.FC = ({ + path, + component, + codePaths, + fetchAllClusters, + unsetGlobalState = false, +}) => { + const globalState = useContext(GlobalStateContext); + const clusterUuid = fetchAllClusters ? null : globalState.cluster_uuid; + const location = useLocation(); + + const { clusters, loaded } = useClusters(clusterUuid, undefined, codePaths); + + // TODO: we will need this when setup mode is migrated + // const inSetupMode = isInSetupMode(); + + const cluster = getClusterFromClusters(clusters, globalState, unsetGlobalState); + + // TODO: check for setupMode too when the setup mode is migrated + if (loaded && !cluster) { + return ; + } + + if (loaded && cluster) { + // check if we need to redirect because of license problems + if ( + location.pathname !== 'license' && + location.pathname !== 'home' && + isExpired(cluster.license) + ) { + return ; + } + + // check if we need to redirect because of attempt at unsupported multi-cluster monitoring + const clusterSupported = cluster.isSupported || clusters.length === 1; + if (location.pathname !== 'home' && !clusterSupported) { + return ; + } + } + + return loaded ? : null; +}; + +const isExpired = (license: any): boolean => { + const { expiry_date_in_millis: expiryDateInMillis } = license; + + if (expiryDateInMillis !== undefined) { + return new Date().getTime() >= expiryDateInMillis; + } + return false; +}; diff --git a/x-pack/plugins/monitoring/public/legacy_shims.ts b/x-pack/plugins/monitoring/public/legacy_shims.ts index 1d897b710d7f..fe754a965e3f 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/lib/get_cluster_from_clusters.d.ts b/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.d.ts new file mode 100644 index 000000000000..5a310c977efa --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.d.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +export const getClusterFromClusters: ( + clusters: any, + globalState: State, + unsetGlobalState: boolean +) => any; diff --git a/x-pack/plugins/monitoring/public/url_state.ts b/x-pack/plugins/monitoring/public/url_state.ts index f490654d579a..25086411c65a 100644 --- a/x-pack/plugins/monitoring/public/url_state.ts +++ b/x-pack/plugins/monitoring/public/url_state.ts @@ -57,6 +57,7 @@ export interface MonitoringAppStateTransitions { const GLOBAL_STATE_KEY = '_g'; const objectEquals = (objA: any, objB: any) => JSON.stringify(objA) === JSON.stringify(objB); +// TODO: clean all angular references after angular is removed export class GlobalState { private readonly stateSyncRef: ISyncStateRef; private readonly stateContainer: StateContainer< @@ -74,8 +75,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 +103,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 +120,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 +137,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; }