Skip to content

Commit a0c4e50

Browse files
dej611kibanamachine
andcommitted
[Lens] Integrate searchSessionId into Lens app (#86297)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
1 parent b6a0554 commit a0c4e50

13 files changed

Lines changed: 189 additions & 44 deletions

File tree

x-pack/plugins/lens/public/app_plugin/app.test.tsx

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ function createMockFrame(): jest.Mocked<EditorFrameInstance> {
7474
};
7575
}
7676

77+
function createMockSearchService() {
78+
let sessionIdCounter = 1;
79+
return {
80+
session: {
81+
start: jest.fn(() => `sessionId-${sessionIdCounter++}`),
82+
clear: jest.fn(),
83+
},
84+
};
85+
}
86+
7787
function createMockFilterManager() {
7888
const unsubscribe = jest.fn();
7989

@@ -118,16 +128,29 @@ function createMockQueryString() {
118128
function createMockTimefilter() {
119129
const unsubscribe = jest.fn();
120130

131+
let timeFilter = { from: 'now-7d', to: 'now' };
132+
let subscriber: () => void;
121133
return {
122-
getTime: jest.fn(() => ({ from: 'now-7d', to: 'now' })),
123-
setTime: jest.fn(),
134+
getTime: jest.fn(() => timeFilter),
135+
setTime: jest.fn((newTimeFilter) => {
136+
timeFilter = newTimeFilter;
137+
if (subscriber) {
138+
subscriber();
139+
}
140+
}),
124141
getTimeUpdate$: () => ({
125142
subscribe: ({ next }: { next: () => void }) => {
143+
subscriber = next;
126144
return unsubscribe;
127145
},
128146
}),
129147
getRefreshInterval: () => {},
130148
getRefreshIntervalDefaults: () => {},
149+
getAutoRefreshFetch$: () => ({
150+
subscribe: ({ next }: { next: () => void }) => {
151+
return next;
152+
},
153+
}),
131154
};
132155
}
133156

@@ -209,6 +232,7 @@ describe('Lens App', () => {
209232
return new Promise((resolve) => resolve({ id }));
210233
}),
211234
},
235+
search: createMockSearchService(),
212236
} as unknown) as DataPublicPluginStart,
213237
storage: {
214238
get: jest.fn(),
@@ -295,6 +319,7 @@ describe('Lens App', () => {
295319
"query": "",
296320
},
297321
"savedQuery": undefined,
322+
"searchSessionId": "sessionId-1",
298323
"showNoDataPopover": [Function],
299324
},
300325
],
@@ -1072,6 +1097,53 @@ describe('Lens App', () => {
10721097
})
10731098
);
10741099
});
1100+
1101+
it('updates the searchSessionId when the user changes query or time in the search bar', () => {
1102+
const { component, frame, services } = mountWith({});
1103+
act(() =>
1104+
component.find(TopNavMenu).prop('onQuerySubmit')!({
1105+
dateRange: { from: 'now-14d', to: 'now-7d' },
1106+
query: { query: '', language: 'lucene' },
1107+
})
1108+
);
1109+
component.update();
1110+
expect(frame.mount).toHaveBeenCalledWith(
1111+
expect.any(Element),
1112+
expect.objectContaining({
1113+
searchSessionId: `sessionId-1`,
1114+
})
1115+
);
1116+
1117+
// trigger again, this time changing just the query
1118+
act(() =>
1119+
component.find(TopNavMenu).prop('onQuerySubmit')!({
1120+
dateRange: { from: 'now-14d', to: 'now-7d' },
1121+
query: { query: 'new', language: 'lucene' },
1122+
})
1123+
);
1124+
component.update();
1125+
expect(frame.mount).toHaveBeenCalledWith(
1126+
expect.any(Element),
1127+
expect.objectContaining({
1128+
searchSessionId: `sessionId-2`,
1129+
})
1130+
);
1131+
1132+
const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern;
1133+
const field = ({ name: 'myfield' } as unknown) as IFieldType;
1134+
act(() =>
1135+
services.data.query.filterManager.setFilters([
1136+
esFilters.buildExistsFilter(field, indexPattern),
1137+
])
1138+
);
1139+
component.update();
1140+
expect(frame.mount).toHaveBeenCalledWith(
1141+
expect.any(Element),
1142+
expect.objectContaining({
1143+
searchSessionId: `sessionId-3`,
1144+
})
1145+
);
1146+
});
10751147
});
10761148

