Phase 1 of search services (#46742) (#47819)

* Phase 1 of search services

* First review feedback

* Start on tests

* Add functional tests for search explorer

* Add unload and fix ts error

* Add index.test.ts files for coverage completeness

* Adding unit tests

* use internal route terminology. No reason this should be a public route, at least not yet.

* Move search service into data plugin

* App mount search context needs to be optional

* Add more unit tests for server stuff

* wip types fix

* fix types for new context container stuff

* put back all jest test coverage paths

* address review comments

* delete the two test files that just tested the instantiation of the search service

* expose search fn on StartContract... tested locally only

* update mocks to account for new startcontract
This commit is contained in:
Stacey Gammon 2019-10-10 09:59:36 -04:00 committed by GitHub
parent 02240b27ef
commit 0419844ee3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
83 changed files with 3721 additions and 4 deletions

View file

@ -39,6 +39,8 @@ export default {
'<rootDir>/test/functional/services/remote',
],
collectCoverageFrom: [
'src/plugins/**/*.{ts,tsx}',
'!src/plugins/**/*.d.ts',
'packages/kbn-ui-framework/src/components/**/*.js',
'!packages/kbn-ui-framework/src/components/index.js',
'!packages/kbn-ui-framework/src/components/**/*/index.js',

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { IEsSearchRequest, IEsSearchResponse, ES_SEARCH_STRATEGY } from './types';

View file

@ -0,0 +1,30 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SearchParams, SearchResponse } from 'elasticsearch';
import { IKibanaSearchRequest, IKibanaSearchResponse } from '../types';
export const ES_SEARCH_STRATEGY = 'es';
export interface IEsSearchRequest extends IKibanaSearchRequest {
params: SearchParams;
}
export interface IEsSearchResponse<Hits = unknown> extends IKibanaSearchResponse {
rawResponse: SearchResponse<Hits>;
}

View file

@ -0,0 +1,26 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ES_SEARCH_STRATEGY } from './es_search';
export { IKibanaSearchResponse, IKibanaSearchRequest } from './types';
export const DEFAULT_SEARCH_STRATEGY = ES_SEARCH_STRATEGY;
export { IEsSearchRequest, IEsSearchResponse, ES_SEARCH_STRATEGY } from './es_search';

View file

@ -0,0 +1,55 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export interface IKibanaSearchResponse {
/**
* Some responses may contain a unique id to identify the request this response came from.
*/
id?: string;
/**
* If relevant to the search strategy, return a percentage
* that represents how progress is indicated.
*/
percentComplete?: number;
/**
* If relevant to the search strategy, return a total number
* that represents how progress is indicated.
*/
total?: number;
/**
* If relevant to the search strategy, return a loaded number
* that represents how progress is indicated.
*/
loaded?: number;
}
export interface IKibanaSearchRequest {
/**
* An id can be used to uniquely identify this request.
*/
id?: string;
/**
* Optionally tell search strategies to output debug information.
*/
debug?: boolean;
}

View file

@ -31,3 +31,6 @@ export * from '../common';
export * from './autocomplete_provider';
export * from './types';
export { IRequestTypesMap, IResponseTypesMap } from './search';
export * from './search';

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { Plugin } from '.';
import { searchSetupMock } from './search/mocks';
export type Setup = jest.Mocked<ReturnType<Plugin['setup']>>;
export type Start = jest.Mocked<ReturnType<Plugin['start']>>;
@ -30,6 +31,7 @@ const autocompleteMock: any = {
const createSetupContract = (): Setup => {
const setupContract: Setup = {
autocomplete: autocompleteMock as Setup['autocomplete'],
search: searchSetupMock,
};
return setupContract;
@ -39,6 +41,7 @@ const createStartContract = (): Start => {
const startContract: Start = {
autocomplete: autocompleteMock as Start['autocomplete'],
getSuggestions: jest.fn(),
search: { search: jest.fn() },
};
return startContract;
};

View file

@ -20,16 +20,21 @@
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public';
import { AutocompleteProviderRegister } from './autocomplete_provider';
import { DataPublicPluginSetup, DataPublicPluginStart } from './types';
import { SearchService } from './search/search_service';
import { getSuggestionsProvider } from './suggestions_provider';
export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPublicPluginStart> {
private readonly autocomplete = new AutocompleteProviderRegister();
private readonly searchService: SearchService;
constructor(initializerContext: PluginInitializerContext) {}
constructor(initializerContext: PluginInitializerContext) {
this.searchService = new SearchService(initializerContext);
}
public setup(core: CoreSetup): DataPublicPluginSetup {
return {
autocomplete: this.autocomplete,
search: this.searchService.setup(core),
};
}
@ -37,6 +42,7 @@ export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPubli
return {
autocomplete: this.autocomplete,
getSuggestions: getSuggestionsProvider(core.uiSettings, core.http),
search: this.searchService.start(core),
};
}

View file

@ -0,0 +1,13 @@
# search
The `search` plugin provides the ability to register search strategies that take in a request
object, and return a response object, of a given shape.
Both client side search strategies can be registered, as well as server side search strategies.
The `search` plugin includes two one concrete client side implementations -
`SYNC_SEARCH_STRATEGY` and `ES_SEARCH_STRATEGY` which uses `SYNC_SEARCH_STRATEGY`. There is also one
default server side search strategy, `ES_SEARCH_STRATEGY`.
Includes the `esSearch` plugin in order to search for data from Elasticsearch using Elasticsearch
DSL.

View file

@ -0,0 +1,69 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createAppMountSearchContext } from './create_app_mount_context_search';
import { from } from 'rxjs';
describe('Create app mount search context', () => {
it('Returns search fn when there are no strategies', () => {
const context = createAppMountSearchContext({});
expect(context.search).toBeDefined();
});
it(`Search throws an error when the strategy doesn't exist`, () => {
const context = createAppMountSearchContext({});
expect(() => context.search({}, {}, 'noexist').toPromise()).toThrowErrorMatchingInlineSnapshot(
`"Strategy with name noexist does not exist"`
);
});
it(`Search fn is called on appropriate strategy name`, done => {
const context = createAppMountSearchContext({
mysearch: search =>
Promise.resolve({
search: () => from(Promise.resolve({ percentComplete: 98 })),
}),
anothersearch: search =>
Promise.resolve({
search: () => from(Promise.resolve({ percentComplete: 0 })),
}),
});
context.search({}, {}, 'mysearch').subscribe(response => {
expect(response).toEqual({ percentComplete: 98 });
done();
});
});
it(`Search fn is called with the passed in request object`, done => {
const context = createAppMountSearchContext({
mysearch: search => {
return Promise.resolve({
search: request => {
expect(request).toEqual({ greeting: 'hi' });
return from(Promise.resolve({}));
},
});
},
});
context
.search({ greeting: 'hi' } as any, {}, 'mysearch')
.subscribe(response => {}, () => {}, done);
});
});

View file

@ -0,0 +1,57 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { mergeMap } from 'rxjs/operators';
import { from } from 'rxjs';
import { ISearchAppMountContext } from './i_search_app_mount_context';
import { ISearchGeneric } from './i_search';
import {
TSearchStrategiesMap,
ISearchStrategy,
TSearchStrategyProviderEnhanced,
} from './i_search_strategy';
import { TStrategyTypes } from './strategy_types';
import { DEFAULT_SEARCH_STRATEGY } from '../../common/search';
export const createAppMountSearchContext = (
searchStrategies: TSearchStrategiesMap
): ISearchAppMountContext => {
const getSearchStrategy = <K extends TStrategyTypes = typeof DEFAULT_SEARCH_STRATEGY>(
strategyName?: K
): Promise<ISearchStrategy<K>> => {
const strategyProvider = searchStrategies[
strategyName ? strategyName : DEFAULT_SEARCH_STRATEGY
] as TSearchStrategyProviderEnhanced<K> | undefined;
if (!strategyProvider) {
throw new Error(`Strategy with name ${strategyName} does not exist`);
}
return strategyProvider(search);
};
const search: ISearchGeneric = (request, options, strategyName) => {
const strategyPromise = getSearchStrategy(strategyName);
return from(strategyPromise).pipe(
mergeMap(strategy => {
return strategy.search(request, options);
})
);
};
return { search };
};

View file

@ -0,0 +1,43 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { coreMock } from '../../../../../core/public/mocks';
import { EsSearchService } from './es_search_service';
import { CoreSetup } from '../../../../../core/public';
import { searchSetupMock } from '../mocks';
describe('ES search strategy service', () => {
let service: EsSearchService;
let mockCoreSetup: MockedKeys<CoreSetup>;
const opaqueId = Symbol();
beforeEach(() => {
service = new EsSearchService({ opaqueId });
mockCoreSetup = coreMock.createSetup();
});
describe('setup()', () => {
it('registers the ES search strategy', async () => {
service.setup(mockCoreSetup, {
search: searchSetupMock,
});
expect(searchSetupMock.registerSearchStrategyProvider).toBeCalled();
});
});
});

View file

@ -0,0 +1,41 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Plugin, CoreSetup, PluginInitializerContext } from '../../../../../core/public';
import { ES_SEARCH_STRATEGY } from '../../../common/search/es_search';
import { esSearchStrategyProvider } from './es_search_strategy';
import { ISearchSetup } from '../i_search_setup';
export interface IEsSearchSetupDependencies {
search: ISearchSetup;
}
export class EsSearchService implements Plugin {
constructor(private initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup, deps: IEsSearchSetupDependencies) {
deps.search.registerSearchStrategyProvider(
this.initializerContext.opaqueId,
ES_SEARCH_STRATEGY,
esSearchStrategyProvider
);
}
public start() {}
public stop() {}
}

View file

@ -0,0 +1,52 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { coreMock } from '../../../../../core/public/mocks';
import { esSearchStrategyProvider } from './es_search_strategy';
import { CoreSetup } from 'kibana/public';
import { ES_SEARCH_STRATEGY } from '../../../common/search/es_search';
describe('ES search strategy', () => {
let mockCoreSetup: MockedKeys<CoreSetup>;
const mockSearch = jest.fn();
beforeEach(() => {
mockCoreSetup = coreMock.createSetup();
mockSearch.mockClear();
});
it('returns a strategy with `search` that calls the sync search `search`', () => {
const request = { params: {} };
const options = {};
const esSearch = esSearchStrategyProvider(
{
core: mockCoreSetup,
},
mockSearch
);
esSearch.search(request, options);
expect(mockSearch.mock.calls[0][0]).toEqual({
...request,
serverStrategy: ES_SEARCH_STRATEGY,
});
expect(mockSearch.mock.calls[0][1]).toBe(options);
});
});

View file

@ -0,0 +1,37 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Observable } from 'rxjs';
import { ES_SEARCH_STRATEGY, IEsSearchResponse } from '../../../common/search';
import { SYNC_SEARCH_STRATEGY } from '../sync_search_strategy';
import { TSearchStrategyProvider, ISearchStrategy, ISearchGeneric, ISearchContext } from '..';
export const esSearchStrategyProvider: TSearchStrategyProvider<typeof ES_SEARCH_STRATEGY> = (
context: ISearchContext,
search: ISearchGeneric
): ISearchStrategy<typeof ES_SEARCH_STRATEGY> => {
return {
search: (request, options) =>
search(
{ ...request, serverStrategy: ES_SEARCH_STRATEGY },
options,
SYNC_SEARCH_STRATEGY
) as Observable<IEsSearchResponse>,
};
};

View file

@ -0,0 +1,25 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { esSearchService } from '.';
it('es search service is instantiated', () => {
const esSearch = esSearchService({ opaqueId: Symbol() });
expect(esSearch).toBeDefined();
});

View file

@ -0,0 +1,25 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { PluginInitializer, PluginInitializerContext } from 'kibana/public';
import { EsSearchService } from './es_search_service';
export const esSearchService: PluginInitializer<void, void> = (
initializerContext: PluginInitializerContext
) => new EsSearchService(initializerContext);

View file

@ -0,0 +1,59 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Observable } from 'rxjs';
import { TStrategyTypes } from './strategy_types';
import {
DEFAULT_SEARCH_STRATEGY,
IKibanaSearchRequest,
IKibanaSearchResponse,
} from '../../common/search';
import { SYNC_SEARCH_STRATEGY, ISyncSearchRequest } from './sync_search_strategy';
import {
ES_SEARCH_STRATEGY,
IEsSearchRequest,
IEsSearchResponse,
} from '../../common/search/es_search';
export interface ISearchOptions {
signal?: AbortSignal;
}
export interface IRequestTypesMap {
[SYNC_SEARCH_STRATEGY]: ISyncSearchRequest;
[ES_SEARCH_STRATEGY]: IEsSearchRequest;
[key: string]: IKibanaSearchRequest;
}
export interface IResponseTypesMap {
[SYNC_SEARCH_STRATEGY]: IKibanaSearchResponse;
[ES_SEARCH_STRATEGY]: IEsSearchResponse;
[key: string]: IKibanaSearchResponse;
}
export type ISearchGeneric = <T extends TStrategyTypes = typeof DEFAULT_SEARCH_STRATEGY>(
request: IRequestTypesMap[T],
options: ISearchOptions,
strategy?: T
) => Observable<IResponseTypesMap[T]>;
export type ISearch<T extends TStrategyTypes = typeof DEFAULT_SEARCH_STRATEGY> = (
request: IRequestTypesMap[T],
options: ISearchOptions
) => Observable<IResponseTypesMap[T]>;

View file

@ -0,0 +1,24 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ISearchGeneric } from './i_search';
export interface ISearchAppMountContext {
search: ISearchGeneric;
}

View file

@ -0,0 +1,23 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { CoreSetup } from '../../../../core/public';
export interface ISearchContext {
core: CoreSetup;
}

View file

@ -0,0 +1,40 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { IContextProvider } from 'kibana/public';
import { ISearchContext } from './i_search_context';
import { TRegisterSearchStrategyProvider, TSearchStrategyProvider } from './i_search_strategy';
/**
* The setup contract exposed by the Search plugin exposes the search strategy extension
* point.
*/
export interface ISearchSetup {
registerSearchStrategyContext: <TContextName extends keyof ISearchContext>(
pluginId: symbol,
contextName: TContextName,
provider: IContextProvider<TSearchStrategyProvider<any>, TContextName>
) => void;
/**
* Extension point exposed for other plugins to register their own search
* strategies.
*/
registerSearchStrategyProvider: TRegisterSearchStrategyProvider;
}

View file

@ -0,0 +1,63 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ISearch, ISearchGeneric } from './i_search';
import { TStrategyTypes } from './strategy_types';
import { ISearchContext } from './i_search_context';
/**
* Search strategy interface contains a search method that takes in
* a request and returns a promise that resolves to a response.
*/
export interface ISearchStrategy<T extends TStrategyTypes> {
search: ISearch<T>;
}
/**
* Search strategy provider creates an instance of a search strategy with the request
* handler context bound to it. This way every search strategy can use
* whatever information they require from the request context.
*/
export type TSearchStrategyProviderEnhanced<T extends TStrategyTypes> = (
search: ISearchGeneric
) => Promise<ISearchStrategy<T>>;
/**
* Search strategy provider creates an instance of a search strategy with the request
* handler context bound to it. This way every search strategy can use
* whatever information they require from the request context.
*/
export type TSearchStrategyProvider<T extends TStrategyTypes> = (
context: ISearchContext,
search: ISearchGeneric
) => ISearchStrategy<T>;
/**
* Extension point exposed for other plugins to register their own search
* strategies.
*/
export type TRegisterSearchStrategyProvider = <T extends TStrategyTypes>(
opaqueId: symbol,
name: T,
searchStrategyProvider: TSearchStrategyProvider<T>
) => void;
export type TSearchStrategiesMap = {
[K in TStrategyTypes]?: TSearchStrategyProviderEnhanced<K>;
};

View file

@ -0,0 +1,42 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { ISearchAppMountContext } from './i_search_app_mount_context';
export { ISearchSetup } from './i_search_setup';
export { ISearchContext } from './i_search_context';
export {
ISearch,
ISearchOptions,
IRequestTypesMap,
IResponseTypesMap,
ISearchGeneric,
} from './i_search';
export { TSearchStrategyProvider, ISearchStrategy } from './i_search_strategy';
export { IEsSearchResponse, IEsSearchRequest, ES_SEARCH_STRATEGY } from '../../common/search';
export { SYNC_SEARCH_STRATEGY } from './sync_search_strategy';
export { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search';
export { ISearchStart } from './search_service';

View file

@ -0,0 +1,23 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export const searchSetupMock = {
registerSearchStrategyContext: jest.fn(),
registerSearchStrategyProvider: jest.fn(),
};

View file

@ -0,0 +1,41 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { coreMock } from '../../../../core/public/mocks';
import { SearchService } from './search_service';
import { CoreSetup } from '../../../../core/public';
describe('Search service', () => {
let searchService: SearchService;
let mockCoreSetup: MockedKeys<CoreSetup>;
const opaqueId = Symbol();
beforeEach(() => {
searchService = new SearchService({ opaqueId });
mockCoreSetup = coreMock.createSetup();
});
describe('setup()', () => {
it('exposes proper contract', async () => {
const setup = searchService.setup(mockCoreSetup);
expect(setup).toHaveProperty('registerSearchStrategyContext');
expect(setup).toHaveProperty('registerSearchStrategyProvider');
});
});
});

View file

@ -0,0 +1,126 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
Plugin,
CoreSetup,
PluginInitializerContext,
CoreStart,
IContextContainer,
PluginOpaqueId,
} from '../../../../core/public';
import { ISearchAppMountContext } from './i_search_app_mount_context';
import { ISearchSetup } from './i_search_setup';
import { createAppMountSearchContext } from './create_app_mount_context_search';
import { SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider } from './sync_search_strategy';
import {
TSearchStrategyProvider,
TRegisterSearchStrategyProvider,
TSearchStrategiesMap,
} from './i_search_strategy';
import { TStrategyTypes } from './strategy_types';
import { esSearchService } from './es_search';
import { ISearchGeneric } from './i_search';
/**
* Extends the AppMountContext so other plugins have access
* to search functionality in their applications.
*/
declare module 'kibana/public' {
interface AppMountContext {
search?: ISearchAppMountContext;
}
}
export interface ISearchStart {
search: ISearchGeneric;
}
/**
* The search plugin exposes two registration methods for other plugins:
* - registerSearchStrategyProvider for plugins to add their own custom
* search strategies
* - registerSearchStrategyContext for plugins to expose information
* and/or functionality for other search strategies to use
*
* It also comes with two search strategy implementations - SYNC_SEARCH_STRATEGY and ES_SEARCH_STRATEGY.
*/
export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
/**
* A mapping of search strategies keyed by a unique identifier. Plugins can use this unique identifier
* to override certain strategy implementations.
*/
private searchStrategies: TSearchStrategiesMap = {};
/**
* Exposes context to the search strategies.
*/
private contextContainer?: IContextContainer<TSearchStrategyProvider<any>>;
private search?: ISearchGeneric;
constructor(private initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup): ISearchSetup {
const search = (this.search = createAppMountSearchContext(this.searchStrategies).search);
core.application.registerMountContext<'search'>('search', () => {
return { search };
});
this.contextContainer = core.context.createContextContainer();
const registerSearchStrategyProvider: TRegisterSearchStrategyProvider = <
T extends TStrategyTypes
>(
plugin: PluginOpaqueId,
name: T,
strategyProvider: TSearchStrategyProvider<T>
) => {
this.searchStrategies[name] = this.contextContainer!.createHandler(plugin, strategyProvider);
};
const api = {
registerSearchStrategyContext: this.contextContainer!.registerContext,
registerSearchStrategyProvider,
};
api.registerSearchStrategyContext(this.initializerContext.opaqueId, 'core', () => core);
api.registerSearchStrategyProvider(
this.initializerContext.opaqueId,
SYNC_SEARCH_STRATEGY,
syncSearchStrategyProvider
);
// ES search capabilities are written in a way that it could easily be a separate plugin,
// however these two plugins are tightly coupled due to the default search strategy using
// es search types.
esSearchService(this.initializerContext).setup(core, { search: api });
return api;
}
public start(core: CoreStart) {
if (!this.search) {
throw new Error('Search should always be defined');
}
return { search: this.search };
}
public stop() {}
}

View file

@ -0,0 +1,40 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ES_SEARCH_STRATEGY } from '../../common/search/es_search';
import { SYNC_SEARCH_STRATEGY } from './sync_search_strategy';
/**
* Contains all known strategy type identifiers that will be used to map to
* request and response shapes. Plugins that wish to add their own custom search
* strategies should extend this type via:
*
* const MY_STRATEGY = 'MY_STRATEGY';
*
* declare module 'src/plugins/data/public' {
* export interface IRequestTypesMap {
* [MY_STRATEGY]: IMySearchRequest;
* }
*
* export interface IResponseTypesMap {
* [MY_STRATEGY]: IMySearchResponse
* }
* }
*/
export type TStrategyTypes = typeof SYNC_SEARCH_STRATEGY | typeof ES_SEARCH_STRATEGY | string;

View file

@ -0,0 +1,58 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { coreMock } from '../../../../core/public/mocks';
import { SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider } from './sync_search_strategy';
import { CoreSetup } from '../../../../core/public';
describe('Sync search strategy', () => {
let mockCoreSetup: MockedKeys<CoreSetup>;
const mockSearch = jest.fn();
beforeEach(() => {
mockCoreSetup = coreMock.createSetup();
});
it('returns a strategy with `search` that calls the backend API', () => {
mockCoreSetup.http.fetch.mockImplementationOnce(() => Promise.resolve());
const syncSearch = syncSearchStrategyProvider(
{
core: mockCoreSetup,
},
mockSearch
);
syncSearch.search(
{
serverStrategy: SYNC_SEARCH_STRATEGY,
},
{}
);
expect(mockCoreSetup.http.fetch.mock.calls[0][0]).toBe(
`/internal/search/${SYNC_SEARCH_STRATEGY}`
);
expect(mockCoreSetup.http.fetch.mock.calls[0][1]).toEqual({
body: JSON.stringify({
serverStrategy: 'SYNC_SEARCH_STRATEGY',
}),
method: 'POST',
signal: undefined,
});
});
});

View file

@ -0,0 +1,56 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { from } from 'rxjs';
import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../common/search';
import { ISearchContext } from './i_search_context';
import { ISearch, ISearchOptions } from './i_search';
import { TSearchStrategyProvider, ISearchStrategy } from './i_search_strategy';
export const SYNC_SEARCH_STRATEGY = 'SYNC_SEARCH_STRATEGY';
export interface ISyncSearchRequest extends IKibanaSearchRequest {
serverStrategy: string;
}
export const syncSearchStrategyProvider: TSearchStrategyProvider<typeof SYNC_SEARCH_STRATEGY> = (
context: ISearchContext
) => {
const search: ISearch<typeof SYNC_SEARCH_STRATEGY> = (
request: ISyncSearchRequest,
options: ISearchOptions
) => {
const response: Promise<IKibanaSearchResponse> = context.core.http.fetch(
`/internal/search/${request.serverStrategy}`,
{
method: 'POST',
body: JSON.stringify(request),
signal: options.signal,
}
);
return from(response);
};
const strategy: ISearchStrategy<typeof SYNC_SEARCH_STRATEGY> = {
search,
};
return strategy;
};

View file

@ -20,14 +20,17 @@
export * from './autocomplete_provider/types';
import { AutocompletePublicPluginSetup, AutocompletePublicPluginStart } from '.';
import { ISearchSetup, ISearchStart } from './search';
import { IGetSuggestions } from './suggestions_provider/types';
export interface DataPublicPluginSetup {
autocomplete: AutocompletePublicPluginSetup;
search: ISearchSetup;
}
export interface DataPublicPluginStart {
autocomplete: AutocompletePublicPluginStart;
getSuggestions: IGetSuggestions;
search: ISearchStart;
}
export { IGetSuggestions } from './suggestions_provider/types';

View file

@ -25,3 +25,7 @@ export function plugin(initializerContext: PluginInitializerContext) {
}
export { DataServerPlugin as Plugin };
export * from './search';
export { IRequestTypesMap, IResponseTypesMap } from './search';

View file

@ -18,10 +18,23 @@
*/
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/server';
import { ISearchSetup } from './search';
import { SearchService } from './search/search_service';
export class DataServerPlugin implements Plugin {
constructor(initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup) {}
export interface DataPluginSetup {
search: ISearchSetup;
}
export class DataServerPlugin implements Plugin<DataPluginSetup> {
private readonly searchService: SearchService;
constructor(initializerContext: PluginInitializerContext) {
this.searchService = new SearchService(initializerContext);
}
public setup(core: CoreSetup) {
return {
search: this.searchService.setup(core),
};
}
public start(core: CoreStart) {}
public stop() {}
}

View file

@ -0,0 +1,13 @@
# search
The `search` plugin provides the ability to register search strategies that take in a request
object, and return a response object, of a given shape.
Both client side search strategies can be registered, as well as server side search strategies.
The `search` plugin includes two one concrete client side implementations -
`SYNC_SEARCH_STRATEGY` and `ES_SEARCH_STRATEGY` which uses `SYNC_SEARCH_STRATEGY`. There is also one
default server side search strategy, `ES_SEARCH_STRATEGY`.
Includes the `esSearch` plugin in order to search for data from Elasticsearch using Elasticsearch
DSL.

View file

@ -0,0 +1,62 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createApi } from './create_api';
import { TSearchStrategiesMap } from './i_search_strategy';
import { IRouteHandlerSearchContext } from './i_route_handler_search_context';
import { DEFAULT_SEARCH_STRATEGY } from '../../common/search';
// let mockCoreSetup: MockedKeys<CoreSetup>;
const mockDefaultSearch = jest.fn(() => Promise.resolve({ percentComplete: 0 }));
const mockDefaultSearchStrategyProvider = jest.fn(() =>
Promise.resolve({
search: mockDefaultSearch,
})
);
const mockStrategies: TSearchStrategiesMap = {
[DEFAULT_SEARCH_STRATEGY]: mockDefaultSearchStrategyProvider,
};
describe('createApi', () => {
let api: IRouteHandlerSearchContext;
beforeEach(() => {
api = createApi({
caller: jest.fn(),
searchStrategies: mockStrategies,
});
mockDefaultSearchStrategyProvider.mockClear();
});
it('should default to DEFAULT_SEARCH_STRATEGY if none is provided', async () => {
await api.search({
params: {},
});
expect(mockDefaultSearchStrategyProvider).toBeCalled();
expect(mockDefaultSearch).toBeCalled();
});
it('should throw if no provider is found for the given name', () => {
expect(api.search({}, 'noneByThisName')).rejects.toThrowErrorMatchingInlineSnapshot(
`"No strategy found for noneByThisName"`
);
});
});

View file

@ -0,0 +1,45 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { APICaller } from 'kibana/server';
import { IRouteHandlerSearchContext } from './i_route_handler_search_context';
import { DEFAULT_SEARCH_STRATEGY } from '../../common/search';
import { TSearchStrategiesMap } from './i_search_strategy';
export function createApi({
caller,
searchStrategies,
}: {
searchStrategies: TSearchStrategiesMap;
caller: APICaller;
}) {
const api: IRouteHandlerSearchContext = {
search: async (request, strategyName) => {
const name = strategyName ? strategyName : DEFAULT_SEARCH_STRATEGY;
const strategyProvider = searchStrategies[name];
if (!strategyProvider) {
throw new Error(`No strategy found for ${strategyName}`);
}
// Give providers access to other search strategies by injecting this function
const strategy = await strategyProvider(caller, api.search);
return strategy.search(request);
},
};
return api;
}

View file

@ -0,0 +1,34 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export interface StringMap<T = unknown> {
[key: string]: T;
}
export type IndexAsString<Map> = {
[k: string]: Map[keyof Map];
} & Map;
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export interface BoolQuery {
must_not: Array<Record<string, any>>;
should: Array<Record<string, any>>;
filter: Array<Record<string, any>>;
}

View file

@ -0,0 +1,60 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { coreMock } from '../../../../../core/server/mocks';
import { EsSearchService } from './es_search_service';
import { PluginInitializerContext } from '../../../../../core/server';
import { searchSetupMock } from '../mocks';
describe('ES search strategy service', () => {
let service: EsSearchService;
const mockCoreSetup = coreMock.createSetup();
const opaqueId = Symbol();
const context: PluginInitializerContext = {
opaqueId,
config: {
createIfExists: jest.fn(),
create: jest.fn(),
},
env: {
mode: {
dev: false,
name: 'development',
prod: false,
},
},
logger: {
get: jest.fn(),
},
};
beforeEach(() => {
service = new EsSearchService(context);
});
describe('setup()', () => {
it('registers the ES search strategy', async () => {
service.setup(mockCoreSetup, {
search: searchSetupMock,
});
expect(searchSetupMock.registerSearchStrategyProvider).toBeCalled();
});
});
});

View file

@ -0,0 +1,42 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ISearchSetup } from '../i_search_setup';
import { PluginInitializerContext, CoreSetup, Plugin } from '../../../../../core/server';
import { esSearchStrategyProvider } from './es_search_strategy';
import { ES_SEARCH_STRATEGY } from '../../../common/search';
interface IEsSearchDependencies {
search: ISearchSetup;
}
export class EsSearchService implements Plugin<void, void, IEsSearchDependencies> {
constructor(private initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup, deps: IEsSearchDependencies) {
deps.search.registerSearchStrategyProvider(
this.initializerContext.opaqueId,
ES_SEARCH_STRATEGY,
esSearchStrategyProvider
);
}
public start() {}
public stop() {}
}

View file

@ -0,0 +1,102 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { coreMock } from '../../../../../core/server/mocks';
import { esSearchStrategyProvider } from './es_search_strategy';
describe('ES search strategy', () => {
const mockCoreSetup = coreMock.createSetup();
const mockApiCaller = jest.fn().mockResolvedValue({
_shards: {
total: 10,
failed: 1,
skipped: 2,
successful: 7,
},
});
const mockSearch = jest.fn();
beforeEach(() => {
mockApiCaller.mockClear();
mockSearch.mockClear();
});
it('returns a strategy with `search`', () => {
const esSearch = esSearchStrategyProvider(
{
core: mockCoreSetup,
},
mockApiCaller,
mockSearch
);
expect(typeof esSearch.search).toBe('function');
});
it('logs the response if `debug` is set to `true`', () => {
const spy = jest.spyOn(console, 'log');
const esSearch = esSearchStrategyProvider(
{
core: mockCoreSetup,
},
mockApiCaller,
mockSearch
);
expect(spy).not.toBeCalled();
esSearch.search({ params: {}, debug: true });
expect(spy).toBeCalled();
});
it('calls the API caller with the params', () => {
const params = { index: 'logstash-*' };
const esSearch = esSearchStrategyProvider(
{
core: mockCoreSetup,
},
mockApiCaller,
mockSearch
);
esSearch.search({ params });
expect(mockApiCaller).toBeCalled();
expect(mockApiCaller.mock.calls[0][0]).toBe('search');
expect(mockApiCaller.mock.calls[0][1]).toEqual(params);
});
it('returns total, loaded, and raw response', async () => {
const params = { index: 'logstash-*' };
const esSearch = esSearchStrategyProvider(
{
core: mockCoreSetup,
},
mockApiCaller,
mockSearch
);
const response = await esSearch.search({ params });
expect(response).toHaveProperty('total');
expect(response).toHaveProperty('loaded');
expect(response).toHaveProperty('rawResponse');
});
});

View file

@ -0,0 +1,53 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { APICaller } from 'kibana/server';
import { SearchResponse } from 'elasticsearch';
import { IEsSearchRequest, ES_SEARCH_STRATEGY } from '../../../common/search';
import { ISearchStrategy, TSearchStrategyProvider } from '../i_search_strategy';
import { ISearchContext } from '..';
export const esSearchStrategyProvider: TSearchStrategyProvider<typeof ES_SEARCH_STRATEGY> = (
context: ISearchContext,
caller: APICaller
): ISearchStrategy<typeof ES_SEARCH_STRATEGY> => {
return {
search: async (request: IEsSearchRequest) => {
if (request.debug) {
// eslint-disable-next-line
console.log(JSON.stringify(request, null, 2));
}
const esSearchResponse = (await caller('search', {
...request.params,
// TODO: could do something like this here?
// ...getCurrentSearchParams(context),
})) as SearchResponse<any>;
// The above query will either complete or timeout and throw an error.
// There is no progress indication on this api.
return {
total: esSearchResponse._shards.total,
loaded:
esSearchResponse._shards.failed +
esSearchResponse._shards.skipped +
esSearchResponse._shards.successful,
rawResponse: esSearchResponse,
};
},
};
};

View file

@ -0,0 +1,27 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { PluginInitializerContext } from '../../../../../core/server';
import { EsSearchService } from './es_search_service';
export { ES_SEARCH_STRATEGY, IEsSearchRequest, IEsSearchResponse } from '../../../common/search';
export function esSearchService(initializerContext: PluginInitializerContext) {
return new EsSearchService(initializerContext);
}

View file

@ -0,0 +1,24 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ISearchGeneric } from './i_search';
export interface IRouteHandlerSearchContext {
search: ISearchGeneric;
}

View file

@ -0,0 +1,42 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search';
import { TStrategyTypes } from './strategy_types';
import { ES_SEARCH_STRATEGY, IEsSearchResponse } from '../../common/search/es_search';
import { IEsSearchRequest } from './es_search';
export interface IRequestTypesMap {
[ES_SEARCH_STRATEGY]: IEsSearchRequest;
[key: string]: IKibanaSearchRequest;
}
export interface IResponseTypesMap {
[ES_SEARCH_STRATEGY]: IEsSearchResponse;
[key: string]: IKibanaSearchResponse;
}
export type ISearchGeneric = <T extends TStrategyTypes = typeof ES_SEARCH_STRATEGY>(
request: IRequestTypesMap[T],
strategy?: T
) => Promise<IResponseTypesMap[T]>;
export type ISearch<T extends TStrategyTypes> = (
request: IRequestTypesMap[T]
) => Promise<IResponseTypesMap[T]>;

View file

@ -0,0 +1,23 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { CoreSetup } from '../../../../core/server';
export interface ISearchContext {
core: CoreSetup;
}

View file

@ -0,0 +1,51 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { IContextProvider, APICaller } from 'kibana/server';
import { ISearchContext } from './i_search_context';
import { IResponseTypesMap, IRequestTypesMap } from './i_search';
import { TRegisterSearchStrategyProvider, TSearchStrategyProvider } from './i_search_strategy';
import { TStrategyTypes } from './strategy_types';
import { DEFAULT_SEARCH_STRATEGY } from '../../common/search';
/**
* The setup contract exposed by the Search plugin exposes the search strategy extension
* point.
*/
export interface ISearchSetup {
registerSearchStrategyContext: <TContextName extends keyof ISearchContext>(
pluginId: symbol,
strategyName: TContextName,
provider: IContextProvider<TSearchStrategyProvider<any>, TContextName>
) => void;
/**
* Extension point exposed for other plugins to register their own search
* strategies.
*/
registerSearchStrategyProvider: TRegisterSearchStrategyProvider;
__LEGACY: {
search: <T extends TStrategyTypes = typeof DEFAULT_SEARCH_STRATEGY>(
caller: APICaller,
request: IRequestTypesMap[T],
strategyName?: T
) => Promise<IResponseTypesMap[T]>;
};
}

View file

@ -0,0 +1,66 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { APICaller } from 'kibana/server';
import { ISearch, ISearchGeneric } from './i_search';
import { TStrategyTypes } from './strategy_types';
import { ISearchContext } from './i_search_context';
/**
* Search strategy interface contains a search method that takes in
* a request and returns a promise that resolves to a response.
*/
export interface ISearchStrategy<T extends TStrategyTypes> {
search: ISearch<T>;
}
/**
* Search strategy provider creates an instance of a search strategy with the request
* handler context bound to it. This way every search strategy can use
* whatever information they require from the request context.
*/
export type TSearchStrategyProviderEnhanced<T extends TStrategyTypes> = (
caller: APICaller,
search: ISearchGeneric
) => Promise<ISearchStrategy<T>>;
/**
* Search strategy provider creates an instance of a search strategy with the request
* handler context bound to it. This way every search strategy can use
* whatever information they require from the request context.
*/
export type TSearchStrategyProvider<T extends TStrategyTypes> = (
context: ISearchContext,
caller: APICaller,
search: ISearchGeneric
) => ISearchStrategy<T>;
/**
* Extension point exposed for other plugins to register their own search
* strategies.
*/
export type TRegisterSearchStrategyProvider = <T extends TStrategyTypes>(
opaqueId: symbol,
name: T,
searchStrategyProvider: TSearchStrategyProvider<T>
) => void;
export type TSearchStrategiesMap = {
[K in TStrategyTypes]?: TSearchStrategyProviderEnhanced<K>;
};

View file

@ -0,0 +1,29 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { ISearchSetup } from './i_search_setup';
export * from '../../common';
export { ISearchContext } from './i_search_context';
export { IRequestTypesMap, IResponseTypesMap } from './i_search';
export { TStrategyTypes } from './strategy_types';
export { TSearchStrategyProvider } from './i_search_strategy';

View file

@ -0,0 +1,26 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export const searchSetupMock = {
registerSearchStrategyContext: jest.fn(),
registerSearchStrategyProvider: jest.fn(),
__LEGACY: {
search: jest.fn(),
},
};

View file

@ -0,0 +1,99 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { httpServiceMock, httpServerMock } from '../../../../../src/core/server/mocks';
import { registerSearchRoute } from './routes';
import { IRouter, ScopedClusterClient } from 'kibana/server';
describe('Search service', () => {
let routerMock: jest.Mocked<IRouter>;
beforeEach(() => {
routerMock = httpServiceMock.createRouter();
});
it('registers a post route', async () => {
registerSearchRoute(routerMock);
expect(routerMock.post).toBeCalled();
});
it('handler calls context.search.search with the given request and strategy', async () => {
const mockSearch = jest.fn().mockResolvedValue('yay');
const mockContext = {
core: {
elasticsearch: {
dataClient: {} as ScopedClusterClient,
adminClient: {} as ScopedClusterClient,
},
},
search: {
search: mockSearch,
},
};
const mockBody = { params: {} };
const mockParams = { strategy: 'foo' };
const mockRequest = httpServerMock.createKibanaRequest({
body: mockBody,
params: mockParams,
});
const mockResponse = httpServerMock.createResponseFactory();
registerSearchRoute(routerMock);
const handler = routerMock.post.mock.calls[0][1];
await handler(mockContext, mockRequest, mockResponse);
expect(mockSearch).toBeCalled();
expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody);
expect(mockSearch.mock.calls[0][1]).toBe(mockParams.strategy);
expect(mockResponse.ok).toBeCalled();
expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: 'yay' });
});
it('handler throws internal error if the search throws an error', async () => {
const mockSearch = jest.fn().mockRejectedValue('oh no');
const mockContext = {
core: {
elasticsearch: {
dataClient: {} as ScopedClusterClient,
adminClient: {} as ScopedClusterClient,
},
},
search: {
search: mockSearch,
},
};
const mockBody = { params: {} };
const mockParams = { strategy: 'foo' };
const mockRequest = httpServerMock.createKibanaRequest({
body: mockBody,
params: mockParams,
});
const mockResponse = httpServerMock.createResponseFactory();
registerSearchRoute(routerMock);
const handler = routerMock.post.mock.calls[0][1];
await handler(mockContext, mockRequest, mockResponse);
expect(mockSearch).toBeCalled();
expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody);
expect(mockSearch.mock.calls[0][1]).toBe(mockParams.strategy);
expect(mockResponse.internalError).toBeCalled();
expect(mockResponse.internalError.mock.calls[0][0]).toEqual({ body: 'oh no' });
});
});

View file

@ -0,0 +1,46 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../../../core/server';
export function registerSearchRoute(router: IRouter): void {
router.post(
{
path: '/internal/search/{strategy}',
validate: {
params: schema.object({ strategy: schema.string() }),
query: schema.object({}, { allowUnknowns: true }),
body: schema.object({}, { allowUnknowns: true }),
},
},
async (context, request, res) => {
const searchRequest = request.body;
const strategy = request.params.strategy;
try {
const response = await context.search!.search(searchRequest, strategy);
return res.ok({ body: response });
} catch (err) {
return res.internalError({ body: err });
}
}
);
}

View file

@ -0,0 +1,56 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { coreMock } from '../../../../core/server/mocks';
import { SearchService } from './search_service';
import { CoreSetup } from '../../../../core/server';
const mockSearchApi = { search: jest.fn() };
jest.mock('./create_api', () => ({
createApi: () => mockSearchApi,
}));
describe('Search service', () => {
let plugin: SearchService;
let mockCoreSetup: MockedKeys<CoreSetup>;
beforeEach(() => {
plugin = new SearchService(coreMock.createPluginInitializerContext({}));
mockCoreSetup = coreMock.createSetup();
mockSearchApi.search.mockClear();
});
describe('setup()', () => {
it('exposes proper contract', async () => {
const setup = plugin.setup(mockCoreSetup);
expect(setup).toHaveProperty('registerSearchStrategyContext');
expect(setup).toHaveProperty('registerSearchStrategyProvider');
expect(setup).toHaveProperty('__LEGACY');
});
});
describe('__LEGACY', () => {
it('calls searchAPI.search', async () => {
const setup = plugin.setup(mockCoreSetup);
setup.__LEGACY.search(jest.fn(), {}, 'foo');
expect(mockSearchApi.search).toBeCalled();
});
});
});

View file

@ -0,0 +1,97 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
PluginInitializerContext,
Plugin,
CoreSetup,
IContextContainer,
} from '../../../../core/server';
import { registerSearchRoute } from './routes';
import { ISearchSetup } from './i_search_setup';
import { createApi } from './create_api';
import {
TSearchStrategiesMap,
TSearchStrategyProvider,
TRegisterSearchStrategyProvider,
} from './i_search_strategy';
import { IRouteHandlerSearchContext } from './i_route_handler_search_context';
import { esSearchService } from './es_search';
declare module 'kibana/server' {
interface RequestHandlerContext {
search?: IRouteHandlerSearchContext;
}
}
export class SearchService implements Plugin<ISearchSetup, void> {
private searchStrategies: TSearchStrategiesMap = {};
private contextContainer?: IContextContainer<TSearchStrategyProvider<any>>;
constructor(private initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup): ISearchSetup {
const router = core.http.createRouter();
registerSearchRoute(router);
this.contextContainer = core.context.createContextContainer();
core.http.registerRouteHandlerContext<'search'>('search', context => {
return createApi({
caller: context.core!.elasticsearch.dataClient.callAsCurrentUser,
searchStrategies: this.searchStrategies,
});
});
const registerSearchStrategyProvider: TRegisterSearchStrategyProvider = (
plugin,
name,
strategyProvider
) => {
this.searchStrategies[name] = this.contextContainer!.createHandler(plugin, strategyProvider);
};
const api: ISearchSetup = {
registerSearchStrategyContext: this.contextContainer!.registerContext,
registerSearchStrategyProvider,
__LEGACY: {
search: (caller, request, strategyName) => {
const searchAPI = createApi({
caller,
searchStrategies: this.searchStrategies,
});
return searchAPI.search(request, strategyName);
},
},
};
api.registerSearchStrategyContext(this.initializerContext.opaqueId, 'core', () => core);
// ES search capabilities are written in a way that it could easily be a separate plugin,
// however these two plugins are tightly coupled due to the default search strategy using
// es search types.
esSearchService(this.initializerContext).setup(core, { search: api });
return api;
}
public start() {}
public stop() {}
}

View file

@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ES_SEARCH_STRATEGY } from '../../common/search/es_search';
/**
* Contains all known strategy type identifiers that will be used to map to
* request and response shapes. Plugins that wish to add their own custom search
* strategies should extend this type via:
*
* const MY_STRATEGY = 'MY_STRATEGY';
*
* declare module 'src/plugins/search/server' {
* export interface IRequestTypesMap {
* [MY_STRATEGY]: IMySearchRequest;
* }
*
* export interface IResponseTypesMap {
* [MY_STRATEGY]: IMySearchResponse
* }
* }
*/
export type TStrategyTypes = typeof ES_SEARCH_STRATEGY | string;

View file

@ -213,6 +213,17 @@ export async function FindProvider({ getService }: FtrProviderContext) {
return await this.filterElementIsDisplayed(allElements);
}
public async allDescendantDisplayedByTagName(
tagName: string,
parentElement: WebElementWrapper
): Promise<WebElementWrapper[]> {
log.debug(`Find.allDescendantDisplayedByTagName('${tagName}')`);
const allElements = await wrapAll(
await parentElement._webElement.findElements(By.tagName(tagName))
);
return await this.filterElementIsDisplayed(allElements);
}
public async displayedByLinkText(
linkText: string,
timeout: number = defaultFindTimeout

View file

@ -72,6 +72,27 @@ export function TestSubjectsProvider({ getService }: FtrProviderContext) {
: find.waitForDeletedByCssSelector(testSubjSelector(selector), timeout));
}
async stringExistsInCodeBlockOrFail(codeBlockSelector: string, stringToFind: string) {
await retry.try(async () => {
const responseCodeBlock = await this.find(codeBlockSelector);
const spans = await find.allDescendantDisplayedByTagName('span', responseCodeBlock);
const foundInSpans = await Promise.all(
spans.map(async span => {
const text = await span.getVisibleText();
if (text === stringToFind) {
log.debug(`"${text}" matched "${stringToFind}"!`);
return true;
} else {
log.debug(`"${text}" did not match "${stringToFind}"`);
}
})
);
if (!foundInSpans.find(foundInSpan => foundInSpan)) {
throw new Error(`"${stringToFind}" was not found. Trying again...`);
}
});
}
public async append(selector: string, text: string): Promise<void> {
return await retry.try(async () => {
log.debug(`TestSubjects.append(${selector}, ${text})`);

View file

@ -33,6 +33,7 @@ export default async function ({ readConfigFile }) {
require.resolve('./test_suites/custom_visualizations'),
require.resolve('./test_suites/embedding_visualizations'),
require.resolve('./test_suites/panel_actions'),
require.resolve('./test_suites/search'),
/**
* @todo Work on re-enabling this test suite after this is merged. These tests pass

View file

@ -0,0 +1,34 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
IKibanaSearchRequest,
IKibanaSearchResponse,
} from '../../../../../src/plugins/data/common/search';
export const DEMO_SEARCH_STRATEGY = 'DEMO_SEARCH_STRATEGY';
export interface IDemoRequest extends IKibanaSearchRequest {
mood: string | 'sad' | 'happy';
name: string;
}
export interface IDemoResponse extends IKibanaSearchResponse {
greeting: string;
}

View file

@ -0,0 +1,10 @@
{
"id": "demoSearch",
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": ["demo_search"],
"server": true,
"ui": true,
"requiredPlugins": ["data"],
"optionalPlugins": []
}

View file

@ -0,0 +1,17 @@
{
"name": "demo_data_search",
"version": "1.0.0",
"main": "target/test/plugin_functional/plugins/demo_data_search",
"kibana": {
"version": "kibana",
"templateVersion": "1.0.0"
},
"license": "Apache-2.0",
"scripts": {
"kbn": "node ../../../../scripts/kbn.js",
"build": "rm -rf './target' && tsc"
},
"devDependencies": {
"typescript": "3.5.3"
}
}

View file

@ -0,0 +1,70 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Observable } from 'rxjs';
import {
ISearchContext,
SYNC_SEARCH_STRATEGY,
ISearchGeneric,
} from '../../../../../src/plugins/data/public';
import { TSearchStrategyProvider, ISearchStrategy } from '../../../../../src/plugins/data/public';
import { DEMO_SEARCH_STRATEGY, IDemoResponse } from '../common';
/**
* This demo search strategy provider simply provides a shortcut for calling the DEMO_SEARCH_STRATEGY
* on the server side, without users having to pass it in explicitly, and it takes advantage of the
* already registered SYNC_SEARCH_STRATEGY that exists on the client.
*
* so instead of callers having to do:
*
* ```
* context.search(
* { ...request, serverStrategy: DEMO_SEARCH_STRATEGY },
* options,
* SYNC_SEARCH_STRATEGY
* ) as Observable<IDemoResponse>,
*```
* They can instead just do
*
* ```
* context.search(request, options, DEMO_SEARCH_STRATEGY);
* ```
*
* and are ensured type safety in regard to the request and response objects.
*
* @param context - context supplied by other plugins.
* @param search - a search function to access other strategies that have already been registered.
*/
export const demoClientSearchStrategyProvider: TSearchStrategyProvider<
typeof DEMO_SEARCH_STRATEGY
> = (
context: ISearchContext,
search: ISearchGeneric
): ISearchStrategy<typeof DEMO_SEARCH_STRATEGY> => {
return {
search: (request, options) =>
search(
{ ...request, serverStrategy: DEMO_SEARCH_STRATEGY },
options,
SYNC_SEARCH_STRATEGY
) as Observable<IDemoResponse>,
};
};

View file

@ -0,0 +1,28 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { PluginInitializer, PluginInitializerContext } from 'kibana/public';
import { DemoDataPlugin } from './plugin';
export { DEMO_SEARCH_STRATEGY } from '../common';
export const plugin: PluginInitializer<void, void> = (
initializerContext: PluginInitializerContext
) => new DemoDataPlugin(initializerContext);

View file

@ -0,0 +1,61 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public';
import { Plugin, CoreSetup, PluginInitializerContext } from '../../../../../src/core/public';
import { DEMO_SEARCH_STRATEGY } from '../common';
import { demoClientSearchStrategyProvider } from './demo_search_strategy';
import { IDemoRequest, IDemoResponse } from '../common';
interface DemoDataSearchSetupDependencies {
data: DataPublicPluginSetup;
}
/**
* Add the typescript mappings for our search strategy to the request and
* response types. This allows typescript to require the right shapes if
* making the call:
* const response = context.search.search(request, {}, DEMO_SEARCH_STRATEGY);
*
* If the caller does not pass in the right `request` shape, typescript will
* complain. The caller will also get a typed response.
*/
declare module '../../../../../src/plugins/data/public' {
export interface IRequestTypesMap {
[DEMO_SEARCH_STRATEGY]: IDemoRequest;
}
export interface IResponseTypesMap {
[DEMO_SEARCH_STRATEGY]: IDemoResponse;
}
}
export class DemoDataPlugin implements Plugin {
constructor(private initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup, deps: DemoDataSearchSetupDependencies) {
deps.data.search.registerSearchStrategyProvider(
this.initializerContext.opaqueId,
DEMO_SEARCH_STRATEGY,
demoClientSearchStrategyProvider
);
}
public start() {}
public stop() {}
}

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export const FAKE_PROGRESS_STRATEGY = 'FAKE_PROGRESS_STRATEGY';

View file

@ -0,0 +1,36 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { TSearchStrategyProvider } from 'src/plugins/data/server';
import { DEMO_SEARCH_STRATEGY } from '../common';
export const demoSearchStrategyProvider: TSearchStrategyProvider<
typeof DEMO_SEARCH_STRATEGY
> = () => {
return {
search: request => {
return Promise.resolve({
greeting:
request.mood === 'happy'
? `Lovely to meet you, ${request.name}`
: `Hope you feel better, ${request.name}`,
});
},
};
};

View file

@ -0,0 +1,25 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { PluginInitializerContext, PluginInitializer } from 'kibana/server';
import { DemoDataPlugin } from './plugin';
export const plugin: PluginInitializer<void, void> = (
initializerContext: PluginInitializerContext
) => new DemoDataPlugin(initializerContext);

View file

@ -0,0 +1,61 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/server';
import { DataPluginSetup } from 'src/plugins/data/server/plugin';
import { demoSearchStrategyProvider } from './demo_search_strategy';
import { DEMO_SEARCH_STRATEGY, IDemoRequest, IDemoResponse } from '../common';
interface IDemoSearchExplorerDeps {
data: DataPluginSetup;
}
/**
* Add the typescript mappings for our search strategy to the request and
* response types. This allows typescript to require the right shapes if
* making the call:
* const response = context.search.search(request, DEMO_SEARCH_STRATEGY);
*
* If the caller does not pass in the right `request` shape, typescript will
* complain. The caller will also get a typed response.
*/
declare module '../../../../../src/plugins/data/server' {
export interface IRequestTypesMap {
[DEMO_SEARCH_STRATEGY]: IDemoRequest;
}
export interface IResponseTypesMap {
[DEMO_SEARCH_STRATEGY]: IDemoResponse;
}
}
export class DemoDataPlugin implements Plugin<void, void, IDemoSearchExplorerDeps> {
constructor(private initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup, deps: IDemoSearchExplorerDeps) {
deps.data.search.registerSearchStrategyProvider(
this.initializerContext.opaqueId,
DEMO_SEARCH_STRATEGY,
demoSearchStrategyProvider
);
}
public start() {}
public stop() {}
}

View file

@ -0,0 +1,16 @@
{
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"common/**/*.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../../../typings/**/*"
],
"exclude": []
}

View file

@ -0,0 +1,10 @@
{
"id": "search_explorer",
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": ["search_explorer"],
"server": false,
"ui": true,
"requiredPlugins": ["data", "demoSearch"],
"optionalPlugins": []
}

View file

@ -0,0 +1,17 @@
{
"name": "search_explorer",
"version": "1.0.0",
"main": "target/test/plugin_functional/plugins/search_explorer",
"kibana": {
"version": "kibana",
"templateVersion": "1.0.0"
},
"license": "Apache-2.0",
"scripts": {
"kbn": "node ../../../../scripts/kbn.js",
"build": "rm -rf './target' && tsc"
},
"devDependencies": {
"typescript": "3.5.3"
}
}

View file

@ -0,0 +1,122 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, withRouter, RouteComponentProps } from 'react-router-dom';
import {
EuiPage,
EuiPageSideBar,
// @ts-ignore
EuiSideNav,
} from '@elastic/eui';
import { AppMountContext, AppMountParameters } from '../../../../../src/core/public';
import { EsSearchTest } from './es_strategy';
import { Page } from './page';
import { DemoStrategy } from './demo_strategy';
import { DocumentationPage } from './documentation';
import { SearchApiPage } from './search_api';
const Home = () => <DocumentationPage />;
interface PageDef {
title: string;
id: string;
component: React.ReactNode;
}
type NavProps = RouteComponentProps & {
navigateToApp: AppMountContext['core']['application']['navigateToApp'];
pages: PageDef[];
};
const Nav = withRouter(({ history, navigateToApp, pages }: NavProps) => {
const navItems = pages.map(page => ({
id: page.id,
name: page.title,
onClick: () => history.push(`/${page.id}`),
'data-test-subj': page.id,
}));
return (
<EuiSideNav
items={[
{
name: 'Search explorer',
id: 'home',
items: [...navItems],
},
]}
/>
);
});
const buildPage = (page: PageDef) => <Page title={page.title}>{page.component}</Page>;
const SearchApp = ({ basename, context }: { basename: string; context: AppMountContext }) => {
const pages: PageDef[] = [
{
id: 'home',
title: 'Home',
component: <Home />,
},
{
title: 'Search API',
id: 'searchAPI',
component: <SearchApiPage />,
},
{
title: 'ES search strategy',
id: 'esSearch',
component: <EsSearchTest search={context.search!.search} />,
},
{
title: 'Demo search strategy',
id: 'demoSearch',
component: <DemoStrategy search={context.search!.search} />,
},
];
const routes = pages.map((page, i) => (
<Route key={i} path={`/${page.id}`} render={props => buildPage(page)} />
));
return (
<Router basename={basename}>
<EuiPage>
<EuiPageSideBar>
<Nav navigateToApp={context.core.application.navigateToApp} pages={pages} />
</EuiPageSideBar>
<Route path="/" exact component={Home} />
{routes}
</EuiPage>
</Router>
);
};
export const renderApp = (
context: AppMountContext,
{ appBasePath, element }: AppMountParameters
) => {
ReactDOM.render(<SearchApp basename={appBasePath} context={context} />, element);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -0,0 +1,134 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import {
EuiPageContentBody,
EuiFormRow,
EuiFlexItem,
EuiFlexGroup,
EuiFieldText,
} from '@elastic/eui';
import { ISearchGeneric } from '../../../../../src/plugins/data/public';
import { DoSearch } from './do_search';
import { GuideSection } from './guide_section';
import { DEMO_SEARCH_STRATEGY } from '../../demo_search/public';
import { IDemoResponse, IDemoRequest } from '../../demo_search/common';
// @ts-ignore
import doSearch from '!!raw-loader!./do_search.tsx';
// @ts-ignore
import demoStrategyServerProvider from '!!raw-loader!./../../demo_search/server/demo_search_strategy';
// @ts-ignore
import demoStrategyPublicProvider from '!!raw-loader!./../../demo_search/public/demo_search_strategy';
// @ts-ignore
import demoStrategyServerPlugin from '!!raw-loader!./../../demo_search/server/plugin';
// @ts-ignore
import demoStrategyPublicPlugin from '!!raw-loader!./../../demo_search/public/plugin';
interface Props {
search: ISearchGeneric;
}
interface State {
results?: IDemoResponse;
searching: boolean;
name: string;
mood: string;
changes: boolean;
error?: any;
}
export class DemoStrategy extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
searching: false,
changes: false,
name: 'Molly',
mood: 'happy',
};
}
renderDemo = () => {
const request: IDemoRequest = {
name: this.state.name,
mood: this.state.mood,
};
return (
<React.Fragment>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow label="What is your name?">
<EuiFieldText
value={this.state.name}
onChange={e => this.setState({ name: e.target.value })}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow label="How are you feeling today?">
<EuiFieldText
value={this.state.mood}
onChange={e => this.setState({ mood: e.target.value })}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<DoSearch
request={request}
strategy={DEMO_SEARCH_STRATEGY}
search={(signal: AbortSignal) =>
this.props.search(request, { signal }, DEMO_SEARCH_STRATEGY)
}
/>
</React.Fragment>
);
};
render() {
return (
<EuiPageContentBody>
<GuideSection
codeSections={[
{
title: 'Public',
code: [
{ description: 'plugin.ts', snippet: demoStrategyPublicPlugin },
{ description: 'demo_search_strategy.ts', snippet: demoStrategyPublicProvider },
],
},
{
title: 'Server',
code: [
{ description: 'plugin.ts', snippet: demoStrategyServerPlugin },
{ description: 'demo_search_strategy.ts', snippet: demoStrategyServerProvider },
],
},
]}
demo={this.renderDemo()}
></GuideSection>
</EuiPageContentBody>
);
}
}

View file

@ -0,0 +1,141 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { EuiButton, EuiCodeBlock, EuiFlexItem, EuiFlexGroup, EuiText } from '@elastic/eui';
import { EuiProgress } from '@elastic/eui';
import { Observable } from 'rxjs';
import {
IKibanaSearchResponse,
IKibanaSearchRequest,
} from '../../../../../src/plugins/data/public';
interface Props {
request: IKibanaSearchRequest;
strategy?: string;
search: (signal: AbortSignal) => Observable<IKibanaSearchResponse>;
}
interface State {
searching: boolean;
response?: IKibanaSearchResponse;
error?: any;
}
export class DoSearch extends React.Component<Props, State> {
private abortController?: AbortController;
constructor(props: Props) {
super(props);
this.state = {
searching: false,
response: undefined,
};
}
search = async () => {
if (this.state.searching && this.abortController) {
this.abortController.abort();
}
this.setState({
searching: true,
response: undefined,
error: undefined,
});
this.abortController = new AbortController();
this.props.search(this.abortController.signal).subscribe(
response => {
this.setState({ response, error: undefined });
},
error => {
this.setState({ error, searching: false, response: undefined });
},
() => {
this.setState({ searching: false, error: undefined });
}
);
};
cancel = () => {
if (this.abortController) {
this.abortController.abort();
}
};
render() {
let responseStr = this.state.error
? JSON.stringify(this.state.error, null, 2)
: JSON.stringify(this.state.response, null, 2);
responseStr = responseStr ? responseStr.substring(0, 2000) : '';
const requestStr = JSON.stringify(this.props.request, null, 2);
return (
<React.Fragment>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton data-test-subj="doSearch" onClick={this.search}>
Search
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton disabled={!this.state.searching} onClick={this.cancel}>
Cancel
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem>
<EuiText>Request:</EuiText>
<EuiCodeBlock language="json" fontSize="m" paddingSize="m" color="dark">
{this.props.strategy
? `data.search
(
${requestStr},
"${this.props.strategy}"
)`
: `data.search
(
${requestStr}
)`}
</EuiCodeBlock>
</EuiFlexItem>
<EuiFlexItem>
<EuiText>Response:</EuiText>
<EuiProgress
value={(this.state.response && this.state.response.percentComplete) || 0}
max={100}
/>
<EuiCodeBlock
language="json"
fontSize="m"
paddingSize="m"
color="dark"
data-test-subj="response"
>
{responseStr}
</EuiCodeBlock>
</EuiFlexItem>
</EuiFlexGroup>
</React.Fragment>
);
}
}

View file

@ -0,0 +1,102 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import {
EuiText,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageContentHeader,
EuiPageContentHeaderSection,
EuiPageHeader,
EuiPageHeaderSection,
EuiTitle,
EuiListGroup,
} from '@elastic/eui';
export const DocumentationPage = () => (
<EuiPageBody data-test-subj="dataPluginExplorerHome">
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>Welcome to the data plugin portal!</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<EuiTitle>
<h2>Documentation links</h2>
</EuiTitle>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
<EuiPageContentBody>
<EuiText>
<h2>Search Services</h2>
<ul>
<li>Provide an abstraction on top of advanced query settings</li>
<li>
Providing an abstraction layer for query cancellation semantics allows us to avoid
wide-spread code changes when ES API changes, allows us to provide a minimum set of
useful functionality first, and allows us to continue adding more advanced features
down the road
</li>
<li>Provide a clean separation of OSS and commercial search strategies.</li>
</ul>
<h2>Extensibility</h2>
<p>
Plugins can register or use different client side, and server side{' '}
<i>search strategies</i>. Search strategies can take advantage of other search stratgies
already registered. For example, the `DEMO_SEARCH_STRATEGY` uses the
`ASYNC_SEARCH_STRATEGY` which uses the `SYNC_SEARCH_STRATEGY`
</p>
<h2>References</h2>
<EuiListGroup
listItems={[
{
label: 'Design document',
href:
'https://docs.google.com/document/d/1ROLq29V1TeLux4ASQIJNllRGkv-xa5XIE72gTU6u16Q/edit#heading=h.3aa9ppqzkvdd',
iconType: 'document',
size: 's',
},
{
label: 'Roadmap',
href: 'https://github.com/elastic/kibana/issues/44661',
iconType: 'logoGithub',
size: 's',
},
{
label: 'Data access API issue',
href: 'https://github.com/elastic/kibana/issues/43371',
iconType: 'logoGithub',
size: 's',
},
]}
/>
</EuiText>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
);

View file

@ -0,0 +1,146 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import {
EuiPageContentBody,
EuiFieldText,
EuiFormRow,
EuiFlexItem,
EuiFlexGroup,
} from '@elastic/eui';
import {
ISearchGeneric,
IEsSearchResponse,
IEsSearchRequest,
} from '../../../../../src/plugins/data/public';
import { DoSearch } from './do_search';
import { GuideSection } from './guide_section';
// @ts-ignore
import serverPlugin from '!!raw-loader!./../../../../../src/plugins/data/server/search/es_search/es_search_service';
// @ts-ignore
import serverStrategy from '!!raw-loader!./../../../../../src/plugins/data/server/search/es_search/es_search_strategy';
// @ts-ignore
import publicPlugin from '!!raw-loader!./../../../../../src/plugins/data/public/search/es_search/es_search_service';
// @ts-ignore
import publicStrategy from '!!raw-loader!./../../../../../src/plugins/data/public/search/es_search/es_search_strategy';
interface Props {
search: ISearchGeneric;
}
interface State {
query: string;
results?: IEsSearchResponse;
index: string;
searching: boolean;
request: IEsSearchRequest;
strategy?: string;
changes: boolean;
error?: any;
}
export class EsSearchTest extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
query: '*',
index: '*',
searching: false,
request: this.getRequest({ index: '*', query: '*' }),
changes: false,
};
}
getRequest({ index, query }: { index: string; query: string }): IEsSearchRequest {
return {
debug: true,
params: {
index,
body: {
query: {
query_string: {
query,
},
},
},
},
};
}
renderDemo() {
const request: IEsSearchRequest = this.getRequest(this.state);
return (
<React.Fragment>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow label="Index pattern">
<EuiFieldText
value={this.state.index}
onChange={e => this.setState({ index: e.target.value, changes: true })}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow label="Query string query">
<EuiFieldText
value={this.state.query}
onChange={e => this.setState({ query: e.target.value, changes: true })}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<DoSearch
request={request}
search={(signal: AbortSignal) => this.props.search(request, { signal })}
/>
</React.Fragment>
);
}
render() {
return (
<EuiPageContentBody>
<GuideSection
codeSections={[
{
title: 'Public',
code: [
{ description: 'es_search_service.ts', snippet: publicPlugin },
{ description: 'es_search_strategy.ts', snippet: publicStrategy },
],
},
{
title: 'Server',
code: [
{ description: 'es_search_service.ts', snippet: serverPlugin },
{ description: 'es_search_strategy.ts', snippet: serverStrategy },
],
},
]}
demo={this.renderDemo()}
></GuideSection>
</EuiPageContentBody>
);
}
}

View file

@ -0,0 +1,137 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { EuiTab, EuiTabs, EuiCodeBlock } from '@elastic/eui';
import { EuiSpacer } from '@elastic/eui';
import { EuiHorizontalRule } from '@elastic/eui';
export interface CodeSection {
title: string;
code: Array<{ description?: string; snippet: string }> | string;
}
interface Props {
demo?: React.ReactNode;
codeSections?: CodeSection[];
}
interface State {
selectedTab: string;
}
export class GuideSection extends React.Component<Props, State> {
private tabs: Array<{ name: string; displayName: string }>;
constructor(props: Props) {
super(props);
if (!props.demo && !props.codeSections) {
throw new Error('Must supply either demo or code sections');
}
if (props.demo) {
this.tabs = [
{
name: 'demo',
displayName: 'Demo',
},
];
} else {
this.tabs = [];
}
if (props.codeSections) {
props.codeSections.forEach(section => {
this.tabs.push({
name: section.title,
displayName: section.title,
});
});
}
this.state = {
selectedTab: this.tabs[0].name,
};
}
onSelectedTabChanged = (selectedTab: string) => {
this.setState({
selectedTab,
});
};
renderTabs() {
return this.tabs.map(tab => (
<EuiTab
onClick={() => this.onSelectedTabChanged(tab.name)}
isSelected={tab.name === this.state.selectedTab}
key={tab.name}
>
{tab.displayName}
</EuiTab>
));
}
removeLicenseBlock(code: string) {
return code.replace(/\/\*[\w\'\s\r\n\*\.\,\(\)\"\;\:\/\-]*\s*\//m, '');
}
renderCodeBlocks() {
if (!this.props.codeSections) {
return undefined;
}
const section = this.props.codeSections.find(s => s.title === this.state.selectedTab);
if (!section) {
throw new Error('No section named ' + this.state.selectedTab);
}
const code = section.code;
if (typeof code === 'string') {
return <EuiCodeBlock language="ts">{this.removeLicenseBlock(code)}</EuiCodeBlock>;
}
return code.map((codeBlock, i) => (
<React.Fragment key={i}>
<EuiSpacer></EuiSpacer>
<h3>{codeBlock.description}</h3>
<EuiCodeBlock language="ts">{this.removeLicenseBlock(codeBlock.snippet)}</EuiCodeBlock>
<EuiHorizontalRule />
</React.Fragment>
));
}
renderContent() {
if (this.state.selectedTab === 'demo') {
return this.props.demo;
} else if (this.props.codeSections) {
return this.renderCodeBlocks();
}
}
render() {
return (
<React.Fragment>
<EuiTabs>{this.renderTabs()}</EuiTabs>
{this.renderContent()}
</React.Fragment>
);
}
}

View file

@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SearchExplorerPlugin } from './plugin';
export const plugin = () => new SearchExplorerPlugin();

View file

@ -0,0 +1,51 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import {
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
EuiPageHeaderSection,
EuiTitle,
} from '@elastic/eui';
interface PageProps {
title: string;
children: React.ReactNode;
}
export function Page({ title, children }: PageProps) {
return (
<EuiPageBody data-test-subj="searchTestPage">
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>{title}</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody>{children}</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
);
}

View file

@ -0,0 +1,42 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Plugin, CoreSetup } from 'kibana/public';
import { ISearchAppMountContext } from '../../../../../src/plugins/data/public';
declare module 'kibana/public' {
interface AppMountContext {
search?: ISearchAppMountContext;
}
}
export class SearchExplorerPlugin implements Plugin {
public setup(core: CoreSetup) {
core.application.register({
id: 'searchExplorer',
title: 'Search Explorer',
async mount(context, params) {
const { renderApp } = await import('./application');
return renderApp(context, params);
},
});
}
public start() {}
public stop() {}
}

View file

@ -0,0 +1,87 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { GuideSection } from './guide_section';
// @ts-ignore
import publicSetupContract from '!!raw-loader!./../../../../../src/plugins/data/public/search/i_search_setup';
// @ts-ignore
import publicSearchStrategy from '!!raw-loader!./../../../../../src/plugins/data/public/search/i_search_strategy';
// @ts-ignore
import publicSearch from '!!raw-loader!./../../../../../src/plugins/data/public/search/i_search';
// @ts-ignore
import publicPlugin from '!!raw-loader!./../../../../../src/plugins/data/public/search/search_service';
// @ts-ignore
import serverSetupContract from '!!raw-loader!./../../../../../src/plugins/data/server/search/i_search_setup';
// @ts-ignore
import serverSearchStrategy from '!!raw-loader!./../../../../../src/plugins/data/server/search/i_search_strategy';
// @ts-ignore
import serverSearch from '!!raw-loader!./../../../../../src/plugins/data/server/search/i_search';
// @ts-ignore
import serverPlugin from '!!raw-loader!./../../../../../src/plugins/data/server/search/search_service';
export const SearchApiPage = () => (
<GuideSection
codeSections={[
{
title: 'Public',
code: [
{
description: 'search_service.ts',
snippet: publicPlugin,
},
{
description: `i_search_setup.ts`,
snippet: publicSetupContract,
},
{
description: 'i_search',
snippet: publicSearch,
},
{
description: 'i_search_strategy',
snippet: publicSearchStrategy,
},
],
},
{
title: 'Server',
code: [
{
description: 'search_service.ts',
snippet: serverPlugin,
},
{
description: `i_search_setup.ts`,
snippet: serverSetupContract,
},
{
description: 'i_search',
snippet: serverSearch,
},
{
description: 'i_search_strategy',
snippet: serverSearchStrategy,
},
],
},
]}
></GuideSection>
);

View file

@ -0,0 +1,15 @@
{
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../../../typings/**/*",
],
"exclude": []
}

View file

@ -0,0 +1,36 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { FtrProviderContext } from 'test/functional/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
describe('demo search strategy', () => {
before(async () => {
await testSubjects.click('demoSearch');
});
it('data is returned', async () => {
await testSubjects.click('doSearch');
await testSubjects.stringExistsInCodeBlockOrFail('response', '"Lovely to meet you, Molly"');
});
});
}

View file

@ -0,0 +1,35 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { FtrProviderContext } from 'test/functional/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
describe('es search strategy', () => {
before(async () => {
await testSubjects.click('esSearch');
});
it('data is returned', async () => {
await testSubjects.click('doSearch');
await testSubjects.stringExistsInCodeBlockOrFail('response', '"animal weights"');
});
});
}

View file

@ -0,0 +1,51 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { FtrProviderContext } from 'test/functional/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function({ getService, getPageObjects, loadTestFile }: FtrProviderContext) {
const browser = getService('browser');
const appsMenu = getService('appsMenu');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects(['common', 'header']);
describe('search services', function() {
before(async () => {
await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/dashboard/current/data');
await esArchiver.load('../functional/fixtures/es_archiver/dashboard/current/kibana');
await kibanaServer.uiSettings.replace({
'dateFormat:tz': 'Australia/North',
defaultIndex: 'logstash-*',
});
await browser.setWindowSize(1300, 900);
await PageObjects.common.navigateToApp('settings');
await appsMenu.clickLink('Search Explorer');
});
after(async function() {
await esArchiver.unload('../functional/fixtures/es_archiver/dashboard/current/data');
await esArchiver.unload('../functional/fixtures/es_archiver/dashboard/current/kibana');
});
loadTestFile(require.resolve('./demo_data'));
loadTestFile(require.resolve('./es_search'));
});
}