Discover locator (#102712)

* Add Discover locator

* Add Discover locator tests

* Expose locator for Discover app and deprecate URL generator

* Use Discover locator in Explore Underlying Data

* Fix explore data unit tests after refactor

* fix: 🐛 update Discover plugin mock

* style: 💄 remove any

* test: 💍 fix test mock

* fix: 🐛 adjust property name after refactor

* test: 💍 fix tests after refactor

Co-authored-by: Vadim Kibana <vadimkibana@gmail.com>
This commit is contained in:
Vadim Dalecky 2021-06-23 13:25:37 +02:00 committed by GitHub
parent 868ae59c93
commit 1386c330fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 589 additions and 91 deletions

View file

@ -17,4 +17,6 @@ export function plugin(initializerContext: PluginInitializerContext) {
export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches';
export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable';
export { loadSharingDataHelpers } from './shared';
export { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from './url_generator';
export { DiscoverAppLocator, DiscoverAppLocatorParams } from './locator';

View file

@ -0,0 +1,270 @@
/*
* 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 { hashedItemStore, getStatesFromKbnUrl } from '../../kibana_utils/public';
import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock';
import { FilterStateStore } from '../../data/common';
import { DiscoverAppLocatorDefinition } from './locator';
import { SerializableState } from 'src/plugins/kibana_utils/common';
const indexPatternId: string = 'c367b774-a4c2-11ea-bb37-0242ac130002';
const savedSearchId: string = '571aaf70-4c88-11e8-b3d7-01146121b73d';
interface SetupParams {
useHash?: boolean;
}
const setup = async ({ useHash = false }: SetupParams = {}) => {
const locator = new DiscoverAppLocatorDefinition({
useHash,
});
return {
locator,
};
};
beforeEach(() => {
// @ts-expect-error
hashedItemStore.storage = mockStorage;
});
describe('Discover url generator', () => {
test('can create a link to Discover with no state and no saved search', async () => {
const { locator } = await setup();
const { app, path } = await locator.getLocation({});
const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
expect(app).toBe('discover');
expect(_a).toEqual({});
expect(_g).toEqual({});
});
test('can create a link to a saved search in Discover', async () => {
const { locator } = await setup();
const { path } = await locator.getLocation({ savedSearchId });
const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
expect(path.startsWith(`#/view/${savedSearchId}`)).toBe(true);
expect(_a).toEqual({});
expect(_g).toEqual({});
});
test('can specify specific index pattern', async () => {
const { locator } = await setup();
const { path } = await locator.getLocation({
indexPatternId,
});
const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
expect(_a).toEqual({
index: indexPatternId,
});
expect(_g).toEqual({});
});
test('can specify specific time range', async () => {
const { locator } = await setup();
const { path } = await locator.getLocation({
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
});
const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
expect(_a).toEqual({});
expect(_g).toEqual({
time: {
from: 'now-15m',
mode: 'relative',
to: 'now',
},
});
});
test('can specify query', async () => {
const { locator } = await setup();
const { path } = await locator.getLocation({
query: {
language: 'kuery',
query: 'foo',
},
});
const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
expect(_a).toEqual({
query: {
language: 'kuery',
query: 'foo',
},
});
expect(_g).toEqual({});
});
test('can specify local and global filters', async () => {
const { locator } = await setup();
const { path } = await locator.getLocation({
filters: [
{
meta: {
alias: 'foo',
disabled: false,
negate: false,
},
$state: {
store: FilterStateStore.APP_STATE,
},
},
{
meta: {
alias: 'bar',
disabled: false,
negate: false,
},
$state: {
store: FilterStateStore.GLOBAL_STATE,
},
},
],
});
const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
expect(_a).toEqual({
filters: [
{
$state: {
store: 'appState',
},
meta: {
alias: 'foo',
disabled: false,
negate: false,
},
},
],
});
expect(_g).toEqual({
filters: [
{
$state: {
store: 'globalState',
},
meta: {
alias: 'bar',
disabled: false,
negate: false,
},
},
],
});
});
test('can set refresh interval', async () => {
const { locator } = await setup();
const { path } = await locator.getLocation({
refreshInterval: {
pause: false,
value: 666,
},
});
const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
expect(_a).toEqual({});
expect(_g).toEqual({
refreshInterval: {
pause: false,
value: 666,
},
});
});
test('can set time range', async () => {
const { locator } = await setup();
const { path } = await locator.getLocation({
timeRange: {
from: 'now-3h',
to: 'now',
},
});
const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
expect(_a).toEqual({});
expect(_g).toEqual({
time: {
from: 'now-3h',
to: 'now',
},
});
});
test('can specify a search session id', async () => {
const { locator } = await setup();
const { path } = await locator.getLocation({
searchSessionId: '__test__',
});
expect(path).toMatchInlineSnapshot(`"#/?_g=()&_a=()&searchSessionId=__test__"`);
expect(path).toContain('__test__');
});
test('can specify columns, interval, sort and savedQuery', async () => {
const { locator } = await setup();
const { path } = await locator.getLocation({
columns: ['_source'],
interval: 'auto',
sort: [['timestamp, asc']] as string[][] & SerializableState,
savedQuery: '__savedQueryId__',
});
expect(path).toMatchInlineSnapshot(
`"#/?_g=()&_a=(columns:!(_source),interval:auto,savedQuery:__savedQueryId__,sort:!(!('timestamp,%20asc')))"`
);
});
describe('useHash property', () => {
describe('when default useHash is set to false', () => {
test('when using default, sets index pattern ID in the generated URL', async () => {
const { locator } = await setup();
const { path } = await locator.getLocation({
indexPatternId,
});
expect(path.indexOf(indexPatternId) > -1).toBe(true);
});
test('when enabling useHash, does not set index pattern ID in the generated URL', async () => {
const { locator } = await setup();
const { path } = await locator.getLocation({
useHash: true,
indexPatternId,
});
expect(path.indexOf(indexPatternId) > -1).toBe(false);
});
});
describe('when default useHash is set to true', () => {
test('when using default, does not set index pattern ID in the generated URL', async () => {
const { locator } = await setup({ useHash: true });
const { path } = await locator.getLocation({
indexPatternId,
});
expect(path.indexOf(indexPatternId) > -1).toBe(false);
});
test('when disabling useHash, sets index pattern ID in the generated URL', async () => {
const { locator } = await setup({ useHash: true });
const { path } = await locator.getLocation({
useHash: false,
indexPatternId,
});
expect(path.indexOf(indexPatternId) > -1).toBe(true);
});
});
});
});

View file

@ -0,0 +1,146 @@
/*
* 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 { esFilters } from '../../data/public';
import { setStateToKbnUrl } from '../../kibana_utils/public';
export const DISCOVER_APP_LOCATOR = 'DISCOVER_APP_LOCATOR';
export interface DiscoverAppLocatorParams extends SerializableState {
/**
* Optionally set saved search ID.
*/
savedSearchId?: string;
/**
* Optionally set index pattern ID.
*/
indexPatternId?: string;
/**
* Optionally set the time range in the time picker.
*/
timeRange?: TimeRange;
/**
* Optionally set the refresh interval.
*/
refreshInterval?: RefreshInterval & SerializableState;
/**
* Optionally apply filters.
*/
filters?: Filter[];
/**
* Optionally set a 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;
/**
* Background search session id
*/
searchSessionId?: string;
/**
* Columns displayed in the table
*/
columns?: string[];
/**
* Used interval of the histogram
*/
interval?: string;
/**
* Array of the used sorting [[field,direction],...]
*/
sort?: string[][] & SerializableState;
/**
* id of the used saved query
*/
savedQuery?: string;
}
export type DiscoverAppLocator = LocatorPublic<DiscoverAppLocatorParams>;
export interface DiscoverAppLocatorDependencies {
useHash: boolean;
}
export class DiscoverAppLocatorDefinition implements LocatorDefinition<DiscoverAppLocatorParams> {
public readonly id = DISCOVER_APP_LOCATOR;
constructor(protected readonly deps: DiscoverAppLocatorDependencies) {}
public readonly getLocation = async (params: DiscoverAppLocatorParams) => {
const {
useHash = this.deps.useHash,
filters,
indexPatternId,
query,
refreshInterval,
savedSearchId,
timeRange,
searchSessionId,
columns,
savedQuery,
sort,
interval,
} = params;
const savedSearchPath = savedSearchId ? `view/${encodeURIComponent(savedSearchId)}` : '';
const appState: {
query?: Query;
filters?: Filter[];
index?: string;
columns?: string[];
interval?: string;
sort?: string[][];
savedQuery?: string;
} = {};
const queryState: QueryState = {};
if (query) appState.query = query;
if (filters && filters.length)
appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f));
if (indexPatternId) appState.index = indexPatternId;
if (columns) appState.columns = columns;
if (savedQuery) appState.savedQuery = savedQuery;
if (sort) appState.sort = sort;
if (interval) appState.interval = interval;
if (timeRange) queryState.time = timeRange;
if (filters && filters.length)
queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f));
if (refreshInterval) queryState.refreshInterval = refreshInterval;
let path = `#/${savedSearchPath}`;
path = setStateToKbnUrl<QueryState>('_g', queryState, { useHash }, path);
path = setStateToKbnUrl('_a', appState, { useHash }, path);
if (searchSessionId) {
path = `${path}&searchSessionId=${searchSessionId}`;
}
return {
app: 'discover',
path,
state: {},
};
};
}

