[Lens] Open lens in new tab via state transfer (#96723)

This commit is contained in:
Shahzad 2021-04-15 14:49:55 +02:00 committed by GitHub
parent fa959c9d23
commit cbf24cd640
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 104 additions and 28 deletions

View file

@ -16,6 +16,7 @@ export interface NavigateToAppOptions
| Property | Type | Description | | Property | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| [openInNewTab](./kibana-plugin-core-public.navigatetoappoptions.openinnewtab.md) | <code>boolean</code> | if true, will open the app in new tab, will share session information via window.open if base |
| [path](./kibana-plugin-core-public.navigatetoappoptions.path.md) | <code>string</code> | optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.app.defaultpath.md)<!-- -->\` as default. | | [path](./kibana-plugin-core-public.navigatetoappoptions.path.md) | <code>string</code> | optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.app.defaultpath.md)<!-- -->\` as default. |
| [replace](./kibana-plugin-core-public.navigatetoappoptions.replace.md) | <code>boolean</code> | if true, will not create a new history entry when navigating (using <code>replace</code> instead of <code>push</code>) | | [replace](./kibana-plugin-core-public.navigatetoappoptions.replace.md) | <code>boolean</code> | if true, will not create a new history entry when navigating (using <code>replace</code> instead of <code>push</code>) |
| [state](./kibana-plugin-core-public.navigatetoappoptions.state.md) | <code>unknown</code> | optional state to forward to the application | | [state](./kibana-plugin-core-public.navigatetoappoptions.state.md) | <code>unknown</code> | optional state to forward to the application |

View file

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

View file

@ -92,6 +92,7 @@ export class ApplicationService {
private registrationClosed = false; private registrationClosed = false;
private history?: History<any>; private history?: History<any>;
private navigate?: (url: string, state: unknown, replace: boolean) => void; private navigate?: (url: string, state: unknown, replace: boolean) => void;
private openInNewTab?: (url: string) => void;
private redirectTo?: (url: string) => void; private redirectTo?: (url: string) => void;
private overlayStart$ = new Subject<OverlayStart>(); private overlayStart$ = new Subject<OverlayStart>();
@ -117,6 +118,11 @@ export class ApplicationService {
return replace ? this.history!.replace(url, state) : this.history!.push(url, state); return replace ? this.history!.replace(url, state) : this.history!.push(url, state);
}; };
this.openInNewTab = (url) => {
// window.open shares session information if base url is same
return window.open(appendAppPath(basename, url), '_blank');
};
this.redirectTo = redirectTo; this.redirectTo = redirectTo;
const registerStatusUpdater = (application: string, updater$: Observable<AppUpdater>) => { const registerStatusUpdater = (application: string, updater$: Observable<AppUpdater>) => {
@ -218,7 +224,7 @@ export class ApplicationService {
const navigateToApp: InternalApplicationStart['navigateToApp'] = async ( const navigateToApp: InternalApplicationStart['navigateToApp'] = async (
appId, appId,
{ path, state, replace = false }: NavigateToAppOptions = {} { path, state, replace = false, openInNewTab = false }: NavigateToAppOptions = {}
) => { ) => {
const currentAppId = this.currentAppId$.value; const currentAppId = this.currentAppId$.value;
const navigatingToSameApp = currentAppId === appId; const navigatingToSameApp = currentAppId === appId;
@ -233,7 +239,12 @@ export class ApplicationService {
if (!navigatingToSameApp) { if (!navigatingToSameApp) {
this.appInternalStates.delete(this.currentAppId$.value!); this.appInternalStates.delete(this.currentAppId$.value!);
} }
this.navigate!(getAppUrl(availableMounters, appId, path), state, replace); if (openInNewTab) {
this.openInNewTab!(getAppUrl(availableMounters, appId, path));
} else {
this.navigate!(getAppUrl(availableMounters, appId, path), state, replace);
}
this.currentAppId$.next(appId); this.currentAppId$.next(appId);
} }
}; };

View file

@ -685,6 +685,11 @@ export interface NavigateToAppOptions {
* if true, will not create a new history entry when navigating (using `replace` instead of `push`) * if true, will not create a new history entry when navigating (using `replace` instead of `push`)
*/ */
replace?: boolean; replace?: boolean;
/**
* if true, will open the app in new tab, will share session information via window.open if base
*/
openInNewTab?: boolean;
} }
/** @public */ /** @public */

View file

@ -932,6 +932,7 @@ export type MountPoint<T extends HTMLElement = HTMLElement> = (element: T) => Un
// //
// @public // @public
export interface NavigateToAppOptions { export interface NavigateToAppOptions {
openInNewTab?: boolean;
path?: string; path?: string;
replace?: boolean; replace?: boolean;
state?: unknown; state?: unknown;

View file

@ -115,6 +115,7 @@ export class EmbeddableStateTransfer {
appId: string, appId: string,
options?: { options?: {
path?: string; path?: string;
openInNewTab?: boolean;
state: EmbeddableEditorState; state: EmbeddableEditorState;
} }
): Promise<void> { ): Promise<void> {
@ -162,7 +163,7 @@ export class EmbeddableStateTransfer {
private async navigateToWithState<OutgoingStateType = unknown>( private async navigateToWithState<OutgoingStateType = unknown>(
appId: string, appId: string,
key: string, key: string,
options?: { path?: string; state?: OutgoingStateType } options?: { path?: string; state?: OutgoingStateType; openInNewTab?: boolean }
): Promise<void> { ): Promise<void> {
const existingAppState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[key] || {}; const existingAppState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[key] || {};
const stateObject = { const stateObject = {
@ -173,6 +174,6 @@ export class EmbeddableStateTransfer {
}, },
}; };
this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateObject); this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateObject);
await this.navigateToApp(appId, { path: options?.path }); await this.navigateToApp(appId, { path: options?.path, openInNewTab: options?.openInNewTab });
} }
} }

View file

@ -600,6 +600,7 @@ export class EmbeddableStateTransfer {
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ApplicationStart" // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ApplicationStart"
navigateToEditor(appId: string, options?: { navigateToEditor(appId: string, options?: {
path?: string; path?: string;
openInNewTab?: boolean;
state: EmbeddableEditorState; state: EmbeddableEditorState;
}): Promise<void>; }): Promise<void>;
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ApplicationStart" // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ApplicationStart"

View file

@ -156,13 +156,17 @@ export const App = (props: {
</EuiFlexItem> </EuiFlexItem>
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiButton <EuiButton
aria-label="Open lens in new tab"
isDisabled={!props.plugins.lens.canUseEditor()} isDisabled={!props.plugins.lens.canUseEditor()}
onClick={() => { onClick={() => {
props.plugins.lens.navigateToPrefilledEditor({ props.plugins.lens.navigateToPrefilledEditor(
id: '', {
timeRange: time, id: '',
attributes: getLensAttributes(props.defaultIndexPattern!, color), timeRange: time,
}); attributes: getLensAttributes(props.defaultIndexPattern!, color),
},
true
);
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
const newColor = '#' + ((Math.random() * 0xffffff) << 0).toString(16); const newColor = '#' + ((Math.random() * 0xffffff) << 0).toString(16);
setColor(newColor); setColor(newColor);

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getEditPath } from './constants';
describe('getEditPath', function () {
it('should return value when no time range', function () {
expect(getEditPath(undefined)).toEqual('#/edit_by_value');
});
it('should return value when no time range but id is given', function () {
expect(getEditPath('1234354')).toEqual('#/edit/1234354');
});
it('should return value when time range is given', function () {
expect(getEditPath(undefined, { from: 'now-15m', to: 'now' })).toEqual(
'#/edit_by_value?_g=(time:(from:now-15m,to:now))'
);
});
it('should return value when time range and id is given', function () {
expect(getEditPath('12345', { from: 'now-15m', to: 'now' })).toEqual(
'#/edit/12345?_g=(time:(from:now-15m,to:now))'
);
});
});

View file

@ -5,6 +5,9 @@
* 2.0. * 2.0.
*/ */
import rison from 'rison-node';
import type { TimeRange } from '../../../../src/plugins/data/common/query';
export const PLUGIN_ID = 'lens'; export const PLUGIN_ID = 'lens';
export const APP_ID = 'lens'; export const APP_ID = 'lens';
export const LENS_EMBEDDABLE_TYPE = 'lens'; export const LENS_EMBEDDABLE_TYPE = 'lens';
@ -17,8 +20,18 @@ export function getBasePath() {
return `#/`; return `#/`;
} }
export function getEditPath(id: string | undefined) { const GLOBAL_RISON_STATE_PARAM = '_g';
return id ? `#/edit/${encodeURIComponent(id)}` : `#/${LENS_EDIT_BY_VALUE}`;
export function getEditPath(id: string | undefined, timeRange?: TimeRange) {
let timeParam = '';
if (timeRange) {
timeParam = `?${GLOBAL_RISON_STATE_PARAM}=${rison.encode({ time: timeRange })}`;
}
return id
? `#/edit/${encodeURIComponent(id)}${timeParam}`
: `#/${LENS_EDIT_BY_VALUE}${timeParam}`;
} }
export function getFullPath(id?: string) { export function getFullPath(id?: string) {

View file

@ -96,7 +96,7 @@ export interface LensPublicStart {
* *
* @experimental * @experimental
*/ */
navigateToPrefilledEditor: (input: LensEmbeddableInput) => void; navigateToPrefilledEditor: (input: LensEmbeddableInput, openInNewTab?: boolean) => void;
/** /**
* Method which returns true if the user has permission to use Lens as defined by application capabilities. * Method which returns true if the user has permission to use Lens as defined by application capabilities.
*/ */
@ -243,8 +243,9 @@ export class LensPlugin {
return { return {
EmbeddableComponent: getEmbeddableComponent(startDependencies.embeddable), EmbeddableComponent: getEmbeddableComponent(startDependencies.embeddable),
navigateToPrefilledEditor: (input: LensEmbeddableInput) => { navigateToPrefilledEditor: (input: LensEmbeddableInput, openInNewTab?: boolean) => {
if (input.timeRange) { // for openInNewTab, we set the time range in url via getEditPath below
if (input.timeRange && !openInNewTab) {
startDependencies.data.query.timefilter.timefilter.setTime(input.timeRange); startDependencies.data.query.timefilter.timefilter.setTime(input.timeRange);
} }
const transfer = new EmbeddableStateTransfer( const transfer = new EmbeddableStateTransfer(
@ -252,7 +253,8 @@ export class LensPlugin {
core.application.currentAppId$ core.application.currentAppId$
); );
transfer.navigateToEditor('lens', { transfer.navigateToEditor('lens', {
path: getEditPath(undefined), openInNewTab,
path: getEditPath(undefined, openInNewTab ? input.timeRange : undefined),
state: { state: {
originatingApp: '', originatingApp: '',
valueInput: input, valueInput: input,

View file

@ -41,13 +41,16 @@ describe('ExploratoryViewHeader', function () {
fireEvent.click(getByText('Open in Lens')); fireEvent.click(getByText('Open in Lens'));
expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1); expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1);
expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith({ expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith(
attributes: { title: 'Performance distribution' }, {
id: '', attributes: { title: 'Performance distribution' },
timeRange: { id: '',
from: 'now-15m', timeRange: {
to: 'now', from: 'now-15m',
to: 'now',
},
}, },
}); true
);
}); });
}); });

View file

@ -45,11 +45,14 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
isDisabled={!lens.canUseEditor() || lensAttributes === null} isDisabled={!lens.canUseEditor() || lensAttributes === null}
onClick={() => { onClick={() => {
if (lensAttributes) { if (lensAttributes) {
lens.navigateToPrefilledEditor({ lens.navigateToPrefilledEditor(
id: '', {
timeRange: series.time, id: '',
attributes: lensAttributes, timeRange: series.time,
}); attributes: lensAttributes,
},
true
);
} }
}} }}
> >