Skip to content

Commit

Permalink
[Stack monitoring] Add global state context and routeInit component (e…
Browse files Browse the repository at this point in the history
…lastic#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 <[email protected]>
  • Loading branch information
estermv and kibanamachine committed Aug 26, 2021
1 parent 2aaad55 commit 4f68e4f
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 27 deletions.
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

0 comments on commit 4f68e4f

Please sign in to comment.