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>
|
||||
|
||||
```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> | |
|
||||
| [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> | |
|
||||
| [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> | |
|
||||
|
|
|
@ -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)
|
||||
//
|
||||
// @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)
|
||||
//
|
||||
|
@ -1050,6 +1050,8 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams {
|
|||
// (undocumented)
|
||||
dataAttrs?: string[];
|
||||
// (undocumented)
|
||||
debounce?: number;
|
||||
// (undocumented)
|
||||
expression: string | ExpressionAstExpression;
|
||||
// (undocumented)
|
||||
onEvent?: (event: ExpressionRendererEvent) => void;
|
||||
|
|
|
@ -113,6 +113,39 @@ describe('ExpressionRenderer', () => {
|
|||
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', () => {
|
||||
const dataSubject = new Subject();
|
||||
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
|
||||
*/
|
||||
reload$?: Observable<unknown>;
|
||||
debounce?: number;
|
||||
}
|
||||
|
||||
export type ReactExpressionRendererType = React.ComponentType<ReactExpressionRendererProps>;
|
||||
|
@ -71,6 +72,7 @@ export const ReactExpressionRenderer = ({
|
|||
expression,
|
||||
onEvent,
|
||||
reload$,
|
||||
debounce,
|
||||
...expressionLoaderOptions
|
||||
}: ReactExpressionRendererProps) => {
|
||||
const mountpoint: React.MutableRefObject<null | HTMLDivElement> = useRef(null);
|
||||
|
@ -85,12 +87,28 @@ export const ReactExpressionRenderer = ({
|
|||
const errorRenderHandlerRef: React.MutableRefObject<null | IInterpreterRenderHandlers> = useRef(
|
||||
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 */
|
||||
// OK to ignore react-hooks/exhaustive-deps because options update is handled by calling .update()
|
||||
useEffect(() => {
|
||||
const subs: Subscription[] = [];
|
||||
expressionLoaderRef.current = new ExpressionLoader(mountpoint.current!, expression, {
|
||||
expressionLoaderRef.current = new ExpressionLoader(mountpoint.current!, activeExpression, {
|
||||
...expressionLoaderOptions,
|
||||
// react component wrapper provides different
|
||||
// error handling api which is easier to work with from react
|
||||
|
@ -146,21 +164,21 @@ export const ReactExpressionRenderer = ({
|
|||
useEffect(() => {
|
||||
const subscription = reload$?.subscribe(() => {
|
||||
if (expressionLoaderRef.current) {
|
||||
expressionLoaderRef.current.update(expression, expressionLoaderOptions);
|
||||
expressionLoaderRef.current.update(activeExpression, expressionLoaderOptions);
|
||||
}
|
||||
});
|
||||
return () => subscription?.unsubscribe();
|
||||
}, [reload$, expression, ...Object.values(expressionLoaderOptions)]);
|
||||
}, [reload$, activeExpression, ...Object.values(expressionLoaderOptions)]);
|
||||
|
||||
// Re-fetch data automatically when the inputs change
|
||||
useShallowCompareEffect(
|
||||
() => {
|
||||
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
|
||||
[{ expression, ...expressionLoaderOptions }]
|
||||
[{ activeExpression, ...expressionLoaderOptions }]
|
||||
);
|
||||
|
||||
/* eslint-enable react-hooks/exhaustive-deps */
|
||||
|
@ -188,7 +206,9 @@ export const ReactExpressionRenderer = ({
|
|||
return (
|
||||
<div {...dataAttrs} className={classes}>
|
||||
{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.error &&
|
||||
renderError &&
|
||||
|
|
|
@ -32,16 +32,11 @@ import {
|
|||
ReactExpressionRendererType,
|
||||
} from '../../../../../../src/plugins/expressions/public';
|
||||
import { prependDatasourceExpression } from './expression_helpers';
|
||||
import { debouncedComponent } from '../../debounced_component';
|
||||
import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry';
|
||||
import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
|
||||
|
||||
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 {
|
||||
activeDatasourceId: string | null;
|
||||
datasourceMap: Record<string, Datasource>;
|
||||
|
@ -82,6 +77,7 @@ const PreviewRenderer = ({
|
|||
className="lnsSuggestionPanel__expressionRenderer"
|
||||
padding="s"
|
||||
expression={expression}
|
||||
debounce={2000}
|
||||
renderError={() => {
|
||||
return (
|
||||
<div className="lnsSuggestionPanel__suggestionIcon">
|
||||
|
@ -104,8 +100,6 @@ const PreviewRenderer = ({
|
|||
);
|
||||
};
|
||||
|
||||
const DebouncedPreviewRenderer = debouncedComponent(PreviewRenderer, 2000);
|
||||
|
||||
const SuggestionPreview = ({
|
||||
preview,
|
||||
ExpressionRenderer: ExpressionRendererComponent,
|
||||
|
@ -126,7 +120,7 @@ const SuggestionPreview = ({
|
|||
return (
|
||||
<EuiToolTip content={preview.title}>
|
||||
<div data-test-subj={`lnsSuggestion-${camelCase(preview.title)}`}>
|
||||
<EuiPanelFixed
|
||||
<EuiPanel
|
||||
className={classNames('lnsSuggestionPanel__button', {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'lnsSuggestionPanel__button-isSelected': selected,
|
||||
|
@ -136,7 +130,7 @@ const SuggestionPreview = ({
|
|||
onClick={onSelect}
|
||||
>
|
||||
{preview.expression ? (
|
||||
<DebouncedPreviewRenderer
|
||||
<PreviewRenderer
|
||||
ExpressionRendererComponent={ExpressionRendererComponent}
|
||||
expression={toExpression(preview.expression)}
|
||||
withLabel={Boolean(showTitleAsLabel)}
|
||||
|
@ -149,7 +143,7 @@ const SuggestionPreview = ({
|
|||
{showTitleAsLabel && (
|
||||
<span className="lnsSuggestionPanel__buttonLabel">{preview.title}</span>
|
||||
)}
|
||||
</EuiPanelFixed>
|
||||
</EuiPanel>
|
||||
</div>
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue