Skip to content

Commit

Permalink
[Lens] Add loading indicator during debounce time (#80158) (#82028)
Browse files Browse the repository at this point in the history
  • Loading branch information
flash1293 authored Oct 29, 2020
1 parent 59e96a5 commit 253a6b4
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) &gt; [ReactExpressionRendererProps](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md) &gt; [debounce](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.debounce.md)

## ReactExpressionRendererProps.debounce property

<b>Signature:</b>

```typescript
debounce?: number;
```
Original file line number Diff line number Diff line change
Expand Up @@ -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 &#124; ExpressionAstExpression</code> | |
| [onEvent](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.onevent.md) | <code>(event: ExpressionRendererEvent) =&gt; void</code> | |
| [padding](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.padding.md) | <code>'xs' &#124; 's' &#124; 'm' &#124; 'l' &#124; 'xl'</code> | |
Expand Down
4 changes: 3 additions & 1 deletion src/plugins/expressions/public/public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
//
Expand All @@ -1050,6 +1050,8 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams {
// (undocumented)
dataAttrs?: string[];
// (undocumented)
debounce?: number;
// (undocumented)
expression: string | ExpressionAstExpression;
// (undocumented)
onEvent?: (event: ExpressionRendererEvent) => void;
Expand Down
33 changes: 33 additions & 0 deletions src/plugins/expressions/public/react_expression_renderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
32 changes: 26 additions & 6 deletions src/plugins/expressions/public/react_expression_renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand All @@ -71,6 +72,7 @@ export const ReactExpressionRenderer = ({
expression,
onEvent,
reload$,
debounce,
...expressionLoaderOptions
}: ReactExpressionRendererProps) => {
const mountpoint: React.MutableRefObject<null | HTMLDivElement> = useRef(null);
Expand All @@ -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
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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 &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down Expand Up @@ -82,6 +77,7 @@ const PreviewRenderer = ({
className="lnsSuggestionPanel__expressionRenderer"
padding="s"
expression={expression}
debounce={2000}
renderError={() => {
return (
<div className="lnsSuggestionPanel__suggestionIcon">
Expand All @@ -104,8 +100,6 @@ const PreviewRenderer = ({
);
};

const DebouncedPreviewRenderer = debouncedComponent(PreviewRenderer, 2000);

const SuggestionPreview = ({
preview,
ExpressionRenderer: ExpressionRendererComponent,
Expand All @@ -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,
Expand All @@ -136,7 +130,7 @@ const SuggestionPreview = ({
onClick={onSelect}
>
{preview.expression ? (
<DebouncedPreviewRenderer
<PreviewRenderer
ExpressionRendererComponent={ExpressionRendererComponent}
expression={toExpression(preview.expression)}
withLabel={Boolean(showTitleAsLabel)}
Expand All @@ -149,7 +143,7 @@ const SuggestionPreview = ({
{showTitleAsLabel && (
<span className="lnsSuggestionPanel__buttonLabel">{preview.title}</span>
)}
</EuiPanelFixed>
</EuiPanel>
</div>
</EuiToolTip>
);
Expand Down

0 comments on commit 253a6b4

Please sign in to comment.