[Time to Visualize] Transition Embeddable State Transfer to Session Storage (#85688) (#85996)

* Transitioned embeddable state transfer service to use sessionStorage
This commit is contained in:
Devon Thomson 2020-12-15 15:41:53 -05:00 committed by GitHub
parent 6ae2d0a542
commit 815793057c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 277 additions and 246 deletions

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) &gt; [EmbeddableStart](./kibana-plugin-plugins-embeddable-public.embeddablestart.md) &gt; [getEmbeddablePanel](./kibana-plugin-plugins-embeddable-public.embeddablestart.getembeddablepanel.md)
## EmbeddableStart.getEmbeddablePanel property
<b>Signature:</b>
```typescript
getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC;
```

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer;
getStateTransfer: (storage?: Storage) => EmbeddableStateTransfer;
```

View file

@ -18,6 +18,5 @@ export interface EmbeddableStart extends PersistableStateService<EmbeddableState
| [getAttributeService](./kibana-plugin-plugins-embeddable-public.embeddablestart.getattributeservice.md) | <code>&lt;A extends {</code><br/><code> title: string;</code><br/><code> }, V extends EmbeddableInput &amp; {</code><br/><code> [ATTRIBUTE_SERVICE_KEY]: A;</code><br/><code> } = EmbeddableInput &amp; {</code><br/><code> [ATTRIBUTE_SERVICE_KEY]: A;</code><br/><code> }, R extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput&gt;(type: string, options: AttributeServiceOptions&lt;A&gt;) =&gt; AttributeService&lt;A, V, R&gt;</code> | |
| [getEmbeddableFactories](./kibana-plugin-plugins-embeddable-public.embeddablestart.getembeddablefactories.md) | <code>() =&gt; IterableIterator&lt;EmbeddableFactory&gt;</code> | |
| [getEmbeddableFactory](./kibana-plugin-plugins-embeddable-public.embeddablestart.getembeddablefactory.md) | <code>&lt;I extends EmbeddableInput = EmbeddableInput, O extends EmbeddableOutput = EmbeddableOutput, E extends IEmbeddable&lt;I, O&gt; = IEmbeddable&lt;I, O&gt;&gt;(embeddableFactoryId: string) =&gt; EmbeddableFactory&lt;I, O, E&gt; &#124; undefined</code> | |
| [getEmbeddablePanel](./kibana-plugin-plugins-embeddable-public.embeddablestart.getembeddablepanel.md) | <code>(stateTransfer?: EmbeddableStateTransfer) =&gt; EmbeddablePanelHOC</code> | |
| [getStateTransfer](./kibana-plugin-plugins-embeddable-public.embeddablestart.getstatetransfer.md) | <code>(history?: ScopedHistory) =&gt; EmbeddableStateTransfer</code> | |
| [getStateTransfer](./kibana-plugin-plugins-embeddable-public.embeddablestart.getstatetransfer.md) | <code>(storage?: Storage) =&gt; EmbeddableStateTransfer</code> | |

View file

@ -9,7 +9,7 @@ Constructs a new instance of the `EmbeddableStateTransfer` class
<b>Signature:</b>
```typescript
constructor(navigateToApp: ApplicationStart['navigateToApp'], scopedHistory?: ScopedHistory<unknown> | undefined, appList?: ReadonlyMap<string, PublicAppInfo> | undefined);
constructor(navigateToApp: ApplicationStart['navigateToApp'], appList?: ReadonlyMap<string, PublicAppInfo> | undefined, customStorage?: Storage);
```
## Parameters
@ -17,6 +17,6 @@ constructor(navigateToApp: ApplicationStart['navigateToApp'], scopedHistory?: Sc
| Parameter | Type | Description |
| --- | --- | --- |
| navigateToApp | <code>ApplicationStart['navigateToApp']</code> | |
| scopedHistory | <code>ScopedHistory&lt;unknown&gt; &#124; undefined</code> | |
| appList | <code>ReadonlyMap&lt;string, PublicAppInfo&gt; &#124; undefined</code> | |
| customStorage | <code>Storage</code> | |

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) &gt; [EmbeddableStateTransfer](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md) &gt; [clearEditorState](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md)
## EmbeddableStateTransfer.clearEditorState() method
<b>Signature:</b>
```typescript
clearEditorState(): void;
```
<b>Returns:</b>
`void`

View file

@ -4,21 +4,19 @@
## EmbeddableStateTransfer.getIncomingEditorState() method
Fetches an [originating app](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) argument from the scoped history's location state.
Fetches an [originating app](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) argument from the sessionStorage
<b>Signature:</b>
```typescript
getIncomingEditorState(options?: {
keysToRemoveAfterFetch?: string[];
}): EmbeddableEditorState | undefined;
getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| options | <code>{</code><br/><code> keysToRemoveAfterFetch?: string[];</code><br/><code> }</code> | |
| removeAfterFetch | <code>boolean</code> | Whether to remove the package state after fetch to prevent duplicates. |
<b>Returns:</b>

View file

