Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] [Stack monitoring] Add global state context and routeInit component (#109790) #110181

Merged
merged 1 commit into from
Aug 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -43,7 +42,7 @@ export function useClusters(codePaths?: string[], fetchAllClusters?: boolean, cc
} catch (err) {
// TODO: handle errors
} finally {
setLoaded(null);
setLoaded(true);
}
};

Expand Down
51 changes: 37 additions & 14 deletions x-pack/plugins/monitoring/public/application/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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>
);
};
Expand All @@ -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>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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 changes: 71 additions & 0 deletions x-pack/plugins/monitoring/public/application/route_init.tsx
Original file line number Diff line number Diff line change
@@ -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;
};
4 changes: 4 additions & 0 deletions x-pack/plugins/monitoring/public/legacy_shims.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,8 @@ export class Legacy {
}
return Legacy._shims;
}

public static isInitializated(): boolean {
return Boolean(Legacy._shims);
}
}
Original file line number Diff line number Diff line change
@@ -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;
27 changes: 18 additions & 9 deletions x-pack/plugins/monitoring/public/url_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand All @@ -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;
Expand All @@ -102,19 +103,24 @@ 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);
this.stateSyncRef.start();
this.startHashSync(rootScope, ngLocation);
this.lastAssignedState = this.getState();

rootScope.$on('$destroy', () => this.destroy());
rootScope.$on?.('$destroy', () => this.destroy());
}

private syncExternalState(externalState: { [key: string]: unknown }) {
Expand All @@ -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;
}
Expand Down