Lens editor auto refresh (#65868)

This commit is contained in:
Joe Reuter 2020-06-03 11:35:55 +02:00 committed by GitHub
parent fbcb74ce28
commit 4a1b05c843
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 88 additions and 18 deletions

View file

@ -88,6 +88,31 @@ describe('ExpressionRenderer', () => {
expect(instance.find(EuiProgress)).toHaveLength(0);
});
it('updates the expression loader when refresh subject emits', () => {
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="" />);
act(() => {
refreshSubject.next();
});
expect(loaderUpdate).toHaveBeenCalled();
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());

View file

@ -19,7 +19,7 @@
import React, { useRef, useEffect, useState, useLayoutEffect } from 'react';
import classNames from 'classnames';
import { Subscription } from 'rxjs';
import { Observable, Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect';
import { EuiLoadingChart, EuiProgress } from '@elastic/eui';
@ -38,6 +38,10 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams {
renderError?: (error?: string | null) => React.ReactElement | React.ReactElement[];
padding?: 'xs' | 's' | 'm' | 'l' | 'xl';
onEvent?: (event: ExpressionRendererEvent) => void;
/**
* An observable which can be used to re-run the expression without destroying the component
*/
reload$?: Observable<unknown>;
}
export type ReactExpressionRendererType = React.ComponentType<ReactExpressionRendererProps>;
@ -63,6 +67,7 @@ export const ReactExpressionRenderer = ({
renderError,
expression,
onEvent,
reload$,
...expressionLoaderOptions
}: ReactExpressionRendererProps) => {
const mountpoint: React.MutableRefObject<null | HTMLDivElement> = useRef(null);
@ -135,6 +140,15 @@ export const ReactExpressionRenderer = ({
};
}, [hasCustomRenderErrorHandler, onEvent]);
useEffect(() => {
const subscription = reload$?.subscribe(() => {
if (expressionLoaderRef.current) {
expressionLoaderRef.current.update(expression, expressionLoaderOptions);
}
});
return () => subscription?.unsubscribe();
}, [reload$, expression, ...Object.values(expressionLoaderOptions)]);
// Re-fetch data automatically when the inputs change
useShallowCompareEffect(
() => {

View file

@ -305,6 +305,7 @@ export function EditorFrame(props: EditorFrameProps) {
dispatch={dispatch}
ExpressionRenderer={props.ExpressionRenderer}
stagedPreview={state.stagedPreview}
plugins={props.plugins}
/>
)
}

View file

@ -21,6 +21,7 @@ import { SuggestionPanel, SuggestionPanelProps } from './suggestion_panel';
import { getSuggestions, Suggestion } from './suggestion_helpers';
import { EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui';
import chartTableSVG from '../../..assets/chart_datatable.svg';
import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
jest.mock('./suggestion_helpers');
@ -85,6 +86,7 @@ describe('suggestion_panel', () => {
dispatch: dispatchMock,
ExpressionRenderer: expressionRendererMock,
frame: createMockFramePublicAPI(),
plugins: { data: dataPluginMock.createStartContract() },
};
});

View file

@ -24,10 +24,14 @@ import classNames from 'classnames';
import { Action, PreviewState } from './state_management';
import { Datasource, Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types';
import { getSuggestions, switchToSuggestion } from './suggestion_helpers';
import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public';
import {
ReactExpressionRendererProps,
ReactExpressionRendererType,
} from '../../../../../../src/plugins/expressions/public';
import { prependDatasourceExpression, prependKibanaContext } 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;
@ -52,6 +56,7 @@ export interface SuggestionPanelProps {
ExpressionRenderer: ReactExpressionRendererType;
frame: FramePublicAPI;
stagedPreview?: PreviewState;
plugins: { data: DataPublicPluginStart };
}
const PreviewRenderer = ({
@ -154,6 +159,7 @@ export function SuggestionPanel({
frame,
ExpressionRenderer: ExpressionRendererComponent,
stagedPreview,
plugins,
}: SuggestionPanelProps) {
const currentDatasourceStates = stagedPreview ? stagedPreview.datasourceStates : datasourceStates;
const currentVisualizationState = stagedPreview
@ -204,6 +210,13 @@ export function SuggestionPanel({
visualizationMap,
]);
const AutoRefreshExpressionRenderer = useMemo(() => {
const autoRefreshFetch$ = plugins.data.query.timefilter.timefilter.getAutoRefreshFetch$();
return (props: ReactExpressionRendererProps) => (
<ExpressionRendererComponent {...props} reload$={autoRefreshFetch$} />
);
}, [plugins.data.query.timefilter.timefilter.getAutoRefreshFetch$, ExpressionRendererComponent]);
const [lastSelectedSuggestion, setLastSelectedSuggestion] = useState<number>(-1);
useEffect(() => {
@ -296,7 +309,7 @@ export function SuggestionPanel({
defaultMessage: 'Current',
}),
}}
ExpressionRenderer={ExpressionRendererComponent}
ExpressionRenderer={AutoRefreshExpressionRenderer}
onSelect={rollbackToCurrentVisualization}
selected={lastSelectedSuggestion === -1}
showTitleAsLabel
@ -312,7 +325,7 @@ export function SuggestionPanel({
icon: suggestion.previewIcon,
title: suggestion.title,
}}
ExpressionRenderer={ExpressionRendererComponent}
ExpressionRenderer={AutoRefreshExpressionRenderer}
key={index}
onSelect={() => {
trackUiEvent('suggestion_clicked');

View file

@ -21,11 +21,17 @@ import { ReactWrapper } from 'enzyme';
import { DragDrop, ChildDragDropProvider } from '../../drag_drop';
import { Ast } from '@kbn/interpreter/common';
import { coreMock } from 'src/core/public/mocks';
import { esFilters, IFieldType, IIndexPattern } from '../../../../../../src/plugins/data/public';
import {
DataPublicPluginStart,
esFilters,
IFieldType,
IIndexPattern,
} from '../../../../../../src/plugins/data/public';
import { TriggerId, UiActionsStart } from '../../../../../../src/plugins/ui_actions/public';
import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/public/mocks';
import { TriggerContract } from '../../../../../../src/plugins/ui_actions/public/triggers';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public/embeddable';
import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
describe('workspace_panel', () => {
let mockVisualization: jest.Mocked<Visualization>;
@ -34,6 +40,7 @@ describe('workspace_panel', () => {
let expressionRendererMock: jest.Mock<React.ReactElement, [ReactExpressionRendererProps]>;
let uiActionsMock: jest.Mocked<UiActionsStart>;
let dataMock: jest.Mocked<DataPublicPluginStart>;
let trigger: jest.Mocked<TriggerContract<TriggerId>>;
let instance: ReactWrapper<WorkspacePanelProps>;
@ -41,6 +48,7 @@ describe('workspace_panel', () => {
beforeEach(() => {
trigger = ({ exec: jest.fn() } as unknown) as jest.Mocked<TriggerContract<TriggerId>>;
uiActionsMock = uiActionsPluginMock.createStartContract();
dataMock = dataPluginMock.createStartContract();
uiActionsMock.getTrigger.mockReturnValue(trigger);
mockVisualization = createMockVisualization();
mockVisualization2 = createMockVisualization();
@ -69,7 +77,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
/>
);
@ -92,7 +100,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
/>
);
@ -115,7 +123,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
/>
);
@ -152,7 +160,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
/>
);
@ -240,7 +248,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
/>
);
@ -292,7 +300,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
/>
);
@ -372,7 +380,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
/>
);
});
@ -427,7 +435,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
/>
);
});
@ -482,7 +490,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
/>
);
@ -520,7 +528,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
/>
);
});
@ -564,7 +572,7 @@ describe('workspace_panel', () => {
dispatch={() => {}}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
/>
);
});
@ -620,7 +628,7 @@ describe('workspace_panel', () => {
dispatch={mockDispatch}
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock }}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
/>
</ChildDragDropProvider>
);

