mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
[Lens] Open lens in new tab via state transfer (#96723)
This commit is contained in:
parent
fa959c9d23
commit
cbf24cd640
13 changed files with 104 additions and 28 deletions
|
@ -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 |
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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);
|
||||||
|
|
30
x-pack/plugins/lens/common/constants.test.ts
Normal file
30
x-pack/plugins/lens/common/constants.test.ts
Normal 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))'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue