Skip to content

Commit

Permalink
Add lazy loading of reusable Spaces UI components
Browse files Browse the repository at this point in the history
  • Loading branch information
jportner committed Feb 19, 2021
1 parent ca00290 commit 83015b5
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 138 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,26 @@
import React, { useEffect, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { get } from 'lodash';
import { Query } from '@elastic/eui';
import { EuiLoadingSpinner, Query } from '@elastic/eui';
import { parse } from 'query-string';
import { i18n } from '@kbn/i18n';
import { CoreStart, ChromeBreadcrumb } from 'src/core/public';
import type {
SpacesAvailableStartContract,
SpacesContextProps,
} from 'src/plugins/spaces_oss/public';
import { DataPublicPluginStart } from '../../../data/public';
import { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public';
import type { SpacesAvailableStartContract } from '../../../spaces_oss/public';
import {
ISavedObjectsManagementServiceRegistry,
SavedObjectsManagementActionServiceStart,
SavedObjectsManagementColumnServiceStart,
} from '../services';
import { SavedObjectsTable } from './objects_table';

const EmptyFunctionComponent: React.FC = ({ children }) => <>{children}</>;
const getEmptyFunctionComponent = async () => ({
default: (({ children }) => <>{children}</>) as React.FC,
});

const SavedObjectsTablePage = ({
coreStart,
Expand Down Expand Up @@ -70,42 +75,43 @@ const SavedObjectsTablePage = ({
]);
}, [setBreadcrumbs]);

const ContextWrapper = useMemo(
() => spacesApi?.ui.components.SpacesContext || EmptyFunctionComponent,
[spacesApi]
const ContextWrapper = React.lazy<React.FC<SpacesContextProps>>(
spacesApi ? spacesApi.ui.components.getSpacesContext : getEmptyFunctionComponent
);

return (
<ContextWrapper>
<SavedObjectsTable
initialQuery={initialQuery}
allowedTypes={allowedTypes}
serviceRegistry={serviceRegistry}
actionRegistry={actionRegistry}
columnRegistry={columnRegistry}
taggingApi={taggingApi}
savedObjectsClient={coreStart.savedObjects.client}
indexPatterns={dataStart.indexPatterns}
search={dataStart.search}
http={coreStart.http}
overlays={coreStart.overlays}
notifications={coreStart.notifications}
applications={coreStart.application}
perPageConfig={itemsPerPage}
goInspectObject={(savedObject) => {
const { editUrl } = savedObject.meta;
if (editUrl) {
return coreStart.application.navigateToUrl(
coreStart.http.basePath.prepend(`/app${editUrl}`)
);
}
}}
canGoInApp={(savedObject) => {
const { inAppUrl } = savedObject.meta;
return inAppUrl ? Boolean(get(capabilities, inAppUrl.uiCapabilitiesPath)) : false;
}}
/>
</ContextWrapper>
<React.Suspense fallback={<EuiLoadingSpinner />}>
<ContextWrapper>
<SavedObjectsTable
initialQuery={initialQuery}
allowedTypes={allowedTypes}
serviceRegistry={serviceRegistry}
actionRegistry={actionRegistry}
columnRegistry={columnRegistry}
taggingApi={taggingApi}
savedObjectsClient={coreStart.savedObjects.client}
indexPatterns={dataStart.indexPatterns}
search={dataStart.search}
http={coreStart.http}
overlays={coreStart.overlays}
notifications={coreStart.notifications}
applications={coreStart.application}
perPageConfig={itemsPerPage}
goInspectObject={(savedObject) => {
const { editUrl } = savedObject.meta;
if (editUrl) {
return coreStart.application.navigateToUrl(
coreStart.http.basePath.prepend(`/app${editUrl}`)
);
}
}}
canGoInApp={(savedObject) => {
const { inAppUrl } = savedObject.meta;
return inAppUrl ? Boolean(get(capabilities, inAppUrl.uiCapabilitiesPath)) : false;
}}
/>
</ContextWrapper>
</React.Suspense>
);
};
// eslint-disable-next-line import/no-default-export
Expand Down
8 changes: 4 additions & 4 deletions src/plugins/spaces_oss/public/api.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ type SpacesApiUiComponentMock = jest.Mocked<SpacesApiUiComponent>;

const createApiUiComponentsMock = () => {
const mock: SpacesApiUiComponentMock = {
SpacesContext: jest.fn(),
ShareToSpaceFlyout: jest.fn(),
SpaceList: jest.fn(),
LegacyUrlConflict: jest.fn(),
getSpacesContext: jest.fn(),
getShareToSpaceFlyout: jest.fn(),
getSpaceList: jest.fn(),
getLegacyUrlConflict: jest.fn(),
};

return mock;
Expand Down
17 changes: 12 additions & 5 deletions src/plugins/spaces_oss/public/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,19 @@ export interface SpacesApi {
ui: SpacesApiUi;
}

/**
* Function that returns a promise for a lazy-loadable component.
*
* @public
*/
export type LazyComponentFn<T> = () => Promise<{ default: FunctionComponent<T> }>;

/**
* @public
*/
export interface SpacesApiUi {
/**
* {@link SpacesApiUiComponent | React components} to support the spaces feature.
* Lazy-loadable {@link SpacesApiUiComponent | React components} to support the spaces feature.
*/
components: SpacesApiUiComponent;
/**
Expand Down Expand Up @@ -62,13 +69,13 @@ export interface SpacesApiUiComponent {
/**
* Provides a context that is required to render some Spaces components.
*/
SpacesContext: FunctionComponent<SpacesContextProps>;
getSpacesContext: LazyComponentFn<SpacesContextProps>;
/**
* Displays a flyout to edit the spaces that an object is shared to.
*
* Note: must be rendered inside of a SpacesContext.
*/
ShareToSpaceFlyout: FunctionComponent<ShareToSpaceFlyoutProps>;
getShareToSpaceFlyout: LazyComponentFn<ShareToSpaceFlyoutProps>;
/**
* Displays a corresponding list of spaces for a given list of saved object namespaces. It shows up to five spaces (and an indicator for
* any number of spaces that the user is not authorized to see) by default. If more than five named spaces would be displayed, the extras
Expand All @@ -77,7 +84,7 @@ export interface SpacesApiUiComponent {
*
* Note: must be rendered inside of a SpacesContext.
*/
SpaceList: FunctionComponent<SpaceListProps>;
getSpaceList: LazyComponentFn<SpaceListProps>;
/**
* Displays a callout that needs to be used if a call to `SavedObjectsClient.resolve()` results in an `"conflict"` outcome, which
* indicates that the user has loaded the page which is associated directly with one object (A), *and* with a legacy URL that points to a
Expand All @@ -95,7 +102,7 @@ export interface SpacesApiUiComponent {
*
* New URL path: `#/workpad/workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e/page/1`
*/
LegacyUrlConflict: FunctionComponent<LegacyUrlConflictProps>;
getLegacyUrlConflict: LazyComponentFn<LegacyUrlConflictProps>;
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/plugins/spaces_oss/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export {
} from './types';

export {
LazyComponentFn,
SpacesApi,
SpacesApiUi,
SpacesApiUiComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import React, { FC, useState } from 'react';

import { EuiButtonEmpty } from '@elastic/eui';
import { EuiButtonEmpty, EuiLoadingSpinner } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { ShareToSpaceFlyoutProps } from 'src/plugins/spaces_oss/public';
import {
Expand Down Expand Up @@ -66,7 +66,9 @@ export const JobSpacesList: FC<Props> = ({ spacesApi, spaceIds, jobId, jobType,
});
}

const { SpaceList, ShareToSpaceFlyout } = spacesApi.ui.components;
const LazySpaceList = React.lazy(spacesApi.ui.components.getSpaceList);
const LazyShareToSpaceFlyout = React.lazy(spacesApi.ui.components.getShareToSpaceFlyout);

const shareToSpaceFlyoutProps: ShareToSpaceFlyoutProps = {
savedObjectTarget: {
type: ML_SAVED_OBJECT_TYPE,
Expand All @@ -81,11 +83,11 @@ export const JobSpacesList: FC<Props> = ({ spacesApi, spaceIds, jobId, jobType,
};

return (
<>
<React.Suspense fallback={<EuiLoadingSpinner />}>
<EuiButtonEmpty onClick={() => setShowFlyout(true)} style={{ height: 'auto' }}>
<SpaceList namespaces={spaceIds} displayLimit={0} behaviorContext="outside-space" />
<LazySpaceList namespaces={spaceIds} displayLimit={0} behaviorContext="outside-space" />
</EuiButtonEmpty>
{showFlyout && <ShareToSpaceFlyout {...shareToSpaceFlyoutProps} />}
</>
{showFlyout && <LazyShareToSpaceFlyout {...shareToSpaceFlyoutProps} />}
</React.Suspense>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import {
EuiText,
EuiTitle,
EuiTabbedContentTab,
EuiLoadingSpinner,
} from '@elastic/eui';

import type { SpacesContextProps } from 'src/plugins/spaces_oss/public';
import { PLUGIN_ID } from '../../../../../../common/constants/app';
import { ManagementAppMountParams } from '../../../../../../../../../src/plugins/management/public/';

Expand Down Expand Up @@ -67,7 +69,9 @@ function usePageState<T extends ListingPageUrlState>(
return [pageState, updateState];
}

const EmptyFunctionComponent: React.FC = ({ children }) => <>{children}</>;
const getEmptyFunctionComponent = async () => ({
default: (({ children }) => <>{children}</>) as React.FC,
});

function useTabs(isMlEnabledInSpace: boolean, spacesApi: SpacesPluginStart | undefined): Tab[] {
const [adPageState, updateAdPageState] = usePageState(getDefaultAnomalyDetectionJobsListState());
Expand Down Expand Up @@ -147,6 +151,14 @@ export const JobsListPage: FC<{
check();
}, []);

const ContextWrapper = useMemo(
() =>
React.lazy<React.FC<SpacesContextProps>>(
spacesApi ? spacesApi.ui.components.getSpacesContext : getEmptyFunctionComponent
),
[spacesApi]
);

if (initialized === false) {
return null;
}
Expand Down Expand Up @@ -185,74 +197,75 @@ export const JobsListPage: FC<{
return <AccessDeniedPage />;
}

const ContextWrapper = spacesApi?.ui.components.SpacesContext || EmptyFunctionComponent;

return (
<RedirectAppLinks application={coreStart.application}>
<I18nContext>
<KibanaContextProvider
services={{ ...coreStart, share, mlServices: getMlGlobalServices(coreStart.http) }}
>
<ContextWrapper feature={PLUGIN_ID}>
<Router history={history}>
<EuiPageContent
id="kibanaManagementMLSection"
data-test-subj="mlPageStackManagementJobsList"
>
<EuiTitle size="l">
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<h1>
{i18n.translate('xpack.ml.management.jobsList.jobsListTitle', {
defaultMessage: 'Machine Learning Jobs',
})}
</h1>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
target="_blank"
iconType="help"
iconSide="left"
color="primary"
href={
currentTabId === 'anomaly_detection_jobs'
? anomalyDetectionJobsUrl
: dataFrameAnalyticsUrl
}
>
{currentTabId === 'anomaly_detection_jobs'
? anomalyDetectionDocsLabel
: analyticsDocsLabel}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiTitle>
<EuiSpacer size="s" />
<EuiTitle size="s">
<EuiText color="subdued">
{i18n.translate('xpack.ml.management.jobsList.jobsListTagline', {
defaultMessage: 'View machine learning analytics and anomaly detection jobs.',
})}
</EuiText>
</EuiTitle>
<EuiSpacer size="l" />
<EuiPageContentBody>
{spacesEnabled && (
<>
<EuiButtonEmpty onClick={() => setShowSyncFlyout(true)}>
{i18n.translate('xpack.ml.management.jobsList.syncFlyoutButton', {
defaultMessage: 'Synchronize saved objects',
})}
</EuiButtonEmpty>
{showSyncFlyout && <JobSpacesSyncFlyout onClose={onCloseSyncFlyout} />}
<EuiSpacer size="s" />
</>
)}
{renderTabs()}
</EuiPageContentBody>
</EuiPageContent>
</Router>
</ContextWrapper>
<React.Suspense fallback={<EuiLoadingSpinner />}>
<ContextWrapper feature={PLUGIN_ID}>
<Router history={history}>
<EuiPageContent
id="kibanaManagementMLSection"
data-test-subj="mlPageStackManagementJobsList"
>
<EuiTitle size="l">
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<h1>
{i18n.translate('xpack.ml.management.jobsList.jobsListTitle', {
defaultMessage: 'Machine Learning Jobs',
})}
</h1>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
target="_blank"
iconType="help"
iconSide="left"
color="primary"
href={
currentTabId === 'anomaly_detection_jobs'
? anomalyDetectionJobsUrl
: dataFrameAnalyticsUrl
}
>
{currentTabId === 'anomaly_detection_jobs'
? anomalyDetectionDocsLabel
: analyticsDocsLabel}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiTitle>
<EuiSpacer size="s" />
<EuiTitle size="s">
<EuiText color="subdued">
{i18n.translate('xpack.ml.management.jobsList.jobsListTagline', {
defaultMessage:
'View machine learning analytics and anomaly detection jobs.',
})}
</EuiText>
</EuiTitle>
<EuiSpacer size="l" />
<EuiPageContentBody>
{spacesEnabled && (
<>
<EuiButtonEmpty onClick={() => setShowSyncFlyout(true)}>
{i18n.translate('xpack.ml.management.jobsList.syncFlyoutButton', {
defaultMessage: 'Synchronize saved objects',
})}
</EuiButtonEmpty>
{showSyncFlyout && <JobSpacesSyncFlyout onClose={onCloseSyncFlyout} />}
<EuiSpacer size="s" />
</>
)}
{renderTabs()}
</EuiPageContentBody>
</EuiPageContent>
</Router>
</ContextWrapper>
</React.Suspense>
</KibanaContextProvider>
</I18nContext>
</RedirectAppLinks>
Expand Down
Loading

0 comments on commit 83015b5

Please sign in to comment.