Preserves originating path when returning from editor (#115118) (#119688)

* Add originatingPath to edit_panel_action

Support originatingPath in embeddable state transfer

Fix ts error

* Fixed jest tests

* Parse originatingPath without using hash

* provide static container context on embeddable panel

* Fixed ts error

Co-authored-by: Anton Dosov <anton.dosov@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Catherine Liu <catherine.liu@elastic.co>
Co-authored-by: Anton Dosov <anton.dosov@elastic.co>
This commit is contained in:
Kibana Machine 2021-11-24 20:25:18 -05:00 committed by GitHub
parent 295f5290e0
commit 14a87f3aa3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 103 additions and 12 deletions

View file

@ -35,6 +35,7 @@ export type {
EmbeddableEditorState,
EmbeddablePackageState,
EmbeddableRendererProps,
EmbeddableContainerContext,
} from './lib';
export {
ACTION_ADD_PANEL,

View file

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

View file

@ -43,7 +43,8 @@ export class EditPanelAction implements Action<ActionContext> {
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<ActionContext> {
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 };

View file

@ -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<EmbeddableInput, EmbeddableOutput>;
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<Props, State> {
editPanel: new EditPanelAction(
this.props.getEmbeddableFactory,
this.props.application,
this.props.stateTransfer
this.props.stateTransfer,
this.props.containerContext?.getCurrentPath
),
};
};

View file

@ -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<EmbeddableState
) => AttributeService<A, V, R>;
}
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<EmbeddableSetup, EmbeddableStart> {
private readonly embeddableFactoryDefinitions: Map<string, EmbeddableFactoryDefinition> =
@ -155,7 +160,15 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
const getEmbeddablePanelHoc =
() =>
({ embeddable, hideHeader }: { embeddable: IEmbeddable; hideHeader?: boolean }) =>
({
embeddable,
hideHeader,
containerContext,
}: {
embeddable: IEmbeddable;
hideHeader?: boolean;
containerContext?: EmbeddableContainerContext;
}) =>
(
<EmbeddablePanel
hideHeader={hideHeader}
@ -169,6 +182,7 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
application={core.application}
inspector={inspector}
SavedObjectFinder={getSavedObjectFinder(core.savedObjects, core.uiSettings)}
containerContext={containerContext}
/>
);

View file

@ -26,6 +26,7 @@ import { VisualizeConstants } from '../..';
export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => {
const [originatingApp, setOriginatingApp] = useState<string>();
const [originatingPath, setOriginatingPath] = useState<string>();
const { services } = useKibana<VisualizeServices>();
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}

View file

@ -27,6 +27,7 @@ import { VisualizeConstants } 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 { services } = useKibana<VisualizeServices>();
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}

View file

@ -37,6 +37,7 @@ interface VisualizeEditorCommonProps {
visEditorRef: RefObject<HTMLDivElement>;
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}

View file

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

View file

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

View file

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

View file

@ -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 (
<div
@ -38,7 +43,10 @@ const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => {
style={{ width: '100%', height: '100%', cursor: 'auto' }}
>
<I18nContext>
<plugins.embeddable.EmbeddablePanel embeddable={embeddableObject} />
<plugins.embeddable.EmbeddablePanel
embeddable={embeddableObject}
containerContext={embeddableContainerContext}
/>
</I18nContext>
</div>
);

View file

@ -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'}
/>

View file

@ -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<Props, State> {
mapEmbeddableInput: props.mapEmbeddableInput,
embeddableId: props.embeddableId,
originatingApp: props.originatingApp,
originatingPath: props.originatingPath,
stateTransfer: props.stateTransfer,
onSaveCallback: this.updateSaveCounter,
}),

View file

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