[Visualize2Lens] Transfers the custom timerange to the converted panel (#155113)

## Summary

Part of https://github.com/elastic/kibana/issues/147646

It passes the custom timerange to the converted Lens panel for both by
ref and by value legacy visualizations.
It works for all paths:
- Edit visualization--> Edit in Lens--> Replace in dashboard
- Convert to Lens --> Replace in dashboard


![2](https://user-images.githubusercontent.com/17003240/233287641-82fe190d-5b92-4368-ace8-0b576a46d32a.gif)

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Marco Liberati <dej611@users.noreply.github.com>
This commit is contained in:
Stratoula Kalafateli 2023-04-27 10:11:35 +01:00 committed by GitHub
parent d80fdd6bce
commit 61c82dc868
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 124 additions and 28 deletions

View file

@ -88,6 +88,7 @@ export class EditInLensAction implements Action<EditInLensContext> {
searchQuery,
isEmbeddable: true,
description: vis.description || embeddable.getOutput().description,
panelTimeRange: embeddable.getInput()?.timeRange,
};
if (navigateToLensConfig) {
if (this.currentAppId) {

View file

@ -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<string>();
const [originatingPath, setOriginatingPath] = useState<string>();
const [embeddableIdValue, setEmbeddableId] = useState<string>();
const [embeddableInput, setEmbeddableInput] = useState<VisualizeInput>();
const { services } = useKibana<VisualizeServices>();
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 () => {

View file

@ -122,6 +122,7 @@ export interface VisInstance {
embeddableHandler: VisualizeEmbeddableContract;
panelTitle?: string;
panelDescription?: string;
panelTimeRange?: TimeRange;
}
export type SavedVisInstance = VisInstance;

View file

@ -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) {

View file

@ -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',
});
});
});

View file

@ -121,6 +121,7 @@ export const getVisualizationInstanceFromInput = async (
savedSearch,
panelTitle: input?.title ?? '',
panelDescription: input?.description ?? '',
panelTimeRange: input?.timeRange ?? undefined,
};
};

View file

@ -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)

View file

@ -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<HTMLDivElement | null>(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(() => {

View file

@ -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<OnSaveProps, 'onTitleDuplicate' | 'newDescription'>
onTitleDuplicate?: OnSaveProps['onTitleDuplicate'];
newDescription?: string;
newTags?: string[];
panelTimeRange?: TimeRange;
};
export function App({

View file

@ -670,6 +670,7 @@ export const LensTopNavMenu = ({
isTitleDuplicateConfirmed: false,
returnToOrigin: true,
newDescription: contextFromEmbeddable ? initialContext.description : '',
panelTimeRange: contextFromEmbeddable ? initialContext.panelTimeRange : undefined,
},
{
saveToLibrary:

View file

@ -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.

View file

@ -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;

View file

@ -246,6 +246,7 @@ export type VisualizeEditorContext<T extends Configuration = Configuration> = {
searchFilters?: Filter[];
title?: string;
description?: string;
panelTimeRange?: TimeRange;
visTypeTitle?: string;
isEmbeddable?: boolean;
} & NavigateToLensContext<T>;

View file

@ -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();
});
});