diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 6a6b5b2df2ddde..c6beccd5e3365c 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -35,6 +35,7 @@ export type { EmbeddableEditorState, EmbeddablePackageState, EmbeddableRendererProps, + EmbeddableContainerContext, } from './lib'; export { ACTION_ADD_PANEL, diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx index 1731907288460a..a8c54ca8cdc33c 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx @@ -44,7 +44,13 @@ test('is compatible when edit url is available, in edit mode and editable', asyn test('redirects to app using state transfer with by value mode', async () => { applicationMock.currentAppId$ = of('superCoolCurrentApp'); - const action = new EditPanelAction(getFactory, applicationMock, stateTransferMock); + const testPath = '/test-path'; + const action = new EditPanelAction( + getFactory, + applicationMock, + stateTransferMock, + () => testPath + ); const embeddable = new EditableEmbeddable( { id: '123', @@ -67,13 +73,20 @@ test('redirects to app using state transfer with by value mode', async () => { coolInput1: 1, coolInput2: 2, }, + originatingPath: testPath, }, }); }); test('redirects to app using state transfer without by value mode', async () => { applicationMock.currentAppId$ = of('superCoolCurrentApp'); - const action = new EditPanelAction(getFactory, applicationMock, stateTransferMock); + const testPath = '/test-path'; + const action = new EditPanelAction( + getFactory, + applicationMock, + stateTransferMock, + () => testPath + ); const embeddable = new EditableEmbeddable( { id: '123', viewMode: ViewMode.EDIT, savedObjectId: '1234' } as SavedObjectEmbeddableInput, true @@ -86,6 +99,7 @@ test('redirects to app using state transfer without by value mode', async () => originatingApp: 'superCoolCurrentApp', embeddableId: '123', valueInput: undefined, + originatingPath: testPath, }, }); }); diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index ea90307ef57a10..fde5c2ae62bc05 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -43,7 +43,8 @@ export class EditPanelAction implements Action { constructor( private readonly getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'], private readonly application: ApplicationStart, - private readonly stateTransfer?: EmbeddableStateTransfer + private readonly stateTransfer?: EmbeddableStateTransfer, + private readonly getOriginatingPath?: () => string ) { if (this.application?.currentAppId$) { this.application.currentAppId$ @@ -104,15 +105,21 @@ export class EditPanelAction implements Action { public getAppTarget({ embeddable }: ActionContext): NavigationContext | undefined { const app = embeddable ? embeddable.getOutput().editApp : undefined; const path = embeddable ? embeddable.getOutput().editPath : undefined; + if (app && path) { if (this.currentAppId) { const byValueMode = !(embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId; + + const originatingPath = this.getOriginatingPath?.(); + const state: EmbeddableEditorState = { originatingApp: this.currentAppId, valueInput: byValueMode ? this.getExplicitInput({ embeddable }) : undefined, embeddableId: embeddable.id, searchSessionId: embeddable.getInput().searchSessionId, + originatingPath, }; + return { app, path, state }; } return { app, path }; diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index cce286bfe73b41..9807a47698a50f 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -53,6 +53,18 @@ const removeById = ({ id }: { id: string }) => disabledActions.indexOf(id) === -1; +/** + * Embeddable container may provide information about its environment, + * Use it for drilling down data that is static or doesn't have to be reactive, + * otherwise prefer passing data with input$ + * */ +export interface EmbeddableContainerContext { + /** + * Current app's path including query and hash starting from {appId} + */ + getCurrentPath?: () => string; +} + interface Props { embeddable: IEmbeddable; getActions: UiActionsService['getTriggerCompatibleActions']; @@ -70,6 +82,7 @@ interface Props { showShadow?: boolean; showBadges?: boolean; showNotifications?: boolean; + containerContext?: EmbeddableContainerContext; } interface State { @@ -373,7 +386,8 @@ export class EmbeddablePanel extends React.Component { editPanel: new EditPanelAction( this.props.getEmbeddableFactory, this.props.application, - this.props.stateTransfer + this.props.stateTransfer, + this.props.containerContext?.getCurrentPath ), }; }; diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index 7e393c6fc14e15..9043514fad317d 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -36,6 +36,7 @@ import { IEmbeddable, EmbeddablePanel, SavedObjectEmbeddableInput, + EmbeddableContainerContext, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; import { EmbeddableStateTransfer } from './lib/state_transfer'; @@ -97,7 +98,11 @@ export interface EmbeddableStart extends PersistableStateService AttributeService; } -export type EmbeddablePanelHOC = React.FC<{ embeddable: IEmbeddable; hideHeader?: boolean }>; +export type EmbeddablePanelHOC = React.FC<{ + embeddable: IEmbeddable; + hideHeader?: boolean; + containerContext?: EmbeddableContainerContext; +}>; export class EmbeddablePublicPlugin implements Plugin { private readonly embeddableFactoryDefinitions: Map = @@ -155,7 +160,15 @@ export class EmbeddablePublicPlugin implements Plugin - ({ embeddable, hideHeader }: { embeddable: IEmbeddable; hideHeader?: boolean }) => + ({ + embeddable, + hideHeader, + containerContext, + }: { + embeddable: IEmbeddable; + hideHeader?: boolean; + containerContext?: EmbeddableContainerContext; + }) => ( ); diff --git a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx index c32d15c336cfb2..bab18c7263a73d 100644 --- a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx @@ -26,6 +26,7 @@ import { VisualizeConstants } from '../..'; export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => { const [originatingApp, setOriginatingApp] = useState(); + const [originatingPath, setOriginatingPath] = useState(); const { services } = useKibana(); const [eventEmitter] = useState(new EventEmitter()); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); @@ -39,8 +40,10 @@ export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => { embeddableId: embeddableIdValue, valueInput: valueInputValue, searchSessionId, + originatingPath: pathValue, } = stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; + setOriginatingPath(pathValue); setOriginatingApp(value); setValueInput(valueInputValue); setEmbeddableId(embeddableIdValue); @@ -64,7 +67,8 @@ export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => { eventEmitter, isChromeVisible, valueInput, - originatingApp + originatingApp, + originatingPath ); const { appState, hasUnappliedChanges } = useVisualizeAppState( services, @@ -99,6 +103,7 @@ export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => { isEmbeddableRendered={isEmbeddableRendered} originatingApp={originatingApp} setOriginatingApp={setOriginatingApp} + originatingPath={originatingPath} setHasUnsavedChanges={setHasUnsavedChanges} visEditorRef={visEditorRef} embeddableId={embeddableId} diff --git a/src/plugins/visualize/public/application/components/visualize_editor.tsx b/src/plugins/visualize/public/application/components/visualize_editor.tsx index e81a886cee5a1b..7688080df3e2c4 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor.tsx @@ -27,6 +27,7 @@ import { VisualizeConstants } 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 { services } = useKibana(); const [eventEmitter] = useState(new EventEmitter()); @@ -61,6 +62,7 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { originatingApp: value, searchSessionId, embeddableId, + originatingPath: pathValue, } = stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; if (searchSessionId) { @@ -71,6 +73,7 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { setEmbeddableId(embeddableId); setOriginatingApp(value); + setOriginatingPath(pathValue); }, [services]); useEffect(() => { @@ -91,6 +94,7 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { isEmbeddableRendered={isEmbeddableRendered} originatingApp={originatingApp} setOriginatingApp={setOriginatingApp} + originatingPath={originatingPath} visualizationIdFromUrl={visualizationIdFromUrl} setHasUnsavedChanges={setHasUnsavedChanges} visEditorRef={visEditorRef} diff --git a/src/plugins/visualize/public/application/components/visualize_editor_common.tsx b/src/plugins/visualize/public/application/components/visualize_editor_common.tsx index 6268ba5c936efb..9e10323070051e 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor_common.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor_common.tsx @@ -37,6 +37,7 @@ interface VisualizeEditorCommonProps { visEditorRef: RefObject; originatingApp?: string; setOriginatingApp?: (originatingApp: string | undefined) => void; + originatingPath?: string; visualizationIdFromUrl?: string; embeddableId?: string; } @@ -52,6 +53,7 @@ export const VisualizeEditorCommon = ({ isEmbeddableRendered, onAppLeave, originatingApp, + originatingPath, setOriginatingApp, visualizationIdFromUrl, embeddableId, @@ -117,6 +119,7 @@ export const VisualizeEditorCommon = ({ isEmbeddableRendered={isEmbeddableRendered} hasUnappliedChanges={hasUnappliedChanges} originatingApp={originatingApp} + originatingPath={originatingPath} setOriginatingApp={setOriginatingApp} visInstance={visInstance} stateContainer={appState} diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index ad933e597f0a74..c602b0193cf9cd 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -29,6 +29,7 @@ interface VisualizeTopNavProps { setHasUnsavedChanges: (value: boolean) => void; hasUnappliedChanges: boolean; originatingApp?: string; + originatingPath?: string; visInstance: VisualizeEditorVisInstance; setOriginatingApp?: (originatingApp: string | undefined) => void; stateContainer: VisualizeAppStateContainer; @@ -46,6 +47,7 @@ const TopNav = ({ hasUnappliedChanges, originatingApp, setOriginatingApp, + originatingPath, visInstance, stateContainer, visualizationIdFromUrl, @@ -88,6 +90,7 @@ const TopNav = ({ openInspector, originatingApp, setOriginatingApp, + originatingPath, visInstance, stateContainer, visualizationIdFromUrl, @@ -104,6 +107,7 @@ const TopNav = ({ hasUnappliedChanges, openInspector, originatingApp, + originatingPath, visInstance, setOriginatingApp, stateContainer, diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 772565734dac43..fd739a97e8cd0f 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -54,6 +54,7 @@ export interface TopNavConfigParams { setHasUnsavedChanges: (value: boolean) => void; openInspector: () => void; originatingApp?: string; + originatingPath?: string; setOriginatingApp?: (originatingApp: string | undefined) => void; hasUnappliedChanges: boolean; visInstance: VisualizeEditorVisInstance; @@ -79,6 +80,7 @@ export const getTopNavConfig = ( setHasUnsavedChanges, openInspector, originatingApp, + originatingPath, setOriginatingApp, hasUnappliedChanges, visInstance, @@ -168,6 +170,8 @@ export const getTopNavConfig = ( if (saveOptions.dashboardId) { path = saveOptions.dashboardId === 'new' ? '#/create' : `#/view/${saveOptions.dashboardId}`; + } else if (originatingPath) { + path = originatingPath; } if (stateTransfer) { @@ -232,7 +236,8 @@ export const getTopNavConfig = ( type: VISUALIZE_EMBEDDABLE_TYPE, searchSessionId: data.search.session.getSessionId(), }; - stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { state }); + + stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { state, path: originatingPath }); }; const navigateToOriginatingApp = () => { diff --git a/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts index 27c9f77b989106..8e0b9692f8aabb 100644 --- a/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts +++ b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts @@ -19,7 +19,8 @@ export const useVisByValue = ( eventEmitter: EventEmitter, isChromeVisible: boolean | undefined, valueInput?: VisualizeInput, - originatingApp?: string + originatingApp?: string, + originatingPath?: string ) => { const [state, setState] = useState<{ byValueVisInstance?: ByValueVisInstance; @@ -55,7 +56,9 @@ export const useVisByValue = ( const originatingAppName = originatingApp ? stateTransferService.getAppNameFromId(originatingApp) : undefined; - const redirectToOrigin = originatingApp ? () => navigateToApp(originatingApp) : undefined; + const redirectToOrigin = originatingApp + ? () => navigateToApp(originatingApp, { path: originatingPath }) + : undefined; chrome?.setBreadcrumbs( getEditBreadcrumbs({ byValue: true, originatingAppName, redirectToOrigin }) ); @@ -76,6 +79,7 @@ export const useVisByValue = ( state.visEditorController, valueInput, originatingApp, + originatingPath, ]); useEffect(() => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index 953746c2808406..0b1adb9559e55c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -20,6 +20,7 @@ import { RendererStrings } from '../../../i18n'; import { embeddableInputToExpression } from './embeddable_input_to_expression'; import { RendererFactory, EmbeddableInput } from '../../../types'; import { CANVAS_EMBEDDABLE_CLASSNAME } from '../../../common/lib'; +import type { EmbeddableContainerContext } from '../../../../../../src/plugins/embeddable/public/'; const { embeddable: strings } = RendererStrings; @@ -31,6 +32,10 @@ const embeddablesRegistry: { const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { const I18nContext = core.i18n.Context; + const embeddableContainerContext: EmbeddableContainerContext = { + getCurrentPath: () => window.location.hash, + }; + return (embeddableObject: IEmbeddable) => { return (
{ style={{ width: '100%', height: '100%', cursor: 'auto' }} > - +
); diff --git a/x-pack/plugins/maps/public/render_app.tsx b/x-pack/plugins/maps/public/render_app.tsx index 3eefaeb6f7a9b9..17c1dfdc98f2ec 100644 --- a/x-pack/plugins/maps/public/render_app.tsx +++ b/x-pack/plugins/maps/public/render_app.tsx @@ -77,7 +77,7 @@ export async function renderApp( setAppChrome(); function renderMapApp(routeProps: RouteComponentProps<{ savedMapId?: string }>) { - const { embeddableId, originatingApp, valueInput } = + const { embeddableId, originatingApp, valueInput, originatingPath } = stateTransfer.getIncomingEditorState(APP_ID) || {}; let mapEmbeddableInput; @@ -98,6 +98,7 @@ export async function renderApp( setHeaderActionMenu={setHeaderActionMenu} stateTransfer={stateTransfer} originatingApp={originatingApp} + originatingPath={originatingPath} history={history} key={routeProps.match.params.savedMapId ? routeProps.match.params.savedMapId : 'new'} /> diff --git a/x-pack/plugins/maps/public/routes/map_page/map_page.tsx b/x-pack/plugins/maps/public/routes/map_page/map_page.tsx index 7e927115a5d06d..b382be1d506bd4 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_page.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_page.tsx @@ -20,6 +20,7 @@ interface Props { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; stateTransfer: EmbeddableStateTransfer; originatingApp?: string; + originatingPath?: string; history: AppMountParameters['history']; } @@ -43,6 +44,7 @@ export class MapPage extends Component { mapEmbeddableInput: props.mapEmbeddableInput, embeddableId: props.embeddableId, originatingApp: props.originatingApp, + originatingPath: props.originatingPath, stateTransfer: props.stateTransfer, onSaveCallback: this.updateSaveCounter, }), diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts index 3cff8d97138303..a6f1eb8572ef18 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts @@ -58,6 +58,7 @@ export class SavedMap { private _mapEmbeddableInput?: MapEmbeddableInput; private readonly _onSaveCallback?: () => void; private _originatingApp?: string; + private _originatingPath?: string; private readonly _stateTransfer?: EmbeddableStateTransfer; private readonly _store: MapStore; private _tags: string[] = []; @@ -69,6 +70,7 @@ export class SavedMap { onSaveCallback, originatingApp, stateTransfer, + originatingPath, }: { defaultLayers?: LayerDescriptor[]; mapEmbeddableInput?: MapEmbeddableInput; @@ -76,12 +78,14 @@ export class SavedMap { onSaveCallback?: () => void; originatingApp?: string; stateTransfer?: EmbeddableStateTransfer; + originatingPath?: string; }) { this._defaultLayers = defaultLayers; this._mapEmbeddableInput = mapEmbeddableInput; this._embeddableId = embeddableId; this._onSaveCallback = onSaveCallback; this._originatingApp = originatingApp; + this._originatingPath = originatingPath; this._stateTransfer = stateTransfer; this._store = createMapStore(); } @@ -379,6 +383,7 @@ export class SavedMap { type: MAP_SAVED_OBJECT_TYPE, input: updatedMapEmbeddableInput, }, + path: this._originatingPath, }); return; } else if (dashboardId) {