mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 03:01:21 -04:00
[Lens] Add loading indicator during debounce time (#80158)
This commit is contained in:
parent
667ff6cd2c
commit
59662eefd2
7 changed files with 79 additions and 18 deletions
|
@ -7,5 +7,5 @@
|
||||||
<b>Signature:</b>
|
<b>Signature:</b>
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
ReactExpressionRenderer: ({ className, dataAttrs, padding, renderError, expression, onEvent, reload$, ...expressionLoaderOptions }: ReactExpressionRendererProps) => JSX.Element
|
ReactExpressionRenderer: ({ className, dataAttrs, padding, renderError, expression, onEvent, reload$, debounce, ...expressionLoaderOptions }: ReactExpressionRendererProps) => JSX.Element
|
||||||
```
|
```
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||||
|
|
||||||
|
[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ReactExpressionRendererProps](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md) > [debounce](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.debounce.md)
|
||||||
|
|
||||||
|
## ReactExpressionRendererProps.debounce property
|
||||||
|
|
||||||
|
<b>Signature:</b>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
debounce?: number;
|
||||||
|
```
|
|
@ -16,6 +16,7 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| [className](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.classname.md) | <code>string</code> | |
|
| [className](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.classname.md) | <code>string</code> | |
|
||||||
| [dataAttrs](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.dataattrs.md) | <code>string[]</code> | |
|
| [dataAttrs](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.dataattrs.md) | <code>string[]</code> | |
|
||||||
|
| [debounce](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.debounce.md) | <code>number</code> | |
|
||||||
| [expression](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.expression.md) | <code>string | ExpressionAstExpression</code> | |
|
| [expression](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.expression.md) | <code>string | ExpressionAstExpression</code> | |
|
||||||
| [onEvent](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.onevent.md) | <code>(event: ExpressionRendererEvent) => void</code> | |
|
| [onEvent](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.onevent.md) | <code>(event: ExpressionRendererEvent) => void</code> | |
|
||||||
| [padding](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.padding.md) | <code>'xs' | 's' | 'm' | 'l' | 'xl'</code> | |
|
| [padding](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.padding.md) | <code>'xs' | 's' | 'm' | 'l' | 'xl'</code> | |
|
||||||
|
|
|
@ -1039,7 +1039,7 @@ export interface Range {
|
||||||
// Warning: (ae-missing-release-tag) "ReactExpressionRenderer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
// Warning: (ae-missing-release-tag) "ReactExpressionRenderer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||||
//
|
//
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const ReactExpressionRenderer: ({ className, dataAttrs, padding, renderError, expression, onEvent, reload$, ...expressionLoaderOptions }: ReactExpressionRendererProps) => JSX.Element;
|
export const ReactExpressionRenderer: ({ className, dataAttrs, padding, renderError, expression, onEvent, reload$, debounce, ...expressionLoaderOptions }: ReactExpressionRendererProps) => JSX.Element;
|
||||||
|
|
||||||
// Warning: (ae-missing-release-tag) "ReactExpressionRendererProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
// Warning: (ae-missing-release-tag) "ReactExpressionRendererProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||||
//
|
//
|
||||||
|
@ -1050,6 +1050,8 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
dataAttrs?: string[];
|
dataAttrs?: string[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
debounce?: number;
|
||||||
|
// (undocumented)
|
||||||
expression: string | ExpressionAstExpression;
|
expression: string | ExpressionAstExpression;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
onEvent?: (event: ExpressionRendererEvent) => void;
|
onEvent?: (event: ExpressionRendererEvent) => void;
|
||||||
|
|
|
@ -113,6 +113,39 @@ describe('ExpressionRenderer', () => {
|
||||||
instance.unmount();
|
instance.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('waits for debounce period if specified', () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
const refreshSubject = new Subject();
|
||||||
|
const loaderUpdate = jest.fn();
|
||||||
|
|
||||||
|
(ExpressionLoader as jest.Mock).mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
render$: new Subject(),
|
||||||
|
data$: new Subject(),
|
||||||
|
loading$: new Subject(),
|
||||||
|
update: loaderUpdate,
|
||||||
|
destroy: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const instance = mount(
|
||||||
|
<ReactExpressionRenderer reload$={refreshSubject} expression="" debounce={1000} />
|
||||||
|
);
|
||||||
|
|
||||||
|
instance.setProps({ expression: 'abc' });
|
||||||
|
|
||||||
|
expect(loaderUpdate).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
jest.runAllTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(loaderUpdate).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
instance.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
it('should display a custom error message if the user provides one and then remove it after successful render', () => {
|
it('should display a custom error message if the user provides one and then remove it after successful render', () => {
|
||||||
const dataSubject = new Subject();
|
const dataSubject = new Subject();
|
||||||
const data$ = dataSubject.asObservable().pipe(share());
|
const data$ = dataSubject.asObservable().pipe(share());
|
||||||
|
|
|
@ -45,6 +45,7 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams {
|
||||||
* An observable which can be used to re-run the expression without destroying the component
|
* An observable which can be used to re-run the expression without destroying the component
|
||||||
*/
|
*/
|
||||||
reload$?: Observable<unknown>;
|
reload$?: Observable<unknown>;
|
||||||
|
debounce?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReactExpressionRendererType = React.ComponentType<ReactExpressionRendererProps>;
|
export type ReactExpressionRendererType = React.ComponentType<ReactExpressionRendererProps>;
|
||||||
|
@ -71,6 +72,7 @@ export const ReactExpressionRenderer = ({
|
||||||
expression,
|
expression,
|
||||||
onEvent,
|
onEvent,
|
||||||
reload$,
|
reload$,
|
||||||
|
debounce,
|
||||||
...expressionLoaderOptions
|
...expressionLoaderOptions
|
||||||
}: ReactExpressionRendererProps) => {
|
}: ReactExpressionRendererProps) => {
|
||||||
const mountpoint: React.MutableRefObject<null | HTMLDivElement> = useRef(null);
|
const mountpoint: React.MutableRefObject<null | HTMLDivElement> = useRef(null);
|
||||||
|
@ -85,12 +87,28 @@ export const ReactExpressionRenderer = ({
|
||||||
const errorRenderHandlerRef: React.MutableRefObject<null | IInterpreterRenderHandlers> = useRef(
|
const errorRenderHandlerRef: React.MutableRefObject<null | IInterpreterRenderHandlers> = useRef(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
const [debouncedExpression, setDebouncedExpression] = useState(expression);
|
||||||
|
useEffect(() => {
|
||||||
|
if (debounce === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedExpression(expression);
|
||||||
|
}, debounce);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [expression, debounce]);
|
||||||
|
|
||||||
|
const activeExpression = debounce !== undefined ? debouncedExpression : expression;
|
||||||
|
const waitingForDebounceToComplete = debounce !== undefined && expression !== debouncedExpression;
|
||||||
|
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
// OK to ignore react-hooks/exhaustive-deps because options update is handled by calling .update()
|
// OK to ignore react-hooks/exhaustive-deps because options update is handled by calling .update()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subs: Subscription[] = [];
|
const subs: Subscription[] = [];
|
||||||
expressionLoaderRef.current = new ExpressionLoader(mountpoint.current!, expression, {
|
expressionLoaderRef.current = new ExpressionLoader(mountpoint.current!, activeExpression, {
|
||||||
...expressionLoaderOptions,
|
...expressionLoaderOptions,
|
||||||
// react component wrapper provides different
|
// react component wrapper provides different
|
||||||
// error handling api which is easier to work with from react
|
// error handling api which is easier to work with from react
|
||||||
|
@ -146,21 +164,21 @@ export const ReactExpressionRenderer = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = reload$?.subscribe(() => {
|
const subscription = reload$?.subscribe(() => {
|
||||||
if (expressionLoaderRef.current) {
|
if (expressionLoaderRef.current) {
|
||||||
expressionLoaderRef.current.update(expression, expressionLoaderOptions);
|
expressionLoaderRef.current.update(activeExpression, expressionLoaderOptions);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => subscription?.unsubscribe();
|
return () => subscription?.unsubscribe();
|
||||||
}, [reload$, expression, ...Object.values(expressionLoaderOptions)]);
|
}, [reload$, activeExpression, ...Object.values(expressionLoaderOptions)]);
|
||||||
|
|
||||||
// Re-fetch data automatically when the inputs change
|
// Re-fetch data automatically when the inputs change
|
||||||
useShallowCompareEffect(
|
useShallowCompareEffect(
|
||||||
() => {
|
() => {
|
||||||
if (expressionLoaderRef.current) {
|
if (expressionLoaderRef.current) {
|
||||||
expressionLoaderRef.current.update(expression, expressionLoaderOptions);
|
expressionLoaderRef.current.update(activeExpression, expressionLoaderOptions);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// when expression is changed by reference and when any other loaderOption is changed by reference
|
// when expression is changed by reference and when any other loaderOption is changed by reference
|
||||||
[{ expression, ...expressionLoaderOptions }]
|
[{ activeExpression, ...expressionLoaderOptions }]
|
||||||
);
|
);
|
||||||
|
|
||||||
/* eslint-enable react-hooks/exhaustive-deps */
|
/* eslint-enable react-hooks/exhaustive-deps */
|
||||||
|
@ -188,7 +206,9 @@ export const ReactExpressionRenderer = ({
|
||||||
return (
|
return (
|
||||||
<div {...dataAttrs} className={classes}>
|
<div {...dataAttrs} className={classes}>
|
||||||
{state.isEmpty && <EuiLoadingChart mono size="l" />}
|
{state.isEmpty && <EuiLoadingChart mono size="l" />}
|
||||||
{state.isLoading && <EuiProgress size="xs" color="accent" position="absolute" />}
|
{(state.isLoading || waitingForDebounceToComplete) && (
|
||||||
|
<EuiProgress size="xs" color="accent" position="absolute" />
|
||||||
|
)}
|
||||||
{!state.isLoading &&
|
{!state.isLoading &&
|
||||||
state.error &&
|
state.error &&
|
||||||
renderError &&
|
renderError &&
|
||||||
|
|
|
@ -32,16 +32,11 @@ import {
|
||||||
ReactExpressionRendererType,
|
ReactExpressionRendererType,
|
||||||
} from '../../../../../../src/plugins/expressions/public';
|
} from '../../../../../../src/plugins/expressions/public';
|
||||||
import { prependDatasourceExpression } from './expression_helpers';
|
import { prependDatasourceExpression } from './expression_helpers';
|
||||||
import { debouncedComponent } from '../../debounced_component';
|
|
||||||
import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry';
|
import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry';
|
||||||
import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
|
import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
|
||||||
|
|
||||||
const MAX_SUGGESTIONS_DISPLAYED = 5;
|
const MAX_SUGGESTIONS_DISPLAYED = 5;
|
||||||
|
|
||||||
// TODO: Remove this <any> when upstream fix is merged https://github.com/elastic/eui/issues/2329
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const EuiPanelFixed = EuiPanel as React.ComponentType<any>;
|
|
||||||
|
|
||||||
export interface SuggestionPanelProps {
|
export interface SuggestionPanelProps {
|
||||||
activeDatasourceId: string | null;
|
activeDatasourceId: string | null;
|
||||||
datasourceMap: Record<string, Datasource>;
|
datasourceMap: Record<string, Datasource>;
|
||||||
|
@ -82,6 +77,7 @@ const PreviewRenderer = ({
|
||||||
className="lnsSuggestionPanel__expressionRenderer"
|
className="lnsSuggestionPanel__expressionRenderer"
|
||||||
padding="s"
|
padding="s"
|
||||||
expression={expression}
|
expression={expression}
|
||||||
|
debounce={2000}
|
||||||
renderError={() => {
|
renderError={() => {
|
||||||
return (
|
return (
|
||||||
<div className="lnsSuggestionPanel__suggestionIcon">
|
<div className="lnsSuggestionPanel__suggestionIcon">
|
||||||
|
@ -104,8 +100,6 @@ const PreviewRenderer = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const DebouncedPreviewRenderer = debouncedComponent(PreviewRenderer, 2000);
|
|
||||||
|
|
||||||
const SuggestionPreview = ({
|
const SuggestionPreview = ({
|
||||||
preview,
|
preview,
|
||||||
ExpressionRenderer: ExpressionRendererComponent,
|
ExpressionRenderer: ExpressionRendererComponent,
|
||||||
|
@ -126,7 +120,7 @@ const SuggestionPreview = ({
|
||||||
return (
|
return (
|
||||||
<EuiToolTip content={preview.title}>
|
<EuiToolTip content={preview.title}>
|
||||||
<div data-test-subj={`lnsSuggestion-${camelCase(preview.title)}`}>
|
<div data-test-subj={`lnsSuggestion-${camelCase(preview.title)}`}>
|
||||||
<EuiPanelFixed
|
<EuiPanel
|
||||||
className={classNames('lnsSuggestionPanel__button', {
|
className={classNames('lnsSuggestionPanel__button', {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
'lnsSuggestionPanel__button-isSelected': selected,
|
'lnsSuggestionPanel__button-isSelected': selected,
|
||||||
|
@ -136,7 +130,7 @@ const SuggestionPreview = ({
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
>
|
>
|
||||||
{preview.expression ? (
|
{preview.expression ? (
|
||||||
<DebouncedPreviewRenderer
|
<PreviewRenderer
|
||||||
ExpressionRendererComponent={ExpressionRendererComponent}
|
ExpressionRendererComponent={ExpressionRendererComponent}
|
||||||
expression={toExpression(preview.expression)}
|
expression={toExpression(preview.expression)}
|
||||||
withLabel={Boolean(showTitleAsLabel)}
|
withLabel={Boolean(showTitleAsLabel)}
|
||||||
|
@ -149,7 +143,7 @@ const SuggestionPreview = ({
|
||||||
{showTitleAsLabel && (
|
{showTitleAsLabel && (
|
||||||
<span className="lnsSuggestionPanel__buttonLabel">{preview.title}</span>
|
<span className="lnsSuggestionPanel__buttonLabel">{preview.title}</span>
|
||||||
)}
|
)}
|
||||||
</EuiPanelFixed>
|
</EuiPanel>
|
||||||
</div>
|
</div>
|
||||||
</EuiToolTip>
|
</EuiToolTip>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue