From 4a1b05c843c39beb3ea2d4db35f444e4b120c428 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 3 Jun 2020 11:35:55 +0200 Subject: [PATCH] Lens editor auto refresh (#65868) --- .../public/react_expression_renderer.test.tsx | 25 ++++++++++++++ .../public/react_expression_renderer.tsx | 16 ++++++++- .../editor_frame/editor_frame.tsx | 1 + .../editor_frame/suggestion_panel.test.tsx | 2 ++ .../editor_frame/suggestion_panel.tsx | 19 +++++++++-- .../editor_frame/workspace_panel.test.tsx | 34 ++++++++++++------- .../editor_frame/workspace_panel.tsx | 9 ++++- 7 files changed, 88 insertions(+), 18 deletions(-) diff --git a/src/plugins/expressions/public/react_expression_renderer.test.tsx b/src/plugins/expressions/public/react_expression_renderer.test.tsx index 702f88d785756..7c1711f056d69 100644 --- a/src/plugins/expressions/public/react_expression_renderer.test.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.test.tsx @@ -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(); + + 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()); diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx index a83c63443906b..bf716a3b9b1e8 100644 --- a/src/plugins/expressions/public/react_expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -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; } export type ReactExpressionRendererType = React.ComponentType; @@ -63,6 +67,7 @@ export const ReactExpressionRenderer = ({ renderError, expression, onEvent, + reload$, ...expressionLoaderOptions }: ReactExpressionRendererProps) => { const mountpoint: React.MutableRefObject = 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( () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 90405b98afe65..07c76a81ed62d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -305,6 +305,7 @@ export function EditorFrame(props: EditorFrameProps) { dispatch={dispatch} ExpressionRenderer={props.ExpressionRenderer} stagedPreview={state.stagedPreview} + plugins={props.plugins} /> ) } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx index 6b0f0338d4015..fd509c0046e13 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx @@ -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() }, }; }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 0f0885d696ba4..b06b316ec79aa 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -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) => ( + + ); + }, [plugins.data.query.timefilter.timefilter.getAutoRefreshFetch$, ExpressionRendererComponent]); + const [lastSelectedSuggestion, setLastSelectedSuggestion] = useState(-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'); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx index 59b5f358e190f..49d12e9f41440 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx @@ -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; @@ -34,6 +40,7 @@ describe('workspace_panel', () => { let expressionRendererMock: jest.Mock; let uiActionsMock: jest.Mocked; + let dataMock: jest.Mocked; let trigger: jest.Mocked>; let instance: ReactWrapper; @@ -41,6 +48,7 @@ describe('workspace_panel', () => { beforeEach(() => { trigger = ({ exec: jest.fn() } as unknown) as jest.Mocked>; 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 }} /> ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx index 44dd9f8364870..76da38ead6523 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx @@ -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...