[Shared UX] Reduce React.lazy usage; fix Discover Empty State load (#132758)

* [Shared UX] Reduce React.lazy usage; fix Discover Empty State load

* Addressing feedback

* We can optimize the calls by replacing methods with impls that return already-retrieved values.
This commit is contained in:
Clint Andrew Hall 2022-05-25 01:35:27 -05:00 committed by GitHub
parent 30c4db69b7
commit 537d812a56
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 142 additions and 233 deletions

View file

@ -0,0 +1,9 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { DataViewIllustration } from './data_view_illustration';

View file

@ -1,24 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { EuiPanel } from '@elastic/eui';
import { withSuspense } from '@kbn/shared-ux-utility';
export const LazyDataViewIllustration = React.lazy(() =>
import('./data_view_illustration').then(({ DataViewIllustration }) => ({
default: DataViewIllustration,
}))
);
export const DataViewIllustration = withSuspense(
LazyDataViewIllustration,
<EuiPanel color="subdued" style={{ width: 226, height: 206 }} />
);

View file

@ -9,38 +9,10 @@
import React from 'react';
import { withSuspense } from '@kbn/shared-ux-utility';
export const LazyToolbarButton = React.lazy(() =>
import('./toolbar').then(({ ToolbarButton }) => ({
default: ToolbarButton,
}))
);
/**
* A `ToolbarButton` component that is wrapped by the `withSuspense` HOC. This component can
* be used directly by consumers and will load the `LazyToolbarButton` component lazily with
* a predefined fallback and error boundary.
*/
export const ToolbarButton = withSuspense(LazyToolbarButton);
/**
* An example of the toolbar button and popover
*/
export { AddFromLibraryButton, ToolbarPopover } from './toolbar';
/**
* The Lazily-loaded `IconButtonGroup` component. Consumers should use `React.Suspense` or the
* `withSuspense` HOC to load this component.
*/
export const LazyIconButtonGroup = React.lazy(() =>
import('./toolbar').then(({ IconButtonGroup }) => ({
default: IconButtonGroup,
}))
);
/**
* The IconButtonGroup component that is wrapped by the `withSuspence` HOC.
*/
export const IconButtonGroup = withSuspense(LazyIconButtonGroup);
export { ToolbarButton, IconButtonGroup, AddFromLibraryButton, ToolbarPopover } from './toolbar';
export { KibanaPageTemplateSolutionNav } from './page_template/solution_nav';
export type { KibanaPageTemplateProps } from './page_template';
export { KibanaPageTemplate } from './page_template';
/**
* A `KibanaNoDataPage` component, with service hooks. Consumers should use `React.Suspennse` or the
@ -58,35 +30,3 @@ export const KibanaNoDataPageLazy = React.lazy(() =>
* a predefined fallback and error boundary.
*/
export const KibanaNoDataPage = withSuspense(KibanaNoDataPageLazy);
/**
* The lazily loaded `KibanaPageTemplate` component that is wrapped by the `withSuspense` HOC. Consumers should use
* `React.Suspense` or `withSuspense` HOC to load this component.
*/
export const KibanaPageTemplateLazy = React.lazy(() =>
import('./page_template').then(({ KibanaPageTemplate }) => ({
default: KibanaPageTemplate,
}))
);
/**
* A `KibanaPageTemplate` component that is wrapped by the `withSuspense` HOC. This component can
* be used directly by consumers and will load the `KibanaPageTemplateLazy` component lazily with
* a predefined fallback and error boundary.
*/
export const KibanaPageTemplate = withSuspense(KibanaPageTemplateLazy);
/**
* A `KibanaPageTemplateProps` type.
*/
export type { KibanaPageTemplateProps } from './page_template';
/**
* The lazily loaded `KibanaPageTemplateSolutionNav` component that is wrapped by the `withSuspense` HOC. Consumers should use
* `React.Suspense` or `withSuspense` HOC to load this component.
*/
export const KibanaPageTemplateSolutionNavLazy = React.lazy(() =>
import('./page_template/solution_nav').then(({ KibanaPageTemplateSolutionNav }) => ({
default: KibanaPageTemplateSolutionNav,
}))
);

View file

@ -7,7 +7,7 @@ exports[`NoDataPage render 1`] = `
<EuiText
textAlign="center"
>
<ForwardRef
<KibanaSolutionAvatar
iconType="logoKibana"
name="Analytics"
size="xxl"

View file

@ -237,7 +237,7 @@ exports[`KibanaPageTemplateSolutionNav renders with icon 1`] = `
size="xs"
>
<h2>
<ForwardRef
<KibanaSolutionAvatar
className="kbnPageTemplateSolutionNav__avatar"
iconType="logoElastic"
name="Solution"

View file

@ -6,24 +6,5 @@
* Side Public License, v 1.
*/
import React from 'react';
import { withSuspense } from '@kbn/shared-ux-utility';
export { KibanaSolutionAvatar } from './solution_avatar';
export type { KibanaSolutionAvatarProps } from './solution_avatar';
/**
* The Lazily-loaded `KibanaSolutionAvatar` component. Consumers should use `React.Suspense` or
* the withSuspense` HOC to load this component.
*/
export const KibanaSolutionAvatarLazy = React.lazy(() =>
import('./solution_avatar').then(({ KibanaSolutionAvatar }) => ({
default: KibanaSolutionAvatar,
}))
);
/**
* A `KibanaSolutionAvatar` component that is wrapped by the `withSuspense` HOC. This component can
* be used directly by consumers and will load the `KibanaPageTemplateSolutionNavAvatarLazy` component lazily with
* a predefined fallback and error boundary.
*/
export const KibanaSolutionAvatar = withSuspense(KibanaSolutionAvatarLazy);

View file

@ -1,41 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { withSuspense } from '@kbn/shared-ux-utility';
export { ExitFullScreenButtonKibanaProvider, ExitFullScreenButtonProvider } from './services';
/**
* Lazy-loaded pure component. Must be wrapped in `React.Suspense`.
*/
export const LazyExitFullScreenButtonComponent = React.lazy(() =>
import('./exit_full_screen_button.component').then(({ ExitFullScreenButton }) => ({
default: ExitFullScreenButton,
}))
);
/**
* A pure component that resembles a button to exit full screen mode.
*/
export const ExitFullScreenButtonComponent = withSuspense(LazyExitFullScreenButtonComponent);
/**
* Lazy-loaded connected component. Must be wrapped in `React.Suspense` and a Provider.
*/
export const LazyExitFullScreenButton = React.lazy(() =>
import('./exit_full_screen_button').then(({ ExitFullScreenButton }) => ({
default: ExitFullScreenButton,
}))
);
/**
* A component that can be used to exit full screen mode in Kibana. Requires a Provider for
* relevant services.
*/
export const ExitFullScreenButton = withSuspense(LazyExitFullScreenButton);

View file

@ -0,0 +1,11 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { ExitFullScreenButtonKibanaProvider, ExitFullScreenButtonProvider } from './services';
export { ExitFullScreenButton as ExitFullScreenButtonComponent } from './exit_full_screen_button.component';
export { ExitFullScreenButton } from './exit_full_screen_button';

View file

@ -6,22 +6,5 @@
* Side Public License, v 1.
*/
import React from 'react';
import { withSuspense } from '@kbn/shared-ux-utility';
export { AnalyticsNoDataPageProvider, AnalyticsNoDataPageKibanaProvider } from './services';
/**
* Lazy-loaded connected component. Must be wrapped in `React.Suspense`.
*/
export const LazyAnalyticsNoDataPage = React.lazy(() =>
import('./analytics_no_data_page').then(({ AnalyticsNoDataPage }) => ({
default: AnalyticsNoDataPage,
}))
);
/**
* An entire page that can be displayed when Kibana "has no data", specifically for Analytics.
* Requires a Provider for relevant services.
*/
export const AnalyticsNoDataPage = withSuspense(LazyAnalyticsNoDataPage);
export { AnalyticsNoDataPage } from './analytics_no_data_page';

View file

@ -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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { NoDataViewsPromptKibanaProvider, NoDataViewsPromptProvider } from './services';
export type { NoDataViewsPromptKibanaServices, NoDataViewsPromptServices } from './services';
export { NoDataViewsPrompt } from './no_data_views';
export { NoDataViewsPrompt as NoDataViewsPromptComponent } from './no_data_views.component';

View file

@ -1,47 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { withSuspense } from '@kbn/shared-ux-utility';
export { NoDataViewsPromptKibanaProvider, NoDataViewsPromptProvider } from './services';
export type { NoDataViewsPromptKibanaServices, NoDataViewsPromptServices } from './services';
/**
* The Lazily-loaded `NoDataViewsPrompt` component. Consumers should use `React.Suspennse` or the
* `withSuspense` HOC to load this component.
*/
export const NoDataViewsPromptLazy = React.lazy(() =>
import('./no_data_views').then(({ NoDataViewsPrompt }) => ({
default: NoDataViewsPrompt,
}))
);
/**
* A `NoDataViewsPrompt` component that is wrapped by the `withSuspense` HOC. This component can
* be used directly by consumers and will load the `NoDataViewsPromptLazy` component lazily with
* a predefined fallback and error boundary.
*/
export const NoDataViewsPrompt = withSuspense(NoDataViewsPromptLazy);
/**
* A pure `NoDataViewsPrompt` component, with no services hooks. Consumers should use `React.Suspennse` or the
* `withSuspense` HOC to load this component.
*/
export const NoDataViewsPromptComponentLazy = React.lazy(() =>
import('./no_data_views.component').then(({ NoDataViewsPrompt: Component }) => ({
default: Component,
}))
);
/**
* A pure `NoDataViewsPrompt` component, with no services hooks. The component is wrapped by the `withSuspense` HOC.
* This component can be used directly by consumers and will load the `NoDataViewsComponentLazy` lazily with
* a predefined fallback and error boundary.
*/
export const NoDataViewsPromptComponent = withSuspense(NoDataViewsPromptComponentLazy);

View file

@ -9,11 +9,11 @@
import React from 'react';
import { css } from '@emotion/react';
import { EuiButton, EuiEmptyPrompt, EuiEmptyPromptProps, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton, EuiEmptyPrompt, EuiEmptyPromptProps } from '@elastic/eui';
import { withSuspense } from '@kbn/shared-ux-utility';
import { DataViewIllustration } from './data_view_illustration';
import { DocumentationLink } from './documentation_link';
export interface Props {
@ -87,9 +87,20 @@ export const NoDataViewsPrompt = ({
</p>
);
const icon = <DataViewIllustration />;
const footer = dataViewsDocLink ? <DocumentationLink href={dataViewsDocLink} /> : undefined;
// Load this illustration lazily
const Illustration = withSuspense(
React.lazy(() =>
import('./data_view_illustration').then(({ DataViewIllustration }) => {
return { default: DataViewIllustration };
})
),
<EuiPanel color="subdued" style={{ width: 226, height: 206 }} />
);
const icon = <Illustration />;
return (
<EuiEmptyPrompt
data-test-subj="noDataViewsPrompt"

View file

@ -57,6 +57,8 @@ export function DiscoverMainRoute() {
const [indexPatternList, setIndexPatternList] = useState<Array<SavedObject<DataViewAttributes>>>(
[]
);
const [hasESData, setHasESData] = useState(false);
const [hasUserDataView, setHasUserDataView] = useState(false);
const [showNoDataPage, setShowNoDataPage] = useState<boolean>(false);
const { id } = useParams<DiscoverLandingParams>();
@ -69,23 +71,33 @@ export function DiscoverMainRoute() {
const loadDefaultOrCurrentIndexPattern = useCallback(
async (searchSource: ISearchSource) => {
try {
const hasUserDataView = await data.dataViews.hasData.hasUserDataView().catch(() => false);
const hasEsData = await data.dataViews.hasData.hasESData().catch(() => false);
if (!hasUserDataView || !hasEsData) {
const hasUserDataViewValue = await data.dataViews.hasData
.hasUserDataView()
.catch(() => false);
const hasESDataValue = await data.dataViews.hasData.hasESData().catch(() => false);
setHasUserDataView(hasUserDataViewValue);
setHasESData(hasESDataValue);
if (!hasUserDataViewValue || !hasESDataValue) {
setShowNoDataPage(true);
return;
}
const defaultDataView = await data.dataViews.getDefaultDataView();
if (!defaultDataView) {
setShowNoDataPage(true);
return;
}
const { appStateContainer } = getState({ history, uiSettings: config });
const { index } = appStateContainer.getState();
const ip = await loadIndexPattern(index || '', data.dataViews, config);
const ipList = ip.list as Array<SavedObject<DataViewAttributes>>;
const indexPatternData = await resolveIndexPattern(ip, searchSource, toastNotifications);
const indexPatternData = resolveIndexPattern(ip, searchSource, toastNotifications);
setIndexPatternList(ipList);
@ -189,9 +201,20 @@ export function DiscoverMainRoute() {
if (showNoDataPage) {
const analyticsServices = {
coreStart: core,
dataViews: data.dataViews,
dataViews: {
...data.dataViews,
hasData: {
...data.dataViews.hasData,
// We've already called this, so we can optimize the analytics services to
// use the already-retrieved data to avoid a double-call.
hasESData: () => Promise.resolve(hasESData),
hasUserDataView: () => Promise.resolve(hasUserDataView),
},
},
dataViewEditor,
};
return (
<AnalyticsNoDataPageKibanaProvider {...analyticsServices}>
<AnalyticsNoDataPage onDataViewCreated={onDataViewCreated} />
@ -204,7 +227,7 @@ export function DiscoverMainRoute() {
}
if (!indexPattern || !savedSearch) {
return <LoadingIndicator />;
return <LoadingIndicator type="elastic" />;
}
return <DiscoverMainAppMemoized indexPatternList={indexPatternList} savedSearch={savedSearch} />;

View file

@ -1,13 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Loading indicator renders correctly 1`] = `
exports[`Loading indicator default renders correctly 1`] = `
<LoadingIndicator>
<EuiFlexGroup
alignItems="center"
gutterSize="none"
justifyContent="spaceAround"
>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceAround euiFlexGroup--directionRow euiFlexGroup--responsive"
className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceAround euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<EuiFlexItem
grow={false}
@ -28,3 +29,45 @@ exports[`Loading indicator renders correctly 1`] = `
</EuiFlexGroup>
</LoadingIndicator>
`;
exports[`Loading indicator elastic renders correctly 1`] = `
<LoadingIndicator
type="elastic"
>
<EuiFlexGroup
alignItems="center"
gutterSize="none"
justifyContent="spaceAround"
>
<div
className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceAround euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<EuiFlexItem
grow={false}
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<EuiLoadingElastic
size="xxl"
>
<span
className="euiLoadingElastic euiLoadingElastic--xxLarge"
>
<EuiIcon
size="xxl"
type="logoElastic"
>
<span
data-euiicon-type="logoElastic"
size="xxl"
/>
</EuiIcon>
</span>
</EuiLoadingElastic>
</div>
</EuiFlexItem>
</div>
</EuiFlexGroup>
</LoadingIndicator>
`;

View file

@ -10,8 +10,12 @@ import React from 'react';
import { mount } from 'enzyme';
describe('Loading indicator', () => {
it('renders correctly', () => {
it('default renders correctly', () => {
const component = mount(<LoadingIndicator />);
expect(component).toMatchSnapshot();
});
it('elastic renders correctly', () => {
const component = mount(<LoadingIndicator type="elastic" />);
expect(component).toMatchSnapshot();
});
});

View file

@ -5,14 +5,18 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiLoadingElastic } from '@elastic/eui';
import React from 'react';
export const LoadingIndicator = () => {
interface Props {
type?: 'spinner' | 'elastic';
}
export const LoadingIndicator = ({ type = 'spinner' }: Props) => {
return (
<EuiFlexGroup justifyContent="spaceAround" alignItems="center">
<EuiFlexGroup justifyContent="spaceAround" alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="l" />
{type === 'spinner' ? <EuiLoadingSpinner size="l" /> : <EuiLoadingElastic size="xxl" />}
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -18,7 +18,7 @@ exports[`it renders without crashing 1`] = `
<EuiSpacer
size="xxl"
/>
<ForwardRef
<KibanaSolutionAvatar
name="Elastic"
size="xl"
/>