Skip to content

Commit 4bde414

Browse files
streamichvadimkibanakibanamachine
committed
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>
1 parent f827815 commit 4bde414

11 files changed

Lines changed: 578 additions & 59 deletions

File tree

src/plugins/dashboard/public/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ export {
2222
DashboardUrlGenerator,
2323
DashboardFeatureFlagConfig,
2424
} from './plugin';
25+
2526
export {
2627
DASHBOARD_APP_URL_GENERATOR,
2728
createDashboardUrlGenerator,
2829
DashboardUrlGeneratorState,
2930
} from './url_generator';
31+
export { DashboardAppLocator, DashboardAppLocatorParams } from './locator';
32+
3033
export { DashboardSavedObject } from './saved_dashboards';
3134
export { SavedDashboardPanel, DashboardContainerInput } from './types';
3235

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import { DashboardAppLocatorDefinition } from './locator';
10+
import { hashedItemStore } from '../../kibana_utils/public';
11+
import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock';
12+
import { esFilters } from '../../data/public';
13+
14+
describe('dashboard locator', () => {
15+
beforeEach(() => {
16+
// @ts-ignore
17+
hashedItemStore.storage = mockStorage;
18+
});
19+
20+
test('creates a link to a saved dashboard', async () => {
21+
const definition = new DashboardAppLocatorDefinition({
22+
useHashedUrl: false,
23+
getDashboardFilterFields: async (dashboardId: string) => [],
24+
});
25+
const location = await definition.getLocation({});
26+
27+
expect(location).toMatchObject({
28+
app: 'dashboards',
29+
path: '#/create?_a=()&_g=()',
30+
state: {},
31+
});
32+
});
33+
34+
test('creates a link with global time range set up', async () => {
35+
const definition = new DashboardAppLocatorDefinition({
36+
useHashedUrl: false,
37+
getDashboardFilterFields: async (dashboardId: string) => [],
38+
});
39+
const location = await definition.getLocation({
40+
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
41+
});
42+
43+
expect(location).toMatchObject({
44+
app: 'dashboards',
45+
path: '#/create?_a=()&_g=(time:(from:now-15m,mode:relative,to:now))',
46+
state: {},
47+
});
48+
});
49+
50+
test('creates a link with filters, time range, refresh interval and query to a saved object', async () => {
51+
const definition = new DashboardAppLocatorDefinition({
52+
useHashedUrl: false,
53+
getDashboardFilterFields: async (dashboardId: string) => [],
54+
});
55+
const location = await definition.getLocation({
56+
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
57+
refreshInterval: { pause: false, value: 300 },
58+
dashboardId: '123',
59+
filters: [
60+
{
61+
meta: {
62+
alias: null,
63+
disabled: false,
64+
negate: false,
65+
},
66+
query: { query: 'hi' },
67+
},
68+
{
69+
meta: {
70+
alias: null,
71+
disabled: false,
72+
negate: false,
73+
},
74+
query: { query: 'hi' },
75+
$state: {
76+
store: esFilters.FilterStateStore.GLOBAL_STATE,
77+
},
78+
},
79+
],
80+
query: { query: 'bye', language: 'kuery' },
81+
});
82+
83+
expect(location).toMatchObject({
84+
app: 'dashboards',
85+
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))`,
86+
state: {},
87+
});
88+
});
89+
90+
test('searchSessionId', async () => {
91+
const definition = new DashboardAppLocatorDefinition({
92+
useHashedUrl: false,
93+
getDashboardFilterFields: async (dashboardId: string) => [],
94+
});
95+
const location = await definition.getLocation({
96+
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
97+
refreshInterval: { pause: false, value: 300 },
98+
dashboardId: '123',
99+
filters: [],
100+
query: { query: 'bye', language: 'kuery' },
101+
searchSessionId: '__sessionSearchId__',
102+
});
103+
104+
expect(location).toMatchObject({
105+
app: 'dashboards',
106+
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__`,
107+
state: {},
108+
});
109+
});
110+
111+
test('savedQuery', async () => {
112+
const definition = new DashboardAppLocatorDefinition({
113+
useHashedUrl: false,
114+
getDashboardFilterFields: async (dashboardId: string) => [],
115+
});
116+
const location = await definition.getLocation({
117+
savedQuery: '__savedQueryId__',
118+
});
119+
120+
expect(location).toMatchObject({
121+
app: 'dashboards',
122+
path: `#/create?_a=(savedQuery:__savedQueryId__)&_g=()`,
123+
state: {},
124+
});
125+
expect(location.path).toContain('__savedQueryId__');
126+
});
127+
128+
test('panels', async () => {
129+
const definition = new DashboardAppLocatorDefinition({
130+
useHashedUrl: false,
131+
getDashboardFilterFields: async (dashboardId: string) => [],
132+
});
133+
const location = await definition.getLocation({
134+
panels: [{ fakePanelContent: 'fakePanelContent' }] as any,
135+
});
136+
137+
expect(location).toMatchObject({
138+
app: 'dashboards',
139+
path: `#/create?_a=(panels:!((fakePanelContent:fakePanelContent)))&_g=()`,
140+
state: {},
141+
});
142+
});
143+
144+
test('if no useHash setting is given, uses the one was start services', async () => {
145+
const definition = new DashboardAppLocatorDefinition({
146+
useHashedUrl: true,
147+
getDashboardFilterFields: async (dashboardId: string) => [],
148+
});
149+
const location = await definition.getLocation({
150+
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
151+
});
152+
153+
expect(location.path.indexOf('relative')).toBe(-1);
154+
});
155+
156+
test('can override a false useHash ui setting', async () => {
157+
const definition = new DashboardAppLocatorDefinition({
158+
useHashedUrl: false,
159+
getDashboardFilterFields: async (dashboardId: string) => [],
160+
});
161+
const location = await definition.getLocation({
162+
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
163+
useHash: true,
164+
});
165+
166+
expect(location.path.indexOf('relative')).toBe(-1);
167+
});
168+
169+
test('can override a true useHash ui setting', async () => {
170+
const definition = new DashboardAppLocatorDefinition({
171+
useHashedUrl: true,
172+
getDashboardFilterFields: async (dashboardId: string) => [],
173+
});
174+
const location = await definition.getLocation({
175+
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
176+
useHash: false,
177+
});
178+
179+
expect(location.path.indexOf('relative')).toBeGreaterThan(1);
180+
});
181+
182+
describe('preserving saved filters', () => {
183+
const savedFilter1 = {
184+
meta: {
185+
alias: null,
186+
disabled: false,
187+
negate: false,
188+
},
189+
query: { query: 'savedfilter1' },
190+
};
191+
192+
const savedFilter2 = {
193+
meta: {
194+
alias: null,
195+
disabled: false,
196+
negate: false,
197+
},
198+
query: { query: 'savedfilter2' },
199+
};
200+
201+
const appliedFilter = {
202+
meta: {
203+
alias: null,
204+
disabled: false,
205+
negate: false,
206+
},
207+
query: { query: 'appliedfilter' },
208+
};
209+
210+
test('attaches filters from destination dashboard', async () => {
211+
const definition = new DashboardAppLocatorDefinition({
212+
useHashedUrl: false,
213+
getDashboardFilterFields: async (dashboardId: string) => {
214+
return dashboardId === 'dashboard1'
215+
? [savedFilter1]
216+
: dashboardId === 'dashboard2'
217+
? [savedFilter2]
218+
: [];
219+
},
220+
});
221+
222+
const location1 = await definition.getLocation({
223+
dashboardId: 'dashboard1',
224+
filters: [appliedFilter],
225+
});
226+
227+
expect(location1.path).toEqual(expect.stringContaining('query:savedfilter1'));
228+
expect(location1.path).toEqual(expect.stringContaining('query:appliedfilter'));
229+
230+
const location2 = await definition.getLocation({
231+
dashboardId: 'dashboard2',
232+
filters: [appliedFilter],
233+
});
234+
235+
expect(location2.path).toEqual(expect.stringContaining('query:savedfilter2'));
236+
expect(location2.path).toEqual(expect.stringContaining('query:appliedfilter'));
237+
});
238+
239+
test("doesn't fail if can't retrieve filters from destination dashboard", async () => {
240+
const definition = new DashboardAppLocatorDefinition({
241+
useHashedUrl: false,
242+
getDashboardFilterFields: async (dashboardId: string) => {
243+
if (dashboardId === 'dashboard1') {
244+
throw new Error('Not found');
245+
}
246+
return [];
247+
},
248+
});
249+
250+
const location = await definition.getLocation({
251+
dashboardId: 'dashboard1',
252+
filters: [appliedFilter],
253+
});
254+
255+
expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1'));
256+
expect(location.path).toEqual(expect.stringContaining('query:appliedfilter'));
257+
});
258+
259+
test('can enforce empty filters', async () => {
260+
const definition = new DashboardAppLocatorDefinition({
261+
useHashedUrl: false,
262+
getDashboardFilterFields: async (dashboardId: string) => {
263+
if (dashboardId === 'dashboard1') {
264+
return [savedFilter1];
265+
}
266+
return [];
267+
},
268+
});
269+
270+
const location = await definition.getLocation({
271+
dashboardId: 'dashboard1',
272+
filters: [],
273+
preserveSavedFilters: false,
274+
});
275+
276+
expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1'));
277+
expect(location.path).not.toEqual(expect.stringContaining('query:appliedfilter'));
278+
expect(location.path).toMatchInlineSnapshot(
279+
`"#/view/dashboard1?_a=(filters:!())&_g=(filters:!())"`
280+
);
281+
});
282+
283+
test('no filters in result url if no filters applied', async () => {
284+
const definition = new DashboardAppLocatorDefinition({
285+
useHashedUrl: false,
286+
getDashboardFilterFields: async (dashboardId: string) => {
287+
if (dashboardId === 'dashboard1') {
288+
return [savedFilter1];
289+
}
290+
return [];
291+
},
292+
});
293+
294+
const location = await definition.getLocation({
295+
dashboardId: 'dashboard1',
296+
});
297+
298+
expect(location.path).not.toEqual(expect.stringContaining('filters'));
299+
expect(location.path).toMatchInlineSnapshot(`"#/view/dashboard1?_a=()&_g=()"`);
300+
});
301+
302+
test('can turn off preserving filters', async () => {
303+
const definition = new DashboardAppLocatorDefinition({
304+
useHashedUrl: false,
305+
getDashboardFilterFields: async (dashboardId: string) => {
306+
if (dashboardId === 'dashboard1') {
307+
return [savedFilter1];
308+
}
309+
return [];
310+
},
311+
});
312+
313+
const location = await definition.getLocation({
314+
dashboardId: 'dashboard1',
315+
filters: [appliedFilter],
316+
preserveSavedFilters: false,
317+
});
318+
319+
expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1'));
320+
expect(location.path).toEqual(expect.stringContaining('query:appliedfilter'));
321+
});
322+
});
323+
});

0 commit comments

Comments
 (0)