Skip to content

Commit 63db582

Browse files
committed
Remove App communication from URL (#67064)
Removed all inter-app communication via url in favour of a new service in the embeddable start contract called the state transfer service.
1 parent c11665a commit 63db582

32 files changed

+600
-170
lines changed

src/plugins/dashboard/public/application/dashboard_app_controller.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ import { getDashboardTitle } from './dashboard_strings';
7575
import { DashboardAppScope } from './dashboard_app';
7676
import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters';
7777
import { RenderDeps } from './application';
78-
import { IKbnUrlStateStorage, removeQueryParam, unhashUrl } from '../../../kibana_utils/public';
78+
import { IKbnUrlStateStorage, unhashUrl } from '../../../kibana_utils/public';
7979
import {
8080
addFatalError,
8181
AngularHttpError,
@@ -132,6 +132,7 @@ export class DashboardAppController {
132132
embeddable,
133133
share,
134134
dashboardCapabilities,
135+
scopedHistory,
135136
embeddableCapabilities: { visualizeCapabilities, mapsCapabilities },
136137
data: { query: queryService },
137138
core: {
@@ -425,15 +426,13 @@ export class DashboardAppController {
425426
refreshDashboardContainer();
426427
});
427428

428-
// This code needs to be replaced with a better mechanism for adding new embeddables of
429-
// any type from the add panel. Likely this will happen via creating a visualization "inline",
430-
// without navigating away from the UX.
431-
if ($routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE]) {
432-
const type = $routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE];
433-
const id = $routeParams[DashboardConstants.ADD_EMBEDDABLE_ID];
434-
container.addNewEmbeddable<SavedObjectEmbeddableInput>(type, { savedObjectId: id });
435-
removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_TYPE);
436-
removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_ID);
429+
const incomingState = embeddable
430+
.getStateTransfer(scopedHistory())
431+
.getIncomingEmbeddablePackage();
432+
if (incomingState) {
433+
container.addNewEmbeddable<SavedObjectEmbeddableInput>(incomingState.type, {
434+
savedObjectId: incomingState.id,
435+
});
437436
}
438437
}
439438

src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
} from '../../../../kibana_react/public';
4747
import { PLACEHOLDER_EMBEDDABLE } from './placeholder';
4848
import { PanelPlacementMethod, IPanelPlacementArgs } from './panel/dashboard_panel_placement';
49+
import { EmbeddableStateTransfer } from '../../../../embeddable/public';
4950

5051
export interface DashboardContainerInput extends ContainerInput {
5152
viewMode: ViewMode;
@@ -98,9 +99,12 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
9899

99100
public renderEmpty?: undefined | (() => React.ReactNode);
100101

102+
private embeddablePanel: EmbeddableStart['EmbeddablePanel'];
103+
101104
constructor(
102105
initialInput: DashboardContainerInput,
103106
private readonly options: DashboardContainerOptions,
107+
stateTransfer?: EmbeddableStateTransfer,
104108
parent?: Container
105109
) {
106110
super(
@@ -111,6 +115,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
111115
options.embeddable.getEmbeddableFactory,
112116
parent
113117
);
118+
this.embeddablePanel = options.embeddable.getEmbeddablePanel(stateTransfer);
114119
}
115120

116121
protected createNewPanelState<
@@ -186,7 +191,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
186191
<DashboardViewport
187192
renderEmpty={this.renderEmpty}
188193
container={this}
189-
PanelComponent={this.options.embeddable.EmbeddablePanel}
194+
PanelComponent={this.embeddablePanel}
190195
/>
191196
</KibanaContextProvider>
192197
</I18nProvider>,

src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
import { i18n } from '@kbn/i18n';
2121
import { UiActionsStart } from 'src/plugins/ui_actions/public';
22-
import { CoreStart } from 'src/core/public';
22+
import { CoreStart, ScopedHistory } from 'src/core/public';
2323
import { Start as InspectorStartContract } from 'src/plugins/inspector/public';
2424
import { EmbeddableFactory, EmbeddableStart } from '../../../../embeddable/public';
2525
import {
@@ -54,7 +54,10 @@ export class DashboardContainerFactoryDefinition
5454
public readonly isContainerType = true;
5555
public readonly type = DASHBOARD_CONTAINER_TYPE;
5656

57-
constructor(private readonly getStartServices: () => Promise<StartServices>) {}
57+
constructor(
58+
private readonly getStartServices: () => Promise<StartServices>,
59+
private getHistory: () => ScopedHistory
60+
) {}
5861

5962
public isEditable = async () => {
6063
const { capabilities } = await this.getStartServices();
@@ -81,6 +84,7 @@ export class DashboardContainerFactoryDefinition
8184
parent?: Container
8285
): Promise<DashboardContainer | ErrorEmbeddable> => {
8386
const services = await this.getStartServices();
84-
return new DashboardContainer(initialInput, services, parent);
87+
const stateTransfer = services.embeddable.getStateTransfer(this.getHistory());
88+
return new DashboardContainer(initialInput, services, stateTransfer, parent);
8589
};
8690
}

src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ function prepare(props?: Partial<DashboardGridProps>) {
6464
embeddable: {
6565
getTriggerCompatibleActions: (() => []) as any,
6666
getEmbeddableFactories: start.getEmbeddableFactories,
67+
getEmbeddablePanel: jest.fn(),
6768
getEmbeddableFactory,
6869
} as any,
6970
notifications: {} as any,

src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ function getProps(
5454
application: applicationServiceMock.createStartContract(),
5555
embeddable: {
5656
getTriggerCompatibleActions: (() => []) as any,
57+
getEmbeddablePanel: jest.fn(),
5758
getEmbeddableFactories: start.getEmbeddableFactories,
5859
getEmbeddableFactory: start.getEmbeddableFactory,
5960
} as any,

src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ import { context } from '../../../../../kibana_react/public';
2626

2727
export interface DashboardViewportProps {
2828
container: DashboardContainer;
29-
renderEmpty?: () => React.ReactNode;
3029
PanelComponent: EmbeddableStart['EmbeddablePanel'];
30+
renderEmpty?: () => React.ReactNode;
3131
}
3232

3333
interface State {

src/plugins/dashboard/public/plugin.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,10 @@ export class DashboardPlugin
206206
};
207207
};
208208

209-
const factory = new DashboardContainerFactoryDefinition(getStartServices);
209+
const factory = new DashboardContainerFactoryDefinition(
210+
getStartServices,
211+
() => this.currentHistory!
212+
);
210213
embeddable.registerEmbeddableFactory(factory.type, factory);
211214

212215
const placeholderFactory = new PlaceholderEmbeddableFactory();

src/plugins/embeddable/public/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import './index.scss';
2222
import { PluginInitializerContext } from 'src/core/public';
2323
import { EmbeddablePublicPlugin } from './plugin';
2424

25-
export { EMBEDDABLE_ORIGINATING_APP_PARAM } from './types';
2625
export {
2726
ACTION_ADD_PANEL,
2827
ACTION_APPLY_FILTER,
@@ -69,6 +68,9 @@ export {
6968
isSavedObjectEmbeddableInput,
7069
isRangeSelectTriggerContext,
7170
isValueClickTriggerContext,
71+
EmbeddableStateTransfer,
72+
EmbeddableOriginatingAppState,
73+
EmbeddablePackageState,
7274
EmbeddableRenderer,
7375
EmbeddableRendererProps,
7476
} from './lib';
@@ -82,4 +84,5 @@ export {
8284
EmbeddableStart,
8385
EmbeddableSetupDependencies,
8486
EmbeddableStartDependencies,
87+
EmbeddablePanelHOC,
8588
} from './plugin';

src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ import { ViewMode } from '../types';
2323
import { ContactCardEmbeddable } from '../test_samples';
2424
import { embeddablePluginMock } from '../../mocks';
2525
import { applicationServiceMock } from '../../../../../core/public/mocks';
26+
import { of } from 'rxjs';
2627

2728
const { doStart } = embeddablePluginMock.createInstance();
2829
const start = doStart();
2930
const getFactory = start.getEmbeddableFactory;
3031
const applicationMock = applicationServiceMock.createStartContract();
32+
const stateTransferMock = embeddablePluginMock.createStartContract().getStateTransfer();
3133

3234
class EditableEmbeddable extends Embeddable {
3335
public readonly type = 'EDITABLE_EMBEDDABLE';
@@ -43,16 +45,28 @@ class EditableEmbeddable extends Embeddable {
4345
}
4446

4547
test('is compatible when edit url is available, in edit mode and editable', async () => {
46-
const action = new EditPanelAction(getFactory, applicationMock);
48+
const action = new EditPanelAction(getFactory, applicationMock, stateTransferMock);
4749
expect(
4850
await action.isCompatible({
4951
embeddable: new EditableEmbeddable({ id: '123', viewMode: ViewMode.EDIT }, true),
5052
})
5153
).toBe(true);
5254
});
5355

56+
test('redirects to app using state transfer', async () => {
57+
applicationMock.currentAppId$ = of('superCoolCurrentApp');
58+
const action = new EditPanelAction(getFactory, applicationMock, stateTransferMock);
59+
const embeddable = new EditableEmbeddable({ id: '123', viewMode: ViewMode.EDIT }, true);
60+
embeddable.getOutput = jest.fn(() => ({ editApp: 'ultraVisualize', editPath: '/123' }));
61+
await action.execute({ embeddable });
62+
expect(stateTransferMock.navigateToWithOriginatingApp).toHaveBeenCalledWith('ultraVisualize', {
63+
path: '/123',
64+
state: { originatingApp: 'superCoolCurrentApp' },
65+
});
66+
});
67+
5468
test('getHref returns the edit urls', async () => {
55-
const action = new EditPanelAction(getFactory, applicationMock);
69+
const action = new EditPanelAction(getFactory, applicationMock, stateTransferMock);
5670
expect(action.getHref).toBeDefined();
5771

5872
if (action.getHref) {
@@ -66,7 +80,7 @@ test('getHref returns the edit urls', async () => {
6680
});
6781

6882
test('is not compatible when edit url is not available', async () => {
69-
const action = new EditPanelAction(getFactory, applicationMock);
83+
const action = new EditPanelAction(getFactory, applicationMock, stateTransferMock);
7084
const embeddable = new ContactCardEmbeddable(
7185
{
7286
id: '123',
@@ -85,7 +99,7 @@ test('is not compatible when edit url is not available', async () => {
8599
});
86100

87101
test('is not visible when edit url is available but in view mode', async () => {
88-
const action = new EditPanelAction(getFactory, applicationMock);
102+
const action = new EditPanelAction(getFactory, applicationMock, stateTransferMock);
89103
expect(
90104
await action.isCompatible({
91105
embeddable: new EditableEmbeddable(
@@ -100,7 +114,7 @@ test('is not visible when edit url is available but in view mode', async () => {
100114
});
101115

102116
test('is not compatible when edit url is available, in edit mode, but not editable', async () => {
103-
const action = new EditPanelAction(getFactory, applicationMock);
117+
const action = new EditPanelAction(getFactory, applicationMock, stateTransferMock);
104118
expect(
105119
await action.isCompatible({
106120
embeddable: new EditableEmbeddable(

src/plugins/embeddable/public/lib/actions/edit_panel_action.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,20 @@ import { take } from 'rxjs/operators';
2424
import { ViewMode } from '../types';
2525
import { EmbeddableFactoryNotFoundError } from '../errors';
2626
import { EmbeddableStart } from '../../plugin';
27-
import { EMBEDDABLE_ORIGINATING_APP_PARAM, IEmbeddable } from '../..';
27+
import { IEmbeddable, EmbeddableOriginatingAppState, EmbeddableStateTransfer } from '../..';
2828

2929
export const ACTION_EDIT_PANEL = 'editPanel';
3030

3131
interface ActionContext {
3232
embeddable: IEmbeddable;
3333
}
3434

35+
interface NavigationContext {
36+
app: string;
37+
path: string;
38+
state?: EmbeddableOriginatingAppState;
39+
}
40+
3541
export class EditPanelAction implements Action<ActionContext> {
3642
public readonly type = ACTION_EDIT_PANEL;
3743
public readonly id = ACTION_EDIT_PANEL;
@@ -40,7 +46,8 @@ export class EditPanelAction implements Action<ActionContext> {
4046

4147
constructor(
4248
private readonly getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'],
43-
private readonly application: ApplicationStart
49+
private readonly application: ApplicationStart,
50+
private readonly stateTransfer?: EmbeddableStateTransfer
4451
) {
4552
if (this.application?.currentAppId$) {
4653
this.application.currentAppId$
@@ -79,9 +86,15 @@ export class EditPanelAction implements Action<ActionContext> {
7986

8087
public async execute(context: ActionContext) {
8188
const appTarget = this.getAppTarget(context);
82-
8389
if (appTarget) {
84-
await this.application.navigateToApp(appTarget.app, { path: appTarget.path });
90+
if (this.stateTransfer && appTarget.state) {
91+
await this.stateTransfer.navigateToWithOriginatingApp(appTarget.app, {
92+
path: appTarget.path,
93+
state: appTarget.state,
94+
});
95+
} else {
96+
await this.application.navigateToApp(appTarget.app, { path: appTarget.path });
97+
}
8598
return;
8699
}
87100

@@ -92,22 +105,17 @@ export class EditPanelAction implements Action<ActionContext> {
92105
}
93106
}
94107

95-
public getAppTarget({ embeddable }: ActionContext): { app: string; path: string } | undefined {
108+
public getAppTarget({ embeddable }: ActionContext): NavigationContext | undefined {
96109
const app = embeddable ? embeddable.getOutput().editApp : undefined;
97-
let path = embeddable ? embeddable.getOutput().editPath : undefined;
110+
const path = embeddable ? embeddable.getOutput().editPath : undefined;
98111
if (app && path) {
99-
if (this.currentAppId) {
100-
path += `?${EMBEDDABLE_ORIGINATING_APP_PARAM}=${this.currentAppId}`;
101-
}
102-
return { app, path };
112+
const state = this.currentAppId ? { originatingApp: this.currentAppId } : undefined;
113+
return { app, path, state };
103114
}
104115
}
105116

106117
public async getHref({ embeddable }: ActionContext): Promise<string> {
107-
let editUrl = embeddable ? embeddable.getOutput().editUrl : undefined;
108-
if (editUrl && this.currentAppId) {
109-
editUrl += `?${EMBEDDABLE_ORIGINATING_APP_PARAM}=${this.currentAppId}`;
110-
}
118+
const editUrl = embeddable ? embeddable.getOutput().editUrl : undefined;
111119
return editUrl ? editUrl : '';
112120
}
113121
}

0 commit comments

Comments
 (0)