Skip to content

Commit

Permalink
Add globalSearch x-pack plugin (#66293)
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
  • Loading branch information
pgayvallet authored Jun 4, 2020
1 parent 3d2c3f1 commit c5546f4
Show file tree
Hide file tree
Showing 81 changed files with 3,294 additions and 66 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
/config/kibana.yml @elastic/kibana-platform
/x-pack/plugins/features/ @elastic/kibana-platform
/x-pack/plugins/licensing/ @elastic/kibana-platform
/x-pack/plugins/global_search/ @elastic/kibana-platform
/x-pack/plugins/cloud/ @elastic/kibana-platform
/packages/kbn-config-schema/ @elastic/kibana-platform
/src/legacy/server/config/ @elastic/kibana-platform
Expand Down
61 changes: 19 additions & 42 deletions rfcs/text/0011_global_search.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ Notes:

### Plugin API

#### server API
#### Common types

```ts
/**
Expand All @@ -208,6 +208,21 @@ type GlobalSearchResult = Omit<GlobalSearchProviderResult, 'url'> & {
url: string;
};


/**
* Response returned from the {@link GlobalSearchServiceStart | global search service}'s `find` API
*/
type GlobalSearchBatchedResults = {
/**
* Results for this batch
*/
results: GlobalSearchResult[];
};
```

#### server API

```ts
/**
* Options for the server-side {@link GlobalSearchServiceStart.find | find API}
*/
Expand All @@ -226,16 +241,6 @@ interface GlobalSearchFindOptions {
aborted$?: Observable<void>;
}

/**
* Response returned from the server-side {@link GlobalSearchServiceStart | global search service}'s `find` API
*/
type GlobalSearchBatchedResults = {
/**
* Results for this batch
*/
results: GlobalSearchResult[];
};

/** @public */
interface GlobalSearchPluginSetup {
registerResultProvider(provider: GlobalSearchResultProvider);
Expand Down Expand Up @@ -265,28 +270,6 @@ interface GlobalSearchFindOptions {
aborted$?: Observable<void>;
}

/**
* Enhanced {@link GlobalSearchResult | result type} for the client-side,
* to allow navigating to a given result.
*/
interface NavigableGlobalSearchResult extends GlobalSearchResult {
/**
* Navigate to this result's associated url. If the result is on this kibana instance, user will be redirected to it
* in a SPA friendly way using `application.navigateToApp`, else, a full page refresh will be performed.
*/
navigate: () => Promise<void>;
}

/**
* Response returned from the client-side {@link GlobalSearchServiceStart | global search service}'s `find` API
*/
type GlobalSearchBatchedResults = {
/**
* Results for this batch
*/
results: NavigableGlobalSearchResult[];
};

/** @public */
interface GlobalSearchPluginSetup {
registerResultProvider(provider: GlobalSearchResultProvider);
Expand All @@ -304,9 +287,6 @@ Notes:
- The `registerResultProvider` setup APIs share the same signature, however the input `GlobalSearchResultProvider`
types are different on the client and server.
- The `find` start API signature got a `KibanaRequest` for `server`, when this parameter is not present for `public`.
- The `find` API returns a observable of `NavigableGlobalSearchResult` instead of plain `GlobalSearchResult`. This type
is here to enhance results with a `navigate` method to let the `GlobalSearch` plugin handle the navigation logic, which is
non-trivial. See the [Redirecting to a result](#redirecting-to-a-result) section for more info.

#### http API

Expand Down Expand Up @@ -395,14 +375,11 @@ In current specification, the only conversion step is to transform the `result.u

#### redirecting to a result

Parsing a relative or absolute result url to perform SPA navigation can be non trivial, and should remains the responsibility
of the GlobalSearch plugin API.

This is why `NavigableGlobalSearchResult.navigate` has been introduced on the client-side version of the `find` API
Parsing a relative or absolute result url to perform SPA navigation can be non trivial. This is why `ApplicationService.navigateToUrl` has been introduced on the client-side core API

When using `navigate` from a result instance, the following logic will be executed:
When using `navigateToUrl` with the url of a result instance, the following logic will be executed:

If all these criteria are true for `result.url`:
If all these criteria are true for `url`:

- (only for absolute URLs) The origin of the URL matches the origin of the browser's current location
- The pathname of the URL starts with the current basePath (eg. /mybasepath/s/my-space)
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);
});
});
});
Loading

0 comments on commit c5546f4

Please sign in to comment.