Skip to content

Commit

Permalink
Add globalSearch x-pack plugin (#66293) (#68259)
Browse files Browse the repository at this point in the history
* add skeleton for global_search plugin

* base implementation of the server-side service

* add utils tests

* add server-side mocks

* move take_in_array to common folder

* implements base of client-side plugin

* add tests for server-side service

* fix server plugin tests

* implement `navigateToUrl` core API

* extract processResults for the client-side

* fetch server results from the client side

* factorize process_results

* fix plugin start params

* move things around

* move all server types to single file

* fix types imports

* add basic FTR tests

* add client-side service tests

* add tests for addNavigate

* add getDefaultPreference & tests

* use optional for RequestHandlerContext

* add registerRoutes test

* add base test for context

* resolve TODO

* common nits/doc

* common nits/doc on public

* update CODEOWNERS

* add import for declare statement

* add license check on the server-side

* add license check on the client-side

* eslint

* address some review comments

* use properly typed errors for obs

* add integration tests for the find endpoint

* fix unit tests

* use licensing start contract

* translate the error message

* fix eslint rule for test_utils

* fix test_utils imports

* remove NavigableGlobalSearchResult, use `application.navigateToUrl` instead.

* use coreProvider plugin in FTR tests

* nits

* fix service start params

* fix service start params, bis

* I really need to fix this typecheck oom error

* add README, update missing jsdoc

* nits on doc
# Conflicts:
#	.github/CODEOWNERS
#	rfcs/text/0011_global_search.md
  • Loading branch information
pgayvallet committed Jun 4, 2020
1 parent feb91c1 commit 29c0acf
Show file tree
Hide file tree
Showing 79 changed files with 3,274 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ module.exports = {
'!src/core/server/index.ts', // relative import
'!src/core/server/mocks{,.ts}',
'!src/core/server/types{,.ts}',
'!src/core/server/test_utils',
'!src/core/server/test_utils{,.ts}',
// for absolute imports until fixed in
// https://github.com/elastic/kibana/issues/36096
'!src/core/server/*.test.mocks{,.ts}',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import supertest from 'supertest';
import { UnwrapPromise } from '@kbn/utility-types';
import { registerBulkCreateRoute } from '../bulk_create';
import { savedObjectsClientMock } from '../../../../../core/server/mocks';
import { setupServer } from './test_utils';
import { setupServer } from '../test_utils';

type setupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import supertest from 'supertest';
import { UnwrapPromise } from '@kbn/utility-types';
import { registerBulkGetRoute } from '../bulk_get';
import { savedObjectsClientMock } from '../../../../../core/server/mocks';
import { setupServer } from './test_utils';
import { setupServer } from '../test_utils';

type setupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import supertest from 'supertest';
import { UnwrapPromise } from '@kbn/utility-types';
import { registerBulkUpdateRoute } from '../bulk_update';
import { savedObjectsClientMock } from '../../../../../core/server/mocks';
import { setupServer } from './test_utils';
import { setupServer } from '../test_utils';

type setupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import supertest from 'supertest';
import { UnwrapPromise } from '@kbn/utility-types';
import { registerCreateRoute } from '../create';
import { savedObjectsClientMock } from '../../service/saved_objects_client.mock';
import { setupServer } from './test_utils';
import { setupServer } from '../test_utils';

type setupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import supertest from 'supertest';
import { UnwrapPromise } from '@kbn/utility-types';
import { registerDeleteRoute } from '../delete';
import { savedObjectsClientMock } from '../../../../../core/server/mocks';
import { setupServer } from './test_utils';
import { setupServer } from '../test_utils';

type setupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import supertest from 'supertest';
import { UnwrapPromise } from '@kbn/utility-types';
import { SavedObjectConfig } from '../../saved_objects_config';
import { registerExportRoute } from '../export';
import { setupServer, createExportableType } from './test_utils';
import { setupServer, createExportableType } from '../test_utils';

type setupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
const exportSavedObjectsToStream = exportMock.exportSavedObjectsToStream as jest.Mock;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import querystring from 'querystring';
import { UnwrapPromise } from '@kbn/utility-types';
import { registerFindRoute } from '../find';
import { savedObjectsClientMock } from '../../../../../core/server/mocks';
import { setupServer } from './test_utils';
import { setupServer } from '../test_utils';

type setupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { UnwrapPromise } from '@kbn/utility-types';
import { registerImportRoute } from '../import';
import { savedObjectsClientMock } from '../../../../../core/server/mocks';
import { SavedObjectConfig } from '../../saved_objects_config';
import { setupServer, createExportableType } from './test_utils';
import { setupServer, createExportableType } from '../test_utils';

type setupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import supertest from 'supertest';
import { UnwrapPromise } from '@kbn/utility-types';
import { registerLogLegacyImportRoute } from '../log_legacy_import';
import { loggingServiceMock } from '../../../logging/logging_service.mock';
import { setupServer } from './test_utils';
import { setupServer } from '../test_utils';

type setupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import supertest from 'supertest';
import { UnwrapPromise } from '@kbn/utility-types';
import { registerResolveImportErrorsRoute } from '../resolve_import_errors';
import { savedObjectsClientMock } from '../../../../../core/server/mocks';
import { setupServer, createExportableType } from './test_utils';
import { setupServer, createExportableType } from '../test_utils';
import { SavedObjectConfig } from '../../saved_objects_config';

type setupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import supertest from 'supertest';
import { UnwrapPromise } from '@kbn/utility-types';
import { registerUpdateRoute } from '../update';
import { savedObjectsClientMock } from '../../../../../core/server/mocks';
import { setupServer } from './test_utils';
import { setupServer } from '../test_utils';

type setupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
* under the License.
*/

import { ContextService } from '../../../context';
import { createHttpServer, createCoreContext } from '../../../http/test_utils';
import { coreMock } from '../../../mocks';
import { SavedObjectsType } from '../../types';
import { ContextService } from '../../context';
import { createHttpServer, createCoreContext } from '../../http/test_utils';
import { coreMock } from '../../mocks';
import { SavedObjectsType } from '../types';

const coreId = Symbol('core');
const defaultCoreId = Symbol('core');

export const setupServer = async () => {
export const setupServer = async (coreId: symbol = defaultCoreId) => {
const coreContext = createCoreContext({ coreId });
const contextService = new ContextService(coreContext);

Expand Down
1 change: 1 addition & 0 deletions src/core/server/test_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@

export { createHttpServer } from './http/test_utils';
export { ServiceStatusLevelSnapshotSerializer } from './status/test_utils';
export { setupServer } from './saved_objects/routes/test_utils';
1 change: 1 addition & 0 deletions src/core/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ export * from './capabilities';
export * from './app_category';
export * from './ui_settings';
export * from './saved_objects';
export * from './serializable';
32 changes: 32 additions & 0 deletions src/core/types/serializable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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 type Serializable =
| string
| number
| boolean
| null
| SerializableArray
| SerializableRecord;

// we need interfaces instead of types here to allow cyclic references
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SerializableArray extends Array<Serializable> {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SerializableRecord extends Record<string, Serializable> {}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "core_provider_plugin",
"version": "0.0.1",
"kibanaVersion": "kibana",
"optionalPlugins": ["core_plugin_a", "core_plugin_b", "licensing"],
"optionalPlugins": ["core_plugin_a", "core_plugin_b", "licensing", "globalSearchTest"],
"server": false,
"ui": true
}
1 change: 1 addition & 0 deletions x-pack/.i18nrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"xpack.endpoint": "plugins/endpoint",
"xpack.features": "plugins/features",
"xpack.fileUpload": "plugins/file_upload",
"xpack.globalSearch": ["plugins/global_search"],
"xpack.graph": ["plugins/graph"],
"xpack.grokDebugger": "plugins/grokdebugger",
"xpack.idxMgmt": "plugins/index_management",
Expand Down
49 changes: 49 additions & 0 deletions x-pack/plugins/global_search/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Kibana GlobalSearch plugin

The GlobalSearch plugin provides an easy way to search for various objects, such as applications
or dashboards from the Kibana instance, from both server and client-side plugins

## Consuming the globalSearch API

```ts
startDeps.globalSearch.find('some term').subscribe({
next: ({ results }) => {
addNewResultsToList(results);
},
error: () => {},
complete: () => {
showAsyncSearchIndicator(false);
}
});
```

## Registering custom result providers

The GlobalSearch API allows to extend provided results by registering your own provider.

```ts
setupDeps.globalSearch.registerResultProvider({
id: 'my_provider',
find: (term, options, context) => {
const resultPromise = myService.search(term, context.core.savedObjects.client);
return from(resultPromise).pipe(takeUntil(options.aborted$);
},
});
```
## Known limitations
### Client-side registered providers
Results from providers registered from the client-side `registerResultProvider` API will
not be available when performing a search from the server-side. For this reason, prefer
registering providers using the server-side API when possible.
Refer to the [RFC](rfcs/text/0011_global_search.md#result_provider_registration) for more details
### Search completion cause
There is currently no way to identify `globalSearch.find` observable completion cause:
searches completing because all providers returned all their results and searches
completing because the consumer aborted the search using the `aborted$` option or because
the internal timout period has been reaches will both complete the same way.
22 changes: 22 additions & 0 deletions x-pack/plugins/global_search/common/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { GlobalSearchFindError } from './errors';

describe('GlobalSearchFindError', () => {
describe('#invalidLicense', () => {
it('create an error with the correct `type`', () => {
const error = GlobalSearchFindError.invalidLicense('foobar');
expect(error.message).toBe('foobar');
expect(error.type).toBe('invalid-license');
});

it('can be identified via instanceof', () => {
const error = GlobalSearchFindError.invalidLicense('foo');
expect(error instanceof GlobalSearchFindError).toBe(true);
});
});
});
27 changes: 27 additions & 0 deletions x-pack/plugins/global_search/common/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

// only one type for now, but already present for future-proof reasons
export type GlobalSearchFindErrorType = 'invalid-license';

/**
* Error thrown from the {@link GlobalSearchPluginStart.find | GlobalSearch find API}'s result observable
*
* @public
*/
export class GlobalSearchFindError extends Error {
public static invalidLicense(message: string) {
return new GlobalSearchFindError('invalid-license', message);
}

private constructor(public readonly type: GlobalSearchFindErrorType, message: string) {
super(message);

// Set the prototype explicitly, see:
// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
Object.setPrototypeOf(this, GlobalSearchFindError.prototype);
}
}
24 changes: 24 additions & 0 deletions x-pack/plugins/global_search/common/license_checker.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { ILicenseChecker } from './license_checker';

const createLicenseCheckerMock = (): jest.Mocked<ILicenseChecker> => {
const mock = {
getState: jest.fn(),
getLicense: jest.fn(),
clean: jest.fn(),
};

mock.getLicense.mockReturnValue(undefined);
mock.getState.mockReturnValue({ valid: true });

return mock;
};

export const licenseCheckerMock = {
create: createLicenseCheckerMock,
};
63 changes: 63 additions & 0 deletions x-pack/plugins/global_search/common/license_checker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { Observable, of, BehaviorSubject } from 'rxjs';
import { licenseMock } from '../../licensing/common/licensing.mock';
import { ILicense, LicenseCheck } from '../../licensing/common/types';
import { LicenseChecker } from './license_checker';

describe('LicenseChecker', () => {
const createLicense = (check: LicenseCheck): ILicense => {
const license = licenseMock.createLicenseMock();
license.check.mockReturnValue(check);
return license;
};

const createLicense$ = (check: LicenseCheck): Observable<ILicense> => of(createLicense(check));

it('returns the correct state of the license', () => {
let checker = new LicenseChecker(createLicense$({ state: 'valid' }));
expect(checker.getState()).toEqual({ valid: true });

checker = new LicenseChecker(createLicense$({ state: 'expired' }));
expect(checker.getState()).toEqual({ valid: false, message: 'expired' });

checker = new LicenseChecker(createLicense$({ state: 'invalid' }));
expect(checker.getState()).toEqual({ valid: false, message: 'invalid' });

checker = new LicenseChecker(createLicense$({ state: 'unavailable' }));
expect(checker.getState()).toEqual({ valid: false, message: 'unavailable' });
});

it('updates the state when the license changes', () => {
const license$ = new BehaviorSubject<ILicense>(createLicense({ state: 'valid' }));

const checker = new LicenseChecker(license$);
expect(checker.getState()).toEqual({ valid: true });

license$.next(createLicense({ state: 'expired' }));
expect(checker.getState()).toEqual({ valid: false, message: 'expired' });

license$.next(createLicense({ state: 'valid' }));
expect(checker.getState()).toEqual({ valid: true });
});

it('removes the subscription when calling `clean`', () => {
const mockUnsubscribe = jest.fn();
const mockObs = {
subscribe: jest.fn().mockReturnValue({ unsubscribe: mockUnsubscribe }),
};

const checker = new LicenseChecker(mockObs as any);

expect(mockObs.subscribe).toHaveBeenCalledTimes(1);
expect(mockUnsubscribe).not.toHaveBeenCalled();

checker.clean();

expect(mockUnsubscribe).toHaveBeenCalledTimes(1);
});
});
Loading

0 comments on commit 29c0acf

Please sign in to comment.