View file

@ -16,6 +16,12 @@ const createSetupContract = (): Setup => {
docViews: {
addDocView: jest.fn(),
},
locator: {
getLocation: jest.fn(),
getUrl: jest.fn(),
useUrl: jest.fn(),
navigate: jest.fn(),
},
};
return setupContract;
};
@ -26,6 +32,12 @@ const createStartContract = (): Start => {
urlGenerator: ({
createUrl: jest.fn(),
} as unknown) as DiscoverStart['urlGenerator'],
locator: {
getLocation: jest.fn(),
getUrl: jest.fn(),
useUrl: jest.fn(),
navigate: jest.fn(),
},
};
return startContract;
};

View file

@ -59,6 +59,7 @@ import {
DiscoverUrlGenerator,
SEARCH_SESSION_ID_QUERY_PARAM,
} from './url_generator';
import { DiscoverAppLocatorDefinition, DiscoverAppLocator } from './locator';
import { SearchEmbeddableFactory } from './application/embeddable';
import { UsageCollectionSetup } from '../../usage_collection/public';
import { replaceUrlHashQuery } from '../../kibana_utils/public/';
@ -83,17 +84,27 @@ export interface DiscoverSetup {
*/
addDocView(docViewRaw: DocViewInput | DocViewInputFn): void;
};
}
export interface DiscoverStart {
savedSearchLoader: SavedObjectLoader;
/**
* `share` plugin URL generator for Discover app. Use it to generate links into
* Discover application, example:
* `share` plugin URL locator for Discover app. Use it to generate links into
* Discover application, for example, navigate:
*
* ```ts
* const url = await plugins.discover.urlGenerator.createUrl({
* await plugins.discover.locator.navigate({
* savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
* indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
* timeRange: {
* to: 'now',
* from: 'now-15m',
* mode: 'relative',
* },
* });
* ```
*
* Generate a location:
*
* ```ts
* const location = await plugins.discover.locator.getLocation({
* savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
* indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
* timeRange: {
@ -104,7 +115,48 @@ export interface DiscoverStart {
* });
* ```
*/
readonly locator: undefined | DiscoverAppLocator;
}
export interface DiscoverStart {
savedSearchLoader: SavedObjectLoader;
/**
* @deprecated Use URL locator instead. URL generaotr will be removed.
*/
readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>;
/**
* `share` plugin URL locator for Discover app. Use it to generate links into
* Discover application, for example, navigate:
*
* ```ts
* await plugins.discover.locator.navigate({
* savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
* indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
* timeRange: {
* to: 'now',
* from: 'now-15m',
* mode: 'relative',
* },
* });
* ```
*
* Generate a location:
*
* ```ts
* const location = await plugins.discover.locator.getLocation({
* savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
* indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
* timeRange: {
* to: 'now',
* from: 'now-15m',
* mode: 'relative',
* },
* });
* ```
*/
readonly locator: undefined | DiscoverAppLocator;
}
/**
@ -156,7 +208,12 @@ export class DiscoverPlugin
private stopUrlTracking: (() => void) | undefined = undefined;
private servicesInitialized: boolean = false;
private innerAngularInitialized: boolean = false;
/**
* @deprecated
*/
private urlGenerator?: DiscoverStart['urlGenerator'];
private locator?: DiscoverAppLocator;
/**
* why are those functions public? they are needed for some mocha tests
@ -179,6 +236,15 @@ export class DiscoverPlugin
})
);
}
if (plugins.share) {
this.locator = plugins.share.url.locators.create(
new DiscoverAppLocatorDefinition({
useHash: core.uiSettings.get('state:storeInSessionStorage'),
})
);
}
this.docViewsRegistry = new DocViewsRegistry();
setDocViewsRegistry(this.docViewsRegistry);
this.docViewsRegistry.addDocView({
@ -323,6 +389,7 @@ export class DiscoverPlugin
docViews: {
addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry),
},
locator: this.locator,
};
}
@ -367,6 +434,7 @@ export class DiscoverPlugin
return {
urlGenerator: this.urlGenerator,
locator: this.locator,
savedSearchLoader: createSavedSearchesLoader({
savedObjectsClient: core.savedObjects.client,
savedObjects: plugins.savedObjects,

View file

@ -7,5 +7,5 @@
"requiredPlugins": ["uiActions", "embeddable", "discover"],
"optionalPlugins": ["share", "kibanaLegacy", "usageCollection"],
"configPath": ["xpack", "discoverEnhanced"],
"requiredBundles": ["kibanaUtils", "data", "share"]
"requiredBundles": ["kibanaUtils", "data"]
}

View file

@ -11,13 +11,13 @@ import { ViewMode, IEmbeddable } from '../../../../../../src/plugins/embeddable/
import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public';
import { KibanaLegacyStart } from '../../../../../../src/plugins/kibana_legacy/public';
import { CoreStart } from '../../../../../../src/core/public';
import { KibanaURL } from '../../../../../../src/plugins/share/public';
import { KibanaLocation } from '../../../../../../src/plugins/share/public';
import * as shared from './shared';
export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA';
export interface PluginDeps {
discover: Pick<DiscoverStart, 'urlGenerator'>;
discover: Pick<DiscoverStart, 'locator'>;
kibanaLegacy?: {
dashboardConfig: {
getHideWriteControls: KibanaLegacyStart['dashboardConfig']['getHideWriteControls'];
@ -26,7 +26,7 @@ export interface PluginDeps {
}
export interface CoreDeps {
application: Pick<CoreStart['application'], 'navigateToApp'>;
application: Pick<CoreStart['application'], 'navigateToApp' | 'getUrlForApp'>;
}
export interface Params {
@ -43,7 +43,7 @@ export abstract class AbstractExploreDataAction<Context extends { embeddable?: I
constructor(protected readonly params: Params) {}
protected abstract getUrl(context: Context): Promise<KibanaURL>;
protected abstract getLocation(context: Context): Promise<KibanaLocation>;
public async isCompatible({ embeddable }: Context): Promise<boolean> {
if (!embeddable) return false;
@ -52,7 +52,7 @@ export abstract class AbstractExploreDataAction<Context extends { embeddable?: I
const { capabilities } = core.application;
if (capabilities.discover && !capabilities.discover.show) return false;
if (!plugins.discover.urlGenerator) return false;
if (!plugins.discover.locator) return false;
const isDashboardOnlyMode = !!this.params
.start()
.plugins.kibanaLegacy?.dashboardConfig.getHideWriteControls();
@ -68,10 +68,10 @@ export abstract class AbstractExploreDataAction<Context extends { embeddable?: I
if (!shared.hasExactlyOneIndexPattern(context.embeddable)) return;
const { core } = this.params.start();
const { appName, appPath } = await this.getUrl(context);
const { app, path } = await this.getLocation(context);
await core.application.navigateToApp(appName, {
path: appPath,
await core.application.navigateToApp(app, {
path,
});
}
@ -82,8 +82,10 @@ export abstract class AbstractExploreDataAction<Context extends { embeddable?: I
throw new Error(`Embeddable not supported for "${this.getDisplayName(context)}" action.`);
}
const { path } = await this.getUrl(context);
const { core } = this.params.start();
const { app, path } = await this.getLocation(context);
const url = await core.application.getUrlForApp(app, { path, absolute: false });
return path;
return url;
}
}

View file

@ -8,7 +8,6 @@
import { ExploreDataChartAction } from './explore_data_chart_action';
import { Params, PluginDeps } from './abstract_explore_data_action';
import { coreMock } from '../../../../../../src/core/public/mocks';
import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public';
import { ExploreDataChartActionContext } from './explore_data_chart_action';
import { i18n } from '@kbn/i18n';
import {
@ -17,6 +16,7 @@ import {
} from '../../../../../../src/plugins/visualizations/public';
import { ViewMode } from '../../../../../../src/plugins/embeddable/public';
import { Filter, RangeFilter } from '../../../../../../src/plugins/data/public';
import { DiscoverAppLocator } from '../../../../../../src/plugins/discover/public';
const i18nTranslateSpy = (i18n.translate as unknown) as jest.SpyInstance;
@ -43,17 +43,23 @@ const setup = (
dashboardOnlyMode?: boolean;
} = { filters: [] }
) => {
type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>;
const core = coreMock.createStart();
const urlGenerator: UrlGenerator = ({
createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')),
} as unknown) as UrlGenerator;
const locator: DiscoverAppLocator = {
getLocation: jest.fn(() =>
Promise.resolve({
app: 'discover',
path: '/foo#bar',
state: {},
})
),
navigate: jest.fn(async () => {}),
getUrl: jest.fn(),
useUrl: jest.fn(),
};
const plugins: PluginDeps = {
discover: {
urlGenerator,
locator,
},
kibanaLegacy: {
dashboardConfig: {
@ -95,7 +101,7 @@ const setup = (
embeddable,
} as ExploreDataChartActionContext;
return { core, plugins, urlGenerator, params, action, input, output, embeddable, context };
return { core, plugins, locator, params, action, input, output, embeddable, context };
};
describe('"Explore underlying data" panel action', () => {
@ -132,7 +138,7 @@ describe('"Explore underlying data" panel action', () => {
test('returns false when URL generator is not present', async () => {
const { action, plugins, context } = setup();
(plugins.discover as any).urlGenerator = undefined;
(plugins.discover as any).locator = undefined;
const isCompatible = await action.isCompatible(context);
@ -205,23 +211,15 @@ describe('"Explore underlying data" panel action', () => {
});
describe('getHref()', () => {
test('returns URL path generated by URL generator', async () => {
const { action, context } = setup();
const href = await action.getHref(context);
expect(href).toBe('/xyz/app/discover/foo#bar');
});
test('calls URL generator with right arguments', async () => {
const { action, urlGenerator, context } = setup();
const { action, locator, context } = setup();
expect(urlGenerator.createUrl).toHaveBeenCalledTimes(0);
expect(locator.getLocation).toHaveBeenCalledTimes(0);
await action.getHref(context);
expect(urlGenerator.createUrl).toHaveBeenCalledTimes(1);
expect(urlGenerator.createUrl).toHaveBeenCalledWith({
expect(locator.getLocation).toHaveBeenCalledTimes(1);
expect(locator.getLocation).toHaveBeenCalledWith({
filters: [],
indexPatternId: 'index-ptr-foo',
timeRange: undefined,
@ -260,11 +258,11 @@ describe('"Explore underlying data" panel action', () => {
},
];
const { action, context, urlGenerator } = setup({ filters, timeFieldName });
const { action, context, locator } = setup({ filters, timeFieldName });
await action.getHref(context);
expect(urlGenerator.createUrl).toHaveBeenCalledWith({
expect(locator.getLocation).toHaveBeenCalledWith({
filters: [
{
meta: {

View file

@ -7,7 +7,7 @@
import { Action } from '../../../../../../src/plugins/ui_actions/public';
import {
DiscoverUrlGeneratorState,
DiscoverAppLocatorParams,
SearchInput,
} from '../../../../../../src/plugins/discover/public';
import {
@ -15,7 +15,7 @@ import {
esFilters,
} from '../../../../../../src/plugins/data/public';
import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public';
import { KibanaURL } from '../../../../../../src/plugins/share/public';
import { KibanaLocation } from '../../../../../../src/plugins/share/public';
import * as shared from './shared';
import { AbstractExploreDataAction } from './abstract_explore_data_action';
@ -43,14 +43,14 @@ export class ExploreDataChartAction
return super.isCompatible(context);
}
protected readonly getUrl = async (
protected readonly getLocation = async (
context: ExploreDataChartActionContext
): Promise<KibanaURL> => {
): Promise<KibanaLocation> => {
const { plugins } = this.params.start();
const { urlGenerator } = plugins.discover;
const { locator } = plugins.discover;
if (!urlGenerator) {
throw new Error('Discover URL generator not available.');
if (!locator) {
throw new Error('Discover URL locator not available.');
}
const { embeddable } = context;
@ -59,23 +59,23 @@ export class ExploreDataChartAction
context.timeFieldName
);
const state: DiscoverUrlGeneratorState = {
const params: DiscoverAppLocatorParams = {
filters,
timeRange,
};
if (embeddable) {
state.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined;
params.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined;
const input = embeddable.getInput() as Readonly<SearchInput>;
if (input.timeRange && !state.timeRange) state.timeRange = input.timeRange;
if (input.query) state.query = input.query;
if (input.filters) state.filters = [...input.filters, ...(state.filters || [])];
if (input.timeRange && !params.timeRange) params.timeRange = input.timeRange;
if (input.query) params.query = input.query;
if (input.filters) params.filters = [...input.filters, ...(params.filters || [])];
}
const path = await urlGenerator.createUrl(state);
const location = await locator.getLocation(params);
return new KibanaURL(path);
return location;
};
}

View file

@ -8,13 +8,13 @@
import { ExploreDataContextMenuAction } from './explore_data_context_menu_action';
import { Params, PluginDeps } from './abstract_explore_data_action';
import { coreMock } from '../../../../../../src/core/public/mocks';
import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public';
import { i18n } from '@kbn/i18n';
import {
VisualizeEmbeddableContract,
VISUALIZE_EMBEDDABLE_TYPE,
} from '../../../../../../src/plugins/visualizations/public';
import { ViewMode } from '../../../../../../src/plugins/embeddable/public';
import { DiscoverAppLocator } from '../../../../../../src/plugins/discover/public';
const i18nTranslateSpy = (i18n.translate as unknown) as jest.SpyInstance;
@ -29,17 +29,23 @@ afterEach(() => {
});
const setup = ({ dashboardOnlyMode = false }: { dashboardOnlyMode?: boolean } = {}) => {
type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>;
const core = coreMock.createStart();
const urlGenerator: UrlGenerator = ({
createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')),
} as unknown) as UrlGenerator;
const locator: DiscoverAppLocator = {
getLocation: jest.fn(() =>
Promise.resolve({
app: 'discover',
path: '/foo#bar',
state: {},
})
),
navigate: jest.fn(async () => {}),
getUrl: jest.fn(),
useUrl: jest.fn(),
};
const plugins: PluginDeps = {
discover: {
urlGenerator,
locator,
},
kibanaLegacy: {
dashboardConfig: {
@ -79,7 +85,7 @@ const setup = ({ dashboardOnlyMode = false }: { dashboardOnlyMode?: boolean } =
embeddable,
};
return { core, plugins, urlGenerator, params, action, input, output, embeddable, context };
return { core, plugins, locator, params, action, input, output, embeddable, context };
};
describe('"Explore underlying data" panel action', () => {
@ -116,7 +122,7 @@ describe('"Explore underlying data" panel action', () => {
test('returns false when URL generator is not present', async () => {
const { action, plugins, context } = setup();
(plugins.discover as any).urlGenerator = undefined;
(plugins.discover as any).locator = undefined;
const isCompatible = await action.isCompatible(context);
@ -189,23 +195,15 @@ describe('"Explore underlying data" panel action', () => {
});
describe('getHref()', () => {
test('returns URL path generated by URL generator', async () => {
const { action, context } = setup();
const href = await action.getHref(context);
expect(href).toBe('/xyz/app/discover/foo#bar');
});
test('calls URL generator with right arguments', async () => {
const { action, urlGenerator, context } = setup();
const { action, locator, context } = setup();
expect(urlGenerator.createUrl).toHaveBeenCalledTimes(0);
expect(locator.getLocation).toHaveBeenCalledTimes(0);
await action.getHref(context);
expect(urlGenerator.createUrl).toHaveBeenCalledTimes(1);
expect(urlGenerator.createUrl).toHaveBeenCalledWith({
expect(locator.getLocation).toHaveBeenCalledTimes(1);
expect(locator.getLocation).toHaveBeenCalledWith({
indexPatternId: 'index-ptr-foo',
});
});

View file

@ -12,8 +12,8 @@ import {
IEmbeddable,
} from '../../../../../../src/plugins/embeddable/public';
import { Query, Filter, TimeRange } from '../../../../../../src/plugins/data/public';
import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public';
import { KibanaURL } from '../../../../../../src/plugins/share/public';
import { DiscoverAppLocatorParams } from '../../../../../../src/plugins/discover/public';
import { KibanaLocation } from '../../../../../../src/plugins/share/public';
import * as shared from './shared';
import { AbstractExploreDataAction } from './abstract_explore_data_action';
@ -40,29 +40,31 @@ export class ExploreDataContextMenuAction
public readonly order = 200;
protected readonly getUrl = async (context: EmbeddableQueryContext): Promise<KibanaURL> => {
protected readonly getLocation = async (
context: EmbeddableQueryContext
): Promise<KibanaLocation> => {
const { plugins } = this.params.start();
const { urlGenerator } = plugins.discover;
const { locator } = plugins.discover;
if (!urlGenerator) {
throw new Error('Discover URL generator not available.');
if (!locator) {
throw new Error('Discover URL locator not available.');
}
const { embeddable } = context;
const state: DiscoverUrlGeneratorState = {};
const params: DiscoverAppLocatorParams = {};
if (embeddable) {
state.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined;
params.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined;
const input = embeddable.getInput();
if (input.timeRange && !state.timeRange) state.timeRange = input.timeRange;
if (input.query) state.query = input.query;
if (input.filters) state.filters = [...input.filters, ...(state.filters || [])];
if (input.timeRange && !params.timeRange) params.timeRange = input.timeRange;
if (input.query) params.query = input.query;
if (input.filters) params.filters = [...input.filters, ...(params.filters || [])];
}
const path = await urlGenerator.createUrl(state);
const location = await locator.getLocation(params);
return new KibanaURL(path);
return location;
};
}