diff --git a/src/plugins/visualizations/public/actions/edit_in_lens_action.tsx b/src/plugins/visualizations/public/actions/edit_in_lens_action.tsx index f082d283f9ec..07268726359b 100644 --- a/src/plugins/visualizations/public/actions/edit_in_lens_action.tsx +++ b/src/plugins/visualizations/public/actions/edit_in_lens_action.tsx @@ -88,6 +88,7 @@ export class EditInLensAction implements Action { searchQuery, isEmbeddable: true, description: vis.description || embeddable.getOutput().description, + panelTimeRange: embeddable.getInput()?.timeRange, }; if (navigateToLensConfig) { if (this.currentAppId) { diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_editor.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_editor.tsx index 221cdcc9d8e1..70c0c111ce58 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_editor.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_editor.tsx @@ -24,23 +24,46 @@ import { VisualizeServices } from '../types'; import { VisualizeEditorCommon } from './visualize_editor_common'; import { VisualizeAppProps } from '../app'; import { VisualizeConstants } from '../../../common/constants'; +import type { VisualizeInput } from '../..'; export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { const { id: visualizationIdFromUrl } = useParams<{ id: string }>(); const [originatingApp, setOriginatingApp] = useState(); const [originatingPath, setOriginatingPath] = useState(); const [embeddableIdValue, setEmbeddableId] = useState(); + const [embeddableInput, setEmbeddableInput] = useState(); const { services } = useKibana(); const [eventEmitter] = useState(new EventEmitter()); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(!visualizationIdFromUrl); const isChromeVisible = useChromeVisibility(services.chrome); + useEffect(() => { + const { stateTransferService, data } = services; + const { + originatingApp: value, + searchSessionId, + embeddableId, + originatingPath: pathValue, + valueInput: valueInputValue, + } = stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; + + if (searchSessionId) { + data.search.session.continue(searchSessionId); + } else { + data.search.session.start(); + } + setEmbeddableInput(valueInputValue); + setEmbeddableId(embeddableId); + setOriginatingApp(value); + setOriginatingPath(pathValue); + }, [services]); const { savedVisInstance, visEditorRef, visEditorController } = useSavedVisInstance( services, eventEmitter, isChromeVisible, originatingApp, - visualizationIdFromUrl + visualizationIdFromUrl, + embeddableInput ); const editorName = savedVisInstance?.vis.type.title.toLowerCase().replace(' ', '_') || ''; @@ -66,26 +89,6 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { useLinkedSearchUpdates(services, eventEmitter, appState, savedVisInstance); useDataViewUpdates(services, eventEmitter, appState, savedVisInstance); - useEffect(() => { - const { stateTransferService, data } = services; - const { - originatingApp: value, - searchSessionId, - embeddableId, - originatingPath: pathValue, - } = stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; - - if (searchSessionId) { - data.search.session.continue(searchSessionId); - } else { - data.search.session.start(); - } - - setEmbeddableId(embeddableId); - setOriginatingApp(value); - setOriginatingPath(pathValue); - }, [services]); - useEffect(() => { // clean up all registered listeners if any is left return () => { diff --git a/src/plugins/visualizations/public/visualize_app/types.ts b/src/plugins/visualizations/public/visualize_app/types.ts index aa16be35277f..b1e0b9dfd2ab 100644 --- a/src/plugins/visualizations/public/visualize_app/types.ts +++ b/src/plugins/visualizations/public/visualize_app/types.ts @@ -122,6 +122,7 @@ export interface VisInstance { embeddableHandler: VisualizeEmbeddableContract; panelTitle?: string; panelDescription?: string; + panelTimeRange?: TimeRange; } export type SavedVisInstance = VisInstance; diff --git a/src/plugins/visualizations/public/visualize_app/utils/get_top_nav_config.tsx b/src/plugins/visualizations/public/visualize_app/utils/get_top_nav_config.tsx index 0fd0ab1117d8..39a840b41dc3 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/get_top_nav_config.tsx +++ b/src/plugins/visualizations/public/visualize_app/utils/get_top_nav_config.tsx @@ -315,6 +315,7 @@ export const getTopNavConfig = ( title: visInstance?.panelTitle || vis.title, visTypeTitle: vis.type.title, description: visInstance?.panelDescription || vis.description, + panelTimeRange: visInstance?.panelTimeRange, isEmbeddable: Boolean(originatingApp), }; if (navigateToLensConfig) { diff --git a/src/plugins/visualizations/public/visualize_app/utils/get_visualization_instance.test.ts b/src/plugins/visualizations/public/visualize_app/utils/get_visualization_instance.test.ts index 985f194acaf8..d4b7dbd5213e 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/get_visualization_instance.test.ts +++ b/src/plugins/visualizations/public/visualize_app/utils/get_visualization_instance.test.ts @@ -170,6 +170,10 @@ describe('getVisualizationInstanceInput', () => { id: 'test-id', description: 'description', title: 'title', + timeRange: { + from: 'now-7d/d', + to: 'now', + }, savedVis: { title: '', description: '', @@ -196,8 +200,15 @@ describe('getVisualizationInstanceInput', () => { }, }, } as unknown as VisualizeInput; - const { savedVis, savedSearch, vis, embeddableHandler, panelDescription, panelTitle } = - await getVisualizationInstanceFromInput(mockServices, input); + const { + savedVis, + savedSearch, + vis, + embeddableHandler, + panelDescription, + panelTitle, + panelTimeRange, + } = await getVisualizationInstanceFromInput(mockServices, input); expect(getSavedVisualization).toHaveBeenCalled(); expect(createVisAsync).toHaveBeenCalledWith(serializedVisMock.type, input.savedVis); @@ -216,5 +227,9 @@ describe('getVisualizationInstanceInput', () => { expect(savedSearch).toBeUndefined(); expect(panelDescription).toBe('description'); expect(panelTitle).toBe('title'); + expect(panelTimeRange).toStrictEqual({ + from: 'now-7d/d', + to: 'now', + }); }); }); diff --git a/src/plugins/visualizations/public/visualize_app/utils/get_visualization_instance.ts b/src/plugins/visualizations/public/visualize_app/utils/get_visualization_instance.ts index f872183d1b28..e4c484ecd9c3 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/get_visualization_instance.ts +++ b/src/plugins/visualizations/public/visualize_app/utils/get_visualization_instance.ts @@ -121,6 +121,7 @@ export const getVisualizationInstanceFromInput = async ( savedSearch, panelTitle: input?.title ?? '', panelDescription: input?.description ?? '', + panelTimeRange: input?.timeRange ?? undefined, }; }; diff --git a/src/plugins/visualizations/public/visualize_app/utils/use/use_saved_vis_instance.test.ts b/src/plugins/visualizations/public/visualize_app/utils/use/use_saved_vis_instance.test.ts index 93f9e2ae0b0a..4b6b87bd04f1 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/use/use_saved_vis_instance.test.ts +++ b/src/plugins/visualizations/public/visualize_app/utils/use/use_saved_vis_instance.test.ts @@ -147,6 +147,46 @@ describe('useSavedVisInstance', () => { expect(result.current.savedVisInstance).toBeDefined(); }); + test('should pass the input timeRange if it exists', async () => { + const embeddableInput = { + timeRange: { + from: 'now-7d/d', + to: 'now', + }, + id: 'panel1', + }; + const { result, waitForNextUpdate } = renderHook(() => + useSavedVisInstance( + mockServices, + eventEmitter, + true, + undefined, + savedVisId, + embeddableInput + ) + ); + + result.current.visEditorRef.current = document.createElement('div'); + expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, savedVisId); + expect(mockGetVisualizationInstance.mock.calls.length).toBe(1); + + await waitForNextUpdate(); + expect(mockServices.chrome.setBreadcrumbs).toHaveBeenCalledWith('Test Vis'); + expect(mockServices.chrome.docTitle.change).toHaveBeenCalledWith('Test Vis'); + expect(getEditBreadcrumbs).toHaveBeenCalledWith( + { originatingAppName: undefined, redirectToOrigin: undefined }, + 'Test Vis' + ); + expect(getCreateBreadcrumbs).not.toHaveBeenCalled(); + expect(mockEmbeddableHandlerRender).not.toHaveBeenCalled(); + expect(result.current.visEditorController).toBeDefined(); + expect(result.current.savedVisInstance).toBeDefined(); + expect(result.current.savedVisInstance?.panelTimeRange).toStrictEqual({ + from: 'now-7d/d', + to: 'now', + }); + }); + test('should destroy the editor and the savedVis on unmount if chrome exists', async () => { const { result, unmount, waitForNextUpdate } = renderHook(() => useSavedVisInstance(mockServices, eventEmitter, true, undefined, savedVisId) diff --git a/src/plugins/visualizations/public/visualize_app/utils/use/use_saved_vis_instance.ts b/src/plugins/visualizations/public/visualize_app/utils/use/use_saved_vis_instance.ts index 27cd03a8cc8a..f84b7928dda3 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/use/use_saved_vis_instance.ts +++ b/src/plugins/visualizations/public/visualize_app/utils/use/use_saved_vis_instance.ts @@ -17,6 +17,7 @@ import { SavedVisInstance, VisualizeServices, IEditorController } from '../../ty import { VisualizeConstants } from '../../../../common/constants'; import { getTypes } from '../../../services'; import { redirectToSavedObjectPage } from '../utils'; +import type { VisualizeInput } from '../../..'; /** * This effect is responsible for instantiating a saved vis or creating a new one @@ -27,13 +28,13 @@ export const useSavedVisInstance = ( eventEmitter: EventEmitter, isChromeVisible: boolean | undefined, originatingApp: string | undefined, - visualizationIdFromUrl: string | undefined + visualizationIdFromUrl: string | undefined, + embeddableInput?: VisualizeInput ) => { const [state, setState] = useState<{ savedVisInstance?: SavedVisInstance; visEditorController?: IEditorController; }>({}); - const visEditorRef = useRef(null); const visId = useRef(''); @@ -82,6 +83,9 @@ export const useSavedVisInstance = ( savedVisInstance = await getVisualizationInstance(services, visualizationIdFromUrl); } + if (embeddableInput && embeddableInput.timeRange) { + savedVisInstance.panelTimeRange = embeddableInput.timeRange; + } const { embeddableHandler, savedVis, vis } = savedVisInstance; const originatingAppName = originatingApp @@ -166,6 +170,7 @@ export const useSavedVisInstance = ( visualizationIdFromUrl, state.savedVisInstance, state.visEditorController, + embeddableInput, ]); useEffect(() => { diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index ebba2083fdf3..e7f4b22a4717 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -8,6 +8,7 @@ import './app.scss'; import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { i18n } from '@kbn/i18n'; +import type { TimeRange } from '@kbn/es-query'; import { EuiBreadcrumb, EuiConfirmModal } from '@elastic/eui'; import { useExecutionContext, useKibana } from '@kbn/kibana-react-plugin/public'; import { OnSaveProps } from '@kbn/saved-objects-plugin/public'; @@ -52,6 +53,7 @@ export type SaveProps = Omit onTitleDuplicate?: OnSaveProps['onTitleDuplicate']; newDescription?: string; newTags?: string[]; + panelTimeRange?: TimeRange; }; export function App({ diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 50d70bb58ae7..9128f6478329 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -670,6 +670,7 @@ export const LensTopNavMenu = ({ isTitleDuplicateConfirmed: false, returnToOrigin: true, newDescription: contextFromEmbeddable ? initialContext.description : '', + panelTimeRange: contextFromEmbeddable ? initialContext.panelTimeRange : undefined, }, { saveToLibrary: diff --git a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx index de5748b8c655..0f5f1c88dbd3 100644 --- a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx +++ b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx @@ -292,11 +292,17 @@ export const runSaveLensVisualization = async ( } } try { - const newInput = (await attributeService.wrapAttributes( + let newInput = (await attributeService.wrapAttributes( docToSave, options.saveToLibrary, originalInput )) as LensEmbeddableInput; + if (saveProps.panelTimeRange) { + newInput = { + ...newInput, + timeRange: saveProps.panelTimeRange, + }; + } if (saveProps.returnToOrigin && redirectToOrigin) { // disabling the validation on app leave because the document has been saved. diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index 193ba130e02e..0db73da11613 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -9,6 +9,7 @@ import type { History } from 'history'; import type { OnSaveProps } from '@kbn/saved-objects-plugin/public'; import { Observable } from 'rxjs'; import { SpacesApi } from '@kbn/spaces-plugin/public'; +import type { TimeRange } from '@kbn/es-query'; import type { ApplicationStart, AppMountParameters, @@ -94,6 +95,7 @@ export type RunSave = ( onTitleDuplicate?: OnSaveProps['onTitleDuplicate']; newDescription?: string; newTags?: string[]; + panelTimeRange?: TimeRange; }, options: { saveToLibrary: boolean; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 1904be8c1541..6d2493103227 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -246,6 +246,7 @@ export type VisualizeEditorContext = { searchFilters?: Filter[]; title?: string; description?: string; + panelTimeRange?: TimeRange; visTypeTitle?: string; isEmbeddable?: boolean; } & NavigateToLensContext; diff --git a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts index ef625b7116ee..579134c3ce28 100644 --- a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts +++ b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts @@ -17,7 +17,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'dashboard', 'canvas', ]); - + const dashboardCustomizePanel = getService('dashboardCustomizePanel'); + const dashboardBadgeActions = getService('dashboardBadgeActions'); + const dashboardPanelActions = getService('dashboardPanelActions'); const testSubjects = getService('testSubjects'); const retry = getService('retry'); const panelActions = getService('dashboardPanelActions'); @@ -42,6 +44,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await dashboard.waitForRenderComplete(); const originalEmbeddableCount = await canvas.getEmbeddableCount(); + await dashboardPanelActions.customizePanel(); + await dashboardCustomizePanel.clickToggleShowCustomTimeRange(); + await dashboardCustomizePanel.clickToggleQuickMenuButton(); + await dashboardCustomizePanel.clickCommonlyUsedTimeRange('Last_30 days'); + await dashboardCustomizePanel.clickSaveButton(); + await dashboard.waitForRenderComplete(); + await dashboardBadgeActions.expectExistsTimeRangeBadgeAction(); await panelActions.openContextMenu(); await panelActions.clickEdit(); @@ -59,6 +68,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); const titles = await dashboard.getPanelTitles(); expect(titles[0]).to.be('My TSVB to Lens viz 1 (converted)'); + await dashboardBadgeActions.expectExistsTimeRangeBadgeAction(); await panelActions.removePanel(); }); @@ -72,6 +82,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await dashboard.waitForRenderComplete(); const originalEmbeddableCount = await canvas.getEmbeddableCount(); + await dashboardPanelActions.customizePanel(); + await dashboardCustomizePanel.clickToggleShowCustomTimeRange(); + await dashboardCustomizePanel.clickToggleQuickMenuButton(); + await dashboardCustomizePanel.clickCommonlyUsedTimeRange('Last_30 days'); + await dashboardCustomizePanel.clickSaveButton(); + await dashboard.waitForRenderComplete(); + await dashboardBadgeActions.expectExistsTimeRangeBadgeAction(); await panelActions.openContextMenu(); await panelActions.clickEdit(); @@ -95,7 +112,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(descendants.length).to.equal(0); const titles = await dashboard.getPanelTitles(); expect(titles[0]).to.be('My TSVB to Lens viz 2 (converted)'); - + await dashboardBadgeActions.expectExistsTimeRangeBadgeAction(); await panelActions.removePanel(); }); });