Discover URL generator (#67937)

* feat: 🎸 stub discover_enhanced plugin

* feat: 🎸 add Discover URL generator

* chore: 🤖 remove x-pack plugin

* fix: 🐛 fix types in URL generator

* test: 💍 setup test file for Discover URL generator

* feat: 🎸 expose Discover URL generator in start life-cycle

* feat: 🎸 add ability to specify saved search ID in URL generator

* docs: ✏️ add JSDoc for Discover URL generator

* fix: 🐛 set correctly global filters in Discover URL generator

* docs: ✏️ remove wrong comment in JsDoc

* style: 💄 format single arg arrow function with parens

* chore: 🤖 disable toggles in Dicover sample drilldown

* feat: 🎸 use Discover URL generator in example plugin

* test: 💍 add urlGenerator mock

* test: 💍 add .registerUrlGenerator() test mock

* test: 💍 correct Karma mock for "share" plugin URL generator
This commit is contained in:
Vadim Dalecky 2020-06-05 14:52:05 +02:00 committed by GitHub
parent a6e9caefe4
commit 7ea8ef1fba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 451 additions and 18 deletions

View file

@ -254,6 +254,9 @@ export const npSetup = {
},
share: {
register: () => {},
urlGenerators: {
registerUrlGenerator: () => {},
},
},
devTools: {
register: () => {},

View file

@ -1,6 +1,7 @@
{
"id": "discover",
"version": "kibana",
"optionalPlugins": ["share"],
"server": true,
"ui": true,
"requiredPlugins": [

View file

@ -27,3 +27,4 @@ export function plugin(initializerContext: PluginInitializerContext) {
export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches';
export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable';
export { DISCOVER_APP_URL_GENERATOR } from './url_generator';

View file

@ -34,6 +34,9 @@ const createSetupContract = (): Setup => {
const createStartContract = (): Start => {
const startContract: Start = {
savedSearchLoader: {} as any,
urlGenerator: {
createUrl: jest.fn(),
} as any,
};
return startContract;
};

View file

@ -34,7 +34,7 @@ import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public';
import { EmbeddableStart, EmbeddableSetup } from 'src/plugins/embeddable/public';
import { ChartsPluginStart } from 'src/plugins/charts/public';
import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public';
import { SharePluginStart } from 'src/plugins/share/public';
import { SharePluginStart, SharePluginSetup, UrlGeneratorContract } from 'src/plugins/share/public';
import { VisualizationsStart, VisualizationsSetup } from 'src/plugins/visualizations/public';
import { KibanaLegacySetup } from 'src/plugins/kibana_legacy/public';
import { HomePublicPluginSetup } from 'src/plugins/home/public';
@ -43,7 +43,7 @@ import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../d
import { SavedObjectLoader } from '../../saved_objects/public';
import { createKbnUrlTracker } from '../../kibana_utils/public';
import { DEFAULT_APP_CATEGORIES } from '../../../core/public';
import { UrlGeneratorState } from '../../share/public';
import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types';
import { DocViewsRegistry } from './application/doc_views/doc_views_registry';
import { DocViewTable } from './application/components/table/table';
@ -59,6 +59,17 @@ import {
import { createSavedSearchesLoader } from './saved_searches';
import { registerFeature } from './register_feature';
import { buildServices } from './build_services';
import {
DiscoverUrlGeneratorState,
DISCOVER_APP_URL_GENERATOR,
DiscoverUrlGenerator,
} from './url_generator';
declare module '../../share/public' {
export interface UrlGeneratorStateMapping {
[DISCOVER_APP_URL_GENERATOR]: UrlGeneratorState<DiscoverUrlGeneratorState>;
}
}
/**
* @public
@ -76,12 +87,31 @@ export interface DiscoverSetup {
export interface DiscoverStart {
savedSearchLoader: SavedObjectLoader;
/**
* `share` plugin URL generator for Discover app. Use it to generate links into
* Discover application, example:
*
* ```ts
* const url = await plugins.discover.urlGenerator.createUrl({
* savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
* indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
* timeRange: {
* to: 'now',
* from: 'now-15m',
* mode: 'relative',
* },
* });
* ```
*/
readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>;
}
/**
* @internal
*/
export interface DiscoverSetupPlugins {
share?: SharePluginSetup;
uiActions: UiActionsSetup;
embeddable: EmbeddableSetup;
kibanaLegacy: KibanaLegacySetup;
@ -122,6 +152,7 @@ export class DiscoverPlugin
private stopUrlTracking: (() => void) | undefined = undefined;
private servicesInitialized: boolean = false;
private innerAngularInitialized: boolean = false;
private urlGenerator?: DiscoverStart['urlGenerator'];
/**
* why are those functions public? they are needed for some mocha tests
@ -131,6 +162,17 @@ export class DiscoverPlugin
public initializeServices?: () => Promise<{ core: CoreStart; plugins: DiscoverStartPlugins }>;
setup(core: CoreSetup<DiscoverStartPlugins, DiscoverStart>, plugins: DiscoverSetupPlugins) {
const baseUrl = core.http.basePath.prepend('/app/discover');
if (plugins.share) {
this.urlGenerator = plugins.share.urlGenerators.registerUrlGenerator(
new DiscoverUrlGenerator({
appBasePath: baseUrl,
useHash: core.uiSettings.get('state:storeInSessionStorage'),
})
);
}
this.docViewsRegistry = new DocViewsRegistry();
setDocViewsRegistry(this.docViewsRegistry);
this.docViewsRegistry.addDocView({
@ -158,7 +200,7 @@ export class DiscoverPlugin
// so history is lazily created (when app is mounted)
// this prevents redundant `#` when not in discover app
getHistory: getScopedHistory,
baseUrl: core.http.basePath.prepend('/app/discover'),
baseUrl,
defaultSubUrl: '#/',
storageKey: `lastUrl:${core.http.basePath.get()}:discover`,
navLinkUpdater$: this.appStateUpdater,
@ -266,6 +308,7 @@ export class DiscoverPlugin
};
return {
urlGenerator: this.urlGenerator,
savedSearchLoader: createSavedSearchesLoader({
savedObjectsClient: core.savedObjects.client,
indexPatterns: plugins.data.indexPatterns,

View file

@ -0,0 +1,259 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { DiscoverUrlGenerator } from './url_generator';
import { hashedItemStore, getStatesFromKbnUrl } from '../../kibana_utils/public';
// eslint-disable-next-line
import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock';
import { FilterStateStore } from '../../data/common';
const appBasePath: string = 'xyz/app/discover';
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 generator = new DiscoverUrlGenerator({
appBasePath,
useHash,
});
return {
generator,
};
};
beforeEach(() => {
// @ts-ignore
hashedItemStore.storage = mockStorage;
});
describe('Discover url generator', () => {
test('can create a link to Discover with no state and no saved search', async () => {
const { generator } = await setup();
const url = await generator.createUrl({});
const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']);
expect(url.startsWith(appBasePath)).toBe(true);
expect(_a).toEqual({});
expect(_g).toEqual({});
});
test('can create a link to a saved search in Discover', async () => {
const { generator } = await setup();
const url = await generator.createUrl({ savedSearchId });
const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']);
expect(url.startsWith(`${appBasePath}#/${savedSearchId}`)).toBe(true);
expect(_a).toEqual({});
expect(_g).toEqual({});
});
test('can specify specific index pattern', async () => {
const { generator } = await setup();
const url = await generator.createUrl({
indexPatternId,
});
const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']);
expect(_a).toEqual({
index: indexPatternId,
});
expect(_g).toEqual({});
});
test('can specify specific time range', async () => {
const { generator } = await setup();
const url = await generator.createUrl({
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
});
const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']);
expect(_a).toEqual({});
expect(_g).toEqual({
time: {
from: 'now-15m',
mode: 'relative',
to: 'now',
},
});
});
test('can specify query', async () => {
const { generator } = await setup();
const url = await generator.createUrl({
query: {
language: 'kuery',
query: 'foo',
},
});
const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']);
expect(_a).toEqual({
query: {
language: 'kuery',
query: 'foo',
},
});
expect(_g).toEqual({});
});
test('can specify local and global filters', async () => {
const { generator } = await setup();
const url = await generator.createUrl({
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(url, ['_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 { generator } = await setup();
const url = await generator.createUrl({
refreshInterval: {
pause: false,
value: 666,
},
});
const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']);
expect(_a).toEqual({});
expect(_g).toEqual({
refreshInterval: {
pause: false,
value: 666,
},
});
});
test('can set time range', async () => {
const { generator } = await setup();
const url = await generator.createUrl({
timeRange: {
from: 'now-3h',
to: 'now',
},
});
const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']);
expect(_a).toEqual({});
expect(_g).toEqual({
time: {
from: 'now-3h',
to: 'now',
},
});
});
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 { generator } = await setup();
const url = await generator.createUrl({
indexPatternId,
});
expect(url.indexOf(indexPatternId) > -1).toBe(true);
});
test('when enabling useHash, does not set index pattern ID in the generated URL', async () => {
const { generator } = await setup();
const url = await generator.createUrl({
useHash: true,
indexPatternId,
});
expect(url.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 { generator } = await setup({ useHash: true });
const url = await generator.createUrl({
indexPatternId,
});
expect(url.indexOf(indexPatternId) > -1).toBe(false);
});
test('when disabling useHash, sets index pattern ID in the generated URL', async () => {
const { generator } = await setup();
const url = await generator.createUrl({
useHash: false,
indexPatternId,
});
expect(url.indexOf(indexPatternId) > -1).toBe(true);
});
});
});
});

View file

@ -0,0 +1,114 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
TimeRange,
Filter,
Query,
esFilters,
QueryState,
RefreshInterval,
} from '../../data/public';
import { setStateToKbnUrl } from '../../kibana_utils/public';
import { UrlGeneratorsDefinition } from '../../share/public';
export const DISCOVER_APP_URL_GENERATOR = 'DISCOVER_APP_URL_GENERATOR';
export interface DiscoverUrlGeneratorState {
/**
* 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;
/**
* Optionally apply filers.
*/
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;
}
interface Params {
appBasePath: string;
useHash: boolean;
}
export class DiscoverUrlGenerator
implements UrlGeneratorsDefinition<typeof DISCOVER_APP_URL_GENERATOR> {
constructor(private readonly params: Params) {}
public readonly id = DISCOVER_APP_URL_GENERATOR;
public readonly createUrl = async ({
filters,
indexPatternId,
query,
refreshInterval,
savedSearchId,
timeRange,
useHash = this.params.useHash,
}: DiscoverUrlGeneratorState): Promise<string> => {
const savedSearchPath = savedSearchId ? encodeURIComponent(savedSearchId) : '';
const appState: {
query?: Query;
filters?: Filter[];
index?: string;
} = {};
const queryState: QueryState = {};
if (query) appState.query = query;
if (filters) appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f));
if (indexPatternId) appState.index = indexPatternId;
if (timeRange) queryState.time = timeRange;
if (filters) queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f));
if (refreshInterval) queryState.refreshInterval = refreshInterval;
let url = `${this.params.appBasePath}#/${savedSearchPath}`;
url = setStateToKbnUrl<QueryState>('_g', queryState, { useHash }, url);
url = setStateToKbnUrl('_a', appState, { useHash }, url);
return url;
};
}

View file

@ -19,16 +19,17 @@
import { CoreStart, StartServicesAccessor } from '../../../../core/public';
export interface StartServices<Plugins = unknown, OwnContract = unknown> {
export interface StartServices<Plugins = unknown, OwnContract = unknown, Core = CoreStart> {
plugins: Plugins;
self: OwnContract;
core: CoreStart;
core: Core;
}
export type StartServicesGetter<Plugins = unknown, OwnContract = unknown> = () => StartServices<
Plugins,
OwnContract
>;
export type StartServicesGetter<
Plugins = unknown,
OwnContract = unknown,
Core = CoreStart
> = () => StartServices<Plugins, OwnContract>;
/**
* Use this utility to create a synchronous *start* service getter in *setup*

View file

@ -24,7 +24,7 @@ import { UrlGeneratorInternal } from './url_generator_internal';
import { UrlGeneratorContract } from './url_generator_contract';
export interface UrlGeneratorsStart {
getUrlGenerator: (urlGeneratorId: UrlGeneratorId) => UrlGeneratorContract<UrlGeneratorId>;
getUrlGenerator: <T extends UrlGeneratorId>(urlGeneratorId: T) => UrlGeneratorContract<T>;
}
export interface UrlGeneratorsSetup {

View file

@ -5,6 +5,6 @@
"configPath": ["ui_actions_enhanced_examples"],
"server": false,
"ui": true,
"requiredPlugins": ["uiActionsEnhanced", "data"],
"requiredPlugins": ["uiActionsEnhanced", "data", "discover"],
"optionalPlugins": []
}

View file

@ -31,9 +31,7 @@ export const DiscoverDrilldownConfig: React.FC<DiscoverDrilldownConfigProps> = (
onIndexPatternSelect,
customIndexPattern,
onCustomIndexPatternToggle,
carryFiltersAndQuery,
onCarryFiltersAndQueryToggle,
carryTimeRange,
onCarryTimeRangeToggle,
}) => {
return (
@ -82,9 +80,10 @@ export const DiscoverDrilldownConfig: React.FC<DiscoverDrilldownConfigProps> = (
{!!onCarryFiltersAndQueryToggle && (
<EuiFormRow hasChildLabel={false}>
<EuiSwitch
disabled
name="carryFiltersAndQuery"
label="Carry over filters and query"
checked={!!carryFiltersAndQuery}
checked={false}
onChange={onCarryFiltersAndQueryToggle}
/>
</EuiFormRow>
@ -92,9 +91,10 @@ export const DiscoverDrilldownConfig: React.FC<DiscoverDrilldownConfigProps> = (
{!!onCarryTimeRangeToggle && (
<EuiFormRow hasChildLabel={false}>
<EuiSwitch
disabled
name="carryTimeRange"
label="Carry over time range"
checked={!!carryTimeRange}
checked={false}
onChange={onCarryTimeRangeToggle}
/>
</EuiFormRow>

View file

@ -22,7 +22,7 @@ const isOutputWithIndexPatterns = (
};
export interface Params {
start: StartServicesGetter<Pick<Start, 'data'>>;
start: StartServicesGetter<Pick<Start, 'data' | 'discover'>>;
}
export class DashboardToDiscoverDrilldown implements Drilldown<Config, ActionContext> {
@ -54,6 +54,10 @@ export class DashboardToDiscoverDrilldown implements Drilldown<Config, ActionCon
};
private readonly getPath = async (config: Config, context: ActionContext): Promise<string> => {
const { urlGenerator } = this.params.start().plugins.discover;
if (!urlGenerator) throw new Error('Discover URL generator not available.');
let indexPatternId =
!!config.customIndexPattern && !!config.indexPatternId ? config.indexPatternId : '';
@ -64,8 +68,9 @@ export class DashboardToDiscoverDrilldown implements Drilldown<Config, ActionCon
}
}
const index = indexPatternId ? `,index:'${indexPatternId}'` : '';
return `#/?_g=(filters:!(),refreshInterval:(pause:!f,value:900000),time:(from:now-7d,to:now))&_a=(columns:!(_source),filters:!()${index},interval:auto,query:(language:kuery,query:''),sort:!())`;
return await urlGenerator.createUrl({
indexPatternId,
});
};
public readonly getHref = async (config: Config, context: ActionContext): Promise<string> => {

View file

@ -14,14 +14,17 @@ import { DashboardHelloWorldDrilldown } from './dashboard_hello_world_drilldown'
import { DashboardToUrlDrilldown } from './dashboard_to_url_drilldown';
import { DashboardToDiscoverDrilldown } from './dashboard_to_discover_drilldown';
import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public';
import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public';
export interface SetupDependencies {
data: DataPublicPluginSetup;
discover: DiscoverSetup;
uiActionsEnhanced: AdvancedUiActionsSetup;
}
export interface StartDependencies {
data: DataPublicPluginStart;
discover: DiscoverStart;
uiActionsEnhanced: AdvancedUiActionsStart;
}