diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 66fb31cc91d5a..1d0f6fc50ee9b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -85,7 +85,6 @@ /x-pack/plugins/ingest_manager/ @elastic/ingest-management /x-pack/legacy/plugins/ingest_manager/ @elastic/ingest-management /x-pack/plugins/observability/ @elastic/observability-ui -/x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/uptime @elastic/uptime diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.action_global_apply_filter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.action_global_apply_filter.md new file mode 100644 index 0000000000000..14075ba1beba0 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.action_global_apply_filter.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ACTION\_GLOBAL\_APPLY\_FILTER](./kibana-plugin-plugins-data-public.action_global_apply_filter.md) + +## ACTION\_GLOBAL\_APPLY\_FILTER variable + +Signature: + +```typescript +ACTION_GLOBAL_APPLY_FILTER = "ACTION_GLOBAL_APPLY_FILTER" +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.init.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.init.md index ce401bec87dbb..595992dc82b74 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.init.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.init.md @@ -7,15 +7,8 @@ Signature: ```typescript -init(forceFieldRefresh?: boolean): Promise; +init(): Promise; ``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| forceFieldRefresh | boolean | | - Returns: `Promise` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index c15cb3358f689..37db063e284ec 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -50,7 +50,7 @@ export declare class IndexPattern implements IIndexPattern | [getScriptedFields()](./kibana-plugin-plugins-data-public.indexpattern.getscriptedfields.md) | | | | [getSourceFiltering()](./kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md) | | | | [getTimeField()](./kibana-plugin-plugins-data-public.indexpattern.gettimefield.md) | | | -| [init(forceFieldRefresh)](./kibana-plugin-plugins-data-public.indexpattern.init.md) | | | +| [init()](./kibana-plugin-plugins-data-public.indexpattern.init.md) | | | | [initFromSpec(spec)](./kibana-plugin-plugins-data-public.indexpattern.initfromspec.md) | | | | [isTimeBased()](./kibana-plugin-plugins-data-public.indexpattern.istimebased.md) | | | | [isTimeBasedWildcard()](./kibana-plugin-plugins-data-public.indexpattern.istimebasedwildcard.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index a5453c7c51d5b..09702df4fdb54 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -89,6 +89,7 @@ | Variable | Description | | --- | --- | +| [ACTION\_GLOBAL\_APPLY\_FILTER](./kibana-plugin-plugins-data-public.action_global_apply_filter.md) | | | [AggGroupLabels](./kibana-plugin-plugins-data-public.agggrouplabels.md) | | | [AggGroupNames](./kibana-plugin-plugins-data-public.agggroupnames.md) | | | [baseFormattersPublic](./kibana-plugin-plugins-data-public.baseformatterspublic.md) | | diff --git a/packages/kbn-es/package.json b/packages/kbn-es/package.json index c3670f648d309..dabf11fdd0b66 100644 --- a/packages/kbn-es/package.json +++ b/packages/kbn-es/package.json @@ -16,7 +16,7 @@ "glob": "^7.1.2", "node-fetch": "^2.6.0", "simple-git": "^1.91.0", - "tar-fs": "^1.16.3", + "tar-fs": "^2.1.0", "tree-kill": "^1.2.2", "yauzl": "^2.10.0" } diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index 129c58a4b4174..f292387c12521 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -23,10 +23,10 @@ "vinyl-fs": "^3.0.3" }, "devDependencies": { - "@types/decompress": "^4.2.3", + "@types/extract-zip": "^1.6.2", "@types/gulp-zip": "^4.0.1", "@types/inquirer": "^6.5.0", - "decompress": "^4.2.1", + "extract-zip": "^2.0.1", "typescript": "4.0.2" } } diff --git a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts index 62f83cd672f3d..be23d8dbde646 100644 --- a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts +++ b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts @@ -22,7 +22,7 @@ import Fs from 'fs'; import execa from 'execa'; import { createStripAnsiSerializer, REPO_ROOT, createReplaceSerializer } from '@kbn/dev-utils'; -import decompress from 'decompress'; +import extract from 'extract-zip'; import del from 'del'; import globby from 'globby'; import loadJsonFile from 'load-json-file'; @@ -81,7 +81,7 @@ it('builds a generated plugin into a viable archive', async () => { info compressing plugin into [fooTestPlugin-7.5.0.zip]" `); - await decompress(PLUGIN_ARCHIVE, TMP_DIR); + await extract(PLUGIN_ARCHIVE, { dir: TMP_DIR }); const files = await globby(['**/*'], { cwd: TMP_DIR }); files.sort((a, b) => a.localeCompare(b)); diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index 24655f8e57026..c84b0a93311bb 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -32,7 +32,7 @@ "parse-link-header": "^1.0.1", "rxjs": "^6.5.5", "strip-ansi": "^5.2.0", - "tar-fs": "^1.16.3", + "tar-fs": "^2.1.0", "tmp": "^0.1.0", "xml2js": "^0.4.22", "zlib": "^1.0.5" diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts index b4cfc3c1efe8b..04979b69b32b9 100644 --- a/src/core/server/ui_settings/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -73,9 +73,9 @@ export function getServices() { httpServerMock.createKibanaRequest() ); - const uiSettings = kbnServer.server.uiSettingsServiceFactory({ - savedObjectsClient, - }); + const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient( + savedObjectsClient + ); services = { kbnServer, diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 9bb091383ab13..1a1f43b93f26e 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -45,7 +45,6 @@ import { LegacyConfig, ILegacyService, ILegacyInternals } from '../../core/serve import { UiPlugins } from '../../core/server/plugins'; import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch'; import { UsageCollectionSetup } from '../../plugins/usage_collection/server'; -import { UiSettingsServiceFactoryOptions } from '../../legacy/ui/ui_settings/ui_settings_service_factory'; import { HomeServerPluginSetup } from '../../plugins/home/server'; // lot of legacy code was assuming this type only had these two methods @@ -78,7 +77,6 @@ declare module 'hapi' { name: string, factoryFn: (request: Request) => Record ) => void; - uiSettingsServiceFactory: (options?: UiSettingsServiceFactoryOptions) => IUiSettingsClient; logWithMetadata: (tags: string[], message: string, meta: Record) => void; newPlatform: KbnServer['newPlatform']; } diff --git a/src/legacy/ui/ui_mixin.js b/src/legacy/ui/ui_mixin.js index 432c4f02bc3e6..54da001d20669 100644 --- a/src/legacy/ui/ui_mixin.js +++ b/src/legacy/ui/ui_mixin.js @@ -19,10 +19,8 @@ import { uiAppsMixin } from './ui_apps'; import { uiRenderMixin } from './ui_render'; -import { uiSettingsMixin } from './ui_settings'; export async function uiMixin(kbnServer) { await kbnServer.mixin(uiAppsMixin); - await kbnServer.mixin(uiSettingsMixin); await kbnServer.mixin(uiRenderMixin); } diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 23fb6028f84db..8cc2cd1321a62 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -21,6 +21,7 @@ import { createHash } from 'crypto'; import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; +import { KibanaRequest } from '../../../core/server'; import { AppBootstrap } from './bootstrap'; import { getApmConfig } from '../apm'; @@ -79,7 +80,10 @@ export function uiRenderMixin(kbnServer, server, config) { auth: authEnabled ? { mode: 'try' } : false, }, async handler(request, h) { - const uiSettings = request.getUiSettingsService(); + const soClient = kbnServer.newPlatform.start.core.savedObjects.getScopedClient( + KibanaRequest.from(request) + ); + const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient(soClient); const darkMode = !authEnabled || request.auth.isAuthenticated diff --git a/src/legacy/ui/ui_settings/index.js b/src/legacy/ui/ui_settings/index.js deleted file mode 100644 index ec3122c4e390e..0000000000000 --- a/src/legacy/ui/ui_settings/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 { uiSettingsMixin } from './ui_settings_mixin'; diff --git a/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts b/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts deleted file mode 100644 index 84a64d3f46f11..0000000000000 --- a/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -/* - * 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 sinon from 'sinon'; -import expect from '@kbn/expect'; - -import { savedObjectsClientMock } from '../../../../core/server/mocks'; -import * as uiSettingsServiceFactoryNS from '../ui_settings_service_factory'; -import * as getUiSettingsServiceForRequestNS from '../ui_settings_service_for_request'; -// @ts-ignore -import { uiSettingsMixin } from '../ui_settings_mixin'; - -interface Decorators { - server: { [name: string]: any }; - request: { [name: string]: any }; -} - -const uiSettingDefaults = { - application: { - defaultProperty1: 'value1', - }, -}; - -describe('uiSettingsMixin()', () => { - const sandbox = sinon.createSandbox(); - - function setup() { - // maps of decorations passed to `server.decorate()` - const decorations: Decorators = { - server: {}, - request: {}, - }; - - // mock hapi server - const server = { - log: sinon.stub(), - route: sinon.stub(), - addMemoizedFactoryToRequest(name: string, factory: (...args: any[]) => any) { - this.decorate('request', name, function (this: typeof server) { - return factory(this); - }); - }, - decorate: sinon.spy((type: keyof Decorators, name: string, value: any) => { - decorations[type][name] = value; - }), - newPlatform: { - setup: { - core: { - uiSettings: { - register: sinon.stub(), - }, - }, - }, - }, - }; - - // "promise" returned from kbnServer.ready() - const readyPromise = { - then: sinon.stub(), - }; - - const kbnServer = { - server, - uiExports: { uiSettingDefaults }, - ready: sinon.stub().returns(readyPromise), - }; - - uiSettingsMixin(kbnServer, server); - - return { - kbnServer, - server, - decorations, - readyPromise, - }; - } - - afterEach(() => sandbox.restore()); - - it('passes uiSettingsDefaults to the new platform', () => { - const { server } = setup(); - sinon.assert.calledOnce(server.newPlatform.setup.core.uiSettings.register); - sinon.assert.calledWithExactly( - server.newPlatform.setup.core.uiSettings.register, - uiSettingDefaults - ); - }); - - describe('server.uiSettingsServiceFactory()', () => { - it('decorates server with "uiSettingsServiceFactory"', () => { - const { decorations } = setup(); - expect(decorations.server).to.have.property('uiSettingsServiceFactory').a('function'); - - const uiSettingsServiceFactoryStub = sandbox.stub( - uiSettingsServiceFactoryNS, - 'uiSettingsServiceFactory' - ); - sinon.assert.notCalled(uiSettingsServiceFactoryStub); - decorations.server.uiSettingsServiceFactory(); - sinon.assert.calledOnce(uiSettingsServiceFactoryStub); - }); - - it('passes `server` and `options` argument to factory', () => { - const { decorations, server } = setup(); - expect(decorations.server).to.have.property('uiSettingsServiceFactory').a('function'); - - const uiSettingsServiceFactoryStub = sandbox.stub( - uiSettingsServiceFactoryNS, - 'uiSettingsServiceFactory' - ); - - sinon.assert.notCalled(uiSettingsServiceFactoryStub); - - const savedObjectsClient = savedObjectsClientMock.create(); - decorations.server.uiSettingsServiceFactory({ - savedObjectsClient, - }); - sinon.assert.calledOnce(uiSettingsServiceFactoryStub); - sinon.assert.calledWithExactly(uiSettingsServiceFactoryStub, server as any, { - savedObjectsClient, - }); - }); - }); - - describe('request.getUiSettingsService()', () => { - it('exposes "getUiSettingsService" on requests', () => { - const { decorations } = setup(); - expect(decorations.request).to.have.property('getUiSettingsService').a('function'); - - const getUiSettingsServiceForRequestStub = sandbox.stub( - getUiSettingsServiceForRequestNS, - 'getUiSettingsServiceForRequest' - ); - sinon.assert.notCalled(getUiSettingsServiceForRequestStub); - decorations.request.getUiSettingsService(); - sinon.assert.calledOnce(getUiSettingsServiceForRequestStub); - }); - - it('passes request to getUiSettingsServiceForRequest', () => { - const { server, decorations } = setup(); - expect(decorations.request).to.have.property('getUiSettingsService').a('function'); - - const getUiSettingsServiceForRequestStub = sandbox.stub( - getUiSettingsServiceForRequestNS, - 'getUiSettingsServiceForRequest' - ); - sinon.assert.notCalled(getUiSettingsServiceForRequestStub); - const request = {}; - decorations.request.getUiSettingsService.call(request); - sinon.assert.calledWith(getUiSettingsServiceForRequestStub, server as any, request as any); - }); - }); - - describe('server.uiSettings()', () => { - it('throws an error, links to pr', () => { - const { decorations } = setup(); - expect(decorations.server).to.have.property('uiSettings').a('function'); - expect(() => { - decorations.server.uiSettings(); - }).to.throwError('http://github.com' as any); // incorrect typings - }); - }); -}); diff --git a/src/legacy/ui/ui_settings/ui_exports_consumer.js b/src/legacy/ui/ui_settings/ui_exports_consumer.js deleted file mode 100644 index d2bb3a00ce0ed..0000000000000 --- a/src/legacy/ui/ui_settings/ui_exports_consumer.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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. - */ - -/** - * The UiExports class accepts consumer objects that it consults while - * trying to consume all of the `uiExport` declarations provided by - * plugins. - * - * UiExportConsumer is instantiated and passed to UiExports, then for - * every `uiExport` declaration the `exportConsumer(type)` method is - * with the key of the declaration. If this consumer knows how to handle - * that key we return a function that will be called with the plugins - * and values of all declarations using that key. - * - * With this, the consumer merges all of the declarations into the - * _uiSettingDefaults map, ensuring that there are not collisions along - * the way. - * - * @class UiExportsConsumer - */ -export class UiExportsConsumer { - _uiSettingDefaults = {}; - - exportConsumer(type) { - switch (type) { - case 'uiSettingDefaults': - return (plugin, settingDefinitions) => { - Object.keys(settingDefinitions).forEach((key) => { - if (key in this._uiSettingDefaults) { - throw new Error(`uiSettingDefaults for key "${key}" are already defined`); - } - - this._uiSettingDefaults[key] = settingDefinitions[key]; - }); - }; - } - } - - /** - * Get the map of uiSettingNames to "default" specifications - * @return {Object} - */ - getUiSettingDefaults() { - return this._uiSettingDefaults; - } -} diff --git a/src/legacy/ui/ui_settings/ui_settings_mixin.js b/src/legacy/ui/ui_settings/ui_settings_mixin.js deleted file mode 100644 index 8190b67732dac..0000000000000 --- a/src/legacy/ui/ui_settings/ui_settings_mixin.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 { uiSettingsServiceFactory } from './ui_settings_service_factory'; -import { getUiSettingsServiceForRequest } from './ui_settings_service_for_request'; - -export function uiSettingsMixin(kbnServer, server) { - const { uiSettingDefaults = {} } = kbnServer.uiExports; - const mergedUiSettingDefaults = Object.keys(uiSettingDefaults).reduce((acc, currentKey) => { - const defaultSetting = uiSettingDefaults[currentKey]; - const updatedDefaultSetting = { - ...defaultSetting, - }; - if (typeof defaultSetting.options === 'function') { - updatedDefaultSetting.options = defaultSetting.options(server); - } - if (typeof defaultSetting.value === 'function') { - updatedDefaultSetting.value = defaultSetting.value(server); - } - acc[currentKey] = updatedDefaultSetting; - return acc; - }, {}); - - server.newPlatform.setup.core.uiSettings.register(mergedUiSettingDefaults); - - server.decorate('server', 'uiSettingsServiceFactory', (options = {}) => { - return uiSettingsServiceFactory(server, options); - }); - - server.addMemoizedFactoryToRequest('getUiSettingsService', (request) => { - return getUiSettingsServiceForRequest(server, request); - }); - - server.decorate('server', 'uiSettings', () => { - throw new Error(` - server.uiSettings has been removed, see https://github.com/elastic/kibana/pull/12243. - `); - }); -} diff --git a/src/legacy/ui/ui_settings/ui_settings_service_factory.ts b/src/legacy/ui/ui_settings/ui_settings_service_factory.ts deleted file mode 100644 index 6c3c50d175dc5..0000000000000 --- a/src/legacy/ui/ui_settings/ui_settings_service_factory.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 { Legacy } from 'kibana'; -import { IUiSettingsClient, SavedObjectsClientContract } from 'src/core/server'; - -export interface UiSettingsServiceFactoryOptions { - savedObjectsClient: SavedObjectsClientContract; -} -/** - * Create an instance of UiSettingsClient that will use the - * passed `savedObjectsClient` to communicate with elasticsearch - * - * @return {IUiSettingsClient} - */ -export function uiSettingsServiceFactory( - server: Legacy.Server, - options: UiSettingsServiceFactoryOptions -): IUiSettingsClient { - return server.newPlatform.start.core.uiSettings.asScopedToClient(options.savedObjectsClient); -} diff --git a/src/legacy/ui/ui_settings/ui_settings_service_for_request.ts b/src/legacy/ui/ui_settings/ui_settings_service_for_request.ts deleted file mode 100644 index 057fc64c9ebd7..0000000000000 --- a/src/legacy/ui/ui_settings/ui_settings_service_for_request.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { Legacy } from 'kibana'; -import { IUiSettingsClient } from 'src/core/server'; -import { uiSettingsServiceFactory } from './ui_settings_service_factory'; - -/** - * Get/create an instance of UiSettingsService bound to a specific request. - * Each call is cached (keyed on the request object itself) and subsequent - * requests will get the first UiSettingsService instance even if the `options` - * have changed. - * - * @param {Hapi.Server} server - * @param {Hapi.Request} request - * @param {Object} [options={}] - - * @return {IUiSettingsClient} - */ -export function getUiSettingsServiceForRequest( - server: Legacy.Server, - request: Legacy.Request -): IUiSettingsClient { - const savedObjectsClient = request.getSavedObjectsClient(); - return uiSettingsServiceFactory(server, { savedObjectsClient }); -} diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 5d6ae61a77e00..ea91a9bb14e1f 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -172,12 +172,12 @@ export class IndexPattern implements IIndexPattern { }); } - private async indexFields(forceFieldRefresh: boolean = false, specs?: FieldSpec[]) { + private async indexFields(specs?: FieldSpec[]) { if (!this.id) { return; } - if (forceFieldRefresh || this.isFieldRefreshRequired(specs)) { + if (this.isFieldRefreshRequired(specs)) { await this.refreshFields(); } else { if (specs) { @@ -213,7 +213,7 @@ export class IndexPattern implements IIndexPattern { return this; } - private updateFromElasticSearch(response: any, forceFieldRefresh: boolean = false) { + private updateFromElasticSearch(response: any) { if (!response.found) { throw new SavedObjectNotFound(savedObjectType, this.id, 'management/kibana/indexPatterns'); } @@ -239,7 +239,7 @@ export class IndexPattern implements IIndexPattern { } this.version = response.version; - return this.indexFields(forceFieldRefresh, response.fields); + return this.indexFields(response.fields); } getComputedFields() { @@ -283,7 +283,7 @@ export class IndexPattern implements IIndexPattern { }; } - async init(forceFieldRefresh = false) { + async init() { if (!this.id) { return this; // no id === no elasticsearch document } @@ -307,7 +307,7 @@ export class IndexPattern implements IIndexPattern { }; // Do this before we attempt to update from ES since that call can potentially perform a save this.originalBody = this.prepBody(); - await this.updateFromElasticSearch(response, forceFieldRefresh); + await this.updateFromElasticSearch(response); // Do it after to ensure we have the most up to date information this.originalBody = this.prepBody(); diff --git a/src/plugins/data/common/search/aggs/agg_type.test.ts b/src/plugins/data/common/search/aggs/agg_type.test.ts index 2fcc6b97b1cc6..bf1136159dfe8 100644 --- a/src/plugins/data/common/search/aggs/agg_type.test.ts +++ b/src/plugins/data/common/search/aggs/agg_type.test.ts @@ -99,6 +99,17 @@ describe('AggType Class', () => { expect(aggType.params[1].name).toBe('customLabel'); }); + test('disables json param', () => { + const aggType = new AggType({ + name: 'name', + title: 'title', + json: false, + }); + + expect(aggType.params.length).toBe(1); + expect(aggType.params[0].name).toBe('customLabel'); + }); + test('can disable customLabel', () => { const aggType = new AggType({ name: 'smart agg', diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 0ba2bb66e7758..2ee604c1bf25d 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -47,6 +47,7 @@ export interface AggTypeConfig< getRequestAggs?: ((aggConfig: TAggConfig) => TAggConfig[]) | (() => TAggConfig[] | void); getResponseAggs?: ((aggConfig: TAggConfig) => TAggConfig[]) | (() => TAggConfig[] | void); customLabels?: boolean; + json?: boolean; decorateAggConfig?: () => any; postFlightRequest?: ( resp: any, @@ -235,13 +236,17 @@ export class AggType< if (config.params && config.params.length && config.params[0] instanceof BaseParamType) { this.params = config.params as TParam[]; } else { - // always append the raw JSON param + // always append the raw JSON param unless it is configured to false const params: any[] = config.params ? [...config.params] : []; - params.push({ - name: 'json', - type: 'json', - advanced: true, - }); + + if (config.json !== false) { + params.push({ + name: 'json', + type: 'json', + advanced: true, + }); + } + // always append custom label if (config.customLabels !== false) { diff --git a/src/plugins/data/common/search/aggs/metrics/count.ts b/src/plugins/data/common/search/aggs/metrics/count.ts index d990599586e81..9c9f36651f4d2 100644 --- a/src/plugins/data/common/search/aggs/metrics/count.ts +++ b/src/plugins/data/common/search/aggs/metrics/count.ts @@ -28,6 +28,7 @@ export const getCountMetricAgg = () => defaultMessage: 'Count', }), hasNoDsl: true, + json: false, makeLabel() { return i18n.translate('data.search.aggs.metrics.countLabel', { defaultMessage: 'Count', diff --git a/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts index 846feb9296fca..32189f07581e6 100644 --- a/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts @@ -34,7 +34,6 @@ describe('agg_expression_functions', () => { "id": undefined, "params": Object { "customLabel": undefined, - "json": undefined, }, "schema": undefined, "type": "count", @@ -42,18 +41,5 @@ describe('agg_expression_functions', () => { } `); }); - - test('correctly parses json string argument', () => { - const actual = fn({ - json: '{ "foo": true }', - }); - - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); - }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/count_fn.ts b/src/plugins/data/common/search/aggs/metrics/count_fn.ts index 338ca18209299..7d4616ffdc619 100644 --- a/src/plugins/data/common/search/aggs/metrics/count_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/count_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; const fnName = 'aggCount'; @@ -55,12 +54,6 @@ export const aggCount = (): FunctionDefinition => ({ defaultMessage: 'Schema to use for this aggregation', }), }, - json: { - types: ['string'], - help: i18n.translate('data.search.aggs.metrics.count.json.help', { - defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', - }), - }, customLabel: { types: ['string'], help: i18n.translate('data.search.aggs.metrics.count.customLabel.help', { @@ -78,10 +71,7 @@ export const aggCount = (): FunctionDefinition => ({ enabled, schema, type: METRIC_TYPES.COUNT, - params: { - ...rest, - json: getParsedValue(args, 'json'), - }, + params: rest, }, }; }, diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index eb5703f1c63c1..27b16c57ffecf 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -440,7 +440,7 @@ export { export { isTimeRange, isQuery, isFilter, isFilters } from '../common'; -export { ApplyGlobalFilterActionContext } from './actions'; +export { ACTION_GLOBAL_APPLY_FILTER, ApplyGlobalFilterActionContext } from './actions'; export * from '../common/field_mapping'; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 9a2a82e8ed206..ba40dece25df9 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -78,6 +78,11 @@ import { UnregisterCallback } from 'history'; import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; import { UserProvidedValues } from 'src/core/server/types'; +// Warning: (ae-missing-release-tag) "ACTION_GLOBAL_APPLY_FILTER" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const ACTION_GLOBAL_APPLY_FILTER = "ACTION_GLOBAL_APPLY_FILTER"; + // Warning: (ae-forgotten-export) The symbol "AggConfigSerialized" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "AggConfigOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1001,7 +1006,7 @@ export class IndexPattern implements IIndexPattern { // (undocumented) id?: string; // (undocumented) - init(forceFieldRefresh?: boolean): Promise; + init(): Promise; // Warning: (ae-forgotten-export) The symbol "IndexPatternSpec" needs to be exported by the entry point index.d.ts // // (undocumented) diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index 987e8f0dae3a0..a0eecef66ff93 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -121,7 +121,7 @@ export const EditIndexPattern = withRouter( const refreshFields = () => { overlays.openConfirm(confirmMessage, confirmModalOptionsRefresh).then(async (isConfirmed) => { if (isConfirmed) { - await indexPattern.init(true); + await indexPattern.refreshFields(); setFields(indexPattern.getNonScriptedFields()); } }); diff --git a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts index ef2f937c8547c..b13ca32601aa9 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -26,7 +26,7 @@ import { IAggType, IndexPattern, IndexPatternField, -} from 'src/plugins/data/public'; +} from '../../../data/public'; import { filterAggTypes, filterAggTypeFields } from '../agg_filters'; import { groupAndSortBy, ComboBoxGroupedOptions } from '../utils'; import { AggTypeState, AggParamsState } from './agg_params_state'; diff --git a/test/functional/apps/visualize/_point_series_options.js b/test/functional/apps/visualize/_point_series_options.js index d08bfe3b90913..c88670ee8b741 100644 --- a/test/functional/apps/visualize/_point_series_options.js +++ b/test/functional/apps/visualize/_point_series_options.js @@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }) { const retry = getService('retry'); const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); + const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects([ 'visualize', 'header', @@ -148,6 +149,10 @@ export default function ({ getService, getPageObjects }) { }); }); + it('should not show advanced json for count agg', async function () { + await testSubjects.missingOrFail('advancedParams-1'); + }); + it('should put secondary axis on the right', async function () { const length = await PageObjects.visChart.getRightValueAxes(); expect(length).to.be(1); diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 69ad9ad33bf72..7d21f958bb80b 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -35,7 +35,7 @@ "xpack.main": "legacy/plugins/xpack_main", "xpack.maps": ["plugins/maps", "legacy/plugins/maps"], "xpack.ml": ["plugins/ml", "legacy/plugins/ml"], - "xpack.monitoring": ["plugins/monitoring", "legacy/plugins/monitoring"], + "xpack.monitoring": ["plugins/monitoring"], "xpack.remoteClusters": "plugins/remote_clusters", "xpack.painlessLab": "plugins/painless_lab", "xpack.reporting": ["plugins/reporting"], diff --git a/x-pack/index.js b/x-pack/index.js index b984782df3986..074b8e6859dc2 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -5,10 +5,9 @@ */ import { xpackMain } from './legacy/plugins/xpack_main'; -import { monitoring } from './legacy/plugins/monitoring'; import { security } from './legacy/plugins/security'; import { spaces } from './legacy/plugins/spaces'; module.exports = function (kibana) { - return [xpackMain(kibana), monitoring(kibana), spaces(kibana), security(kibana)]; + return [xpackMain(kibana), spaces(kibana), security(kibana)]; }; diff --git a/x-pack/legacy/plugins/monitoring/README.md b/x-pack/legacy/plugins/monitoring/README.md deleted file mode 100644 index 0222f06e7ae91..0000000000000 --- a/x-pack/legacy/plugins/monitoring/README.md +++ /dev/null @@ -1,89 +0,0 @@ -## Using Monitoring - -The easiest way to get to know the new Monitoring is probably by [reading the -docs](https://github.com/elastic/x-plugins/blob/master/docs/public/marvel/index.asciidoc). - -Install the distribution the way a customer would is pending the first release -of Unified X-Pack plugins. - -## Developing - -You will need to get Elasticsearch and X-Pack plugins for ES that match the -version of the UI. The best way to do this is to run `gradle run` from a clone -of the x-plugins repository. - -To set up Monitoring and automatic file syncing code changes into Kibana's plugin -directory, clone the kibana and x-plugins repos in the same directory and from -`x-plugins/kibana/monitoring`, run `yarn start`. - -Once the syncing process has run at least once, start the Kibana server in -development mode. It will handle restarting the server and re-optimizing the -bundles as-needed. Go to https://localhost:5601 and click Monitoring from the App -Drawer. - -## Running tests - -- Run the command: - ``` - yarn test - ``` - -- Debug tests -Add a `debugger` line to create a breakpoint, and then: - - ``` - gulp sync && mocha debug --compilers js:@babel/register /pathto/kibana/plugins/monitoring/pathto/__tests__/testfile.js - ``` - -## Multicluster Setup for Development - -To run the UI with multiple clusters, the easiest way is to run 2 nodes out of -the same Elasticsearch directory, but use different start up commands for each one. One -node will be assigned to the "monitoring" cluster and the other will be for the "production" -cluster. - -1. Add the Security users: - ``` - % ./bin/x-pack/users useradd -r remote_monitoring_agent -p notsecure remote - % ./bin/x-pack/users useradd -r monitoring_user -p notsecure monitoring_user - ``` - -1. Start up the Monitoring cluster: - ``` - % ./bin/elasticsearch \ - -Ehttp.port=9210 \ - -Ecluster.name=monitoring \ - -Epath.data=monitoring-data \ - -Enode.name=monitor1node1 - ``` - -1. Start up the Production cluster: - ``` - % ./bin/elasticsearch \ - -Expack.monitoring.exporters.id2.type=http \ - -Expack.monitoring.exporters.id2.host=http://127.0.0.1:9210 \ - -Expack.monitoring.exporters.id2.auth.username=remote \ - -Expack.monitoring.exporters.id2.auth.password=notsecure \ - -Ecluster.name=production \ - -Enode.name=prod1node1 \ - -Epath.data=production-data - ``` - -1. Set the Kibana config: - ``` - % cat config/kibana.dev.yml - monitoring.ui.elasticsearch: - hosts: "http://localhost:9210" - username: "kibana_system" - password: "changeme" - ``` - -1. Start another Kibana instance: - ``` - % yarn start - ``` - -1. Start a Kibana instance connected to the Monitoring cluster (for running queries in Sense on Monitoring data): - ``` - % ./bin/kibana --config config/kibana.dev.yml --elasticsearch.hosts http://localhost:9210 --server.name monitoring-kibana --server.port 5611 - ``` diff --git a/x-pack/legacy/plugins/monitoring/config.ts b/x-pack/legacy/plugins/monitoring/config.ts deleted file mode 100644 index 52f4b866dd7b2..0000000000000 --- a/x-pack/legacy/plugins/monitoring/config.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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. - */ - -/** - * User-configurable settings for xpack.monitoring via configuration schema - * @param {Object} Joi - HapiJS Joi module that allows for schema validation - * @return {Object} config schema - */ -export const config = (Joi: any) => { - const DEFAULT_REQUEST_HEADERS = ['authorization']; - - return Joi.object({ - enabled: Joi.boolean().default(true), - ui: Joi.object({ - enabled: Joi.boolean().default(true), - logs: Joi.object({ - index: Joi.string().default('filebeat-*'), - }).default(), - ccs: Joi.object({ - enabled: Joi.boolean().default(true), - }).default(), - container: Joi.object({ - elasticsearch: Joi.object({ - enabled: Joi.boolean().default(false), - }).default(), - logstash: Joi.object({ - enabled: Joi.boolean().default(false), - }).default(), - }).default(), - max_bucket_size: Joi.number().default(10000), - min_interval_seconds: Joi.number().default(10), - show_license_expiration: Joi.boolean().default(true), - elasticsearch: Joi.object({ - customHeaders: Joi.object().default({}), - logQueries: Joi.boolean().default(false), - requestHeadersWhitelist: Joi.array().items().single().default(DEFAULT_REQUEST_HEADERS), - sniffOnStart: Joi.boolean().default(false), - sniffInterval: Joi.number().allow(false).default(false), - sniffOnConnectionFault: Joi.boolean().default(false), - hosts: Joi.array() - .items(Joi.string().uri({ scheme: ['http', 'https'] })) - .single(), // if empty, use Kibana's connection config - username: Joi.string(), - password: Joi.string(), - requestTimeout: Joi.number().default(30000), - pingTimeout: Joi.number().default(30000), - ssl: Joi.object({ - verificationMode: Joi.string().valid('none', 'certificate', 'full').default('full'), - certificateAuthorities: Joi.array().single().items(Joi.string()), - certificate: Joi.string(), - key: Joi.string(), - keyPassphrase: Joi.string(), - keystore: Joi.object({ - path: Joi.string(), - password: Joi.string(), - }).default(), - truststore: Joi.object({ - path: Joi.string(), - password: Joi.string(), - }).default(), - alwaysPresentCertificate: Joi.boolean().default(false), - }).default(), - apiVersion: Joi.string().default('master'), - logFetchCount: Joi.number().default(10), - }).default(), - }).default(), - kibana: Joi.object({ - collection: Joi.object({ - enabled: Joi.boolean().default(true), - interval: Joi.number().default(10000), // op status metrics get buffered at `ops.interval` and flushed to the bulk endpoint at this interval - }).default(), - }).default(), - elasticsearch: Joi.object({ - customHeaders: Joi.object().default({}), - logQueries: Joi.boolean().default(false), - requestHeadersWhitelist: Joi.array().items().single().default(DEFAULT_REQUEST_HEADERS), - sniffOnStart: Joi.boolean().default(false), - sniffInterval: Joi.number().allow(false).default(false), - sniffOnConnectionFault: Joi.boolean().default(false), - hosts: Joi.array() - .items(Joi.string().uri({ scheme: ['http', 'https'] })) - .single(), // if empty, use Kibana's connection config - username: Joi.string(), - password: Joi.string(), - requestTimeout: Joi.number().default(30000), - pingTimeout: Joi.number().default(30000), - ssl: Joi.object({ - verificationMode: Joi.string().valid('none', 'certificate', 'full').default('full'), - certificateAuthorities: Joi.array().single().items(Joi.string()), - certificate: Joi.string(), - key: Joi.string(), - keyPassphrase: Joi.string(), - keystore: Joi.object({ - path: Joi.string(), - password: Joi.string(), - }).default(), - truststore: Joi.object({ - path: Joi.string(), - password: Joi.string(), - }).default(), - alwaysPresentCertificate: Joi.boolean().default(false), - }).default(), - apiVersion: Joi.string().default('master'), - }).default(), - cluster_alerts: Joi.object({ - enabled: Joi.boolean().default(true), - email_notifications: Joi.object({ - enabled: Joi.boolean().default(true), - email_address: Joi.string().email(), - }).default(), - }).default(), - licensing: Joi.object({ - api_polling_frequency: Joi.number().default(30001), - }), - agent: Joi.object({ - interval: Joi.string() - .regex(/[\d\.]+[yMwdhms]/) - .default('10s'), - }).default(), - tests: Joi.object({ - cloud_detector: Joi.object({ - enabled: Joi.boolean().default(true), - }).default(), - }).default(), - }).default(); -}; diff --git a/x-pack/legacy/plugins/monitoring/index.ts b/x-pack/legacy/plugins/monitoring/index.ts deleted file mode 100644 index f03e1ebc009f5..0000000000000 --- a/x-pack/legacy/plugins/monitoring/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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 Hapi from 'hapi'; -import { config } from './config'; - -/** - * Invokes plugin modules to instantiate the Monitoring plugin for Kibana - * @param kibana {Object} Kibana plugin instance - * @return {Object} Monitoring UI Kibana plugin object - */ -const deps = ['kibana', 'elasticsearch', 'xpack_main']; -export const monitoring = (kibana: any) => { - return new kibana.Plugin({ - require: deps, - id: 'monitoring', - configPrefix: 'monitoring', - init(server: Hapi.Server) { - const npMonitoring = server.newPlatform.setup.plugins.monitoring as object & { - registerLegacyAPI: (api: unknown) => void; - }; - if (npMonitoring) { - const kbnServerStatus = this.kbnServer.status; - npMonitoring.registerLegacyAPI({ - getServerStatus: () => { - const status = kbnServerStatus.toJSON(); - return status?.overall?.state; - }, - }); - } - }, - config, - }); -}; diff --git a/x-pack/package.json b/x-pack/package.json index c2fc16153290e..d49188b29481b 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -195,7 +195,7 @@ "jsdom": "13.1.0", "jsondiffpatch": "0.4.1", "jsts": "^1.6.2", - "kea": "^2.0.1", + "kea": "2.2.0-rc.4", "loader-utils": "^1.2.3", "lz-string": "^1.4.4", "madge": "3.4.4", diff --git a/x-pack/plugins/apm/public/application/application.test.tsx b/x-pack/plugins/apm/public/application/application.test.tsx new file mode 100644 index 0000000000000..fc369b9cf672a --- /dev/null +++ b/x-pack/plugins/apm/public/application/application.test.tsx @@ -0,0 +1,74 @@ +/* + * 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 { act } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +import { Observable } from 'rxjs'; +import { AppMountParameters, CoreStart, HttpSetup } from 'src/core/public'; +import { mockApmPluginContextValue } from '../context/ApmPluginContext/MockApmPluginContext'; +import { ApmPluginSetupDeps } from '../plugin'; +import { createCallApmApi } from '../services/rest/createCallApmApi'; +import { renderApp } from './'; +import { disableConsoleWarning } from '../utils/testHelpers'; + +describe('renderApp', () => { + let mockConsole: jest.SpyInstance; + + beforeAll(() => { + // The RUM agent logs an unnecessary message here. There's a couple open + // issues need to be fixed to get the ability to turn off all of the logging: + // + // * https://github.com/elastic/apm-agent-rum-js/issues/799 + // * https://github.com/elastic/apm-agent-rum-js/issues/861 + // + // for now, override `console.warn` to filter those messages out. + mockConsole = disableConsoleWarning('[Elastic APM]'); + }); + + afterAll(() => { + mockConsole.mockRestore(); + }); + + it('renders the app', () => { + const { core, config } = mockApmPluginContextValue; + const plugins = { + licensing: { license$: new Observable() }, + triggers_actions_ui: { actionTypeRegistry: {}, alertTypeRegistry: {} }, + usageCollection: { reportUiStats: () => {} }, + }; + const params = { + element: document.createElement('div'), + history: createMemoryHistory(), + }; + jest.spyOn(window, 'scrollTo').mockReturnValueOnce(undefined); + createCallApmApi((core.http as unknown) as HttpSetup); + + jest + .spyOn(window.console, 'warn') + .mockImplementationOnce((message: string) => { + if (message.startsWith('[Elastic APM')) { + return; + } else { + console.warn(message); // eslint-disable-line no-console + } + }); + + let unmount: () => void; + + act(() => { + unmount = renderApp( + (core as unknown) as CoreStart, + (plugins as unknown) as ApmPluginSetupDeps, + (params as unknown) as AppMountParameters, + config + ); + }); + + expect(() => { + unmount(); + }).not.toThrowError(); + }); +}); diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index cf3fe2decfa44..d76ed5c2100b2 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -16,11 +16,11 @@ import { ApmPluginSetupDeps } from '../plugin'; import { KibanaContextProvider, useUiSetting$, + RedirectAppLinks, } from '../../../../../src/plugins/kibana_react/public'; import { px, units } from '../style/variables'; import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs'; import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; -import { history, resetHistory } from '../utils/history'; import 'react-vis/dist/style.css'; import { RumHome } from '../components/app/RumDashboard/RumHome'; import { ConfigSchema } from '../index'; @@ -70,12 +70,12 @@ function CsmApp() { export function CsmAppRoot({ core, deps, - routerHistory, + history, config, }: { core: CoreStart; deps: ApmPluginSetupDeps; - routerHistory: typeof history; + history: AppMountParameters['history']; config: ConfigSchema; }) { const i18nCore = core.i18n; @@ -86,19 +86,21 @@ export function CsmAppRoot({ plugins, }; return ( - - - - - - - - - - - - - + + + + + + + + + + + + + + + ); } @@ -109,19 +111,13 @@ export function CsmAppRoot({ export const renderApp = ( core: CoreStart, deps: ApmPluginSetupDeps, - { element }: AppMountParameters, + { element, history }: AppMountParameters, config: ConfigSchema ) => { createCallApmApi(core.http); - resetHistory(); ReactDOM.render( - , + , element ); return () => { diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index 5e502f58e5f56..3f4f3116152c4 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -5,36 +5,36 @@ */ import { ApmRoute } from '@elastic/apm-rum-react'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import React from 'react'; import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; -import styled, { ThemeProvider, DefaultTheme } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import { CoreStart, AppMountParameters } from '../../../../../src/core/public'; -import { ApmPluginSetupDeps } from '../plugin'; +import 'react-vis/dist/style.css'; +import styled, { DefaultTheme, ThemeProvider } from 'styled-components'; +import { ConfigSchema } from '../'; +import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; +import { + KibanaContextProvider, + RedirectAppLinks, + useUiSetting$, +} from '../../../../../src/plugins/kibana_react/public'; +import { AlertsContextProvider } from '../../../triggers_actions_ui/public'; +import { routes } from '../components/app/Main/route_config'; +import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; +import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs'; import { ApmPluginContext } from '../context/ApmPluginContext'; import { LicenseProvider } from '../context/LicenseContext'; import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext'; import { LocationProvider } from '../context/LocationContext'; import { MatchedRouteProvider } from '../context/MatchedRouteContext'; import { UrlParamsProvider } from '../context/UrlParamsContext'; -import { AlertsContextProvider } from '../../../triggers_actions_ui/public'; +import { ApmPluginSetupDeps } from '../plugin'; +import { createCallApmApi } from '../services/rest/createCallApmApi'; import { createStaticIndexPattern } from '../services/rest/index_pattern'; -import { - KibanaContextProvider, - useUiSetting$, -} from '../../../../../src/plugins/kibana_react/public'; -import { px, units } from '../style/variables'; -import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs'; -import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; -import { routes } from '../components/app/Main/route_config'; -import { history, resetHistory } from '../utils/history'; import { setHelpExtension } from '../setHelpExtension'; +import { px, units } from '../style/variables'; import { setReadonlyBadge } from '../updateBadge'; -import { createCallApmApi } from '../services/rest/createCallApmApi'; -import { ConfigSchema } from '..'; -import 'react-vis/dist/style.css'; const MainContainer = styled.div` padding: ${px(units.plus)}; @@ -68,12 +68,12 @@ function App() { export function ApmAppRoot({ core, deps, - routerHistory, + history, config, }: { core: CoreStart; deps: ApmPluginSetupDeps; - routerHistory: typeof history; + history: AppMountParameters['history']; config: ConfigSchema; }) { const i18nCore = core.i18n; @@ -84,36 +84,38 @@ export function ApmAppRoot({ plugins, }; return ( - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + ); } @@ -124,7 +126,7 @@ export function ApmAppRoot({ export const renderApp = ( core: CoreStart, deps: ApmPluginSetupDeps, - { element }: AppMountParameters, + { element, history }: AppMountParameters, config: ConfigSchema ) => { // render APM feedback link in global help menu @@ -133,8 +135,6 @@ export const renderApp = ( createCallApmApi(core.http); - resetHistory(); - // Automatically creates static index pattern and stores as saved object createStaticIndexPattern().catch((e) => { // eslint-disable-next-line no-console @@ -142,12 +142,7 @@ export const renderApp = ( }); ReactDOM.render( - , + , element ); return () => { diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.stories.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.stories.tsx new file mode 100644 index 0000000000000..5ad6fd547169d --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.stories.tsx @@ -0,0 +1,807 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import React from 'react'; +import { EuiThemeProvider } from '../../../../../../observability/public'; +import { Exception } from '../../../../../typings/es_schemas/raw/error_raw'; +import { ExceptionStacktrace } from './ExceptionStacktrace'; + +storiesOf('app/ErrorGroupDetails/DetailView/ExceptionStacktrace', module) + .addDecorator((storyFn) => { + return {storyFn()}; + }) + .add('JavaScript with some context', () => { + const exceptions: Exception[] = [ + { + code: '503', + stacktrace: [ + { + library_frame: true, + exclude_from_grouping: false, + filename: 'node_modules/elastic-apm-http-client/index.js', + abs_path: '/app/node_modules/elastic-apm-http-client/index.js', + line: { + number: 711, + context: + " const err = new Error('Unexpected APM Server response when polling config')", + }, + function: 'processConfigErrorResponse', + context: { + pre: ['', 'function processConfigErrorResponse (res, buf) {'], + post: ['', ' err.code = res.statusCode'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'node_modules/elastic-apm-http-client/index.js', + abs_path: '/app/node_modules/elastic-apm-http-client/index.js', + line: { + number: 196, + context: + ' res.destroy(processConfigErrorResponse(res, buf))', + }, + function: '', + context: { + pre: [' }', ' } else {'], + post: [' }', ' })'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'node_modules/fast-stream-to-buffer/index.js', + abs_path: '/app/node_modules/fast-stream-to-buffer/index.js', + line: { + number: 20, + context: ' cb(err, buffers[0], stream)', + }, + function: 'IncomingMessage.', + context: { + pre: [' break', ' case 1:'], + post: [' break', ' default:'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'node_modules/once/once.js', + abs_path: '/app/node_modules/once/once.js', + line: { + number: 25, + context: ' return f.value = fn.apply(this, arguments)', + }, + function: 'f', + context: { + pre: [' if (f.called) return f.value', ' f.called = true'], + post: [' }', ' f.called = false'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'node_modules/end-of-stream/index.js', + abs_path: '/app/node_modules/end-of-stream/index.js', + line: { + number: 36, + context: '\t\tif (!writable) callback.call(stream);', + }, + function: 'onend', + context: { + pre: ['\tvar onend = function() {', '\t\treadable = false;'], + post: ['\t};', ''], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: 'events.js', + filename: 'events.js', + line: { + number: 327, + }, + function: 'emit', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: '_stream_readable.js', + abs_path: '_stream_readable.js', + line: { + number: 1220, + }, + function: 'endReadableNT', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'internal/process/task_queues.js', + abs_path: 'internal/process/task_queues.js', + line: { + number: 84, + }, + function: 'processTicksAndRejections', + }, + ], + module: 'elastic-apm-http-client', + handled: false, + attributes: { + response: + '\r\n503 Service Temporarily Unavailable\r\n\r\n

503 Service Temporarily Unavailable

\r\n
nginx/1.17.7
\r\n\r\n\r\n', + }, + type: 'Error', + message: 'Unexpected APM Server response when polling config', + }, + ]; + + return ( + + ); + }) + .add('Ruby with context and library frames', () => { + const exceptions: Exception[] = [ + { + stacktrace: [ + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_record/core.rb', + abs_path: + '/usr/local/bundle/gems/activerecord-5.2.4.1/lib/active_record/core.rb', + line: { + number: 177, + }, + function: 'find', + }, + { + library_frame: false, + exclude_from_grouping: false, + filename: 'api/orders_controller.rb', + abs_path: '/app/app/controllers/api/orders_controller.rb', + line: { + number: 23, + context: ' render json: Order.find(params[:id])\n', + }, + function: 'show', + context: { + pre: ['\n', ' def show\n'], + post: [' end\n', ' end\n'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_controller/metal/basic_implicit_render.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/basic_implicit_render.rb', + line: { + number: 6, + }, + function: 'send_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'abstract_controller/base.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/base.rb', + line: { + number: 194, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_controller/metal/rendering.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/rendering.rb', + line: { + number: 30, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'abstract_controller/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/callbacks.rb', + line: { + number: 42, + }, + function: 'block in process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/callbacks.rb', + line: { + number: 132, + }, + function: 'run_callbacks', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'abstract_controller/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/callbacks.rb', + line: { + number: 41, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/rescue.rb', + filename: 'action_controller/metal/rescue.rb', + line: { + number: 22, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/instrumentation.rb', + filename: 'action_controller/metal/instrumentation.rb', + line: { + number: 34, + }, + function: 'block in process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/notifications.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/notifications.rb', + line: { + number: 168, + }, + function: 'block in instrument', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/notifications/instrumenter.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/notifications/instrumenter.rb', + line: { + number: 23, + }, + function: 'instrument', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/notifications.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/notifications.rb', + line: { + number: 168, + }, + function: 'instrument', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_controller/metal/instrumentation.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/instrumentation.rb', + line: { + number: 32, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/params_wrapper.rb', + filename: 'action_controller/metal/params_wrapper.rb', + line: { + number: 256, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_record/railties/controller_runtime.rb', + abs_path: + '/usr/local/bundle/gems/activerecord-5.2.4.1/lib/active_record/railties/controller_runtime.rb', + line: { + number: 24, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'abstract_controller/base.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/base.rb', + line: { + number: 134, + }, + function: 'process', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_view/rendering.rb', + abs_path: + '/usr/local/bundle/gems/actionview-5.2.4.1/lib/action_view/rendering.rb', + line: { + number: 32, + }, + function: 'process', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_controller/metal.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal.rb', + line: { + number: 191, + }, + function: 'dispatch', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal.rb', + filename: 'action_controller/metal.rb', + line: { + number: 252, + }, + function: 'dispatch', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'action_dispatch/routing/route_set.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/routing/route_set.rb', + line: { + number: 52, + }, + function: 'dispatch', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/routing/route_set.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/routing/route_set.rb', + line: { + number: 34, + }, + function: 'serve', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/journey/router.rb', + filename: 'action_dispatch/journey/router.rb', + line: { + number: 52, + }, + function: 'block in serve', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/journey/router.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/journey/router.rb', + line: { + number: 35, + }, + function: 'each', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/journey/router.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/journey/router.rb', + line: { + number: 35, + }, + function: 'serve', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/routing/route_set.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/routing/route_set.rb', + line: { + number: 840, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'rack/static.rb', + abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/static.rb', + line: { + number: 161, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/tempfile_reaper.rb', + abs_path: + '/usr/local/bundle/gems/rack-2.2.3/lib/rack/tempfile_reaper.rb', + line: { + number: 15, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/etag.rb', + abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/etag.rb', + line: { + number: 27, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/conditional_get.rb', + abs_path: + '/usr/local/bundle/gems/rack-2.2.3/lib/rack/conditional_get.rb', + line: { + number: 27, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/head.rb', + abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/head.rb', + line: { + number: 12, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/http/content_security_policy.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/http/content_security_policy.rb', + line: { + number: 18, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'rack/session/abstract/id.rb', + abs_path: + '/usr/local/bundle/gems/rack-2.2.3/lib/rack/session/abstract/id.rb', + line: { + number: 266, + }, + function: 'context', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/session/abstract/id.rb', + abs_path: + '/usr/local/bundle/gems/rack-2.2.3/lib/rack/session/abstract/id.rb', + line: { + number: 260, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/cookies.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/cookies.rb', + line: { + number: 670, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/callbacks.rb', + line: { + number: 28, + }, + function: 'block in call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/callbacks.rb', + line: { + number: 98, + }, + function: 'run_callbacks', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/callbacks.rb', + line: { + number: 26, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/debug_exceptions.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/debug_exceptions.rb', + line: { + number: 61, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'action_dispatch/middleware/show_exceptions.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/show_exceptions.rb', + line: { + number: 33, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'lograge/rails_ext/rack/logger.rb', + abs_path: + '/usr/local/bundle/gems/lograge-0.11.2/lib/lograge/rails_ext/rack/logger.rb', + line: { + number: 15, + }, + function: 'call_app', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'rails/rack/logger.rb', + abs_path: + '/usr/local/bundle/gems/railties-5.2.4.1/lib/rails/rack/logger.rb', + line: { + number: 28, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/remote_ip.rb', + filename: 'action_dispatch/middleware/remote_ip.rb', + line: { + number: 81, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'request_store/middleware.rb', + abs_path: + '/usr/local/bundle/gems/request_store-1.5.0/lib/request_store/middleware.rb', + line: { + number: 19, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/request_id.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/request_id.rb', + line: { + number: 27, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/method_override.rb', + abs_path: + '/usr/local/bundle/gems/rack-2.2.3/lib/rack/method_override.rb', + line: { + number: 24, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/runtime.rb', + abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/runtime.rb', + line: { + number: 22, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/cache/strategy/local_cache_middleware.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/cache/strategy/local_cache_middleware.rb', + line: { + number: 29, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/executor.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/executor.rb', + line: { + number: 14, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/static.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/static.rb', + line: { + number: 127, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/sendfile.rb', + abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/sendfile.rb', + line: { + number: 110, + }, + function: 'call', + }, + { + library_frame: false, + exclude_from_grouping: false, + filename: 'opbeans_shuffle.rb', + abs_path: '/app/lib/opbeans_shuffle.rb', + line: { + number: 32, + context: ' @app.call(env)\n', + }, + function: 'call', + context: { + pre: [' end\n', ' else\n'], + post: [' end\n', ' rescue Timeout::Error\n'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'elastic_apm/middleware.rb', + abs_path: + '/usr/local/bundle/gems/elastic-apm-3.8.0/lib/elastic_apm/middleware.rb', + line: { + number: 36, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rails/engine.rb', + abs_path: + '/usr/local/bundle/gems/railties-5.2.4.1/lib/rails/engine.rb', + line: { + number: 524, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'puma/configuration.rb', + abs_path: + '/usr/local/bundle/gems/puma-4.3.5/lib/puma/configuration.rb', + line: { + number: 228, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'puma/server.rb', + abs_path: '/usr/local/bundle/gems/puma-4.3.5/lib/puma/server.rb', + line: { + number: 713, + }, + function: 'handle_request', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'puma/server.rb', + abs_path: '/usr/local/bundle/gems/puma-4.3.5/lib/puma/server.rb', + line: { + number: 472, + }, + function: 'process_client', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'puma/server.rb', + abs_path: '/usr/local/bundle/gems/puma-4.3.5/lib/puma/server.rb', + line: { + number: 328, + }, + function: 'block in run', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'puma/thread_pool.rb', + abs_path: + '/usr/local/bundle/gems/puma-4.3.5/lib/puma/thread_pool.rb', + line: { + number: 134, + }, + function: 'block in spawn_thread', + }, + ], + handled: false, + module: 'ActiveRecord', + message: "Couldn't find Order with 'id'=956", + type: 'ActiveRecord::RecordNotFound', + }, + ]; + + return ; + }); diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index 4e1af6e0dc239..5202ca13ed102 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -6,40 +6,40 @@ import { EuiButtonEmpty, + EuiIcon, EuiPanel, EuiSpacer, EuiTab, EuiTabs, EuiTitle, - EuiIcon, EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Location } from 'history'; +import { first } from 'lodash'; import React from 'react'; +import { useHistory } from 'react-router-dom'; import styled from 'styled-components'; -import { first } from 'lodash'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ErrorGroupAPIResponse } from '../../../../../server/lib/errors/get_error_group'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { px, unit, units } from '../../../../style/variables'; +import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; import { DiscoverErrorLink } from '../../../shared/Links/DiscoverLinks/DiscoverErrorLink'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; -import { history } from '../../../../utils/history'; import { ErrorMetadata } from '../../../shared/MetadataTable/ErrorMetadata'; import { Stacktrace } from '../../../shared/Stacktrace'; +import { Summary } from '../../../shared/Summary'; +import { HttpInfoSummaryItem } from '../../../shared/Summary/HttpInfoSummaryItem'; +import { UserAgentSummaryItem } from '../../../shared/Summary/UserAgentSummaryItem'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; import { ErrorTab, exceptionStacktraceTab, getTabs, logStacktraceTab, } from './ErrorTabs'; -import { Summary } from '../../../shared/Summary'; -import { TimestampTooltip } from '../../../shared/TimestampTooltip'; -import { HttpInfoSummaryItem } from '../../../shared/Summary/HttpInfoSummaryItem'; -import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; -import { UserAgentSummaryItem } from '../../../shared/Summary/UserAgentSummaryItem'; import { ExceptionStacktrace } from './ExceptionStacktrace'; const HeaderContainer = styled.div` @@ -71,6 +71,7 @@ function getCurrentTab( } export function DetailView({ errorGroup, urlParams, location }: Props) { + const history = useHistory(); const { transaction, error, occurrencesCount } = errorGroup; if (!error) { diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx index a173f4068db6a..5798deaf19c9c 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx @@ -6,10 +6,11 @@ import { mount } from 'enzyme'; import React from 'react'; +import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockUrlParamsContextProvider } from '../../../../../context/UrlParamsContext/MockUrlParamsContextProvider'; import { mockMoment, toJson } from '../../../../../utils/testHelpers'; import { ErrorGroupList } from '../index'; import props from './props.json'; -import { MockUrlParamsContextProvider } from '../../../../../context/UrlParamsContext/MockUrlParamsContextProvider'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { @@ -36,9 +37,11 @@ describe('ErrorGroupOverview -> List', () => { it('should render with data', () => { const wrapper = mount( - - - + + + + + ); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap index 40522edc21b52..5183432b4ae0f 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap @@ -784,11 +784,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > a0ce2 @@ -829,11 +829,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > @@ -876,13 +876,13 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > List should render with data 1`] = ` > f3ac9 @@ -1063,11 +1063,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > @@ -1110,13 +1110,13 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > List should render with data 1`] = ` > e9086 @@ -1297,11 +1297,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > @@ -1344,13 +1344,13 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > List should render with data 1`] = ` > 8673d @@ -1531,11 +1531,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > @@ -1578,13 +1578,13 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > { 'rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0' ); const breadcrumbs = setBreadcrumbs.mock.calls[0][0]; - expect(breadcrumbs).toEqual([ - { - text: 'APM', - href: - '#/?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', - }, - { - text: 'Services', - href: - '#/services?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', - }, - { - text: 'opbeans-node', - href: - '#/services/opbeans-node?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', - }, - { - text: 'Errors', - href: - '#/services/opbeans-node/errors?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', - }, - { text: 'myGroupId', href: undefined }, - ]); + expect(breadcrumbs).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + text: 'APM', + href: + '/basepath/app/apm/?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', + }), + expect.objectContaining({ + text: 'Services', + href: + '/basepath/app/apm/services?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', + }), + expect.objectContaining({ + text: 'opbeans-node', + href: + '/basepath/app/apm/services/opbeans-node?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', + }), + expect.objectContaining({ + text: 'Errors', + href: + '/basepath/app/apm/services/opbeans-node/errors?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', + }), + expect.objectContaining({ text: 'myGroupId', href: undefined }), + ]) + ); + expect(changeTitle).toHaveBeenCalledWith([ 'myGroupId', 'Errors', @@ -95,12 +98,23 @@ describe('UpdateBreadcrumbs', () => { it('/services/:serviceName/errors', () => { mountBreadcrumb('/services/opbeans-node/errors'); const breadcrumbs = setBreadcrumbs.mock.calls[0][0]; - expect(breadcrumbs).toEqual([ - { text: 'APM', href: '#/?kuery=myKuery' }, - { text: 'Services', href: '#/services?kuery=myKuery' }, - { text: 'opbeans-node', href: '#/services/opbeans-node?kuery=myKuery' }, - { text: 'Errors', href: undefined }, - ]); + expect(breadcrumbs).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + text: 'APM', + href: '/basepath/app/apm/?kuery=myKuery', + }), + expect.objectContaining({ + text: 'Services', + href: '/basepath/app/apm/services?kuery=myKuery', + }), + expect.objectContaining({ + text: 'opbeans-node', + href: '/basepath/app/apm/services/opbeans-node?kuery=myKuery', + }), + expect.objectContaining({ text: 'Errors', href: undefined }), + ]) + ); expect(changeTitle).toHaveBeenCalledWith([ 'Errors', 'opbeans-node', @@ -112,12 +126,24 @@ describe('UpdateBreadcrumbs', () => { it('/services/:serviceName/transactions', () => { mountBreadcrumb('/services/opbeans-node/transactions'); const breadcrumbs = setBreadcrumbs.mock.calls[0][0]; - expect(breadcrumbs).toEqual([ - { text: 'APM', href: '#/?kuery=myKuery' }, - { text: 'Services', href: '#/services?kuery=myKuery' }, - { text: 'opbeans-node', href: '#/services/opbeans-node?kuery=myKuery' }, - { text: 'Transactions', href: undefined }, - ]); + expect(breadcrumbs).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + text: 'APM', + href: '/basepath/app/apm/?kuery=myKuery', + }), + expect.objectContaining({ + text: 'Services', + href: '/basepath/app/apm/services?kuery=myKuery', + }), + expect.objectContaining({ + text: 'opbeans-node', + href: '/basepath/app/apm/services/opbeans-node?kuery=myKuery', + }), + expect.objectContaining({ text: 'Transactions', href: undefined }), + ]) + ); + expect(changeTitle).toHaveBeenCalledWith([ 'Transactions', 'opbeans-node', @@ -132,16 +158,33 @@ describe('UpdateBreadcrumbs', () => { 'transactionName=my-transaction-name' ); const breadcrumbs = setBreadcrumbs.mock.calls[0][0]; - expect(breadcrumbs).toEqual([ - { text: 'APM', href: '#/?kuery=myKuery' }, - { text: 'Services', href: '#/services?kuery=myKuery' }, - { text: 'opbeans-node', href: '#/services/opbeans-node?kuery=myKuery' }, - { - text: 'Transactions', - href: '#/services/opbeans-node/transactions?kuery=myKuery', - }, - { text: 'my-transaction-name', href: undefined }, - ]); + + expect(breadcrumbs).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + text: 'APM', + href: '/basepath/app/apm/?kuery=myKuery', + }), + expect.objectContaining({ + text: 'Services', + href: '/basepath/app/apm/services?kuery=myKuery', + }), + expect.objectContaining({ + text: 'opbeans-node', + href: '/basepath/app/apm/services/opbeans-node?kuery=myKuery', + }), + expect.objectContaining({ + text: 'Transactions', + href: + '/basepath/app/apm/services/opbeans-node/transactions?kuery=myKuery', + }), + expect.objectContaining({ + text: 'my-transaction-name', + href: undefined, + }), + ]) + ); + expect(changeTitle).toHaveBeenCalledWith([ 'my-transaction-name', 'Transactions', diff --git a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx index e7657c63f41bb..5bf5cea587f93 100644 --- a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx @@ -5,20 +5,20 @@ */ import { Location } from 'history'; -import React from 'react'; -import { AppMountContext } from 'src/core/public'; +import React, { MouseEvent } from 'react'; +import { CoreStart } from 'src/core/public'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; import { Breadcrumb, - ProvideBreadcrumbs, BreadcrumbRoute, + ProvideBreadcrumbs, } from './ProvideBreadcrumbs'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; interface Props { location: Location; breadcrumbs: Breadcrumb[]; - core: AppMountContext['core']; + core: CoreStart; } function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumb[]) { @@ -27,15 +27,24 @@ function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumb[]) { class UpdateBreadcrumbsComponent extends React.Component { public updateHeaderBreadcrumbs() { + const { basePath } = this.props.core.http; const breadcrumbs = this.props.breadcrumbs.map( ({ value, match }, index) => { + const { search } = this.props.location; const isLastBreadcrumbItem = index === this.props.breadcrumbs.length - 1; + const href = isLastBreadcrumbItem + ? undefined // makes the breadcrumb item not clickable + : getAPMHref({ basePath, path: match.url, search }); return { text: value, - href: isLastBreadcrumbItem - ? undefined // makes the breadcrumb item not clickable - : getAPMHref(match.url, this.props.location.search), + href, + onClick: (event: MouseEvent) => { + if (href) { + event.preventDefault(); + this.props.core.application.navigateToUrl(href); + } + }, }; } ); diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index 8caddc94b6907..56026dcf477ec 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -38,14 +38,28 @@ interface RouteParams { } export const renderAsRedirectTo = (to: string) => { - return ({ location }: RouteComponentProps) => ( - - ); + return ({ location }: RouteComponentProps) => { + let resolvedUrl: URL | undefined; + + // Redirect root URLs with a hash to support backward compatibility with URLs + // from before we switched to the non-hash platform history. + if (location.pathname === '' && location.hash.length > 0) { + // We just want the search and pathname so the host doesn't matter + resolvedUrl = new URL(location.hash.slice(1), 'http://localhost'); + to = resolvedUrl.pathname; + } + + return ( + + ); + }; }; export const routes: BreadcrumbRoute[] = [ diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx new file mode 100644 index 0000000000000..ad12afe35fa20 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx @@ -0,0 +1,40 @@ +/* + * 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 { routes } from './'; + +describe('routes', () => { + describe('/', () => { + const route = routes.find((r) => r.path === '/'); + + describe('with no hash path', () => { + it('redirects to /services', () => { + const location = { hash: '', pathname: '/', search: '' }; + expect( + (route as any).render({ location } as any).props.to.pathname + ).toEqual('/services'); + }); + }); + + describe('with a hash path', () => { + it('redirects to the hash path', () => { + const location = { + hash: + '#/services/opbeans-python/transactions/view?rangeFrom=now-24h&rangeTo=now&refreshInterval=10000&refreshPaused=false&traceId=d919c89dc7ca48d84b9dde1fef01d1f8&transactionId=1b542853d787ba7b&transactionName=GET%20opbeans.views.product_customers&transactionType=request&flyoutDetailTab=&waterfallItemId=1b542853d787ba7b', + pathname: '', + search: '', + }; + + expect(((route as any).render({ location }) as any).props.to).toEqual({ + hash: '', + pathname: '/services/opbeans-python/transactions/view', + search: + '?rangeFrom=now-24h&rangeTo=now&refreshInterval=10000&refreshPaused=false&traceId=d919c89dc7ca48d84b9dde1fef01d1f8&transactionId=1b542853d787ba7b&transactionName=GET%20opbeans.views.product_customers&transactionType=request&flyoutDetailTab=&waterfallItemId=1b542853d787ba7b', + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx index 14c912d0bd519..d99dc4d5cd37a 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx @@ -5,13 +5,14 @@ */ import React from 'react'; +import { useHistory } from 'react-router-dom'; import { useFetcher } from '../../../../../hooks/useFetcher'; -import { history } from '../../../../../utils/history'; +import { toQuery } from '../../../../shared/Links/url_helpers'; import { Settings } from '../../../Settings'; import { AgentConfigurationCreateEdit } from '../../../Settings/AgentConfigurations/AgentConfigurationCreateEdit'; -import { toQuery } from '../../../../shared/Links/url_helpers'; export function EditAgentConfigurationRouteHandler() { + const history = useHistory(); const { search } = history.location; // typescript complains because `pageStop` does not exist in `APMQueryParams` @@ -40,6 +41,7 @@ export function EditAgentConfigurationRouteHandler() { } export function CreateAgentConfigurationRouteHandler() { + const history = useHistory(); const { search } = history.location; // Ignoring here because we specifically DO NOT want to add the query params to the global route handler diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx index 9211504a2dffe..c76be19edfe47 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx @@ -4,33 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import numeral from '@elastic/numeral'; import { Axis, BarSeries, BrushEndListener, Chart, + DARK_THEME, + LIGHT_THEME, niceTimeFormatByDay, ScaleType, SeriesNameFn, Settings, timeFormatter, } from '@elastic/charts'; -import { DARK_THEME, LIGHT_THEME } from '@elastic/charts'; - +import { Position } from '@elastic/charts/dist/utils/commons'; import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT, } from '@elastic/eui/dist/eui_charts_theme'; +import numeral from '@elastic/numeral'; import moment from 'moment'; -import { Position } from '@elastic/charts/dist/utils/commons'; -import { I18LABELS } from '../translations'; -import { history } from '../../../../utils/history'; -import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; -import { ChartWrapper } from '../ChartWrapper'; +import React from 'react'; +import { useHistory } from 'react-router-dom'; import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public'; import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; +import { ChartWrapper } from '../ChartWrapper'; +import { I18LABELS } from '../translations'; interface Props { data?: Array>; @@ -38,6 +38,7 @@ interface Props { } export function PageViewsChart({ data, loading }: Props) { + const history = useHistory(); const { urlParams } = useUrlParams(); const { start, end } = urlParams; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.test.tsx new file mode 100644 index 0000000000000..1187b71dff825 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { render } from '@testing-library/react'; +import cytoscape from 'cytoscape'; +import React, { ReactNode } from 'react'; +import { ThemeContext } from 'styled-components'; +import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { Controls } from './Controls'; +import { CytoscapeContext } from './Cytoscape'; + +const cy = cytoscape({ + elements: [{ classes: 'primary', data: { id: 'test node' } }], +}); + +function Wrapper({ children }: { children?: ReactNode }) { + return ( + + + + {children} + + + + ); +} + +describe('Controls', () => { + describe('with a primary node', () => { + it('links to the full map', async () => { + const result = render(, { wrapper: Wrapper }); + const { findByTestId } = result; + + const button = await findByTestId('viewFullMapButton'); + + expect(button.getAttribute('href')).toEqual( + '/basepath/app/apm/service-map' + ); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx index bcc87cbf35819..c8f586240471f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx @@ -4,16 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useEffect, useState } from 'react'; import { EuiButtonIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import React, { useContext, useEffect, useState } from 'react'; import styled from 'styled-components'; -import { CytoscapeContext } from './Cytoscape'; -import { getAnimationOptions, getNodeHeight } from './cytoscapeOptions'; -import { getAPMHref } from '../../shared/Links/apm/APMLink'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useTheme } from '../../../hooks/useTheme'; import { useUrlParams } from '../../../hooks/useUrlParams'; +import { getAPMHref } from '../../shared/Links/apm/APMLink'; import { APMQueryParams } from '../../shared/Links/url_helpers'; -import { useTheme } from '../../../hooks/useTheme'; +import { CytoscapeContext } from './Cytoscape'; +import { getAnimationOptions, getNodeHeight } from './cytoscapeOptions'; const ControlsContainer = styled('div')` left: ${({ theme }) => theme.eui.gutterTypes.gutterMedium}; @@ -96,6 +97,8 @@ function useDebugDownloadUrl(cy?: cytoscape.Core) { } export function Controls() { + const { core } = useApmPluginContext(); + const { basePath } = core.http; const theme = useTheme(); const cy = useContext(CytoscapeContext); const { urlParams } = useUrlParams(); @@ -103,6 +106,12 @@ export function Controls() { const [zoom, setZoom] = useState((cy && cy.zoom()) || 1); const duration = parseInt(theme.eui.euiAnimSpeedFast, 10); const downloadUrl = useDebugDownloadUrl(cy); + const viewFullMapUrl = getAPMHref({ + basePath, + path: '/service-map', + search: currentSearch, + query: urlParams as APMQueryParams, + }); // Handle zoom events useEffect(() => { @@ -209,11 +218,8 @@ export function Controls() { + {formatter(value).formatted} + + ); +} + +describe('useFormatter', () => { + const timeSeries = ([ + { + data: [ + { x: 1, y: toMicroseconds(11, 'minutes') }, + { x: 2, y: toMicroseconds(1, 'minutes') }, + { x: 3, y: toMicroseconds(60, 'seconds') }, + ], + }, + { + data: [ + { x: 1, y: toMicroseconds(120, 'seconds') }, + { x: 2, y: toMicroseconds(1, 'minutes') }, + { x: 3, y: toMicroseconds(60, 'seconds') }, + ], + }, + { + data: [ + { x: 1, y: toMicroseconds(60, 'seconds') }, + { x: 2, y: toMicroseconds(5, 'minutes') }, + { x: 3, y: toMicroseconds(100, 'seconds') }, + ], + }, + ] as unknown) as TimeSeries[]; + it('returns new formatter when disabled series state changes', () => { + const { getByText } = render( + + ); + expect(getByText('2.0 min')).toBeInTheDocument(); + act(() => { + fireEvent.click(getByText('disable series')); + }); + expect(getByText('120 s')).toBeInTheDocument(); + }); + it('falls back to the first formatter when disabled series is empty', () => { + const { getByText } = render( + + ); + expect(getByText('2.0 min')).toBeInTheDocument(); + act(() => { + fireEvent.click(getByText('disable series')); + }); + expect(getByText('2.0 min')).toBeInTheDocument(); + // const { formatter, setDisabledSeriesState } = useFormatter(timeSeries); + // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min'); + // setDisabledSeriesState([true, true, false]); + // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min'); + }); + it('falls back to the first formatter when disabled series is all true', () => { + const { getByText } = render( + + ); + expect(getByText('2.0 min')).toBeInTheDocument(); + act(() => { + fireEvent.click(getByText('disable series')); + }); + expect(getByText('2.0 min')).toBeInTheDocument(); + // const { formatter, setDisabledSeriesState } = useFormatter(timeSeries); + // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min'); + // setDisabledSeriesState([true, true, false]); + // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min'); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts new file mode 100644 index 0000000000000..8cd8929c89960 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts @@ -0,0 +1,30 @@ +/* + * 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 { useState, Dispatch, SetStateAction } from 'react'; +import { isEmpty } from 'lodash'; +import { + getDurationFormatter, + TimeFormatter, +} from '../../../../utils/formatters'; +import { TimeSeries } from '../../../../../typings/timeseries'; +import { getMaxY } from './helper'; + +export const useFormatter = ( + series: TimeSeries[] +): { + formatter: TimeFormatter; + setDisabledSeriesState: Dispatch>; +} => { + const [disabledSeriesState, setDisabledSeriesState] = useState([]); + const visibleSeries = series.filter( + (serie, index) => disabledSeriesState[index] !== true + ); + const maxY = getMaxY(isEmpty(visibleSeries) ? series : visibleSeries); + const formatter = getDurationFormatter(maxY); + + return { formatter, setDisabledSeriesState }; +}; diff --git a/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx b/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx index 8c38cdcda958d..8334efffbd511 100644 --- a/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx +++ b/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx @@ -3,11 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; +import { Observable, of } from 'rxjs'; import { ApmPluginContext, ApmPluginContextValue } from '.'; -import { createCallApmApi } from '../../services/rest/createCallApmApi'; import { ConfigSchema } from '../..'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; +import { createCallApmApi } from '../../services/rest/createCallApmApi'; const uiSettings: Record = { [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [ @@ -33,8 +34,17 @@ const uiSettings: Record = { }; const mockCore = { + application: { + capabilities: { + apm: {}, + }, + currentAppId$: new Observable(), + }, chrome: { + docTitle: { change: () => {} }, setBreadcrumbs: () => {}, + setHelpExtension: () => {}, + setBadge: () => {}, }, docLinks: { DOC_LINK_VERSION: '0', @@ -45,6 +55,9 @@ const mockCore = { prepend: (path: string) => `/basepath${path}`, }, }, + i18n: { + Context: ({ children }: { children: ReactNode }) => children, + }, notifications: { toasts: { addWarning: () => {}, @@ -53,6 +66,7 @@ const mockCore = { }, uiSettings: { get: (key: string) => uiSettings[key], + get$: (key: string) => of(mockCore.uiSettings.get(key)), }, }; diff --git a/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx b/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx index 37304d292540d..39d961f6a8164 100644 --- a/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx +++ b/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx @@ -4,16 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CoreStart } from 'kibana/public'; import { createContext } from 'react'; -import { AppMountContext } from 'kibana/public'; -import { ConfigSchema } from '../..'; +import { ConfigSchema } from '../../'; import { ApmPluginSetupDeps } from '../../plugin'; -export type AppMountContextBasePath = AppMountContext['core']['http']['basePath']; - export interface ApmPluginContextValue { config: ConfigSchema; - core: AppMountContext['core']; + core: CoreStart; plugins: ApmPluginSetupDeps; } diff --git a/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx b/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx index f93b69a877057..801c1d7e53f2e 100644 --- a/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx +++ b/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx @@ -5,10 +5,10 @@ */ import React, { ReactNode, useMemo, useState } from 'react'; -import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; -import { history } from '../utils/history'; -import { useUrlParams } from '../hooks/useUrlParams'; +import { useHistory } from 'react-router-dom'; +import { fromQuery, toQuery } from '../components/shared/Links/url_helpers'; import { useFetcher } from '../hooks/useFetcher'; +import { useUrlParams } from '../hooks/useUrlParams'; const ChartsSyncContext = React.createContext<{ hoverX: number | null; @@ -18,6 +18,7 @@ const ChartsSyncContext = React.createContext<{ } | null>(null); function ChartsSyncContextProvider({ children }: { children: ReactNode }) { + const history = useHistory(); const [time, setTime] = useState(null); const { urlParams, uiFilters } = useUrlParams(); @@ -75,7 +76,7 @@ function ChartsSyncContextProvider({ children }: { children: ReactNode }) { }; return { ...hoverXHandlers }; - }, [time, data.annotations]); + }, [history, time, data.annotations]); return ; } diff --git a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts index 45ede7e7f2607..0ed26fe089487 100644 --- a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts @@ -5,21 +5,21 @@ */ import { omit } from 'lodash'; -import { useFetcher } from './useFetcher'; +import { useHistory } from 'react-router-dom'; +import { Projection } from '../../common/projections'; +import { pickKeys } from '../../common/utils/pick_keys'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LocalUIFiltersAPIResponse } from '../../server/lib/ui_filters/local_ui_filters'; -import { useUrlParams } from './useUrlParams'; import { LocalUIFilterName, localUIFilters, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/ui_filters/local_ui_filters/config'; -import { history } from '../utils/history'; -import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; +import { fromQuery, toQuery } from '../components/shared/Links/url_helpers'; import { removeUndefinedProps } from '../context/UrlParamsContext/helpers'; -import { Projection } from '../../common/projections'; -import { pickKeys } from '../../common/utils/pick_keys'; import { useCallApi } from './useCallApi'; +import { useFetcher } from './useFetcher'; +import { useUrlParams } from './useUrlParams'; const getInitialData = ( filterNames: LocalUIFilterName[] @@ -39,6 +39,7 @@ export function useLocalUIFilters({ filterNames: LocalUIFilterName[]; params?: Record; }) { + const history = useHistory(); const { uiFilters, urlParams } = useUrlParams(); const callApi = useCallApi(); diff --git a/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts b/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts index c4d59beb4b7a2..ca8a4919dd216 100644 --- a/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts +++ b/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts @@ -20,9 +20,12 @@ describe('duration formatters', () => { '10,000 ms' ); expect(asDuration(toMicroseconds(20, 'seconds'))).toEqual('20 s'); - expect(asDuration(toMicroseconds(10, 'minutes'))).toEqual('10 min'); + expect(asDuration(toMicroseconds(10, 'minutes'))).toEqual('600 s'); + expect(asDuration(toMicroseconds(11, 'minutes'))).toEqual('11 min'); expect(asDuration(toMicroseconds(1, 'hours'))).toEqual('60 min'); - expect(asDuration(toMicroseconds(1.5, 'hours'))).toEqual('1.5 h'); + expect(asDuration(toMicroseconds(1.5, 'hours'))).toEqual('90 min'); + expect(asDuration(toMicroseconds(10, 'hours'))).toEqual('600 min'); + expect(asDuration(toMicroseconds(11, 'hours'))).toEqual('11 h'); }); it('falls back to default value', () => { diff --git a/x-pack/plugins/apm/public/utils/formatters/duration.ts b/x-pack/plugins/apm/public/utils/formatters/duration.ts index 64a9e3b952b98..8381b0afb5f07 100644 --- a/x-pack/plugins/apm/public/utils/formatters/duration.ts +++ b/x-pack/plugins/apm/public/utils/formatters/duration.ts @@ -127,10 +127,10 @@ export const toMicroseconds = (value: number, timeUnit: TimeUnit) => moment.duration(value, timeUnit).asMilliseconds() * 1000; function getDurationUnitKey(max: number): DurationTimeUnit { - if (max > toMicroseconds(1, 'hours')) { + if (max > toMicroseconds(10, 'hours')) { return 'hours'; } - if (max > toMicroseconds(1, 'minutes')) { + if (max > toMicroseconds(10, 'minutes')) { return 'minutes'; } if (max > toMicroseconds(10, 'seconds')) { diff --git a/x-pack/plugins/apm/public/utils/history.ts b/x-pack/plugins/apm/public/utils/history.ts deleted file mode 100644 index bd2203fe92066..0000000000000 --- a/x-pack/plugins/apm/public/utils/history.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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 { createHashHistory } from 'history'; - -// Make history singleton available across APM project -// TODO: Explore using React context or hook instead? -let history = createHashHistory(); - -export const resetHistory = () => { - history = createHashHistory(); -}; - -export { history }; diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index a750a9ea7af67..037da01c74464 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -26,6 +26,20 @@ import { } from '../../typings/elasticsearch'; import { MockApmPluginContextWrapper } from '../context/ApmPluginContext/MockApmPluginContext'; +const originalConsoleWarn = console.warn; // eslint-disable-line no-console +/** + * A dependency we're using is using deprecated react methods. Override the + * console to hide the warnings. These should go away when we switch to + * Elastic Charts + */ +export function disableConsoleWarning(messageToDisable: string) { + return jest.spyOn(console, 'warn').mockImplementation((message) => { + if (!message.startsWith(messageToDisable)) { + originalConsoleWarn(message); + } + }); +} + export function toJson(wrapper: ReactWrapper) { return enzymeToJson(wrapper, { noKey: true, diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/error_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/error_raw.ts index 8e49d02beb908..b8eb79aabdcc5 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/error_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/error_raw.ts @@ -12,7 +12,7 @@ import { Kubernetes } from './fields/kubernetes'; import { Page } from './fields/page'; import { Process } from './fields/process'; import { Service } from './fields/service'; -import { IStackframe } from './fields/stackframe'; +import { Stackframe } from './fields/stackframe'; import { Url } from './fields/url'; import { User } from './fields/user'; import { Observer } from './fields/observer'; @@ -23,16 +23,20 @@ interface Processor { } export interface Exception { + attributes?: { + response?: string; + }; + code?: string; message?: string; // either message or type are given type?: string; module?: string; handled?: boolean; - stacktrace?: IStackframe[]; + stacktrace?: Stackframe[]; } interface Log { message: string; - stacktrace?: IStackframe[]; + stacktrace?: Stackframe[]; } export interface ErrorRaw extends APMBaseDoc { diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/fields/stackframe.ts b/x-pack/plugins/apm/typings/es_schemas/raw/fields/stackframe.ts index 05b0eb88da40b..901b02814371a 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/fields/stackframe.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/fields/stackframe.ts @@ -4,27 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ -type IStackframeBase = { - function?: string; - library_frame?: boolean; - exclude_from_grouping?: boolean; +interface Line { + column?: number; + number: number; +} + +interface Sourcemap { + error?: string; + updated?: boolean; +} + +interface StackframeBase { + abs_path?: string; + classname?: string; context?: { post?: string[]; pre?: string[]; }; + exclude_from_grouping?: boolean; + filename?: string; + function?: string; + module?: string; + library_frame?: boolean; + line?: Line; + sourcemap?: Sourcemap; vars?: { [key: string]: unknown; }; - line?: { - number: number; - }; -} & ({ classname: string } | { filename: string }); +} -export type IStackframeWithLineContext = IStackframeBase & { - line: { - number: number; +export type StackframeWithLineContext = StackframeBase & { + line: Line & { context: string; }; }; -export type IStackframe = IStackframeBase | IStackframeWithLineContext; +export type Stackframe = StackframeBase | StackframeWithLineContext; diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts index f6c4fce76f134..5c2e391059783 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts @@ -5,7 +5,7 @@ */ import { APMBaseDoc } from './apm_base_doc'; -import { IStackframe } from './fields/stackframe'; +import { Stackframe } from './fields/stackframe'; import { Observer } from './fields/observer'; interface Processor { @@ -24,7 +24,7 @@ export interface SpanRaw extends APMBaseDoc { duration: { us: number }; id: string; name: string; - stacktrace?: IStackframe[]; + stacktrace?: Stackframe[]; subtype?: string; sync?: boolean; type: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts index 0fb3bb8080d82..3f71759390879 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -4,28 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kea } from 'kea'; +import { kea, MakeLogicType } from 'kea'; import { IInitialAppData } from '../../../common/types'; -import { IKeaLogic } from '../shared/types'; -export interface IAppLogicValues { +export interface IAppValues { hasInitialized: boolean; } -export interface IAppLogicActions { +export interface IAppActions { initializeAppData(props: IInitialAppData): void; } -export const AppLogic = kea({ - actions: (): IAppLogicActions => ({ +export const AppLogic = kea>({ + actions: { initializeAppData: (props) => props, - }), - reducers: () => ({ + }, + reducers: { hasInitialized: [ false, { initializeAppData: () => true, }, ], - }), -}) as IKeaLogic; + }, +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 234201a157ec9..6575e44f509eb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -11,8 +11,8 @@ import { useActions, useValues } from 'kea'; import { i18n } from '@kbn/i18n'; import { KibanaContext, IKibanaContext } from '../index'; -import { HttpLogic, IHttpLogicValues } from '../shared/http'; -import { AppLogic, IAppLogicActions, IAppLogicValues } from './app_logic'; +import { HttpLogic } from '../shared/http'; +import { AppLogic } from './app_logic'; import { IInitialAppData } from '../../../common/types'; import { APP_SEARCH_PLUGIN } from '../../../common/constants'; @@ -48,9 +48,9 @@ export const AppSearchUnconfigured: React.FC = () => ( ); export const AppSearchConfigured: React.FC = (props) => { - const { hasInitialized } = useValues(AppLogic) as IAppLogicValues; - const { initializeAppData } = useActions(AppLogic) as IAppLogicActions; - const { errorConnecting } = useValues(HttpLogic) as IHttpLogicValues; + const { hasInitialized } = useValues(AppLogic); + const { initializeAppData } = useActions(AppLogic); + const { errorConnecting } = useValues(HttpLogic); useEffect(() => { if (!hasInitialized) initializeAppData(props); diff --git a/x-pack/plugins/enterprise_search/public/applications/kea.d.ts b/x-pack/plugins/enterprise_search/public/applications/kea.d.ts deleted file mode 100644 index 961d93ccc12e6..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/kea.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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. - */ - -declare module 'kea' { - export function useValues(logic?: object): object; - export function useActions(logic?: object): object; - export function getContext(): { store: object }; - export function resetContext(context: object): object; - export function kea(logic: object): object; -} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx index 5a909a287795c..d184eb4dcd644 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import { useValues } from 'kea'; import { EuiCallOut, EuiCallOutProps, EuiSpacer } from '@elastic/eui'; -import { FlashMessagesLogic, IFlashMessagesValues } from './flash_messages_logic'; +import { FlashMessagesLogic } from './flash_messages_logic'; const FLASH_MESSAGE_TYPES = { success: { color: 'success' as EuiCallOutProps['color'], icon: 'check' }, @@ -18,7 +18,7 @@ const FLASH_MESSAGE_TYPES = { }; export const FlashMessages: React.FC = ({ children }) => { - const { messages } = useValues(FlashMessagesLogic) as IFlashMessagesValues; + const { messages } = useValues(FlashMessagesLogic); // If we have no messages to display, do not render the element at all if (!messages.length) return null; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts index 96c7817832c52..3ae48f352b2c1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts @@ -4,12 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kea } from 'kea'; +import { kea, MakeLogicType } from 'kea'; import { ReactNode } from 'react'; import { History } from 'history'; -import { IKeaLogic, TKeaReducers, IKeaParams } from '../types'; - export interface IFlashMessage { type: 'success' | 'info' | 'warning' | 'error'; message: ReactNode; @@ -22,27 +20,27 @@ export interface IFlashMessagesValues { historyListener: Function | null; } export interface IFlashMessagesActions { - setFlashMessages(messages: IFlashMessage | IFlashMessage[]): void; + setFlashMessages(messages: IFlashMessage | IFlashMessage[]): { messages: IFlashMessage[] }; clearFlashMessages(): void; - setQueuedMessages(messages: IFlashMessage | IFlashMessage[]): void; + setQueuedMessages(messages: IFlashMessage | IFlashMessage[]): { messages: IFlashMessage[] }; clearQueuedMessages(): void; - listenToHistory(history: History): void; - setHistoryListener(historyListener: Function): void; + listenToHistory(history: History): History; + setHistoryListener(historyListener: Function): { historyListener: Function }; } const convertToArray = (messages: IFlashMessage | IFlashMessage[]) => !Array.isArray(messages) ? [messages] : messages; -export const FlashMessagesLogic = kea({ - actions: (): IFlashMessagesActions => ({ +export const FlashMessagesLogic = kea>({ + actions: { setFlashMessages: (messages) => ({ messages: convertToArray(messages) }), clearFlashMessages: () => null, setQueuedMessages: (messages) => ({ messages: convertToArray(messages) }), clearQueuedMessages: () => null, listenToHistory: (history) => history, setHistoryListener: (historyListener) => ({ historyListener }), - }), - reducers: (): TKeaReducers => ({ + }, + reducers: { messages: [ [], { @@ -63,8 +61,8 @@ export const FlashMessagesLogic = kea({ setHistoryListener: (_, { historyListener }) => historyListener, }, ], - }), - listeners: ({ values, actions }): Partial => ({ + }, + listeners: ({ values, actions }) => ({ listenToHistory: (history) => { // On React Router navigation, clear previous flash messages and load any queued messages const unlisten = history.listen(() => { @@ -81,7 +79,4 @@ export const FlashMessagesLogic = kea({ if (removeHistoryListener) removeHistoryListener(); }, }), -} as IKeaParams) as IKeaLogic< - IFlashMessagesValues, - IFlashMessagesActions ->; +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx index 584124468a91f..a3ceabcf6ac8a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx @@ -8,19 +8,15 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; import { History } from 'history'; -import { - FlashMessagesLogic, - IFlashMessagesValues, - IFlashMessagesActions, -} from './flash_messages_logic'; +import { FlashMessagesLogic } from './flash_messages_logic'; interface IFlashMessagesProviderProps { history: History; } export const FlashMessagesProvider: React.FC = ({ history }) => { - const { historyListener } = useValues(FlashMessagesLogic) as IFlashMessagesValues; - const { listenToHistory } = useActions(FlashMessagesLogic) as IFlashMessagesActions; + const { historyListener } = useValues(FlashMessagesLogic); + const { listenToHistory } = useActions(FlashMessagesLogic); useEffect(() => { if (!historyListener) listenToHistory(history); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts index 7bf7a19ed451f..fb2d9b1061723 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts @@ -4,32 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kea } from 'kea'; +import { kea, MakeLogicType } from 'kea'; import { HttpSetup } from 'src/core/public'; -import { IKeaLogic, IKeaParams, TKeaReducers } from '../../shared/types'; - -export interface IHttpLogicValues { +export interface IHttpValues { http: HttpSetup; httpInterceptors: Function[]; errorConnecting: boolean; } -export interface IHttpLogicActions { - initializeHttp({ http, errorConnecting }: { http: HttpSetup; errorConnecting?: boolean }): void; +export interface IHttpActions { + initializeHttp({ + http, + errorConnecting, + }: { + http: HttpSetup; + errorConnecting?: boolean; + }): { http: HttpSetup; errorConnecting?: boolean }; initializeHttpInterceptors(): void; - setHttpInterceptors(httpInterceptors: Function[]): void; - setErrorConnecting(errorConnecting: boolean): void; + setHttpInterceptors(httpInterceptors: Function[]): { httpInterceptors: Function[] }; + setErrorConnecting(errorConnecting: boolean): { errorConnecting: boolean }; } -export const HttpLogic = kea({ - actions: (): IHttpLogicActions => ({ +export const HttpLogic = kea>({ + actions: { initializeHttp: ({ http, errorConnecting }) => ({ http, errorConnecting }), initializeHttpInterceptors: () => null, setHttpInterceptors: (httpInterceptors) => ({ httpInterceptors }), setErrorConnecting: (errorConnecting) => ({ errorConnecting }), - }), - reducers: (): TKeaReducers => ({ + }, + reducers: { http: [ (null as unknown) as HttpSetup, { @@ -49,7 +53,7 @@ export const HttpLogic = kea({ setErrorConnecting: (_, { errorConnecting }) => errorConnecting, }, ], - }), + }, listeners: ({ values, actions }) => ({ initializeHttpInterceptors: () => { const httpInterceptors = []; @@ -80,7 +84,4 @@ export const HttpLogic = kea({ }); }, }), -} as IKeaParams) as IKeaLogic< - IHttpLogicValues, - IHttpLogicActions ->; +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx index 6febc1869054f..4c2160195a1af 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx @@ -9,7 +9,7 @@ import { useActions } from 'kea'; import { HttpSetup } from 'src/core/public'; -import { HttpLogic, IHttpLogicActions } from './http_logic'; +import { HttpLogic } from './http_logic'; interface IHttpProviderProps { http: HttpSetup; @@ -17,7 +17,7 @@ interface IHttpProviderProps { } export const HttpProvider: React.FC = (props) => { - const { initializeHttp, initializeHttpInterceptors } = useActions(HttpLogic) as IHttpLogicActions; + const { initializeHttp, initializeHttpInterceptors } = useActions(HttpLogic); useEffect(() => { initializeHttp(props); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts index 449ff9d56debf..db65e80ca25c2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { HttpLogic, IHttpLogicValues, IHttpLogicActions } from './http_logic'; +export { HttpLogic, IHttpValues, IHttpActions } from './http_logic'; export { HttpProvider } from './http_provider'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index 561016d36921d..3fd1dcad0066e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -5,63 +5,3 @@ */ export { IFlashMessage } from './flash_messages'; - -export interface IKeaLogic { - mount(): Function; - values: IKeaValues; - actions: IKeaActions; -} - -/** - * This reusable interface mostly saves us a few characters / allows us to skip - * defining params inline. Unfortunately, the return values *do not work* as - * expected (hence the voids). While I can tell selectors to use TKeaSelectors, - * the return value is *not* properly type checked if it's not declared inline. :/ - * - * Also note that if you switch to Kea 2.1's plain object notation - - * `selectors: {}` vs. `selectors: () => ({})` - * - type checking also stops working and type errors become significantly less - * helpful - showing less specific error messages and highlighting. 👎 - */ -export interface IKeaParams { - selectors?(params: { selectors: IKeaValues }): void; - listeners?(params: { actions: IKeaActions; values: IKeaValues }): void; - events?(params: { actions: IKeaActions; values: IKeaValues }): void; -} - -/** - * This reducers() type checks that: - * - * 1. The value object keys are defined within IKeaValues - * 2. The default state (array[0]) matches the type definition within IKeaValues - * 3. The action object keys (array[1]) are defined within IKeaActions - * 3. The new state returned by the action matches the type definition within IKeaValues - */ -export type TKeaReducers = { - [Value in keyof IKeaValues]?: [ - IKeaValues[Value], - { - [Action in keyof IKeaActions]?: ( - state: IKeaValues[Value], - payload: IKeaValues - ) => IKeaValues[Value]; - } - ]; -}; - -/** - * This selectors() type checks that: - * - * 1. The object keys are defined within IKeaValues - * 2. The selected values are defined within IKeaValues - * 3. The returned value match the type definition within IKeaValues - * - * The unknown[] and any[] are unfortunately because I have no idea how to - * assert for arbitrary type/values as an array - */ -export type TKeaSelectors = { - [Value in keyof IKeaValues]?: [ - (selectors: IKeaValues) => unknown[], - (...args: any[]) => IKeaValues[Value] // eslint-disable-line @typescript-eslint/no-explicit-any - ]; -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts index b7116f02663c1..5bf2b41cfc264 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kea } from 'kea'; +import { kea, MakeLogicType } from 'kea'; import { IInitialAppData } from '../../../common/types'; import { IWorkplaceSearchInitialData } from '../../../common/types/workplace_search'; -import { IKeaLogic } from '../shared/types'; export interface IAppValues extends IWorkplaceSearchInitialData { hasInitialized: boolean; @@ -17,16 +16,16 @@ export interface IAppActions { initializeAppData(props: IInitialAppData): void; } -export const AppLogic = kea({ - actions: (): IAppActions => ({ +export const AppLogic = kea>({ + actions: { initializeAppData: ({ workplaceSearch }) => workplaceSearch, - }), - reducers: () => ({ + }, + reducers: { hasInitialized: [ false, { initializeAppData: () => true, }, ], - }), -}) as IKeaLogic; + }, +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index c0a51d5670a14..23e24e343937d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -10,8 +10,8 @@ import { useActions, useValues } from 'kea'; import { IInitialAppData } from '../../../common/types'; import { KibanaContext, IKibanaContext } from '../index'; -import { HttpLogic, IHttpLogicValues } from '../shared/http'; -import { AppLogic, IAppActions, IAppValues } from './app_logic'; +import { HttpLogic } from '../shared/http'; +import { AppLogic } from './app_logic'; import { Layout } from '../shared/layout'; import { WorkplaceSearchNav } from './components/layout/nav'; @@ -27,9 +27,9 @@ export const WorkplaceSearch: React.FC = (props) => { }; export const WorkplaceSearchConfigured: React.FC = (props) => { - const { hasInitialized } = useValues(AppLogic) as IAppValues; - const { initializeAppData } = useActions(AppLogic) as IAppActions; - const { errorConnecting } = useValues(HttpLogic) as IHttpLogicValues; + const { hasInitialized } = useValues(AppLogic); + const { initializeAppData } = useActions(AppLogic); + const { errorConnecting } = useValues(HttpLogic); useEffect(() => { if (!hasInitialized) initializeAppData(props); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/index.ts index e5169a51ce522..9e86993a5289d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { setMockValues, mockLogicValues, mockLogicActions } from './overview_logic.mock'; +export { setMockValues, mockValues, mockActions } from './overview_logic.mock'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts index 05715c648e5dc..9ce3021917a21 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts @@ -7,7 +7,7 @@ import { IOverviewValues } from '../overview_logic'; import { IAccount, IOrganization } from '../../../types'; -export const mockLogicValues = { +export const mockValues = { accountsCount: 0, activityFeed: [], canCreateContentSources: false, @@ -24,21 +24,21 @@ export const mockLogicValues = { dataLoading: true, } as IOverviewValues; -export const mockLogicActions = { +export const mockActions = { initializeOverview: jest.fn(() => ({})), }; jest.mock('kea', () => ({ ...(jest.requireActual('kea') as object), - useActions: jest.fn(() => ({ ...mockLogicActions })), - useValues: jest.fn(() => ({ ...mockLogicValues })), + useActions: jest.fn(() => ({ ...mockActions })), + useValues: jest.fn(() => ({ ...mockValues })), })); import { useValues } from 'kea'; export const setMockValues = (values: object) => { (useValues as jest.Mock).mockImplementationOnce(() => ({ - ...mockLogicValues, + ...mockValues, ...values, })); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx index fa4decccb34b1..5598123f1c286 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx @@ -28,7 +28,7 @@ import { ORG_SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes'; import { ContentSection } from '../../components/shared/content_section'; -import { OverviewLogic, IOverviewValues } from './overview_logic'; +import { OverviewLogic } from './overview_logic'; import { OnboardingCard } from './onboarding_card'; @@ -68,7 +68,7 @@ export const OnboardingSteps: React.FC = () => { fpAccount: { isCurated }, organization: { name, defaultOrgName }, isFederatedAuth, - } = useValues(OverviewLogic) as IOverviewValues; + } = useValues(OverviewLogic); const accountsPath = !isFederatedAuth && (canCreateInvitations || isCurated) ? USERS_PATH : undefined; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx index 53549cfcdbce7..4dc762e29deba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import { ContentSection } from '../../components/shared/content_section'; import { ORG_SOURCES_PATH, USERS_PATH } from '../../routes'; -import { OverviewLogic, IOverviewValues } from './overview_logic'; +import { OverviewLogic } from './overview_logic'; import { StatisticCard } from './statistic_card'; @@ -25,7 +25,7 @@ export const OrganizationStats: React.FC = () => { accountsCount, personalSourcesCount, isFederatedAuth, - } = useValues(OverviewLogic) as IOverviewValues; + } = useValues(OverviewLogic); return ( { it('calls initialize function', async () => { mount(); - expect(mockLogicActions.initializeOverview).toHaveBeenCalled(); + expect(mockActions.initializeOverview).toHaveBeenCalled(); }); it('renders onboarding state', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx index 134fc9389694d..dbc007c2aa97d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx @@ -14,7 +14,7 @@ import { useActions, useValues } from 'kea'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; -import { OverviewLogic, IOverviewActions, IOverviewValues } from './overview_logic'; +import { OverviewLogic } from './overview_logic'; import { Loading } from '../../components/shared/loading'; import { ProductButton } from '../../components/shared/product_button'; @@ -44,7 +44,7 @@ const HEADER_DESCRIPTION = i18n.translate( ); export const Overview: React.FC = () => { - const { initializeOverview } = useActions(OverviewLogic) as IOverviewActions; + const { initializeOverview } = useActions(OverviewLogic); const { dataLoading, @@ -52,7 +52,7 @@ export const Overview: React.FC = () => { hasOrgSources, isOldAccount, organization: { name: orgName, defaultOrgName }, - } = useValues(OverviewLogic) as IOverviewValues; + } = useValues(OverviewLogic); useEffect(() => { initializeOverview(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts index 61108d7cb1f2f..6989635064ca9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts @@ -9,7 +9,7 @@ import { resetContext } from 'kea'; jest.mock('../../../shared/http', () => ({ HttpLogic: { values: { http: { get: jest.fn() } } } })); import { HttpLogic } from '../../../shared/http'; -import { mockLogicValues } from './__mocks__'; +import { mockValues } from './__mocks__'; import { OverviewLogic } from './overview_logic'; describe('OverviewLogic', () => { @@ -20,7 +20,7 @@ describe('OverviewLogic', () => { }); it('has expected default values', () => { - expect(OverviewLogic.values).toEqual(mockLogicValues); + expect(OverviewLogic.values).toEqual(mockValues); }); describe('setServerData', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts index 6606e5b55cb33..2c6846b6db7db 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kea } from 'kea'; +import { kea, MakeLogicType } from 'kea'; import { HttpLogic } from '../../../shared/http'; import { IAccount, IOrganization } from '../../types'; -import { IKeaLogic, TKeaReducers, IKeaParams } from '../../../shared/types'; import { IFeedActivity } from './recent_activity'; @@ -29,7 +28,7 @@ export interface IOverviewServerData { } export interface IOverviewActions { - setServerData(serverData: IOverviewServerData): void; + setServerData(serverData: IOverviewServerData): IOverviewServerData; initializeOverview(): void; } @@ -37,12 +36,12 @@ export interface IOverviewValues extends IOverviewServerData { dataLoading: boolean; } -export const OverviewLogic = kea({ - actions: (): IOverviewActions => ({ +export const OverviewLogic = kea>({ + actions: { setServerData: (serverData) => serverData, initializeOverview: () => null, - }), - reducers: (): TKeaReducers => ({ + }, + reducers: { organization: [ {} as IOrganization, { @@ -127,11 +126,11 @@ export const OverviewLogic = kea({ setServerData: () => false, }, ], - }), - listeners: ({ actions }): Partial => ({ + }, + listeners: ({ actions }) => ({ initializeOverview: async () => { const response = await HttpLogic.values.http.get('/api/workplace_search/overview'); actions.setServerData(response); }, }), -} as IKeaParams) as IKeaLogic; +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx index ada89c33be7e2..3c476be8d10e6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx @@ -17,7 +17,7 @@ import { sendTelemetry } from '../../../shared/telemetry'; import { KibanaContext, IKibanaContext } from '../../../index'; import { getSourcePath } from '../../routes'; -import { OverviewLogic, IOverviewValues } from './overview_logic'; +import { OverviewLogic } from './overview_logic'; import './recent_activity.scss'; @@ -33,7 +33,7 @@ export const RecentActivity: React.FC = () => { const { organization: { name, defaultOrgName }, activityFeed, - } = useValues(OverviewLogic) as IOverviewValues; + } = useValues(OverviewLogic); return ( { +interface Props { rolloverEnabled: boolean; errors?: PhaseValidationErrors; phase: keyof Phases & string; @@ -72,7 +77,7 @@ interface Props { isShowingErrors: boolean; } -export const MinAgeInput = ({ +export const MinAgeInput = ({ rolloverEnabled, errors, phaseData, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx index 0ce2c0d7ea566..b4ff62bfb03dc 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx @@ -20,7 +20,7 @@ import { LearnMoreLink } from './learn_more_link'; import { ErrableFormRow } from './form_errors'; import { useLoadNodes } from '../../../services/api'; import { NodeAttrsDetails } from './node_attrs_details'; -import { ColdPhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; +import { PhaseWithAllocationAction, Phases } from '../../../services/policies/types'; import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; const learnMoreLink = ( @@ -38,14 +38,14 @@ const learnMoreLink = ( ); -interface Props { +interface Props { phase: keyof Phases & string; errors?: PhaseValidationErrors; phaseData: T; setPhaseData: (dataKey: keyof T & string, value: string) => void; isShowingErrors: boolean; } -export const NodeAllocation = ({ +export const NodeAllocation = ({ phase, setPhaseData, errors, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx index 1da7508049f24..1505532a2b16e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx @@ -10,17 +10,17 @@ import { EuiFieldNumber, EuiTextColor, EuiDescribedFormGroup } from '@elastic/eu import { LearnMoreLink } from './'; import { OptionalLabel } from './'; import { ErrableFormRow } from './'; -import { ColdPhase, HotPhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; +import { PhaseWithIndexPriority, Phases } from '../../../services/policies/types'; import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; -interface Props { +interface Props { errors?: PhaseValidationErrors; phase: keyof Phases & string; phaseData: T; setPhaseData: (dataKey: keyof T & string, value: any) => void; isShowingErrors: boolean; } -export const SetPriorityInput = ({ +export const SetPriorityInput = ({ errors, phaseData, phase, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index c99d01b546679..db58c64a8ae8c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -28,7 +28,7 @@ import { import { toasts } from '../../services/notification'; -import { Policy, PolicyFromES } from '../../services/policies/types'; +import { Phases, Policy, PolicyFromES } from '../../services/policies/types'; import { validatePolicy, ValidationErrors, @@ -42,7 +42,7 @@ import { } from '../../services/policies/policy_serialization'; import { ErrableFormRow, LearnMoreLink, PolicyJsonFlyout } from './components'; -import { ColdPhase, DeletePhase, HotPhase, WarmPhase } from './phases'; +import { ColdPhase, DeletePhase, FrozenPhase, HotPhase, WarmPhase } from './phases'; interface Props { policies: PolicyFromES[]; @@ -118,7 +118,7 @@ export const EditPolicy: React.FunctionComponent = ({ setIsShowingPolicyJsonFlyout(!isShowingPolicyJsonFlyout); }; - const setPhaseData = (phase: 'hot' | 'warm' | 'cold' | 'delete', key: string, value: any) => { + const setPhaseData = (phase: keyof Phases, key: string, value: any) => { setPolicy({ ...policy, phases: { @@ -303,6 +303,16 @@ export const EditPolicy: React.FunctionComponent = ({ + 0} + setPhaseData={(key, value) => setPhaseData('frozen', key, value)} + phaseData={policy.phases.frozen} + hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} + /> + + + 0} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx index fb32752fe24ea..9df6da7a88b2f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/eui'; import { ColdPhase as ColdPhaseInterface, Phases } from '../../../services/policies/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; import { LearnMoreLink, @@ -36,9 +36,8 @@ const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeInd defaultMessage: 'Freeze index', }); -const coldProperty = propertyof('cold'); -const phaseProperty = (propertyName: keyof ColdPhaseInterface) => - propertyof(propertyName); +const coldProperty: keyof Phases = 'cold'; +const phaseProperty = (propertyName: keyof ColdPhaseInterface) => propertyName; interface Props { setPhaseData: (key: keyof ColdPhaseInterface & string, value: string | boolean) => void; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx index d3c73090f25f2..eab93777a72bd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui'; import { DeletePhase as DeletePhaseInterface, Phases } from '../../../services/policies/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; import { ActiveBadge, @@ -20,9 +20,8 @@ import { SnapshotPolicies, } from '../components'; -const deleteProperty = propertyof('delete'); -const phaseProperty = (propertyName: keyof DeletePhaseInterface) => - propertyof(propertyName); +const deleteProperty: keyof Phases = 'delete'; +const phaseProperty = (propertyName: keyof DeletePhaseInterface) => propertyName; interface Props { setPhaseData: (key: keyof DeletePhaseInterface & string, value: string | boolean) => void; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx new file mode 100644 index 0000000000000..782906a56a9ba --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx @@ -0,0 +1,210 @@ +/* + * 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 React, { PureComponent, Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiFieldNumber, + EuiDescribedFormGroup, + EuiSwitch, + EuiTextColor, +} from '@elastic/eui'; + +import { FrozenPhase as FrozenPhaseInterface, Phases } from '../../../services/policies/types'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; + +import { + LearnMoreLink, + ActiveBadge, + PhaseErrorMessage, + OptionalLabel, + ErrableFormRow, + MinAgeInput, + NodeAllocation, + SetPriorityInput, +} from '../components'; + +const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.frozenPhase.freezeIndexLabel', { + defaultMessage: 'Freeze index', +}); + +const frozenProperty: keyof Phases = 'frozen'; +const phaseProperty = (propertyName: keyof FrozenPhaseInterface) => propertyName; + +interface Props { + setPhaseData: (key: keyof FrozenPhaseInterface & string, value: string | boolean) => void; + phaseData: FrozenPhaseInterface; + isShowingErrors: boolean; + errors?: PhaseValidationErrors; + hotPhaseRolloverEnabled: boolean; +} +export class FrozenPhase extends PureComponent { + render() { + const { + setPhaseData, + phaseData, + errors, + isShowingErrors, + hotPhaseRolloverEnabled, + } = this.props; + + return ( +
+ +

+ +

{' '} + {phaseData.phaseEnabled && !isShowingErrors ? : null} + +
+ } + titleSize="s" + description={ + +

+ +

+ + } + id={`${frozenProperty}-${phaseProperty('phaseEnabled')}`} + checked={phaseData.phaseEnabled} + onChange={(e) => { + setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); + }} + aria-controls="frozenPhaseContent" + /> +
+ } + fullWidth + > + + {phaseData.phaseEnabled ? ( + + + errors={errors} + phaseData={phaseData} + phase={frozenProperty} + isShowingErrors={isShowingErrors} + setPhaseData={setPhaseData} + rolloverEnabled={hotPhaseRolloverEnabled} + /> + + + + phase={frozenProperty} + setPhaseData={setPhaseData} + errors={errors} + phaseData={phaseData} + isShowingErrors={isShowingErrors} + /> + + + + + + + + } + isShowingErrors={isShowingErrors} + errors={errors?.freezeEnabled} + helpText={i18n.translate( + 'xpack.indexLifecycleMgmt.frozenPhase.replicaCountHelpText', + { + defaultMessage: 'By default, the number of replicas remains the same.', + } + )} + > + { + setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value); + }} + min={0} + /> + + + + + ) : ( +
+ )} + + + {phaseData.phaseEnabled ? ( + + + + + } + description={ + + {' '} + + + } + fullWidth + titleSize="xs" + > + { + setPhaseData(phaseProperty('freezeEnabled'), e.target.checked); + }} + label={freezeLabel} + aria-label={freezeLabel} + /> + + + errors={errors} + phaseData={phaseData} + phase={frozenProperty} + isShowingErrors={isShowingErrors} + setPhaseData={setPhaseData} + /> + + ) : null} +
+ ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx index 22f0114d16afe..106e3b9139a9b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/eui'; import { HotPhase as HotPhaseInterface, Phases } from '../../../services/policies/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; import { LearnMoreLink, @@ -112,9 +112,8 @@ const maxAgeUnits = [ }), }, ]; -const hotProperty = propertyof('hot'); -const phaseProperty = (propertyName: keyof HotPhaseInterface) => - propertyof(propertyName); +const hotProperty: keyof Phases = 'hot'; +const phaseProperty = (propertyName: keyof HotPhaseInterface) => propertyName; interface Props { errors?: PhaseValidationErrors; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts index 8d1ace5950497..d59f2ff6413fd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts @@ -7,4 +7,5 @@ export { HotPhase } from './hot_phase'; export { WarmPhase } from './warm_phase'; export { ColdPhase } from './cold_phase'; +export { FrozenPhase } from './frozen_phase'; export { DeletePhase } from './delete_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx index f7b8c60a5c71f..2733d01ac222d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx @@ -30,7 +30,7 @@ import { } from '../components'; import { Phases, WarmPhase as WarmPhaseInterface } from '../../../services/policies/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; const shrinkLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { defaultMessage: 'Shrink index', @@ -47,9 +47,8 @@ const forcemergeLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.force defaultMessage: 'Force merge data', }); -const warmProperty = propertyof('warm'); -const phaseProperty = (propertyName: keyof WarmPhaseInterface) => - propertyof(propertyName); +const warmProperty: keyof Phases = 'warm'; +const phaseProperty = (propertyName: keyof WarmPhaseInterface) => propertyName; interface Props { setPhaseData: (key: keyof WarmPhaseInterface & string, value: boolean | string) => void; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts index 6cc43042ed4ff..7fa82a004b872 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts @@ -152,9 +152,9 @@ export const validateColdPhase = (phase: ColdPhase): PhaseValidationErrors { + const phase = { ...frozenPhaseInitialization }; + if (phaseSerialized === undefined || phaseSerialized === null) { + return phase; + } + + phase.phaseEnabled = true; + + if (phaseSerialized.min_age) { + const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); + phase.selectedMinimumAge = minAge; + phase.selectedMinimumAgeUnits = minAgeUnits; + } + + if (phaseSerialized.actions) { + const actions = phaseSerialized.actions; + if (actions.allocate) { + const allocate = actions.allocate; + if (allocate.require) { + Object.entries(allocate.require).forEach((entry) => { + phase.selectedNodeAttrs = entry.join(':'); + }); + if (allocate.number_of_replicas) { + phase.selectedReplicaCount = allocate.number_of_replicas.toString(); + } + } + } + + if (actions.freeze) { + phase.freezeEnabled = true; + } + + if (actions.set_priority) { + phase.phaseIndexPriority = actions.set_priority.priority + ? actions.set_priority.priority.toString() + : ''; + } + } + + return phase; +}; + +export const frozenPhaseToES = ( + phase: FrozenPhase, + originalPhase?: SerializedFrozenPhase +): SerializedFrozenPhase => { + if (!originalPhase) { + originalPhase = { ...serializedPhaseInitialization }; + } + + const esPhase = { ...originalPhase }; + + if (isNumber(phase.selectedMinimumAge)) { + esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; + } + + esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; + + if (phase.selectedNodeAttrs) { + const [name, value] = phase.selectedNodeAttrs.split(':'); + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.require = { + [name]: value, + }; + } else { + if (esPhase.actions.allocate) { + // @ts-expect-error + delete esPhase.actions.allocate.require; + } + } + + if (isNumber(phase.selectedReplicaCount)) { + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.number_of_replicas = parseInt(phase.selectedReplicaCount, 10); + } else { + if (esPhase.actions.allocate) { + // @ts-expect-error + delete esPhase.actions.allocate.number_of_replicas; + } + } + + if ( + esPhase.actions.allocate && + !esPhase.actions.allocate.require && + !isNumber(esPhase.actions.allocate.number_of_replicas) && + isEmpty(esPhase.actions.allocate.include) && + isEmpty(esPhase.actions.allocate.exclude) + ) { + // remove allocate action if it does not define require or number of nodes + // and both include and exclude are empty objects (ES will fail to parse if we don't) + delete esPhase.actions.allocate; + } + + if (phase.freezeEnabled) { + esPhase.actions.freeze = {}; + } else { + delete esPhase.actions.freeze; + } + + if (isNumber(phase.phaseIndexPriority)) { + esPhase.actions.set_priority = { + priority: parseInt(phase.phaseIndexPriority, 10), + }; + } else { + delete esPhase.actions.set_priority; + } + + return esPhase; +}; + +export const validateFrozenPhase = (phase: FrozenPhase): PhaseValidationErrors => { + if (!phase.phaseEnabled) { + return {}; + } + + const phaseErrors = {} as PhaseValidationErrors; + + // index priority is optional, but if it's set, it needs to be a positive number + if (phase.phaseIndexPriority) { + if (!isNumber(phase.phaseIndexPriority)) { + phaseErrors.phaseIndexPriority = [numberRequiredMessage]; + } else if (parseInt(phase.phaseIndexPriority, 10) < 0) { + phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage]; + } + } + + // min age needs to be a positive number + if (!isNumber(phase.selectedMinimumAge)) { + phaseErrors.selectedMinimumAge = [numberRequiredMessage]; + } else if (parseInt(phase.selectedMinimumAge, 10) < 0) { + phaseErrors.selectedMinimumAge = [positiveNumberRequiredMessage]; + } + + return { ...phaseErrors }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts index 3953521df1817..807a6fe8ec395 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts @@ -9,6 +9,7 @@ import { defaultNewDeletePhase, defaultNewHotPhase, defaultNewWarmPhase, + defaultNewFrozenPhase, serializedPhaseInitialization, } from '../../constants'; @@ -17,6 +18,7 @@ import { Policy, PolicyFromES, SerializedPolicy } from './types'; import { hotPhaseFromES, hotPhaseToES } from './hot_phase'; import { warmPhaseFromES, warmPhaseToES } from './warm_phase'; import { coldPhaseFromES, coldPhaseToES } from './cold_phase'; +import { frozenPhaseFromES, frozenPhaseToES } from './frozen_phase'; import { deletePhaseFromES, deletePhaseToES } from './delete_phase'; export const splitSizeAndUnits = (field: string): { size: string; units: string } => { @@ -53,6 +55,7 @@ export const initializeNewPolicy = (newPolicyName: string = ''): Policy => { hot: { ...defaultNewHotPhase }, warm: { ...defaultNewWarmPhase }, cold: { ...defaultNewColdPhase }, + frozen: { ...defaultNewFrozenPhase }, delete: { ...defaultNewDeletePhase }, }, }; @@ -70,6 +73,7 @@ export const deserializePolicy = (policy: PolicyFromES): Policy => { hot: hotPhaseFromES(phases.hot), warm: warmPhaseFromES(phases.warm), cold: coldPhaseFromES(phases.cold), + frozen: frozenPhaseFromES(phases.frozen), delete: deletePhaseFromES(phases.delete), }, }; @@ -94,6 +98,13 @@ export const serializePolicy = ( serializedPolicy.phases.cold = coldPhaseToES(policy.phases.cold, originalEsPolicy.phases.cold); } + if (policy.phases.frozen.phaseEnabled) { + serializedPolicy.phases.frozen = frozenPhaseToES( + policy.phases.frozen, + originalEsPolicy.phases.frozen + ); + } + if (policy.phases.delete.phaseEnabled) { serializedPolicy.phases.delete = deletePhaseToES( policy.phases.delete, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts index 545488be2cd5e..6fdbc4babd3f3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts @@ -9,7 +9,17 @@ import { validateHotPhase } from './hot_phase'; import { validateWarmPhase } from './warm_phase'; import { validateColdPhase } from './cold_phase'; import { validateDeletePhase } from './delete_phase'; -import { ColdPhase, DeletePhase, HotPhase, Phase, Policy, PolicyFromES, WarmPhase } from './types'; +import { validateFrozenPhase } from './frozen_phase'; + +import { + ColdPhase, + DeletePhase, + FrozenPhase, + HotPhase, + Policy, + PolicyFromES, + WarmPhase, +} from './types'; export const propertyof = (propertyName: keyof T & string) => propertyName; @@ -100,7 +110,7 @@ export const policyNameAlreadyUsedErrorMessage = i18n.translate( defaultMessage: 'That policy name is already used.', } ); -export type PhaseValidationErrors = { +export type PhaseValidationErrors = { [P in keyof Partial]: string[]; }; @@ -108,6 +118,7 @@ export interface ValidationErrors { hot: PhaseValidationErrors; warm: PhaseValidationErrors; cold: PhaseValidationErrors; + frozen: PhaseValidationErrors; delete: PhaseValidationErrors; policyName: string[]; } @@ -148,12 +159,14 @@ export const validatePolicy = ( const hotPhaseErrors = validateHotPhase(policy.phases.hot); const warmPhaseErrors = validateWarmPhase(policy.phases.warm); const coldPhaseErrors = validateColdPhase(policy.phases.cold); + const frozenPhaseErrors = validateFrozenPhase(policy.phases.frozen); const deletePhaseErrors = validateDeletePhase(policy.phases.delete); const isValid = policyNameErrors.length === 0 && Object.keys(hotPhaseErrors).length === 0 && Object.keys(warmPhaseErrors).length === 0 && Object.keys(coldPhaseErrors).length === 0 && + Object.keys(frozenPhaseErrors).length === 0 && Object.keys(deletePhaseErrors).length === 0; return [ isValid, @@ -162,6 +175,7 @@ export const validatePolicy = ( hot: hotPhaseErrors, warm: warmPhaseErrors, cold: coldPhaseErrors, + frozen: frozenPhaseErrors, delete: deletePhaseErrors, }, ]; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts index 2e2ed5b38bb87..3d4c73cf4a82c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts @@ -13,6 +13,7 @@ export interface Phases { hot?: SerializedHotPhase; warm?: SerializedWarmPhase; cold?: SerializedColdPhase; + frozen?: SerializedFrozenPhase; delete?: SerializedDeletePhase; } @@ -68,6 +69,16 @@ export interface SerializedColdPhase extends SerializedPhase { }; } +export interface SerializedFrozenPhase extends SerializedPhase { + actions: { + freeze?: {}; + allocate?: AllocateAction; + set_priority?: { + priority: number | null; + }; + }; +} + export interface SerializedDeletePhase extends SerializedPhase { actions: { wait_for_snapshot?: { @@ -94,47 +105,66 @@ export interface Policy { hot: HotPhase; warm: WarmPhase; cold: ColdPhase; + frozen: FrozenPhase; delete: DeletePhase; }; } -export interface Phase { +export interface CommonPhaseSettings { phaseEnabled: boolean; } -export interface HotPhase extends Phase { + +export interface PhaseWithMinAge { + selectedMinimumAge: string; + selectedMinimumAgeUnits: string; +} + +export interface PhaseWithAllocationAction { + selectedNodeAttrs: string; + selectedReplicaCount: string; +} + +export interface PhaseWithIndexPriority { + phaseIndexPriority: string; +} + +export interface HotPhase extends CommonPhaseSettings, PhaseWithIndexPriority { rolloverEnabled: boolean; selectedMaxSizeStored: string; selectedMaxSizeStoredUnits: string; selectedMaxDocuments: string; selectedMaxAge: string; selectedMaxAgeUnits: string; - phaseIndexPriority: string; } -export interface WarmPhase extends Phase { +export interface WarmPhase + extends CommonPhaseSettings, + PhaseWithMinAge, + PhaseWithAllocationAction, + PhaseWithIndexPriority { warmPhaseOnRollover: boolean; - selectedMinimumAge: string; - selectedMinimumAgeUnits: string; - selectedNodeAttrs: string; - selectedReplicaCount: string; shrinkEnabled: boolean; selectedPrimaryShardCount: string; forceMergeEnabled: boolean; selectedForceMergeSegments: string; - phaseIndexPriority: string; } -export interface ColdPhase extends Phase { - selectedMinimumAge: string; - selectedMinimumAgeUnits: string; - selectedNodeAttrs: string; - selectedReplicaCount: string; +export interface ColdPhase + extends CommonPhaseSettings, + PhaseWithMinAge, + PhaseWithAllocationAction, + PhaseWithIndexPriority { freezeEnabled: boolean; - phaseIndexPriority: string; } -export interface DeletePhase extends Phase { - selectedMinimumAge: string; - selectedMinimumAgeUnits: string; +export interface FrozenPhase + extends CommonPhaseSettings, + PhaseWithMinAge, + PhaseWithAllocationAction, + PhaseWithIndexPriority { + freezeEnabled: boolean; +} + +export interface DeletePhase extends CommonPhaseSettings, PhaseWithMinAge { waitForSnapshotPolicy: string; } diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js index a1eac5264bb6a..8d01f4a4c200e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js @@ -176,6 +176,12 @@ export const ilmFilterExtension = (indices) => { defaultMessage: 'Warm', }), }, + { + value: 'frozen', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.frozenLabel', { + defaultMessage: 'Frozen', + }), + }, { value: 'cold', view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.coldLabel', { diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts index 2d02802119e47..9b51164fd4c28 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts @@ -104,6 +104,23 @@ const coldPhaseSchema = schema.maybe( }) ); +const frozenPhaseSchema = schema.maybe( + schema.object({ + min_age: minAgeSchema, + actions: schema.object({ + set_priority: setPrioritySchema, + unfollow: unfollowSchema, + allocate: allocateSchema, + freeze: schema.maybe(schema.object({})), // Freeze has no options + searchable_snapshot: schema.maybe( + schema.object({ + snapshot_repository: schema.string(), + }) + ), + }), + }) +); + const deletePhaseSchema = schema.maybe( schema.object({ min_age: minAgeSchema, @@ -129,6 +146,7 @@ const bodySchema = schema.object({ hot: hotPhaseSchema, warm: warmPhaseSchema, cold: coldPhaseSchema, + frozen: frozenPhaseSchema, delete: deletePhaseSchema, }), }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/constants.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/constants.ts new file mode 100644 index 0000000000000..e75ba56277c5c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/constants.ts @@ -0,0 +1,75 @@ +/* + * 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 { Pipeline } from '../../../../../common/types'; +import { VerboseTestOutput, Document } from '../types'; + +export const PROCESSORS: Pick = { + processors: [ + { + set: { + field: 'field1', + value: 'value1', + }, + }, + ], +}; + +export const DOCUMENTS: Document[] = [ + { + _index: 'index', + _id: 'id1', + _source: { + name: 'foo', + }, + }, + { + _index: 'index', + _id: 'id2', + _source: { + name: 'bar', + }, + }, +]; + +export const SIMULATE_RESPONSE: VerboseTestOutput = { + docs: [ + { + processor_results: [ + { + processor_type: 'set', + status: 'success', + tag: 'some_tag', + doc: { + _index: 'index', + _id: 'id1', + _source: { + name: 'foo', + foo: 'bar', + }, + }, + }, + ], + }, + { + processor_results: [ + { + processor_type: 'set', + status: 'success', + tag: 'some_tag', + doc: { + _index: 'index', + _id: 'id2', + _source: { + name: 'bar', + foo: 'bar', + }, + }, + }, + ], + }, + ], +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/http_requests.helpers.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/http_requests.helpers.ts new file mode 100644 index 0000000000000..541a6853a99b3 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/http_requests.helpers.ts @@ -0,0 +1,45 @@ +/* + * 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 sinon, { SinonFakeServer } from 'sinon'; + +type HttpResponse = Record | any[]; + +// Register helpers to mock HTTP Requests +const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { + const setSimulatePipelineResponse = (response?: HttpResponse, error?: any) => { + const status = error ? error.status || 400 : 200; + const body = error ? JSON.stringify(error.body) : JSON.stringify(response); + + server.respondWith('POST', '/api/ingest_pipelines/simulate', [ + status, + { 'Content-Type': 'application/json' }, + body, + ]); + }; + + return { + setSimulatePipelineResponse, + }; +}; + +export const initHttpRequests = () => { + const server = sinon.fakeServer.create(); + + server.respondImmediately = true; + + // Define default response for unhandled requests. + // We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry, + // and we can mock them all with a 200 instead of mocking each one individually. + server.respondWith([200, {}, 'DefaultSinonMockServerResponse']); + + const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server); + + return { + server, + httpRequestsMockHelpers, + }; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.helpers.tsx new file mode 100644 index 0000000000000..fec3259fa019b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.helpers.tsx @@ -0,0 +1,231 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import React from 'react'; +import axios from 'axios'; +import axiosXhrAdapter from 'axios/lib/adapters/xhr'; + +import { notificationServiceMock, scopedHistoryMock } from 'src/core/public/mocks'; + +import { LocationDescriptorObject } from 'history'; +import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; +/* eslint-disable @kbn/eslint/no-restricted-paths */ +import { usageCollectionPluginMock } from 'src/plugins/usage_collection/public/mocks'; + +import { registerTestBed, TestBed } from '../../../../../../../test_utils'; +import { stubWebWorker } from '../../../../../../../test_utils/stub_web_worker'; + +import { + breadcrumbService, + uiMetricService, + documentationService, + apiService, +} from '../../../services'; + +import { + ProcessorsEditorContextProvider, + Props, + GlobalOnFailureProcessorsEditor, + ProcessorsEditor, +} from '../'; +import { TestPipelineActions } from '../'; + +import { initHttpRequests } from './http_requests.helpers'; + +stubWebWorker(); + +jest.mock('../../../../../../../../src/plugins/kibana_react/public', () => { + const original = jest.requireActual('../../../../../../../../src/plugins/kibana_react/public'); + return { + ...original, + // Mocking CodeEditor, which uses React Monaco under the hood + CodeEditor: (props: any) => ( + { + props.onChange(e.jsonContent); + }} + /> + ), + }; +}); + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...original, + // Mocking EuiCodeEditor, which uses React Ace under the hood + EuiCodeEditor: (props: any) => ( + { + props.onChange(syntheticEvent.jsonString); + }} + /> + ), + }; +}); + +jest.mock('react-virtualized', () => { + const original = jest.requireActual('react-virtualized'); + + return { + ...original, + AutoSizer: ({ children }: { children: any }) => ( +
{children({ height: 500, width: 500 })}
+ ), + }; +}); + +const history = scopedHistoryMock.create(); +history.createHref.mockImplementation((location: LocationDescriptorObject) => { + return `${location.pathname}?${location.search}`; +}); + +const appServices = { + breadcrumbs: breadcrumbService, + metric: uiMetricService, + documentation: documentationService, + api: apiService, + notifications: notificationServiceMock.createSetupContract(), + history, + uiSettings: {}, +}; + +const testBedSetup = registerTestBed( + (props: Props) => ( + + + + + + + + ), + { + doMountAsync: false, + } +); + +export interface SetupResult extends TestBed { + actions: ReturnType; +} + +const createActions = (testBed: TestBed) => { + const { find, component, form } = testBed; + + return { + clickAddDocumentsButton() { + act(() => { + find('addDocumentsButton').simulate('click'); + }); + component.update(); + }, + + async clickViewOutputButton() { + await act(async () => { + find('viewOutputButton').simulate('click'); + }); + component.update(); + }, + + closeTestPipelineFlyout() { + act(() => { + find('euiFlyoutCloseButton').simulate('click'); + }); + component.update(); + }, + + clickProcessorOutputTab() { + act(() => { + find('outputTab').simulate('click'); + }); + component.update(); + }, + + async clickRefreshOutputButton() { + await act(async () => { + find('refreshOutputButton').simulate('click'); + }); + component.update(); + }, + + async clickRunPipelineButton() { + await act(async () => { + find('runPipelineButton').simulate('click'); + }); + component.update(); + }, + + async toggleVerboseSwitch() { + await act(async () => { + form.toggleEuiSwitch('verboseOutputToggle'); + }); + component.update(); + }, + + addDocumentsJson(jsonString: string) { + find('documentsEditor').simulate('change', { + jsonString, + }); + }, + + async clickProcessor(processorSelector: string) { + await act(async () => { + find(`${processorSelector}.manageItemButton`).simulate('click'); + }); + component.update(); + }, + }; +}; + +export const setup = async (props: Props): Promise => { + const testBed = await testBedSetup(props); + return { + ...testBed, + actions: createActions(testBed), + }; +}; + +const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); + +export const setupEnvironment = () => { + // Initialize mock services + uiMetricService.setup(usageCollectionPluginMock.createSetupContract()); + // @ts-ignore + apiService.setup(mockHttpClient, uiMetricService); + + const { server, httpRequestsMockHelpers } = initHttpRequests(); + + return { + server, + httpRequestsMockHelpers, + }; +}; + +type TestSubject = + | 'addDocumentsButton' + | 'testPipelineFlyout' + | 'documentsDropdown' + | 'outputTab' + | 'documentsEditor' + | 'runPipelineButton' + | 'documentsTabContent' + | 'outputTabContent' + | 'verboseOutputToggle' + | 'refreshOutputButton' + | 'viewOutputButton' + | 'pipelineExecutionError' + | 'euiFlyoutCloseButton' + | 'processorStatusIcon' + | 'documentsTab' + | 'manageItemButton' + | 'processorSettingsForm' + | 'configurationTab' + | 'outputTab' + | 'processorOutputTabContent' + | string; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.test.tsx new file mode 100644 index 0000000000000..339c840bb86f1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.test.tsx @@ -0,0 +1,240 @@ +/* + * 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 { Pipeline } from '../../../../../common/types'; + +import { VerboseTestOutput, Document } from '../types'; +import { setup, SetupResult, setupEnvironment } from './test_pipeline.helpers'; +import { DOCUMENTS, SIMULATE_RESPONSE, PROCESSORS } from './constants'; + +interface ReqBody { + documents: Document[]; + verbose?: boolean; + pipeline: Pick; +} + +describe('Test pipeline', () => { + let onUpdate: jest.Mock; + let testBed: SetupResult; + + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + server.restore(); + jest.useRealTimers(); + }); + + beforeEach(async () => { + onUpdate = jest.fn(); + testBed = await setup({ + value: { + ...PROCESSORS, + }, + onFlyoutOpen: jest.fn(), + onUpdate, + }); + }); + + describe('Test pipeline actions', () => { + it('should successfully add sample documents and execute the pipeline', async () => { + const { find, actions, exists } = testBed; + + httpRequestsMockHelpers.setSimulatePipelineResponse(SIMULATE_RESPONSE); + + // Flyout and document dropdown should not be visible + expect(exists('testPipelineFlyout')).toBe(false); + expect(exists('documentsDropdown')).toBe(false); + + // Open flyout + actions.clickAddDocumentsButton(); + + // Flyout should be visible with output tab initially disabled + expect(exists('testPipelineFlyout')).toBe(true); + expect(exists('documentsTabContent')).toBe(true); + expect(exists('outputTabContent')).toBe(false); + expect(find('outputTab').props().disabled).toEqual(true); + + // Add sample documents and click run + actions.addDocumentsJson(JSON.stringify(DOCUMENTS)); + await actions.clickRunPipelineButton(); + + // Verify request + const latestRequest = server.requests[server.requests.length - 1]; + const requestBody: ReqBody = JSON.parse(JSON.parse(latestRequest.requestBody).body); + const { + documents: reqDocuments, + verbose: reqVerbose, + pipeline: { processors: reqProcessors }, + } = requestBody; + + expect(reqDocuments).toEqual(DOCUMENTS); + expect(reqVerbose).toEqual(true); + + // We programatically add a unique tag field when calling the simulate API + // We do not know this value in the test, so we simply check that the field exists + // and only verify the processor configuration + reqProcessors.forEach((processor, index) => { + Object.entries(processor).forEach(([key, value]) => { + const { tag, ...config } = value; + expect(tag).toBeDefined(); + expect(config).toEqual(PROCESSORS.processors[index][key]); + }); + }); + + // Verify output tab is active + expect(find('outputTab').props().disabled).toEqual(false); + expect(exists('documentsTabContent')).toBe(false); + expect(exists('outputTabContent')).toBe(true); + + // Click reload button and verify request + const totalRequests = server.requests.length; + await actions.clickRefreshOutputButton(); + expect(server.requests.length).toBe(totalRequests + 1); + expect(server.requests[server.requests.length - 1].url).toBe( + '/api/ingest_pipelines/simulate' + ); + + // Click verbose toggle and verify request + await actions.toggleVerboseSwitch(); + expect(server.requests.length).toBe(totalRequests + 2); + expect(server.requests[server.requests.length - 1].url).toBe( + '/api/ingest_pipelines/simulate' + ); + }); + + test('should enable the output tab if cached documents exist', async () => { + const { actions, exists } = testBed; + + httpRequestsMockHelpers.setSimulatePipelineResponse(SIMULATE_RESPONSE); + + // Open flyout + actions.clickAddDocumentsButton(); + + // Add sample documents and click run + actions.addDocumentsJson(JSON.stringify(DOCUMENTS)); + await actions.clickRunPipelineButton(); + + // Close flyout + actions.closeTestPipelineFlyout(); + expect(exists('testPipelineFlyout')).toBe(false); + expect(exists('addDocumentsButton')).toBe(false); + expect(exists('documentsDropdown')).toBe(true); + + // Reopen flyout and verify output tab is enabled + await actions.clickViewOutputButton(); + expect(exists('testPipelineFlyout')).toBe(true); + expect(exists('documentsTabContent')).toBe(false); + expect(exists('outputTabContent')).toBe(true); + }); + + test('should surface API errors from the request', async () => { + const { actions, find, exists } = testBed; + + const error = { + status: 400, + error: 'Bad Request', + message: + '"[parse_exception] [_source] required property is missing, with { property_name="_source" }"', + }; + + httpRequestsMockHelpers.setSimulatePipelineResponse(undefined, { body: error }); + + // Open flyout + actions.clickAddDocumentsButton(); + + // Add invalid sample documents array and run the pipeline + actions.addDocumentsJson(JSON.stringify([{}])); + await actions.clickRunPipelineButton(); + + // Verify error rendered + expect(exists('pipelineExecutionError')).toBe(true); + expect(find('pipelineExecutionError').text()).toContain(error.message); + }); + }); + + describe('Processors', () => { + // This is a hack + // We need to provide the processor id in the mocked output; + // this is generated dynamically and not something we can stub. + // As a workaround, the value is added as a data attribute in the UI + // and we retrieve it to generate the mocked output. + const addProcessorTagtoMockOutput = (output: VerboseTestOutput) => { + const { find } = testBed; + + const docs = output.docs.map((doc) => { + const results = doc.processor_results.map((result, index) => { + const tag = find(`processors>${index}`).props()['data-processor-id']; + return { + ...result, + tag, + }; + }); + return { processor_results: results }; + }); + return { docs }; + }; + + it('should show "inactive" processor status by default', async () => { + const { find } = testBed; + + const statusIconLabel = find('processors>0.processorStatusIcon').props()['aria-label']; + + expect(statusIconLabel).toEqual('Not run'); + }); + + it('should update the processor status after execution', async () => { + const { actions, find } = testBed; + + const mockVerboseOutputWithProcessorTag = addProcessorTagtoMockOutput(SIMULATE_RESPONSE); + httpRequestsMockHelpers.setSimulatePipelineResponse(mockVerboseOutputWithProcessorTag); + + // Open flyout + actions.clickAddDocumentsButton(); + + // Add sample documents and click run + actions.addDocumentsJson(JSON.stringify(DOCUMENTS)); + await actions.clickRunPipelineButton(); + actions.closeTestPipelineFlyout(); + + // Verify status + const statusIconLabel = find('processors>0.processorStatusIcon').props()['aria-label']; + expect(statusIconLabel).toEqual('Success'); + }); + + describe('Output tab', () => { + beforeEach(async () => { + const { actions } = testBed; + + const mockVerboseOutputWithProcessorTag = addProcessorTagtoMockOutput(SIMULATE_RESPONSE); + httpRequestsMockHelpers.setSimulatePipelineResponse(mockVerboseOutputWithProcessorTag); + + // Add documents and run the pipeline + actions.clickAddDocumentsButton(); + actions.addDocumentsJson(JSON.stringify(DOCUMENTS)); + await actions.clickRunPipelineButton(); + actions.closeTestPipelineFlyout(); + }); + + it('should show the output of the processor', async () => { + const { actions, exists } = testBed; + + // Click processor to open manage flyout + await actions.clickProcessor('processors>0'); + // Verify flyout opened + expect(exists('processorSettingsForm')).toBe(true); + + // Navigate to "Output" tab + actions.clickProcessorOutputTab(); + // Verify content + expect(exists('processorOutputTabContent')).toBe(true); + }); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.tsx index e9aa5c1d56f73..e26b6a2890fe4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.tsx @@ -62,6 +62,7 @@ export const DocumentsDropdown: FunctionComponent = ({ updateSelectedDocument(Number(e.target.value)); }} aria-label={i18nTexts.ariaLabel} + data-test-subj="documentsDropdown" /> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_output.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_output.tsx index c081f69fd41fe..c30fdad969b24 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_output.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_output.tsx @@ -91,7 +91,7 @@ export const ProcessorOutput: React.FunctionComponent = ({ } = processorOutput!; return ( - <> +

{i18nTexts.tabDescription}

@@ -212,6 +212,6 @@ export const ProcessorOutput: React.FunctionComponent = ({ )} - +
); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx index 4a67e27d2ebe6..bf69f817183ab 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx @@ -141,6 +141,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( alignItems="center" justifyContent="spaceBetween" data-test-subj={selectorToDataTestSubject(selector)} + data-processor-id={processor.id} > diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_status.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_status.tsx index 26ff113b97440..a58d482022b4d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_status.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_status.tsx @@ -79,7 +79,13 @@ export const PipelineProcessorsItemStatus: FunctionComponent = ({ process return ( {label}

}> - +
); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx index 361e32c77d59b..6fd1adad54f84 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx @@ -37,7 +37,7 @@ export const TestOutputButton: FunctionComponent = ({ @@ -51,7 +51,7 @@ export const TestOutputButton: FunctionComponent = ({ {i18nTexts.buttonLabel} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx index e8bb1aa1d357f..b26c6f536366d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx @@ -182,6 +182,7 @@ export const TestPipelineFlyout: React.FunctionComponent = ({ } color="danger" iconType="alert" + data-test-subj="pipelineExecutionError" >

{testingError.message}

diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_documents.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_documents.tsx index 8968416683c3e..dd12cdab0c934 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_documents.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_documents.tsx @@ -72,7 +72,7 @@ export const DocumentsTab: React.FunctionComponent = ({ }); return ( - <> +

= ({ path="documents" component={JsonEditorField} componentProps={{ - ['data-test-subj']: 'documentsField', euiCodeEditorProps: { + 'data-test-subj': 'documentsEditor', height: '300px', 'aria-label': i18n.translate( 'xpack.ingestPipelines.testPipelineFlyout.documentsTab.editorFieldAriaLabel', @@ -128,6 +128,7 @@ export const DocumentsTab: React.FunctionComponent = ({ = ({ )} - +

); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_output.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_output.tsx index 586fc9e60017a..926bab6da993c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_output.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_output.tsx @@ -56,7 +56,7 @@ export const OutputTab: React.FunctionComponent = ({ } return ( - <> +

= ({ } checked={isVerboseEnabled} onChange={(e) => onEnableVerbose(e.target.checked)} + data-test-subj="verboseOutputToggle" /> @@ -88,6 +89,7 @@ export const OutputTab: React.FunctionComponent = ({ handleTestPipeline({ documents: cachedDocuments!, verbose: isVerboseEnabled }) } iconType="refresh" + data-test-subj="refreshOutputButton" > = ({ {content} - +

); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/test_pipeline_tabs.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/test_pipeline_tabs.tsx index d0ea226e8db80..abfb86c2afda1 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/test_pipeline_tabs.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/test_pipeline_tabs.tsx @@ -50,7 +50,7 @@ export const Tabs: React.FunctionComponent = ({ isSelected={tab.id === selectedTab} key={tab.id} disabled={getIsDisabled(tab.id)} - data-test-subj={tab.id.toLowerCase() + '_tab'} + data-test-subj={tab.id.toLowerCase() + 'Tab'} > {tab.name} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts index 9b7c2069fcddd..a70c0d281cf95 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts @@ -4,71 +4,143 @@ * you may not use this file except in compliance with the Elastic License. */ -import { deserialize } from './deserialize'; +import { deserialize, deserializeVerboseTestOutput } from './deserialize'; -describe('deserialize', () => { - it('tolerates certain bad values correctly', () => { - expect( - deserialize({ +describe('Deserialization', () => { + describe('deserialize()', () => { + it('tolerates certain bad values correctly', () => { + expect( + deserialize({ + processors: [ + { set: { field: 'test', value: 123 } }, + { badType1: null } as any, + { badType2: 1 } as any, + ], + onFailure: [ + { + gsub: { + field: '_index', + pattern: '(.monitoring-\\w+-)6(-.+)', + replacement: '$17$2', + }, + }, + ], + }) + ).toEqual({ processors: [ - { set: { field: 'test', value: 123 } }, - { badType1: null } as any, - { badType2: 1 } as any, + { + id: expect.any(String), + type: 'set', + options: { + field: 'test', + value: 123, + }, + }, + { + id: expect.any(String), + onFailure: undefined, + type: 'badType1', + options: {}, + }, + { + id: expect.any(String), + onFailure: undefined, + type: 'badType2', + options: {}, + }, ], onFailure: [ { - gsub: { + id: expect.any(String), + type: 'gsub', + onFailure: undefined, + options: { field: '_index', pattern: '(.monitoring-\\w+-)6(-.+)', replacement: '$17$2', }, }, ], - }) - ).toEqual({ - processors: [ + }); + }); + + it('throws for unacceptable values', () => { + expect(() => { + deserialize({ + processors: [{ reallyBad: undefined } as any, 1 as any], + onFailure: [], + }); + }).toThrow('Invalid processor type'); + }); + }); + + describe('deserializeVerboseOutput()', () => { + it('deserializes the verbose output of a simulated pipeline', () => { + expect( + deserializeVerboseTestOutput({ + docs: [ + { + processor_results: [ + { + doc: { + _id: 'id1', + _source: { + name: 'foo', + foo: 'bar', + }, + }, + processor_type: 'set', + status: 'success', + tag: 'e457615c-69c9-4d14-9e85-c477ad96e60f', + }, + ], + }, + { + processor_results: [ + { + doc: { + _id: 'id2', + _source: { + name: 'baz', + foo: 'bar', + }, + }, + processor_type: 'set', + status: 'success', + tag: 'e457615c-69c9-4d14-9e85-c477ad96e60f', + }, + ], + }, + ], + }) + ).toEqual([ { - id: expect.any(String), - type: 'set', - options: { - field: 'test', - value: 123, + 'e457615c-69c9-4d14-9e85-c477ad96e60f': { + doc: { + _id: 'id1', + _source: { + name: 'foo', + foo: 'bar', + }, + }, + processor_type: 'set', + status: 'success', }, }, { - id: expect.any(String), - onFailure: undefined, - type: 'badType1', - options: {}, - }, - { - id: expect.any(String), - onFailure: undefined, - type: 'badType2', - options: {}, - }, - ], - onFailure: [ - { - id: expect.any(String), - type: 'gsub', - onFailure: undefined, - options: { - field: '_index', - pattern: '(.monitoring-\\w+-)6(-.+)', - replacement: '$17$2', + 'e457615c-69c9-4d14-9e85-c477ad96e60f': { + doc: { + _id: 'id2', + _source: { + name: 'baz', + foo: 'bar', + }, + }, + processor_type: 'set', + status: 'success', }, }, - ], + ]); }); }); - - it('throws for unacceptable values', () => { - expect(() => { - deserialize({ - processors: [{ reallyBad: undefined } as any, 1 as any], - onFailure: [], - }); - }).toThrow('Invalid processor type'); - }); }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts index 9083985b0ff2e..5229f5eb0bb21 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts @@ -90,7 +90,7 @@ export type ProcessorStatus = export interface ProcessorResult { processor_type: string; status: ProcessorStatus; - doc: Document; + doc?: Document; tag: string; ignored_error?: any; error?: any; diff --git a/x-pack/plugins/licensing/public/mocks.ts b/x-pack/plugins/licensing/public/mocks.ts index 8421a343d91ca..1ddde892de0d9 100644 --- a/x-pack/plugins/licensing/public/mocks.ts +++ b/x-pack/plugins/licensing/public/mocks.ts @@ -6,12 +6,14 @@ import { BehaviorSubject } from 'rxjs'; import { LicensingPluginSetup, LicensingPluginStart } from './types'; import { licenseMock } from '../common/licensing.mock'; +import { featureUsageMock } from './services/feature_usage_service.mock'; const createSetupMock = () => { const license = licenseMock.createLicense(); const mock: jest.Mocked = { license$: new BehaviorSubject(license), refresh: jest.fn(), + featureUsage: featureUsageMock.createSetup(), }; mock.refresh.mockResolvedValue(license); @@ -23,6 +25,7 @@ const createStartMock = () => { const mock: jest.Mocked = { license$: new BehaviorSubject(license), refresh: jest.fn(), + featureUsage: featureUsageMock.createStart(), }; mock.refresh.mockResolvedValue(license); diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index ec42a73f610c0..aa0c25364f2c7 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -6,12 +6,12 @@ import { Observable, Subject, Subscription } from 'rxjs'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; - import { ILicense } from '../common/types'; import { LicensingPluginSetup, LicensingPluginStart } from './types'; import { createLicenseUpdate } from '../common/license_update'; import { License } from '../common/license'; import { mountExpiredBanner } from './expired_banner'; +import { FeatureUsageService } from './services'; export const licensingSessionStorageKey = 'xpack.licensing'; @@ -39,6 +39,7 @@ export class LicensingPlugin implements Plugin Promise; private license$?: Observable; + private featureUsage = new FeatureUsageService(); constructor( context: PluginInitializerContext, @@ -116,6 +117,7 @@ export class LicensingPlugin implements Plugin => { + const mock = { + register: jest.fn(), + }; + + return mock; +}; + +const createStartMock = (): jest.Mocked => { + const mock = { + notifyUsage: jest.fn(), + }; + + return mock; +}; + +const createServiceMock = (): jest.Mocked> => { + const mock = { + setup: jest.fn(), + start: jest.fn(), + }; + + mock.setup.mockImplementation(() => createSetupMock()); + mock.start.mockImplementation(() => createStartMock()); + + return mock; +}; + +export const featureUsageMock = { + create: createServiceMock, + createSetup: createSetupMock, + createStart: createStartMock, +}; diff --git a/x-pack/plugins/licensing/public/services/feature_usage_service.test.ts b/x-pack/plugins/licensing/public/services/feature_usage_service.test.ts new file mode 100644 index 0000000000000..eba2d1e67b509 --- /dev/null +++ b/x-pack/plugins/licensing/public/services/feature_usage_service.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { httpServiceMock } from '../../../../../src/core/public/mocks'; +import { FeatureUsageService } from './feature_usage_service'; + +describe('FeatureUsageService', () => { + let http: ReturnType; + let service: FeatureUsageService; + + beforeEach(() => { + http = httpServiceMock.createSetupContract(); + service = new FeatureUsageService(); + }); + + describe('#setup', () => { + describe('#register', () => { + it('calls the endpoint with the correct parameters', async () => { + const setup = service.setup({ http }); + await setup.register('my-feature', 'platinum'); + expect(http.post).toHaveBeenCalledTimes(1); + expect(http.post).toHaveBeenCalledWith('/internal/licensing/feature_usage/register', { + body: JSON.stringify({ + featureName: 'my-feature', + licenseType: 'platinum', + }), + }); + }); + }); + }); + + describe('#start', () => { + describe('#notifyUsage', () => { + it('calls the endpoint with the correct parameters', async () => { + service.setup({ http }); + const start = service.start({ http }); + await start.notifyUsage('my-feature', 42); + + expect(http.post).toHaveBeenCalledTimes(1); + expect(http.post).toHaveBeenCalledWith('/internal/licensing/feature_usage/notify', { + body: JSON.stringify({ + featureName: 'my-feature', + lastUsed: 42, + }), + }); + }); + + it('correctly convert dates', async () => { + service.setup({ http }); + const start = service.start({ http }); + + const now = new Date(); + + await start.notifyUsage('my-feature', now); + + expect(http.post).toHaveBeenCalledTimes(1); + expect(http.post).toHaveBeenCalledWith('/internal/licensing/feature_usage/notify', { + body: JSON.stringify({ + featureName: 'my-feature', + lastUsed: now.getTime(), + }), + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/licensing/public/services/feature_usage_service.ts b/x-pack/plugins/licensing/public/services/feature_usage_service.ts new file mode 100644 index 0000000000000..588d22eeb818d --- /dev/null +++ b/x-pack/plugins/licensing/public/services/feature_usage_service.ts @@ -0,0 +1,68 @@ +/* + * 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 isDate from 'lodash/isDate'; +import type { HttpSetup, HttpStart } from 'src/core/public'; +import { LicenseType } from '../../common/types'; + +/** @public */ +export interface FeatureUsageServiceSetup { + /** + * Register a feature to be able to notify of it's usages using the {@link FeatureUsageServiceStart | service start contract}. + */ + register(featureName: string, licenseType: LicenseType): Promise; +} + +/** @public */ +export interface FeatureUsageServiceStart { + /** + * Notify of a registered feature usage at given time. + * + * @param featureName - the name of the feature to notify usage of + * @param usedAt - Either a `Date` or an unix timestamp with ms. If not specified, it will be set to the current time. + */ + notifyUsage(featureName: string, usedAt?: Date | number): Promise; +} + +interface SetupDeps { + http: HttpSetup; +} + +interface StartDeps { + http: HttpStart; +} + +/** + * @internal + */ +export class FeatureUsageService { + public setup({ http }: SetupDeps): FeatureUsageServiceSetup { + return { + register: async (featureName, licenseType) => { + await http.post('/internal/licensing/feature_usage/register', { + body: JSON.stringify({ + featureName, + licenseType, + }), + }); + }, + }; + } + + public start({ http }: StartDeps): FeatureUsageServiceStart { + return { + notifyUsage: async (featureName, usedAt = Date.now()) => { + const lastUsed = isDate(usedAt) ? usedAt.getTime() : usedAt; + await http.post('/internal/licensing/feature_usage/notify', { + body: JSON.stringify({ + featureName, + lastUsed, + }), + }); + }, + }; + } +} diff --git a/x-pack/plugins/licensing/public/services/index.ts b/x-pack/plugins/licensing/public/services/index.ts new file mode 100644 index 0000000000000..fc890dd3c927d --- /dev/null +++ b/x-pack/plugins/licensing/public/services/index.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +export { + FeatureUsageService, + FeatureUsageServiceSetup, + FeatureUsageServiceStart, +} from './feature_usage_service'; diff --git a/x-pack/plugins/licensing/public/types.ts b/x-pack/plugins/licensing/public/types.ts index 71a4a452d163d..43b146c51d9a8 100644 --- a/x-pack/plugins/licensing/public/types.ts +++ b/x-pack/plugins/licensing/public/types.ts @@ -6,6 +6,7 @@ import { Observable } from 'rxjs'; import { ILicense } from '../common/types'; +import { FeatureUsageServiceSetup, FeatureUsageServiceStart } from './services'; /** @public */ export interface LicensingPluginSetup { @@ -19,6 +20,10 @@ export interface LicensingPluginSetup { * @deprecated in favour of the counterpart provided from start contract */ refresh(): Promise; + /** + * APIs to register licensed feature usage. + */ + featureUsage: FeatureUsageServiceSetup; } /** @public */ @@ -31,4 +36,8 @@ export interface LicensingPluginStart { * Triggers licensing information re-fetch. */ refresh(): Promise; + /** + * APIs to manage licensed feature usage. + */ + featureUsage: FeatureUsageServiceStart; } diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 6cdba0ac46644..2ee8d26419571 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -133,7 +133,9 @@ export class LicensingPlugin implements Plugin ) { registerInfoRoute(router); registerFeatureUsageRoute(router, getStartServices); + registerRegisterFeatureRoute(router, featureUsageSetup); + registerNotifyFeatureUsageRoute(router); } diff --git a/x-pack/plugins/licensing/server/routes/internal/index.ts b/x-pack/plugins/licensing/server/routes/internal/index.ts new file mode 100644 index 0000000000000..a3b06c223fc12 --- /dev/null +++ b/x-pack/plugins/licensing/server/routes/internal/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { registerNotifyFeatureUsageRoute } from './notify_feature_usage'; +export { registerRegisterFeatureRoute } from './register_feature'; diff --git a/x-pack/plugins/licensing/server/routes/internal/notify_feature_usage.ts b/x-pack/plugins/licensing/server/routes/internal/notify_feature_usage.ts new file mode 100644 index 0000000000000..ec70472574be3 --- /dev/null +++ b/x-pack/plugins/licensing/server/routes/internal/notify_feature_usage.ts @@ -0,0 +1,32 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; + +export function registerNotifyFeatureUsageRoute(router: IRouter) { + router.post( + { + path: '/internal/licensing/feature_usage/notify', + validate: { + body: schema.object({ + featureName: schema.string(), + lastUsed: schema.number(), + }), + }, + }, + async (context, request, response) => { + const { featureName, lastUsed } = request.body; + + context.licensing.featureUsage.notifyUsage(featureName, lastUsed); + + return response.ok({ + body: { + success: true, + }, + }); + } + ); +} diff --git a/x-pack/plugins/licensing/server/routes/internal/register_feature.ts b/x-pack/plugins/licensing/server/routes/internal/register_feature.ts new file mode 100644 index 0000000000000..14f7952f86f5a --- /dev/null +++ b/x-pack/plugins/licensing/server/routes/internal/register_feature.ts @@ -0,0 +1,43 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +import { LicenseType, LICENSE_TYPE } from '../../../common/types'; +import { FeatureUsageServiceSetup } from '../../services'; + +export function registerRegisterFeatureRoute( + router: IRouter, + featureUsageSetup: FeatureUsageServiceSetup +) { + router.post( + { + path: '/internal/licensing/feature_usage/register', + validate: { + body: schema.object({ + featureName: schema.string(), + licenseType: schema.string({ + validate: (value) => { + if (!(value in LICENSE_TYPE)) { + return `Invalid license type: ${value}`; + } + }, + }), + }), + }, + }, + async (context, request, response) => { + const { featureName, licenseType } = request.body; + + featureUsageSetup.register(featureName, licenseType as LicenseType); + + return response.ok({ + body: { + success: true, + }, + }); + } + ); +} diff --git a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts index ed0656a2fc265..d064dfb1c4a37 100644 --- a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts +++ b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts @@ -53,6 +53,7 @@ export type TooltipState = { }; export type DrawState = { + actionId: string; drawType: DRAW_TYPE; filterLabel?: string; // point radius filter alias geoFieldName?: string; diff --git a/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap b/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap index 85a073c8d9ace..2d39a52dfe974 100644 --- a/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap +++ b/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap @@ -38,6 +38,10 @@ exports[`should not render relation select when geo field is geo_point 1`] = ` } } /> + @@ -121,6 +125,10 @@ exports[`should not show "within" relation when filter geometry is not closed 1` value="INTERSECTS" /> + @@ -177,6 +185,10 @@ exports[`should render error message 1`] = ` } } /> + @@ -267,6 +279,10 @@ exports[`should render relation select when geo field is geo_shape 1`] = ` value="INTERSECTS" /> + diff --git a/x-pack/plugins/maps/public/components/_action_select.scss b/x-pack/plugins/maps/public/components/_action_select.scss new file mode 100644 index 0000000000000..be903ad7d6962 --- /dev/null +++ b/x-pack/plugins/maps/public/components/_action_select.scss @@ -0,0 +1,3 @@ +.mapActionSelectIcon { + margin-right: $euiSizeS; +} diff --git a/x-pack/plugins/maps/public/components/_index.scss b/x-pack/plugins/maps/public/components/_index.scss index 76e27338bdcd4..76ce9f1bc79e3 100644 --- a/x-pack/plugins/maps/public/components/_index.scss +++ b/x-pack/plugins/maps/public/components/_index.scss @@ -1,3 +1,4 @@ +@import 'action_select'; @import 'metric_editors'; @import './geometry_filter'; @import 'tooltip_selector/tooltip_selector'; diff --git a/x-pack/plugins/maps/public/components/action_select.tsx b/x-pack/plugins/maps/public/components/action_select.tsx new file mode 100644 index 0000000000000..ad61a6a129974 --- /dev/null +++ b/x-pack/plugins/maps/public/components/action_select.tsx @@ -0,0 +1,87 @@ +/* + * 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 React, { Component } from 'react'; +import { EuiFormRow, EuiSuperSelect, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; + +interface Props { + value?: string; + onChange: (value: string) => void; + getFilterActions?: () => Promise; + getActionContext?: () => ActionExecutionContext; +} + +interface State { + actions: Action[]; +} + +export class ActionSelect extends Component { + private _isMounted = false; + state: State = { + actions: [], + }; + + componentDidMount() { + this._isMounted = true; + this._loadActions(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + async _loadActions() { + if (!this.props.getFilterActions || !this.props.getActionContext) { + return; + } + const actions = await this.props.getFilterActions(); + if (this._isMounted) { + this.setState({ actions }); + } + } + + render() { + if (this.state.actions.length === 0 || !this.props.getActionContext) { + return null; + } + + if (this.state.actions.length === 1 && this.props.value === this.state.actions[0].id) { + return null; + } + + const actionContext = this.props.getActionContext(); + const options = this.state.actions.map((action) => { + const iconType = action.getIconType(actionContext); + return { + value: action.id, + inputDisplay: ( +
+ {iconType ? : null} + {action.getDisplayName(actionContext)} +
+ ), + }; + }); + + return ( + + + + ); + } +} diff --git a/x-pack/plugins/maps/public/components/distance_filter_form.tsx b/x-pack/plugins/maps/public/components/distance_filter_form.tsx index 768be1558bd69..24d9aec5b77b4 100644 --- a/x-pack/plugins/maps/public/components/distance_filter_form.tsx +++ b/x-pack/plugins/maps/public/components/distance_filter_form.tsx @@ -14,18 +14,25 @@ import { EuiTextAlign, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select'; import { GeoFieldWithIndex } from './geo_field_with_index'; +import { ActionSelect } from './action_select'; +import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public'; interface Props { className?: string; buttonLabel: string; geoFields: GeoFieldWithIndex[]; + getFilterActions?: () => Promise; + getActionContext?: () => ActionExecutionContext; onSubmit: ({ + actionId, filterLabel, indexPatternId, geoFieldName, }: { + actionId: string; filterLabel: string; indexPatternId: string; geoFieldName: string; @@ -33,12 +40,14 @@ interface Props { } interface State { + actionId: string; selectedField: GeoFieldWithIndex | undefined; filterLabel: string; } export class DistanceFilterForm extends Component { - state = { + state: State = { + actionId: ACTION_GLOBAL_APPLY_FILTER, selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined, filterLabel: '', }; @@ -53,11 +62,16 @@ export class DistanceFilterForm extends Component { }); }; + _onActionIdChange = (value: string) => { + this.setState({ actionId: value }); + }; + _onSubmit = () => { if (!this.state.selectedField) { return; } this.props.onSubmit({ + actionId: this.state.actionId, filterLabel: this.state.filterLabel, indexPatternId: this.state.selectedField.indexPatternId, geoFieldName: this.state.selectedField.geoFieldName, @@ -86,6 +100,13 @@ export class DistanceFilterForm extends Component { onChange={this._onGeoFieldChange} /> + + diff --git a/x-pack/plugins/maps/public/components/geometry_filter_form.js b/x-pack/plugins/maps/public/components/geometry_filter_form.js index d5cdda3c1c324..fde07e8c16bc5 100644 --- a/x-pack/plugins/maps/public/components/geometry_filter_form.js +++ b/x-pack/plugins/maps/public/components/geometry_filter_form.js @@ -20,11 +20,15 @@ import { i18n } from '@kbn/i18n'; import { ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../common/constants'; import { getEsSpatialRelationLabel } from '../../common/i18n_getters'; import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select'; +import { ActionSelect } from './action_select'; +import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public'; export class GeometryFilterForm extends Component { static propTypes = { buttonLabel: PropTypes.string.isRequired, geoFields: PropTypes.array.isRequired, + getFilterActions: PropTypes.func, + getActionContext: PropTypes.func, intitialGeometryLabel: PropTypes.string.isRequired, onSubmit: PropTypes.func.isRequired, isFilterGeometryClosed: PropTypes.bool, @@ -36,6 +40,7 @@ export class GeometryFilterForm extends Component { }; state = { + actionId: ACTION_GLOBAL_APPLY_FILTER, selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined, geometryLabel: this.props.intitialGeometryLabel, relation: ES_SPATIAL_RELATIONS.INTERSECTS, @@ -57,8 +62,13 @@ export class GeometryFilterForm extends Component { }); }; + _onActionIdChange = (value) => { + this.setState({ actionId: value }); + }; + _onSubmit = () => { this.props.onSubmit({ + actionId: this.state.actionId, geometryLabel: this.state.geometryLabel, indexPatternId: this.state.selectedField.indexPatternId, geoFieldName: this.state.selectedField.geoFieldName, @@ -134,6 +144,13 @@ export class GeometryFilterForm extends Component { {this._renderRelationInput()} + + {error} diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap index 3b3d82c92fbb7..29df06a64a3f2 100644 --- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap @@ -1,11 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FeatureProperties should not show filter button 1`] = ` +exports[`FeatureProperties should render 1`] = `
+
+
@@ -56,12 +60,13 @@ exports[`FeatureProperties should show error message if unable to load tooltip c `; -exports[`FeatureProperties should show only filter button for filterable properties 1`] = ` +exports[`FeatureProperties should show filter button for filterable properties 1`] = ` + +
- + > + +
+ +
+`; + +exports[`FeatureProperties should show view actions button when there are available actions 1`] = ` + + + + + + + + +
+ prop1 + + + + + + + + + + +
+ prop2 + +
diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/_index.scss b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/_index.scss index fb75cc1e2db69..abd747c8fa47a 100644 --- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/_index.scss +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/_index.scss @@ -1,6 +1,5 @@ .mapFeatureTooltip_table { width: 100%; - display: block; max-height: calc(49vh - #{$euiSizeXL * 2}); td { @@ -8,6 +7,10 @@ } } +.mapFeatureTooltip_row { + border-bottom: 1px solid $euiColorLightestShade; +} + .mapFeatureTooltip_actionLinks { padding: $euiSizeXS; } @@ -20,3 +23,10 @@ max-width: $euiSizeXL * 4; font-weight: $euiFontWeightSemiBold; } + +.mapFeatureTooltip_actionsRow { + > span { + display: flex; + justify-content: flex-end; + } +} diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js index b0ce52b4db7ab..98267965fd30f 100644 --- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component, Fragment } from 'react'; -import { EuiIcon } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import { URL_MAX_LENGTH } from '../../../../../../../src/core/public'; @@ -95,28 +93,7 @@ export class FeatureGeometryFilterForm extends Component { this.props.onClose(); }; - _renderHeader() { - return ( - - ); - } - - _renderForm() { + render() { return ( ); } - - render() { - return ( - - {this._renderHeader()} - {this._renderForm()} - - ); - } } diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js index 5e2a153b2ccbf..edd501f266690 100644 --- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js @@ -5,12 +5,21 @@ */ import React from 'react'; -import { EuiCallOut, EuiLoadingSpinner, EuiTextAlign, EuiButtonIcon } from '@elastic/eui'; +import { + EuiCallOut, + EuiLoadingSpinner, + EuiTextAlign, + EuiButtonEmpty, + EuiIcon, + EuiContextMenu, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../../../src/plugins/data/public'; export class FeatureProperties extends React.Component { state = { properties: null, + actions: [], loadPropertiesErrorMsg: null, prevWidth: null, prevHeight: null, @@ -21,6 +30,7 @@ export class FeatureProperties extends React.Component { this.prevLayerId = undefined; this.prevFeatureId = undefined; this._loadProperties(); + this._loadActions(); } componentDidUpdate() { @@ -31,6 +41,16 @@ export class FeatureProperties extends React.Component { this._isMounted = false; } + async _loadActions() { + if (!this.props.getFilterActions) { + return; + } + const actions = await this.props.getFilterActions(); + if (this._isMounted) { + this.setState({ actions }); + } + } + _loadProperties = async () => { this._fetchProperties({ nextFeatureId: this.props.featureId, @@ -39,6 +59,10 @@ export class FeatureProperties extends React.Component { }); }; + _showFilterActions = (tooltipProperty) => { + this.props.showFilterActions(this._renderFilterActions(tooltipProperty)); + }; + _fetchProperties = async ({ nextLayerId, nextFeatureId, mbProperties }) => { if (this.prevLayerId === nextLayerId && this.prevFeatureId === nextFeatureId) { // do not reload same feature properties @@ -83,35 +107,108 @@ export class FeatureProperties extends React.Component { } if (this._isMounted) { - this.setState({ - properties, - }); + this.setState({ properties }); } }; + _renderFilterActions(tooltipProperty) { + const panel = { + id: 0, + items: this.state.actions.map((action) => { + const actionContext = this.props.getActionContext(); + const iconType = action.getIconType(actionContext); + const name = action.getDisplayName(actionContext); + return { + name, + icon: iconType ? : null, + onClick: async () => { + this.props.onCloseTooltip(); + const filters = await tooltipProperty.getESFilters(); + this.props.addFilters(filters, action.id); + }, + ['data-test-subj']: `mapFilterActionButton__${name}`, + }; + }), + }; + + return ( +
+ (this._node = node)} + > + + + + + +
+ {tooltipProperty.getPropertyName()} + +
+ +
+ ); + } + _renderFilterCell(tooltipProperty) { if (!this.props.showFilterButtons || !tooltipProperty.isFilterable()) { - return null; + return ; } - return ( - - { - this.props.onCloseTooltip(); - const filters = await tooltipProperty.getESFilters(); - this.props.addFilters(filters); - }} - aria-label={i18n.translate('xpack.maps.tooltip.filterOnPropertyAriaLabel', { - defaultMessage: 'Filter on property', - })} - data-test-subj="mapTooltipCreateFilterButton" - /> + const applyFilterButton = ( + { + this.props.onCloseTooltip(); + const filters = await tooltipProperty.getESFilters(); + this.props.addFilters(filters); + }} + aria-label={i18n.translate('xpack.maps.tooltip.filterOnPropertyAriaLabel', { + defaultMessage: 'Filter on property', + })} + data-test-subj="mapTooltipCreateFilterButton" + > + + + ); + + return this.state.actions.length === 0 || + (this.state.actions.length === 1 && + this.state.actions[0].id === ACTION_GLOBAL_APPLY_FILTER) ? ( + {applyFilterButton} + ) : ( + + + {applyFilterButton} + { + this._showFilterActions(tooltipProperty); + }} + aria-label={i18n.translate('xpack.maps.tooltip.viewActionsTitle', { + defaultMessage: 'View filter actions', + })} + data-test-subj="mapTooltipMoreActionsButton" + > + + + ); } @@ -154,7 +251,7 @@ export class FeatureProperties extends React.Component { const rows = this.state.properties.map((tooltipProperty) => { const label = tooltipProperty.getPropertyName(); return ( - + {label} {}, showFilterButtons: false, + getFilterActions: () => { + return [{ id: ACTION_GLOBAL_APPLY_FILTER }]; + }, }; const mockTooltipProperties = [ @@ -44,10 +48,29 @@ const mockTooltipProperties = [ ]; describe('FeatureProperties', () => { - test('should not show filter button', async () => { + test('should render', async () => { + const component = shallow( + { + return mockTooltipProperties; + }} + /> + ); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); + + test('should show filter button for filterable properties', async () => { const component = shallow( { return mockTooltipProperties; }} @@ -62,7 +85,7 @@ describe('FeatureProperties', () => { expect(component).toMatchSnapshot(); }); - test('should show only filter button for filterable properties', async () => { + test('should show view actions button when there are available actions', async () => { const component = shallow( { loadFeatureProperties={() => { return mockTooltipProperties; }} + getFilterActions={() => { + return [{ id: 'drilldown1' }]; + }} /> ); diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js index d91bc8e803ab9..8547219b42e30 100644 --- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js @@ -4,20 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; -import { EuiLink } from '@elastic/eui'; +import React, { Component, Fragment } from 'react'; +import { EuiIcon, EuiLink } from '@elastic/eui'; import { FeatureProperties } from './feature_properties'; -import { FormattedMessage } from '@kbn/i18n/react'; import { GEO_JSON_TYPE, ES_GEO_FIELD_TYPE } from '../../../../common/constants'; import { FeatureGeometryFilterForm } from './feature_geometry_filter_form'; import { TooltipHeader } from './tooltip_header'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; const VIEWS = { PROPERTIES_VIEW: 'PROPERTIES_VIEW', GEOMETRY_FILTER_VIEW: 'GEOMETRY_FILTER_VIEW', + FILTER_ACTIONS_VIEW: 'FILTER_ACTIONS_VIEW', }; -export class FeaturesTooltip extends React.Component { +export class FeaturesTooltip extends Component { state = {}; static getDerivedStateFromProps(nextProps, prevState) { @@ -41,7 +43,11 @@ export class FeaturesTooltip extends React.Component { }; _showPropertiesView = () => { - this.setState({ view: VIEWS.PROPERTIES_VIEW }); + this.setState({ view: VIEWS.PROPERTIES_VIEW, filterView: null }); + }; + + _showFilterActionsView = (filterView) => { + this.setState({ view: VIEWS.FILTER_ACTIONS_VIEW, filterView }); }; _renderActions(geoFields) { @@ -96,6 +102,22 @@ export class FeaturesTooltip extends React.Component { }); }; + _renderBackButton(label) { + return ( + + ); + } + render() { if (!this.state.currentFeature) { return null; @@ -109,14 +131,36 @@ export class FeaturesTooltip extends React.Component { if (this.state.view === VIEWS.GEOMETRY_FILTER_VIEW && currentFeatureGeometry) { return ( - + + {this._renderBackButton( + i18n.translate('xpack.maps.tooltip.showGeometryFilterViewLinkLabel', { + defaultMessage: 'Filter by geometry', + }) + )} + + + ); + } + + if (this.state.view === VIEWS.FILTER_ACTIONS_VIEW) { + return ( + + {this._renderBackButton( + i18n.translate('xpack.maps.tooltip.showAddFilterActionsViewLabel', { + defaultMessage: 'Filter actions', + }) + )} + {this.state.filterView} + ); } @@ -137,6 +181,9 @@ export class FeaturesTooltip extends React.Component { showFilterButtons={!!this.props.addFilters && this.props.isLocked} onCloseTooltip={this.props.closeTooltip} addFilters={this.props.addFilters} + getFilterActions={this.props.getFilterActions} + getActionContext={this.props.getActionContext} + showFilterActions={this._showFilterActionsView} /> {this._renderActions(geoFields)} diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js index 6de936fa4a8f1..49675ac6a3924 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js @@ -62,11 +62,12 @@ export class DrawControl extends React.Component { } }, 0); - _onDraw = (e) => { + _onDraw = async (e) => { if (!e.features.length) { return; } + let filter; if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { const circle = e.features[0]; const distanceKm = _.round( @@ -82,7 +83,7 @@ export class DrawControl extends React.Component { } else if (distanceKm <= 100) { precision = 3; } - const filter = createDistanceFilterWithMeta({ + filter = createDistanceFilterWithMeta({ alias: this.props.drawState.filterLabel, distanceKm, geoFieldName: this.props.drawState.geoFieldName, @@ -92,17 +93,12 @@ export class DrawControl extends React.Component { _.round(circle.properties.center[1], precision), ], }); - this.props.addFilters([filter]); - this.props.disableDrawState(); - return; - } - - const geometry = e.features[0].geometry; - // MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number - roundCoordinates(geometry.coordinates); + } else { + const geometry = e.features[0].geometry; + // MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number + roundCoordinates(geometry.coordinates); - try { - const filter = createSpatialFilterWithGeometry({ + filter = createSpatialFilterWithGeometry({ geometry: this.props.drawState.drawType === DRAW_TYPE.BOUNDS ? getBoundingBoxGeometry(geometry) @@ -113,7 +109,10 @@ export class DrawControl extends React.Component { geometryLabel: this.props.drawState.geometryLabel, relation: this.props.drawState.relation, }); - this.props.addFilters([filter]); + } + + try { + await this.props.addFilters([filter], this.props.drawState.actionId); } catch (error) { // TODO notify user why filter was not created console.error(error); diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js index 84a29db852539..87d6f8e1d8e71 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js @@ -195,6 +195,8 @@ export class TooltipControl extends React.Component { mbMap={this.props.mbMap} layerList={this.props.layerList} addFilters={this.props.addFilters} + getFilterActions={this.props.getFilterActions} + getActionContext={this.props.getActionContext} renderTooltipContent={this.props.renderTooltipContent} geoFields={this.props.geoFields} features={features} diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js index 6c42057680408..4cfddf0034039 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js @@ -117,6 +117,8 @@ export class TooltipPopover extends Component { _renderTooltipContent = () => { const publicProps = { addFilters: this.props.addFilters, + getFilterActions: this.props.getFilterActions, + getActionContext: this.props.getActionContext, closeTooltip: this.props.closeTooltip, features: this.props.features, isLocked: this.props.isLocked, diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/plugins/maps/public/connected_components/map/mb/view.js index 5a38f6039ae4b..22c374aceedd5 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/view.js @@ -309,6 +309,8 @@ export class MBMap extends React.Component { diff --git a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx index beb1eb0947c50..bf75c86ac249d 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx @@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { Filter } from 'src/plugins/data/public'; +import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; // @ts-expect-error import { MBMap } from '../map/mb'; // @ts-expect-error @@ -35,7 +36,9 @@ import 'mapbox-gl/dist/mapbox-gl.css'; const RENDER_COMPLETE_EVENT = 'renderComplete'; interface Props { - addFilters: ((filters: Filter[]) => void) | null; + addFilters: ((filters: Filter[]) => Promise) | null; + getFilterActions?: () => Promise; + getActionContext?: () => ActionExecutionContext; areLayersLoaded: boolean; cancelAllInFlightRequests: () => void; exitFullScreen: () => void; @@ -183,6 +186,8 @@ export class MapContainer extends Component { render() { const { addFilters, + getFilterActions, + getActionContext, flyoutDisplay, isFullScreen, exitFullScreen, @@ -230,11 +235,18 @@ export class MapContainer extends Component { {!this.props.hideToolbarOverlay && ( - + )} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js index a4f85163512f7..a9dc3f822060c 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js @@ -12,14 +12,18 @@ import { FitToData } from './fit_to_data'; export class ToolbarOverlay extends React.Component { _renderToolsControl() { - const { addFilters, geoFields } = this.props; + const { addFilters, geoFields, getFilterActions, getActionContext } = this.props; if (!addFilters || !geoFields.length) { return null; } return ( - + ); } diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js index a06def086b861..017f0369e0b73 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js @@ -123,6 +123,8 @@ export class ToolsControl extends Component { className="mapDrawControl__geometryFilterForm" buttonLabel={DRAW_SHAPE_LABEL_SHORT} geoFields={this.props.geoFields} + getFilterActions={this.props.getFilterActions} + getActionContext={this.props.getActionContext} intitialGeometryLabel={i18n.translate( 'xpack.maps.toolbarOverlay.drawShape.initialGeometryLabel', { @@ -141,6 +143,8 @@ export class ToolsControl extends Component { className="mapDrawControl__geometryFilterForm" buttonLabel={DRAW_BOUNDS_LABEL_SHORT} geoFields={this.props.geoFields} + getFilterActions={this.props.getFilterActions} + getActionContext={this.props.getActionContext} intitialGeometryLabel={i18n.translate( 'xpack.maps.toolbarOverlay.drawBounds.initialGeometryLabel', { @@ -161,6 +165,8 @@ export class ToolsControl extends Component { geoFields={this.props.geoFields.filter(({ geoFieldType }) => { return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT; })} + getFilterActions={this.props.getFilterActions} + getActionContext={this.props.getActionContext} onSubmit={this._initiateDistanceDraw} /> ), diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 43ff274b1353f..1cb393bede956 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -11,7 +11,12 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { Subscription } from 'rxjs'; import { Unsubscribe } from 'redux'; import { Embeddable, IContainer } from '../../../../../src/plugins/embeddable/public'; -import { APPLY_FILTER_TRIGGER } from '../../../../../src/plugins/ui_actions/public'; +import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public'; +import { + APPLY_FILTER_TRIGGER, + ActionExecutionContext, + TriggerContextMapping, +} from '../../../../../src/plugins/ui_actions/public'; import { esFilters, TimeRange, @@ -99,6 +104,10 @@ export class MapEmbeddable extends Embeddable this.onContainerStateChanged(input)); } + supportedTriggers(): Array { + return [APPLY_FILTER_TRIGGER]; + } + setRenderTooltipContent = (renderTooltipContent: RenderToolTipContent) => { this._renderTooltipContent = renderTooltipContent; }; @@ -226,6 +235,8 @@ export class MapEmbeddable extends Embeddable @@ -243,13 +254,36 @@ export class MapEmbeddable extends Embeddable(replaceLayerList(this._layerList)); } - addFilters = (filters: Filter[]) => { - getUiActions().executeTriggerActions(APPLY_FILTER_TRIGGER, { - embeddable: this, + addFilters = async (filters: Filter[], actionId: string = ACTION_GLOBAL_APPLY_FILTER) => { + const executeContext = { + ...this.getActionContext(), filters, + }; + const action = getUiActions().getAction(actionId); + if (!action) { + throw new Error('Unable to apply filter, could not locate action'); + } + action.execute(executeContext); + }; + + getFilterActions = async () => { + return await getUiActions().getTriggerCompatibleActions(APPLY_FILTER_TRIGGER, { + embeddable: this, + filters: [], }); }; + getActionContext = () => { + const trigger = getUiActions().getTrigger(APPLY_FILTER_TRIGGER); + if (!trigger) { + throw new Error('Unable to get context, could not locate trigger'); + } + return { + embeddable: this, + trigger, + } as ActionExecutionContext; + }; + destroy() { super.destroy(); if (this._unsubscribeFromStore) { diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js index b23b4fc888120..5d8af8d71b7fc 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js @@ -5,7 +5,7 @@ */ import { defaultsDeep, uniq, compact } from 'lodash'; - +import { ServiceStatusLevels } from '../../../../../src/core/server'; import { TELEMETRY_COLLECTION_INTERVAL, KIBANA_STATS_TYPE_MONITORING, @@ -30,7 +30,7 @@ import { sendBulkPayload, monitoringBulk } from './lib'; * @param {Object} xpackInfo server.plugins.xpack_main.info object */ export class BulkUploader { - constructor({ log, interval, elasticsearch, kibanaStats }) { + constructor({ log, interval, elasticsearch, statusGetter$, kibanaStats }) { if (typeof interval !== 'number') { throw new Error('interval number of milliseconds is required'); } @@ -52,11 +52,11 @@ export class BulkUploader { }); this.kibanaStats = kibanaStats; - this.kibanaStatusGetter = null; - } - setKibanaStatusGetter(getter) { - this.kibanaStatusGetter = getter; + this.kibanaStatus = null; + this.kibanaStatusGetter$ = statusGetter$.subscribe((nextStatus) => { + this.kibanaStatus = nextStatus.level; + }); } filterCollectorSet(usageCollection) { @@ -128,7 +128,7 @@ export class BulkUploader { async _fetchAndUpload(usageCollection) { const collectorsReady = await usageCollection.areAllCollectorsReady(); const hasUsageCollectors = usageCollection.some(usageCollection.isUsageCollector); - if (!collectorsReady || typeof this.kibanaStatusGetter !== 'function') { + if (!collectorsReady) { this._log.debug('Skipping bulk uploading because not all collectors are ready'); if (hasUsageCollectors) { this._lastFetchUsageTime = null; @@ -172,10 +172,23 @@ export class BulkUploader { return await sendBulkPayload(this._cluster, this._interval, payload, this._log); } + getConvertedKibanaStatuss() { + if (this.kibanaStatus === ServiceStatusLevels.available) { + return 'green'; + } + if (this.kibanaStatus === ServiceStatusLevels.critical) { + return 'red'; + } + if (this.kibanaStatus === ServiceStatusLevels.degraded) { + return 'yellow'; + } + return 'unknown'; + } + getKibanaStats(type) { const stats = { ...this.kibanaStats, - status: this.kibanaStatusGetter(), + status: this.getConvertedKibanaStatuss(), }; if (type === KIBANA_STATS_TYPE_MONITORING) { diff --git a/x-pack/plugins/monitoring/server/plugin.test.ts b/x-pack/plugins/monitoring/server/plugin.test.ts index 8bd43b6be0a8d..a2520593c436d 100644 --- a/x-pack/plugins/monitoring/server/plugin.test.ts +++ b/x-pack/plugins/monitoring/server/plugin.test.ts @@ -5,8 +5,6 @@ */ import { Plugin } from './plugin'; import { combineLatest } from 'rxjs'; -// @ts-ignore -import { initBulkUploader } from './kibana_monitoring'; import { AlertsFactory } from './alerts'; jest.mock('rxjs', () => ({ @@ -27,10 +25,6 @@ jest.mock('./license_service', () => ({ })), })); -jest.mock('./kibana_monitoring', () => ({ - initBulkUploader: jest.fn(), -})); - describe('Monitoring plugin', () => { const initializerContext = { logger: { @@ -71,6 +65,11 @@ describe('Monitoring plugin', () => { createClient: jest.fn(), }, }, + status: { + overall$: { + subscribe: jest.fn(), + }, + }, }; const setupPlugins = { @@ -113,19 +112,13 @@ describe('Monitoring plugin', () => { afterEach(() => { (setupPlugins.alerts.registerType as jest.Mock).mockReset(); + (coreSetup.status.overall$.subscribe as jest.Mock).mockReset(); }); it('always create the bulk uploader', async () => { - const setKibanaStatusGetter = jest.fn(); - (initBulkUploader as jest.Mock).mockImplementation(() => { - return { - setKibanaStatusGetter, - }; - }); const plugin = new Plugin(initializerContext as any); - const contract = await plugin.setup(coreSetup as any, setupPlugins as any); - contract.registerLegacyAPI(null as any); - expect(setKibanaStatusGetter).toHaveBeenCalled(); + await plugin.setup(coreSetup as any, setupPlugins as any); + expect(coreSetup.status.overall$.subscribe).toHaveBeenCalled(); }); it('should register all alerts', async () => { diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 501c96b12fde8..f5cbadb523a81 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -46,7 +46,6 @@ import { IBulkUploader, PluginsSetup, PluginsStart, - LegacyAPI, LegacyRequest, } from './types'; @@ -158,6 +157,7 @@ export class Plugin { elasticsearch: core.elasticsearch, config, log: kibanaMonitoringLog, + statusGetter$: core.status.overall$, kibanaStats: { uuid: this.initializerContext.env.instanceUuid, name: serverInfo.name, @@ -221,11 +221,6 @@ export class Plugin { } return { - // The legacy plugin calls this to register certain legacy dependencies - // that are necessary for the plugin to properly run - registerLegacyAPI: (legacyAPI: LegacyAPI) => { - this.setupLegacy(legacyAPI); - }, // OSS stats api needs to call this in order to centralize how // we fetch kibana specific stats getKibanaStats: () => this.bulkUploader.getKibanaStats(), @@ -280,11 +275,6 @@ export class Plugin { }); } - async setupLegacy(legacyAPI: LegacyAPI) { - // Set the stats getter - this.bulkUploader.setKibanaStatusGetter(() => legacyAPI.getServerStatus()); - } - getLegacyShim( config: MonitoringConfig, legacyConfig: any, diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index a0ef6d3e2d984..e6a4b174df55d 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -33,10 +33,6 @@ export interface MonitoringElasticsearchConfig { hosts: string[]; } -export interface LegacyAPI { - getServerStatus: () => string; -} - export interface PluginsSetup { encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; telemetryCollectionManager?: TelemetryCollectionManagerPluginSetup; @@ -77,7 +73,6 @@ export interface LegacyShimDependencies { } export interface IBulkUploader { - setKibanaStatusGetter: (getter: () => string | undefined) => void; getKibanaStats: () => any; } diff --git a/x-pack/plugins/observability/public/application/application.test.tsx b/x-pack/plugins/observability/public/application/application.test.tsx index db7fca140be89..19995ed233e8d 100644 --- a/x-pack/plugins/observability/public/application/application.test.tsx +++ b/x-pack/plugins/observability/public/application/application.test.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { createMemoryHistory } from 'history'; import React from 'react'; -import { renderApp } from './'; import { Observable } from 'rxjs'; -import { CoreStart, AppMountParameters } from 'src/core/public'; +import { AppMountParameters, CoreStart } from 'src/core/public'; +import { renderApp } from './'; describe('renderApp', () => { it('renders', () => { @@ -19,6 +20,7 @@ describe('renderApp', () => { } as unknown) as CoreStart; const params = ({ element: window.document.createElement('div'), + history: createMemoryHistory(), } as unknown) as AppMountParameters; expect(() => { diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 4c0147dc3cd51..fa691a7f41ddb 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { createHashHistory } from 'history'; import React, { useEffect } from 'react'; import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; @@ -52,10 +51,10 @@ function App() { ); } -export const renderApp = (core: CoreStart, { element }: AppMountParameters) => { +export const renderApp = (core: CoreStart, { element, history }: AppMountParameters) => { const i18nCore = core.i18n; const isDarkMode = core.uiSettings.get('theme:darkMode'); - const history = createHashHistory(); + ReactDOM.render( diff --git a/x-pack/plugins/security_solution/public/common/components/news_feed/no_news/index.tsx b/x-pack/plugins/security_solution/public/common/components/news_feed/no_news/index.tsx index d626433de1b63..c2ef6b9b192c7 100644 --- a/x-pack/plugins/security_solution/public/common/components/news_feed/no_news/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/news_feed/no_news/index.tsx @@ -4,24 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiLink, EuiText } from '@elastic/eui'; -import React from 'react'; +import { EuiText } from '@elastic/eui'; +import React, { useCallback } from 'react'; import * as i18n from '../translations'; -import { useBasePath } from '../../../lib/kibana'; +import { useKibana } from '../../../lib/kibana'; +import { LinkAnchor } from '../../links'; export const NoNews = React.memo(() => { - const basePath = useBasePath(); + const { getUrlForApp, navigateToApp, capabilities } = useKibana().services.application; + const canSeeAdvancedSettings = capabilities.management.kibana.settings ?? false; + const goToKibanaSettings = useCallback( + () => navigateToApp('management', { path: '/kibana/settings' }), + [navigateToApp] + ); + return ( - <> - - {i18n.NO_NEWS_MESSAGE}{' '} - - {i18n.ADVANCED_SETTINGS_LINK_TITLE} - - {'.'} - - + + {canSeeAdvancedSettings ? i18n.NO_NEWS_MESSAGE_ADMIN : i18n.NO_NEWS_MESSAGE} + {canSeeAdvancedSettings && ( + <> + {' '} + + {i18n.ADVANCED_SETTINGS_LINK_TITLE} + + {'.'} + + )} + ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/news_feed/translations.ts b/x-pack/plugins/security_solution/public/common/components/news_feed/translations.ts index b0f9507266e52..dabaa38178884 100644 --- a/x-pack/plugins/security_solution/public/common/components/news_feed/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/news_feed/translations.ts @@ -7,10 +7,17 @@ import { i18n } from '@kbn/i18n'; export const NO_NEWS_MESSAGE = i18n.translate('xpack.securitySolution.newsFeed.noNewsMessage', { - defaultMessage: - 'Your current news feed URL returned no recent news. You may update the URL or disable security news via', + defaultMessage: 'Your current news feed URL returned no recent news.', }); +export const NO_NEWS_MESSAGE_ADMIN = i18n.translate( + 'xpack.securitySolution.newsFeed.noNewsMessageForAdmin', + { + defaultMessage: + 'Your current news feed URL returned no recent news. You may update the URL or disable security news via', + } +); + export const ADVANCED_SETTINGS_LINK_TITLE = i18n.translate( 'xpack.securitySolution.newsFeed.advancedSettingsLinkTitle', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 86334308558b8..8f18a173f3bed 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -272,9 +272,6 @@ export interface NewTimelineProps { export const NewTimeline = React.memo( ({ closeGearMenu, outline = false, timelineId, title = i18n.NEW_TIMELINE }) => { - const uiCapabilities = useKibana().services.application.capabilities; - const capabilitiesCanUserCRUD: boolean = !!uiCapabilities.siem.crud; - const { getButton } = useCreateTimelineButton({ timelineId, timelineType: TimelineType.default, @@ -282,7 +279,7 @@ export const NewTimeline = React.memo( }); const button = getButton({ outline, title }); - return capabilitiesCanUserCRUD ? button : null; + return button; } ); NewTimeline.displayName = 'NewTimeline'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx index 70257c97a6887..12eab4942128f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx @@ -22,7 +22,6 @@ import { TimelineType, } from '../../../../../common/types/timeline'; import { InspectButton, InspectButtonContainer } from '../../../../common/components/inspect'; -import { useKibana } from '../../../../common/lib/kibana'; import { Note } from '../../../../common/lib/note'; import { AssociateNote } from '../../notes/helpers'; @@ -121,8 +120,6 @@ const PropertiesRightComponent: React.FC = ({ updateNote, usersViewing, }) => { - const uiCapabilities = useKibana().services.application.capabilities; - const capabilitiesCanUserCRUD: boolean = !!uiCapabilities.siem.crud; return ( @@ -143,15 +140,13 @@ const PropertiesRightComponent: React.FC = ({ repositionOnScroll > - {capabilitiesCanUserCRUD && ( - - - - )} + + + { }); }); + describe('create draft timeline in read-only permission', () => { + const timelineId = null; + const initialDraftTimeline = { + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + }, + { + columnHeaderType: 'not-filtered', + id: 'event.action', + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + }, + ], + dataProviders: [], + description: 'x', + eventType: 'all', + filters: [], + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + }, + title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, + dateRange: { + start: 1590998565409, + end: 1591084965409, + }, + savedQueryId: null, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + status: TimelineStatus.draft, + }; + + const version = null; + const fetchMock = jest.fn(); + const postMock = jest.fn(); + const patchMock = jest.fn(); + + beforeAll(() => { + jest.resetAllMocks(); + jest.resetModules(); + + (KibanaServices.get as jest.Mock).mockReturnValue({ + http: { + fetch: fetchMock.mockRejectedValue({ + body: { status_code: 403, message: 'you do not have the permission' }, + }), + post: postMock.mockRejectedValue({ + body: { status_code: 403, message: 'you do not have the permission' }, + }), + patch: patchMock.mockRejectedValue({ + body: { status_code: 403, message: 'you do not have the permission' }, + }), + }, + }); + }); + + test('it should return your request timeline with code and message', async () => { + const persist = await api.persistTimeline({ + timelineId, + timeline: initialDraftTimeline, + version, + }); + expect(persist).toEqual({ + data: { + persistTimeline: { + code: 403, + message: 'you do not have the permission', + timeline: { ...initialDraftTimeline, savedObjectId: '', version: '' }, + }, + }, + }); + }); + }); + describe('create active timeline (import)', () => { const timelineId = null; const importTimeline = { diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.ts index e08d52066ebdc..c6794d125368e 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/api.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/api.ts @@ -6,6 +6,7 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; +import isEmpty from 'lodash/isEmpty'; import { throwErrors } from '../../../../case/common/api'; import { @@ -99,44 +100,63 @@ export const persistTimeline = async ({ timeline, version, }: RequestPersistTimeline): Promise => { - if (timelineId == null && timeline.status === TimelineStatus.draft && timeline) { - const draftTimeline = await cleanDraftTimeline({ - timelineType: timeline.timelineType!, - templateTimelineId: timeline.templateTimelineId ?? undefined, - templateTimelineVersion: timeline.templateTimelineVersion ?? undefined, - }); - - const templateTimelineInfo = - timeline.timelineType! === TimelineType.template - ? { - templateTimelineId: - draftTimeline.data.persistTimeline.timeline.templateTimelineId ?? - timeline.templateTimelineId, - templateTimelineVersion: - draftTimeline.data.persistTimeline.timeline.templateTimelineVersion ?? - timeline.templateTimelineVersion, - } - : {}; + try { + if (isEmpty(timelineId) && timeline.status === TimelineStatus.draft && timeline) { + const draftTimeline = await cleanDraftTimeline({ + timelineType: timeline.timelineType!, + templateTimelineId: timeline.templateTimelineId ?? undefined, + templateTimelineVersion: timeline.templateTimelineVersion ?? undefined, + }); + + const templateTimelineInfo = + timeline.timelineType! === TimelineType.template + ? { + templateTimelineId: + draftTimeline.data.persistTimeline.timeline.templateTimelineId ?? + timeline.templateTimelineId, + templateTimelineVersion: + draftTimeline.data.persistTimeline.timeline.templateTimelineVersion ?? + timeline.templateTimelineVersion, + } + : {}; + + return patchTimeline({ + timelineId: draftTimeline.data.persistTimeline.timeline.savedObjectId, + timeline: { + ...timeline, + ...templateTimelineInfo, + }, + version: draftTimeline.data.persistTimeline.timeline.version ?? '', + }); + } + + if (isEmpty(timelineId)) { + return postTimeline({ timeline }); + } return patchTimeline({ - timelineId: draftTimeline.data.persistTimeline.timeline.savedObjectId, - timeline: { - ...timeline, - ...templateTimelineInfo, - }, - version: draftTimeline.data.persistTimeline.timeline.version ?? '', + timelineId: timelineId ?? '-1', + timeline, + version: version ?? '', }); + } catch (err) { + if (err.status_code === 403 || err.body.status_code === 403) { + return Promise.resolve({ + data: { + persistTimeline: { + code: 403, + message: err.message || err.body.message, + timeline: { + ...timeline, + savedObjectId: '', + version: '', + }, + }, + }, + }); + } + return Promise.resolve(err); } - - if (timelineId == null) { - return postTimeline({ timeline }); - } - - return patchTimeline({ - timelineId, - timeline, - version: version ?? '', - }); }; export const importTimelines = async ({ diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx index c22acf6ba7cc1..1d2e16b3fe5b8 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx @@ -64,13 +64,11 @@ export const TimelinesPageComponent: React.FC = () => { {tabName === TimelineType.default ? ( - {capabilitiesCanUserCRUD && ( - - )} + ) : ( diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index dc07044ce8ed7..07b646df74b9f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1115,7 +1115,6 @@ "data.search.aggs.metrics.count.customLabel.help": "このアグリゲーションのカスタムラベルを表します", "data.search.aggs.metrics.count.enabled.help": "このアグリゲーションが有効かどうかを指定します", "data.search.aggs.metrics.count.id.help": "このアグリゲーションのID", - "data.search.aggs.metrics.count.json.help": "アグリゲーションがElasticsearchに送信されるときに含める高度なJSON", "data.search.aggs.metrics.count.schema.help": "このアグリゲーションで使用するスキーマ", "data.search.aggs.metrics.countLabel": "カウント", "data.search.aggs.metrics.countTitle": "カウント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 144bc1cac1852..ffd7d0cfb0f87 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1116,7 +1116,6 @@ "data.search.aggs.metrics.count.customLabel.help": "表示此聚合的定制标签", "data.search.aggs.metrics.count.enabled.help": "指定是否启用此聚合", "data.search.aggs.metrics.count.id.help": "此聚合的 ID", - "data.search.aggs.metrics.count.json.help": "聚合发送至 Elasticsearch 时要包括的高级 json", "data.search.aggs.metrics.count.schema.help": "要用于此聚合的方案", "data.search.aggs.metrics.countLabel": "计数", "data.search.aggs.metrics.countTitle": "计数", diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts index f0222de02697d..015d9a4925f3e 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts @@ -203,28 +203,32 @@ describe('monitor availability', () => { }, }, }, - ], - "minimum_should_match": 1, - "should": Array [ Object { "bool": Object { "minimum_should_match": 1, "should": Array [ Object { - "match_phrase": Object { - "monitor.id": "apm-dev", + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "monitor.id": "apm-dev", + }, + }, + ], }, }, - ], - }, - }, - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ Object { - "match_phrase": Object { - "monitor.id": "auto-http-0X8D6082B94BBE3B8A", + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "monitor.id": "auto-http-0X8D6082B94BBE3B8A", + }, + }, + ], }, }, ], @@ -797,6 +801,133 @@ describe('monitor availability', () => { ] `); }); + + it('does not overwrite filters', async () => { + const [callES, esMock] = setupMockEsCompositeQuery< + AvailabilityKey, + GetMonitorAvailabilityResult, + AvailabilityDoc + >( + [ + { + bucketCriteria: [], + }, + ], + genBucketItem + ); + await getMonitorAvailability({ + callES, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + range: 3, + rangeUnit: 's', + threshold: '99', + filters: JSON.stringify({ bool: { filter: [{ term: { 'monitor.id': 'foo' } }] } }), + }); + const [, params] = esMock.callAsCurrentUser.mock.calls[0]; + expect(params).toMatchInlineSnapshot(` + Object { + "body": Object { + "aggs": Object { + "monitors": Object { + "aggs": Object { + "down_sum": Object { + "sum": Object { + "field": "summary.down", + "missing": 0, + }, + }, + "fields": Object { + "top_hits": Object { + "size": 1, + "sort": Array [ + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + }, + }, + "filtered": Object { + "bucket_selector": Object { + "buckets_path": Object { + "threshold": "ratio.value", + }, + "script": "params.threshold < 0.99", + }, + }, + "ratio": Object { + "bucket_script": Object { + "buckets_path": Object { + "downTotal": "down_sum", + "upTotal": "up_sum", + }, + "script": " + if (params.upTotal + params.downTotal > 0) { + return params.upTotal / (params.upTotal + params.downTotal); + } return null;", + }, + }, + "up_sum": Object { + "sum": Object { + "field": "summary.up", + "missing": 0, + }, + }, + }, + "composite": Object { + "size": 2000, + "sources": Array [ + Object { + "monitorId": Object { + "terms": Object { + "field": "monitor.id", + }, + }, + }, + Object { + "location": Object { + "terms": Object { + "field": "observer.geo.name", + "missing_bucket": true, + }, + }, + }, + ], + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-3s", + "lte": "now", + }, + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "monitor.id": "foo", + }, + }, + ], + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "heartbeat-8*", + } + `); + }); }); describe('formatBuckets', () => { diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts index 7dba71a8126e2..e61d736e37106 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts @@ -148,28 +148,32 @@ describe('getMonitorStatus', () => { }, }, }, - ], - "minimum_should_match": 1, - "should": Array [ Object { "bool": Object { "minimum_should_match": 1, "should": Array [ Object { - "match_phrase": Object { - "monitor.id": "apm-dev", + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "monitor.id": "apm-dev", + }, + }, + ], }, }, - ], - }, - }, - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ Object { - "match_phrase": Object { - "monitor.id": "auto-http-0X8D6082B94BBE3B8A", + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "monitor.id": "auto-http-0X8D6082B94BBE3B8A", + }, + }, + ], }, }, ], @@ -286,6 +290,296 @@ describe('getMonitorStatus', () => { `); }); + it('properly assigns filters for complex kuery filters', async () => { + const [callES, esMock] = setupMockEsCompositeQuery( + [{ bucketCriteria: [] }], + genBucketItem + ); + const clientParameters = { + timerange: { + from: 'now-15m', + to: 'now', + }, + numTimes: 5, + locations: [], + filters: { + bool: { + filter: [ + { + bool: { + should: [ + { + match_phrase: { + tags: 'org:google', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + should: [ + { + match_phrase: { + 'monitor.type': 'http', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match_phrase: { + 'monitor.type': 'tcp', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + }; + await getMonitorStatus({ + callES, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + ...clientParameters, + }); + expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(1); + const [, params] = esMock.callAsCurrentUser.mock.calls[0]; + expect(params).toMatchInlineSnapshot(` + Object { + "body": Object { + "aggs": Object { + "monitors": Object { + "aggs": Object { + "fields": Object { + "top_hits": Object { + "size": 1, + }, + }, + }, + "composite": Object { + "size": 2000, + "sources": Array [ + Object { + "monitorId": Object { + "terms": Object { + "field": "monitor.id", + }, + }, + }, + Object { + "status": Object { + "terms": Object { + "field": "monitor.status", + }, + }, + }, + Object { + "location": Object { + "terms": Object { + "field": "observer.geo.name", + "missing_bucket": true, + }, + }, + }, + ], + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "monitor.status": "down", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-15m", + "lte": "now", + }, + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "tags": "org:google", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "monitor.type": "http", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "monitor.type": "tcp", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "heartbeat-8*", + } + `); + }); + + it('properly assigns filters for complex kuery filters object', async () => { + const [callES, esMock] = setupMockEsCompositeQuery( + [{ bucketCriteria: [] }], + genBucketItem + ); + const clientParameters = { + timerange: { + from: 'now-15m', + to: 'now', + }, + numTimes: 5, + locations: [], + filters: { + bool: { + filter: { + exists: { + field: 'monitor.status', + }, + }, + }, + }, + }; + await getMonitorStatus({ + callES, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + ...clientParameters, + }); + expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(1); + const [, params] = esMock.callAsCurrentUser.mock.calls[0]; + expect(params).toMatchInlineSnapshot(` + Object { + "body": Object { + "aggs": Object { + "monitors": Object { + "aggs": Object { + "fields": Object { + "top_hits": Object { + "size": 1, + }, + }, + }, + "composite": Object { + "size": 2000, + "sources": Array [ + Object { + "monitorId": Object { + "terms": Object { + "field": "monitor.id", + }, + }, + }, + Object { + "status": Object { + "terms": Object { + "field": "monitor.status", + }, + }, + }, + Object { + "location": Object { + "terms": Object { + "field": "observer.geo.name", + "missing_bucket": true, + }, + }, + }, + ], + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "monitor.status": "down", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-15m", + "lte": "now", + }, + }, + }, + Object { + "bool": Object { + "filter": Object { + "exists": Object { + "field": "monitor.status", + }, + }, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "heartbeat-8*", + } + `); + }); + it('fetches single page of results', async () => { const [callES, esMock] = setupMockEsCompositeQuery( [ diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts index 0801fc5769363..f78048100b998 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts @@ -47,6 +47,11 @@ export const getMonitorAvailability: UMElasticsearchQueryFn< const gte = `now-${range}${rangeUnit}`; + let parsedFilters: any; + if (filters) { + parsedFilters = JSON.parse(filters); + } + do { const esParams: any = { index: dynamicSettings.heartbeatIndices, @@ -62,6 +67,8 @@ export const getMonitorAvailability: UMElasticsearchQueryFn< }, }, }, + // append user filters, if defined + ...(parsedFilters?.bool ? [parsedFilters] : []), ], }, }, @@ -139,11 +146,6 @@ export const getMonitorAvailability: UMElasticsearchQueryFn< }, }; - if (filters) { - const parsedFilter = JSON.parse(filters); - esParams.body.query.bool = { ...esParams.body.query.bool, ...parsedFilter.bool }; - } - if (afterKey) { esParams.body.aggs.monitors.composite.after = afterKey; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts index beed8e8335314..caf505610e991 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts @@ -71,6 +71,8 @@ export const getMonitorStatus: UMElasticsearchQueryFn< }, }, }, + // append user filters, if defined + ...(filters?.bool ? [filters] : []), ], }, }, @@ -116,10 +118,6 @@ export const getMonitorStatus: UMElasticsearchQueryFn< }, }; - if (filters?.bool) { - esParams.body.query.bool = Object.assign({}, esParams.body.query.bool, filters.bool); - } - /** * Perform a logical `and` against the selected location filters. */ diff --git a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js index a996910d4787a..10754d20118e9 100644 --- a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js +++ b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js @@ -13,31 +13,62 @@ export default function ({ getPageObjects, getService }) { const filterBar = getService('filterBar'); describe('tooltip filter actions', () => { - before(async () => { + async function loadDashboardAndOpenTooltip() { await kibanaServer.uiSettings.replace({ defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', }); await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.dashboard.loadSavedDashboard('dash for tooltip filter action test'); await PageObjects.maps.lockTooltipAtPosition(200, -200); - }); + } + + describe('apply filter to current view', () => { + before(async () => { + await loadDashboardAndOpenTooltip(); + }); + + it('should display create filter button when tooltip is locked', async () => { + const exists = await testSubjects.exists('mapTooltipCreateFilterButton'); + expect(exists).to.be(true); + }); + + it('should create filters when create filter button is clicked', async () => { + await testSubjects.click('mapTooltipCreateFilterButton'); + await testSubjects.click('applyFiltersPopoverButton'); + + // TODO: Fix me #64861 + // const hasSourceFilter = await filterBar.hasFilter('name', 'charlie'); + // expect(hasSourceFilter).to.be(true); - it('should display create filter button when tooltip is locked', async () => { - const exists = await testSubjects.exists('mapTooltipCreateFilterButton'); - expect(exists).to.be(true); + const hasJoinFilter = await filterBar.hasFilter('shape_name', 'charlie'); + expect(hasJoinFilter).to.be(true); + }); }); - it('should create filters when create filter button is clicked', async () => { - await testSubjects.click('mapTooltipCreateFilterButton'); - await testSubjects.click('applyFiltersPopoverButton'); + describe('panel actions', () => { + before(async () => { + await loadDashboardAndOpenTooltip(); + }); + + it('should display more actions button when tooltip is locked', async () => { + const exists = await testSubjects.exists('mapTooltipMoreActionsButton'); + expect(exists).to.be(true); + }); + + it('should trigger drilldown action when clicked', async () => { + await testSubjects.click('mapTooltipMoreActionsButton'); + await testSubjects.click('mapFilterActionButton__drilldown1'); - // TODO: Fix me #64861 - // const hasSourceFilter = await filterBar.hasFilter('name', 'charlie'); - // expect(hasSourceFilter).to.be(true); + // Assert on new dashboard with filter from action + await PageObjects.dashboard.waitForRenderComplete(); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.equal(2); - const hasJoinFilter = await filterBar.hasFilter('shape_name', 'charlie'); - expect(hasJoinFilter).to.be(true); + const hasJoinFilter = await filterBar.hasFilter('shape_name', 'charlie'); + expect(hasJoinFilter).to.be(true); + }); }); }); } diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index 198174bccb286..0f1fd3c09d706 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -1048,7 +1048,7 @@ "title" : "dash for tooltip filter action test", "hits" : 0, "description" : "Zoomed in so entire screen is covered by filter so click to open tooltip can not miss.", - "panelsJSON" : "[{\"gridData\":{\"x\":0,\"y\":0,\"w\":48,\"h\":26,\"i\":\"1\"},\"version\":\"8.0.0\",\"panelIndex\":\"1\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":-1.31919,\"lon\":59.53306,\"zoom\":9.67},\"isLayerTOCOpen\":false,\"openTOCDetails\":[\"n1t6f\"]},\"panelRefName\":\"panel_0\"}]", + "panelsJSON" : "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":48,\"h\":26,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":-1.31919,\"lon\":59.53306,\"zoom\":9.67},\"isLayerTOCOpen\":false,\"openTOCDetails\":[\"n1t6f\"],\"hiddenLayers\":[],\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"669a3521-1215-4228-9ced-77e2edf5ad17\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"drilldown1\",\"config\":{\"dashboardId\":\"19906970-2e40-11e9-85cb-6965aae20f13\",\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}}},\"panelRefName\":\"panel_0\"}]", "optionsJSON" : "{\"useMargins\":true,\"hidePanelTitles\":false}", "version" : 1, "timeRestore" : true, @@ -1071,9 +1071,9 @@ } ], "migrationVersion" : { - "dashboard" : "7.0.0" + "dashboard" : "7.3.0" }, - "updated_at" : "2019-06-14T14:09:25.039Z" + "updated_at" : "2020-08-26T14:32:27.854Z" } } } diff --git a/x-pack/test/licensing_plugin/public/feature_usage.ts b/x-pack/test/licensing_plugin/public/feature_usage.ts new file mode 100644 index 0000000000000..15d302d71bfab --- /dev/null +++ b/x-pack/test/licensing_plugin/public/feature_usage.ts @@ -0,0 +1,82 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../services'; +import { + LicensingPluginSetup, + LicensingPluginStart, + LicenseType, +} from '../../../plugins/licensing/public'; +import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; + +interface FeatureUsage { + last_used?: number; + license_level: LicenseType; + name: string; +} + +// eslint-disable-next-line import/no-default-export +export default function (ftrContext: FtrProviderContext) { + const { getService, getPageObjects } = ftrContext; + const supertest = getService('supertest'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['common', 'security']); + + const registerFeature = async (featureName: string, licenseType: LicenseType) => { + await browser.executeAsync( + async (feature, type, cb) => { + const { setup } = window._coreProvider; + const licensing: LicensingPluginSetup = setup.plugins.licensing; + await licensing.featureUsage.register(feature, type); + cb(); + }, + featureName, + licenseType + ); + }; + + const notifyFeatureUsage = async (featureName: string, lastUsed: number) => { + await browser.executeAsync( + async (feature, time, cb) => { + const { start } = window._coreProvider; + const licensing: LicensingPluginStart = start.plugins.licensing; + await licensing.featureUsage.notifyUsage(feature, time); + cb(); + }, + featureName, + lastUsed + ); + }; + + describe('feature_usage API', () => { + before(async () => { + await PageObjects.security.login(); + }); + + it('allows to register features to the server', async () => { + await registerFeature('test-client-A', 'gold'); + await registerFeature('test-client-B', 'enterprise'); + + const response = await supertest.get('/api/licensing/feature_usage').expect(200); + const features = response.body.features.map(({ name }: FeatureUsage) => name); + + expect(features).to.contain('test-client-A'); + expect(features).to.contain('test-client-B'); + }); + + it('allows to notify feature usage', async () => { + const now = new Date(); + + await notifyFeatureUsage('test-client-A', now.getTime()); + + const response = await supertest.get('/api/licensing/feature_usage').expect(200); + const features = response.body.features as FeatureUsage[]; + + expect(features.find((f) => f.name === 'test-client-A')?.last_used).to.be(now.toISOString()); + expect(features.find((f) => f.name === 'test-client-B')?.last_used).to.be(null); + }); + }); +} diff --git a/x-pack/test/licensing_plugin/public/index.ts b/x-pack/test/licensing_plugin/public/index.ts index 86a3c21cfdb39..268a74c56bd72 100644 --- a/x-pack/test/licensing_plugin/public/index.ts +++ b/x-pack/test/licensing_plugin/public/index.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../services'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Licensing plugin public client', function () { this.tags('ciGroup2'); + loadTestFile(require.resolve('./feature_usage')); // MUST BE LAST! CHANGES LICENSE TYPE! loadTestFile(require.resolve('./updates')); }); diff --git a/yarn.lock b/yarn.lock index 496331aba0485..0d8ae918f27dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3643,13 +3643,6 @@ resolved "https://registry.yarnpkg.com/@types/d3/-/d3-3.5.43.tgz#e9b4992817e0b6c5efaa7d6e5bb2cee4d73eab58" integrity sha512-t9ZmXOcpVxywRw86YtIC54g7M9puRh8hFedRvVfHKf5YyOP6pSxA0TvpXpfseXSCInoW4P7bggTrSDiUOs4g5w== -"@types/decompress@^4.2.3": - version "4.2.3" - resolved "https://registry.yarnpkg.com/@types/decompress/-/decompress-4.2.3.tgz#98eed48af80001038aa05690b2094915f296fe65" - integrity sha512-W24e3Ycz1UZPgr1ZEDHlK4XnvOr+CpJH3qNsFeqXwwlW/9END9gxn3oJSsp7gYdiQxrXUHwUUd3xuzVz37MrZQ== - dependencies: - "@types/node" "*" - "@types/dedent@^0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@types/dedent/-/dedent-0.7.0.tgz#155f339ca404e6dd90b9ce46a3f78fd69ca9b050" @@ -3716,6 +3709,11 @@ resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5" integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg== +"@types/extract-zip@^1.6.2": + version "1.6.2" + resolved "https://registry.yarnpkg.com/@types/extract-zip/-/extract-zip-1.6.2.tgz#5c7eb441c41136167a42b88b64051e6260c29e86" + integrity sha1-XH60QcQRNhZ6QriLZAUeYmDCnoY= + "@types/fancy-log@^1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@types/fancy-log/-/fancy-log-1.3.1.tgz#dd94fbc8c2e2ab8ab402ca8d04bb8c34965f0696" @@ -7343,19 +7341,14 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bl@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" - integrity sha1-ysMo977kVzDUBLaSID/LWQ4XLV4= - dependencies: - readable-stream "^2.0.5" - -bl@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88" - integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A== +bl@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" + integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== dependencies: - readable-stream "^3.0.1" + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" block-stream@*: version "0.0.9" @@ -7717,19 +7710,6 @@ btoa-lite@^1.0.0: resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= -buffer-alloc-unsafe@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" - integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== - -buffer-alloc@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" - integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== - dependencies: - buffer-alloc-unsafe "^1.1.0" - buffer-fill "^1.0.0" - buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -7750,11 +7730,6 @@ buffer-equal@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74= -buffer-fill@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" - integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= - buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -7787,7 +7762,7 @@ buffer@^5.1.0, buffer@^5.2.0: base64-js "^1.0.2" ieee754 "^1.1.4" -buffer@^5.2.1: +buffer@^5.5.0: version "5.6.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== @@ -8450,7 +8425,7 @@ chokidar@^3.2.2, chokidar@^3.4.0: optionalDependencies: fsevents "~2.1.2" -chownr@^1.0.1, chownr@^1.1.1, chownr@^1.1.2: +chownr@^1.1.1, chownr@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== @@ -10334,59 +10309,6 @@ decompress-response@^5.0.0: dependencies: mimic-response "^2.0.0" -decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" - integrity sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ== - dependencies: - file-type "^5.2.0" - is-stream "^1.1.0" - tar-stream "^1.5.2" - -decompress-tarbz2@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b" - integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A== - dependencies: - decompress-tar "^4.1.0" - file-type "^6.1.0" - is-stream "^1.1.0" - seek-bzip "^1.0.5" - unbzip2-stream "^1.0.9" - -decompress-targz@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee" - integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w== - dependencies: - decompress-tar "^4.1.1" - file-type "^5.2.0" - is-stream "^1.1.0" - -decompress-unzip@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/decompress-unzip/-/decompress-unzip-4.0.1.tgz#deaaccdfd14aeaf85578f733ae8210f9b4848f69" - integrity sha1-3qrM39FK6vhVePczroIQ+bSEj2k= - dependencies: - file-type "^3.8.0" - get-stream "^2.2.0" - pify "^2.3.0" - yauzl "^2.4.2" - -decompress@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.1.tgz#007f55cc6a62c055afa37c07eb6a4ee1b773f118" - integrity sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ== - dependencies: - decompress-tar "^4.0.0" - decompress-tarbz2 "^4.0.0" - decompress-targz "^4.0.0" - decompress-unzip "^4.0.1" - graceful-fs "^4.1.10" - make-dir "^1.0.0" - pify "^2.3.0" - strip-dirs "^2.0.0" - dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" @@ -12698,6 +12620,17 @@ extract-zip@^2.0.0: optionalDependencies: "@types/yauzl" "^2.9.1" +extract-zip@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -12958,21 +12891,6 @@ file-type@^10.9.0: resolved "https://registry.yarnpkg.com/file-type/-/file-type-10.9.0.tgz#f6c12c7cb9e6b8aeefd6917555fd4f9eadf31891" integrity sha512-9C5qtGR/fNibHC5gzuMmmgnjH3QDDLKMa8lYe9CiZVmAnI4aUaoMh40QyUPzzs0RYo837SOBKh7TYwle4G8E4w== -file-type@^3.8.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" - integrity sha1-JXoHg4TR24CHvESdEH1SpSZyuek= - -file-type@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6" - integrity sha1-LdvqfHP/42No365J3DOMBYwritY= - -file-type@^6.1.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919" - integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg== - file-type@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/file-type/-/file-type-9.0.0.tgz#a68d5ad07f486414dfb2c8866f73161946714a18" @@ -13778,14 +13696,6 @@ get-stream@3.0.0, get-stream@^3.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= -get-stream@^2.2.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" - integrity sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4= - dependencies: - object-assign "^4.0.1" - pinkie-promise "^2.0.0" - get-stream@^4.0.0, get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -14394,7 +14304,7 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4: +graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== @@ -16483,11 +16393,6 @@ is-native@^1.0.1: is-nil "^1.0.0" to-source-code "^1.0.0" -is-natural-number@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" - integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg= - is-negated-glob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" @@ -18099,10 +18004,10 @@ kdbush@^3.0.0: resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-3.0.0.tgz#f8484794d47004cc2d85ed3a79353dbe0abc2bf0" integrity sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew== -kea@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/kea/-/kea-2.1.1.tgz#6e3c65c4873b67d270a2ec7bf73b0d178937234c" - integrity sha512-W9o4lHLOcEDIu3ASHPrWJJJzL1bMkGyxaHn9kuaDgI96ztBShVrf52R0QPGlQ2k9ca3XnkB/dnVHio1UB8kGWA== +kea@2.2.0-rc.4: + version "2.2.0-rc.4" + resolved "https://registry.yarnpkg.com/kea/-/kea-2.2.0-rc.4.tgz#cc0376950530a6751f73387c4b25a39efa1faa77" + integrity sha512-pYuwaCiJkBvHZShi8kqhk8dC4DjeELdK51Lw7Pn0tNdJgZJDF6COhsUiF/yrh9d7woNYDxKfuxH+QWZFfo8PkA== kew@~0.1.7: version "0.1.7" @@ -19900,6 +19805,11 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp@0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -22823,14 +22733,6 @@ puid@1.0.7: resolved "https://registry.yarnpkg.com/puid/-/puid-1.0.7.tgz#fa638a737d7b20419059d93965aed36ca20e1c84" integrity sha1-+mOKc317IEGQWdk5Za7TbKIOHIQ= -pump@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" - integrity sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - pump@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" @@ -24064,7 +23966,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -"readable-stream@1 || 2", readable-stream@^2.3.0, readable-stream@^2.3.7, readable-stream@~2.3.3: +"readable-stream@1 || 2", readable-stream@^2.3.7, readable-stream@~2.3.3: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -24087,7 +23989,7 @@ readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0": isarray "0.0.1" string_decoder "~0.10.x" -"readable-stream@2 || 3", readable-stream@^3.0.1, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: +"readable-stream@2 || 3", readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== @@ -25446,13 +25348,6 @@ seedrandom@^3.0.5: resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== -seek-bzip@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4" - integrity sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ== - dependencies: - commander "^2.8.1" - select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -26824,13 +26719,6 @@ strip-bom@^4.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== -strip-dirs@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5" - integrity sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g== - dependencies: - is-natural-number "^4.0.1" - strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" @@ -27286,45 +27174,22 @@ tape@^5.0.1: string.prototype.trim "^1.2.1" through "^2.3.8" -tar-fs@^1.16.3: - version "1.16.3" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509" - integrity sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw== - dependencies: - chownr "^1.0.1" - mkdirp "^0.5.1" - pump "^1.0.0" - tar-stream "^1.1.2" - -tar-stream@^1.1.2: - version "1.5.5" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.5.5.tgz#5cad84779f45c83b1f2508d96b09d88c7218af55" - integrity sha512-mQdgLPc/Vjfr3VWqWbfxW8yQNiJCbAZ+Gf6GDu1Cy0bdb33ofyiNGBtAY96jHFhDuivCwgW1H9DgTON+INiXgg== - dependencies: - bl "^1.0.0" - end-of-stream "^1.0.0" - readable-stream "^2.0.0" - xtend "^4.0.0" - -tar-stream@^1.5.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" - integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== +tar-fs@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5" + integrity sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg== dependencies: - bl "^1.0.0" - buffer-alloc "^1.2.0" - end-of-stream "^1.0.0" - fs-constants "^1.0.0" - readable-stream "^2.3.0" - to-buffer "^1.1.1" - xtend "^4.0.0" + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.0.0" -tar-stream@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3" - integrity sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw== +tar-stream@^2.0.0, tar-stream@^2.1.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.3.tgz#1e2022559221b7866161660f118255e20fa79e41" + integrity sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA== dependencies: - bl "^3.0.0" + bl "^4.0.1" end-of-stream "^1.4.1" fs-constants "^1.0.0" inherits "^2.0.3" @@ -27748,11 +27613,6 @@ to-arraybuffer@^1.0.0: resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= -to-buffer@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" - integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== - to-camel-case@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/to-camel-case/-/to-camel-case-1.0.0.tgz#1a56054b2f9d696298ce66a60897322b6f423e46" @@ -28301,14 +28161,6 @@ uid-safe@2.1.5: dependencies: random-bytes "~1.0.0" -unbzip2-stream@^1.0.9: - version "1.4.3" - resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" - integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== - dependencies: - buffer "^5.2.1" - through "^2.3.8" - unc-path-regex@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" @@ -30598,7 +30450,7 @@ yargs@~3.10.0: decamelize "^1.0.0" window-size "0.1.0" -yauzl@2.10.0, yauzl@^2.10.0, yauzl@^2.4.2: +yauzl@2.10.0, yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=