Unify style for embeddable-stack loaders (#171238)

## Summary

Fix https://github.com/elastic/kibana/issues/170428

The bug this is intended to resolve requires some in-depth steps to
reproduce. Follow the instructions in the issue above. Then, merge in
this branch and compare.


### Checklist

- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Drew Tate 2023-11-28 12:30:47 -07:00 committed by GitHub
parent 9f5651d3bf
commit a8647151cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 111 additions and 22 deletions

1
.github/CODEOWNERS vendored
View file

@ -566,6 +566,7 @@ packages/kbn-osquery-io-ts-types @elastic/security-asset-management
x-pack/plugins/osquery @elastic/security-defend-workflows
examples/partial_results_example @elastic/kibana-data-discovery
x-pack/plugins/painless_lab @elastic/platform-deployment-management
packages/kbn-panel-loader @elastic/kibana-presentation
packages/kbn-peggy @elastic/kibana-operations
packages/kbn-peggy-loader @elastic/kibana-operations
packages/kbn-performance-testing-dataset-extractor @elastic/kibana-performance-testing

View file

@ -581,6 +581,7 @@
"@kbn/osquery-plugin": "link:x-pack/plugins/osquery",
"@kbn/paertial-results-example-plugin": "link:examples/partial_results_example",
"@kbn/painless-lab-plugin": "link:x-pack/plugins/painless_lab",
"@kbn/panel-loader": "link:packages/kbn-panel-loader",
"@kbn/portable-dashboards-example": "link:examples/portable_dashboards_example",
"@kbn/preboot-example-plugin": "link:examples/preboot_example",
"@kbn/presentation-util-plugin": "link:src/plugins/presentation_util",

View file

@ -0,0 +1,3 @@
# @kbn/panel-loader
Contains a generic loader which should be used to indicate that a chart is loading

View file

@ -9,14 +9,14 @@
import React from 'react';
import { EuiLoadingChart, EuiPanel } from '@elastic/eui';
export const EmbeddableLoadingIndicator = ({ showShadow }: { showShadow?: boolean }) => {
export const PanelLoader = (props: { showShadow?: boolean; dataTestSubj?: string }) => {
return (
<EuiPanel
role="figure"
paddingSize="none"
hasShadow={showShadow}
hasShadow={props.showShadow ?? false}
className={'embPanel embPanel--loading embPanel-isLoading'}
data-test-subj="embeddablePanelLoadingIndicator"
data-test-subj={props.dataTestSubj}
>
<EuiLoadingChart size="l" mono />
</EuiPanel>

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-panel-loader'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-browser",
"id": "@kbn/panel-loader",
"owner": "@elastic/kibana-presentation"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/panel-loader",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": []
}

View file

@ -108,6 +108,7 @@ export const Item = React.forwardRef<HTMLDivElement, Props>(
key={type}
index={index}
showBadges={true}
showShadow={true}
showNotifications={true}
onPanelStatusChange={onPanelStatusChange}
embeddable={() => container.untilEmbeddableLoaded(id)}

View file

@ -12,9 +12,8 @@ import { distinct, map } from 'rxjs';
import React, { ReactNode, useEffect, useMemo, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, htmlIdGenerator } from '@elastic/eui';
import { isPromise } from '@kbn/std';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { PanelLoader } from '@kbn/panel-loader';
import {
EditPanelAction,
RemovePanelAction,
@ -51,6 +50,7 @@ const getEventStatus = (output: EmbeddableOutput): EmbeddablePhase => {
export const EmbeddablePanel = (panelProps: UnwrappedEmbeddablePanelProps) => {
const { hideHeader, showShadow, embeddable, hideInspector, onPanelStatusChange } = panelProps;
const [node, setNode] = useState<ReactNode | undefined>();
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
const embeddableRoot: React.RefObject<HTMLDivElement> = useMemo(() => React.createRef(), []);
const headerId = useMemo(() => htmlIdGenerator()(), []);
@ -129,6 +129,11 @@ export const EmbeddablePanel = (panelProps: UnwrappedEmbeddablePanelProps) => {
* Select state from the embeddable
*/
const loading = useSelectFromEmbeddableOutput('loading', embeddable);
if (loading === false && !initialLoadComplete) {
setInitialLoadComplete(true);
}
const viewMode = useSelectFromEmbeddableInput('viewMode', embeddable);
/**
@ -136,21 +141,30 @@ export const EmbeddablePanel = (panelProps: UnwrappedEmbeddablePanelProps) => {
*/
useEffect(() => {
if (!embeddableRoot.current) return;
const nextNode = embeddable.render(embeddableRoot.current) ?? undefined;
if (isPromise(nextNode)) {
nextNode.then((resolved) => setNode(resolved));
} else {
let cancelled = false;
const render = async (root: HTMLDivElement) => {
const nextNode = (await embeddable.render(root)) ?? undefined;
if (cancelled) return;
setNode(nextNode);
}
};
render(embeddableRoot.current);
const errorSubscription = embeddable.getOutput$().subscribe({
next: (output) => {
setOutputError(output.error);
},
error: (error) => setOutputError(error),
});
return () => {
embeddable?.destroy();
errorSubscription?.unsubscribe();
cancelled = true;
};
}, [embeddable, embeddableRoot]);
@ -207,7 +221,13 @@ export const EmbeddablePanel = (panelProps: UnwrappedEmbeddablePanelProps) => {
</EuiFlexItem>
</EuiFlexGroup>
)}
<div className="embPanel__content" ref={embeddableRoot} {...contentAttrs}>
{!initialLoadComplete && <PanelLoader />}
<div
css={initialLoadComplete ? undefined : { display: 'none !important' }}
className="embPanel__content"
ref={embeddableRoot}
{...contentAttrs}
>
{node}
</div>
</EuiPanel>

View file

@ -8,16 +8,19 @@
import React from 'react';
import { PanelLoader } from '@kbn/panel-loader';
import { EmbeddablePanelProps } from './types';
import { useEmbeddablePanel } from './use_embeddable_panel';
import { EmbeddableLoadingIndicator } from './embeddable_loading_indicator';
/**
* Loads and renders an embeddable.
*/
export const EmbeddablePanel = (props: EmbeddablePanelProps) => {
const result = useEmbeddablePanel({ embeddable: props.embeddable });
if (!result) return <EmbeddableLoadingIndicator />;
if (!result)
return (
<PanelLoader showShadow={props.showShadow} dataTestSubj="embeddablePanelLoadingIndicator" />
);
const { embeddable, ...passThroughProps } = props;
return <result.Panel embeddable={result.unwrappedEmbeddable} {...passThroughProps} />;
};

View file

@ -34,6 +34,7 @@
"@kbn/react-kibana-mount",
"@kbn/unified-search-plugin",
"@kbn/data-views-plugin",
"@kbn/panel-loader",
],
"exclude": ["target/**/*"]
}

View file

@ -8,7 +8,8 @@
import React, { useRef } from 'react';
import classNames from 'classnames';
import { EuiLoadingChart, EuiProgress, useEuiTheme } from '@elastic/eui';
import { PanelLoader } from '@kbn/panel-loader';
import { EuiProgress, useEuiTheme } from '@elastic/eui';
import { ExpressionRenderError } from '../types';
import type { ExpressionRendererParams } from './use_expression_renderer';
import { useExpressionRenderer } from './use_expression_renderer';
@ -56,7 +57,7 @@ export function ReactExpressionRenderer({
return (
<div {...dataAttrs} className={classes}>
{isEmpty && <EuiLoadingChart mono size="l" />}
{isEmpty && <PanelLoader />}
{isLoading && (
<EuiProgress size="xs" color="accent" position="absolute" style={{ zIndex: 1 }} />
)}

View file

@ -7,7 +7,7 @@
*/
import React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { PanelLoader } from '@kbn/panel-loader';
import type { ReactExpressionRendererProps } from './react_expression_renderer';
const ReactExpressionRendererComponent = lazy(async () => {
@ -17,7 +17,7 @@ const ReactExpressionRendererComponent = lazy(async () => {
});
export const ReactExpressionRenderer = (props: ReactExpressionRendererProps) => (
<Suspense fallback={<EuiLoadingSpinner />}>
<Suspense fallback={<PanelLoader />}>
<ReactExpressionRendererComponent {...props} />
</Suspense>
);

View file

@ -16,6 +16,7 @@
"@kbn/std",
"@kbn/core-execution-context-common",
"@kbn/tinymath",
"@kbn/panel-loader",
],
"exclude": [
"target/**/*",

View file

@ -1126,6 +1126,8 @@
"@kbn/paertial-results-example-plugin/*": ["examples/partial_results_example/*"],
"@kbn/painless-lab-plugin": ["x-pack/plugins/painless_lab"],
"@kbn/painless-lab-plugin/*": ["x-pack/plugins/painless_lab/*"],
"@kbn/panel-loader": ["packages/kbn-panel-loader"],
"@kbn/panel-loader/*": ["packages/kbn-panel-loader/*"],
"@kbn/peggy": ["packages/kbn-peggy"],
"@kbn/peggy/*": ["packages/kbn-peggy/*"],
"@kbn/peggy-loader": ["packages/kbn-peggy-loader"],

View file

@ -5,11 +5,13 @@
* 2.0.
*/
import React, { type FC } from 'react';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiEmptyPrompt } from '@elastic/eui';
export const NoChangePointsWarning: FC = () => {
export const NoChangePointsWarning = (props: { onRenderComplete?: () => void }) => {
props.onRenderComplete?.();
return (
<EuiEmptyPrompt
iconType="search"

View file

@ -243,7 +243,7 @@ export const ChartGridEmbeddableWrapper: FC<
) : emptyState ? (
emptyState
) : (
<NoChangePointsWarning />
<NoChangePointsWarning onRenderComplete={onRenderComplete} />
)}
</div>
);

View file

@ -9,6 +9,7 @@ import React, { FC, useEffect } from 'react';
import type { CoreStart } from '@kbn/core/public';
import type { Action, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { Start as InspectorStartContract } from '@kbn/inspector-plugin/public';
import { PanelLoader } from '@kbn/panel-loader';
import { EuiLoadingChart } from '@elastic/eui';
import {
EmbeddableFactory,
@ -160,7 +161,7 @@ const EmbeddablePanelWrapper: FC<EmbeddablePanelWrapperProps> = ({
}, [embeddable, input]);
if (loading || !embeddable) {
return <EuiLoadingChart />;
return <PanelLoader />;
}
return (

View file

@ -92,12 +92,13 @@
"@kbn/core-plugins-server",
"@kbn/text-based-languages",
"@kbn/field-utils",
"@kbn/panel-loader",
"@kbn/shared-ux-button-toolbar",
"@kbn/cell-actions",
"@kbn/calculate-width-from-char-count",
"@kbn/discover-utils"
],
"exclude": [
"target/**/*",
"target/**/*"
]
}

View file

@ -409,6 +409,10 @@ export const SwimlaneContainer: FC<SwimlaneProps> = ({
const noSwimLaneData = !isLoading && !showSwimlane && !!noDataWarning;
if (noSwimLaneData) {
onRenderComplete?.();
}
// A resize observer is required to compute the bucket span based on the chart width to fetch the data accordingly
return (
<EuiResizeObserver onResize={resizeHandler}>

View file

@ -108,6 +108,7 @@ export const EmbeddableSwimLaneContainer: FC<ExplorerSwimlaneContainerProps> = (
);
if (error) {
onRenderComplete();
return (
<EuiCallOut
title={

View file

@ -5189,6 +5189,10 @@
version "0.0.0"
uid ""
"@kbn/panel-loader@link:packages/kbn-panel-loader":
version "0.0.0"
uid ""
"@kbn/peggy-loader@link:packages/kbn-peggy-loader":
version "0.0.0"
uid ""