View file

@ -36,6 +36,7 @@ import { debouncedComponent } from '../../debounced_component';
import { trackUiEvent } from '../../lens_ui_telemetry';
import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public';
import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
export interface WorkspacePanelProps {
activeVisualizationId: string | null;
@ -54,7 +55,7 @@ export interface WorkspacePanelProps {
dispatch: (action: Action) => void;
ExpressionRenderer: ReactExpressionRendererType;
core: CoreStart | CoreSetup;
plugins: { uiActions?: UiActionsStart };
plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart };
}
export const WorkspacePanel = debouncedComponent(InnerWorkspacePanel);
@ -135,6 +136,11 @@ export function InnerWorkspacePanel({
framePublicAPI.filters,
]);
const autoRefreshFetch$ = useMemo(
() => plugins.data.query.timefilter.timefilter.getAutoRefreshFetch$(),
[plugins.data.query.timefilter.timefilter.getAutoRefreshFetch$]
);
useEffect(() => {
// reset expression error if component attempts to run it again
if (expression && localState.expressionBuildError) {
@ -224,6 +230,7 @@ export function InnerWorkspacePanel({
className="lnsExpressionRenderer__component"
padding="m"
expression={expression!}
reload$={autoRefreshFetch$}
onEvent={(event: ExpressionRendererEvent) => {
if (!plugins.uiActions) {
// ui actions not available, not handling event...