Skip to content

Commit 938771a

Browse files
authored
[Logs + Metrics UI] Clean up async plugin initialization (#67654)
This refactors the browser-side plugin bootstrap code such that the eagerly loaded bundle `infra.plugin.js` is minimal and the rest of the logs and metrics app bundles are loaded only when the apps are visited.
1 parent 2e35786 commit 938771a

36 files changed

+528
-780
lines changed

x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,3 +384,7 @@ export const Expressions: React.FC<Props> = (props) => {
384384
</>
385385
);
386386
};
387+
388+
// required for dynamic import
389+
// eslint-disable-next-line import/no-default-export
390+
export default Expressions;

x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
*/
66

77
import { i18n } from '@kbn/i18n';
8-
import { isNumber } from 'lodash';
98
import {
109
MetricExpressionParams,
1110
Comparator,
@@ -106,3 +105,5 @@ export function validateMetricThreshold({
106105

107106
return validationResult;
108107
}
108+
109+
const isNumber = (value: unknown): value is number => typeof value === 'number';

x-pack/plugins/infra/public/alerting/metric_threshold/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66
import { i18n } from '@kbn/i18n';
7+
import React from 'react';
78
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
89
import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types';
9-
import { Expressions } from './components/expression';
1010
import { validateMetricThreshold } from './components/validation';
1111
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
1212
import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../server/lib/alerting/metric_threshold/types';
@@ -18,7 +18,7 @@ export function createMetricThresholdAlertType(): AlertTypeModel {
1818
defaultMessage: 'Metric threshold',
1919
}),
2020
iconClass: 'bell',
21-
alertParamsExpression: Expressions,
21+
alertParamsExpression: React.lazy(() => import('./components/expression')),
2222
validate: validateMetricThreshold,
2323
defaultActionMessage: i18n.translate(
2424
'xpack.infra.metrics.alerting.threshold.defaultActionMessage',
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React from 'react';
8+
import { CoreStart } from 'kibana/public';
9+
import { ApolloClient } from 'apollo-client';
10+
import {
11+
useUiSetting$,
12+
KibanaContextProvider,
13+
} from '../../../../../src/plugins/kibana_react/public';
14+
import { TriggersActionsProvider } from '../utils/triggers_actions_context';
15+
import { ClientPluginDeps } from '../types';
16+
import { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions_ui/public';
17+
import { ApolloClientContext } from '../utils/apollo_context';
18+
import { EuiThemeProvider } from '../../../observability/public';
19+
import { NavigationWarningPromptProvider } from '../utils/navigation_warning_prompt';
20+
21+
export const CommonInfraProviders: React.FC<{
22+
apolloClient: ApolloClient<{}>;
23+
triggersActionsUI: TriggersAndActionsUIPublicPluginStart;
24+
}> = ({ apolloClient, children, triggersActionsUI }) => {
25+
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
26+
27+
return (
28+
<TriggersActionsProvider triggersActionsUI={triggersActionsUI}>
29+
<ApolloClientContext.Provider value={apolloClient}>
30+
<EuiThemeProvider darkMode={darkMode}>
31+
<NavigationWarningPromptProvider>{children}</NavigationWarningPromptProvider>
32+
</EuiThemeProvider>
33+
</ApolloClientContext.Provider>
34+
</TriggersActionsProvider>
35+
);
36+
};
37+
38+
export const CoreProviders: React.FC<{
39+
core: CoreStart;
40+
plugins: ClientPluginDeps;
41+
}> = ({ children, core, plugins }) => {
42+
return (
43+
<KibanaContextProvider services={{ ...core, ...plugins }}>
44+
<core.i18n.Context>{children}</core.i18n.Context>
45+
</KibanaContextProvider>
46+
);
47+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
export const CONTAINER_CLASSNAME = 'infra-container-element';
8+
9+
export const prepareMountElement = (element: HTMLElement) => {
10+
// Ensure the element we're handed from application mounting is assigned a class
11+
// for our index.scss styles to apply to.
12+
element.classList.add(CONTAINER_CLASSNAME);
13+
};
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { EuiErrorBoundary } from '@elastic/eui';
8+
import { createBrowserHistory, History } from 'history';
9+
import { AppMountParameters } from 'kibana/public';
10+
import React from 'react';
11+
import ReactDOM from 'react-dom';
12+
import { Route, RouteProps, Router, Switch } from 'react-router-dom';
13+
import url from 'url';
14+
15+
// This exists purely to facilitate legacy app/infra URL redirects.
16+
// It will be removed in 8.0.0.
17+
export async function renderApp({ element }: AppMountParameters) {
18+
const history = createBrowserHistory();
19+
20+
ReactDOM.render(<LegacyApp history={history} />, element);
21+
22+
return () => {
23+
ReactDOM.unmountComponentAtNode(element);
24+
};
25+
}
26+
27+
const LegacyApp: React.FunctionComponent<{ history: History<unknown> }> = ({ history }) => {
28+
return (
29+
<EuiErrorBoundary>
30+
<Router history={history}>
31+
<Switch>
32+
<Route
33+
path={'/'}
34+
render={({ location }: RouteProps) => {
35+
if (!location) {
36+
return null;
37+
}
38+
39+
let nextPath = '';
40+
let nextBasePath = '';
41+
let nextSearch;
42+
43+
if (
44+
location.hash.indexOf('#infrastructure') > -1 ||
45+
location.hash.indexOf('#/infrastructure') > -1
46+
) {
47+
nextPath = location.hash.replace(
48+
new RegExp(
49+
'#infrastructure/|#/infrastructure/|#/infrastructure|#infrastructure',
50+
'g'
51+
),
52+
''
53+
);
54+
nextBasePath = location.pathname.replace('app/infra', 'app/metrics');
55+
} else if (
56+
location.hash.indexOf('#logs') > -1 ||
57+
location.hash.indexOf('#/logs') > -1
58+
) {
59+
nextPath = location.hash.replace(
60+
new RegExp('#logs/|#/logs/|#/logs|#logs', 'g'),
61+
''
62+
);
63+
nextBasePath = location.pathname.replace('app/infra', 'app/logs');
64+
} else {
65+
// This covers /app/infra and /app/infra/home (both of which used to render
66+
// the metrics inventory page)
67+
nextPath = 'inventory';
68+
nextBasePath = location.pathname.replace('app/infra', 'app/metrics');
69+
nextSearch = undefined;
70+
}
71+
72+
// app/infra#infrastructure/metrics/:type/:node was changed to app/metrics/detail/:type/:node, this
73+
// accounts for that edge case
74+
nextPath = nextPath.replace('metrics/', 'detail/');
75+
76+
// Query parameters (location.search) will arrive as part of location.hash and not location.search
77+
const nextPathParts = nextPath.split('?');
78+
nextPath = nextPathParts[0];
79+
nextSearch = nextPathParts[1] ? nextPathParts[1] : undefined;
80+
81+
let nextUrl = url.format({
82+
pathname: `${nextBasePath}/${nextPath}`,
83+
hash: undefined,
84+
search: nextSearch,
85+
});
86+
87+
nextUrl = nextUrl.replace('//', '/');
88+
89+
window.location.href = nextUrl;
90+
91+
return null;
92+
}}
93+
/>
94+
</Switch>
95+
</Router>
96+
</EuiErrorBoundary>
97+
);
98+
};
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { ApolloClient } from 'apollo-client';
8+
import { History } from 'history';
9+
import { CoreStart } from 'kibana/public';
10+
import React from 'react';
11+
import ReactDOM from 'react-dom';
12+
import { Route, Router, Switch } from 'react-router-dom';
13+
import { AppMountParameters } from '../../../../../src/core/public';
14+
import '../index.scss';
15+
import { NotFoundPage } from '../pages/404';
16+
import { LinkToLogsPage } from '../pages/link_to/link_to_logs';
17+
import { LogsPage } from '../pages/logs';
18+
import { ClientPluginDeps } from '../types';
19+
import { createApolloClient } from '../utils/apollo_client';
20+
import { CommonInfraProviders, CoreProviders } from './common_providers';
21+
import { prepareMountElement } from './common_styles';
22+
23+
export const renderApp = (
24+
core: CoreStart,
25+
plugins: ClientPluginDeps,
26+
{ element, history }: AppMountParameters
27+
) => {
28+
const apolloClient = createApolloClient(core.http.fetch);
29+
30+
prepareMountElement(element);
31+
32+
ReactDOM.render(
33+
<LogsApp apolloClient={apolloClient} core={core} history={history} plugins={plugins} />,
34+
element
35+
);
36+
37+
return () => {
38+
ReactDOM.unmountComponentAtNode(element);
39+
};
40+
};
41+
42+
const LogsApp: React.FC<{
43+
apolloClient: ApolloClient<{}>;
44+
core: CoreStart;
45+
history: History<unknown>;
46+
plugins: ClientPluginDeps;
47+
}> = ({ apolloClient, core, history, plugins }) => {
48+
const uiCapabilities = core.application.capabilities;
49+
50+
return (
51+
<CoreProviders core={core} plugins={plugins}>
52+
<CommonInfraProviders
53+
apolloClient={apolloClient}
54+
triggersActionsUI={plugins.triggers_actions_ui}
55+
>
56+
<Router history={history}>
57+
<Switch>
58+
<Route path="/link-to" component={LinkToLogsPage} />
59+
{uiCapabilities?.logs?.show && <Route path="/" component={LogsPage} />}
60+
<Route component={NotFoundPage} />
61+
</Switch>
62+
</Router>
63+
</CommonInfraProviders>
64+
</CoreProviders>
65+
);
66+
};
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { ApolloClient } from 'apollo-client';
8+
import { History } from 'history';
9+
import { CoreStart } from 'kibana/public';
10+
import React from 'react';
11+
import ReactDOM from 'react-dom';
12+
import { Route, Router, Switch } from 'react-router-dom';
13+
import { AppMountParameters } from '../../../../../src/core/public';
14+
import '../index.scss';
15+
import { NotFoundPage } from '../pages/404';
16+
import { LinkToMetricsPage } from '../pages/link_to/link_to_metrics';
17+
import { InfrastructurePage } from '../pages/metrics';
18+
import { MetricDetail } from '../pages/metrics/metric_detail';
19+
import { ClientPluginDeps } from '../types';
20+
import { createApolloClient } from '../utils/apollo_client';
21+
import { RedirectWithQueryParams } from '../utils/redirect_with_query_params';
22+
import { CommonInfraProviders, CoreProviders } from './common_providers';
23+
import { prepareMountElement } from './common_styles';
24+
25+
export const renderApp = (
26+
core: CoreStart,
27+
plugins: ClientPluginDeps,
28+
{ element, history }: AppMountParameters
29+
) => {
30+
const apolloClient = createApolloClient(core.http.fetch);
31+
32+
prepareMountElement(element);
33+
34+
ReactDOM.render(
35+
<MetricsApp apolloClient={apolloClient} core={core} history={history} plugins={plugins} />,
36+
element
37+
);
38+
39+
return () => {
40+
ReactDOM.unmountComponentAtNode(element);
41+
};
42+
};
43+
44+
const MetricsApp: React.FC<{
45+
apolloClient: ApolloClient<{}>;
46+
core: CoreStart;
47+
history: History<unknown>;
48+
plugins: ClientPluginDeps;
49+
}> = ({ apolloClient, core, history, plugins }) => {
50+
const uiCapabilities = core.application.capabilities;
51+
52+
return (
53+
<CoreProviders core={core} plugins={plugins}>
54+
<CommonInfraProviders
55+
apolloClient={apolloClient}
56+
triggersActionsUI={plugins.triggers_actions_ui}
57+
>
58+
<Router history={history}>
59+
<Switch>
60+
<Route path="/link-to" component={LinkToMetricsPage} />
61+
{uiCapabilities?.infrastructure?.show && (
62+
<RedirectWithQueryParams from="/" exact={true} to="/inventory" />
63+
)}
64+
{uiCapabilities?.infrastructure?.show && (
65+
<RedirectWithQueryParams from="/snapshot" exact={true} to="/inventory" />
66+
)}
67+
{uiCapabilities?.infrastructure?.show && (
68+
<RedirectWithQueryParams from="/metrics-explorer" exact={true} to="/explorer" />
69+
)}
70+
{uiCapabilities?.infrastructure?.show && (
71+
<Route path="/detail/:type/:node" component={MetricDetail} />
72+
)}
73+
{uiCapabilities?.infrastructure?.show && (
74+
<Route path="/" component={InfrastructurePage} />
75+
)}
76+
<Route component={NotFoundPage} />
77+
</Switch>
78+
</Router>
79+
</CommonInfraProviders>
80+
</CoreProviders>
81+
);
82+
};

0 commit comments

Comments
 (0)