mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
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:
parent
bc6ee27a29
commit
baf2de5415
11 changed files with 578 additions and 59 deletions
|
@ -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';
|
||||
|
||||
|
|
323
src/plugins/dashboard/public/locator.test.ts
Normal file
323
src/plugins/dashboard/public/locator.test.ts
Normal 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'));
|
||||
});
|
||||
});
|
||||
});
|
160
src/plugins/dashboard/public/locator.ts
Normal file
160
src/plugins/dashboard/public/locator.ts
Normal 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: {},
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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!,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
"dashboardEnhanced",
|
||||
"embeddable",
|
||||
"kibanaUtils",
|
||||
"kibanaReact",
|
||||
"share"
|
||||
"kibanaReact"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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 });
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue