mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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  ### 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:
parent
d80fdd6bce
commit
61c82dc868
14 changed files with 124 additions and 28 deletions
|
@ -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) {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -122,6 +122,7 @@ export interface VisInstance {
|
|||
embeddableHandler: VisualizeEmbeddableContract;
|
||||
panelTitle?: string;
|
||||
panelDescription?: string;
|
||||
panelTimeRange?: TimeRange;
|
||||
}
|
||||
|
||||
export type SavedVisInstance = VisInstance;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -121,6 +121,7 @@ export const getVisualizationInstanceFromInput = async (
|
|||
savedSearch,
|
||||
panelTitle: input?.title ?? '',
|
||||
panelDescription: input?.description ?? '',
|
||||
panelTimeRange: input?.timeRange ?? undefined,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -670,6 +670,7 @@ export const LensTopNavMenu = ({
|
|||
isTitleDuplicateConfirmed: false,
|
||||
returnToOrigin: true,
|
||||
newDescription: contextFromEmbeddable ? initialContext.description : '',
|
||||
panelTimeRange: contextFromEmbeddable ? initialContext.panelTimeRange : undefined,
|
||||
},
|
||||
{
|
||||
saveToLibrary:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue