[Alerting] Adds lazy loading to AlertType and Flyout components (#65678)

This PR:
1. Adds support for lazy loading AlertType components and migrates the built-in IndexThreshold components to lazy load.
2. Adds lazy loading of the components contained in the flyout so that only the wrapper component is imported by other plugins and the internal components are loaded when needed.
This commit is contained in:
Gidi Meir Morris 2020-05-12 17:05:46 +01:00 committed by GitHub
parent 25a3fcea52
commit eef9ecefe0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 188 additions and 88 deletions

View file

@ -69,13 +69,13 @@ export function getAlertType(): AlertTypeModel {
id: '.index-threshold',
name: 'Index threshold',
iconClass: 'alert',
alertParamsExpression: IndexThresholdAlertTypeExpression,
alertParamsExpression: lazy(() => import('./index_threshold_expression')),
validate: validateAlertType,
};
}
```
alertParamsExpression form represented as an expression using `EuiExpression` components:
alertParamsExpression should be a lazy loaded React component extending an expression using `EuiExpression` components:
![Index Threshold Alert expression form](https://i.imgur.com/Ysk1ljY.png)
```
@ -171,6 +171,7 @@ export const alertReducer = (state: any, action: AlertReducerAction) => {
```
The Expression component should be lazy loaded which means it'll have to be the default export in `index_threshold_expression.ts`:
```
export const IndexThresholdAlertTypeExpression: React.FunctionComponent<IndexThresholdProps> = ({
@ -224,6 +225,9 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<IndexThr
</Fragment>
);
};
// Export as default in order to support lazy loading
export {IndexThresholdAlertTypeExpression as default};
```
Index Threshold Alert form with validation:
@ -237,7 +241,9 @@ Each alert type should be defined as `AlertTypeModel` object with the these prop
name: string;
iconClass: string;
validate: (alertParams: any) => ValidationResult;
alertParamsExpression: React.FunctionComponent<any>;
alertParamsExpression: React.LazyExoticComponent<
ComponentType<AlertTypeParamsExpressionProps<AlertParamsType, AlertsContextValue>>
>;
defaultActionMessage?: string;
```
|Property|Description|
@ -246,7 +252,7 @@ Each alert type should be defined as `AlertTypeModel` object with the these prop
|name|Name of the alert type that will be displayed on the select card in the UI.|
|iconClass|Icon of the alert type that will be displayed on the select card in the UI.|
|validate|Validation function for the alert params.|
|alertParamsExpression|React functional component for building UI of the current alert type params.|
|alertParamsExpression| A lazy loaded React component for building UI of the current alert type params.|
|defaultActionMessage|Optional property for providing default message for all added actions with `message` property.|
IMPORTANT: The current UI supports a single action group only.
@ -295,8 +301,8 @@ Below is a list of steps that should be done to build and register a new alert t
1. At any suitable place in Kibana, create a file, which will expose an object implementing interface [AlertTypeModel](https://github.com/elastic/kibana/blob/55b7905fb5265b73806006e7265739545d7521d0/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts#L83). Example:
```
import { lazy } from 'react';
import { AlertTypeModel } from '../../../../types';
import { ExampleExpression } from './expression';
import { validateExampleAlertType } from './validation';
export function getAlertType(): AlertTypeModel {
@ -304,7 +310,7 @@ export function getAlertType(): AlertTypeModel {
id: 'example',
name: 'Example Alert Type',
iconClass: 'bell',
alertParamsExpression: ExampleExpression,
alertParamsExpression: lazy(() => import('./expression')),
validate: validateExampleAlertType,
defaultActionMessage: 'Alert [{{ctx.metadata.name}}] has exceeded the threshold',
};
@ -361,6 +367,9 @@ export const ExampleExpression: React.FunctionComponent<ExampleProps> = ({
);
};
// Export as default in order to support lazy loading
export {ExampleExpression as default};
```
This alert type form becomes available, when the card of `Example Alert Type` is selected.
Each expression word here is `EuiExpression` component and implements the basic aggregation, grouping and comparison methods.
@ -1017,7 +1026,7 @@ Below is a list of steps that should be done to build and register a new action
1. At any suitable place in Kibana, create a file, which will expose an object implementing interface [ActionTypeModel]:
```
import React, { Fragment } from 'react';
import React, { Fragment, lazy } from 'react';
import { i18n } from '@kbn/i18n';
import {
ActionTypeModel,

View file

@ -3,8 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { lazy, Suspense } from 'react';
import { Switch, Route, Redirect, HashRouter, RouteComponentProps } from 'react-router-dom';
import React, { lazy } from 'react';
import { Switch, Route, Redirect, HashRouter } from 'react-router-dom';
import {
ChromeStart,
DocLinksStart,
@ -15,7 +15,6 @@ import {
ChromeBreadcrumb,
CoreStart,
} from 'kibana/public';
import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { BASE_PATH, Section, routeToAlertDetails } from './constants';
import { AppContextProvider, useAppDependencies } from './app_context';
import { hasShowAlertsCapability } from './lib/capabilities';
@ -24,6 +23,7 @@ import { TypeRegistry } from './type_registry';
import { ChartsPluginStart } from '../../../../../src/plugins/charts/public';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import { PluginStartContract as AlertingStart } from '../../../alerting/public';
import { suspendedComponentWithProps } from './lib/suspended_component_with_props';
const TriggersActionsUIHome = lazy(async () => import('./home'));
const AlertDetailsRoute = lazy(() =>
@ -68,30 +68,15 @@ export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) =
<Switch>
<Route
path={`${BASE_PATH}/:section(${sectionsRegex})`}
component={suspendedRouteComponent(TriggersActionsUIHome)}
component={suspendedComponentWithProps(TriggersActionsUIHome, 'xl')}
/>
{canShowAlerts && (
<Route path={routeToAlertDetails} component={suspendedRouteComponent(AlertDetailsRoute)} />
<Route
path={routeToAlertDetails}
component={suspendedComponentWithProps(AlertDetailsRoute, 'xl')}
/>
)}
<Redirect from={`${BASE_PATH}`} to={`${BASE_PATH}/${DEFAULT_SECTION}`} />
</Switch>
);
};
function suspendedRouteComponent<T = unknown>(
RouteComponent: React.ComponentType<RouteComponentProps<T>>
) {
return (props: RouteComponentProps<T>) => (
<Suspense
fallback={
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" />
</EuiFlexItem>
</EuiFlexGroup>
}
>
<RouteComponent {...props} />
</Suspense>
);
}

View file

@ -42,6 +42,7 @@ import {
} from '../../../../common';
import { builtInAggregationTypes } from '../../../../common/constants';
import { IndexThresholdAlertParams } from './types';
import { AlertTypeParamsExpressionProps } from '../../../../types';
import { AlertsContextValue } from '../../../context/alerts_context';
import './expression.scss';
@ -66,23 +67,10 @@ const expressionFieldsWithValidation = [
'timeWindowSize',
];
interface IndexThresholdProps {
alertParams: IndexThresholdAlertParams;
alertInterval: string;
setAlertParams: (property: string, value: any) => void;
setAlertProperty: (key: string, value: any) => void;
errors: { [key: string]: string[] };
alertsContext: AlertsContextValue;
}
export const IndexThresholdAlertTypeExpression: React.FunctionComponent<IndexThresholdProps> = ({
alertParams,
alertInterval,
setAlertParams,
setAlertProperty,
errors,
alertsContext,
}) => {
export const IndexThresholdAlertTypeExpression: React.FunctionComponent<AlertTypeParamsExpressionProps<
IndexThresholdAlertParams,
AlertsContextValue
>> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, alertsContext }) => {
const {
index,
timeField,
@ -476,3 +464,6 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<IndexThr
</Fragment>
);
};
// eslint-disable-next-line import/no-default-export
export { IndexThresholdAlertTypeExpression as default };

View file

@ -3,16 +3,19 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { AlertTypeModel } from '../../../../types';
import { IndexThresholdAlertTypeExpression } from './expression';
import { validateExpression } from './validation';
import { lazy } from 'react';
export function getAlertType(): AlertTypeModel {
import { AlertTypeModel } from '../../../../types';
import { validateExpression } from './validation';
import { IndexThresholdAlertParams } from './types';
import { AlertsContextValue } from '../../../context/alerts_context';
export function getAlertType(): AlertTypeModel<IndexThresholdAlertParams, AlertsContextValue> {
return {
id: '.index-threshold',
name: 'Index threshold',
iconClass: 'alert',
alertParamsExpression: IndexThresholdAlertTypeExpression,
alertParamsExpression: lazy(() => import('./expression')),
validate: validateExpression,
};
}

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Suspense } from 'react';
import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiLoadingSpinnerSize } from '@elastic/eui/src/components/loading/loading_spinner';
export function suspendedComponentWithProps<T = unknown>(
ComponentToSuspend: React.ComponentType<T>,
size?: EuiLoadingSpinnerSize
) {
return (props: T) => (
<Suspense
fallback={
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size={size ?? 'm'} />
</EuiFlexItem>
</EuiFlexGroup>
}
>
<ComponentToSuspend {...props} />
</Suspense>
);
}

View file

@ -10,7 +10,7 @@ import { ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ValidationResult, Alert, AlertAction } from '../../../types';
import { ActionForm } from './action_form';
import ActionForm from './action_form';
jest.mock('../../lib/action_connector_api', () => ({
loadAllActions: jest.fn(),
loadActionTypes: jest.fn(),

View file

@ -713,3 +713,6 @@ export const ActionForm = ({
</Fragment>
);
};
// eslint-disable-next-line import/no-default-export
export { ActionForm as default };

View file

@ -6,7 +6,7 @@
import * as React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { ConnectorAddFlyout } from './connector_add_flyout';
import ConnectorAddFlyout from './connector_add_flyout';
import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ValidationResult } from '../../../types';

View file

@ -319,3 +319,6 @@ const UpgradeYourLicenseCallOut = ({ http }: { http: HttpSetup }) => (
</EuiFlexGroup>
</EuiCallOut>
);
// eslint-disable-next-line import/no-default-export
export { ConnectorAddFlyout as default };

View file

@ -9,7 +9,7 @@ import { coreMock } from '../../../../../../../src/core/public/mocks';
import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ValidationResult } from '../../../types';
import { ConnectorEditFlyout } from './connector_edit_flyout';
import ConnectorEditFlyout from './connector_edit_flyout';
import { AppContextProvider } from '../../app_context';
const actionTypeRegistry = actionTypeRegistryMock.create();

View file

@ -254,3 +254,6 @@ export const ConnectorEditFlyout = ({
</EuiFlyout>
);
};
// eslint-disable-next-line import/no-default-export
export { ConnectorEditFlyout as default };

View file

@ -4,6 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { ConnectorAddFlyout } from './connector_add_flyout';
export { ConnectorEditFlyout } from './connector_edit_flyout';
export { ActionForm } from './action_form';
import { lazy } from 'react';
import { suspendedComponentWithProps } from '../../lib/suspended_component_with_props';
export const ConnectorAddFlyout = suspendedComponentWithProps(
lazy(() => import('./connector_add_flyout'))
);
export const ConnectorEditFlyout = suspendedComponentWithProps(
lazy(() => import('./connector_edit_flyout'))
);
export const ActionForm = suspendedComponentWithProps(lazy(() => import('./action_form')));

View file

@ -202,11 +202,12 @@ describe('actions_connectors_list component with items', () => {
expect(wrapper.find('[data-test-subj="preConfiguredTitleMessage"]')).toHaveLength(2);
});
test('if select item for edit should render ConnectorEditFlyout', () => {
wrapper
test('if select item for edit should render ConnectorEditFlyout', async () => {
await wrapper
.find('[data-test-subj="edit1"]')
.first()
.simulate('click');
expect(wrapper.find('ConnectorEditFlyout')).toHaveLength(1);
});
});

View file

@ -22,7 +22,9 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { useAppDependencies } from '../../../app_context';
import { loadAllActions, loadActionTypes, deleteActions } from '../../../lib/action_connector_api';
import { ConnectorAddFlyout, ConnectorEditFlyout } from '../../action_connector_form';
import ConnectorAddFlyout from '../../action_connector_form/connector_add_flyout';
import ConnectorEditFlyout from '../../action_connector_form/connector_edit_flyout';
import { hasDeleteActionsCapability, hasSaveActionsCapability } from '../../../lib/capabilities';
import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation';
import { ActionsConnectorsContextProvider } from '../../../context/actions_connectors_context';

View file

@ -9,7 +9,7 @@ import { act } from 'react-dom/test-utils';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFormLabel } from '@elastic/eui';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { AlertAdd } from './alert_add';
import AlertAdd from './alert_add';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ValidationResult } from '../../../types';
import { AlertsContextProvider, useAlertsContext } from '../../context/alerts_context';

View file

@ -219,3 +219,6 @@ const parseErrors: (errors: IErrorObject) => boolean = errors =>
if (isObject(errorList)) return parseErrors(errorList as IErrorObject);
return errorList.length >= 1;
});
// eslint-disable-next-line import/no-default-export
export { AlertAdd as default };

View file

@ -12,7 +12,7 @@ import { ValidationResult } from '../../../types';
import { AlertsContextProvider } from '../../context/alerts_context';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { ReactWrapper } from 'enzyme';
import { AlertEdit } from './alert_edit';
import AlertEdit from './alert_edit';
import { AppContextProvider } from '../../app_context';
const actionTypeRegistry = actionTypeRegistryMock.create();
const alertTypeRegistry = alertTypeRegistryMock.create();

View file

@ -201,3 +201,6 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => {
</EuiPortal>
);
};
// eslint-disable-next-line import/no-default-export
export { AlertEdit as default };

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, useState, useEffect } from 'react';
import React, { Fragment, useState, useEffect, Suspense } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@ -23,6 +23,7 @@ import {
EuiIconTip,
EuiButtonIcon,
EuiHorizontalRule,
EuiLoadingSpinner,
} from '@elastic/eui';
import { some, filter, map, fold } from 'fp-ts/lib/Option';
import { pipe } from 'fp-ts/lib/pipeable';
@ -36,7 +37,7 @@ import { AlertReducerAction } from './alert_reducer';
import { AlertTypeModel, Alert, IErrorObject, AlertAction, AlertTypeIndex } from '../../../types';
import { getTimeOptions } from '../../../common/lib/get_time_options';
import { useAlertsContext } from '../../context/alerts_context';
import { ActionForm } from '../action_connector_form/action_form';
import { ActionForm } from '../action_connector_form';
export function validateBaseProperties(alertObject: Alert) {
const validationResult = { errors: {} };
@ -222,14 +223,24 @@ export const AlertForm = ({
) : null}
</EuiFlexGroup>
{AlertParamsExpressionComponent ? (
<AlertParamsExpressionComponent
alertParams={alert.params}
alertInterval={`${alertInterval ?? 1}${alertIntervalUnit}`}
errors={errors}
setAlertParams={setAlertParams}
setAlertProperty={setAlertProperty}
alertsContext={alertsContext}
/>
<Suspense
fallback={
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
</EuiFlexGroup>
}
>
<AlertParamsExpressionComponent
alertParams={alert.params}
alertInterval={`${alertInterval ?? 1}${alertIntervalUnit}`}
errors={errors}
setAlertParams={setAlertParams}
setAlertProperty={setAlertProperty}
alertsContext={alertsContext}
/>
</Suspense>
) : null}
{defaultActionGroupId ? (
<ActionForm

View file

@ -1,8 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { AlertAdd } from './alert_add';
export { AlertEdit } from './alert_edit';

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { lazy } from 'react';
import { suspendedComponentWithProps } from '../../lib/suspended_component_with_props';
export const AlertAdd = suspendedComponentWithProps(lazy(() => import('./alert_add')));
export const AlertEdit = suspendedComponentWithProps(lazy(() => import('./alert_edit')));

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { lazy } from 'react';
import { suspendedComponentWithProps } from '../lib/suspended_component_with_props';
export const AlertAdd = suspendedComponentWithProps(lazy(() => import('./alert_form/alert_add')));
export const AlertEdit = suspendedComponentWithProps(lazy(() => import('./alert_form/alert_edit')));
export const ConnectorAddFlyout = suspendedComponentWithProps(
lazy(() => import('./action_connector_form/connector_add_flyout'))
);
export const ConnectorEditFlyout = suspendedComponentWithProps(
lazy(() => import('./action_connector_form/connector_edit_flyout'))
);
export const ActionForm = suspendedComponentWithProps(
lazy(() => import('./action_connector_form/action_form'))
);

View file

@ -20,11 +20,12 @@ import { getTimeUnitLabel } from '../lib/get_time_unit_label';
import { TIME_UNITS } from '../../application/constants';
import { getTimeOptions } from '../lib/get_time_options';
import { ClosablePopoverTitle } from './components';
import { IErrorObject } from '../../types';
interface ForLastExpressionProps {
timeWindowSize?: number;
timeWindowUnit?: string;
errors: { [key: string]: string[] };
errors: IErrorObject;
onChangeWindowSize: (selectedWindowSize: number | undefined) => void;
onChangeWindowUnit: (selectedWindowUnit: string) => void;
popupPosition?:

View file

@ -19,10 +19,11 @@ import {
import { builtInGroupByTypes } from '../constants';
import { GroupByType } from '../types';
import { ClosablePopoverTitle } from './components';
import { IErrorObject } from '../../types';
interface GroupByExpressionProps {
groupBy: string;
errors: { [key: string]: string[] };
errors: IErrorObject;
onChangeSelectedTermSize: (selectedTermSize?: number) => void;
onChangeSelectedTermField: (selectedTermField?: string) => void;
onChangeSelectedGroupBy: (selectedGroupBy?: string) => void;

View file

@ -11,7 +11,13 @@ export { AlertsContextProvider } from './application/context/alerts_context';
export { ActionsConnectorsContextProvider } from './application/context/actions_connectors_context';
export { AlertAdd } from './application/sections/alert_form';
export { ActionForm } from './application/sections/action_connector_form';
export { AlertAction, Alert, AlertTypeModel, ActionType } from './types';
export {
AlertAction,
Alert,
AlertTypeModel,
AlertTypeParamsExpressionProps,
ActionType,
} from './types';
export {
ConnectorAddFlyout,
ConnectorEditFlyout,

View file

@ -110,12 +110,28 @@ export interface AlertTableItem extends Alert {
tagsText: string;
}
export interface AlertTypeModel {
export interface AlertTypeParamsExpressionProps<
AlertParamsType = unknown,
AlertsContextValue = unknown
> {
alertParams: AlertParamsType;
alertInterval: string;
setAlertParams: (property: string, value: any) => void;
setAlertProperty: (key: string, value: any) => void;
errors: IErrorObject;
alertsContext: AlertsContextValue;
}
export interface AlertTypeModel<AlertParamsType = any, AlertsContextValue = any> {
id: string;
name: string | JSX.Element;
iconClass: string;
validate: (alertParams: any) => ValidationResult;
alertParamsExpression: React.FunctionComponent<any>;
validate: (alertParams: AlertParamsType) => ValidationResult;
alertParamsExpression:
| React.FunctionComponent<any>
| React.LazyExoticComponent<
ComponentType<AlertTypeParamsExpressionProps<AlertParamsType, AlertsContextValue>>
>;
defaultActionMessage?: string;
}

View file

@ -63,7 +63,9 @@ export const initMonitorStatusAlertType: AlertTypeInitializer = ({
id: CLIENT_ALERT_TYPES.MONITOR_STATUS,
name: <MonitorStatusTitle />,
iconClass: 'uptimeApp',
alertParamsExpression: params => <AlertMonitorStatus {...params} autocomplete={autocomplete} />,
alertParamsExpression: (params: any) => (
<AlertMonitorStatus {...params} autocomplete={autocomplete} />
),
validate,
defaultActionMessage,
});