mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -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 |
|
||||
| --- | --- | --- |
|
||||
| [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 |
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
|
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.
|
||||
*/
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue