[Lens] Integrate searchSessionId into Lens app (#86297)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marco Liberati 2020-12-29 14:18:30 +01:00 committed by GitHub
parent d843450620
commit bd908c6ba3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 189 additions and 44 deletions

View file

@ -74,6 +74,16 @@ function createMockFrame(): jest.Mocked<EditorFrameInstance> {
};
}
function createMockSearchService() {
let sessionIdCounter = 1;
return {
session: {
start: jest.fn(() => `sessionId-${sessionIdCounter++}`),
clear: jest.fn(),
},
};
}
function createMockFilterManager() {
const unsubscribe = jest.fn();
@ -118,16 +128,29 @@ function createMockQueryString() {
function createMockTimefilter() {
const unsubscribe = jest.fn();
let timeFilter = { from: 'now-7d', to: 'now' };
let subscriber: () => void;
return {
getTime: jest.fn(() => ({ from: 'now-7d', to: 'now' })),
setTime: jest.fn(),
getTime: jest.fn(() => timeFilter),
setTime: jest.fn((newTimeFilter) => {
timeFilter = newTimeFilter;
if (subscriber) {
subscriber();
}
}),
getTimeUpdate$: () => ({
subscribe: ({ next }: { next: () => void }) => {
subscriber = next;
return unsubscribe;
},
}),
getRefreshInterval: () => {},
getRefreshIntervalDefaults: () => {},
getAutoRefreshFetch$: () => ({
subscribe: ({ next }: { next: () => void }) => {
return next;
},
}),
};
}
@ -209,6 +232,7 @@ describe('Lens App', () => {
return new Promise((resolve) => resolve({ id }));
}),
},
search: createMockSearchService(),
} as unknown) as DataPublicPluginStart,
storage: {
get: jest.fn(),
@ -295,6 +319,7 @@ describe('Lens App', () => {
"query": "",
},
"savedQuery": undefined,
"searchSessionId": "sessionId-1",
"showNoDataPopover": [Function],
},
],
@ -1072,6 +1097,53 @@ describe('Lens App', () => {
})
);
});
it('updates the searchSessionId when the user changes query or time in the search bar', () => {
const { component, frame, services } = mountWith({});
act(() =>
component.find(TopNavMenu).prop('onQuerySubmit')!({
dateRange: { from: 'now-14d', to: 'now-7d' },
query: { query: '', language: 'lucene' },
})
);
component.update();
expect(frame.mount).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
searchSessionId: `sessionId-1`,
})
);
// trigger again, this time changing just the query
act(() =>
component.find(TopNavMenu).prop('onQuerySubmit')!({
dateRange: { from: 'now-14d', to: 'now-7d' },
query: { query: 'new', language: 'lucene' },
})
);
component.update();
expect(frame.mount).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
searchSessionId: `sessionId-2`,
})
);
const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern;
const field = ({ name: 'myfield' } as unknown) as IFieldType;
act(() =>
services.data.query.filterManager.setFilters([
esFilters.buildExistsFilter(field, indexPattern),
])
);
component.update();
expect(frame.mount).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
searchSessionId: `sessionId-3`,
})
);
});
});
describe('saved query handling', () => {
@ -1165,6 +1237,37 @@ describe('Lens App', () => {
);
});
it('updates the searchSessionId when the query is updated', () => {
const { component, frame } = mountWith({});
act(() => {
component.find(TopNavMenu).prop('onSaved')!({
id: '1',
attributes: {
title: '',
description: '',
query: { query: '', language: 'lucene' },
},
});
});
act(() => {
component.find(TopNavMenu).prop('onSavedQueryUpdated')!({
id: '2',
attributes: {
title: 'new title',
description: '',
query: { query: '', language: 'lucene' },
},
});
});
component.update();
expect(frame.mount).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
searchSessionId: `sessionId-1`,
})
);
});
it('clears all existing unpinned filters when the active saved query is cleared', () => {
const { component, frame, services } = mountWith({});
act(() =>
@ -1190,6 +1293,32 @@ describe('Lens App', () => {
})
);
});
it('updates the searchSessionId when the active saved query is cleared', () => {
const { component, frame, services } = mountWith({});
act(() =>
component.find(TopNavMenu).prop('onQuerySubmit')!({
dateRange: { from: 'now-14d', to: 'now-7d' },
query: { query: 'new', language: 'lucene' },
})
);
const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern;
const field = ({ name: 'myfield' } as unknown) as IFieldType;
const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType;
const unpinned = esFilters.buildExistsFilter(field, indexPattern);
const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern);
FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE);
act(() => services.data.query.filterManager.setFilters([pinned, unpinned]));
component.update();
act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!());
component.update();
expect(frame.mount).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
searchSessionId: `sessionId-2`,
})
);
});
});
describe('showing a confirm message when leaving', () => {

View file

@ -7,7 +7,7 @@
import './app.scss';
import _ from 'lodash';
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { NotificationsStart } from 'kibana/public';
import { EuiBreadcrumb } from '@elastic/eui';
@ -71,7 +71,6 @@ export function App({
} = useKibana<LensAppServices>().services;
const [state, setState] = useState<LensAppState>(() => {
const currentRange = data.query.timefilter.timefilter.getTime();
return {
query: data.query.queryString.getQuery(),
// Do not use app-specific filters from previous app,
@ -81,14 +80,11 @@ export function App({
: data.query.filterManager.getFilters(),
isLoading: Boolean(initialInput),
indexPatternsForTopNav: [],
dateRange: {
fromDate: currentRange.from,
toDate: currentRange.to,
},
isLinkedToOriginatingApp: Boolean(incomingState?.originatingApp),
isSaveModalVisible: false,
indicateNoData: false,
isSaveable: false,
searchSessionId: data.search.session.start(),
};
});
@ -107,10 +103,14 @@ export function App({
state.indicateNoData,
state.query,
state.filters,
state.dateRange,
state.indexPatternsForTopNav,
state.searchSessionId,
]);
// Need a stable reference for the frame component of the dateRange
const { from: fromDate, to: toDate } = data.query.timefilter.timefilter.getTime();
const currentDateRange = useMemo(() => ({ fromDate, toDate }), [fromDate, toDate]);
const onError = useCallback(
(e: { message: string }) =>
notifications.toasts.addDanger({
@ -160,24 +160,35 @@ export function App({
const filterSubscription = data.query.filterManager.getUpdates$().subscribe({
next: () => {
setState((s) => ({ ...s, filters: data.query.filterManager.getFilters() }));
setState((s) => ({
...s,
filters: data.query.filterManager.getFilters(),
searchSessionId: data.search.session.start(),
}));
trackUiEvent('app_filters_updated');
},
});
const timeSubscription = data.query.timefilter.timefilter.getTimeUpdate$().subscribe({
next: () => {
const currentRange = data.query.timefilter.timefilter.getTime();
setState((s) => ({
...s,
dateRange: {
fromDate: currentRange.from,
toDate: currentRange.to,
},
searchSessionId: data.search.session.start(),
}));
},
});
const autoRefreshSubscription = data.query.timefilter.timefilter
.getAutoRefreshFetch$()
.subscribe({
next: () => {
setState((s) => ({
...s,
searchSessionId: data.search.session.start(),
}));
},
});
const kbnUrlStateStorage = createKbnUrlStateStorage({
history,
useHash: uiSettings.get('state:storeInSessionStorage'),
@ -192,10 +203,12 @@ export function App({
stopSyncingQueryServiceStateWithUrl();
filterSubscription.unsubscribe();
timeSubscription.unsubscribe();
autoRefreshSubscription.unsubscribe();
};
}, [
data.query.filterManager,
data.query.timefilter.timefilter,
data.search.session,
notifications.toasts,
uiSettings,
data.query,
@ -594,21 +607,21 @@ export function App({
appName={'lens'}
onQuerySubmit={(payload) => {
const { dateRange, query } = payload;
if (
dateRange.from !== state.dateRange.fromDate ||
dateRange.to !== state.dateRange.toDate
) {
const currentRange = data.query.timefilter.timefilter.getTime();
if (dateRange.from !== currentRange.from || dateRange.to !== currentRange.to) {
data.query.timefilter.timefilter.setTime(dateRange);
trackUiEvent('app_date_change');
} else {
// Query has changed, renew the session id.
// Time change will be picked up by the time subscription
setState((s) => ({
...s,
searchSessionId: data.search.session.start(),
}));
trackUiEvent('app_query_change');
}
setState((s) => ({
...s,
dateRange: {
fromDate: dateRange.from,
toDate: dateRange.to,
},
query: query || s.query,
}));
}}
@ -622,12 +635,6 @@ export function App({
setState((s) => ({
...s,
savedQuery: { ...savedQuery }, // Shallow query for reference issues
dateRange: savedQuery.attributes.timefilter
? {
fromDate: savedQuery.attributes.timefilter.from,
toDate: savedQuery.attributes.timefilter.to,
}
: s.dateRange,
}));
}}
onClearSavedQuery={() => {
@ -640,8 +647,8 @@ export function App({
}));
}}
query={state.query}
dateRangeFrom={state.dateRange.fromDate}
dateRangeTo={state.dateRange.toDate}
dateRangeFrom={fromDate}
dateRangeTo={toDate}
indicateNoData={state.indicateNoData}
/>
</div>
@ -650,7 +657,8 @@ export function App({
className="lnsApp__frame"
render={editorFrame.mount}
nativeProps={{
dateRange: state.dateRange,
searchSessionId: state.searchSessionId,
dateRange: currentDateRange,
query: state.query,
filters: state.filters,
savedQuery: state.savedQuery,

View file

@ -216,6 +216,7 @@ export async function mountApp(
params.element
);
return () => {
data.search.session.clear();
instance.unmount();
unmountComponentAtNode(params.element);
unlistenParentHistory();

View file

@ -55,16 +55,12 @@ export interface LensAppState {
// Determines whether the lens editor shows the 'save and return' button, and the originating app breadcrumb.
isLinkedToOriginatingApp?: boolean;
// Properties needed to interface with TopNav
dateRange: {
fromDate: string;
toDate: string;
};
query: Query;
filters: Filter[];
savedQuery?: SavedQuery;
isSaveable: boolean;
activeData?: TableInspectorAdapter;
searchSessionId: string;
}
export interface RedirectToOriginProps {

View file

@ -60,6 +60,7 @@ function getDefaultProps() {
},
palettes: chartPluginMock.createPaletteRegistry(),
showNoDataPopover: jest.fn(),
searchSessionId: 'sessionId',
};
}
@ -264,6 +265,7 @@ describe('editor_frame', () => {
filters: [],
dateRange: { fromDate: 'now-7d', toDate: 'now' },
availablePalettes: defaultProps.palettes,
searchSessionId: 'sessionId',
});
});

View file

@ -43,6 +43,7 @@ export interface EditorFrameProps {
query: Query;
filters: Filter[];
savedQuery?: SavedQuery;
searchSessionId: string;
onChange: (arg: {
filterableIndexPatterns: string[];
doc: Document;
@ -105,7 +106,7 @@ export function EditorFrame(props: EditorFrameProps) {
dateRange: props.dateRange,
query: props.query,
filters: props.filters,
searchSessionId: props.searchSessionId,
availablePalettes: props.palettes,
addNewLayer() {

View file

@ -39,6 +39,7 @@ describe('editor_frame state management', () => {
query: { query: '', language: 'lucene' },
filters: [],
showNoDataPopover: jest.fn(),
searchSessionId: 'sessionId',
};
});

View file

@ -273,16 +273,18 @@ export function SuggestionPanel({
const contextRef = useRef<ExecutionContextSearch>(context);
contextRef.current = context;
const sessionIdRef = useRef<string>(frame.searchSessionId);
sessionIdRef.current = frame.searchSessionId;
const AutoRefreshExpressionRenderer = useMemo(() => {
const autoRefreshFetch$ = plugins.data.query.timefilter.timefilter.getAutoRefreshFetch$();
return (props: ReactExpressionRendererProps) => (
<ExpressionRendererComponent
{...props}
searchContext={contextRef.current}
reload$={autoRefreshFetch$}
searchSessionId={sessionIdRef.current}
/>
);
}, [plugins.data.query.timefilter.timefilter, ExpressionRendererComponent]);
}, [ExpressionRendererComponent]);
const [lastSelectedSuggestion, setLastSelectedSuggestion] = useState<number>(-1);

View file

@ -362,8 +362,6 @@ export const InnerVisualizationWrapper = ({
};
ExpressionRendererComponent: ReactExpressionRendererType;
}) => {
const autoRefreshFetch$ = useMemo(() => timefilter.getAutoRefreshFetch$(), [timefilter]);
const context: ExecutionContextSearch = useMemo(
() => ({
query: framePublicAPI.query,
@ -482,7 +480,7 @@ export const InnerVisualizationWrapper = ({
padding="m"
expression={expression!}
searchContext={context}
reload$={autoRefreshFetch$}
searchSessionId={framePublicAPI.searchSessionId}
onEvent={onEvent}
onData$={onData$}
renderMode="edit"

View file

@ -132,6 +132,7 @@ export function createMockFramePublicAPI(): FrameMock {
get: () => palette,
getAll: () => [palette],
},
searchSessionId: 'sessionId',
};
}

View file

@ -57,6 +57,7 @@ describe('editor_frame service', () => {
indexPatternId: '1',
fieldName: 'test',
},
searchSessionId: 'sessionId',
});
instance.unmount();
})()
@ -78,6 +79,7 @@ describe('editor_frame service', () => {
query: { query: '', language: 'lucene' },
filters: [],
showNoDataPopover: jest.fn(),
searchSessionId: 'sessionId',
});
instance.unmount();

View file

@ -138,6 +138,7 @@ export class EditorFrameService {
onChange,
showNoDataPopover,
initialContext,
searchSessionId,
}
) => {
domElement = element;
@ -172,6 +173,7 @@ export class EditorFrameService {
onChange={onChange}
showNoDataPopover={showNoDataPopover}
initialContext={initialContext}
searchSessionId={searchSessionId}
/>
</I18nProvider>,
domElement

View file

@ -46,6 +46,7 @@ export interface EditorFrameProps {
query: Query;
filters: Filter[];
savedQuery?: SavedQuery;
searchSessionId: string;
initialContext?: VisualizeFieldContext;
// Frame loader (app or embeddable) is expected to call this when it loads and updates
@ -456,6 +457,7 @@ export interface FramePublicAPI {
dateRange: DateRange;
query: Query;
filters: Filter[];
searchSessionId: string;
/**
* A map of all available palettes (keys being the ids).