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

Co-authored-by: Shahzad <shahzad.muhammad@elastic.co>
This commit is contained in:
Kibana Machine 2021-04-15 11:05:54 -04:00 committed by GitHub
parent 7ce5d60ba7
commit 297a85f97c
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 |
| --- | --- | --- |
| [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. |
| [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 |

View file

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

View file

@ -92,6 +92,7 @@ export class ApplicationService {
private registrationClosed = false;
private history?: History<any>;
private navigate?: (url: string, state: unknown, replace: boolean) => void;
private openInNewTab?: (url: string) => void;
private redirectTo?: (url: string) => void;
private overlayStart$ = new Subject<OverlayStart>();
@ -117,6 +118,11 @@ export class ApplicationService {
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;
const registerStatusUpdater = (application: string, updater$: Observable<AppUpdater>) => {
@ -218,7 +224,7 @@ export class ApplicationService {
const navigateToApp: InternalApplicationStart['navigateToApp'] = async (
appId,
{ path, state, replace = false }: NavigateToAppOptions = {}
{ path, state, replace = false, openInNewTab = false }: NavigateToAppOptions = {}
) => {
const currentAppId = this.currentAppId$.value;
const navigatingToSameApp = currentAppId === appId;
@ -233,7 +239,12 @@ export class ApplicationService {
if (!navigatingToSameApp) {
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);
}
};

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`)
*/
replace?: boolean;
/**
* if true, will open the app in new tab, will share session information via window.open if base
*/
openInNewTab?: boolean;
}
/** @public */

View file

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

View file

@ -115,6 +115,7 @@ export class EmbeddableStateTransfer {
appId: string,
options?: {
path?: string;
openInNewTab?: boolean;
state: EmbeddableEditorState;
}
): Promise<void> {
@ -162,7 +163,7 @@ export class EmbeddableStateTransfer {
private async navigateToWithState<OutgoingStateType = unknown>(
appId: string,
key: string,
options?: { path?: string; state?: OutgoingStateType }
options?: { path?: string; state?: OutgoingStateType; openInNewTab?: boolean }
): Promise<void> {
const existingAppState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[key] || {};
const stateObject = {
@ -173,6 +174,6 @@ export class EmbeddableStateTransfer {
},
};
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"
navigateToEditor(appId: string, options?: {
path?: string;
openInNewTab?: boolean;
state: EmbeddableEditorState;
}): Promise<void>;
// 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 grow={false}>
<EuiButton
aria-label="Open lens in new tab"
isDisabled={!props.plugins.lens.canUseEditor()}
onClick={() => {
props.plugins.lens.navigateToPrefilledEditor({
id: '',
timeRange: time,
attributes: getLensAttributes(props.defaultIndexPattern!, color),
});
props.plugins.lens.navigateToPrefilledEditor(
{
id: '',
timeRange: time,
attributes: getLensAttributes(props.defaultIndexPattern!, color),
},
true
);
// eslint-disable-next-line no-bitwise
const newColor = '#' + ((Math.random() * 0xffffff) << 0).toString(16);
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.
*/
import rison from 'rison-node';
import type { TimeRange } from '../../../../src/plugins/data/common/query';
export const PLUGIN_ID = 'lens';
export const APP_ID = 'lens';
export const LENS_EMBEDDABLE_TYPE = 'lens';
@ -17,8 +20,18 @@ export function getBasePath() {
return `#/`;
}
export function getEditPath(id: string | undefined) {
return id ? `#/edit/${encodeURIComponent(id)}` : `#/${LENS_EDIT_BY_VALUE}`;
const GLOBAL_RISON_STATE_PARAM = '_g';
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) {

View file

@ -96,7 +96,7 @@ export interface LensPublicStart {
*
* @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.
*/
@ -243,8 +243,9 @@ export class LensPlugin {
return {
EmbeddableComponent: getEmbeddableComponent(startDependencies.embeddable),
navigateToPrefilledEditor: (input: LensEmbeddableInput) => {
if (input.timeRange) {
navigateToPrefilledEditor: (input: LensEmbeddableInput, openInNewTab?: boolean) => {
// for openInNewTab, we set the time range in url via getEditPath below
if (input.timeRange && !openInNewTab) {
startDependencies.data.query.timefilter.timefilter.setTime(input.timeRange);
}
const transfer = new EmbeddableStateTransfer(
@ -252,7 +253,8 @@ export class LensPlugin {
core.application.currentAppId$
);
transfer.navigateToEditor('lens', {
path: getEditPath(undefined),
openInNewTab,
path: getEditPath(undefined, openInNewTab ? input.timeRange : undefined),
state: {
originatingApp: '',
valueInput: input,

View file

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

View file

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