10771149
describe('saved query handling', () => {
@@ -1165,6 +1237,37 @@ describe('Lens App', () => {
11651237
);
11661238
});
11671239

1240+
it('updates the searchSessionId when the query is updated', () => {
1241+
const { component, frame } = mountWith({});
1242+
act(() => {
1243+
component.find(TopNavMenu).prop('onSaved')!({
1244+
id: '1',
1245+
attributes: {
1246+
title: '',
1247+
description: '',
1248+
query: { query: '', language: 'lucene' },
1249+
},
1250+
});
1251+
});
1252+
act(() => {
1253+
component.find(TopNavMenu).prop('onSavedQueryUpdated')!({
1254+
id: '2',
1255+
attributes: {
1256+
title: 'new title',
1257+
description: '',
1258+
query: { query: '', language: 'lucene' },
1259+
},
1260+
});
1261+
});
1262+
component.update();
1263+
expect(frame.mount).toHaveBeenCalledWith(
1264+
expect.any(Element),
1265+
expect.objectContaining({
1266+
searchSessionId: `sessionId-1`,
1267+
})
1268+
);
1269+
});
1270+
11681271
it('clears all existing unpinned filters when the active saved query is cleared', () => {
11691272
const { component, frame, services } = mountWith({});
11701273
act(() =>
@@ -1190,6 +1293,32 @@ describe('Lens App', () => {
11901293
})
11911294
);
11921295
});
1296+
1297+
it('updates the searchSessionId when the active saved query is cleared', () => {
1298+
const { component, frame, services } = mountWith({});
1299+
act(() =>
1300+
component.find(TopNavMenu).prop('onQuerySubmit')!({
1301+
dateRange: { from: 'now-14d', to: 'now-7d' },
1302+
query: { query: 'new', language: 'lucene' },
1303+
})
1304+
);
1305+
const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern;
1306+
const field = ({ name: 'myfield' } as unknown) as IFieldType;
1307+
const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType;
1308+
const unpinned = esFilters.buildExistsFilter(field, indexPattern);
1309+
const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern);
1310+
FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE);
1311+
act(() => services.data.query.filterManager.setFilters([pinned, unpinned]));
1312+
component.update();
1313+
act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!());
1314+
component.update();
1315+
expect(frame.mount).toHaveBeenCalledWith(
1316+
expect.any(Element),
1317+
expect.objectContaining({
1318+
searchSessionId: `sessionId-2`,
1319+
})
1320+
);
1321+
});
11931322
});
11941323

11951324
describe('showing a confirm message when leaving', () => {

x-pack/plugins/lens/public/app_plugin/app.tsx

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import './app.scss';
88

99
import _ from 'lodash';
10-
import React, { useState, useEffect, useCallback } from 'react';
10+
import React, { useState, useEffect, useCallback, useMemo } from 'react';
1111
import { i18n } from '@kbn/i18n';
1212
import { NotificationsStart } from 'kibana/public';
1313
import { EuiBreadcrumb } from '@elastic/eui';
@@ -71,7 +71,6 @@ export function App({
7171
} = useKibana<LensAppServices>().services;
7272

7373
const [state, setState] = useState<LensAppState>(() => {
74-
const currentRange = data.query.timefilter.timefilter.getTime();
7574
return {
7675
query: data.query.queryString.getQuery(),
7776
// Do not use app-specific filters from previous app,
@@ -81,14 +80,11 @@ export function App({
8180
: data.query.filterManager.getFilters(),
8281
isLoading: Boolean(initialInput),
8382
indexPatternsForTopNav: [],
84-
dateRange: {
85-
fromDate: currentRange.from,
86-
toDate: currentRange.to,
87-
},
8883
isLinkedToOriginatingApp: Boolean(incomingState?.originatingApp),
8984
isSaveModalVisible: false,
9085
indicateNoData: false,
9186
isSaveable: false,
87+
searchSessionId: data.search.session.start(),
9288
};
9389
});
9490

@@ -107,10 +103,14 @@ export function App({
107103
state.indicateNoData,
108104
state.query,
109105
state.filters,
110-
state.dateRange,
111106
state.indexPatternsForTopNav,
107+
state.searchSessionId,
112108
]);
113109

110+
// Need a stable reference for the frame component of the dateRange
111+
const { from: fromDate, to: toDate } = data.query.timefilter.timefilter.getTime();
112+
const currentDateRange = useMemo(() => ({ fromDate, toDate }), [fromDate, toDate]);
113+
114114
const onError = useCallback(
115115
(e: { message: string }) =>
116116
notifications.toasts.addDanger({
@@ -160,24 +160,35 @@ export function App({
160160

161161
const filterSubscription = data.query.filterManager.getUpdates$().subscribe({
162162
next: () => {
163-
setState((s) => ({ ...s, filters: data.query.filterManager.getFilters() }));
163+
setState((s) => ({
164+
...s,
165+
filters: data.query.filterManager.getFilters(),
166+
searchSessionId: data.search.session.start(),
167+
}));
164168
trackUiEvent('app_filters_updated');
165169
},
166170
});
167171

168172
const timeSubscription = data.query.timefilter.timefilter.getTimeUpdate$().subscribe({
169173
next: () => {
170-
const currentRange = data.query.timefilter.timefilter.getTime();
171174
setState((s) => ({
172175
...s,
173-
dateRange: {
174-
fromDate: currentRange.from,
175-
toDate: currentRange.to,
176-
},
176+
searchSessionId: data.search.session.start(),
177177
}));
178178
},
179179
});
180180

181+
const autoRefreshSubscription = data.query.timefilter.timefilter
182+
.getAutoRefreshFetch$()
183+
.subscribe({
184+
next: () => {
185+
setState((s) => ({
186+
...s,
187+
searchSessionId: data.search.session.start(),
188+
}));
189+
},
190+
});
191+
181192
const kbnUrlStateStorage = createKbnUrlStateStorage({
182193
history,
183194
useHash: uiSettings.get('state:storeInSessionStorage'),
@@ -192,10 +203,12 @@ export function App({
192203
stopSyncingQueryServiceStateWithUrl();
193204
filterSubscription.unsubscribe();
194205
timeSubscription.unsubscribe();
206+
autoRefreshSubscription.unsubscribe();
195207
};
196208
}, [
197209
data.query.filterManager,
198210
data.query.timefilter.timefilter,
211+
data.search.session,
199212
notifications.toasts,
200213
uiSettings,
201214
data.query,
@@ -594,21 +607,21 @@ export function App({
594607
appName={'lens'}
595608
onQuerySubmit={(payload) => {
596609
const { dateRange, query } = payload;
597-
if (
598-
dateRange.from !== state.dateRange.fromDate ||
599-
dateRange.to !== state.dateRange.toDate
600-
) {
610+
const currentRange = data.query.timefilter.timefilter.getTime();
611+
if (dateRange.from !== currentRange.from || dateRange.to !== currentRange.to) {
601612
data.query.timefilter.timefilter.setTime(dateRange);
602613
trackUiEvent('app_date_change');
603614
} else {
615+
// Query has changed, renew the session id.
616+
// Time change will be picked up by the time subscription
617+
setState((s) => ({
618+
...s,
619+
searchSessionId: data.search.session.start(),
620+
}));
604621
trackUiEvent('app_query_change');
605622
}
606623
setState((s) => ({
607624
...s,
608-
dateRange: {
609-
fromDate: dateRange.from,
610-
toDate: dateRange.to,
611-
},
612625
query: query || s.query,
613626
}));
614627
}}
@@ -622,12 +635,6 @@ export function App({
622635
setState((s) => ({
623636
...s,
624637
savedQuery: { ...savedQuery }, // Shallow query for reference issues
625-
dateRange: savedQuery.attributes.timefilter
626-
? {
627-
fromDate: savedQuery.attributes.timefilter.from,
628-
toDate: savedQuery.attributes.timefilter.to,
629-
}
630-
: s.dateRange,
631638
}));
632639
}}
633640
onClearSavedQuery={() => {
@@ -640,8 +647,8 @@ export function App({
640647
}));
641648
}}
642649
query={state.query}
643-
dateRangeFrom={state.dateRange.fromDate}
644-
dateRangeTo={state.dateRange.toDate}
650+
dateRangeFrom={fromDate}
651+
dateRangeTo={toDate}
645652
indicateNoData={state.indicateNoData}
646653
/>
647654
</div>
@@ -650,7 +657,8 @@ export function App({
650657
className="lnsApp__frame"
651658
render={editorFrame.mount}
652659
nativeProps={{
653-
dateRange: state.dateRange,
660+
searchSessionId: state.searchSessionId,
661+
dateRange: currentDateRange,
654662
query: state.query,
655663
filters: state.filters,
656664
savedQuery: state.savedQuery,

x-pack/plugins/lens/public/app_plugin/mounter.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ export async function mountApp(
216216
params.element
217217
);
218218
return () => {
219+
data.search.session.clear();
219220
instance.unmount();
220221
unmountComponentAtNode(params.element);
221222
unlistenParentHistory();

x-pack/plugins/lens/public/app_plugin/types.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,12 @@ export interface LensAppState {
5555
// Determines whether the lens editor shows the 'save and return' button, and the originating app breadcrumb.
5656
isLinkedToOriginatingApp?: boolean;
5757

58-
// Properties needed to interface with TopNav
59-
dateRange: {
60-
fromDate: string;
61-
toDate: string;
62-
};
6358
query: Query;
6459
filters: Filter[];
6560
savedQuery?: SavedQuery;
6661
isSaveable: boolean;
6762
activeData?: TableInspectorAdapter;
63+
searchSessionId: string;
6864
}
6965

7066
export interface RedirectToOriginProps {

x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ function getDefaultProps() {
6060
},
6161
palettes: chartPluginMock.createPaletteRegistry(),
6262
showNoDataPopover: jest.fn(),
63+
searchSessionId: 'sessionId',
6364
};
6465
}
6566

@@ -264,6 +265,7 @@ describe('editor_frame', () => {
264265
filters: [],
265266
dateRange: { fromDate: 'now-7d', toDate: 'now' },
266267
availablePalettes: defaultProps.palettes,
268+
searchSessionId: 'sessionId',
267269
});
268270
});
269271

0 commit comments

Comments
 (0)