[Search] Use session service on a dashboard (#81297)

This commit is contained in:
Anton Dosov 2020-10-29 16:43:22 +01:00 committed by GitHub
parent 3ee6656837
commit 6deafd06b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 202 additions and 20 deletions

View file

@ -19,5 +19,6 @@ export declare type EmbeddableInput = {
timeRange?: TimeRange;
query?: Query;
filters?: Filter[];
searchSessionId?: string;
};
```

View file

@ -139,7 +139,7 @@ export class DashboardAppController {
dashboardCapabilities,
scopedHistory,
embeddableCapabilities: { visualizeCapabilities, mapsCapabilities },
data: { query: queryService },
data: { query: queryService, search: searchService },
core: {
notifications,
overlays,
@ -412,8 +412,9 @@ export class DashboardAppController {
>(DASHBOARD_CONTAINER_TYPE);
if (dashboardFactory) {
const searchSessionId = searchService.session.start();
dashboardFactory
.create(getDashboardInput())
.create({ ...getDashboardInput(), searchSessionId })
.then((container: DashboardContainer | ErrorEmbeddable | undefined) => {
if (container && !isErrorEmbeddable(container)) {
dashboardContainer = container;
@ -572,7 +573,7 @@ export class DashboardAppController {
differences.filters = appStateDashboardInput.filters;
}
Object.keys(_.omit(containerInput, ['filters'])).forEach((key) => {
Object.keys(_.omit(containerInput, ['filters', 'searchSessionId'])).forEach((key) => {
const containerValue = (containerInput as { [key: string]: unknown })[key];
const appStateValue = ((appStateDashboardInput as unknown) as { [key: string]: unknown })[
key
@ -590,7 +591,8 @@ export class DashboardAppController {
const refreshDashboardContainer = () => {
const changes = getChangesFromAppStateForContainerState();
if (changes && dashboardContainer) {
dashboardContainer.updateInput(changes);
const searchSessionId = searchService.session.start();
dashboardContainer.updateInput({ ...changes, searchSessionId });
}
};
@ -1109,12 +1111,6 @@ export class DashboardAppController {
$scope.model.filters = filterManager.getFilters();
$scope.model.query = queryStringManager.getQuery();
dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters);
if (dashboardContainer) {
dashboardContainer.updateInput({
filters: $scope.model.filters,
query: $scope.model.query,
});
}
},
});
@ -1159,6 +1155,7 @@ export class DashboardAppController {
if (dashboardContainer) {
dashboardContainer.destroy();
}
searchService.session.clear();
});
}
}

View file

@ -134,3 +134,25 @@ test('Container view mode change propagates to new children', async () => {
expect(embeddable.getInput().viewMode).toBe(ViewMode.EDIT);
});
test('searchSessionId propagates to children', async () => {
const searchSessionId1 = 'searchSessionId1';
const container = new DashboardContainer(
getSampleDashboardInput({ searchSessionId: searchSessionId1 }),
options
);
const embeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
ContactCardEmbeddable
>(CONTACT_CARD_EMBEDDABLE, {
firstName: 'Bob',
});
expect(embeddable.getInput().searchSessionId).toBe(searchSessionId1);
const searchSessionId2 = 'searchSessionId2';
container.updateInput({ searchSessionId: searchSessionId2 });
expect(embeddable.getInput().searchSessionId).toBe(searchSessionId2);
});

View file

@ -78,6 +78,7 @@ export interface InheritedChildInput extends IndexSignature {
viewMode: ViewMode;
hidePanelTitles?: boolean;
id: string;
searchSessionId?: string;
}
export interface DashboardContainerOptions {
@ -228,7 +229,15 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
}
protected getInheritedInput(id: string): InheritedChildInput {
const { viewMode, refreshConfig, timeRange, query, hidePanelTitles, filters } = this.input;
const {
viewMode,
refreshConfig,
timeRange,
query,
hidePanelTitles,
filters,
searchSessionId,
} = this.input;
return {
filters,
hidePanelTitles,
@ -237,6 +246,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
refreshConfig,
viewMode,
id,
searchSessionId,
};
}
}

View file

@ -65,6 +65,7 @@ export interface RequestHandlerParams {
metricsAtAllLevels?: boolean;
visParams?: any;
abortSignal?: AbortSignal;
searchSessionId?: string;
}
const name = 'esaggs';
@ -82,6 +83,7 @@ const handleCourierRequest = async ({
inspectorAdapters,
filterManager,
abortSignal,
searchSessionId,
}: RequestHandlerParams) => {
// Create a new search source that inherits the original search source
// but has the appropriate timeRange applied via a filter.
@ -143,6 +145,7 @@ const handleCourierRequest = async ({
defaultMessage:
'This request queries Elasticsearch to fetch the data for the visualization.',
}),
searchSessionId,
}
);
request.stats(getRequestInspectorStats(requestSearchSource));
@ -150,6 +153,7 @@ const handleCourierRequest = async ({
try {
const response = await requestSearchSource.fetch({
abortSignal,
sessionId: searchSessionId,
});
request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response });
@ -248,7 +252,7 @@ export const esaggs = (): EsaggsExpressionFunctionDefinition => ({
multi: true,
},
},
async fn(input, args, { inspectorAdapters, abortSignal }) {
async fn(input, args, { inspectorAdapters, abortSignal, getSearchSessionId }) {
const indexPatterns = getIndexPatterns();
const { filterManager } = getQueryService();
const searchService = getSearchService();
@ -276,6 +280,7 @@ export const esaggs = (): EsaggsExpressionFunctionDefinition => ({
inspectorAdapters: inspectorAdapters as Adapters,
filterManager,
abortSignal: (abortSignal as unknown) as AbortSignal,
searchSessionId: getSearchSessionId(),
});
const table: Datatable = {

View file

@ -266,6 +266,8 @@ export class SearchEmbeddable
}
private fetch = async () => {
const searchSessionId = this.input.searchSessionId;
if (!this.searchScope) return;
const { searchSource } = this.savedSearch;
@ -292,7 +294,11 @@ export class SearchEmbeddable
const description = i18n.translate('discover.embeddable.inspectorRequestDescription', {
defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.',
});
const inspectorRequest = this.inspectorAdaptors.requests.start(title, { description });
const inspectorRequest = this.inspectorAdaptors.requests.start(title, {
description,
searchSessionId,
});
inspectorRequest.stats(getRequestInspectorStats(searchSource));
searchSource.getSearchRequestBody().then((body: Record<string, unknown>) => {
inspectorRequest.json(body);
@ -303,6 +309,7 @@ export class SearchEmbeddable
// Make the request
const resp = await searchSource.fetch({
abortSignal: this.abortController.signal,
sessionId: searchSessionId,
});
this.updateOutput({ loading: false, error: undefined });

View file

@ -67,4 +67,9 @@ export type EmbeddableInput = {
* Visualization filters used to narrow down results.
*/
filters?: Filter[];
/**
* Search session id to group searches
*/
searchSessionId?: string;
};

View file

@ -25,6 +25,7 @@ import { EmbeddableOutput, EmbeddableInput } from './i_embeddable';
import { ViewMode } from '../types';
import { ContactCardEmbeddable } from '../test_samples/embeddables/contact_card/contact_card_embeddable';
import { FilterableEmbeddable } from '../test_samples/embeddables/filterable_embeddable';
import type { Filter } from '../../../../data/public';
class TestClass {
constructor() {}
@ -79,6 +80,20 @@ test('Embeddable reload is called if lastReloadRequest input time changes', asyn
expect(hello.reload).toBeCalledTimes(1);
});
test('Embeddable reload is called if lastReloadRequest input time changed and new input is used', async () => {
const hello = new FilterableEmbeddable({ id: '123', filters: [], lastReloadRequestTime: 0 });
const aFilter = ({} as unknown) as Filter;
hello.reload = jest.fn(() => {
// when reload is called embeddable already has new input
expect(hello.getInput().filters).toEqual([aFilter]);
});
hello.updateInput({ lastReloadRequestTime: 1, filters: [aFilter] });
expect(hello.reload).toBeCalledTimes(1);
});
test('Embeddable reload is not called if lastReloadRequest input time does not change', async () => {
const hello = new FilterableEmbeddable({ id: '123', filters: [], lastReloadRequestTime: 1 });

View file

@ -195,14 +195,15 @@ export abstract class Embeddable<
private onResetInput(newInput: TEmbeddableInput) {
if (!isEqual(this.input, newInput)) {
if (this.input.lastReloadRequestTime !== newInput.lastReloadRequestTime) {
this.reload();
}
const oldLastReloadRequestTime = this.input.lastReloadRequestTime;
this.input = newInput;
this.input$.next(newInput);
this.updateOutput({
title: getPanelTitle(this.input, this.output),
} as Partial<TEmbeddableOutput>);
if (oldLastReloadRequestTime !== newInput.lastReloadRequestTime) {
this.reload();
}
}
}

View file

@ -425,6 +425,7 @@ export type EmbeddableInput = {
timeRange?: TimeRange;
query?: Query;
filters?: Filter[];
searchSessionId?: string;
};
// Warning: (ae-missing-release-tag) "EmbeddableInstanceConfiguration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)

View file

@ -49,6 +49,7 @@ export interface Request extends RequestParams {
export interface RequestParams {
id?: string;
description?: string;
searchSessionId?: string;
}
export interface RequestStatistics {

View file

@ -153,6 +153,21 @@ export class RequestsViewComponent extends Component<InspectorViewProps, Request
</EuiText>
)}
{this.state.request && this.state.request.searchSessionId && (
<EuiText size="xs">
<p
data-test-subj={'inspectorRequestSearchSessionId'}
data-search-session-id={this.state.request.searchSessionId}
>
<FormattedMessage
id="inspector.requests.searchSessionId"
defaultMessage="Search session id: {searchSessionId}"
values={{ searchSessionId: this.state.request.searchSessionId }}
/>
</p>
</EuiText>
)}
<EuiSpacer size="m" />
{this.state.request && <RequestDetails request={this.state.request} />}

View file

@ -374,6 +374,7 @@ export class VisualizeEmbeddable
query: this.input.query,
filters: this.input.filters,
},
searchSessionId: this.input.searchSessionId,
uiState: this.vis.uiState,
inspectorAdapters: this.inspectorAdapters,
};

View file

@ -172,6 +172,7 @@ describe('embeddable', () => {
timeRange,
query,
filters,
searchSessionId: 'searchSessionId',
});
expect(expressionRenderer).toHaveBeenCalledTimes(2);
@ -182,7 +183,13 @@ describe('embeddable', () => {
const query: Query = { language: 'kquery', query: '' };
const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }];
const input = { savedObjectId: '123', timeRange, query, filters } as LensEmbeddableInput;
const input = {
savedObjectId: '123',
timeRange,
query,
filters,
searchSessionId: 'searchSessionId',
} as LensEmbeddableInput;
const embeddable = new Embeddable(
{
@ -214,6 +221,8 @@ describe('embeddable', () => {
filters,
})
);
expect(expressionRenderer.mock.calls[0][0].searchSessionId).toBe(input.searchSessionId);
});
it('should merge external context with query and filters of the saved object', async () => {

View file

@ -177,6 +177,7 @@ export class Embeddable
ExpressionRenderer={this.expressionRenderer}
expression={this.expression || null}
searchContext={this.getMergedSearchContext()}
searchSessionId={this.input.searchSessionId}
handleEvent={this.handleEvent}
/>,
domNode

View file

@ -19,6 +19,7 @@ export interface ExpressionWrapperProps {
ExpressionRenderer: ReactExpressionRendererType;
expression: string | null;
searchContext: ExecutionContextSearch;
searchSessionId?: string;
handleEvent: (event: ExpressionRendererEvent) => void;
}
@ -27,6 +28,7 @@ export function ExpressionWrapper({
expression,
searchContext,
handleEvent,
searchSessionId,
}: ExpressionWrapperProps) {
return (
<I18nProvider>
@ -51,6 +53,7 @@ export function ExpressionWrapper({
padding="m"
expression={expression}
searchContext={searchContext}
searchSessionId={searchSessionId}
renderError={(errorMessage, error) => (
<div data-test-subj="expression-renderer-error">
<EuiFlexGroup direction="column" alignItems="center" justifyContent="center">

View file

@ -12,6 +12,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const log = getService('log');
const PageObjects = getPageObjects(['common', 'header', 'dashboard', 'visChart']);
const dashboardPanelActions = getService('dashboardPanelActions');
const inspector = getService('inspector');
const queryBar = getService('queryBar');
describe('dashboard with async search', () => {
before(async function () {
@ -24,7 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('not delayed should load', async () => {
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.gotoDashboardEditMode('Not Delayed');
await PageObjects.dashboard.loadSavedDashboard('Not Delayed');
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.missingOrFail('embeddableErrorLabel');
const data = await PageObjects.visChart.getBarChartData('Sum of bytes');
@ -33,7 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('delayed should load', async () => {
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.gotoDashboardEditMode('Delayed 5s');
await PageObjects.dashboard.loadSavedDashboard('Delayed 5s');
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.missingOrFail('embeddableErrorLabel');
const data = await PageObjects.visChart.getBarChartData('');
@ -42,10 +45,47 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('timed out should show error', async () => {
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.gotoDashboardEditMode('Delayed 15s');
await PageObjects.dashboard.loadSavedDashboard('Delayed 15s');
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.existOrFail('embeddableErrorLabel');
await testSubjects.existOrFail('searchTimeoutError');
});
it('multiple searches are grouped and only single error popup is shown', async () => {
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.loadSavedDashboard('Multiple delayed');
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.existOrFail('embeddableErrorLabel');
// there should be two failed panels
expect((await testSubjects.findAll('embeddableErrorLabel')).length).to.be(2);
// but only single error toast because searches are grouped
expect((await testSubjects.findAll('searchTimeoutError')).length).to.be(1);
// check that session ids are the same
const getSearchSessionIdByPanel = async (panelTitle: string) => {
await dashboardPanelActions.openInspectorByTitle(panelTitle);
await inspector.openInspectorRequestsView();
const searchSessionId = await (
await testSubjects.find('inspectorRequestSearchSessionId')
).getAttribute('data-search-session-id');
await inspector.close();
return searchSessionId;
};
const panel1SessionId1 = await getSearchSessionIdByPanel('Sum of Bytes by Extension');
const panel2SessionId1 = await getSearchSessionIdByPanel(
'Sum of Bytes by Extension (Delayed 5s)'
);
expect(panel1SessionId1).to.be(panel2SessionId1);
await queryBar.clickQuerySubmitButton();
const panel1SessionId2 = await getSearchSessionIdByPanel('Sum of Bytes by Extension');
const panel2SessionId2 = await getSearchSessionIdByPanel(
'Sum of Bytes by Extension (Delayed 5s)'
);
expect(panel1SessionId2).to.be(panel2SessionId2);
expect(panel1SessionId1).not.to.be(panel1SessionId2);
});
});
}

View file

@ -194,4 +194,52 @@
}
}
{
"type": "doc",
"value": {
"id": "dashboard:a41c6790-075d-11eb-be70-0bd5e8b57d03",
"index": ".kibana",
"source": {
"dashboard": {
"description": "",
"hits": 0,
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
},
"optionsJSON": "{\"useMargins\":true,\"hidePanelTitles\":false}",
"panelsJSON": "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"ec585931-ce8e-43fd-aa94-a1a9612d24ba\"},\"panelIndex\":\"ec585931-ce8e-43fd-aa94-a1a9612d24ba\",\"embeddableConfig\":{},\"panelRefName\":\"panel_0\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"c7b18010-462b-4e55-a974-fdec2ae64b06\"},\"panelIndex\":\"c7b18010-462b-4e55-a974-fdec2ae64b06\",\"embeddableConfig\":{},\"panelRefName\":\"panel_1\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"e67704f7-20b7-4ade-8dee-972a9d187107\"},\"panelIndex\":\"e67704f7-20b7-4ade-8dee-972a9d187107\",\"embeddableConfig\":{},\"panelRefName\":\"panel_2\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"f0b03592-10f1-41cd-9929-0cb4163bcd16\"},\"panelIndex\":\"f0b03592-10f1-41cd-9929-0cb4163bcd16\",\"embeddableConfig\":{},\"panelRefName\":\"panel_3\"}]",
"refreshInterval": { "pause": true, "value": 0 },
"timeFrom": "2015-09-19T17:34:10.297Z",
"timeRestore": true,
"timeTo": "2015-09-23T00:09:17.180Z",
"title": "Multiple delayed",
"version": 1
},
"references": [
{
"id": "14501a50-01e3-11eb-9b63-176d7b28a352",
"name": "panel_0",
"type": "visualization"
},
{
"id": "50a67010-075d-11eb-be70-0bd5e8b57d02",
"name": "panel_1",
"type": "visualization"
},
{
"id": "6c9f3830-01e3-11eb-9b63-176d7b28a352",
"name": "panel_2",
"type": "visualization"
},
{
"id": "50a67010-075d-11eb-be70-0bd5e8b57d02",
"name": "panel_3",
"type": "visualization"
}
],
"type": "dashboard",
"updated_at": "2020-03-19T11:59:53.701Z"
}
}
}