[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>
This commit is contained in:
parent
9e04d2c5c7
commit
9c922a078c
|
@ -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<ng.IRootScopeService> = {
|
||||
$on: (
|
||||
name: string,
|
||||
listener: (event: ng.IAngularEvent, ...args: any[]) => any
|
||||
): (() => void) => () => {},
|
||||
$applyAsync: () => {},
|
||||
};
|
||||
|
||||
const fakeAngularLocation: Partial<ng.ILocationService> = {
|
||||
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 <GlobalStateContext.Provider value={localState}>{children}</GlobalStateContext.Provider>;
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<KibanaContextProvider services={{ ...core, ...plugins }}>
|
||||
<HashRouter>
|
||||
<Switch>
|
||||
<Route path="/loading" component={LoadingPage} />
|
||||
<Route path="/no-data" component={NoData} />
|
||||
<Route path="/home" component={Home} />
|
||||
<Route path="/overview" component={ClusterOverview} />
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: '/loading',
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
</HashRouter>
|
||||
<GlobalStateProvider query={plugins.data.query} toasts={core.notifications.toasts}>
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route path="/no-data" component={NoData} />
|
||||
<Route path="/loading" component={LoadingPage} />
|
||||
<RouteInit
|
||||
path="/license"
|
||||
component={License}
|
||||
codePaths={['all']}
|
||||
fetchAllClusters={false}
|
||||
/>
|
||||
<RouteInit path="/home" component={Home} codePaths={['all']} fetchAllClusters={false} />
|
||||
<RouteInit
|
||||
path="/overview"
|
||||
component={ClusterOverview}
|
||||
codePaths={['all']}
|
||||
fetchAllClusters={false}
|
||||
/>
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: '/loading',
|
||||
search: history.location.search,
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
</Router>
|
||||
</GlobalStateProvider>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
};
|
||||
|
@ -59,3 +78,7 @@ const Home: React.FC<{}> = () => {
|
|||
const ClusterOverview: React.FC<{}> = () => {
|
||||
return <div>Cluster overview page</div>;
|
||||
};
|
||||
|
||||
const License: React.FC<{}> = () => {
|
||||
return <div>License page</div>;
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
71
x-pack/plugins/monitoring/public/application/route_init.tsx
Normal file
71
x-pack/plugins/monitoring/public/application/route_init.tsx
Normal file
|
@ -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<RouteInitProps> = ({
|
||||
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 <Redirect to="/no-data" />;
|
||||
}
|
||||
|
||||
if (loaded && cluster) {
|
||||
// check if we need to redirect because of license problems
|
||||
if (
|
||||
location.pathname !== 'license' &&
|
||||
location.pathname !== 'home' &&
|
||||
isExpired(cluster.license)
|
||||
) {
|
||||
return <Redirect to="/license" />;
|
||||
}
|
||||
|
||||
// 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 <Redirect to="/home" />;
|
||||
}
|
||||
}
|
||||
|
||||
return loaded ? <Route path={path} component={component} /> : null;
|
||||
};
|
||||
|
||||
const isExpired = (license: any): boolean => {
|
||||
const { expiry_date_in_millis: expiryDateInMillis } = license;
|
||||
|
||||
if (expiryDateInMillis !== undefined) {
|
||||
return new Date().getTime() >= expiryDateInMillis;
|
||||
}
|
||||
return false;
|
||||
};
|
|
@ -147,4 +147,8 @@ export class Legacy {
|
|||
}
|
||||
return Legacy._shims;
|
||||
}
|
||||
|
||||
public static isInitializated(): boolean {
|
||||
return Boolean(Legacy._shims);
|
||||
}
|
||||
}
|
||||
|
|
12
x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.d.ts
vendored
Normal file
12
x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.d.ts
vendored
Normal file
|
@ -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;
|
|
@ -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<ng.IRootScopeService>,
|
||||
ngLocation: Partial<ng.ILocationService>,
|
||||
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<ng.IRootScopeService>,
|
||||
ngLocation: Partial<ng.ILocationService>
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue