Dashboard locator (#102854)

* Add dashboard locator

* feat: 🎸 expose dashboard locator from dashboard plugin

* Use dashboard locator in dashboard drilldown

* Add tests for dashboard locator

* Fix dashboard drilldown tests after refactor

* Deprecate dashboard URL generator

* Fix TypeScript errors in exmaple plugin

* Use correct type for dashboard locator

* refactor: 💡 change "route" attribute to "path"

* chore: 🤖 remove unused bundle

Co-authored-by: Vadim Kibana <vadimkibana@gmail.com>
Co-authored-by: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Vadim Dalecky 2021-06-25 07:58:03 +02:00 committed by GitHub
parent bc6ee27a29
commit baf2de5415
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 578 additions and 59 deletions

View file

@ -22,11 +22,14 @@ export {
DashboardUrlGenerator,
DashboardFeatureFlagConfig,
} from './plugin';
export {
DASHBOARD_APP_URL_GENERATOR,
createDashboardUrlGenerator,
DashboardUrlGeneratorState,
} from './url_generator';
export { DashboardAppLocator, DashboardAppLocatorParams } from './locator';
export { DashboardSavedObject } from './saved_dashboards';
export { SavedDashboardPanel, DashboardContainerInput } from './types';

View file

@ -0,0 +1,323 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { DashboardAppLocatorDefinition } from './locator';
import { hashedItemStore } from '../../kibana_utils/public';
import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock';
import { esFilters } from '../../data/public';
describe('dashboard locator', () => {
beforeEach(() => {
// @ts-ignore
hashedItemStore.storage = mockStorage;
});
test('creates a link to a saved dashboard', async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: false,
getDashboardFilterFields: async (dashboardId: string) => [],
});
const location = await definition.getLocation({});
expect(location).toMatchObject({
app: 'dashboards',
path: '#/create?_a=()&_g=()',
state: {},
});
});
test('creates a link with global time range set up', async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: false,
getDashboardFilterFields: async (dashboardId: string) => [],
});
const location = await definition.getLocation({
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
});
expect(location).toMatchObject({
app: 'dashboards',
path: '#/create?_a=()&_g=(time:(from:now-15m,mode:relative,to:now))',
state: {},
});
});
test('creates a link with filters, time range, refresh interval and query to a saved object', async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: false,
getDashboardFilterFields: async (dashboardId: string) => [],
});
const location = await definition.getLocation({
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
refreshInterval: { pause: false, value: 300 },
dashboardId: '123',
filters: [
{
meta: {
alias: null,
disabled: false,
negate: false,
},
query: { query: 'hi' },
},
{
meta: {
alias: null,
disabled: false,
negate: false,
},
query: { query: 'hi' },
$state: {
store: esFilters.FilterStateStore.GLOBAL_STATE,
},
},
],
query: { query: 'bye', language: 'kuery' },
});
expect(location).toMatchObject({
app: 'dashboards',
path: `#/view/123?_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),query:(language:kuery,query:bye))&_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))`,
state: {},
});
});
test('searchSessionId', async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: false,
getDashboardFilterFields: async (dashboardId: string) => [],
});
const location = await definition.getLocation({
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
refreshInterval: { pause: false, value: 300 },
dashboardId: '123',
filters: [],
query: { query: 'bye', language: 'kuery' },
searchSessionId: '__sessionSearchId__',
});
expect(location).toMatchObject({
app: 'dashboards',
path: `#/view/123?_a=(filters:!(),query:(language:kuery,query:bye))&_g=(filters:!(),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&searchSessionId=__sessionSearchId__`,
state: {},
});
});
test('savedQuery', async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: false,
getDashboardFilterFields: async (dashboardId: string) => [],
});
const location = await definition.getLocation({
savedQuery: '__savedQueryId__',
});
expect(location).toMatchObject({
app: 'dashboards',
path: `#/create?_a=(savedQuery:__savedQueryId__)&_g=()`,
state: {},
});
expect(location.path).toContain('__savedQueryId__');
});
test('panels', async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: false,
getDashboardFilterFields: async (dashboardId: string) => [],
});
const location = await definition.getLocation({
panels: [{ fakePanelContent: 'fakePanelContent' }] as any,
});
expect(location).toMatchObject({
app: 'dashboards',
path: `#/create?_a=(panels:!((fakePanelContent:fakePanelContent)))&_g=()`,
state: {},
});
});
test('if no useHash setting is given, uses the one was start services', async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: true,
getDashboardFilterFields: async (dashboardId: string) => [],
});
const location = await definition.getLocation({
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
});
expect(location.path.indexOf('relative')).toBe(-1);
});
test('can override a false useHash ui setting', async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: false,
getDashboardFilterFields: async (dashboardId: string) => [],
});
const location = await definition.getLocation({
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
useHash: true,
});
expect(location.path.indexOf('relative')).toBe(-1);
});
test('can override a true useHash ui setting', async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: true,
getDashboardFilterFields: async (dashboardId: string) => [],
});
const location = await definition.getLocation({
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
useHash: false,
});
expect(location.path.indexOf('relative')).toBeGreaterThan(1);
});
describe('preserving saved filters', () => {
const savedFilter1 = {
meta: {
alias: null,
disabled: false,
negate: false,
},
query: { query: 'savedfilter1' },
};
const savedFilter2 = {
meta: {
alias: null,
disabled: false,
negate: false,
},
query: { query: 'savedfilter2' },
};
const appliedFilter = {
meta: {
alias: null,
disabled: false,
negate: false,
},
query: { query: 'appliedfilter' },
};
test('attaches filters from destination dashboard', async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: false,
getDashboardFilterFields: async (dashboardId: string) => {
return dashboardId === 'dashboard1'
? [savedFilter1]
: dashboardId === 'dashboard2'
? [savedFilter2]
: [];
},
});
const location1 = await definition.getLocation({
dashboardId: 'dashboard1',
filters: [appliedFilter],
});
expect(location1.path).toEqual(expect.stringContaining('query:savedfilter1'));
expect(location1.path).toEqual(expect.stringContaining('query:appliedfilter'));
const location2 = await definition.getLocation({
dashboardId: 'dashboard2',
filters: [appliedFilter],
});
expect(location2.path).toEqual(expect.stringContaining('query:savedfilter2'));
expect(location2.path).toEqual(expect.stringContaining('query:appliedfilter'));
});
test("doesn't fail if can't retrieve filters from destination dashboard", async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: false,
getDashboardFilterFields: async (dashboardId: string) => {
if (dashboardId === 'dashboard1') {
throw new Error('Not found');
}
return [];
},
});
const location = await definition.getLocation({
dashboardId: 'dashboard1',
filters: [appliedFilter],
});
expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1'));
expect(location.path).toEqual(expect.stringContaining('query:appliedfilter'));
});
test('can enforce empty filters', async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: false,
getDashboardFilterFields: async (dashboardId: string) => {
if (dashboardId === 'dashboard1') {
return [savedFilter1];
}
return [];
},
});
const location = await definition.getLocation({
dashboardId: 'dashboard1',
filters: [],
preserveSavedFilters: false,
});
expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1'));
expect(location.path).not.toEqual(expect.stringContaining('query:appliedfilter'));
expect(location.path).toMatchInlineSnapshot(
`"#/view/dashboard1?_a=(filters:!())&_g=(filters:!())"`
);
});
test('no filters in result url if no filters applied', async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: false,
getDashboardFilterFields: async (dashboardId: string) => {
if (dashboardId === 'dashboard1') {
return [savedFilter1];
}
return [];
},
});
const location = await definition.getLocation({
dashboardId: 'dashboard1',
});
expect(location.path).not.toEqual(expect.stringContaining('filters'));
expect(location.path).toMatchInlineSnapshot(`"#/view/dashboard1?_a=()&_g=()"`);
});
test('can turn off preserving filters', async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: false,
getDashboardFilterFields: async (dashboardId: string) => {
if (dashboardId === 'dashboard1') {
return [savedFilter1];
}
return [];
},
});
const location = await definition.getLocation({
dashboardId: 'dashboard1',
filters: [appliedFilter],
preserveSavedFilters: false,
});
expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1'));
expect(location.path).toEqual(expect.stringContaining('query:appliedfilter'));
});
});
});

View file

@ -0,0 +1,160 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { SerializableState } from 'src/plugins/kibana_utils/common';
import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public';
import type { LocatorDefinition, LocatorPublic } from '../../share/public';
import type { SavedDashboardPanel } from '../common/types';
import { esFilters } from '../../data/public';
import { setStateToKbnUrl } from '../../kibana_utils/public';
import { ViewMode } from '../../embeddable/public';
import { DashboardConstants } from './dashboard_constants';
const cleanEmptyKeys = (stateObj: Record<string, unknown>) => {
Object.keys(stateObj).forEach((key) => {
if (stateObj[key] === undefined) {
delete stateObj[key];
}
});
return stateObj;
};
export const DASHBOARD_APP_LOCATOR = 'DASHBOARD_APP_LOCATOR';
export interface DashboardAppLocatorParams extends SerializableState {
/**
* If given, the dashboard saved object with this id will be loaded. If not given,
* a new, unsaved dashboard will be loaded up.
*/
dashboardId?: string;
/**
* Optionally set the time range in the time picker.
*/
timeRange?: TimeRange;
/**
* Optionally set the refresh interval.
*/
refreshInterval?: RefreshInterval & SerializableState;
/**
* Optionally apply filers. NOTE: if given and used in conjunction with `dashboardId`, and the
* saved dashboard has filters saved with it, this will _replace_ those filters.
*/
filters?: Filter[];
/**
* Optionally set a query. NOTE: if given and used in conjunction with `dashboardId`, and the
* saved dashboard has a query saved with it, this will _replace_ that query.
*/
query?: Query;
/**
* If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines
* whether to hash the data in the url to avoid url length issues.
*/
useHash?: boolean;
/**
* When `true` filters from saved filters from destination dashboard as merged with applied filters
* When `false` applied filters take precedence and override saved filters
*
* true is default
*/
preserveSavedFilters?: boolean;
/**
* View mode of the dashboard.
*/
viewMode?: ViewMode;
/**
* Search search session ID to restore.
* (Background search)
*/
searchSessionId?: string;
/**
* List of dashboard panels
*/
panels?: SavedDashboardPanel[] & SerializableState;
/**
* Saved query ID
*/
savedQuery?: string;
}
export type DashboardAppLocator = LocatorPublic<DashboardAppLocatorParams>;
export interface DashboardAppLocatorDependencies {
useHashedUrl: boolean;
getDashboardFilterFields: (dashboardId: string) => Promise<Filter[]>;
}
export class DashboardAppLocatorDefinition implements LocatorDefinition<DashboardAppLocatorParams> {
public readonly id = DASHBOARD_APP_LOCATOR;
constructor(protected readonly deps: DashboardAppLocatorDependencies) {}
public readonly getLocation = async (params: DashboardAppLocatorParams) => {
const useHash = params.useHash ?? this.deps.useHashedUrl;
const hash = params.dashboardId ? `view/${params.dashboardId}` : `create`;
const getSavedFiltersFromDestinationDashboardIfNeeded = async (): Promise<Filter[]> => {
if (params.preserveSavedFilters === false) return [];
if (!params.dashboardId) return [];
try {
return await this.deps.getDashboardFilterFields(params.dashboardId);
} catch (e) {
// In case dashboard is missing, build the url without those filters.
// The Dashboard app will handle redirect to landing page with a toast message.
return [];
}
};
// leave filters `undefined` if no filters was applied
// in this case dashboard will restore saved filters on its own
const filters = params.filters && [
...(await getSavedFiltersFromDestinationDashboardIfNeeded()),
...params.filters,
];
let path = setStateToKbnUrl(
'_a',
cleanEmptyKeys({
query: params.query,
filters: filters?.filter((f) => !esFilters.isFilterPinned(f)),
viewMode: params.viewMode,
panels: params.panels,
savedQuery: params.savedQuery,
}),
{ useHash },
`#/${hash}`
);
path = setStateToKbnUrl<QueryState>(
'_g',
cleanEmptyKeys({
time: params.timeRange,
filters: filters?.filter((f) => esFilters.isFilterPinned(f)),
refreshInterval: params.refreshInterval,
}),
{ useHash },
path
);
if (params.searchSessionId) {
path = `${path}&${DashboardConstants.SEARCH_SESSION_ID}=${params.searchSessionId}`;
}
return {
app: DashboardConstants.DASHBOARDS_ID,
path,
state: {},
};
};
}

View file

@ -72,6 +72,7 @@ import {
DASHBOARD_APP_URL_GENERATOR,
DashboardUrlGeneratorState,
} from './url_generator';
import { DashboardAppLocatorDefinition, DashboardAppLocator } from './locator';
import { createSavedDashboardLoader } from './saved_dashboards';
import { DashboardConstants } from './dashboard_constants';
import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder';
@ -121,14 +122,25 @@ export interface DashboardStartDependencies {
visualizations: VisualizationsStart;
}
export type DashboardSetup = void;
export interface DashboardSetup {
locator?: DashboardAppLocator;
}
export interface DashboardStart {
getSavedDashboardLoader: () => SavedObjectLoader;
getDashboardContainerByValueRenderer: () => ReturnType<
typeof createDashboardContainerByValueRenderer
>;
/**
* @deprecated Use dashboard locator instead. Dashboard locator is available
* under `.locator` key. This dashboard URL generator will be removed soon.
*
* ```ts
* plugins.dashboard.locator.getLocation({ ... });
* ```
*/
dashboardUrlGenerator?: DashboardUrlGenerator;
locator?: DashboardAppLocator;
dashboardFeatureFlagConfig: DashboardFeatureFlagConfig;
}
@ -142,7 +154,11 @@ export class DashboardPlugin
private currentHistory: ScopedHistory | undefined = undefined;
private dashboardFeatureFlagConfig?: DashboardFeatureFlagConfig;
/**
* @deprecated Use locator instead.
*/
private dashboardUrlGenerator?: DashboardUrlGenerator;
private locator?: DashboardAppLocator;
public setup(
core: CoreSetup<DashboardStartDependencies, DashboardStart>,
@ -205,6 +221,19 @@ export class DashboardPlugin
};
};
if (share) {
this.locator = share.url.locators.create(
new DashboardAppLocatorDefinition({
useHashedUrl: core.uiSettings.get('state:storeInSessionStorage'),
getDashboardFilterFields: async (dashboardId: string) => {
const [, , selfStart] = await core.getStartServices();
const dashboard = await selfStart.getSavedDashboardLoader().get(dashboardId);
return dashboard?.searchSource?.getField('filter') ?? [];
},
})
);
}
const {
appMounted,
appUnMounted,
@ -333,6 +362,10 @@ export class DashboardPlugin
order: 100,
});
}
return {
locator: this.locator,
};
}
public start(core: CoreStart, plugins: DashboardStartDependencies): DashboardStart {
@ -417,6 +450,7 @@ export class DashboardPlugin
});
},
dashboardUrlGenerator: this.dashboardUrlGenerator,
locator: this.locator,
dashboardFeatureFlagConfig: this.dashboardFeatureFlagConfig!,
};
}

View file

@ -26,6 +26,9 @@ export const GLOBAL_STATE_STORAGE_KEY = '_g';
export const DASHBOARD_APP_URL_GENERATOR = 'DASHBOARD_APP_URL_GENERATOR';
/**
* @deprecated Use dashboard locator instead.
*/
export interface DashboardUrlGeneratorState {
/**
* If given, the dashboard saved object with this id will be loaded. If not given,
@ -88,6 +91,9 @@ export interface DashboardUrlGeneratorState {
savedQuery?: string;
}
/**
* @deprecated Use dashboard locator instead.
*/
export const createDashboardUrlGenerator = (
getStartServices: () => Promise<{
appBasePath: string;

View file

@ -19,7 +19,6 @@
"dashboardEnhanced",
"embeddable",
"kibanaUtils",
"kibanaReact",
"share"
"kibanaReact"
]
}

View file

@ -10,7 +10,7 @@ import {
DashboardEnhancedAbstractDashboardDrilldownConfig as Config,
} from '../../../../../plugins/dashboard_enhanced/public';
import { SAMPLE_APP1_CLICK_TRIGGER, SampleApp1ClickContext } from '../../triggers';
import { KibanaURL } from '../../../../../../src/plugins/share/public';
import { KibanaLocation } from '../../../../../../src/plugins/share/public';
export const APP1_TO_DASHBOARD_DRILLDOWN = 'APP1_TO_DASHBOARD_DRILLDOWN';
@ -21,12 +21,11 @@ export class App1ToDashboardDrilldown extends AbstractDashboardDrilldown<Context
public readonly supportedTriggers = () => [SAMPLE_APP1_CLICK_TRIGGER];
protected async getURL(config: Config, context: Context): Promise<KibanaURL> {
const path = await this.urlGenerator.createUrl({
protected async getLocation(config: Config, context: Context): Promise<KibanaLocation> {
const location = await this.locator.getLocation({
dashboardId: config.dashboardId,
});
const url = new KibanaURL(path);
return url;
return location;
}
}

View file

@ -10,7 +10,7 @@ import {
DashboardEnhancedAbstractDashboardDrilldownConfig as Config,
} from '../../../../../plugins/dashboard_enhanced/public';
import { SAMPLE_APP2_CLICK_TRIGGER, SampleApp2ClickContext } from '../../triggers';
import { KibanaURL } from '../../../../../../src/plugins/share/public';
import { KibanaLocation } from '../../../../../../src/plugins/share/public';
export const APP2_TO_DASHBOARD_DRILLDOWN = 'APP2_TO_DASHBOARD_DRILLDOWN';
@ -21,12 +21,11 @@ export class App2ToDashboardDrilldown extends AbstractDashboardDrilldown<Context
public readonly supportedTriggers = () => [SAMPLE_APP2_CLICK_TRIGGER];
protected async getURL(config: Config, context: Context): Promise<KibanaURL> {
const path = await this.urlGenerator.createUrl({
protected async getLocation(config: Config, context: Context): Promise<KibanaLocation> {
const location = await this.locator.getLocation({
dashboardId: config.dashboardId,
});
const url = new KibanaURL(path);
return url;
return location;
}
}

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import type { KibanaLocation } from 'src/plugins/share/public';
import React from 'react';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { DashboardStart } from 'src/plugins/dashboard/public';
@ -20,7 +21,6 @@ import {
CollectConfigProps,
StartServicesGetter,
} from '../../../../../../../src/plugins/kibana_utils/public';
import { KibanaURL } from '../../../../../../../src/plugins/share/public';
import { Config } from './types';
export interface Params {
@ -39,7 +39,7 @@ export abstract class AbstractDashboardDrilldown<Context extends object = object
public abstract readonly supportedTriggers: () => string[];
protected abstract getURL(config: Config, context: Context): Promise<KibanaURL>;
protected abstract getLocation(config: Config, context: Context): Promise<KibanaLocation>;
public readonly order = 100;
@ -65,19 +65,25 @@ export abstract class AbstractDashboardDrilldown<Context extends object = object
};
public readonly getHref = async (config: Config, context: Context): Promise<string> => {
const url = await this.getURL(config, context);
return url.path;
const { app, path } = await this.getLocation(config, context);
const url = await this.params.start().core.application.getUrlForApp(app, {
path,
absolute: true,
});
return url;
};
public readonly execute = async (config: Config, context: Context) => {
const url = await this.getURL(config, context);
await this.params.start().core.application.navigateToApp(url.appName, { path: url.appPath });
const { app, path, state } = await this.getLocation(config, context);
await this.params.start().core.application.navigateToApp(app, {
path,
state,
});
};
protected get urlGenerator() {
const urlGenerator = this.params.start().plugins.dashboard.dashboardUrlGenerator;
if (!urlGenerator)
throw new Error('Dashboard URL generator is required for dashboard drilldown.');
return urlGenerator;
protected get locator() {
const locator = this.params.start().plugins.dashboard.locator;
if (!locator) throw new Error('Dashboard locator is required for dashboard drilldown.');
return locator;
}
}

View file

@ -7,7 +7,7 @@
import { EmbeddableToDashboardDrilldown } from './embeddable_to_dashboard_drilldown';
import { AbstractDashboardDrilldownConfig as Config } from '../abstract_dashboard_drilldown';
import { coreMock, savedObjectsServiceMock } from '../../../../../../../src/core/public/mocks';
import { savedObjectsServiceMock } from '../../../../../../../src/core/public/mocks';
import {
Filter,
FilterStateStore,
@ -19,10 +19,11 @@ import {
ApplyGlobalFilterActionContext,
esFilters,
} from '../../../../../../../src/plugins/data/public';
import { createDashboardUrlGenerator } from '../../../../../../../src/plugins/dashboard/public/url_generator';
import { UrlGeneratorsService } from '../../../../../../../src/plugins/share/public/url_generators';
import {
DashboardAppLocatorDefinition,
DashboardAppLocatorParams,
} from '../../../../../../../src/plugins/dashboard/public/locator';
import { StartDependencies } from '../../../plugin';
import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public';
import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_utils/public/core';
import { EnhancedEmbeddableContext } from '../../../../../embeddable_enhanced/public';
@ -74,13 +75,6 @@ test('inject/extract are defined', () => {
});
describe('.execute() & getHref', () => {
/**
* A convenience test setup helper
* Beware: `dataPluginMock.createStartContract().actions` and extracting filters from event is mocked!
* The url generation is not mocked and uses real implementation
* So this tests are mostly focused on making sure the filters returned from `dataPluginMock.createStartContract().actions` helpers
* end up in resulting navigation path
*/
async function setupTestBed(
config: Partial<Config>,
embeddableInput: { filters?: Filter[]; timeRange?: TimeRange; query?: Query },
@ -90,7 +84,10 @@ describe('.execute() & getHref', () => {
const navigateToApp = jest.fn();
const getUrlForApp = jest.fn((app, opt) => `${app}/${opt.path}`);
const savedObjectsClient = savedObjectsServiceMock.createStartContract().client;
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: false,
getDashboardFilterFields: async () => [],
});
const drilldown = new EmbeddableToDashboardDrilldown({
start: ((() => ({
core: {
@ -105,17 +102,11 @@ describe('.execute() & getHref', () => {
plugins: {
uiActionsEnhanced: {},
dashboard: {
dashboardUrlGenerator: new UrlGeneratorsService()
.setup(coreMock.createSetup())
.registerUrlGenerator(
createDashboardUrlGenerator(() =>
Promise.resolve({
appBasePath: 'xyz/app/dashboards',
useHashedUrl: false,
savedDashboardLoader: ({} as unknown) as SavedObjectLoader,
})
)
),
locator: {
getLocation: async (params: DashboardAppLocatorParams) => {
return await definition.getLocation(params);
},
},
},
},
self: {},

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { DashboardUrlGeneratorState } from '../../../../../../../src/plugins/dashboard/public';
import type { KibanaLocation } from 'src/plugins/share/public';
import { DashboardAppLocatorParams } from '../../../../../../../src/plugins/dashboard/public';
import {
ApplyGlobalFilterActionContext,
APPLY_FILTER_TRIGGER,
@ -23,7 +24,6 @@ import {
AbstractDashboardDrilldownParams,
AbstractDashboardDrilldownConfig as Config,
} from '../abstract_dashboard_drilldown';
import { KibanaURL } from '../../../../../../../src/plugins/share/public';
import { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants';
import { createExtract, createInject } from '../../../../common';
import { EnhancedEmbeddableContext } from '../../../../../embeddable_enhanced/public';
@ -49,26 +49,26 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown<C
public readonly supportedTriggers = () => [APPLY_FILTER_TRIGGER];
protected async getURL(config: Config, context: Context): Promise<KibanaURL> {
const state: DashboardUrlGeneratorState = {
protected async getLocation(config: Config, context: Context): Promise<KibanaLocation> {
const params: DashboardAppLocatorParams = {
dashboardId: config.dashboardId,
};
if (context.embeddable) {
const embeddable = context.embeddable as IEmbeddable<EmbeddableQueryInput>;
const input = embeddable.getInput();
if (isQuery(input.query) && config.useCurrentFilters) state.query = input.query;
if (isQuery(input.query) && config.useCurrentFilters) params.query = input.query;
// if useCurrentDashboardDataRange is enabled, then preserve current time range
// if undefined is passed, then destination dashboard will figure out time range itself
// for brush event this time range would be overwritten
if (isTimeRange(input.timeRange) && config.useCurrentDateRange)
state.timeRange = input.timeRange;
params.timeRange = input.timeRange;
// if useCurrentDashboardFilters enabled, then preserve all the filters (pinned and unpinned)
// otherwise preserve only pinned
if (isFilters(input.filters))
state.filters = config.useCurrentFilters
params.filters = config.useCurrentFilters
? input.filters
: input.filters?.filter((f) => esFilters.isFilterPinned(f));
}
@ -79,17 +79,16 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown<C
} = esFilters.extractTimeRange(context.filters, context.timeFieldName);
if (filtersFromEvent) {
state.filters = [...(state.filters ?? []), ...filtersFromEvent];
params.filters = [...(params.filters ?? []), ...filtersFromEvent];
}
if (timeRangeFromEvent) {
state.timeRange = timeRangeFromEvent;
params.timeRange = timeRangeFromEvent;
}
const path = await this.urlGenerator.createUrl(state);
const url = new KibanaURL(path);
const location = await this.locator.getLocation(params);
return url;
return location;
}
public readonly inject = createInject({ drilldownId: this.id });