@ -4,21 +4,19 @@
## EmbeddableStateTransfer.getIncomingEmbeddablePackage() method
Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) argument from the scoped history's location state.
Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) argument from the sessionStorage
<b>Signature:</b>
```typescript
getIncomingEmbeddablePackage(options?: {
keysToRemoveAfterFetch?: string[];
}): EmbeddablePackageState | undefined;
getIncomingEmbeddablePackage(removeAfterFetch?: boolean): EmbeddablePackageState | undefined;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| options | <code>{</code><br/><code> keysToRemoveAfterFetch?: string[];</code><br/><code> }</code> | |
| removeAfterFetch | <code>boolean</code> | Whether to remove the package state after fetch to prevent duplicates. |
<b>Returns:</b>

View file

@ -4,7 +4,7 @@
## EmbeddableStateTransfer class
A wrapper around the state object in which provides strongly typed helper methods for common incoming and outgoing states used by the embeddable infrastructure.
A wrapper around the session storage which provides strongly typed helper methods for common incoming and outgoing states used by the embeddable infrastructure.
<b>Signature:</b>
@ -16,7 +16,7 @@ export declare class EmbeddableStateTransfer
| Constructor | Modifiers | Description |
| --- | --- | --- |
| [(constructor)(navigateToApp, scopedHistory, appList)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer._constructor_.md) | | Constructs a new instance of the <code>EmbeddableStateTransfer</code> class |
| [(constructor)(navigateToApp, appList, customStorage)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer._constructor_.md) | | Constructs a new instance of the <code>EmbeddableStateTransfer</code> class |
## Properties
@ -28,8 +28,9 @@ export declare class EmbeddableStateTransfer
| Method | Modifiers | Description |
| --- | --- | --- |
| [getIncomingEditorState(options)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md) | | Fetches an [originating app](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) argument from the scoped history's location state. |
| [getIncomingEmbeddablePackage(options)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md) | | Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) argument from the scoped history's location state. |
| [clearEditorState()](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md) | | |
| [getIncomingEditorState(removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md) | | Fetches an [originating app](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) argument from the sessionStorage |
| [getIncomingEmbeddablePackage(removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md) | | Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) argument from the sessionStorage |
| [navigateToEditor(appId, options)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.navigatetoeditor.md) | | A wrapper around the method which navigates to the specified appId with [embeddable editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) |
| [navigateToWithEmbeddablePackage(appId, options)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.navigatetowithembeddablepackage.md) | | A wrapper around the method which navigates to the specified appId with [embeddable package state](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) |

View file

@ -12,7 +12,6 @@ A wrapper around the method which navigates to the specified appId with [embedd
navigateToEditor(appId: string, options?: {
path?: string;
state: EmbeddableEditorState;
appendToExistingState?: boolean;
}): Promise<void>;
```
@ -21,7 +20,7 @@ navigateToEditor(appId: string, options?: {
| Parameter | Type | Description |
| --- | --- | --- |
| appId | <code>string</code> | |
| options | <code>{</code><br/><code> path?: string;</code><br/><code> state: EmbeddableEditorState;</code><br/><code> appendToExistingState?: boolean;</code><br/><code> }</code> | |
| options | <code>{</code><br/><code> path?: string;</code><br/><code> state: EmbeddableEditorState;</code><br/><code> }</code> | |
<b>Returns:</b>

View file

@ -12,7 +12,6 @@ A wrapper around the method which navigates to the specified appId with [embedd
navigateToWithEmbeddablePackage(appId: string, options?: {
path?: string;
state: EmbeddablePackageState;
appendToExistingState?: boolean;
}): Promise<void>;
```
@ -21,7 +20,7 @@ navigateToWithEmbeddablePackage(appId: string, options?: {
| Parameter | Type | Description |
| --- | --- | --- |
| appId | <code>string</code> | |
| options | <code>{</code><br/><code> path?: string;</code><br/><code> state: EmbeddablePackageState;</code><br/><code> appendToExistingState?: boolean;</code><br/><code> }</code> | |
| options | <code>{</code><br/><code> path?: string;</code><br/><code> state: EmbeddablePackageState;</code><br/><code> }</code> | |
<b>Returns:</b>

View file

@ -17,7 +17,7 @@
| [EmbeddableFactoryNotFoundError](./kibana-plugin-plugins-embeddable-public.embeddablefactorynotfounderror.md) | |
| [EmbeddablePanel](./kibana-plugin-plugins-embeddable-public.embeddablepanel.md) | |
| [EmbeddableRoot](./kibana-plugin-plugins-embeddable-public.embeddableroot.md) | |
| [EmbeddableStateTransfer](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md) | A wrapper around the state object in which provides strongly typed helper methods for common incoming and outgoing states used by the embeddable infrastructure. |
| [EmbeddableStateTransfer](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md) | A wrapper around the session storage which provides strongly typed helper methods for common incoming and outgoing states used by the embeddable infrastructure. |
| [ErrorEmbeddable](./kibana-plugin-plugins-embeddable-public.errorembeddable.md) | |
| [PanelNotFoundError](./kibana-plugin-plugins-embeddable-public.panelnotfounderror.md) | |

View file

@ -36,7 +36,6 @@ import {
EmbeddableStart,
EmbeddableOutput,
EmbeddableFactory,
EmbeddableStateTransfer,
} from '../../services/embeddable';
import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants';
import { createPanelState } from './panel';
@ -111,8 +110,6 @@ const defaultCapabilities = {
export class DashboardContainer extends Container<InheritedChildInput, DashboardContainerInput> {
public readonly type = DASHBOARD_CONTAINER_TYPE;
private embeddablePanel: EmbeddableStart['EmbeddablePanel'];
public switchViewMode?: (newViewMode: ViewMode) => void;
public getPanelCount = () => {
@ -122,7 +119,6 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
constructor(
initialInput: DashboardContainerInput,
private readonly services: DashboardContainerServices,
stateTransfer?: EmbeddableStateTransfer,
parent?: Container
) {
super(
@ -134,7 +130,6 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
services.embeddable.getEmbeddableFactory,
parent
);
this.embeddablePanel = services.embeddable.getEmbeddablePanel(stateTransfer);
}
protected createNewPanelState<
@ -258,11 +253,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
ReactDOM.render(
<I18nProvider>
<KibanaContextProvider services={this.services}>
<DashboardViewport
container={this}
PanelComponent={this.embeddablePanel}
switchViewMode={this.switchViewMode}
/>
<DashboardViewport container={this} switchViewMode={this.switchViewMode} />
</KibanaContextProvider>
</I18nProvider>,
dom

View file

@ -18,7 +18,6 @@
*/
import { i18n } from '@kbn/i18n';
import { ScopedHistory } from 'src/core/public';
import {
Container,
ErrorEmbeddable,
@ -44,10 +43,7 @@ export class DashboardContainerFactoryDefinition
public readonly isContainerType = true;
public readonly type = DASHBOARD_CONTAINER_TYPE;
constructor(
private readonly getStartServices: () => Promise<DashboardContainerServices>,
private getHistory: () => ScopedHistory
) {}
constructor(private readonly getStartServices: () => Promise<DashboardContainerServices>) {}
public isEditable = async () => {
// Currently unused for dashboards
@ -74,7 +70,6 @@ export class DashboardContainerFactoryDefinition
parent?: Container
): Promise<DashboardContainer | ErrorEmbeddable> => {
const services = await this.getStartServices();
const stateTransfer = services.embeddable.getStateTransfer(this.getHistory());
return new DashboardContainer(initialInput, services, stateTransfer, parent);
return new DashboardContainer(initialInput, services, parent);
};
}

View file

@ -83,7 +83,6 @@ function prepare(props?: Partial<DashboardGridProps>) {
dashboardContainer = new DashboardContainer(initialInput, options);
const defaultTestProps: DashboardGridProps = {
container: dashboardContainer,
PanelComponent: () => <div />,
kibana: null as any,
intl: null as any,
};

View file

@ -30,7 +30,7 @@ import React from 'react';
import { Subscription } from 'rxjs';
import ReactGridLayout, { Layout } from 'react-grid-layout';
import { GridData } from '../../../../common';
import { ViewMode, EmbeddableChildPanel, EmbeddableStart } from '../../../services/embeddable';
import { ViewMode, EmbeddableChildPanel } from '../../../services/embeddable';
import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../dashboard_constants';
import { DashboardPanelState } from '../types';
import { withKibana } from '../../../services/kibana_react';
@ -115,7 +115,6 @@ const ResponsiveSizedGrid = sizeMe(config)(ResponsiveGrid);
export interface DashboardGridProps extends ReactIntl.InjectedIntlProps {
kibana: DashboardReactContextValue;
PanelComponent: EmbeddableStart['EmbeddablePanel'];
container: DashboardContainer;
}
@ -277,7 +276,7 @@ class DashboardGridUi extends React.Component<DashboardGridProps, State> {
key={panel.type}
embeddableId={panel.explicitInput.id}
container={this.props.container}
PanelComponent={this.props.PanelComponent}
PanelComponent={this.props.kibana.services.embeddable.EmbeddablePanel}
/>
</div>
);

View file

@ -92,7 +92,6 @@ function getProps(
dashboardContainer = new DashboardContainer(input, options);
const defaultTestProps: DashboardViewportProps = {
container: dashboardContainer,
PanelComponent: () => <div />,
};
return {

View file

@ -21,7 +21,6 @@ import React from 'react';
import { Subscription } from 'rxjs';
import {
PanelState,
EmbeddableStart,
ViewMode,
isErrorEmbeddable,
openAddPanelFlyout,
@ -33,7 +32,6 @@ import { context } from '../../../services/kibana_react';
import { DashboardEmptyScreen } from '../empty_screen/dashboard_empty_screen';
export interface DashboardViewportProps {
PanelComponent: EmbeddableStart['EmbeddablePanel'];
switchViewMode?: (newViewMode: ViewMode) => void;
container: DashboardContainer;
}
@ -131,7 +129,7 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
};
public render() {
const { container, PanelComponent } = this.props;
const { container } = this.props;
const isEditMode = container.getInput().viewMode !== ViewMode.VIEW;
const {
isEmbeddedExternally,
@ -174,7 +172,7 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
/>
</div>
)}
<DashboardGrid container={container} PanelComponent={PanelComponent} />
<DashboardGrid container={container} />
</div>
</React.Fragment>
);

View file

@ -79,9 +79,7 @@ export const useDashboardContainer = (
searchSession.restore(searchSessionIdFromURL);
}
const incomingEmbeddable = embeddable
.getStateTransfer(scopedHistory())
.getIncomingEmbeddablePackage();
const incomingEmbeddable = embeddable.getStateTransfer().getIncomingEmbeddablePackage(true);
(async function createContainer() {
const newContainer = await dashboardFactory.create(

View file

@ -280,11 +280,8 @@ export class DashboardPlugin
getHistory: () => this.currentHistory!,
});
const factory = new DashboardContainerFactoryDefinition(
getStartServices,
() => this.currentHistory!
);
embeddable.registerEmbeddableFactory(factory.type, factory);
const dashboardContainerFactory = new DashboardContainerFactoryDefinition(getStartServices);
embeddable.registerEmbeddableFactory(dashboardContainerFactory.type, dashboardContainerFactory);
const placeholderFactory = new PlaceholderEmbeddableFactory();
embeddable.registerEmbeddableFactory(placeholderFactory.type, placeholderFactory);

View file

@ -17,24 +17,45 @@
* under the License.
*/
import { coreMock, scopedHistoryMock } from '../../../../../core/public/mocks';
import { coreMock } from '../../../../../core/public/mocks';
import { Storage } from '../../../../kibana_utils/public';
import { EmbeddableStateTransfer } from '.';
import { ApplicationStart, PublicAppInfo } from '../../../../../core/public';
import { EMBEDDABLE_EDITOR_STATE_KEY, EMBEDDABLE_PACKAGE_STATE_KEY } from './types';
import { EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY } from './embeddable_state_transfer';
function mockHistoryState(state: unknown) {
return scopedHistoryMock.create({ state });
}
const createStorage = (): Storage => {
const createMockStore = () => {
let innerStore: Record<string, any> = {};
return {
getItem: jest.fn().mockImplementation((key) => innerStore[key]),
setItem: jest.fn().mockImplementation((key, value) => (innerStore[key] = value)),
removeItem: jest.fn().mockImplementation((key: string) => delete innerStore[key]),
clear: jest.fn().mockImplementation(() => (innerStore = {})),
};
};
const store = createMockStore();
const storage = new Storage(store);
storage.get = jest.fn().mockImplementation((key) => store.getItem(key));
storage.set = jest.fn().mockImplementation((key, value) => store.setItem(key, value));
storage.remove = jest.fn().mockImplementation((key: string) => store.removeItem(key));
storage.clear = jest.fn().mockImplementation(() => store.clear());
return storage;
};
describe('embeddable state transfer', () => {
let application: jest.Mocked<ApplicationStart>;
let stateTransfer: EmbeddableStateTransfer;
let store: Storage;
const destinationApp = 'superUltraVisualize';
const originatingApp = 'superUltraTestDashboard';
beforeEach(() => {
const core = coreMock.createStart();
application = core.application;
stateTransfer = new EmbeddableStateTransfer(application.navigateToApp);
store = createStorage();
stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, undefined, store);
});
it('cannot fetch app name when given no app list', async () => {
@ -46,7 +67,7 @@ describe('embeddable state transfer', () => {
['testId', { title: 'State Transfer Test App Hello' } as PublicAppInfo],
['testId2', { title: 'State Transfer Test App Goodbye' } as PublicAppInfo],
]);
stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, undefined, appsList);
stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, appsList);
expect(stateTransfer.getAppNameFromId('kibanana')).toBeUndefined();
});
@ -55,31 +76,34 @@ describe('embeddable state transfer', () => {
['testId', { title: 'State Transfer Test App Hello' } as PublicAppInfo],
['testId2', { title: 'State Transfer Test App Goodbye' } as PublicAppInfo],
]);
stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, undefined, appsList);
stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, appsList);
expect(stateTransfer.getAppNameFromId('testId')).toBe('State Transfer Test App Hello');
expect(stateTransfer.getAppNameFromId('testId2')).toBe('State Transfer Test App Goodbye');
});
it('can send an outgoing originating app state', async () => {
it('can send an outgoing editor state', async () => {
await stateTransfer.navigateToEditor(destinationApp, { state: { originatingApp } });
expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', {
state: { originatingApp: 'superUltraTestDashboard' },
});
});
it('can send an outgoing originating app state in append mode', async () => {
const historyMock = mockHistoryState({ kibanaIsNowForSports: 'extremeSportsKibana' });
stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, historyMock);
await stateTransfer.navigateToEditor(destinationApp, {
state: { originatingApp },
appendToExistingState: true,
expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
[EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' },
});
expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', {
path: undefined,
});
});
it('can send an outgoing editor state and retain other embeddable state keys', async () => {
store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
kibanaIsNowForSports: 'extremeSportsKibana',
});
await stateTransfer.navigateToEditor(destinationApp, {
state: { originatingApp },
});
expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
kibanaIsNowForSports: 'extremeSportsKibana',
[EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' },
});
expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', {
path: undefined,
state: {
kibanaIsNowForSports: 'extremeSportsKibana',
originatingApp: 'superUltraTestDashboard',
},
});
});
@ -87,87 +111,81 @@ describe('embeddable state transfer', () => {
await stateTransfer.navigateToWithEmbeddablePackage(destinationApp, {
state: { type: 'coolestType', input: { savedObjectId: '150' } },
});
expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', {
state: { type: 'coolestType', input: { savedObjectId: '150' } },
});
});
it('can send an outgoing embeddable package state in append mode', async () => {
const historyMock = mockHistoryState({ kibanaIsNowForSports: 'extremeSportsKibana' });
stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, historyMock);
await stateTransfer.navigateToWithEmbeddablePackage(destinationApp, {
state: { type: 'coolestType', input: { savedObjectId: '150' } },
appendToExistingState: true,
expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
[EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } },
});
expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', {
path: undefined,
state: {
kibanaIsNowForSports: 'extremeSportsKibana',
type: 'coolestType',
input: { savedObjectId: '150' },
},
});
});
it('can fetch an incoming originating app state', async () => {
const historyMock = mockHistoryState({ originatingApp: 'extremeSportsKibana' });
stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, historyMock);
const fetchedState = stateTransfer.getIncomingEditorState();
expect(fetchedState).toEqual({ originatingApp: 'extremeSportsKibana' });
it('can send an outgoing embeddable and retain other embeddable state keys', async () => {
store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
kibanaIsNowForSports: 'extremeSportsKibana',
});
await stateTransfer.navigateToWithEmbeddablePackage(destinationApp, {
state: { type: 'coolestType', input: { savedObjectId: '150' } },
});
expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
kibanaIsNowForSports: 'extremeSportsKibana',
[EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } },
});
expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', {
path: undefined,
});
});
it('returns undefined with originating app state is not in the right shape', async () => {
const historyMock = mockHistoryState({ kibanaIsNowForSports: 'extremeSportsKibana' });
stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, historyMock);
it('can fetch an incoming editor state', async () => {
store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
[EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' },
});
const fetchedState = stateTransfer.getIncomingEditorState();
expect(fetchedState).toEqual({ originatingApp: 'superUltraTestDashboard' });
});
it('incoming editor state returns undefined when state is not in the right shape', async () => {
store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
[EMBEDDABLE_EDITOR_STATE_KEY]: { helloSportsKibana: 'superUltraTestDashboard' },
});
const fetchedState = stateTransfer.getIncomingEditorState();
expect(fetchedState).toBeUndefined();
});
it('can fetch an incoming embeddable package state', async () => {
const historyMock = mockHistoryState({
type: 'skisEmbeddable',
input: { savedObjectId: '123' },
store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
[EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'skisEmbeddable', input: { savedObjectId: '123' } },
});
stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, historyMock);
const fetchedState = stateTransfer.getIncomingEmbeddablePackage();
expect(fetchedState).toEqual({ type: 'skisEmbeddable', input: { savedObjectId: '123' } });
});
it('returns undefined when embeddable package is not in the right shape', async () => {
const historyMock = mockHistoryState({ kibanaIsNowForSports: 'extremeSportsKibana' });
stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, historyMock);
it('embeddable package state returns undefined when state is not in the right shape', async () => {
store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
[EMBEDDABLE_PACKAGE_STATE_KEY]: { kibanaIsFor: 'sports' },
});
const fetchedState = stateTransfer.getIncomingEmbeddablePackage();
expect(fetchedState).toBeUndefined();
});
it('removes all keys in the keysToRemoveAfterFetch array', async () => {
const historyMock = mockHistoryState({
type: 'skisEmbeddable',
input: { savedObjectId: '123' },
test1: 'test1',
test2: 'test2',
it('removes embeddable package key when removeAfterFetch is true', async () => {
store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
[EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } },
iSHouldStillbeHere: 'doing the sports thing',
});
stateTransfer.getIncomingEmbeddablePackage(true);
expect(store.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)).toEqual({
iSHouldStillbeHere: 'doing the sports thing',
});
stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, historyMock);
stateTransfer.getIncomingEmbeddablePackage({ keysToRemoveAfterFetch: ['type', 'input'] });
expect(historyMock.replace).toHaveBeenCalledWith(
expect.objectContaining({ state: { test1: 'test1', test2: 'test2' } })
);
});
it('leaves state as is when no keysToRemove are supplied', async () => {
const historyMock = mockHistoryState({
type: 'skisEmbeddable',
input: { savedObjectId: '123' },
test1: 'test1',
test2: 'test2',
it('removes editor state key when removeAfterFetch is true', async () => {
store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
[EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superCoolFootballDashboard' },
iSHouldStillbeHere: 'doing the sports thing',
});
stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, historyMock);
stateTransfer.getIncomingEmbeddablePackage();
expect(historyMock.location.state).toEqual({
type: 'skisEmbeddable',
input: { savedObjectId: '123' },
test1: 'test1',
test2: 'test2',
stateTransfer.getIncomingEditorState(true);
expect(store.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)).toEqual({
iSHouldStillbeHere: 'doing the sports thing',
});
});
});

View file

@ -18,26 +18,35 @@
*/
import { cloneDeep } from 'lodash';
import { ScopedHistory, ApplicationStart, PublicAppInfo } from '../../../../../core/public';
import { Storage } from '../../../../kibana_utils/public';
import { ApplicationStart, PublicAppInfo } from '../../../../../core/public';
import {
EmbeddableEditorState,
isEmbeddableEditorState,
EmbeddablePackageState,
isEmbeddablePackageState,
EMBEDDABLE_PACKAGE_STATE_KEY,
EMBEDDABLE_EDITOR_STATE_KEY,
} from './types';
export const EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY = 'EMBEDDABLE_STATE_TRANSFER';
/**
* A wrapper around the state object in {@link ScopedHistory | core scoped history} which provides
* strongly typed helper methods for common incoming and outgoing states used by the embeddable infrastructure.
* A wrapper around the session storage which provides strongly typed helper methods
* for common incoming and outgoing states used by the embeddable infrastructure.
*
* @public
*/
export class EmbeddableStateTransfer {
private storage: Storage;
constructor(
private navigateToApp: ApplicationStart['navigateToApp'],
private scopedHistory?: ScopedHistory,
private appList?: ReadonlyMap<string, PublicAppInfo> | undefined
) {}
private appList?: ReadonlyMap<string, PublicAppInfo> | undefined,
customStorage?: Storage
) {
this.storage = customStorage ? customStorage : new Storage(sessionStorage);
}
/**
* Fetches an internationalized app title when given an appId.
@ -46,33 +55,43 @@ export class EmbeddableStateTransfer {
public getAppNameFromId = (appId: string): string | undefined => this.appList?.get(appId)?.title;
/**
* Fetches an {@link EmbeddableEditorState | originating app} argument from the scoped
* history's location state.
* Fetches an {@link EmbeddableEditorState | originating app} argument from the sessionStorage
*
* @param history - the scoped history to fetch from
* @param options.keysToRemoveAfterFetch - an array of keys to be removed from the state after they are retrieved
* @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates.
*/
public getIncomingEditorState(options?: {
keysToRemoveAfterFetch?: string[];
}): EmbeddableEditorState | undefined {
return this.getIncomingState<EmbeddableEditorState>(isEmbeddableEditorState, {
keysToRemoveAfterFetch: options?.keysToRemoveAfterFetch,
});
public getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined {
return this.getIncomingState<EmbeddableEditorState>(
isEmbeddableEditorState,
EMBEDDABLE_EDITOR_STATE_KEY,
{
keysToRemoveAfterFetch: removeAfterFetch ? [EMBEDDABLE_EDITOR_STATE_KEY] : undefined,
}
);
}
public clearEditorState() {
const currentState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY);
if (currentState) {
delete currentState[EMBEDDABLE_EDITOR_STATE_KEY];
this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, currentState);
}
}
/**
* Fetches an {@link EmbeddablePackageState | embeddable package} argument from the scoped
* history's location state.
* Fetches an {@link EmbeddablePackageState | embeddable package} argument from the sessionStorage
*
* @param history - the scoped history to fetch from
* @param options.keysToRemoveAfterFetch - an array of keys to be removed from the state after they are retrieved
* @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates.
*/
public getIncomingEmbeddablePackage(options?: {
keysToRemoveAfterFetch?: string[];
}): EmbeddablePackageState | undefined {
return this.getIncomingState<EmbeddablePackageState>(isEmbeddablePackageState, {
keysToRemoveAfterFetch: options?.keysToRemoveAfterFetch,
});
public getIncomingEmbeddablePackage(
removeAfterFetch?: boolean
): EmbeddablePackageState | undefined {
return this.getIncomingState<EmbeddablePackageState>(
isEmbeddablePackageState,
EMBEDDABLE_PACKAGE_STATE_KEY,
{
keysToRemoveAfterFetch: removeAfterFetch ? [EMBEDDABLE_PACKAGE_STATE_KEY] : undefined,
}
);
}
/**
@ -84,10 +103,12 @@ export class EmbeddableStateTransfer {
options?: {
path?: string;
state: EmbeddableEditorState;
appendToExistingState?: boolean;
}
): Promise<void> {
await this.navigateToWithState<EmbeddableEditorState>(appId, options);
await this.navigateToWithState<EmbeddableEditorState>(appId, EMBEDDABLE_EDITOR_STATE_KEY, {
...options,
appendToExistingState: true,
});
}
/**
@ -96,44 +117,46 @@ export class EmbeddableStateTransfer {
*/
public async navigateToWithEmbeddablePackage(
appId: string,
options?: { path?: string; state: EmbeddablePackageState; appendToExistingState?: boolean }
options?: { path?: string; state: EmbeddablePackageState }
): Promise<void> {
await this.navigateToWithState<EmbeddablePackageState>(appId, options);
await this.navigateToWithState<EmbeddablePackageState>(appId, EMBEDDABLE_PACKAGE_STATE_KEY, {
...options,
appendToExistingState: true,
});
}
private getIncomingState<IncomingStateType>(
guard: (state: unknown) => state is IncomingStateType,
key: string,
options?: {
keysToRemoveAfterFetch?: string[];
}
): IncomingStateType | undefined {
if (!this.scopedHistory) {
throw new TypeError('ScopedHistory is required to fetch incoming state');
}
const incomingState = this.scopedHistory.location?.state;
const incomingState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[key];
const castState =
!guard || guard(incomingState) ? (cloneDeep(incomingState) as IncomingStateType) : undefined;
if (castState && options?.keysToRemoveAfterFetch) {
const stateReplace = { ...(this.scopedHistory.location.state as { [key: string]: unknown }) };
options.keysToRemoveAfterFetch.forEach((key: string) => {
delete stateReplace[key];
const stateReplace = { ...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY) };
options.keysToRemoveAfterFetch.forEach((keyToRemove: string) => {
delete stateReplace[keyToRemove];
});
this.scopedHistory.replace({ ...this.scopedHistory.location, state: stateReplace });
this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateReplace);
}
return castState;
}
private async navigateToWithState<OutgoingStateType = unknown>(
appId: string,
key: string,
options?: { path?: string; state?: OutgoingStateType; appendToExistingState?: boolean }
): Promise<void> {
const stateObject =
options?.appendToExistingState && this.scopedHistory
? {
...(this.scopedHistory?.location.state as { [key: string]: unknown }),
...options.state,
}
: options?.state;
await this.navigateToApp(appId, { path: options?.path, state: stateObject });
const stateObject = options?.appendToExistingState
? {
...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY),
[key]: options.state,
}
: { [key]: options?.state };
this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateObject);
await this.navigateToApp(appId, { path: options?.path });
}
}

View file

@ -20,6 +20,8 @@
import { Optional } from '@kbn/utility-types';
import { EmbeddableInput, SavedObjectEmbeddableInput } from '..';
export const EMBEDDABLE_EDITOR_STATE_KEY = 'embeddable_editor_state';
/**
* A state package that contains information an editor will need to create or edit an embeddable then redirect back.
* @public
@ -34,6 +36,8 @@ export function isEmbeddableEditorState(state: unknown): state is EmbeddableEdit
return ensureFieldOfTypeExists('originatingApp', state, 'string');
}
export const EMBEDDABLE_PACKAGE_STATE_KEY = 'embeddable_package_state';
/**
* A state package that contains all fields necessary to create or update an embeddable by reference or by value in a container.
* @public

View file

@ -81,6 +81,7 @@ export const createEmbeddablePanelMock = ({
export const createEmbeddableStateTransferMock = (): Partial<EmbeddableStateTransfer> => {
return {
clearEditorState: jest.fn(),
getIncomingEditorState: jest.fn(),
getIncomingEmbeddablePackage: jest.fn(),
navigateToEditor: jest.fn(),
@ -125,7 +126,6 @@ const createStartContract = (): Start => {
inject: jest.fn(),
migrate: jest.fn(),
EmbeddablePanel: jest.fn(),
getEmbeddablePanel: jest.fn(),
getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer),
getAttributeService: jest.fn(),
};

View file

@ -28,7 +28,6 @@ import {
CoreSetup,
CoreStart,
Plugin,
ScopedHistory,
PublicAppInfo,
} from '../../../core/public';
import {
@ -50,6 +49,7 @@ import {
} from './lib';
import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition';
import { EmbeddableStateTransfer } from './lib/state_transfer';
import { Storage } from '../../kibana_utils/public';
import { PersistableStateService, SerializableState } from '../../kibana_utils/common';
import { ATTRIBUTE_SERVICE_KEY, AttributeService } from './lib/attribute_service';
import { AttributeServiceOptions } from './lib/attribute_service/attribute_service';
@ -95,8 +95,7 @@ export interface EmbeddableStart extends PersistableStateService<EmbeddableState
) => EmbeddableFactory<I, O, E> | undefined;
getEmbeddableFactories: () => IterableIterator<EmbeddableFactory>;
EmbeddablePanel: EmbeddablePanelHOC;
getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC;
getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer;
getStateTransfer: (storage?: Storage) => EmbeddableStateTransfer;
getAttributeService: <
A extends { title: string },
V extends EmbeddableInput & { [ATTRIBUTE_SERVICE_KEY]: A } = EmbeddableInput & {
@ -119,7 +118,7 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map();
private readonly enhancements: EnhancementsRegistry = new Map();
private customEmbeddableFactoryProvider?: EmbeddableFactoryProvider;
private outgoingOnlyStateTransfer: EmbeddableStateTransfer = {} as EmbeddableStateTransfer;
private stateTransferService: EmbeddableStateTransfer = {} as EmbeddableStateTransfer;
private isRegistryReady = false;
private appList?: ReadonlyMap<string, PublicAppInfo>;
private appListSubscription?: Subscription;
@ -160,14 +159,13 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
this.appList = appList;
});
this.outgoingOnlyStateTransfer = new EmbeddableStateTransfer(
this.stateTransferService = new EmbeddableStateTransfer(
core.application.navigateToApp,
undefined,
this.appList
);
this.isRegistryReady = true;
const getEmbeddablePanelHoc = (stateTransfer?: EmbeddableStateTransfer) => ({
const getEmbeddablePanelHoc = () => ({
embeddable,
hideHeader,
}: {
@ -177,7 +175,7 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
<EmbeddablePanel
hideHeader={hideHeader}
embeddable={embeddable}
stateTransfer={stateTransfer ? stateTransfer : this.outgoingOnlyStateTransfer}
stateTransfer={this.stateTransferService}
getActions={uiActions.getTriggerCompatibleActions}
getEmbeddableFactory={this.getEmbeddableFactory}
getAllEmbeddableFactories={this.getEmbeddableFactories}
@ -206,13 +204,11 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
options,
this.getEmbeddableFactory
),
getStateTransfer: (history?: ScopedHistory) => {
return history
? new EmbeddableStateTransfer(core.application.navigateToApp, history, this.appList)
: this.outgoingOnlyStateTransfer;
},
getStateTransfer: (storage?: Storage) =>
storage
? new EmbeddableStateTransfer(core.application.navigateToApp, this.appList, storage)
: this.stateTransferService,
EmbeddablePanel: getEmbeddablePanelHoc(),
getEmbeddablePanel: getEmbeddablePanelHoc,
telemetry: getTelemetryFunction(commonContract),
extract: getExtractFunction(commonContract),
inject: getInjectFunction(commonContract),

View file

@ -39,7 +39,7 @@ import { I18nStart as I18nStart_2 } from 'src/core/public';
import { IconType } from '@elastic/eui';
import { ISearchOptions } from 'src/plugins/data/public';
import { ISearchSource } from 'src/plugins/data/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { IStorageWrapper as IStorageWrapper_2 } from 'src/plugins/kibana_utils/public';
import { IUiSettingsClient as IUiSettingsClient_2 } from 'src/core/public';
import { KibanaClient } from '@elastic/elasticsearch/api/kibana';
import { KibanaConfigType } from 'src/core/server/kibana_config';
@ -604,12 +604,10 @@ export interface EmbeddableStart extends PersistableStateService<EmbeddableState
getEmbeddableFactories: () => IterableIterator<EmbeddableFactory>;
// (undocumented)
getEmbeddableFactory: <I extends EmbeddableInput = EmbeddableInput, O extends EmbeddableOutput = EmbeddableOutput, E extends IEmbeddable<I, O> = IEmbeddable<I, O>>(embeddableFactoryId: string) => EmbeddableFactory<I, O, E> | undefined;
// (undocumented)
getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC;
// Warning: (ae-forgotten-export) The symbol "ScopedHistory" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Storage" needs to be exported by the entry point index.d.ts
//
// (undocumented)
getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer;
getStateTransfer: (storage?: Storage) => EmbeddableStateTransfer;
}
// Warning: (ae-missing-release-tag) "EmbeddableStartDependencies" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@ -630,31 +628,25 @@ export interface EmbeddableStartDependencies {
uiActions: UiActionsStart;
}
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ScopedHistory"
//
// @public
export class EmbeddableStateTransfer {
// Warning: (ae-forgotten-export) The symbol "ApplicationStart" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "PublicAppInfo" needs to be exported by the entry point index.d.ts
constructor(navigateToApp: ApplicationStart['navigateToApp'], scopedHistory?: ScopedHistory<unknown> | undefined, appList?: ReadonlyMap<string, PublicAppInfo> | undefined);
constructor(navigateToApp: ApplicationStart['navigateToApp'], appList?: ReadonlyMap<string, PublicAppInfo> | undefined, customStorage?: Storage);
// (undocumented)
clearEditorState(): void;
getAppNameFromId: (appId: string) => string | undefined;
getIncomingEditorState(options?: {
keysToRemoveAfterFetch?: string[];
}): EmbeddableEditorState | undefined;
getIncomingEmbeddablePackage(options?: {
keysToRemoveAfterFetch?: string[];
}): EmbeddablePackageState | undefined;
getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined;
getIncomingEmbeddablePackage(removeAfterFetch?: boolean): EmbeddablePackageState | undefined;
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ApplicationStart"
navigateToEditor(appId: string, options?: {
path?: string;
state: EmbeddableEditorState;
appendToExistingState?: boolean;
}): Promise<void>;
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ApplicationStart"
navigateToWithEmbeddablePackage(appId: string, options?: {
path?: string;
state: EmbeddablePackageState;
appendToExistingState?: boolean;
}): Promise<void>;
}

View file

@ -45,10 +45,7 @@ export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => {
useEffect(() => {
const { originatingApp: value, embeddableId: embeddableIdValue, valueInput: valueInputValue } =
services.embeddable
.getStateTransfer(services.scopedHistory)
.getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'embeddableId', 'valueInput'] }) ||
{};
services.embeddable.getStateTransfer().getIncomingEditorState() || {};
setOriginatingApp(value);
setValueInput(valueInputValue);
setEmbeddableId(embeddableIdValue);

View file

@ -65,9 +65,7 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => {
useEffect(() => {
const { originatingApp: value } =
services.embeddable
.getStateTransfer(services.scopedHistory)
.getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {};
services.embeddable.getStateTransfer().getIncomingEditorState() || {};
setOriginatingApp(value);
}, [services]);

View file

@ -45,6 +45,7 @@ export const VisualizeListing = () => {
savedVisualizations,
toastNotifications,
visualizations,
embeddable,
savedObjects,
savedObjectsPublic,
savedObjectsTagging,
@ -72,6 +73,8 @@ export const VisualizeListing = () => {
}, [history, pathname, visualizations]);
useMount(() => {
// Reset editor state if the visualize listing page is loaded.
embeddable.getStateTransfer().clearEditorState();
chrome.setBreadcrumbs([
{
text: i18n.translate('visualize.visualizeListingBreadcrumbsTitle', {

View file

@ -77,6 +77,7 @@ export const getTopNavConfig = (
{
application,
chrome,
embeddable,
history,
share,
setActiveUrl,
@ -137,6 +138,8 @@ export const getTopNavConfig = (
} else {
if (setOriginatingApp && originatingApp && newlyCreated) {
setOriginatingApp(undefined);
// remove editor state so the connection is still broken after reload
stateTransfer.clearEditorState();
}
chrome.docTitle.change(savedVis.lastSavedTitle);
chrome.setBreadcrumbs(getEditBreadcrumbs(savedVis.lastSavedTitle));

View file

@ -37,9 +37,13 @@ import {
LensByReferenceInput,
} from '../editor_frame_service/embeddable/embeddable';
import { SavedObjectReference } from '../../../../../src/core/types';
import { mockAttributeService } from '../../../../../src/plugins/embeddable/public/mocks';
import {
mockAttributeService,
createEmbeddableStateTransferMock,
} from '../../../../../src/plugins/embeddable/public/mocks';
import { LensAttributeService } from '../lens_attribute_service';
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
import { EmbeddableStateTransfer } from '../../../../../src/plugins/embeddable/public';
jest.mock('../editor_frame_service/editor_frame/expression_helpers');
jest.mock('src/core/public');
@ -181,6 +185,7 @@ describe('Lens App', () => {
attributeService: makeAttributeService(),
savedObjectsClient: core.savedObjects.client,
dashboardFeatureFlag: { allowByValueEmbeddables: false },
stateTransfer: createEmbeddableStateTransferMock() as EmbeddableStateTransfer,
getOriginatingAppName: jest.fn(() => 'defaultOriginatingApp'),
application: {
...core.application,

View file

@ -59,6 +59,7 @@ export function App({
navigation,
uiSettings,
application,
stateTransfer,
notifications,
attributeService,
savedObjectsClient,
@ -463,6 +464,9 @@ export function App({
isSaveModalVisible: false,
isLinkedToOriginatingApp: false,
}));
// remove editor state so the connection is still broken after reload
stateTransfer.clearEditorState();
redirectTo(newInput.savedObjectId);
return;
}

View file

@ -46,7 +46,7 @@ export async function mountApp(
const instance = await createEditorFrame();
const storage = new Storage(localStorage);
const stateTransfer = embeddable?.getStateTransfer(params.history);
const stateTransfer = embeddable?.getStateTransfer();
const historyLocationState = params.history.location.state as HistoryLocationState;
const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState();
@ -54,6 +54,7 @@ export async function mountApp(
data,
storage,
navigation,
stateTransfer,
savedObjectsTagging,
attributeService: await attributeService(),
http: coreStart.http,
@ -86,14 +87,15 @@ export async function mountApp(
);
const getInitialInput = (
routeProps: RouteComponentProps<{ id?: string }>
routeProps: RouteComponentProps<{ id?: string }>,
editByValue?: boolean
): LensEmbeddableInput | undefined => {
if (editByValue) {
return embeddableEditorIncomingState?.valueInput as LensByValueInput;
}
if (routeProps.match.params.id) {
return { savedObjectId: routeProps.match.params.id } as LensByReferenceInput;
}
if (embeddableEditorIncomingState?.valueInput) {
return embeddableEditorIncomingState?.valueInput as LensByValueInput;
}
};
const redirectTo = (routeProps: RouteComponentProps<{ id?: string }>, savedObjectId?: string) => {
@ -142,14 +144,16 @@ export async function mountApp(
}
};
// const featureFlagConfig = await getByValueFeatureFlag();
const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => {
const renderEditor = (
routeProps: RouteComponentProps<{ id?: string }>,
editByValue?: boolean
) => {
trackUiEvent('loaded');
return (
<App
incomingState={embeddableEditorIncomingState}
editorFrame={instance}
initialInput={getInitialInput(routeProps)}
initialInput={getInitialInput(routeProps, editByValue)}
redirectTo={(savedObjectId?: string) => redirectTo(routeProps, savedObjectId)}
redirectToOrigin={redirectToOrigin}
redirectToDashboard={redirectToDashboard}
@ -182,7 +186,11 @@ export async function mountApp(
<HashRouter>
<Switch>
<Route exact path="/edit/:id" render={renderEditor} />
<Route exact path={`/${LENS_EDIT_BY_VALUE}`} render={renderEditor} />
<Route
exact
path={`/${LENS_EDIT_BY_VALUE}`}
render={(routeProps) => renderEditor(routeProps, true)}
/>
<Route exact path="/" render={renderEditor} />
<Route path="/" component={NotFound} />
</Switch>

View file

@ -33,7 +33,10 @@ import {
VisualizeFieldContext,
ACTION_VISUALIZE_LENS_FIELD,
} from '../../../../../src/plugins/ui_actions/public';
import { EmbeddableEditorState } from '../../../../../src/plugins/embeddable/public';
import {
EmbeddableEditorState,
EmbeddableStateTransfer,
} from '../../../../../src/plugins/embeddable/public';
import { TableInspectorAdapter } from '../editor_frame_service/types';
import { EditorFrameInstance } from '..';
@ -100,6 +103,7 @@ export interface LensAppServices {
uiSettings: IUiSettingsClient;
application: ApplicationStart;
notifications: NotificationsStart;
stateTransfer: EmbeddableStateTransfer;
navigation: NavigationPublicPluginStart;
attributeService: LensAttributeService;
savedObjectsClient: SavedObjectsStart['client'];

View file

@ -76,12 +76,10 @@ export async function renderApp({
setAppChrome();
function renderMapApp(routeProps: RouteComponentProps<{ savedMapId?: string }>) {
const stateTransfer = getEmbeddableService()?.getStateTransfer(
history as AppMountParameters['history']
);
const stateTransfer = getEmbeddableService()?.getStateTransfer();
const { embeddableId, originatingApp, valueInput } =
stateTransfer?.getIncomingEditorState({ keysToRemoveAfterFetch: ['originatingApp'] }) || {};
stateTransfer?.getIncomingEditorState() || {};
let mapEmbeddableInput;
if (routeProps.match.params.savedMapId) {

View file

@ -329,6 +329,10 @@ export class SavedMap {
this._mapEmbeddableInput = updatedMapEmbeddableInput;
// break connection to originating application
this._originatingApp = undefined;
// remove editor state so the connection is still broken after reload
this._getStateTransfer().clearEditorState();
getToasts().addSuccess({
title: i18n.translate('xpack.maps.topNav.saveSuccessMessage', {
defaultMessage: `Saved '{title}'`,