From 46901f557b11e3e53256c44534e579712d2e18e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 23 Apr 2020 15:24:00 +0100 Subject: [PATCH 01/35] [APM] Mark an Agent configuration as applied without etag attribute. (#63967) * adding new attribute to mark config as applied * changing query parameter name * changing property name * refactoring * updating test description --- .../routes/settings/agent_configuration.ts | 17 ++++++-- .../apis/apm/agent_configuration.ts | 40 +++++++++++++++++-- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts index 8cfd736a336c2..6268f5899d7ff 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -143,11 +143,15 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ params: { body: t.intersection([ t.type({ service: serviceRt }), - t.partial({ etag: t.string }) + t.partial({ etag: t.string, mark_as_applied_by_agent: t.boolean }) ]) }, handler: async ({ context, request }) => { - const { service, etag } = context.params.body; + const { + service, + etag, + mark_as_applied_by_agent: markAsAppliedByAgent + } = context.params.body; const setup = await setupRequest(context, request); const config = await searchConfigurations({ @@ -166,9 +170,14 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ `Config was found for ${service.name}/${service.environment}` ); - // update `applied_by_agent` field if etags match + // update `applied_by_agent` field + // when `markAsAppliedByAgent` is true (Jaeger agent doesn't have etags) + // or if etags match. // this happens in the background and doesn't block the response - if (etag === config._source.etag && !config._source.applied_by_agent) { + if ( + (markAsAppliedByAgent || etag === config._source.etag) && + !config._source.applied_by_agent + ) { markAppliedByAgent({ id: config._id, body: config._source, setup }); } diff --git a/x-pack/test/api_integration/apis/apm/agent_configuration.ts b/x-pack/test/api_integration/apis/apm/agent_configuration.ts index 41d78995711f2..8af648e062cf4 100644 --- a/x-pack/test/api_integration/apis/apm/agent_configuration.ts +++ b/x-pack/test/api_integration/apis/apm/agent_configuration.ts @@ -182,15 +182,21 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte service: { name: 'myservice', environment: 'development' }, settings: { transaction_sample_rate: '0.9' }, }; + const configProduction = { + service: { name: 'myservice', environment: 'production' }, + settings: { transaction_sample_rate: '0.9' }, + }; let etag: string; before(async () => { log.debug('creating agent configuration'); await createConfiguration(config); + await createConfiguration(configProduction); }); after(async () => { await deleteConfiguration(config); + await deleteConfiguration(configProduction); }); it(`should have 'applied_by_agent=false' before supplying etag`, async () => { @@ -210,17 +216,45 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte }); it(`should have 'applied_by_agent=true' after supplying etag`, async () => { - async function getAppliedByAgent() { + await searchConfigurations({ + service: { name: 'myservice', environment: 'development' }, + etag, + }); + + async function hasBeenAppliedByAgent() { const { body } = await searchConfigurations({ service: { name: 'myservice', environment: 'development' }, - etag, }); return body._source.applied_by_agent; } // wait until `applied_by_agent` has been updated in elasticsearch - expect(await waitFor(getAppliedByAgent)).to.be(true); + expect(await waitFor(hasBeenAppliedByAgent)).to.be(true); + }); + it(`should have 'applied_by_agent=false' before marking as applied`, async () => { + const res1 = await searchConfigurations({ + service: { name: 'myservice', environment: 'production' }, + }); + + expect(res1.body._source.applied_by_agent).to.be(false); + }); + it(`should have 'applied_by_agent=true' when 'mark_as_applied_by_agent' attribute is true`, async () => { + await searchConfigurations({ + service: { name: 'myservice', environment: 'production' }, + mark_as_applied_by_agent: true, + }); + + async function hasBeenAppliedByAgent() { + const { body } = await searchConfigurations({ + service: { name: 'myservice', environment: 'production' }, + }); + + return body._source.applied_by_agent; + } + + // wait until `applied_by_agent` has been updated in elasticsearch + expect(await waitFor(hasBeenAppliedByAgent)).to.be(true); }); }); }); From f7e1c17f28128deecbc3350350098ec57850d0eb Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 23 Apr 2020 07:53:54 -0700 Subject: [PATCH 02/35] Implemented do not return the config from the action APIs get and getAll (#64266) --- .../actions/server/actions_client.test.ts | 6 --- .../plugins/actions/server/actions_client.ts | 2 - x-pack/plugins/actions/server/types.ts | 2 +- .../security_and_spaces/tests/actions/get.ts | 3 -- .../tests/actions/get_all.ts | 48 ------------------- .../spaces_only/tests/actions/get.ts | 3 -- .../spaces_only/tests/actions/get_all.ts | 32 ------------- 7 files changed, 1 insertion(+), 95 deletions(-) diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 955e1569380a5..14441bfd52dd7 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -348,9 +348,6 @@ describe('get()', () => { actionTypeId: '.slack', isPreconfigured: true, name: 'test', - config: { - foo: 'bar', - }, }); expect(savedObjectsClient.get).not.toHaveBeenCalled(); }); @@ -418,9 +415,6 @@ describe('getAll()', () => { actionTypeId: '.slack', isPreconfigured: true, name: 'test', - config: { - foo: 'bar', - }, referencedByCount: 2, }, ]); diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 8f73bfb31ea4d..f52e293296955 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -152,7 +152,6 @@ export class ActionsClient { id, actionTypeId: preconfiguredActionsList.actionTypeId, name: preconfiguredActionsList.name, - config: preconfiguredActionsList.config, isPreconfigured: true, }; } @@ -184,7 +183,6 @@ export class ActionsClient { id: preconfiguredAction.id, actionTypeId: preconfiguredAction.actionTypeId, name: preconfiguredAction.name, - config: preconfiguredAction.config, isPreconfigured: true, })), ].sort((a, b) => a.name.localeCompare(b.name)); diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 92e38d77314f8..088398b40830e 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -54,7 +54,7 @@ export interface ActionResult { id: string; actionTypeId: string; name: string; - config: Record; + config?: Record; isPreconfigured: boolean; } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts index c84b089d48c85..e43f9dba4b2dc 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts @@ -141,9 +141,6 @@ export default function getActionTests({ getService }: FtrProviderContext) { actionTypeId: '.slack', name: 'Slack#xyz', isPreconfigured: true, - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, }); break; default: diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts index 0b637326d4667..95b564e63d715 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts @@ -73,11 +73,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -85,9 +80,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 0, }, { @@ -95,11 +87,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -107,9 +94,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); @@ -194,11 +178,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -206,9 +185,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 1, }, { @@ -216,11 +192,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -228,9 +199,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); @@ -281,11 +249,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -293,9 +256,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 0, }, { @@ -303,11 +263,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -315,9 +270,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts index a4a13441fb766..4eb8c16f4fb3a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts @@ -79,9 +79,6 @@ export default function getActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, }); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts index ec59e56b08308..62abdddc6a1bf 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts @@ -50,11 +50,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -62,9 +57,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 0, }, { @@ -72,11 +64,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -84,9 +71,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); @@ -115,11 +99,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -127,9 +106,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 0, }, { @@ -137,11 +113,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -149,9 +120,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); From 3e0907fabfdc8e4c8271145acbe28497bde5c41a Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Thu, 23 Apr 2020 09:13:00 -0600 Subject: [PATCH 03/35] Use runtime publicPath for KP plugin bundles (#64226) --- .../basic_optimization.test.ts.snap | 4 +- .../src/worker/webpack.config.ts | 9 ++- packages/kbn-ui-shared-deps/index.d.ts | 5 ++ packages/kbn-ui-shared-deps/index.js | 1 + .../kbn-ui-shared-deps/public_path_loader.js | 23 ++++++ packages/kbn-ui-shared-deps/webpack.config.js | 12 ++- src/legacy/core_plugins/tests_bundle/index.js | 13 +++ .../ui/ui_render/bootstrap/template.js.hbs | 1 + src/legacy/ui/ui_render/ui_render_mixin.js | 14 ++++ src/optimize/bundles_route/bundles_route.js | 79 +++++++++++-------- .../bundles_route/dynamic_asset_response.js | 9 ++- tasks/config/karma.js | 2 + 12 files changed, 132 insertions(+), 40 deletions(-) create mode 100644 packages/kbn-ui-shared-deps/public_path_loader.js diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 4b4bb1282d939..fe0f75c05c646 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -57,6 +57,6 @@ OptimizerConfig { } `; -exports[`builds expected bundles, saves bundle counts to metadata: bar bundle 1`] = `"var __kbnBundles__=typeof __kbnBundles__===\\"object\\"?__kbnBundles__:{};__kbnBundles__[\\"plugin/bar\\"]=function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i `/${bundle.type}:${bundle.id}/${Path.relative( bundle.sourceRoot, @@ -146,6 +144,13 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { ], rules: [ + { + include: Path.join(bundle.contextDir, bundle.entry), + loader: UiSharedDeps.publicPathLoader, + options: { + key: bundle.id, + }, + }, { test: /\.css$/, include: /node_modules/, diff --git a/packages/kbn-ui-shared-deps/index.d.ts b/packages/kbn-ui-shared-deps/index.d.ts index dec519da69641..b829c87d91c4a 100644 --- a/packages/kbn-ui-shared-deps/index.d.ts +++ b/packages/kbn-ui-shared-deps/index.d.ts @@ -53,3 +53,8 @@ export const lightCssDistFilename: string; export const externals: { [key: string]: string; }; + +/** + * Webpack loader for configuring the public path lookup from `window.__kbnPublicPath__`. + */ +export const publicPathLoader: string; diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index 666ec7a46ff06..42ed08259ac8f 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -64,3 +64,4 @@ exports.externals = { 'elasticsearch-browser': '__kbnSharedDeps__.ElasticsearchBrowser', 'elasticsearch-browser/elasticsearch': '__kbnSharedDeps__.ElasticsearchBrowser', }; +exports.publicPathLoader = require.resolve('./public_path_loader'); diff --git a/packages/kbn-ui-shared-deps/public_path_loader.js b/packages/kbn-ui-shared-deps/public_path_loader.js new file mode 100644 index 0000000000000..6b7a27c9ca52b --- /dev/null +++ b/packages/kbn-ui-shared-deps/public_path_loader.js @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = function(source) { + const options = this.query; + return `__webpack_public_path__ = window.__kbnPublicPath__['${options.key}'];${source}`; +}; diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index a875274544905..bf63c57765859 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -46,7 +46,6 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ path: UiSharedDeps.distDir, filename: '[name].js', sourceMapFilename: '[file].map', - publicPath: '__REPLACE_WITH_PUBLIC_PATH__', devtoolModuleFilenameTemplate: info => `kbn-ui-shared-deps/${Path.relative(REPO_ROOT, info.absoluteResourcePath)}`, library: '__kbnSharedDeps__', @@ -55,6 +54,17 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ module: { noParse: [MOMENT_SRC], rules: [ + { + include: [require.resolve('./entry.js')], + use: [ + { + loader: UiSharedDeps.publicPathLoader, + options: { + key: 'kbn-ui-shared-deps', + }, + }, + ], + }, { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'], diff --git a/src/legacy/core_plugins/tests_bundle/index.js b/src/legacy/core_plugins/tests_bundle/index.js index 5e78047088d2a..e1966a9e8b266 100644 --- a/src/legacy/core_plugins/tests_bundle/index.js +++ b/src/legacy/core_plugins/tests_bundle/index.js @@ -148,6 +148,19 @@ export default kibana => { .type('text/css'); }, }); + + // Sets global variables normally set by the bootstrap.js script + kbnServer.server.route({ + path: '/test_bundle/karma/globals.js', + method: 'GET', + async handler(req, h) { + const basePath = config.get('server.basePath'); + + const file = `window.__kbnPublicPath__ = { 'kbn-ui-shared-deps': "${basePath}/bundles/kbn-ui-shared-deps/" };`; + + return h.response(file).header('content-type', 'application/json'); + }, + }); }, __globalImportAliases__: { diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index 7250fa4fc9eca..8a71c6ccb1506 100644 --- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs +++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs @@ -1,6 +1,7 @@ var kbnCsp = JSON.parse(document.querySelector('kbn-csp').getAttribute('data')); window.__kbnStrictCsp__ = kbnCsp.strictCsp; window.__kbnDarkMode__ = {{darkMode}}; +window.__kbnPublicPath__ = {{publicPathMap}}; if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { var legacyBrowserError = document.getElementById('kbn_legacy_browser_error'); diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 1e84405dd5153..801eecf5b608b 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -153,11 +153,25 @@ export function uiRenderMixin(kbnServer, server, config) { `${regularBundlePath}/plugin/kibanaReact/kibanaReact.plugin.js`, ]; + const uiPluginIds = [...kbnServer.newPlatform.__internals.uiPlugins.public.keys()]; + + // These paths should align with the bundle routes configured in + // src/optimize/bundles_route/bundles_route.js + const publicPathMap = JSON.stringify({ + core: `${regularBundlePath}/core/`, + 'kbn-ui-shared-deps': `${regularBundlePath}/kbn-ui-shared-deps/`, + ...uiPluginIds.reduce( + (acc, pluginId) => ({ ...acc, [pluginId]: `${regularBundlePath}/plugin/${pluginId}/` }), + {} + ), + }); + const bootstrap = new AppBootstrap({ templateData: { darkMode, jsDependencyPaths, styleSheetPaths, + publicPathMap, entryBundlePath: isCore ? `${regularBundlePath}/core/core.entry.js` : `${regularBundlePath}/${app.getId()}.bundle.js`, diff --git a/src/optimize/bundles_route/bundles_route.js b/src/optimize/bundles_route/bundles_route.js index 530dabb9a46a3..4030988c8552c 100644 --- a/src/optimize/bundles_route/bundles_route.js +++ b/src/optimize/bundles_route/bundles_route.js @@ -72,43 +72,57 @@ export function createBundlesRoute({ } return [ - buildRouteForBundles( - `${basePublicPath}/bundles/kbn-ui-shared-deps/`, - '/bundles/kbn-ui-shared-deps/', - UiSharedDeps.distDir, - fileHashCache - ), + buildRouteForBundles({ + publicPath: `${basePublicPath}/bundles/kbn-ui-shared-deps/`, + routePath: '/bundles/kbn-ui-shared-deps/', + bundlesPath: UiSharedDeps.distDir, + fileHashCache, + replacePublicPath: false, + }), ...npUiPluginPublicDirs.map(({ id, path }) => - buildRouteForBundles( - `${basePublicPath}/bundles/plugin/${id}/`, - `/bundles/plugin/${id}/`, - path, - fileHashCache - ) - ), - buildRouteForBundles( - `${basePublicPath}/bundles/core/`, - `/bundles/core/`, - fromRoot(join('src', 'core', 'target', 'public')), - fileHashCache - ), - buildRouteForBundles( - `${basePublicPath}/bundles/`, - '/bundles/', - regularBundlesPath, - fileHashCache + buildRouteForBundles({ + publicPath: `${basePublicPath}/bundles/plugin/${id}/`, + routePath: `/bundles/plugin/${id}/`, + bundlesPath: path, + fileHashCache, + replacePublicPath: false, + }) ), - buildRouteForBundles( - `${basePublicPath}/built_assets/dlls/`, - '/built_assets/dlls/', - dllBundlesPath, - fileHashCache - ), - buildRouteForBundles(`${basePublicPath}/`, '/built_assets/css/', builtCssPath, fileHashCache), + buildRouteForBundles({ + publicPath: `${basePublicPath}/bundles/core/`, + routePath: `/bundles/core/`, + bundlesPath: fromRoot(join('src', 'core', 'target', 'public')), + fileHashCache, + replacePublicPath: false, + }), + buildRouteForBundles({ + publicPath: `${basePublicPath}/bundles/`, + routePath: '/bundles/', + bundlesPath: regularBundlesPath, + fileHashCache, + }), + buildRouteForBundles({ + publicPath: `${basePublicPath}/built_assets/dlls/`, + routePath: '/built_assets/dlls/', + bundlesPath: dllBundlesPath, + fileHashCache, + }), + buildRouteForBundles({ + publicPath: `${basePublicPath}/`, + routePath: '/built_assets/css/', + bundlesPath: builtCssPath, + fileHashCache, + }), ]; } -function buildRouteForBundles(publicPath, routePath, bundlesPath, fileHashCache) { +function buildRouteForBundles({ + publicPath, + routePath, + bundlesPath, + fileHashCache, + replacePublicPath = true, +}) { return { method: 'GET', path: `${routePath}{path*}`, @@ -129,6 +143,7 @@ function buildRouteForBundles(publicPath, routePath, bundlesPath, fileHashCache) bundlesPath, fileHashCache, publicPath, + replacePublicPath, }); }, }, diff --git a/src/optimize/bundles_route/dynamic_asset_response.js b/src/optimize/bundles_route/dynamic_asset_response.js index 7af780a79e430..80c49a26270fd 100644 --- a/src/optimize/bundles_route/dynamic_asset_response.js +++ b/src/optimize/bundles_route/dynamic_asset_response.js @@ -52,7 +52,7 @@ import { replacePlaceholder } from '../public_path_placeholder'; * @property {LruCache} options.fileHashCache */ export async function createDynamicAssetResponse(options) { - const { request, h, bundlesPath, publicPath, fileHashCache } = options; + const { request, h, bundlesPath, publicPath, fileHashCache, replacePublicPath } = options; let fd; try { @@ -78,11 +78,14 @@ export async function createDynamicAssetResponse(options) { }); fd = null; // read stream is now responsible for fd + const content = replacePublicPath ? replacePlaceholder(read, publicPath) : read; + const etag = replacePublicPath ? `${hash}-${publicPath}` : hash; + return h - .response(replacePlaceholder(read, publicPath)) + .response(content) .takeover() .code(200) - .etag(`${hash}-${publicPath}`) + .etag(etag) .header('cache-control', 'must-revalidate') .type(request.server.mime.path(path).type); } catch (error) { diff --git a/tasks/config/karma.js b/tasks/config/karma.js index 4e106ef3e039a..1ec7c831b4864 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -53,6 +53,8 @@ module.exports = function(grunt) { function getKarmaFiles(shardNum) { return [ 'http://localhost:5610/test_bundle/built_css.css', + // Sets global variables normally set by the bootstrap.js script + 'http://localhost:5610/test_bundle/karma/globals.js', ...UiSharedDeps.jsDepFilenames.map( chunkFilename => `http://localhost:5610/bundles/kbn-ui-shared-deps/${chunkFilename}` From bc6291349c710ec9332c4fb84d3366ed92824650 Mon Sep 17 00:00:00 2001 From: Nick Partridge Date: Thu, 23 Apr 2020 11:59:19 -0500 Subject: [PATCH 04/35] Update elastic charts v18.4.1 (#64257) --- package.json | 2 +- packages/kbn-ui-shared-deps/package.json | 2 +- .../lens/public/xy_visualization/xy_expression.test.tsx | 2 +- yarn.lock | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index a7729b6dab7a1..9bb308d4cdcf1 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "@babel/core": "^7.9.0", "@babel/register": "^7.9.0", "@elastic/apm-rum": "^5.1.1", - "@elastic/charts": "18.3.0", + "@elastic/charts": "18.4.1", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.8.0", "@elastic/eui": "22.3.0", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 3aa01179c00f3..46f55da87575d 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --watch" }, "dependencies": { - "@elastic/charts": "18.3.0", + "@elastic/charts": "18.4.1", "@elastic/eui": "22.3.0", "@kbn/i18n": "1.0.0", "abortcontroller-polyfill": "^1.4.0", diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx index 80d33d1b95b61..e75e5fe763d6a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx @@ -439,7 +439,7 @@ describe('xy_expression', () => { }); test('onElementClick returns correct context data', () => { - const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1' }; + const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1', mark: null }; const series = { key: 'spec{d}yAccessor{d}splitAccessors{b-2}', specId: 'd', diff --git a/yarn.lock b/yarn.lock index 30747ee555fe2..79d3911919693 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1236,10 +1236,10 @@ dependencies: "@elastic/apm-rum-core" "^5.2.0" -"@elastic/charts@18.3.0": - version "18.3.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.3.0.tgz#cbdeec1860af274edc7a5f5b9dd26ec48c64bb64" - integrity sha512-4kSlSwdDRsVKVX8vRUkwxOu1IT6WIepgLnP0OZT7cFjgrC1SV/16c3YLw2NZDaVe0M/H4rpeNWW30VyrzZVhyw== +"@elastic/charts@18.4.1": + version "18.4.1" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.4.1.tgz#19d82c39aef347fd00b33e33b68b683ac4d745b0" + integrity sha512-vV5AAKIKbwgY923OD2Rsr77XFHmsUsWWg/aeCZvG5/b6Yb+fNgM0RF94GADiDMvRvQANhTn2CPPVvNfL18MegQ== dependencies: classnames "^2.2.6" d3-array "^1.2.4" From 09c2727d786ed91a79250c58842c9dbaf53f382d Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Thu, 23 Apr 2020 18:06:33 +0100 Subject: [PATCH 05/35] [ML] Adds API integration tests for data viz and fields endpoints (#64165) * [ML] Adds API integration tests for data viz and fields endpoints * [ML] Fix review comments and errors from settings endpoints Co-authored-by: Elastic Machine --- .../models/calendar/calendar_manager.ts | 175 ++++---- .../server/models/calendar/event_manager.ts | 36 +- .../ml/server/routes/anomaly_detectors.ts | 4 +- x-pack/plugins/ml/server/routes/apidoc.json | 2 +- .../ml/server/routes/data_visualizer.ts | 12 +- .../ml/server/routes/fields_service.ts | 7 +- .../ml/server/routes/job_audit_messages.ts | 9 +- .../plugins/ml/server/routes/job_service.ts | 7 +- x-pack/plugins/ml/server/routes/modules.ts | 8 +- .../schemas/anomaly_detectors_schema.ts | 2 +- .../routes/schemas/data_visualizer_schema.ts | 15 + .../routes/schemas/fields_service_schema.ts | 9 + .../schemas/job_audit_messages_schema.ts | 5 +- .../routes/schemas/job_service_schema.ts | 1 + .../apis/ml/anomaly_detectors/create.ts | 145 +++++++ .../apis/ml/anomaly_detectors/index.ts | 12 + .../ml/data_visualizer/get_field_stats.ts | 248 ++++++++++++ .../ml/data_visualizer/get_overall_stats.ts | 154 ++++++++ .../apis/ml/data_visualizer/index.ts | 13 + .../ml/fields_service/field_cardinality.ts | 115 ++++++ .../apis/ml/fields_service/index.ts | 13 + .../ml/fields_service/time_field_range.ts | 119 ++++++ x-pack/test/api_integration/apis/ml/index.ts | 12 +- .../bucket_span_estimator.ts | 4 +- .../calculate_model_memory_limit.ts | 4 +- .../apis/ml/job_validation/index.ts | 13 + .../categorization_field_examples.ts | 4 +- .../api_integration/apis/ml/jobs/index.ts | 13 + .../apis/ml/jobs/jobs_summary.ts | 374 ++++++++++++++++++ .../apis/ml/{ => modules}/get_module.ts | 4 +- .../api_integration/apis/ml/modules/index.ts | 14 + .../apis/ml/{ => modules}/recognize_module.ts | 4 +- .../apis/ml/{ => modules}/setup_module.ts | 6 +- 33 files changed, 1404 insertions(+), 159 deletions(-) create mode 100644 x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts create mode 100644 x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts create mode 100644 x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts create mode 100644 x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts create mode 100644 x-pack/test/api_integration/apis/ml/data_visualizer/index.ts create mode 100644 x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts create mode 100644 x-pack/test/api_integration/apis/ml/fields_service/index.ts create mode 100644 x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts rename x-pack/test/api_integration/apis/ml/{ => job_validation}/bucket_span_estimator.ts (97%) rename x-pack/test/api_integration/apis/ml/{ => job_validation}/calculate_model_memory_limit.ts (97%) create mode 100644 x-pack/test/api_integration/apis/ml/job_validation/index.ts rename x-pack/test/api_integration/apis/ml/{ => jobs}/categorization_field_examples.ts (98%) create mode 100644 x-pack/test/api_integration/apis/ml/jobs/index.ts create mode 100644 x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts rename x-pack/test/api_integration/apis/ml/{ => modules}/get_module.ts (92%) create mode 100644 x-pack/test/api_integration/apis/ml/modules/index.ts rename x-pack/test/api_integration/apis/ml/{ => modules}/recognize_module.ts (93%) rename x-pack/test/api_integration/apis/ml/{ => modules}/setup_module.ts (96%) diff --git a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts index 64d750f511f3a..581770e59043f 100644 --- a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts @@ -5,7 +5,6 @@ */ import { difference } from 'lodash'; -import Boom from 'boom'; import { IScopedClusterClient } from 'kibana/server'; import { EventManager, CalendarEvent } from './event_manager'; @@ -33,43 +32,31 @@ export class CalendarManager { } async getCalendar(calendarId: string) { - try { - const resp = await this._client('ml.calendars', { - calendarId, - }); - - const calendars = resp.calendars; - if (calendars.length) { - const calendar = calendars[0]; - calendar.events = await this._eventManager.getCalendarEvents(calendarId); - return calendar; - } else { - throw Boom.notFound(`Calendar with the id "${calendarId}" not found`); - } - } catch (error) { - throw Boom.badRequest(error); - } + const resp = await this._client('ml.calendars', { + calendarId, + }); + + const calendars = resp.calendars; + const calendar = calendars[0]; // Endpoint throws a 404 if calendar is not found. + calendar.events = await this._eventManager.getCalendarEvents(calendarId); + return calendar; } async getAllCalendars() { - try { - const calendarsResp = await this._client('ml.calendars'); - - const events: CalendarEvent[] = await this._eventManager.getAllEvents(); - const calendars: Calendar[] = calendarsResp.calendars; - calendars.forEach(cal => (cal.events = [])); - - // loop events and combine with related calendars - events.forEach(event => { - const calendar = calendars.find(cal => cal.calendar_id === event.calendar_id); - if (calendar) { - calendar.events.push(event); - } - }); - return calendars; - } catch (error) { - throw Boom.badRequest(error); - } + const calendarsResp = await this._client('ml.calendars'); + + const events: CalendarEvent[] = await this._eventManager.getAllEvents(); + const calendars: Calendar[] = calendarsResp.calendars; + calendars.forEach(cal => (cal.events = [])); + + // loop events and combine with related calendars + events.forEach(event => { + const calendar = calendars.find(cal => cal.calendar_id === event.calendar_id); + if (calendar) { + calendar.events.push(event); + } + }); + return calendars; } /** @@ -78,12 +65,8 @@ export class CalendarManager { * @returns {Promise<*>} */ async getCalendarsByIds(calendarIds: string) { - try { - const calendars: Calendar[] = await this.getAllCalendars(); - return calendars.filter(calendar => calendarIds.includes(calendar.calendar_id)); - } catch (error) { - throw Boom.badRequest(error); - } + const calendars: Calendar[] = await this.getAllCalendars(); + return calendars.filter(calendar => calendarIds.includes(calendar.calendar_id)); } async newCalendar(calendar: FormCalendar) { @@ -91,75 +74,67 @@ export class CalendarManager { const events = calendar.events; delete calendar.calendarId; delete calendar.events; - try { - await this._client('ml.addCalendar', { - calendarId, - body: calendar, - }); - - if (events.length) { - await this._eventManager.addEvents(calendarId, events); - } + await this._client('ml.addCalendar', { + calendarId, + body: calendar, + }); - // return the newly created calendar - return await this.getCalendar(calendarId); - } catch (error) { - throw Boom.badRequest(error); + if (events.length) { + await this._eventManager.addEvents(calendarId, events); } + + // return the newly created calendar + return await this.getCalendar(calendarId); } async updateCalendar(calendarId: string, calendar: Calendar) { const origCalendar: Calendar = await this.getCalendar(calendarId); - try { - // update job_ids - const jobsToAdd = difference(calendar.job_ids, origCalendar.job_ids); - const jobsToRemove = difference(origCalendar.job_ids, calendar.job_ids); - - // workout the differences between the original events list and the new one - // if an event has no event_id, it must be new - const eventsToAdd = calendar.events.filter( - event => origCalendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined - ); - - // if an event in the original calendar cannot be found, it must have been deleted - const eventsToRemove: CalendarEvent[] = origCalendar.events.filter( - event => calendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined - ); - - // note, both of the loops below could be removed if the add and delete endpoints - // allowed multiple job_ids - - // add all new jobs - if (jobsToAdd.length) { - await this._client('ml.addJobToCalendar', { - calendarId, - jobId: jobsToAdd.join(','), - }); - } - - // remove all removed jobs - if (jobsToRemove.length) { - await this._client('ml.removeJobFromCalendar', { - calendarId, - jobId: jobsToRemove.join(','), - }); - } + // update job_ids + const jobsToAdd = difference(calendar.job_ids, origCalendar.job_ids); + const jobsToRemove = difference(origCalendar.job_ids, calendar.job_ids); + + // workout the differences between the original events list and the new one + // if an event has no event_id, it must be new + const eventsToAdd = calendar.events.filter( + event => origCalendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined + ); + + // if an event in the original calendar cannot be found, it must have been deleted + const eventsToRemove: CalendarEvent[] = origCalendar.events.filter( + event => calendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined + ); + + // note, both of the loops below could be removed if the add and delete endpoints + // allowed multiple job_ids + + // add all new jobs + if (jobsToAdd.length) { + await this._client('ml.addJobToCalendar', { + calendarId, + jobId: jobsToAdd.join(','), + }); + } - // add all new events - if (eventsToAdd.length !== 0) { - await this._eventManager.addEvents(calendarId, eventsToAdd); - } + // remove all removed jobs + if (jobsToRemove.length) { + await this._client('ml.removeJobFromCalendar', { + calendarId, + jobId: jobsToRemove.join(','), + }); + } - // remove all removed events - await Promise.all( - eventsToRemove.map(async event => { - await this._eventManager.deleteEvent(calendarId, event.event_id); - }) - ); - } catch (error) { - throw Boom.badRequest(error); + // add all new events + if (eventsToAdd.length !== 0) { + await this._eventManager.addEvents(calendarId, eventsToAdd); } + // remove all removed events + await Promise.all( + eventsToRemove.map(async event => { + await this._eventManager.deleteEvent(calendarId, event.event_id); + }) + ); + // return the updated calendar return await this.getCalendar(calendarId); } diff --git a/x-pack/plugins/ml/server/models/calendar/event_manager.ts b/x-pack/plugins/ml/server/models/calendar/event_manager.ts index 488839f68b3fe..41240e2695f6f 100644 --- a/x-pack/plugins/ml/server/models/calendar/event_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/event_manager.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; - import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; export interface CalendarEvent { @@ -23,41 +21,29 @@ export class EventManager { } async getCalendarEvents(calendarId: string) { - try { - const resp = await this._client('ml.events', { calendarId }); + const resp = await this._client('ml.events', { calendarId }); - return resp.events; - } catch (error) { - throw Boom.badRequest(error); - } + return resp.events; } // jobId is optional async getAllEvents(jobId?: string) { const calendarId = GLOBAL_CALENDAR; - try { - const resp = await this._client('ml.events', { - calendarId, - jobId, - }); + const resp = await this._client('ml.events', { + calendarId, + jobId, + }); - return resp.events; - } catch (error) { - throw Boom.badRequest(error); - } + return resp.events; } async addEvents(calendarId: string, events: CalendarEvent[]) { const body = { events }; - try { - return await this._client('ml.addEvent', { - calendarId, - body, - }); - } catch (error) { - throw Boom.badRequest(error); - } + return await this._client('ml.addEvent', { + calendarId, + body, + }); } async deleteEvent(calendarId: string, eventId: string) { diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index a675eb58dc792..ca63d69f403f6 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -138,12 +138,14 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup AnomalyDetectors * - * @api {put} /api/ml/anomaly_detectors/:jobId Instantiate an anomaly detection job + * @api {put} /api/ml/anomaly_detectors/:jobId Create an anomaly detection job * @apiName CreateAnomalyDetectors * @apiDescription Creates an anomaly detection job. * * @apiSchema (params) jobIdSchema * @apiSchema (body) anomalyDetectionJobSchema + * + * @apiSuccess {Object} job the configuration of the job that has been created. */ router.put( { diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 4848de6db7049..555053089cb95 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -49,7 +49,7 @@ "GetCategoryExamples", "GetPartitionFieldsValues", - "DataRecognizer", + "Modules", "RecognizeIndex", "GetModule", "SetupModule", diff --git a/x-pack/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts index a4c0d5553a4b2..20029fbd8d1a6 100644 --- a/x-pack/plugins/ml/server/routes/data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts @@ -74,10 +74,12 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) * * @api {post} /api/ml/data_visualizer/get_field_stats/:indexPatternTitle Get stats for fields * @apiName GetStatsForFields - * @apiDescription Returns fields stats of the index pattern. + * @apiDescription Returns the stats on individual fields in the specified index pattern. * * @apiSchema (params) indexPatternTitleSchema * @apiSchema (body) dataVisualizerFieldStatsSchema + * + * @apiSuccess {Object} fieldName stats by field, keyed on the name of the field. */ router.post( { @@ -130,10 +132,16 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) * * @api {post} /api/ml/data_visualizer/get_overall_stats/:indexPatternTitle Get overall stats * @apiName GetOverallStats - * @apiDescription Returns overall stats of the index pattern. + * @apiDescription Returns the top level overall stats for the specified index pattern. * * @apiSchema (params) indexPatternTitleSchema * @apiSchema (body) dataVisualizerOverallStatsSchema + * + * @apiSuccess {number} totalCount total count of documents. + * @apiSuccess {Object} aggregatableExistsFields stats on aggregatable fields that exist in documents. + * @apiSuccess {Object} aggregatableNotExistsFields stats on aggregatable fields that do not exist in documents. + * @apiSuccess {Object} nonAggregatableExistsFields stats on non-aggregatable fields that exist in documents. + * @apiSuccess {Object} nonAggregatableNotExistsFields stats on non-aggregatable fields that do not exist in documents. */ router.post( { diff --git a/x-pack/plugins/ml/server/routes/fields_service.ts b/x-pack/plugins/ml/server/routes/fields_service.ts index 9a5f47409c8a0..577e8e0161342 100644 --- a/x-pack/plugins/ml/server/routes/fields_service.ts +++ b/x-pack/plugins/ml/server/routes/fields_service.ts @@ -37,6 +37,8 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { * @apiDescription Returns the cardinality of one or more fields. Returns an Object whose keys are the names of the fields, with values equal to the cardinality of the field * * @apiSchema (body) getCardinalityOfFieldsSchema + * + * @apiSuccess {number} fieldName cardinality of the field. */ router.post( { @@ -64,9 +66,12 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { * * @api {post} /api/ml/fields_service/time_field_range Get time field range * @apiName GetTimeFieldRange - * @apiDescription Returns the timefield range for the given index + * @apiDescription Returns the time range for the given index and query using the specified time range. * * @apiSchema (body) getTimeFieldRangeSchema + * + * @apiSuccess {Object} start start of time range with epoch and string properties. + * @apiSuccess {Object} end end of time range with epoch and string properties. */ router.post( { diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts index 71499748691f6..1fe5a7af95d4f 100644 --- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -7,7 +7,10 @@ import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { jobAuditMessagesProvider } from '../models/job_audit_messages'; -import { jobAuditMessagesQuerySchema, jobIdSchema } from './schemas/job_audit_messages_schema'; +import { + jobAuditMessagesQuerySchema, + jobAuditMessagesJobIdSchema, +} from './schemas/job_audit_messages_schema'; /** * Routes for job audit message routes @@ -20,14 +23,14 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio * @apiName GetJobAuditMessages * @apiDescription Returns audit messages for specified job ID * - * @apiSchema (params) jobIdSchema + * @apiSchema (params) jobAuditMessagesJobIdSchema * @apiSchema (query) jobAuditMessagesQuerySchema */ router.get( { path: '/api/ml/job_audit_messages/messages/{jobId}', validate: { - params: jobIdSchema, + params: jobAuditMessagesJobIdSchema, query: jobAuditMessagesQuerySchema, }, }, diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index 493974cbafe36..cf973a914391c 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -176,9 +176,14 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * * @api {post} /api/ml/jobs/jobs_summary Jobs summary * @apiName JobsSummary - * @apiDescription Creates a summary jobs list. Jobs include job stats, datafeed stats, and calendars. + * @apiDescription Returns a list of anomaly detection jobs, with summary level information for every job. + * For any supplied job IDs, full job information will be returned, which include the analysis configuration, + * job stats, datafeed stats, and calendars. * * @apiSchema (body) jobIdsSchema + * + * @apiSuccess {Array} jobsList list of jobs. For any supplied job IDs, the job object will contain a fullJob property + * which includes the full configuration and stats for the job. */ router.post( { diff --git a/x-pack/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts index 2d462b6dc207a..2891144fc4574 100644 --- a/x-pack/plugins/ml/server/routes/modules.ts +++ b/x-pack/plugins/ml/server/routes/modules.ts @@ -81,7 +81,7 @@ function dataRecognizerJobsExist(context: RequestHandlerContext, moduleId: strin */ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { /** - * @apiGroup DataRecognizer + * @apiGroup Modules * * @api {get} /api/ml/modules/recognize/:indexPatternTitle Recognize index pattern * @apiName RecognizeIndex @@ -111,7 +111,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { ); /** - * @apiGroup DataRecognizer + * @apiGroup Modules * * @api {get} /api/ml/modules/get_module/:moduleId Get module * @apiName GetModule @@ -146,7 +146,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { ); /** - * @apiGroup DataRecognizer + * @apiGroup Modules * * @api {post} /api/ml/modules/setup/:moduleId Setup module * @apiName SetupModule @@ -204,7 +204,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { ); /** - * @apiGroup DataRecognizer + * @apiGroup Modules * * @api {post} /api/ml/modules/jobs_exist/:moduleId Check if module jobs exist * @apiName CheckExistingModuleJobs diff --git a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts index ab1305d9bc354..9b86e3e06096e 100644 --- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts @@ -119,7 +119,7 @@ export const anomalyDetectionJobSchema = { }; export const jobIdSchema = schema.object({ - /** Job id */ + /** Job ID. */ jobId: schema.string(), }); diff --git a/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts index 1a1d02f991b55..b2d665954bd4d 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts @@ -7,26 +7,41 @@ import { schema } from '@kbn/config-schema'; export const indexPatternTitleSchema = schema.object({ + /** Title of the index pattern for which to return stats. */ indexPatternTitle: schema.string(), }); export const dataVisualizerFieldStatsSchema = schema.object({ + /** Query to match documents in the index. */ query: schema.any(), fields: schema.arrayOf(schema.any()), + /** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */ samplerShardSize: schema.number(), + /** Name of the time field in the index (optional). */ timeFieldName: schema.maybe(schema.string()), + /** Earliest timestamp for search, as epoch ms (optional). */ earliest: schema.maybe(schema.number()), + /** Latest timestamp for search, as epoch ms (optional). */ latest: schema.maybe(schema.number()), + /** Aggregation interval to use for obtaining document counts over time (optional). */ interval: schema.maybe(schema.string()), + /** Maximum number of examples to return for text type fields. */ maxExamples: schema.number(), }); export const dataVisualizerOverallStatsSchema = schema.object({ + /** Query to match documents in the index. */ query: schema.any(), + /** Names of aggregatable fields for which to return stats. */ aggregatableFields: schema.arrayOf(schema.string()), + /** Names of non-aggregatable fields for which to return stats. */ nonAggregatableFields: schema.arrayOf(schema.string()), + /** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */ samplerShardSize: schema.number(), + /** Name of the time field in the index (optional). */ timeFieldName: schema.maybe(schema.string()), + /** Earliest timestamp for search, as epoch ms (optional). */ earliest: schema.maybe(schema.number()), + /** Latest timestamp for search, as epoch ms (optional). */ latest: schema.maybe(schema.number()), }); diff --git a/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts index e0fba498e0d58..ba397e0084e27 100644 --- a/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts @@ -7,16 +7,25 @@ import { schema } from '@kbn/config-schema'; export const getCardinalityOfFieldsSchema = schema.object({ + /** Index or indexes for which to return the time range. */ index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), + /** Name(s) of the field(s) to return cardinality information. */ fieldNames: schema.maybe(schema.arrayOf(schema.string())), + /** Query to match documents in the index(es) (optional). */ query: schema.maybe(schema.any()), + /** Name of the time field in the index. */ timeFieldName: schema.maybe(schema.string()), + /** Earliest timestamp for search, as epoch ms (optional). */ earliestMs: schema.maybe(schema.oneOf([schema.number(), schema.string()])), + /** Latest timestamp for search, as epoch ms (optional). */ latestMs: schema.maybe(schema.oneOf([schema.number(), schema.string()])), }); export const getTimeFieldRangeSchema = schema.object({ + /** Index or indexes for which to return the time range. */ index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), + /** Name of the time field in the index. */ timeFieldName: schema.maybe(schema.string()), + /** Query to match documents in the index(es). */ query: schema.maybe(schema.any()), }); diff --git a/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts index b94a004384eb1..ac489b3a6ce6f 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts @@ -6,7 +6,10 @@ import { schema } from '@kbn/config-schema'; -export const jobIdSchema = schema.object({ jobId: schema.maybe(schema.string()) }); +export const jobAuditMessagesJobIdSchema = schema.object({ + /** Job ID. */ + jobId: schema.maybe(schema.string()), +}); export const jobAuditMessagesQuerySchema = schema.maybe( schema.object({ from: schema.maybe(schema.any()) }) diff --git a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts index d2036b8a7c0fa..1ca1e5287e9d0 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts @@ -40,6 +40,7 @@ export const forceStartDatafeedSchema = schema.object({ }); export const jobIdsSchema = schema.object({ + /** Optional list of job ID(s). */ jobIds: schema.maybe( schema.oneOf([schema.string(), schema.arrayOf(schema.maybe(schema.string()))]) ), diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts new file mode 100644 index 0000000000000..bbc766df34dcf --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts @@ -0,0 +1,145 @@ +/* + * 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 '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const jobId = `fq_single_${Date.now()}`; + + const testDataList = [ + { + testTitle: 'ML Poweruser creates a single metric job', + user: USER.ML_POWERUSER, + jobId: `${jobId}_1`, + requestBody: { + job_id: `${jobId}_1`, + description: + 'Single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)', + groups: ['automated', 'farequote', 'single-metric'], + analysis_config: { + bucket_span: '30m', + detectors: [{ function: 'mean', field_name: 'responsetime' }], + influencers: [], + summary_count_field_name: 'doc_count', + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '11MB' }, + model_plot_config: { enabled: true }, + }, + expected: { + responseCode: 200, + responseBody: { + job_id: `${jobId}_1`, + job_type: 'anomaly_detector', + groups: ['automated', 'farequote', 'single-metric'], + description: + 'Single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)', + analysis_config: { + bucket_span: '30m', + summary_count_field_name: 'doc_count', + detectors: [ + { + detector_description: 'mean(responsetime)', + function: 'mean', + field_name: 'responsetime', + detector_index: 0, + }, + ], + influencers: [], + }, + analysis_limits: { model_memory_limit: '11mb', categorization_examples_limit: 4 }, + data_description: { time_field: '@timestamp', time_format: 'epoch_ms' }, + model_plot_config: { enabled: true }, + model_snapshot_retention_days: 1, + results_index_name: 'shared', + allow_lazy_open: false, + }, + }, + }, + { + testTitle: 'ML viewer cannot create a job', + user: USER.ML_VIEWER, + jobId: `${jobId}_2`, + requestBody: { + job_id: `${jobId}_2`, + description: + 'Single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)', + groups: ['automated', 'farequote', 'single-metric'], + analysis_config: { + bucket_span: '30m', + detectors: [{ function: 'mean', field_name: 'responsetime' }], + influencers: [], + summary_count_field_name: 'doc_count', + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '11MB' }, + model_plot_config: { enabled: true }, + }, + expected: { + responseCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + message: + '[security_exception] action [cluster:admin/xpack/ml/job/put] is unauthorized for user [ml_viewer]', + }, + }, + }, + ]; + + describe('create', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const { body } = await supertest + .put(`/api/ml/anomaly_detectors/${testData.jobId}`) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) + .set(COMMON_HEADERS) + .send(testData.requestBody) + .expect(testData.expected.responseCode); + + if (body.error === undefined) { + // Validate the important parts of the response. + const expectedResponse = testData.expected.responseBody; + expect(body.job_id).to.eql(expectedResponse.job_id); + expect(body.groups).to.eql(expectedResponse.groups); + expect(body.analysis_config!.bucket_span).to.eql( + expectedResponse.analysis_config!.bucket_span + ); + expect(body.analysis_config.detectors).to.have.length( + expectedResponse.analysis_config!.detectors.length + ); + expect(body.analysis_config.detectors[0]).to.eql( + expectedResponse.analysis_config!.detectors[0] + ); + } else { + expect(body.error).to.eql(testData.expected.responseBody.error); + expect(body.message).to.eql(testData.expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts new file mode 100644 index 0000000000000..fb8acaf5c3ae9 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('anomaly detectors', function() { + loadTestFile(require.resolve('./create')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts new file mode 100644 index 0000000000000..dfa81b5d78c65 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts @@ -0,0 +1,248 @@ +/* + * 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 '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const metricFieldsTestData = { + testTitle: 'returns stats for metric fields over all time', + index: 'ft_farequote', + user: USER.ML_POWERUSER, + requestBody: { + query: { + bool: { + must: { + term: { airline: 'JZA' }, // Only use one airline to ensure no sampling. + }, + }, + }, + fields: [ + { type: 'number', cardinality: 0 }, + { fieldName: 'responsetime', type: 'number', cardinality: 4249 }, + ], + samplerShardSize: -1, // No sampling, as otherwise counts could vary on each run. + timeFieldName: '@timestamp', + interval: '1d', + maxExamples: 10, + }, + expected: { + responseCode: 200, + responseBody: [ + { + documentCounts: { + interval: '1d', + buckets: { + '1454803200000': 846, + '1454889600000': 846, + '1454976000000': 859, + '1455062400000': 851, + '1455148800000': 858, + }, + }, + }, + { + // Cannot verify median and percentiles responses as the ES percentiles agg is non-deterministic. + fieldName: 'responsetime', + count: 4260, + min: 963.4293212890625, + max: 1042.13525390625, + avg: 1000.0378077547315, + isTopValuesSampled: false, + topValues: [ + { key: 980.0411987304688, doc_count: 2 }, + { key: 989.278076171875, doc_count: 2 }, + { key: 989.763916015625, doc_count: 2 }, + { key: 991.290771484375, doc_count: 2 }, + { key: 992.0765991210938, doc_count: 2 }, + { key: 993.8115844726562, doc_count: 2 }, + { key: 993.8973999023438, doc_count: 2 }, + { key: 994.0230102539062, doc_count: 2 }, + { key: 994.364990234375, doc_count: 2 }, + { key: 994.916015625, doc_count: 2 }, + ], + topValuesSampleSize: 4260, + topValuesSamplerShardSize: -1, + }, + ], + }, + }; + + const nonMetricFieldsTestData = { + testTitle: 'returns stats for non-metric fields specifying query and time range', + index: 'ft_farequote', + user: USER.ML_POWERUSER, + requestBody: { + query: { + bool: { + must: { + term: { airline: 'AAL' }, + }, + }, + }, + fields: [ + { fieldName: '@timestamp', type: 'date', cardinality: 4751 }, + { fieldName: '@version.keyword', type: 'keyword', cardinality: 1 }, + { fieldName: 'airline', type: 'keyword', cardinality: 19 }, + { fieldName: 'type', type: 'text', cardinality: 0 }, + { fieldName: 'type.keyword', type: 'keyword', cardinality: 1 }, + ], + samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run. + timeFieldName: '@timestamp', + earliest: 1454889600000, // February 8, 2016 12:00:00 AM GMT + latest: 1454976000000, // February 9, 2016 12:00:00 AM GMT + maxExamples: 10, + }, + expected: { + responseCode: 200, + responseBody: [ + { fieldName: '@timestamp', count: 1733, earliest: 1454889602000, latest: 1454975948000 }, + { + fieldName: '@version.keyword', + isTopValuesSampled: false, + topValues: [{ key: '1', doc_count: 1733 }], + topValuesSampleSize: 1733, + topValuesSamplerShardSize: -1, + }, + { + fieldName: 'airline', + isTopValuesSampled: false, + topValues: [{ key: 'AAL', doc_count: 1733 }], + topValuesSampleSize: 1733, + topValuesSamplerShardSize: -1, + }, + { + fieldName: 'type.keyword', + isTopValuesSampled: false, + topValues: [{ key: 'farequote', doc_count: 1733 }], + topValuesSampleSize: 1733, + topValuesSamplerShardSize: -1, + }, + { fieldName: 'type', examples: ['farequote'] }, + ], + }, + }; + + const errorTestData = { + testTitle: 'returns error for index which does not exist', + index: 'ft_farequote_not_exists', + user: USER.ML_POWERUSER, + requestBody: { + query: { bool: { must: [{ match_all: {} }] } }, + fields: [ + { type: 'number', cardinality: 0 }, + { fieldName: 'responsetime', type: 'number', cardinality: 4249 }, + ], + samplerShardSize: -1, // No sampling, as otherwise counts could vary on each run. + timeFieldName: '@timestamp', + maxExamples: 10, + }, + expected: { + responseCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: + '[index_not_found_exception] no such index [ft_farequote_not_exists], with { resource.type="index_or_alias" & resource.id="ft_farequote_not_exists" & index_uuid="_na_" & index="ft_farequote_not_exists" }', + }, + }, + }; + + async function runGetFieldStatsRequest( + index: string, + user: USER, + requestBody: object, + expectedResponsecode: number + ): Promise { + const { body } = await supertest + .post(`/api/ml/data_visualizer/get_field_stats/${index}`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_HEADERS) + .send(requestBody) + .expect(expectedResponsecode); + + return body; + } + + function compareByFieldName(a: { fieldName: string }, b: { fieldName: string }) { + if (a.fieldName < b.fieldName) { + return -1; + } + if (a.fieldName > b.fieldName) { + return 1; + } + return 0; + } + + describe('get_field_stats', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + it(`${metricFieldsTestData.testTitle}`, async () => { + const body = await runGetFieldStatsRequest( + metricFieldsTestData.index, + metricFieldsTestData.user, + metricFieldsTestData.requestBody, + metricFieldsTestData.expected.responseCode + ); + + // Cannot verify median and percentiles responses as the ES percentiles agg is non-deterministic. + const expected = metricFieldsTestData.expected; + expect(body).to.have.length(expected.responseBody.length); + + const actualDocCounts = body[0]; + const expectedDocCounts = expected.responseBody[0]; + expect(actualDocCounts).to.eql(expectedDocCounts); + + const actualFieldData = { ...body[1] }; + delete actualFieldData.median; + delete actualFieldData.distribution; + + expect(actualFieldData).to.eql(expected.responseBody[1]); + }); + + it(`${nonMetricFieldsTestData.testTitle}`, async () => { + const body = await runGetFieldStatsRequest( + nonMetricFieldsTestData.index, + nonMetricFieldsTestData.user, + nonMetricFieldsTestData.requestBody, + nonMetricFieldsTestData.expected.responseCode + ); + + // Sort the fields in the response before validating. + const expectedRspFields = nonMetricFieldsTestData.expected.responseBody.sort( + compareByFieldName + ); + const actualRspFields = body.sort(compareByFieldName); + expect(actualRspFields).to.eql(expectedRspFields); + }); + + it(`${errorTestData.testTitle}`, async () => { + const body = await runGetFieldStatsRequest( + errorTestData.index, + errorTestData.user, + errorTestData.requestBody, + errorTestData.expected.responseCode + ); + + expect(body.error).to.eql(errorTestData.expected.responseBody.error); + expect(body.message).to.eql(errorTestData.expected.responseBody.message); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts new file mode 100644 index 0000000000000..6490c19c64483 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts @@ -0,0 +1,154 @@ +/* + * 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 '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataList = [ + { + testTitle: 'returns stats over all time', + index: 'ft_farequote', + user: USER.ML_POWERUSER, + requestBody: { + query: { bool: { must: [{ match_all: {} }] } }, + aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'], + nonAggregatableFields: ['type'], + samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run. + timeFieldName: '@timestamp', + }, + expected: { + responseCode: 200, + responseBody: { + totalCount: 86274, + aggregatableExistsFields: [ + { + fieldName: '@timestamp', + existsInDocs: true, + stats: { sampleCount: 86274, count: 86274, cardinality: 78580 }, + }, + { + fieldName: 'airline', + existsInDocs: true, + stats: { sampleCount: 86274, count: 86274, cardinality: 19 }, + }, + { + fieldName: 'responsetime', + existsInDocs: true, + stats: { sampleCount: 86274, count: 86274, cardinality: 83346 }, + }, + ], + aggregatableNotExistsFields: [{ fieldName: 'sourcetype', existsInDocs: false }], + nonAggregatableExistsFields: [{ fieldName: 'type', existsInDocs: true, stats: {} }], + nonAggregatableNotExistsFields: [], + }, + }, + }, + { + testTitle: 'returns stats when specifying query and time range', + index: 'ft_farequote', + user: USER.ML_POWERUSER, + requestBody: { + query: { + bool: { + must: { + term: { airline: 'AAL' }, + }, + }, + }, + aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'], + nonAggregatableFields: ['type'], + samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run. + timeFieldName: '@timestamp', + earliest: 1454889600000, // February 8, 2016 12:00:00 AM GMT + latest: 1454976000000, // February 9, 2016 12:00:00 AM GMT + }, + expected: { + responseCode: 200, + responseBody: { + totalCount: 1733, + aggregatableExistsFields: [ + { + fieldName: '@timestamp', + existsInDocs: true, + stats: { sampleCount: 1733, count: 1733, cardinality: 1713 }, + }, + { + fieldName: 'airline', + existsInDocs: true, + stats: { sampleCount: 1733, count: 1733, cardinality: 1 }, + }, + { + fieldName: 'responsetime', + existsInDocs: true, + stats: { sampleCount: 1733, count: 1733, cardinality: 1730 }, + }, + ], + aggregatableNotExistsFields: [{ fieldName: 'sourcetype', existsInDocs: false }], + nonAggregatableExistsFields: [{ fieldName: 'type', existsInDocs: true, stats: {} }], + nonAggregatableNotExistsFields: [], + }, + }, + }, + { + testTitle: 'returns error for index which does not exist', + index: 'ft_farequote_not_exist', + user: USER.ML_POWERUSER, + requestBody: { + query: { bool: { must: [{ match_all: {} }] } }, + aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'], + nonAggregatableFields: ['@version', 'type'], + samplerShardSize: 1000, + timeFieldName: '@timestamp', + }, + expected: { + responseCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: + '[index_not_found_exception] no such index [ft_farequote_not_exist], with { resource.type="index_or_alias" & resource.id="ft_farequote_not_exist" & index_uuid="_na_" & index="ft_farequote_not_exist" }', + }, + }, + }, + ]; + + describe('get_overall_stats', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const { body } = await supertest + .post(`/api/ml/data_visualizer/get_overall_stats/${testData.index}`) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) + .set(COMMON_HEADERS) + .send(testData.requestBody) + .expect(testData.expected.responseCode); + + if (body.error === undefined) { + expect(body).to.eql(testData.expected.responseBody); + } else { + expect(body.error).to.eql(testData.expected.responseBody.error); + expect(body.message).to.eql(testData.expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts new file mode 100644 index 0000000000000..ce9e44618f1af --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('data visualizer', function() { + loadTestFile(require.resolve('./get_field_stats')); + loadTestFile(require.resolve('./get_overall_stats')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts b/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts new file mode 100644 index 0000000000000..245375562b5c1 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts @@ -0,0 +1,115 @@ +/* + * 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 '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataList = [ + { + testTitle: 'returns cardinality of customer name fields over full time range', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + fieldNames: ['customer_first_name.keyword', 'customer_last_name.keyword'], + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + }, + expected: { + responseBody: { + 'customer_first_name.keyword': 46, + 'customer_last_name.keyword': 183, + }, + }, + }, + { + testTitle: 'returns cardinality of geoip fields over specified range', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + fieldNames: ['geoip.city_name', 'geoip.continent_name', 'geoip.country_iso_code'], + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + earliestMs: 1560556800000, // June 15, 2019 12:00:00 AM GMT + latestMs: 1560643199000, // June 15, 2019 11:59:59 PM GMT + }, + expected: { + responseBody: { + 'geoip.city_name': 10, + 'geoip.continent_name': 5, + 'geoip.country_iso_code': 9, + }, + }, + }, + { + testTitle: 'returns empty response for non aggregatable field', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + fieldNames: ['manufacturer'], + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + earliestMs: 1560556800000, // June 15, 2019 12:00:00 AM GMT + latestMs: 1560643199000, // June 15, 2019 11:59:59 PM GMT + }, + expected: { + responseBody: {}, + }, + }, + { + testTitle: 'returns error for index which does not exist', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce_not_exist', + fieldNames: ['customer_first_name.keyword', 'customer_last_name.keyword'], + timeFieldName: 'order_date', + }, + expected: { + responseBody: { + statusCode: 404, + error: 'Not Found', + message: + '[index_not_found_exception] no such index [ft_ecommerce_not_exist], with { resource.type="index_or_alias" & resource.id="ft_ecommerce_not_exist" & index_uuid="_na_" & index="ft_ecommerce_not_exist" }', + }, + }, + }, + ]; + + describe('field_cardinality', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/ecommerce'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const { body } = await supertest + .post('/api/ml/fields_service/field_cardinality') + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) + .set(COMMON_HEADERS) + .send(testData.requestBody); + + if (body.error === undefined) { + expect(body).to.eql(testData.expected.responseBody); + } else { + expect(body.error).to.eql(testData.expected.responseBody.error); + expect(body.message).to.eql(testData.expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/fields_service/index.ts b/x-pack/test/api_integration/apis/ml/fields_service/index.ts new file mode 100644 index 0000000000000..312602e589119 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/fields_service/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('fields service', function() { + loadTestFile(require.resolve('./field_cardinality')); + loadTestFile(require.resolve('./time_field_range')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts b/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts new file mode 100644 index 0000000000000..2f0fd4fc6c5e3 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts @@ -0,0 +1,119 @@ +/* + * 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 '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataList = [ + { + testTitle: 'returns expected time range with index and match_all query', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + }, + expected: { + responseCode: 200, + responseBody: { + start: { + epoch: 1560297859000, + string: '2019-06-12T00:04:19.000Z', + }, + end: { + epoch: 1562975136000, + string: '2019-07-12T23:45:36.000Z', + }, + success: true, + }, + }, + }, + { + testTitle: 'returns expected time range with index and query', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + query: { + term: { + 'customer_first_name.keyword': { + value: 'Brigitte', + }, + }, + }, + timeFieldName: 'order_date', + }, + expected: { + responseCode: 200, + responseBody: { + start: { + epoch: 1560298982000, + string: '2019-06-12T00:23:02.000Z', + }, + end: { + epoch: 1562973754000, + string: '2019-07-12T23:22:34.000Z', + }, + success: true, + }, + }, + }, + { + testTitle: 'returns error for index which does not exist', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce_not_exist', + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + }, + expected: { + responseCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: + '[index_not_found_exception] no such index [ft_ecommerce_not_exist], with { resource.type="index_or_alias" & resource.id="ft_ecommerce_not_exist" & index_uuid="_na_" & index="ft_ecommerce_not_exist" }', + }, + }, + }, + ]; + + describe('time_field_range', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/ecommerce'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const { body } = await supertest + .post('/api/ml/fields_service/time_field_range') + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) + .set(COMMON_HEADERS) + .send(testData.requestBody) + .expect(testData.expected.responseCode); + + if (body.error === undefined) { + expect(body).to.eql(testData.expected.responseBody); + } else { + expect(body.error).to.eql(testData.expected.responseBody.error); + expect(body.message).to.eql(testData.expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index f012883c46ca3..58356637c63ac 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -31,11 +31,11 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { await ml.testResources.resetKibanaTimeZone(); }); - loadTestFile(require.resolve('./bucket_span_estimator')); - loadTestFile(require.resolve('./calculate_model_memory_limit')); - loadTestFile(require.resolve('./categorization_field_examples')); - loadTestFile(require.resolve('./get_module')); - loadTestFile(require.resolve('./recognize_module')); - loadTestFile(require.resolve('./setup_module')); + loadTestFile(require.resolve('./modules')); + loadTestFile(require.resolve('./anomaly_detectors')); + loadTestFile(require.resolve('./data_visualizer')); + loadTestFile(require.resolve('./fields_service')); + loadTestFile(require.resolve('./job_validation')); + loadTestFile(require.resolve('./jobs')); }); } diff --git a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts b/x-pack/test/api_integration/apis/ml/job_validation/bucket_span_estimator.ts similarity index 97% rename from x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts rename to x-pack/test/api_integration/apis/ml/job_validation/bucket_span_estimator.ts index bc0dc3019d7c9..0b4aca9660be4 100644 --- a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/bucket_span_estimator.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts b/x-pack/test/api_integration/apis/ml/job_validation/calculate_model_memory_limit.ts similarity index 97% rename from x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts rename to x-pack/test/api_integration/apis/ml/job_validation/calculate_model_memory_limit.ts index 59e3dfcca00f9..f17814633ce8f 100644 --- a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/calculate_model_memory_limit.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/job_validation/index.ts b/x-pack/test/api_integration/apis/ml/job_validation/index.ts new file mode 100644 index 0000000000000..6ca9dcbbe9e5b --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/job_validation/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('job validation', function() { + loadTestFile(require.resolve('./bucket_span_estimator')); + loadTestFile(require.resolve('./calculate_model_memory_limit')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts b/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts similarity index 98% rename from x-pack/test/api_integration/apis/ml/categorization_field_examples.ts rename to x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts index df0153f965942..bcc6c4907100c 100644 --- a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/jobs/index.ts b/x-pack/test/api_integration/apis/ml/jobs/index.ts new file mode 100644 index 0000000000000..70a64f198d6f4 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/jobs/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('jobs', function() { + loadTestFile(require.resolve('./categorization_field_examples')); + loadTestFile(require.resolve('./jobs_summary')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts new file mode 100644 index 0000000000000..6a57db1687868 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts @@ -0,0 +1,374 @@ +/* + * 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 '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; +import { Job } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +const SINGLE_METRIC_JOB_CONFIG: Job = { + job_id: `jobs_summary_fq_single_${Date.now()}`, + description: 'mean(responsetime) on farequote dataset with 15m bucket span', + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, + model_plot_config: { enabled: true }, +}; + +const MULTI_METRIC_JOB_CONFIG: Job = { + job_id: `jobs_summary_fq_multi_${Date.now()}`, + description: 'mean(responsetime) partition=airline on farequote dataset with 1h bucket span', + groups: ['farequote', 'automated', 'multi-metric'], + analysis_config: { + bucket_span: '1h', + influencers: ['airline'], + detectors: [{ function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' }], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '20mb' }, + model_plot_config: { enabled: true }, +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testSetupJobConfigs = [SINGLE_METRIC_JOB_CONFIG, MULTI_METRIC_JOB_CONFIG]; + + const testDataListNoJobId = [ + { + testTitle: 'as ML Poweruser', + user: USER.ML_POWERUSER, + requestBody: {}, + expected: { + responseCode: 200, + responseBody: [ + { + id: SINGLE_METRIC_JOB_CONFIG.job_id, + description: SINGLE_METRIC_JOB_CONFIG.description, + groups: SINGLE_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + { + id: MULTI_METRIC_JOB_CONFIG.job_id, + description: MULTI_METRIC_JOB_CONFIG.description, + groups: MULTI_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + ], + }, + }, + { + testTitle: 'as ML Viewer', + user: USER.ML_VIEWER, + requestBody: {}, + expected: { + responseCode: 200, + responseBody: [ + { + id: SINGLE_METRIC_JOB_CONFIG.job_id, + description: SINGLE_METRIC_JOB_CONFIG.description, + groups: SINGLE_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + { + id: MULTI_METRIC_JOB_CONFIG.job_id, + description: MULTI_METRIC_JOB_CONFIG.description, + groups: MULTI_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + ], + }, + }, + ]; + + const testDataListWithJobId = [ + { + testTitle: 'as ML Poweruser', + user: USER.ML_POWERUSER, + requestBody: { + jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id], + }, + expected: { + responseCode: 200, + responseBody: [ + { + id: SINGLE_METRIC_JOB_CONFIG.job_id, + description: SINGLE_METRIC_JOB_CONFIG.description, + groups: SINGLE_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + fullJob: { + // Only tests against some of the fields in the fullJob property. + job_id: SINGLE_METRIC_JOB_CONFIG.job_id, + job_type: 'anomaly_detector', + description: SINGLE_METRIC_JOB_CONFIG.description, + groups: SINGLE_METRIC_JOB_CONFIG.groups, + analysis_config: { + bucket_span: '15m', + detectors: [ + { + detector_description: 'mean(responsetime)', + function: 'mean', + field_name: 'responsetime', + detector_index: 0, + }, + ], + influencers: [], + }, + }, + }, + { + id: MULTI_METRIC_JOB_CONFIG.job_id, + description: MULTI_METRIC_JOB_CONFIG.description, + groups: MULTI_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + ], + }, + }, + ]; + + const testDataListNegative = [ + { + testTitle: 'as ML Unauthorized user', + user: USER.ML_UNAUTHORIZED, + requestBody: {}, + // Note that the jobs and datafeeds are loaded async so the actual error message is not deterministic. + expected: { + responseCode: 403, + error: 'Forbidden', + }, + }, + ]; + + async function runJobsSummaryRequest( + user: USER, + requestBody: object, + expectedResponsecode: number + ): Promise { + const { body } = await supertest + .post('/api/ml/jobs/jobs_summary') + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_HEADERS) + .send(requestBody) + .expect(expectedResponsecode); + + return body; + } + + function compareById(a: { id: string }, b: { id: string }) { + if (a.id < b.id) { + return -1; + } + if (a.id > b.id) { + return 1; + } + return 0; + } + + function getGroups(jobs: Array<{ groups: string[] }>) { + const groupIds: string[] = []; + jobs.forEach(job => { + const groups = job.groups; + groups.forEach(group => { + if (groupIds.indexOf(group) === -1) { + groupIds.push(group); + } + }); + }); + return groupIds.sort(); + } + + describe('jobs_summary', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('sets up jobs', async () => { + for (const job of testSetupJobConfigs) { + await ml.api.createAnomalyDetectionJob(job); + } + }); + + for (const testData of testDataListNoJobId) { + describe('gets job summary with no job IDs supplied', function() { + it(`${testData.testTitle}`, async () => { + const body = await runJobsSummaryRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + // Validate the important parts of the response. + const expectedResponse = testData.expected.responseBody; + + // Validate job count. + expect(body).to.have.length(expectedResponse.length); + + // Validate job IDs. + const expectedRspJobIds = expectedResponse + .map((job: { id: string }) => { + return { id: job.id }; + }) + .sort(compareById); + const actualRspJobIds = body + .map((job: { id: string }) => { + return { id: job.id }; + }) + .sort(compareById); + + expect(actualRspJobIds).to.eql(expectedRspJobIds); + + // Validate created group IDs. + const expectedRspGroupIds = getGroups(expectedResponse); + const actualRspGroupsIds = getGroups(body); + expect(actualRspGroupsIds).to.eql(expectedRspGroupIds); + }); + }); + } + + for (const testData of testDataListWithJobId) { + describe('gets job summary with job ID supplied', function() { + it(`${testData.testTitle}`, async () => { + const body = await runJobsSummaryRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + // Validate the important parts of the response. + const expectedResponse = testData.expected.responseBody; + + // Validate job count. + expect(body).to.have.length(expectedResponse.length); + + // Validate job IDs. + const expectedRspJobIds = expectedResponse + .map((job: { id: string }) => { + return { id: job.id }; + }) + .sort(compareById); + const actualRspJobIds = body + .map((job: { id: string }) => { + return { id: job.id }; + }) + .sort(compareById); + + expect(actualRspJobIds).to.eql(expectedRspJobIds); + + // Validate created group IDs. + const expectedRspGroupIds = getGroups(expectedResponse); + const actualRspGroupsIds = getGroups(body); + expect(actualRspGroupsIds).to.eql(expectedRspGroupIds); + + // Validate the response for the specified job IDs contains a fullJob property. + const requestedJobIds = testData.requestBody.jobIds; + for (const job of body) { + if (requestedJobIds.includes(job.id)) { + expect(job).to.have.property('fullJob'); + } else { + expect(job).not.to.have.property('fullJob'); + } + } + + for (const expectedJob of expectedResponse) { + const expectedJobId = expectedJob.id; + const actualJob = body.find((job: { id: string }) => job.id === expectedJobId); + if (expectedJob.fullJob) { + expect(actualJob).to.have.property('fullJob'); + expect(actualJob.fullJob).to.have.property('analysis_config'); + expect(actualJob.fullJob.analysis_config).to.eql(expectedJob.fullJob.analysis_config); + } else { + expect(actualJob).not.to.have.property('fullJob'); + } + } + }); + }); + } + + for (const testData of testDataListNegative) { + describe('rejects request', function() { + it(testData.testTitle, async () => { + const body = await runJobsSummaryRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + expect(body) + .to.have.property('error') + .eql(testData.expected.error); + + expect(body).to.have.property('message'); + }); + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/get_module.ts b/x-pack/test/api_integration/apis/ml/modules/get_module.ts similarity index 92% rename from x-pack/test/api_integration/apis/ml/get_module.ts rename to x-pack/test/api_integration/apis/ml/modules/get_module.ts index a50d3c0abe430..e19d45999c88e 100644 --- a/x-pack/test/api_integration/apis/ml/get_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/get_module.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/modules/index.ts b/x-pack/test/api_integration/apis/ml/modules/index.ts new file mode 100644 index 0000000000000..4fdc404c607aa --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/modules/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('modules', function() { + loadTestFile(require.resolve('./get_module')); + loadTestFile(require.resolve('./recognize_module')); + loadTestFile(require.resolve('./setup_module')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/recognize_module.ts b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts similarity index 93% rename from x-pack/test/api_integration/apis/ml/recognize_module.ts rename to x-pack/test/api_integration/apis/ml/modules/recognize_module.ts index 8e360579c1459..948728189b8bd 100644 --- a/x-pack/test/api_integration/apis/ml/recognize_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts similarity index 96% rename from x-pack/test/api_integration/apis/ml/setup_module.ts rename to x-pack/test/api_integration/apis/ml/modules/setup_module.ts index e603782b25717..23ddd3b63a2ef 100644 --- a/x-pack/test/api_integration/apis/ml/setup_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts @@ -6,10 +6,10 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; -import { JOB_STATE, DATAFEED_STATE } from '../../../../plugins/ml/common/constants/states'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { JOB_STATE, DATAFEED_STATE } from '../../../../../plugins/ml/common/constants/states'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', From 95ac47d3d26a5f0e3c19669f86c69c82fc9c1614 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Thu, 23 Apr 2020 19:15:45 +0200 Subject: [PATCH 06/35] Allow IdP initiated SAML login with session containing expired token. (#59686) --- .../authentication/providers/saml.test.ts | 204 +++++++++--------- .../server/authentication/providers/saml.ts | 12 +- .../server/authentication/tokens.test.ts | 151 +++++++------ .../security/server/authentication/tokens.ts | 20 +- .../apis/security/saml_login.ts | 165 ++++++++------ 5 files changed, 311 insertions(+), 241 deletions(-) diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts index a7a43a3031571..ec50ac090f1e7 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts @@ -315,117 +315,123 @@ describe('SAMLAuthenticationProvider', () => { }); }); - it('redirects to the home page if new SAML Response is for the same user.', async () => { - const request = httpServerMock.createKibanaRequest({ headers: {} }); - const state = { - username: 'user', - accessToken: 'existing-valid-token', - refreshToken: 'existing-valid-refresh-token', - realm: 'test-realm', - }; - const authorization = `Bearer ${state.accessToken}`; - - const user = { username: 'user' }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); - mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - - mockOptions.client.callAsInternalUser.mockResolvedValue({ - username: 'user', - access_token: 'new-valid-token', - refresh_token: 'new-valid-refresh-token', - }); - - mockOptions.tokens.invalidate.mockResolvedValue(undefined); - - await expect( - provider.login( - request, - { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, - state - ) - ).resolves.toEqual( - AuthenticationResult.redirectTo('/base-path/', { - state: { - username: 'user', - accessToken: 'new-valid-token', - refreshToken: 'new-valid-refresh-token', - realm: 'test-realm', - }, - }) - ); - - expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + for (const [description, response] of [ + ['session is valid', Promise.resolve({ username: 'user' })], + [ + 'session is is expired', + Promise.reject(ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())), + ], + ] as Array<[string, Promise]>) { + it(`redirects to the home page if new SAML Response is for the same user if ${description}.`, async () => { + const request = httpServerMock.createKibanaRequest({ headers: {} }); + const state = { + username: 'user', + accessToken: 'existing-token', + refreshToken: 'existing-refresh-token', + realm: 'test-realm', + }; + const authorization = `Bearer ${state.accessToken}`; - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, - } - ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockImplementation(() => response); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ - accessToken: state.accessToken, - refreshToken: state.refreshToken, - }); - }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ + username: 'user', + access_token: 'new-valid-token', + refresh_token: 'new-valid-refresh-token', + }); + + mockOptions.tokens.invalidate.mockResolvedValue(undefined); + + await expect( + provider.login( + request, + { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, + state + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/', { + state: { + username: 'user', + accessToken: 'new-valid-token', + refreshToken: 'new-valid-refresh-token', + realm: 'test-realm', + }, + }) + ); - it('redirects to `overwritten_session` if new SAML Response is for the another user.', async () => { - const request = httpServerMock.createKibanaRequest({ headers: {} }); - const state = { - username: 'user', - accessToken: 'existing-valid-token', - refreshToken: 'existing-valid-refresh-token', - realm: 'test-realm', - }; - const authorization = `Bearer ${state.accessToken}`; + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); - const existingUser = { username: 'user' }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(existingUser); - mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( + 'shield.samlAuthenticate', + { + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + } + ); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - username: 'new-user', - access_token: 'new-valid-token', - refresh_token: 'new-valid-refresh-token', + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ + accessToken: state.accessToken, + refreshToken: state.refreshToken, + }); }); - mockOptions.tokens.invalidate.mockResolvedValue(undefined); + it(`redirects to \`overwritten_session\` if new SAML Response is for the another user if ${description}.`, async () => { + const request = httpServerMock.createKibanaRequest({ headers: {} }); + const state = { + username: 'user', + accessToken: 'existing-token', + refreshToken: 'existing-refresh-token', + realm: 'test-realm', + }; + const authorization = `Bearer ${state.accessToken}`; - await expect( - provider.login( - request, - { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, - state - ) - ).resolves.toEqual( - AuthenticationResult.redirectTo('/mock-server-basepath/security/overwritten_session', { - state: { - username: 'new-user', - accessToken: 'new-valid-token', - refreshToken: 'new-valid-refresh-token', - realm: 'test-realm', - }, - }) - ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockImplementation(() => response); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + mockOptions.client.callAsInternalUser.mockResolvedValue({ + username: 'new-user', + access_token: 'new-valid-token', + refresh_token: 'new-valid-refresh-token', + }); + + mockOptions.tokens.invalidate.mockResolvedValue(undefined); + + await expect( + provider.login( + request, + { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, + state + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/mock-server-basepath/security/overwritten_session', { + state: { + username: 'new-user', + accessToken: 'new-valid-token', + refreshToken: 'new-valid-refresh-token', + realm: 'test-realm', + }, + }) + ); - expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, - } - ); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( + 'shield.samlAuthenticate', + { + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + } + ); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ - accessToken: state.accessToken, - refreshToken: state.refreshToken, + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ + accessToken: state.accessToken, + refreshToken: state.refreshToken, + }); }); - }); + } }); describe('User initiated login with captured redirect URL', () => { diff --git a/x-pack/plugins/security/server/authentication/providers/saml.ts b/x-pack/plugins/security/server/authentication/providers/saml.ts index e14d34d1901eb..5c5ec49890901 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.ts @@ -158,10 +158,14 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { return await this.loginWithSAMLResponse(request, samlResponse, state); } - if (authenticationResult.succeeded()) { - // If user has been authenticated via session, but request also includes SAML payload - // we should check whether this payload is for the exactly same user and if not - // we'll re-authenticate user and forward to a page with the respective warning. + // If user has been authenticated via session or failed to do so because of expired access token, + // but request also includes SAML payload we should check whether this payload is for the exactly + // same user and if not we'll re-authenticate user and forward to a page with the respective warning. + if ( + authenticationResult.succeeded() || + (authenticationResult.failed() && + Tokens.isAccessTokenExpiredError(authenticationResult.error)) + ) { return await this.loginWithNewSAMLResponse( request, samlResponse, diff --git a/x-pack/plugins/security/server/authentication/tokens.test.ts b/x-pack/plugins/security/server/authentication/tokens.test.ts index 82f29310c04c0..57366183050d7 100644 --- a/x-pack/plugins/security/server/authentication/tokens.test.ts +++ b/x-pack/plugins/security/server/authentication/tokens.test.ts @@ -25,7 +25,7 @@ describe('Tokens', () => { tokens = new Tokens(tokensOptions); }); - it('isAccessTokenExpiredError() returns `true` only if token expired or its document is missing', () => { + it('isAccessTokenExpiredError() returns `true` only if token expired', () => { const nonExpirationErrors = [ {}, new Error(), @@ -91,55 +91,66 @@ describe('Tokens', () => { }); describe('invalidate()', () => { - it('throws if call to delete access token responds with an error', async () => { - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - - const failureReason = new Error('failed to delete token'); - mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { - if (args && args.body && args.body.token) { - return Promise.reject(failureReason); - } - - return Promise.resolve({ invalidated_tokens: 1 }); + for (const [description, failureReason] of [ + ['an unknown error', new Error('failed to delete token')], + ['a 404 error without body', { statusCode: 404 }], + ] as Array<[string, object]>) { + it(`throws if call to delete access token responds with ${description}`, async () => { + const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + + mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { + if (args && args.body && args.body.token) { + return Promise.reject(failureReason); + } + + return Promise.resolve({ invalidated_tokens: 1 }); + }); + + await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); + + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { token: tokenPair.accessToken }, + } + ); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { refresh_token: tokenPair.refreshToken }, + } + ); }); - await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); - - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { token: tokenPair.accessToken } } - ); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { refresh_token: tokenPair.refreshToken } } - ); - }); - - it('throws if call to delete refresh token responds with an error', async () => { - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - - const failureReason = new Error('failed to delete token'); - mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { - if (args && args.body && args.body.refresh_token) { - return Promise.reject(failureReason); - } - - return Promise.resolve({ invalidated_tokens: 1 }); + it(`throws if call to delete refresh token responds with ${description}`, async () => { + const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + + mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { + if (args && args.body && args.body.refresh_token) { + return Promise.reject(failureReason); + } + + return Promise.resolve({ invalidated_tokens: 1 }); + }); + + await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); + + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { token: tokenPair.accessToken }, + } + ); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { refresh_token: tokenPair.refreshToken }, + } + ); }); - - await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); - - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { token: tokenPair.accessToken } } - ); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { refresh_token: tokenPair.refreshToken } } - ); - }); + } it('invalidates all provided tokens', async () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; @@ -187,23 +198,35 @@ describe('Tokens', () => { ); }); - it('does not fail if none of the tokens were invalidated', async () => { - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - - mockClusterClient.callAsInternalUser.mockResolvedValue({ invalidated_tokens: 0 }); - - await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined); - - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { token: tokenPair.accessToken } } - ); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { refresh_token: tokenPair.refreshToken } } - ); - }); + for (const [description, response] of [ + ['none of the tokens were invalidated', Promise.resolve({ invalidated_tokens: 0 })], + [ + '404 error is returned', + Promise.reject({ statusCode: 404, body: { invalidated_tokens: 0 } }), + ], + ] as Array<[string, Promise]>) { + it(`does not fail if ${description}`, async () => { + const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + + mockClusterClient.callAsInternalUser.mockImplementation(() => response); + + await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined); + + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { token: tokenPair.accessToken }, + } + ); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { refresh_token: tokenPair.refreshToken }, + } + ); + }); + } it('does not fail if more than one token per access or refresh token were invalidated', async () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; diff --git a/x-pack/plugins/security/server/authentication/tokens.ts b/x-pack/plugins/security/server/authentication/tokens.ts index ea7b5d5a9ff38..9117c9a679a4a 100644 --- a/x-pack/plugins/security/server/authentication/tokens.ts +++ b/x-pack/plugins/security/server/authentication/tokens.ts @@ -103,8 +103,15 @@ export class Tokens { ).invalidated_tokens; } catch (err) { this.logger.debug(`Failed to invalidate refresh token: ${err.message}`); - // We don't re-throw the error here to have a chance to invalidate access token if it's provided. - invalidationError = err; + + // When using already deleted refresh token, Elasticsearch responds with 404 and a body that + // shows that no tokens were invalidated. + if (getErrorStatusCode(err) === 404 && err.body?.invalidated_tokens === 0) { + invalidatedTokensCount = err.body.invalidated_tokens; + } else { + // We don't re-throw the error here to have a chance to invalidate access token if it's provided. + invalidationError = err; + } } if (invalidatedTokensCount === 0) { @@ -128,7 +135,14 @@ export class Tokens { ).invalidated_tokens; } catch (err) { this.logger.debug(`Failed to invalidate access token: ${err.message}`); - invalidationError = err; + + // When using already deleted access token, Elasticsearch responds with 404 and a body that + // shows that no tokens were invalidated. + if (getErrorStatusCode(err) === 404 && err.body?.invalidated_tokens === 0) { + invalidatedTokensCount = err.body.invalidated_tokens; + } else { + invalidationError = err; + } } if (invalidatedTokensCount === 0) { diff --git a/x-pack/test/saml_api_integration/apis/security/saml_login.ts b/x-pack/test/saml_api_integration/apis/security/saml_login.ts index 3b11ef61a1ab2..0b127288e7958 100644 --- a/x-pack/test/saml_api_integration/apis/security/saml_login.ts +++ b/x-pack/test/saml_api_integration/apis/security/saml_login.ts @@ -35,7 +35,7 @@ export default function({ getService }: FtrProviderContext) { }); } - async function checkSessionCookie(sessionCookie: Cookie) { + async function checkSessionCookie(sessionCookie: Cookie, username = 'a@b.c') { expect(sessionCookie.key).to.be('sid'); expect(sessionCookie.value).to.not.be.empty(); expect(sessionCookie.path).to.be('/'); @@ -59,7 +59,7 @@ export default function({ getService }: FtrProviderContext) { 'authentication_provider', ]); - expect(apiResponse.body.username).to.be('a@b.c'); + expect(apiResponse.body.username).to.be(username); } describe('SAML authentication', () => { @@ -668,6 +668,29 @@ export default function({ getService }: FtrProviderContext) { const existingUsername = 'a@b.c'; let existingSessionCookie: Cookie; + const testScenarios: Array<[string, () => Promise]> = [ + // Default scenario when active cookie has an active access token. + ['when access token is valid', async () => {}], + // Scenario when active cookie has an expired access token. Access token expiration is set + // to 15s for API integration tests so we need to wait for 20s to make sure token expires. + ['when access token is expired', async () => await delay(20000)], + // Scenario when active cookie references to access/refresh token pair that were already + // removed from Elasticsearch (to simulate 24h when expired tokens are removed). + [ + 'when access token document is missing', + async () => { + const esResponse = await getService('legacyEs').deleteByQuery({ + index: '.security-tokens', + q: 'doc_type:token', + refresh: true, + }); + expect(esResponse) + .to.have.property('deleted') + .greaterThan(0); + }, + ], + ]; + beforeEach(async () => { const captureURLResponse = await supertest .get('/abc/xyz/handshake?one=two three') @@ -701,76 +724,76 @@ export default function({ getService }: FtrProviderContext) { )!; }); - it('should renew session and redirect to the home page if login is for the same user', async () => { - const samlAuthenticationResponse = await supertest - .post('/api/security/saml/callback') - .set('kbn-xsrf', 'xxx') - .set('Cookie', existingSessionCookie.cookieString()) - .send({ SAMLResponse: await createSAMLResponse({ username: existingUsername }) }) - .expect('location', '/') - .expect(302); - - const newSessionCookie = request.cookie( - samlAuthenticationResponse.headers['set-cookie'][0] - )!; - expect(newSessionCookie.value).to.not.be.empty(); - expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value); - - // Tokens from old cookie are invalidated. - const rejectedResponse = await supertest - .get('/internal/security/me') - .set('kbn-xsrf', 'xxx') - .set('Cookie', existingSessionCookie.cookieString()) - .expect(400); - expect(rejectedResponse.body).to.have.property( - 'message', - 'Both access and refresh tokens are expired.' - ); - - // Only tokens from new session are valid. - const acceptedResponse = await supertest - .get('/internal/security/me') - .set('kbn-xsrf', 'xxx') - .set('Cookie', newSessionCookie.cookieString()) - .expect(200); - expect(acceptedResponse.body).to.have.property('username', existingUsername); - }); - - it('should create a new session and redirect to the `overwritten_session` if login is for another user', async () => { - const newUsername = 'c@d.e'; - const samlAuthenticationResponse = await supertest - .post('/api/security/saml/callback') - .set('kbn-xsrf', 'xxx') - .set('Cookie', existingSessionCookie.cookieString()) - .send({ SAMLResponse: await createSAMLResponse({ username: newUsername }) }) - .expect('location', '/security/overwritten_session') - .expect(302); - - const newSessionCookie = request.cookie( - samlAuthenticationResponse.headers['set-cookie'][0] - )!; - expect(newSessionCookie.value).to.not.be.empty(); - expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value); - - // Tokens from old cookie are invalidated. - const rejectedResponse = await supertest - .get('/internal/security/me') - .set('kbn-xsrf', 'xxx') - .set('Cookie', existingSessionCookie.cookieString()) - .expect(400); - expect(rejectedResponse.body).to.have.property( - 'message', - 'Both access and refresh tokens are expired.' - ); + for (const [description, setup] of testScenarios) { + it(`should renew session and redirect to the home page if login is for the same user ${description}`, async () => { + await setup(); + + const samlAuthenticationResponse = await supertest + .post('/api/security/saml/callback') + .set('kbn-xsrf', 'xxx') + .set('Cookie', existingSessionCookie.cookieString()) + .send({ SAMLResponse: await createSAMLResponse({ username: existingUsername }) }) + .expect(302); + + expect(samlAuthenticationResponse.headers.location).to.be('/'); + + const newSessionCookie = request.cookie( + samlAuthenticationResponse.headers['set-cookie'][0] + )!; + expect(newSessionCookie.value).to.not.be.empty(); + expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value); + + // Tokens from old cookie are invalidated. + const rejectedResponse = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', existingSessionCookie.cookieString()) + .expect(400); + expect(rejectedResponse.body).to.have.property( + 'message', + 'Both access and refresh tokens are expired.' + ); + + // Only tokens from new session are valid. + await checkSessionCookie(newSessionCookie); + }); - // Only tokens from new session are valid. - const acceptedResponse = await supertest - .get('/internal/security/me') - .set('kbn-xsrf', 'xxx') - .set('Cookie', newSessionCookie.cookieString()) - .expect(200); - expect(acceptedResponse.body).to.have.property('username', newUsername); - }); + it(`should create a new session and redirect to the \`overwritten_session\` if login is for another user ${description}`, async () => { + await setup(); + + const newUsername = 'c@d.e'; + const samlAuthenticationResponse = await supertest + .post('/api/security/saml/callback') + .set('kbn-xsrf', 'xxx') + .set('Cookie', existingSessionCookie.cookieString()) + .send({ SAMLResponse: await createSAMLResponse({ username: newUsername }) }) + .expect(302); + + expect(samlAuthenticationResponse.headers.location).to.be( + '/security/overwritten_session' + ); + + const newSessionCookie = request.cookie( + samlAuthenticationResponse.headers['set-cookie'][0] + )!; + expect(newSessionCookie.value).to.not.be.empty(); + expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value); + + // Tokens from old cookie are invalidated. + const rejectedResponse = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', existingSessionCookie.cookieString()) + .expect(400); + expect(rejectedResponse.body).to.have.property( + 'message', + 'Both access and refresh tokens are expired.' + ); + + // Only tokens from new session are valid. + await checkSessionCookie(newSessionCookie, newUsername); + }); + } }); describe('handshake with very long URL path or fragment', () => { From e2baff36b8d3a4792af6f865c8bc1b8100fbce43 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Thu, 23 Apr 2020 11:39:38 -0600 Subject: [PATCH 07/35] [data.search.aggs]: Create agg types function for terms agg. (#63541) * Minor cleanup on AggConfigJson interface. * Add types for terms agg params & mapping. * Add terms agg expression function. * Register agg functions with expressions service. * Add unit tests for terms expression function. * Update API docs. * AggConfigJson -> AggConfigSerialized * Add serialize(), enforce serializable params, fix subexpressions in terms agg fn. * Simplify getAggTypesFunctions * it() -> test() * Add help text. * Ensure serialize() is used by agg param type. * Add toExpressionAst to AggConfig. * Add json arg to terms agg fn. * Update docs. * Fix typo which caused functional test failures. * Add AggParam.toExpressionAst so AggConfig.toExpressionAst can return function ast instead of expression ast. * Clean up overlooked items. --- ...ns-data-public.aggconfigoptions.enabled.md | 11 - ...plugins-data-public.aggconfigoptions.id.md | 11 - ...in-plugins-data-public.aggconfigoptions.md | 17 +- ...ins-data-public.aggconfigoptions.params.md | 11 - ...ins-data-public.aggconfigoptions.schema.md | 11 - ...ugins-data-public.aggconfigoptions.type.md | 11 - ...lugins-data-public.aggparamtype.makeagg.md | 2 +- ...plugin-plugins-data-public.aggparamtype.md | 2 +- .../kibana-plugin-plugins-data-public.md | 2 +- src/plugins/data/public/plugin.ts | 1 + src/plugins/data/public/public.api.md | 17 +- .../public/search/aggs/agg_config.test.ts | 218 +++++++++++++++++- .../data/public/search/aggs/agg_config.ts | 80 ++++++- .../data/public/search/aggs/agg_configs.ts | 4 +- .../data/public/search/aggs/agg_type.ts | 10 + .../data/public/search/aggs/agg_types.ts | 5 + .../data/public/search/aggs/buckets/terms.ts | 22 +- .../search/aggs/buckets/terms_fn.test.ts | 164 +++++++++++++ .../public/search/aggs/buckets/terms_fn.ts | 181 +++++++++++++++ .../metrics/lib/parent_pipeline_agg_helper.ts | 12 +- .../lib/sibling_pipeline_agg_helper.ts | 14 +- .../aggs/metrics/percentile_ranks.test.ts | 28 +-- .../search/aggs/metrics/percentiles.test.ts | 7 +- .../public/search/aggs/param_types/agg.ts | 16 +- .../public/search/aggs/param_types/base.ts | 3 + .../aggs/test_helpers/function_wrapper.ts | 49 ++++ .../public/search/aggs/test_helpers/index.ts | 1 + src/plugins/data/public/search/aggs/types.ts | 32 ++- .../expressions/utils/serialize_agg_config.ts | 2 +- .../data/public/search/search_service.test.ts | 4 +- .../data/public/search/search_service.ts | 14 +- 31 files changed, 810 insertions(+), 152 deletions(-) delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.id.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.params.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.schema.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.type.md create mode 100644 src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/terms_fn.ts create mode 100644 src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md deleted file mode 100644 index 2ef8c797f4054..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [enabled](./kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md) - -## AggConfigOptions.enabled property - -Signature: - -```typescript -enabled?: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.id.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.id.md deleted file mode 100644 index 8939854ab19ca..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.id.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [id](./kibana-plugin-plugins-data-public.aggconfigoptions.id.md) - -## AggConfigOptions.id property - -Signature: - -```typescript -id?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md index b841d9b04d6a7..ff8055b8cf1b1 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md @@ -2,21 +2,12 @@ [Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) -## AggConfigOptions interface +## AggConfigOptions type Signature: ```typescript -export interface AggConfigOptions +export declare type AggConfigOptions = Assign; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [enabled](./kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md) | boolean | | -| [id](./kibana-plugin-plugins-data-public.aggconfigoptions.id.md) | string | | -| [params](./kibana-plugin-plugins-data-public.aggconfigoptions.params.md) | Record<string, any> | | -| [schema](./kibana-plugin-plugins-data-public.aggconfigoptions.schema.md) | string | | -| [type](./kibana-plugin-plugins-data-public.aggconfigoptions.type.md) | IAggType | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.params.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.params.md deleted file mode 100644 index 45219a837cc33..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.params.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [params](./kibana-plugin-plugins-data-public.aggconfigoptions.params.md) - -## AggConfigOptions.params property - -Signature: - -```typescript -params?: Record; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.schema.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.schema.md deleted file mode 100644 index b2b42eb2e5b4d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.schema.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [schema](./kibana-plugin-plugins-data-public.aggconfigoptions.schema.md) - -## AggConfigOptions.schema property - -Signature: - -```typescript -schema?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.type.md deleted file mode 100644 index 866065ce52ba6..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [type](./kibana-plugin-plugins-data-public.aggconfigoptions.type.md) - -## AggConfigOptions.type property - -Signature: - -```typescript -type: IAggType; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md index 43f30d73ca6df..a91db7e7aac8b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md @@ -7,5 +7,5 @@ Signature: ```typescript -makeAgg: (agg: TAggConfig, state?: any) => TAggConfig; +makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md index b75065da91abd..f9733529a315d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md @@ -21,5 +21,5 @@ export declare class AggParamType ex | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [allowedAggs](./kibana-plugin-plugins-data-public.aggparamtype.allowedaggs.md) | | string[] | | -| [makeAgg](./kibana-plugin-plugins-data-public.aggparamtype.makeagg.md) | | (agg: TAggConfig, state?: any) => TAggConfig | | +| [makeAgg](./kibana-plugin-plugins-data-public.aggparamtype.makeagg.md) | | (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig | | 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 5ef317a57fb0e..604ac5120922b 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 @@ -49,7 +49,6 @@ | Interface | Description | | --- | --- | -| [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) | | | [AggParamOption](./kibana-plugin-plugins-data-public.aggparamoption.md) | | | [DataPublicPluginSetup](./kibana-plugin-plugins-data-public.datapublicpluginsetup.md) | | | [DataPublicPluginStart](./kibana-plugin-plugins-data-public.datapublicpluginstart.md) | | @@ -118,6 +117,7 @@ | Type Alias | Description | | --- | --- | +| [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) | | | [AggParam](./kibana-plugin-plugins-data-public.aggparam.md) | | | [CustomFilter](./kibana-plugin-plugins-data-public.customfilter.md) | | | [EsQuerySortValue](./kibana-plugin-plugins-data-public.esquerysortvalue.md) | | diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 49ef1000d7993..924fcd6730f93 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -139,6 +139,7 @@ export class DataPublicPlugin implements Plugin; - // (undocumented) - schema?: string; - // (undocumented) +export type AggConfigOptions = Assign; // Warning: (ae-missing-release-tag) "AggGroupNames" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -112,7 +105,7 @@ export class AggParamType extends Ba // (undocumented) allowedAggs: string[]; // (undocumented) - makeAgg: (agg: TAggConfig, state?: any) => TAggConfig; + makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig; } // Warning: (ae-missing-release-tag) "AggTypeFieldFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/data/public/search/aggs/agg_config.test.ts b/src/plugins/data/public/search/aggs/agg_config.test.ts index 2813e3b9c5373..b5df90313230c 100644 --- a/src/plugins/data/public/search/aggs/agg_config.test.ts +++ b/src/plugins/data/public/search/aggs/agg_config.test.ts @@ -24,18 +24,21 @@ import { AggConfigs, CreateAggConfigParams } from './agg_configs'; import { AggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { mockDataServices, mockAggTypesRegistry } from './test_helpers'; +import { MetricAggType } from './metrics/metric_agg_type'; import { Field as IndexPatternField, IndexPattern } from '../../index_patterns'; import { stubIndexPatternWithFields } from '../../../public/stubs'; +import { FieldFormatsStart } from '../../field_formats'; import { fieldFormatsServiceMock } from '../../field_formats/mocks'; describe('AggConfig', () => { let indexPattern: IndexPattern; let typesRegistry: AggTypesRegistryStart; - const fieldFormats = fieldFormatsServiceMock.createStartContract(); + let fieldFormats: FieldFormatsStart; beforeEach(() => { jest.restoreAllMocks(); mockDataServices(); + fieldFormats = fieldFormatsServiceMock.createStartContract(); indexPattern = stubIndexPatternWithFields as IndexPattern; typesRegistry = mockAggTypesRegistry(); }); @@ -325,7 +328,7 @@ describe('AggConfig', () => { }); }); - describe('#toJSON', () => { + describe('#serialize', () => { it('includes the aggs id, params, type and schema', () => { const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); const configStates = { @@ -342,7 +345,7 @@ describe('AggConfig', () => { expect(aggConfig.type).toHaveProperty('name', 'date_histogram'); expect(typeof aggConfig.schema).toBe('string'); - const state = aggConfig.toJSON(); + const state = aggConfig.serialize(); expect(state).toHaveProperty('id', '1'); expect(typeof state.params).toBe('object'); expect(state).toHaveProperty('type', 'date_histogram'); @@ -367,6 +370,201 @@ describe('AggConfig', () => { }); }); + describe('#toExpressionAst', () => { + beforeEach(() => { + fieldFormats.getDefaultInstance = (() => ({ + getConverterFor: (t?: string) => t || identity, + })) as any; + indexPattern.fields.getByName = name => + ({ + format: { + getConverterFor: (t?: string) => t || identity, + }, + } as IndexPatternField); + }); + + it('works with primitive param types', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'machine.os.keyword', + order: 'asc', + }, + }; + const aggConfig = ac.createAggConfig(configStates); + expect(aggConfig.toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "id": Array [ + "1", + ], + "missingBucket": Array [ + false, + ], + "missingBucketLabel": Array [ + "Missing", + ], + "order": Array [ + "asc", + ], + "otherBucket": Array [ + false, + ], + "otherBucketLabel": Array [ + "Other", + ], + "schema": Array [ + "segment", + ], + "size": Array [ + 5, + ], + }, + "function": "aggTerms", + "type": "function", + } + `); + }); + + it('creates a subexpression for params of type "agg"', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + type: 'terms', + params: { + field: 'machine.os.keyword', + order: 'asc', + orderAgg: { + enabled: true, + type: 'terms', + params: { + field: 'bytes', + order: 'asc', + size: 5, + }, + }, + }, + }; + const aggConfig = ac.createAggConfig(configStates); + const aggArg = aggConfig.toExpressionAst()?.arguments.orderAgg; + expect(aggArg).toMatchInlineSnapshot(` + Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "id": Array [ + "1-orderAgg", + ], + "missingBucket": Array [ + false, + ], + "missingBucketLabel": Array [ + "Missing", + ], + "order": Array [ + "asc", + ], + "otherBucket": Array [ + false, + ], + "otherBucketLabel": Array [ + "Other", + ], + "schema": Array [ + "orderAgg", + ], + "size": Array [ + 5, + ], + }, + "function": "aggTerms", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); + }); + + it('creates a subexpression for param types other than "agg" which have specified toExpressionAst', () => { + // Overwrite the `ranges` param in the `range` agg with a mock toExpressionAst function + const range: MetricAggType = typesRegistry.get('range'); + range.expressionName = 'aggRange'; + const rangesParam = range.params.find(p => p.name === 'ranges'); + rangesParam!.toExpressionAst = (val: any) => ({ + type: 'function', + function: 'aggRanges', + arguments: { + ranges: ['oh hi there!'], + }, + }); + + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + type: 'range', + params: { + field: 'bytes', + }, + }; + + const aggConfig = ac.createAggConfig(configStates); + const ranges = aggConfig.toExpressionAst()!.arguments.ranges; + expect(ranges).toMatchInlineSnapshot(` + Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "ranges": Array [ + "oh hi there!", + ], + }, + "function": "aggRanges", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); + }); + + it('stringifies any other params which are an object', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + type: 'terms', + params: { + field: 'machine.os.keyword', + order: 'asc', + json: { foo: 'bar' }, + }, + }; + const aggConfig = ac.createAggConfig(configStates); + const json = aggConfig.toExpressionAst()?.arguments.json; + expect(json).toEqual([JSON.stringify(configStates.params.json)]); + }); + + it(`returns undefined if an expressionName doesn't exist on the agg type`, () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + type: 'unknown type', + params: {}, + }; + const aggConfig = ac.createAggConfig(configStates); + expect(aggConfig.toExpressionAst()).toBe(undefined); + }); + }); + describe('#makeLabel', () => { let aggConfig: AggConfig; @@ -422,6 +620,9 @@ describe('AggConfig', () => { let aggConfig: AggConfig; beforeEach(() => { + fieldFormats.getDefaultInstance = (() => ({ + getConverterFor: (t?: string) => t || identity, + })) as any; indexPattern.fields.getByName = name => ({ format: { @@ -434,11 +635,7 @@ describe('AggConfig', () => { type: 'histogram', schema: 'bucket', params: { - field: { - format: { - getConverterFor: (t?: string) => t || identity, - }, - }, + field: 'bytes', }, }; const ac = new AggConfigs(indexPattern, [configStates], { typesRegistry, fieldFormats }); @@ -446,6 +643,11 @@ describe('AggConfig', () => { }); it("returns the field's formatter", () => { + aggConfig.params.field = { + format: { + getConverterFor: (t?: string) => t || identity, + }, + }; expect(aggConfig.fieldFormatter().toString()).toBe( aggConfig .getField() diff --git a/src/plugins/data/public/search/aggs/agg_config.ts b/src/plugins/data/public/search/aggs/agg_config.ts index 6188849e0e6d4..973c69e3d4f5f 100644 --- a/src/plugins/data/public/search/aggs/agg_config.ts +++ b/src/plugins/data/public/search/aggs/agg_config.ts @@ -19,6 +19,8 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionAstFunction, ExpressionAstArgument } from 'src/plugins/expressions/public'; import { IAggType } from './agg_type'; import { writeParams } from './agg_params'; import { IAggConfigs } from './agg_configs'; @@ -27,11 +29,17 @@ import { ISearchSource } from '../search_source'; import { FieldFormatsContentType, KBN_FIELD_TYPES } from '../../../common'; import { FieldFormatsStart } from '../../field_formats'; -export interface AggConfigOptions { - type: IAggType; +type State = string | number | boolean | null | undefined | SerializableState; + +interface SerializableState { + [key: string]: State | State[]; +} + +export interface AggConfigSerialized { + type: string; enabled?: boolean; id?: string; - params?: Record; + params?: SerializableState; schema?: string; } @@ -39,6 +47,8 @@ export interface AggConfigDependencies { fieldFormats: FieldFormatsStart; } +export type AggConfigOptions = Assign; + /** * @name AggConfig * @@ -257,7 +267,10 @@ export class AggConfig { return configDsl; } - toJSON() { + /** + * @returns Returns a serialized representation of an AggConfig. + */ + serialize(): AggConfigSerialized { const params = this.params; const outParams = _.transform( @@ -281,7 +294,64 @@ export class AggConfig { enabled: this.enabled, type: this.type && this.type.name, schema: this.schema, - params: outParams, + params: outParams as SerializableState, + }; + } + + /** + * @deprecated - Use serialize() instead. + */ + toJSON(): AggConfigSerialized { + return this.serialize(); + } + + /** + * @returns Returns an ExpressionAst representing the function for this agg type. + */ + toExpressionAst(): ExpressionAstFunction | undefined { + const functionName = this.type && this.type.expressionName; + const { type, ...rest } = this.serialize(); + if (!functionName || !rest.params) { + // Return undefined - there is no matching expression function for this agg + return; + } + + // Go through each of the params and convert to an array of expression args. + const params = Object.entries(rest.params).reduce((acc, [key, value]) => { + const deserializedParam = this.getAggParams().find(p => p.name === key); + + if (deserializedParam && deserializedParam.toExpressionAst) { + // If the param provides `toExpressionAst`, we call it with the value + const paramExpressionAst = deserializedParam.toExpressionAst(this.getParam(key)); + if (paramExpressionAst) { + acc[key] = [ + { + type: 'expression', + chain: [paramExpressionAst], + }, + ]; + } + } else if (typeof value === 'object') { + // For object params which don't provide `toExpressionAst`, we stringify + acc[key] = [JSON.stringify(value)]; + } else if (typeof value !== 'undefined') { + // Everything else just gets stored in an array if it is defined + acc[key] = [value]; + } + + return acc; + }, {} as Record); + + return { + type: 'function', + function: functionName, + arguments: { + ...params, + // Expression args which are provided to all functions + id: [this.id], + enabled: [this.enabled], + ...(this.schema ? { schema: [this.schema] } : {}), // schema may be undefined + }, }; } diff --git a/src/plugins/data/public/search/aggs/agg_configs.ts b/src/plugins/data/public/search/aggs/agg_configs.ts index 5ad09f824d3e4..d2151a2c5ed4d 100644 --- a/src/plugins/data/public/search/aggs/agg_configs.ts +++ b/src/plugins/data/public/search/aggs/agg_configs.ts @@ -20,7 +20,7 @@ import _ from 'lodash'; import { Assign } from '@kbn/utility-types'; -import { AggConfig, AggConfigOptions, IAggConfig } from './agg_config'; +import { AggConfig, AggConfigSerialized, IAggConfig } from './agg_config'; import { IAggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { AggGroupNames } from './agg_groups'; @@ -51,7 +51,7 @@ export interface AggConfigsOptions { fieldFormats: FieldFormatsStart; } -export type CreateAggConfigParams = Assign; +export type CreateAggConfigParams = Assign; /** * @name AggConfigs diff --git a/src/plugins/data/public/search/aggs/agg_type.ts b/src/plugins/data/public/search/aggs/agg_type.ts index 70c116d560c6f..fb0cb609a08cf 100644 --- a/src/plugins/data/public/search/aggs/agg_type.ts +++ b/src/plugins/data/public/search/aggs/agg_type.ts @@ -39,6 +39,7 @@ export interface AggTypeConfig< createFilter?: (aggConfig: TAggConfig, key: any, params?: any) => any; type?: string; dslName?: string; + expressionName?: string; makeLabel?: ((aggConfig: TAggConfig) => string) | (() => string); ordered?: any; hasNoDsl?: boolean; @@ -88,6 +89,14 @@ export class AggType< * @type {string} */ dslName: string; + /** + * the name of the expression function that this aggType represents. + * TODO: this should probably be a required field. + * + * @property name + * @type {string} + */ + expressionName?: string; /** * the user friendly name that will be shown in the ui for this aggType * @@ -219,6 +228,7 @@ export class AggType< this.name = config.name; this.type = config.type || 'metrics'; this.dslName = config.dslName || config.name; + this.expressionName = config.expressionName; this.title = config.title; this.makeLabel = config.makeLabel || constant(this.name); this.ordered = config.ordered; diff --git a/src/plugins/data/public/search/aggs/agg_types.ts b/src/plugins/data/public/search/aggs/agg_types.ts index 4b154c338d48c..da07f581c9274 100644 --- a/src/plugins/data/public/search/aggs/agg_types.ts +++ b/src/plugins/data/public/search/aggs/agg_types.ts @@ -37,6 +37,7 @@ import { getDerivativeMetricAgg } from './metrics/derivative'; import { getCumulativeSumMetricAgg } from './metrics/cumulative_sum'; import { getMovingAvgMetricAgg } from './metrics/moving_avg'; import { getSerialDiffMetricAgg } from './metrics/serial_diff'; + import { getDateHistogramBucketAgg } from './buckets/date_histogram'; import { getHistogramBucketAgg } from './buckets/histogram'; import { getRangeBucketAgg } from './buckets/range'; @@ -103,3 +104,7 @@ export const getAggTypes = ({ getGeoTitleBucketAgg({ getInternalStartServices }), ], }); + +import { aggTerms } from './buckets/terms_fn'; + +export const getAggTypesFunctions = () => [aggTerms]; diff --git a/src/plugins/data/public/search/aggs/buckets/terms.ts b/src/plugins/data/public/search/aggs/buckets/terms.ts index 698e0dfb1d340..a12a1d7ac2d3d 100644 --- a/src/plugins/data/public/search/aggs/buckets/terms.ts +++ b/src/plugins/data/public/search/aggs/buckets/terms.ts @@ -26,7 +26,7 @@ import { isStringOrNumberType, migrateIncludeExcludeFormat, } from './migrate_include_exclude_format'; -import { IAggConfigs } from '../agg_configs'; +import { AggConfigSerialized, IAggConfigs } from '../types'; import { Adapters } from '../../../../../inspector/public'; import { ISearchSource } from '../../search_source'; @@ -63,10 +63,27 @@ export interface TermsBucketAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } +export interface AggParamsTerms { + field: string; + order: 'asc' | 'desc'; + orderBy: string; + orderAgg?: AggConfigSerialized; + size?: number; + missingBucket?: boolean; + missingBucketLabel?: string; + otherBucket?: boolean; + otherBucketLabel?: string; + // advanced + exclude?: string; + include?: string; + json?: string; +} + export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDependencies) => new BucketAggType( { name: BUCKET_TYPES.TERMS, + expressionName: 'aggTerms', title: termsTitle, makeLabel(agg) { const params = agg.params; @@ -154,8 +171,7 @@ export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDe type: 'agg', allowedAggs: termsAggFilter, default: null, - makeAgg(termsAgg, state) { - state = state || {}; + makeAgg(termsAgg, state = { type: 'count' }) { state.schema = 'orderAgg'; const orderAgg = termsAgg.aggConfigs.createAggConfig(state, { addToAggConfigs: false, diff --git a/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts b/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts new file mode 100644 index 0000000000000..f55f1de796013 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts @@ -0,0 +1,164 @@ +/* + * 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 { functionWrapper } from '../test_helpers'; +import { aggTerms } from './terms_fn'; + +describe('agg_expression_functions', () => { + describe('aggTerms', () => { + const fn = functionWrapper(aggTerms()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + order: 'asc', + orderBy: '1', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "exclude": undefined, + "field": "machine.os.keyword", + "include": undefined, + "json": undefined, + "missingBucket": false, + "missingBucketLabel": "Missing", + "order": "asc", + "orderAgg": undefined, + "orderBy": "1", + "otherBucket": false, + "otherBucketLabel": "Other", + "size": 5, + }, + "schema": undefined, + "type": "terms", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + id: '1', + enabled: false, + schema: 'whatever', + field: 'machine.os.keyword', + order: 'desc', + orderBy: '2', + size: 6, + missingBucket: true, + missingBucketLabel: 'missing', + otherBucket: true, + otherBucketLabel: 'other', + exclude: 'ios', + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": false, + "id": "1", + "params": Object { + "exclude": "ios", + "field": "machine.os.keyword", + "include": undefined, + "json": undefined, + "missingBucket": true, + "missingBucketLabel": "missing", + "order": "desc", + "orderAgg": undefined, + "orderBy": "2", + "otherBucket": true, + "otherBucketLabel": "other", + "size": 6, + }, + "schema": "whatever", + "type": "terms", + } + `); + }); + + test('handles orderAgg as a subexpression', () => { + const actual = fn({ + field: 'machine.os.keyword', + order: 'asc', + orderBy: '1', + orderAgg: fn({ field: 'name', order: 'asc', orderBy: '1' }), + }); + + expect(actual.value.params).toMatchInlineSnapshot(` + Object { + "exclude": undefined, + "field": "machine.os.keyword", + "include": undefined, + "json": undefined, + "missingBucket": false, + "missingBucketLabel": "Missing", + "order": "asc", + "orderAgg": Object { + "enabled": true, + "id": undefined, + "params": Object { + "exclude": undefined, + "field": "name", + "include": undefined, + "json": undefined, + "missingBucket": false, + "missingBucketLabel": "Missing", + "order": "asc", + "orderAgg": undefined, + "orderBy": "1", + "otherBucket": false, + "otherBucketLabel": "Other", + "size": 5, + }, + "schema": undefined, + "type": "terms", + }, + "orderBy": "1", + "otherBucket": false, + "otherBucketLabel": "Other", + "size": 5, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + order: 'asc', + orderBy: '1', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + field: 'machine.os.keyword', + order: 'asc', + orderBy: '1', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/buckets/terms_fn.ts b/src/plugins/data/public/search/aggs/buckets/terms_fn.ts new file mode 100644 index 0000000000000..7980bfabe79fb --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/terms_fn.ts @@ -0,0 +1,181 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs } from '../'; + +const aggName = 'terms'; +const fnName = 'aggTerms'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +// Since the orderAgg param is an agg nested in a subexpression, we need to +// overwrite the param type to expect a value of type AggExpressionType. +type Arguments = AggArgs & + Assign< + AggArgs, + { orderAgg?: AggArgs['orderAgg'] extends undefined ? undefined : AggExpressionType } + >; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggTerms = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.buckets.terms.help', { + defaultMessage: 'Generates a serialized agg config for a terms agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.terms.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.buckets.terms.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + order: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.buckets.terms.order.help', { + defaultMessage: 'Order in which to return the results: asc or desc', + }), + }, + orderBy: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.orderBy.help', { + defaultMessage: 'Field to order results by', + }), + }, + orderAgg: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.buckets.terms.orderAgg.help', { + defaultMessage: 'Agg config to use for ordering results', + }), + }, + size: { + types: ['number'], + default: 5, + help: i18n.translate('data.search.aggs.buckets.terms.size.help', { + defaultMessage: 'Max number of buckets to retrieve', + }), + }, + missingBucket: { + types: ['boolean'], + default: false, + help: i18n.translate('data.search.aggs.buckets.terms.missingBucket.help', { + defaultMessage: 'When set to true, groups together any buckets with missing fields', + }), + }, + missingBucketLabel: { + types: ['string'], + default: i18n.translate('data.search.aggs.buckets.terms.missingBucketLabel', { + defaultMessage: 'Missing', + description: `Default label used in charts when documents are missing a field. + Visible when you create a chart with a terms aggregation and enable "Show missing values"`, + }), + help: i18n.translate('data.search.aggs.buckets.terms.missingBucketLabel.help', { + defaultMessage: 'Default label used in charts when documents are missing a field.', + }), + }, + otherBucket: { + types: ['boolean'], + default: false, + help: i18n.translate('data.search.aggs.buckets.terms.otherBucket.help', { + defaultMessage: 'When set to true, groups together any buckets beyond the allowed size', + }), + }, + otherBucketLabel: { + types: ['string'], + default: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel', { + defaultMessage: 'Other', + }), + help: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel.help', { + defaultMessage: 'Default label used in charts for documents in the Other bucket', + }), + }, + exclude: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.exclude.help', { + defaultMessage: 'Specific bucket values to exclude from results', + }), + }, + include: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.include.help', { + defaultMessage: 'Specific bucket values to include in results', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + let json; + try { + json = args.json ? JSON.parse(args.json) : undefined; + } catch (e) { + throw new Error('Unable to parse json argument string'); + } + + // Need to spread this object to work around TS bug: + // https://github.com/microsoft/TypeScript/issues/15300#issuecomment-436793742 + const orderAgg = args.orderAgg?.value ? { ...args.orderAgg.value } : undefined; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: aggName, + params: { + ...rest, + orderAgg, + json, + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts index 3868d8f1bcd16..947394c97bdcd 100644 --- a/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts @@ -36,14 +36,14 @@ const metricAggFilter = [ '!geo_centroid', ]; -const parentPipelineType = i18n.translate( +export const parentPipelineType = i18n.translate( 'data.search.aggs.metrics.parentPipelineAggregationsSubtypeTitle', { defaultMessage: 'Parent Pipeline Aggregations', } ); -const parentPipelineAggHelper = { +export const parentPipelineAggHelper = { subtype: parentPipelineType, params() { return [ @@ -56,13 +56,9 @@ const parentPipelineAggHelper = { name: 'customMetric', type: 'agg', allowedAggs: metricAggFilter, - makeAgg(termsAgg, state: any) { - state = state || { type: 'count' }; - + makeAgg(termsAgg, state = { type: 'count' }) { const metricAgg = termsAgg.aggConfigs.createAggConfig(state, { addToAggConfigs: false }); - metricAgg.id = termsAgg.id + '-metric'; - return metricAgg; }, modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart( @@ -89,5 +85,3 @@ const parentPipelineAggHelper = { return subAgg ? subAgg.type.getFormat(subAgg) : new (FieldFormat.from(identity))(); }, }; - -export { parentPipelineAggHelper, parentPipelineType }; diff --git a/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts index c1d05a39285b7..cee7841a8c3b9 100644 --- a/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts @@ -43,14 +43,14 @@ const metricAggFilter: string[] = [ ]; const bucketAggFilter: string[] = []; -const siblingPipelineType = i18n.translate( +export const siblingPipelineType = i18n.translate( 'data.search.aggs.metrics.siblingPipelineAggregationsSubtypeTitle', { defaultMessage: 'Sibling pipeline aggregations', } ); -const siblingPipelineAggHelper = { +export const siblingPipelineAggHelper = { subtype: siblingPipelineType, params() { return [ @@ -59,11 +59,9 @@ const siblingPipelineAggHelper = { type: 'agg', allowedAggs: bucketAggFilter, default: null, - makeAgg(agg: IMetricAggConfig, state: any) { - state = state || { type: 'date_histogram' }; + makeAgg(agg: IMetricAggConfig, state = { type: 'date_histogram' }) { const orderAgg = agg.aggConfigs.createAggConfig(state, { addToAggConfigs: false }); orderAgg.id = agg.id + '-bucket'; - return orderAgg; }, modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart( @@ -76,11 +74,9 @@ const siblingPipelineAggHelper = { type: 'agg', allowedAggs: metricAggFilter, default: null, - makeAgg(agg: IMetricAggConfig, state: any) { - state = state || { type: 'count' }; + makeAgg(agg: IMetricAggConfig, state = { type: 'count' }) { const orderAgg = agg.aggConfigs.createAggConfig(state, { addToAggConfigs: false }); orderAgg.id = agg.id + '-metric'; - return orderAgg; }, modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart( @@ -98,5 +94,3 @@ const siblingPipelineAggHelper = { : new (FieldFormat.from(identity))(); }, }; - -export { siblingPipelineAggHelper, siblingPipelineType }; diff --git a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts index d20530a17ca65..7491f15aa3002 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts @@ -25,21 +25,28 @@ import { import { AggConfigs, IAggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; +import { FieldFormatsStart } from '../../../field_formats'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; import { InternalStartServices } from '../../../types'; describe('AggTypesMetricsPercentileRanksProvider class', function() { let aggConfigs: IAggConfigs; - const aggTypesDependencies: PercentileRanksMetricAggDependencies = { - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; + let fieldFormats: FieldFormatsStart; + let aggTypesDependencies: PercentileRanksMetricAggDependencies; beforeEach(() => { + fieldFormats = fieldFormatsServiceMock.createStartContract(); + fieldFormats.getDefaultInstance = (() => ({ + convert: (t?: string) => t, + })) as any; + aggTypesDependencies = { + getInternalStartServices: () => + (({ + fieldFormats, + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), + }; const typesRegistry = mockAggTypesRegistry([getPercentileRanksMetricAgg(aggTypesDependencies)]); const field = { name: 'bytes', @@ -61,12 +68,7 @@ describe('AggTypesMetricsPercentileRanksProvider class', function() { type: METRIC_TYPES.PERCENTILE_RANKS, schema: 'metric', params: { - field: { - displayName: 'bytes', - format: { - convert: jest.fn(x => x), - }, - }, + field: 'bytes', customLabel: 'my custom field label', values: [5000, 10000], }, diff --git a/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts b/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts index 0ac1e8417514c..76382c01bcc10 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts @@ -61,12 +61,7 @@ describe('AggTypesMetricsPercentilesProvider class', () => { type: METRIC_TYPES.PERCENTILES, schema: 'metric', params: { - field: { - displayName: 'bytes', - format: { - convert: jest.fn(x => `${x}th`), - }, - }, + field: 'bytes', customLabel: 'prince', percents: [95], }, diff --git a/src/plugins/data/public/search/aggs/param_types/agg.ts b/src/plugins/data/public/search/aggs/param_types/agg.ts index e5b53020c3159..e3f8c7c922170 100644 --- a/src/plugins/data/public/search/aggs/param_types/agg.ts +++ b/src/plugins/data/public/search/aggs/param_types/agg.ts @@ -17,13 +17,13 @@ * under the License. */ -import { AggConfig, IAggConfig } from '../agg_config'; +import { AggConfig, IAggConfig, AggConfigSerialized } from '../agg_config'; import { BaseParamType } from './base'; export class AggParamType extends BaseParamType< TAggConfig > { - makeAgg: (agg: TAggConfig, state?: any) => TAggConfig; + makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig; allowedAggs: string[] = []; constructor(config: Record) { @@ -42,17 +42,25 @@ export class AggParamType extends Ba } if (!config.serialize) { this.serialize = (agg: TAggConfig) => { - return agg.toJSON(); + return agg.serialize(); }; } if (!config.deserialize) { - this.deserialize = (state: unknown, agg?: TAggConfig): TAggConfig => { + this.deserialize = (state: AggConfigSerialized, agg?: TAggConfig): TAggConfig => { if (!agg) { throw new Error('aggConfig was not provided to AggParamType deserialize function'); } return this.makeAgg(agg, state); }; } + if (!config.toExpressionAst) { + this.toExpressionAst = (agg: TAggConfig) => { + if (!agg || !agg.toExpressionAst) { + throw new Error('aggConfig was not provided to AggParamType toExpressionAst function'); + } + return agg.toExpressionAst(); + }; + } this.makeAgg = config.makeAgg; this.valueType = AggConfig; diff --git a/src/plugins/data/public/search/aggs/param_types/base.ts b/src/plugins/data/public/search/aggs/param_types/base.ts index 2cbc5866e284d..a6f7e5adea043 100644 --- a/src/plugins/data/public/search/aggs/param_types/base.ts +++ b/src/plugins/data/public/search/aggs/param_types/base.ts @@ -17,6 +17,7 @@ * under the License. */ +import { ExpressionAstFunction } from 'src/plugins/expressions/public'; import { IAggConfigs } from '../agg_configs'; import { IAggConfig } from '../agg_config'; import { FetchOptions } from '../../fetch'; @@ -37,6 +38,7 @@ export class BaseParamType { ) => void; serialize: (value: any, aggConfig?: TAggConfig) => any; deserialize: (value: any, aggConfig?: TAggConfig) => any; + toExpressionAst?: (value: any) => ExpressionAstFunction | undefined; options: any[]; valueType?: any; @@ -77,6 +79,7 @@ export class BaseParamType { this.write = config.write || defaultWrite; this.serialize = config.serialize; this.deserialize = config.deserialize; + this.toExpressionAst = config.toExpressionAst; this.options = config.options; this.modifyAggConfigOnSearchRequestStart = config.modifyAggConfigOnSearchRequestStart || function() {}; diff --git a/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts b/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts new file mode 100644 index 0000000000000..cb0e37c0296d7 --- /dev/null +++ b/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts @@ -0,0 +1,49 @@ +/* + * 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 { mapValues } from 'lodash'; +import { + AnyExpressionFunctionDefinition, + ExpressionFunctionDefinition, + ExecutionContext, +} from '../../../../../../plugins/expressions/public'; + +/** + * Takes a function spec and passes in default args, + * overriding with any provided args. + * + * Similar to the test helper used in Expressions & Canvas, + * however in this case we are ignoring the input & execution + * context, as they are not applicable to the agg type + * expression functions. + */ +export const functionWrapper = (spec: T) => { + const defaultArgs = mapValues(spec.args, argSpec => argSpec.default); + return ( + args: T extends ExpressionFunctionDefinition< + infer Name, + infer Input, + infer Arguments, + infer Output, + infer Context + > + ? Arguments + : never + ) => spec.fn(null, { ...defaultArgs, ...args }, {} as ExecutionContext); +}; diff --git a/src/plugins/data/public/search/aggs/test_helpers/index.ts b/src/plugins/data/public/search/aggs/test_helpers/index.ts index 131f921586144..63f8ae0ce5f58 100644 --- a/src/plugins/data/public/search/aggs/test_helpers/index.ts +++ b/src/plugins/data/public/search/aggs/test_helpers/index.ts @@ -17,5 +17,6 @@ * under the License. */ +export { functionWrapper } from './function_wrapper'; export { mockAggTypesRegistry } from './mock_agg_types_registry'; export { mockDataServices } from './mock_data_services'; diff --git a/src/plugins/data/public/search/aggs/types.ts b/src/plugins/data/public/search/aggs/types.ts index 4b2b1620ad1d3..95a7a45013567 100644 --- a/src/plugins/data/public/search/aggs/types.ts +++ b/src/plugins/data/public/search/aggs/types.ts @@ -19,21 +19,23 @@ import { IndexPattern } from '../../index_patterns'; import { + AggConfig, + AggConfigSerialized, + AggConfigs, + AggParamsTerms, AggType, + aggTypeFieldFilters, AggTypesRegistrySetup, AggTypesRegistryStart, - AggConfig, - AggConfigs, CreateAggConfigParams, FieldParamType, getCalculateAutoTimeExpression, MetricAggType, - aggTypeFieldFilters, parentPipelineAggHelper, siblingPipelineAggHelper, } from './'; -export { IAggConfig } from './agg_config'; +export { IAggConfig, AggConfigSerialized } from './agg_config'; export { CreateAggConfigParams, IAggConfigs } from './agg_configs'; export { IAggType } from './agg_type'; export { AggParam, AggParamOption } from './agg_params'; @@ -70,3 +72,25 @@ export interface SearchAggsStart { ) => InstanceType; types: AggTypesRegistryStart; } + +/** @internal */ +export interface AggExpressionType { + type: 'agg_type'; + value: AggConfigSerialized; +} + +/** @internal */ +export type AggExpressionFunctionArgs< + Name extends keyof AggParamsMapping +> = AggParamsMapping[Name] & Pick; + +/** + * A global list of the param interfaces for each agg type. + * For now this is internal, but eventually we will probably + * want to make it public. + * + * @internal + */ +export interface AggParamsMapping { + terms: AggParamsTerms; +} diff --git a/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts b/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts index 4ca976d328c91..78b4935077d10 100644 --- a/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts +++ b/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts @@ -27,7 +27,7 @@ export const serializeAggConfig = (aggConfig: IAggConfig): KibanaDatatableColumn return { type: aggConfig.type.name, indexPatternId: aggConfig.getIndexPattern().id, - aggConfigParams: aggConfig.toJSON().params, + aggConfigParams: aggConfig.serialize().params, }; }; diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts index 19308dd387d3a..b1f7925bec4bb 100644 --- a/src/plugins/data/public/search/search_service.test.ts +++ b/src/plugins/data/public/search/search_service.test.ts @@ -18,9 +18,10 @@ */ import { coreMock } from '../../../../core/public/mocks'; +import { CoreSetup } from '../../../../core/public'; +import { expressionsPluginMock } from '../../../../plugins/expressions/public/mocks'; import { SearchService } from './search_service'; -import { CoreSetup } from '../../../../core/public'; describe('Search service', () => { let searchService: SearchService; @@ -35,6 +36,7 @@ describe('Search service', () => { it('exposes proper contract', async () => { const setup = searchService.setup(mockCoreSetup, { packageInfo: { version: '8' }, + expressions: expressionsPluginMock.createSetupContract(), } as any); expect(setup).toHaveProperty('registerSearchStrategyProvider'); }); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 3f3266b5fe90f..b59524baa9fa7 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -18,6 +18,7 @@ */ import { Plugin, CoreSetup, CoreStart, PackageInfo } from '../../../../core/public'; +import { ExpressionsSetup } from '../../../../plugins/expressions/public'; import { SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider } from './sync_search_strategy'; import { @@ -37,6 +38,7 @@ import { GetInternalStartServicesFn } from '../types'; import { SearchInterceptor } from './search_interceptor'; import { getAggTypes, + getAggTypesFunctions, AggType, AggTypesRegistry, AggConfig, @@ -52,9 +54,10 @@ import { FieldFormatsStart } from '../field_formats'; import { ISearchGeneric } from './i_search'; interface SearchServiceSetupDependencies { + expressions: ExpressionsSetup; + getInternalStartServices: GetInternalStartServicesFn; packageInfo: PackageInfo; query: QuerySetup; - getInternalStartServices: GetInternalStartServicesFn; } interface SearchServiceStartDependencies { @@ -97,22 +100,27 @@ export class SearchService implements Plugin { public setup( core: CoreSetup, - { packageInfo, query, getInternalStartServices }: SearchServiceSetupDependencies + { expressions, packageInfo, query, getInternalStartServices }: SearchServiceSetupDependencies ): ISearchSetup { this.esClient = getEsClient(core.injectedMetadata, core.http, packageInfo); this.registerSearchStrategyProvider(SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider); this.registerSearchStrategyProvider(ES_SEARCH_STRATEGY, esSearchStrategyProvider); const aggTypesSetup = this.aggTypesRegistry.setup(); + + // register each agg type const aggTypes = getAggTypes({ query, uiSettings: core.uiSettings, getInternalStartServices, }); - aggTypes.buckets.forEach(b => aggTypesSetup.registerBucket(b)); aggTypes.metrics.forEach(m => aggTypesSetup.registerMetric(m)); + // register expression functions for each agg type + const aggFunctions = getAggTypesFunctions(); + aggFunctions.forEach(fn => expressions.registerFunction(fn)); + return { aggs: { calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings), From a53d53369c091c68f688e36d8ab35e97572687ec Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Thu, 23 Apr 2020 11:11:18 -0700 Subject: [PATCH 08/35] [DOCS] Fixes formatting in alerting doc (#64338) --- docs/user/alerting/pre-configured-connectors.asciidoc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/user/alerting/pre-configured-connectors.asciidoc b/docs/user/alerting/pre-configured-connectors.asciidoc index 3db13acfb423e..4c408da92f579 100644 --- a/docs/user/alerting/pre-configured-connectors.asciidoc +++ b/docs/user/alerting/pre-configured-connectors.asciidoc @@ -20,8 +20,7 @@ action are predefined, including the connector name and ID. The following example shows a valid configuration 2 out-of-the box connector. -[source,console] ------------------------- +```js xpack.actions.preconfigured: - id: 'my-slack1' <1> actionTypeId: .slack <2> @@ -40,7 +39,7 @@ The following example shows a valid configuration 2 out-of-the box connector. secrets: <5> user: elastic password: changeme ------------------------- +``` <1> `id` is the action connector identifier. <2> `actionTypeId` is the action type identifier. From 44f9cbcb60187e69343c207d9bd4443aeb6322fa Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 23 Apr 2020 15:23:39 -0400 Subject: [PATCH 09/35] Expose ability to check if API Keys are enabled (#63454) * expose ability to check if API Keys are enabled * fix mock * Fix typo in test name * simplify key check * fix privilege check * remove unused variable * address PR feedback Co-authored-by: Elastic Machine --- .../authentication/authentication_service.ts | 11 +- .../public/authentication/index.mock.ts | 1 + .../overwritten_session_app.ts | 2 +- .../overwritten_session_page.tsx | 2 +- .../api_keys/api_keys_api_client.ts | 1 + .../api_keys_grid/api_keys_grid_page.test.tsx | 10 +- .../api_keys_grid/api_keys_grid_page.tsx | 46 +++-- .../plugins/security/public/plugin.test.tsx | 4 +- .../server/authentication/api_keys.test.ts | 76 ++++++++ .../server/authentication/api_keys.ts | 34 ++++ .../server/authentication/index.mock.ts | 1 + .../security/server/authentication/index.ts | 1 + x-pack/plugins/security/server/plugin.test.ts | 1 + .../server/routes/api_keys/enabled.test.ts | 118 +++++++++++++ .../server/routes/api_keys/enabled.ts | 27 +++ .../security/server/routes/api_keys/index.ts | 2 + .../server/routes/api_keys/privileges.test.ts | 167 +++++++++++++----- .../server/routes/api_keys/privileges.ts | 31 ++-- .../api_integration/apis/security/api_keys.ts | 28 +++ .../api_integration/apis/security/index.js | 1 + .../apis/security/security_basic.ts | 1 + .../api_integration/config_security_basic.js | 1 + 22 files changed, 479 insertions(+), 87 deletions(-) create mode 100644 x-pack/plugins/security/server/routes/api_keys/enabled.test.ts create mode 100644 x-pack/plugins/security/server/routes/api_keys/enabled.ts create mode 100644 x-pack/test/api_integration/apis/security/api_keys.ts diff --git a/x-pack/plugins/security/public/authentication/authentication_service.ts b/x-pack/plugins/security/public/authentication/authentication_service.ts index 979f7095cf933..2e73b8cd04482 100644 --- a/x-pack/plugins/security/public/authentication/authentication_service.ts +++ b/x-pack/plugins/security/public/authentication/authentication_service.ts @@ -25,6 +25,11 @@ export interface AuthenticationServiceSetup { * Returns currently authenticated user and throws if current user isn't authenticated. */ getCurrentUser: () => Promise; + + /** + * Determines if API Keys are currently enabled. + */ + areAPIKeysEnabled: () => Promise; } export class AuthenticationService { @@ -37,11 +42,15 @@ export class AuthenticationService { const getCurrentUser = async () => (await http.get('/internal/security/me', { asSystemRequest: true })) as AuthenticatedUser; + const areAPIKeysEnabled = async () => + ((await http.get('/internal/security/api_key/_enabled')) as { apiKeysEnabled: boolean }) + .apiKeysEnabled; + loginApp.create({ application, config, getStartServices, http }); logoutApp.create({ application, http }); loggedOutApp.create({ application, getStartServices, http }); overwrittenSessionApp.create({ application, authc: { getCurrentUser }, getStartServices }); - return { getCurrentUser }; + return { getCurrentUser, areAPIKeysEnabled }; } } diff --git a/x-pack/plugins/security/public/authentication/index.mock.ts b/x-pack/plugins/security/public/authentication/index.mock.ts index c8d77a5b62c6f..dee0a24ab27c2 100644 --- a/x-pack/plugins/security/public/authentication/index.mock.ts +++ b/x-pack/plugins/security/public/authentication/index.mock.ts @@ -9,5 +9,6 @@ import { AuthenticationServiceSetup } from './authentication_service'; export const authenticationMock = { createSetup: (): jest.Mocked => ({ getCurrentUser: jest.fn(), + areAPIKeysEnabled: jest.fn(), }), }; diff --git a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts index 8e0ee73dfb613..213c26d5287dc 100644 --- a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts +++ b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts @@ -10,7 +10,7 @@ import { AuthenticationServiceSetup } from '../authentication_service'; interface CreateDeps { application: ApplicationSetup; - authc: AuthenticationServiceSetup; + authc: Pick; getStartServices: StartServicesAccessor; } diff --git a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx index 1093957761d1c..5b77266068ebf 100644 --- a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx +++ b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx @@ -14,7 +14,7 @@ import { AuthenticationStatePage } from '../components'; interface Props { basePath: IBasePath; - authc: AuthenticationServiceSetup; + authc: Pick; } export function OverwrittenSessionPage({ authc, basePath }: Props) { diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts index 372b1e56a73c4..a127379d97241 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts @@ -10,6 +10,7 @@ import { ApiKey, ApiKeyToInvalidate } from '../../../common/model'; interface CheckPrivilegesResponse { areApiKeysEnabled: boolean; isAdmin: boolean; + canManage: boolean; } interface InvalidateApiKeysResponse { diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx index ae6ef4aa0fc34..dea04a0eac396 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx @@ -18,7 +18,6 @@ import { APIKeysGridPage } from './api_keys_grid_page'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { apiKeysAPIClientMock } from '../index.mock'; -const mock403 = () => ({ body: { statusCode: 403 } }); const mock500 = () => ({ body: { error: 'Internal Server Error', message: '', statusCode: 500 } }); const waitForRender = async ( @@ -48,6 +47,7 @@ describe('APIKeysGridPage', () => { apiClientMock.checkPrivileges.mockResolvedValue({ isAdmin: true, areApiKeysEnabled: true, + canManage: true, }); apiClientMock.getApiKeys.mockResolvedValue({ apiKeys: [ @@ -82,6 +82,7 @@ describe('APIKeysGridPage', () => { it('renders a callout when API keys are not enabled', async () => { apiClientMock.checkPrivileges.mockResolvedValue({ isAdmin: true, + canManage: true, areApiKeysEnabled: false, }); @@ -95,7 +96,11 @@ describe('APIKeysGridPage', () => { }); it('renders permission denied if user does not have required permissions', async () => { - apiClientMock.checkPrivileges.mockRejectedValue(mock403()); + apiClientMock.checkPrivileges.mockResolvedValue({ + canManage: false, + isAdmin: false, + areApiKeysEnabled: true, + }); const wrapper = mountWithIntl(); @@ -152,6 +157,7 @@ describe('APIKeysGridPage', () => { beforeEach(() => { apiClientMock.checkPrivileges.mockResolvedValue({ isAdmin: false, + canManage: true, areApiKeysEnabled: true, }); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index 698c0d37dbc64..9db09a34d3c3f 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -26,7 +26,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment-timezone'; -import _ from 'lodash'; import { NotificationsStart } from 'src/core/public'; import { SectionLoading } from '../../../../../../../src/plugins/es_ui_shared/public'; import { ApiKey, ApiKeyToInvalidate } from '../../../../common/model'; @@ -47,10 +46,10 @@ interface State { isLoadingApp: boolean; isLoadingTable: boolean; isAdmin: boolean; + canManage: boolean; areApiKeysEnabled: boolean; apiKeys: ApiKey[]; selectedItems: ApiKey[]; - permissionDenied: boolean; error: any; } @@ -63,9 +62,9 @@ export class APIKeysGridPage extends Component { isLoadingApp: true, isLoadingTable: false, isAdmin: false, + canManage: false, areApiKeysEnabled: false, apiKeys: [], - permissionDenied: false, selectedItems: [], error: undefined, }; @@ -77,19 +76,15 @@ export class APIKeysGridPage extends Component { public render() { const { - permissionDenied, isLoadingApp, isLoadingTable, areApiKeysEnabled, isAdmin, + canManage, error, apiKeys, } = this.state; - if (permissionDenied) { - return ; - } - if (isLoadingApp) { return ( @@ -103,6 +98,10 @@ export class APIKeysGridPage extends Component { ); } + if (!canManage) { + return ; + } + if (error) { const { body: { error: errorTitle, message, statusCode }, @@ -495,26 +494,25 @@ export class APIKeysGridPage extends Component { private async checkPrivileges() { try { - const { isAdmin, areApiKeysEnabled } = await this.props.apiKeysAPIClient.checkPrivileges(); - this.setState({ isAdmin, areApiKeysEnabled }); + const { + isAdmin, + canManage, + areApiKeysEnabled, + } = await this.props.apiKeysAPIClient.checkPrivileges(); + this.setState({ isAdmin, canManage, areApiKeysEnabled }); - if (areApiKeysEnabled) { - this.initiallyLoadApiKeys(); - } else { - // We're done loading and will just show the "Disabled" error. + if (!canManage || !areApiKeysEnabled) { this.setState({ isLoadingApp: false }); - } - } catch (e) { - if (_.get(e, 'body.statusCode') === 403) { - this.setState({ permissionDenied: true, isLoadingApp: false }); } else { - this.props.notifications.toasts.addDanger( - i18n.translate('xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', { - defaultMessage: 'Error checking privileges: {message}', - values: { message: _.get(e, 'body.message', '') }, - }) - ); + this.initiallyLoadApiKeys(); } + } catch (e) { + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', { + defaultMessage: 'Error checking privileges: {message}', + values: { message: e.body?.message ?? '' }, + }) + ); } } diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index 122b26378d22b..7c57c4dd997a2 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -37,7 +37,7 @@ describe('Security Plugin', () => { ) ).toEqual({ __legacyCompat: { logoutUrl: '/some-base-path/logout', tenant: '/some-base-path' }, - authc: { getCurrentUser: expect.any(Function) }, + authc: { getCurrentUser: expect.any(Function), areAPIKeysEnabled: expect.any(Function) }, license: { isEnabled: expect.any(Function), getFeatures: expect.any(Function), @@ -63,7 +63,7 @@ describe('Security Plugin', () => { expect(setupManagementServiceMock).toHaveBeenCalledTimes(1); expect(setupManagementServiceMock).toHaveBeenCalledWith({ - authc: { getCurrentUser: expect.any(Function) }, + authc: { getCurrentUser: expect.any(Function), areAPIKeysEnabled: expect.any(Function) }, license: { isEnabled: expect.any(Function), getFeatures: expect.any(Function), diff --git a/x-pack/plugins/security/server/authentication/api_keys.test.ts b/x-pack/plugins/security/server/authentication/api_keys.test.ts index 836740d0a547f..9f2a628b575d5 100644 --- a/x-pack/plugins/security/server/authentication/api_keys.test.ts +++ b/x-pack/plugins/security/server/authentication/api_keys.test.ts @@ -40,6 +40,82 @@ describe('API Keys', () => { }); }); + describe('areAPIKeysEnabled()', () => { + it('returns false when security feature is disabled', async () => { + mockLicense.isEnabled.mockReturnValue(false); + + const result = await apiKeys.areAPIKeysEnabled(); + expect(result).toEqual(false); + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect(mockScopedClusterClient.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockClusterClient.callAsInternalUser).not.toHaveBeenCalled(); + }); + + it('returns false when the exception metadata indicates api keys are disabled', async () => { + mockLicense.isEnabled.mockReturnValue(true); + const error = new Error(); + (error as any).body = { + error: { 'disabled.feature': 'api_keys' }, + }; + mockClusterClient.callAsInternalUser.mockRejectedValue(error); + const result = await apiKeys.areAPIKeysEnabled(); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(result).toEqual(false); + }); + + it('returns true when the operation completes without error', async () => { + mockLicense.isEnabled.mockReturnValue(true); + mockClusterClient.callAsInternalUser.mockResolvedValue({}); + const result = await apiKeys.areAPIKeysEnabled(); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(result).toEqual(true); + }); + + it('throws the original error when exception metadata does not indicate that api keys are disabled', async () => { + mockLicense.isEnabled.mockReturnValue(true); + const error = new Error(); + (error as any).body = { + error: { 'disabled.feature': 'something_else' }, + }; + + mockClusterClient.callAsInternalUser.mockRejectedValue(error); + expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + }); + + it('throws the original error when exception metadata does not contain `disabled.feature`', async () => { + mockLicense.isEnabled.mockReturnValue(true); + const error = new Error(); + (error as any).body = {}; + + mockClusterClient.callAsInternalUser.mockRejectedValue(error); + expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + }); + + it('throws the original error when exception contains no metadata', async () => { + mockLicense.isEnabled.mockReturnValue(true); + const error = new Error(); + + mockClusterClient.callAsInternalUser.mockRejectedValue(error); + expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + }); + + it('calls callCluster with proper parameters', async () => { + mockLicense.isEnabled.mockReturnValue(true); + mockClusterClient.callAsInternalUser.mockResolvedValueOnce({}); + + const result = await apiKeys.areAPIKeysEnabled(); + expect(result).toEqual(true); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('shield.invalidateAPIKey', { + body: { + id: 'kibana-api-key-service-test', + }, + }); + }); + }); + describe('create()', () => { it('returns null when security feature is disabled', async () => { mockLicense.isEnabled.mockReturnValue(false); diff --git a/x-pack/plugins/security/server/authentication/api_keys.ts b/x-pack/plugins/security/server/authentication/api_keys.ts index 9df7219cec334..29ff7e1f69f95 100644 --- a/x-pack/plugins/security/server/authentication/api_keys.ts +++ b/x-pack/plugins/security/server/authentication/api_keys.ts @@ -125,6 +125,35 @@ export class APIKeys { this.license = license; } + /** + * Determines if API Keys are enabled in Elasticsearch. + */ + async areAPIKeysEnabled(): Promise { + if (!this.license.isEnabled()) { + return false; + } + + const id = `kibana-api-key-service-test`; + + this.logger.debug( + `Testing if API Keys are enabled by attempting to invalidate a non-existant key: ${id}` + ); + + try { + await this.clusterClient.callAsInternalUser('shield.invalidateAPIKey', { + body: { + id, + }, + }); + return true; + } catch (e) { + if (this.doesErrorIndicateAPIKeysAreDisabled(e)) { + return false; + } + throw e; + } + } + /** * Tries to create an API key for the current user. * @param request Request instance. @@ -247,6 +276,11 @@ export class APIKeys { return result; } + private doesErrorIndicateAPIKeysAreDisabled(e: Record) { + const disabledFeature = e.body?.error?.['disabled.feature']; + return disabledFeature === 'api_keys'; + } + private getGrantParams(authorizationHeader: HTTPAuthorizationHeader): GrantAPIKeyParams { if (authorizationHeader.scheme.toLowerCase() === 'bearer') { return { diff --git a/x-pack/plugins/security/server/authentication/index.mock.ts b/x-pack/plugins/security/server/authentication/index.mock.ts index 8092c1c81017b..9397a7a42b326 100644 --- a/x-pack/plugins/security/server/authentication/index.mock.ts +++ b/x-pack/plugins/security/server/authentication/index.mock.ts @@ -11,6 +11,7 @@ export const authenticationMock = { login: jest.fn(), logout: jest.fn(), isProviderTypeEnabled: jest.fn(), + areAPIKeysEnabled: jest.fn(), createAPIKey: jest.fn(), getCurrentUser: jest.fn(), grantAPIKeyAsInternalUser: jest.fn(), diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 5d7b49de68d28..d76a5a533d498 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -172,6 +172,7 @@ export async function setupAuthentication({ getSessionInfo: authenticator.getSessionInfo.bind(authenticator), isProviderTypeEnabled: authenticator.isProviderTypeEnabled.bind(authenticator), getCurrentUser, + areAPIKeysEnabled: () => apiKeys.areAPIKeysEnabled(), createAPIKey: (request: KibanaRequest, params: CreateAPIKeyParams) => apiKeys.create(request, params), grantAPIKeyAsInternalUser: (request: KibanaRequest) => apiKeys.grantAsInternalUser(request), diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 4767f57de764c..3ce0198273af9 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -69,6 +69,7 @@ describe('Security Plugin', () => { "registerPrivilegesWithCluster": [Function], }, "authc": Object { + "areAPIKeysEnabled": [Function], "createAPIKey": [Function], "getCurrentUser": [Function], "getSessionInfo": [Function], diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts new file mode 100644 index 0000000000000..3c6dc3c0d7bda --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts @@ -0,0 +1,118 @@ +/* + * 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 { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { LicenseCheck } from '../../../../licensing/server'; + +import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../index.mock'; +import Boom from 'boom'; +import { defineEnabledApiKeysRoutes } from './enabled'; +import { APIKeys } from '../../authentication/api_keys'; + +interface TestOptions { + licenseCheckResult?: LicenseCheck; + apiResponse?: () => Promise; + asserts: { statusCode: number; result?: Record }; +} + +describe('API keys enabled', () => { + const enabledApiKeysTest = ( + description: string, + { licenseCheckResult = { state: 'valid' }, apiResponse, asserts }: TestOptions + ) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + const apiKeys = new APIKeys({ + logger: mockRouteDefinitionParams.logger, + clusterClient: mockRouteDefinitionParams.clusterClient, + license: mockRouteDefinitionParams.license, + }); + + mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockImplementation(() => + apiKeys.areAPIKeysEnabled() + ); + + if (apiResponse) { + mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementation(apiResponse); + } + + defineEnabledApiKeysRoutes(mockRouteDefinitionParams); + const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: '/internal/security/api_key/_enabled', + headers, + }); + const mockContext = ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (apiResponse) { + expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.invalidateAPIKey', + { + body: { + id: expect.any(String), + }, + } + ); + } else { + expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + }); + }; + + describe('failure', () => { + enabledApiKeysTest('returns result of license checker', { + licenseCheckResult: { state: 'invalid', message: 'test forbidden message' }, + asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, + }); + + const error = Boom.notAcceptable('test not acceptable message'); + enabledApiKeysTest('returns error from cluster client', { + apiResponse: async () => { + throw error; + }, + asserts: { statusCode: 406, result: error }, + }); + }); + + describe('success', () => { + enabledApiKeysTest('returns true if API Keys are enabled', { + apiResponse: async () => ({}), + asserts: { + statusCode: 200, + result: { + apiKeysEnabled: true, + }, + }, + }); + enabledApiKeysTest('returns false if API Keys are disabled', { + apiResponse: async () => { + const error = new Error(); + (error as any).body = { + error: { 'disabled.feature': 'api_keys' }, + }; + throw error; + }, + asserts: { + statusCode: 200, + result: { + apiKeysEnabled: false, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.ts new file mode 100644 index 0000000000000..2f5b8343bcd89 --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/enabled.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +export function defineEnabledApiKeysRoutes({ router, authc }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/api_key/_enabled', + validate: false, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const apiKeysEnabled = await authc.areAPIKeysEnabled(); + + return response.ok({ body: { apiKeysEnabled } }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/api_keys/index.ts b/x-pack/plugins/security/server/routes/api_keys/index.ts index d75eb1bcbe961..7ac37bbead613 100644 --- a/x-pack/plugins/security/server/routes/api_keys/index.ts +++ b/x-pack/plugins/security/server/routes/api_keys/index.ts @@ -7,9 +7,11 @@ import { defineGetApiKeysRoutes } from './get'; import { defineCheckPrivilegesRoutes } from './privileges'; import { defineInvalidateApiKeysRoutes } from './invalidate'; +import { defineEnabledApiKeysRoutes } from './enabled'; import { RouteDefinitionParams } from '..'; export function defineApiKeysRoutes(params: RouteDefinitionParams) { + defineEnabledApiKeysRoutes(params); defineGetApiKeysRoutes(params); defineCheckPrivilegesRoutes(params); defineInvalidateApiKeysRoutes(params); diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts index 311d50e9eb169..afb67dc3bbfca 100644 --- a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts @@ -11,25 +11,53 @@ import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../ import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; import { defineCheckPrivilegesRoutes } from './privileges'; +import { APIKeys } from '../../authentication/api_keys'; interface TestOptions { licenseCheckResult?: LicenseCheck; - apiResponses?: Array<() => Promise>; - asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; + callAsInternalUserResponses?: Array<() => Promise>; + callAsCurrentUserResponses?: Array<() => Promise>; + asserts: { + statusCode: number; + result?: Record; + callAsInternalUserAPIArguments?: unknown[][]; + callAsCurrentUserAPIArguments?: unknown[][]; + }; } describe('Check API keys privileges', () => { const getPrivilegesTest = ( description: string, - { licenseCheckResult = { state: 'valid' }, apiResponses = [], asserts }: TestOptions + { + licenseCheckResult = { state: 'valid' }, + callAsInternalUserResponses = [], + callAsCurrentUserResponses = [], + asserts, + }: TestOptions ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + const apiKeys = new APIKeys({ + logger: mockRouteDefinitionParams.logger, + clusterClient: mockRouteDefinitionParams.clusterClient, + license: mockRouteDefinitionParams.license, + }); + + mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockImplementation(() => + apiKeys.areAPIKeysEnabled() + ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - for (const apiResponse of apiResponses) { + for (const apiResponse of callAsCurrentUserResponses) { mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); } + for (const apiResponse of callAsInternalUserResponses) { + mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementationOnce( + apiResponse + ); + } defineCheckPrivilegesRoutes(mockRouteDefinitionParams); const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; @@ -48,8 +76,8 @@ describe('Check API keys privileges', () => { expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); - if (Array.isArray(asserts.apiArguments)) { - for (const apiArguments of asserts.apiArguments) { + if (Array.isArray(asserts.callAsCurrentUserAPIArguments)) { + for (const apiArguments of asserts.callAsCurrentUserAPIArguments) { expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith( mockRequest ); @@ -58,6 +86,17 @@ describe('Check API keys privileges', () => { } else { expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); } + + if (Array.isArray(asserts.callAsInternalUserAPIArguments)) { + for (const apiArguments of asserts.callAsInternalUserAPIArguments) { + expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).toHaveBeenCalledWith( + ...apiArguments + ); + } + } else { + expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); }; @@ -70,16 +109,21 @@ describe('Check API keys privileges', () => { const error = Boom.notAcceptable('test not acceptable message'); getPrivilegesTest('returns error from cluster client', { - apiResponses: [ + callAsCurrentUserResponses: [ async () => { throw error; }, - async () => {}, ], + callAsInternalUserResponses: [async () => {}], asserts: { - apiArguments: [ - ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], - ['shield.getAPIKeys', { owner: true }], + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, + ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], ], statusCode: 406, result: error, @@ -89,14 +133,16 @@ describe('Check API keys privileges', () => { describe('success', () => { getPrivilegesTest('returns areApiKeysEnabled and isAdmin', { - apiResponses: [ + callAsCurrentUserResponses: [ async () => ({ username: 'elastic', has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true }, + cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: false }, index: {}, application: {}, }), + ], + callAsInternalUserResponses: [ async () => ({ api_keys: [ { @@ -112,71 +158,108 @@ describe('Check API keys privileges', () => { }), ], asserts: { - apiArguments: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, + ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], ], statusCode: 200, - result: { areApiKeysEnabled: true, isAdmin: true }, + result: { areApiKeysEnabled: true, isAdmin: true, canManage: true }, }, }); getPrivilegesTest( - 'returns areApiKeysEnabled=false when getAPIKeys error message includes "api keys are not enabled"', + 'returns areApiKeysEnabled=false when API Keys are disabled in Elasticsearch', { - apiResponses: [ + callAsCurrentUserResponses: [ async () => ({ username: 'elastic', has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true }, + cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: true }, index: {}, application: {}, }), + ], + callAsInternalUserResponses: [ async () => { - throw Boom.unauthorized('api keys are not enabled'); + const error = new Error(); + (error as any).body = { + error: { + 'disabled.feature': 'api_keys', + }, + }; + throw error; }, ], asserts: { - apiArguments: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, + ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], ], statusCode: 200, - result: { areApiKeysEnabled: false, isAdmin: true }, + result: { areApiKeysEnabled: false, isAdmin: true, canManage: true }, }, } ); getPrivilegesTest('returns isAdmin=false when user has insufficient privileges', { - apiResponses: [ + callAsCurrentUserResponses: [ async () => ({ username: 'elastic', has_all_requested: true, - cluster: { manage_api_key: false, manage_security: false }, + cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: false }, index: {}, application: {}, }), - async () => ({ - api_keys: [ - { - id: 'si8If24B1bKsmSLTAhJV', - name: 'my-api-key', - creation: 1574089261632, - expiration: 1574175661632, - invalidated: false, - username: 'elastic', - realm: 'reserved', - }, + ], + callAsInternalUserResponses: [async () => ({})], + asserts: { + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], + ], + statusCode: 200, + result: { areApiKeysEnabled: true, isAdmin: false, canManage: false }, + }, + }); + + getPrivilegesTest('returns canManage=true when user can manage their own API Keys', { + callAsCurrentUserResponses: [ + async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: true }, + index: {}, + application: {}, }), ], + callAsInternalUserResponses: [async () => ({})], asserts: { - apiArguments: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, + ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], ], statusCode: 200, - result: { areApiKeysEnabled: true, isAdmin: false }, + result: { areApiKeysEnabled: true, isAdmin: false, canManage: true }, }, }); }); diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.ts index 216d1ef1bf4a4..9cccb96752772 100644 --- a/x-pack/plugins/security/server/routes/api_keys/privileges.ts +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.ts @@ -8,7 +8,11 @@ import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; -export function defineCheckPrivilegesRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineCheckPrivilegesRoutes({ + router, + clusterClient, + authc, +}: RouteDefinitionParams) { router.get( { path: '/internal/security/api_key/privileges', @@ -20,26 +24,25 @@ export function defineCheckPrivilegesRoutes({ router, clusterClient }: RouteDefi const [ { - cluster: { manage_security: manageSecurity, manage_api_key: manageApiKey }, + cluster: { + manage_security: manageSecurity, + manage_api_key: manageApiKey, + manage_own_api_key: manageOwnApiKey, + }, }, - { areApiKeysEnabled }, + areApiKeysEnabled, ] = await Promise.all([ scopedClusterClient.callAsCurrentUser('shield.hasPrivileges', { - body: { cluster: ['manage_security', 'manage_api_key'] }, + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, }), - scopedClusterClient.callAsCurrentUser('shield.getAPIKeys', { owner: true }).then( - // If the API returns a truthy result that means it's enabled. - result => ({ areApiKeysEnabled: !!result }), - // This is a brittle dependency upon message. Tracked by https://github.com/elastic/elasticsearch/issues/47759. - e => - e.message.includes('api keys are not enabled') - ? Promise.resolve({ areApiKeysEnabled: false }) - : Promise.reject(e) - ), + authc.areAPIKeysEnabled(), ]); + const isAdmin = manageSecurity || manageApiKey; + const canManage = manageSecurity || manageApiKey || manageOwnApiKey; + return response.ok({ - body: { areApiKeysEnabled, isAdmin: manageSecurity || manageApiKey }, + body: { areApiKeysEnabled, isAdmin, canManage }, }); } catch (error) { return response.customError(wrapIntoCustomErrorResponse(error)); diff --git a/x-pack/test/api_integration/apis/security/api_keys.ts b/x-pack/test/api_integration/apis/security/api_keys.ts new file mode 100644 index 0000000000000..276a5367a419e --- /dev/null +++ b/x-pack/test/api_integration/apis/security/api_keys.ts @@ -0,0 +1,28 @@ +/* + * 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/expect.js'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('API Keys', () => { + describe('GET /internal/security/api_key/_enabled', () => { + it('should indicate that API Keys are enabled', async () => { + await supertest + .get('/internal/security/api_key/_enabled') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200) + .then((response: Record) => { + const payload = response.body; + expect(payload).to.eql({ apiKeysEnabled: true }); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/security/index.js b/x-pack/test/api_integration/apis/security/index.js index ad1876cb717f1..7bb79a589d522 100644 --- a/x-pack/test/api_integration/apis/security/index.js +++ b/x-pack/test/api_integration/apis/security/index.js @@ -11,6 +11,7 @@ export default function({ loadTestFile }) { // Updates here should be mirrored in `./security_basic.ts` if tests // should also run under a basic license. + loadTestFile(require.resolve('./api_keys')); loadTestFile(require.resolve('./basic_login')); loadTestFile(require.resolve('./builtin_es_privileges')); loadTestFile(require.resolve('./change_password')); diff --git a/x-pack/test/api_integration/apis/security/security_basic.ts b/x-pack/test/api_integration/apis/security/security_basic.ts index dcbdb17724249..3e426f210afa8 100644 --- a/x-pack/test/api_integration/apis/security/security_basic.ts +++ b/x-pack/test/api_integration/apis/security/security_basic.ts @@ -13,6 +13,7 @@ export default function({ loadTestFile }: FtrProviderContext) { // Updates here should be mirrored in `./index.js` if tests // should also run under a trial/platinum license. + loadTestFile(require.resolve('./api_keys')); loadTestFile(require.resolve('./basic_login')); loadTestFile(require.resolve('./builtin_es_privileges')); loadTestFile(require.resolve('./change_password')); diff --git a/x-pack/test/api_integration/config_security_basic.js b/x-pack/test/api_integration/config_security_basic.js index d21bfa4d7031a..4c4b77ee5b080 100644 --- a/x-pack/test/api_integration/config_security_basic.js +++ b/x-pack/test/api_integration/config_security_basic.js @@ -13,6 +13,7 @@ export default async function({ readConfigFile }) { config.esTestCluster.serverArgs = [ 'xpack.license.self_generated.type=basic', 'xpack.security.enabled=true', + 'xpack.security.authc.api_key.enabled=true', ]; config.testFiles = [require.resolve('./apis/security/security_basic')]; return config; From a145aa9d5e965246e048637b63cd23d76b5bff06 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 23 Apr 2020 22:42:22 +0300 Subject: [PATCH 10/35] [SIEM][CASE] Api Integration Tests: Configuration (#63948) * Init * Init get_connectors * Test post_configuration * Test patch_configuration * Rename folder Co-authored-by: Elastic Machine --- .../api/cases/configure/patch_configure.ts | 2 +- .../test/case_api_integration/basic/config.ts | 14 +++ .../basic/tests/configure/get_configure.ts | 55 +++++++++++ .../basic/tests/configure/get_connectors.ts | 27 ++++++ .../basic/tests/configure/patch_configure.ts | 81 ++++++++++++++++ .../basic/tests/configure/post_configure.ts | 62 ++++++++++++ .../case_api_integration/basic/tests/index.ts | 20 ++++ .../case_api_integration/common/config.ts | 94 +++++++++++++++++++ .../common/ftr_provider_context.d.ts | 11 +++ .../case_api_integration/common/lib/utils.ts | 71 ++++++++++++++ .../case_api_integration/common/services.ts | 7 ++ 11 files changed, 443 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/case_api_integration/basic/config.ts create mode 100644 x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts create mode 100644 x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts create mode 100644 x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts create mode 100644 x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts create mode 100644 x-pack/test/case_api_integration/basic/tests/index.ts create mode 100644 x-pack/test/case_api_integration/common/config.ts create mode 100644 x-pack/test/case_api_integration/common/ftr_provider_context.d.ts create mode 100644 x-pack/test/case_api_integration/common/lib/utils.ts create mode 100644 x-pack/test/case_api_integration/common/services.ts diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts index 47f7d503e32b8..29df97c5f8476 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts @@ -39,7 +39,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout if (myCaseConfigure.saved_objects.length === 0) { throw Boom.conflict( - 'You can not patch this configuration since you did not created first with a post' + 'You can not patch this configuration since you did not created first with a post.' ); } diff --git a/x-pack/test/case_api_integration/basic/config.ts b/x-pack/test/case_api_integration/basic/config.ts new file mode 100644 index 0000000000000..f9c248ec3d56f --- /dev/null +++ b/x-pack/test/case_api_integration/basic/config.ts @@ -0,0 +1,14 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('basic', { + disabledPlugins: [], + license: 'basic', + ssl: true, +}); diff --git a/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts new file mode 100644 index 0000000000000..a9fc2706a6ba2 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts @@ -0,0 +1,55 @@ +/* + * 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 '../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL } from '../../../../../plugins/case/common/constants'; +import { + getConfiguration, + removeServerGeneratedPropertiesFromConfigure, + getConfigurationOutput, + deleteConfiguration, +} from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('get_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should return an empty find body correctly if no configuration is loaded', async () => { + const { body } = await supertest + .get(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql({}); + }); + + it('should return a configuration', async () => { + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const { body } = await supertest + .get(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromConfigure(body); + expect(data).to.eql(getConfigurationOutput()); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts new file mode 100644 index 0000000000000..836c76d500034 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../plugins/case/common/constants'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + + describe('get_connectors', () => { + it('should return an empty find body correctly if no connectors are loaded', async () => { + const { body } = await supertest + .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql([]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts new file mode 100644 index 0000000000000..d66baa2a2eee2 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts @@ -0,0 +1,81 @@ +/* + * 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 '../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL } from '../../../../../plugins/case/common/constants'; +import { + getConfiguration, + removeServerGeneratedPropertiesFromConfigure, + getConfigurationOutput, + deleteConfiguration, +} from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('post_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should patch a configuration', async () => { + const res = await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const { body } = await supertest + .patch(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send({ closure_type: 'close-by-pushing', version: res.body.version }) + .expect(200); + + const data = removeServerGeneratedPropertiesFromConfigure(body); + expect(data).to.eql({ ...getConfigurationOutput(true), closure_type: 'close-by-pushing' }); + }); + + it('should handle patch request when there is no configuration', async () => { + const { body } = await supertest + .patch(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send({ closure_type: 'close-by-pushing', version: 'no-version' }) + .expect(409); + + expect(body).to.eql({ + error: 'Conflict', + message: + 'You can not patch this configuration since you did not created first with a post.', + statusCode: 409, + }); + }); + + it('should handle patch request when versions are different', async () => { + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const { body } = await supertest + .patch(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send({ closure_type: 'close-by-pushing', version: 'no-version' }) + .expect(409); + + expect(body).to.eql({ + error: 'Conflict', + message: + 'This configuration has been updated. Please refresh before saving additional updates.', + statusCode: 409, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts new file mode 100644 index 0000000000000..c2284492e5b77 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts @@ -0,0 +1,62 @@ +/* + * 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 '../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL } from '../../../../../plugins/case/common/constants'; +import { + getConfiguration, + removeServerGeneratedPropertiesFromConfigure, + getConfigurationOutput, + deleteConfiguration, +} from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('post_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should create a configuration', async () => { + const { body } = await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const data = removeServerGeneratedPropertiesFromConfigure(body); + expect(data).to.eql(getConfigurationOutput()); + }); + + it('should keep only the latest configuration', async () => { + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration('connector-2')) + .expect(200); + + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const { body } = await supertest + .get(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromConfigure(body); + expect(data).to.eql(getConfigurationOutput()); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/index.ts b/x-pack/test/case_api_integration/basic/tests/index.ts new file mode 100644 index 0000000000000..efd5369c019d8 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('case api basic', function() { + // Fastest ciGroup for the moment. + this.tags('ciGroup2'); + + loadTestFile(require.resolve('./configure/get_configure')); + loadTestFile(require.resolve('./configure/post_configure')); + loadTestFile(require.resolve('./configure/patch_configure')); + loadTestFile(require.resolve('./configure/get_connectors')); + }); +}; diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts new file mode 100644 index 0000000000000..862705ab9610b --- /dev/null +++ b/x-pack/test/case_api_integration/common/config.ts @@ -0,0 +1,94 @@ +/* + * 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 path from 'path'; + +import { CA_CERT_PATH } from '@kbn/dev-utils'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +import { services } from './services'; + +interface CreateTestConfigOptions { + license: string; + disabledPlugins?: string[]; + ssl?: boolean; +} + +// test.not-enabled is specifically not enabled +const enabledActionTypes = [ + '.email', + '.index', + '.pagerduty', + '.server-log', + '.servicenow', + '.slack', + '.webhook', + 'test.authorization', + 'test.failing', + 'test.index-record', + 'test.noop', + 'test.rate-limit', +]; + +export function createTestConfig(name: string, options: CreateTestConfigOptions) { + const { license = 'trial', disabledPlugins = [], ssl = false } = options; + + return async ({ readConfigFile }: FtrConfigProviderContext) => { + const xPackApiIntegrationTestsConfig = await readConfigFile( + require.resolve('../../api_integration/config.js') + ); + + const servers = { + ...xPackApiIntegrationTestsConfig.get('servers'), + elasticsearch: { + ...xPackApiIntegrationTestsConfig.get('servers.elasticsearch'), + protocol: ssl ? 'https' : 'http', + }, + }; + + return { + testFiles: [require.resolve(`../${name}/tests/`)], + servers, + services, + junit: { + reportName: 'X-Pack Case API Integration Tests', + }, + esArchiver: xPackApiIntegrationTestsConfig.get('esArchiver'), + esTestCluster: { + ...xPackApiIntegrationTestsConfig.get('esTestCluster'), + license, + ssl, + serverArgs: [ + `xpack.license.self_generated.type=${license}`, + `xpack.security.enabled=${!disabledPlugins.includes('security') && + ['trial', 'basic'].includes(license)}`, + ], + }, + kbnTestServer: { + ...xPackApiIntegrationTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), + `--xpack.actions.whitelistedHosts=${JSON.stringify([ + 'localhost', + 'some.non.existent.com', + ])}`, + `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + '--xpack.alerting.enabled=true', + '--xpack.eventLog.logEntries=true', + ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`, + ...(ssl + ? [ + `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + ] + : []), + ], + }, + }; + }; +} diff --git a/x-pack/test/case_api_integration/common/ftr_provider_context.d.ts b/x-pack/test/case_api_integration/common/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..e3add3748f56d --- /dev/null +++ b/x-pack/test/case_api_integration/common/ftr_provider_context.d.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. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts new file mode 100644 index 0000000000000..6d0db69309b90 --- /dev/null +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -0,0 +1,71 @@ +/* + * 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 { CasesConfigureRequest, CasesConfigureResponse } from '../../../../plugins/case/common/api'; + +export const getConfiguration = (connector_id: string = 'connector-1'): CasesConfigureRequest => { + return { + connector_id, + connector_name: 'Connector 1', + closure_type: 'close-by-user', + }; +}; + +export const getConfigurationOutput = (update = false): Partial => { + return { + ...getConfiguration(), + created_by: { email: null, full_name: null, username: 'elastic' }, + updated_by: update ? { email: null, full_name: null, username: 'elastic' } : null, + }; +}; + +export const removeServerGeneratedPropertiesFromConfigure = ( + config: Partial +): Partial => { + const { created_at, updated_at, version, ...rest } = config; + return rest; +}; + +export const deleteConfiguration = async (es: any): Promise => { + await es.deleteByQuery({ + index: '.kibana', + q: 'type:cases-configure', + waitForCompletion: true, + refresh: 'wait_for', + body: {}, + }); +}; + +export const getConnector = () => ({ + name: 'ServiceNow Connector', + actionTypeId: '.servicenow', + secrets: { + username: 'admin', + password: 'admin', + }, + config: { + apiUrl: 'localhost', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, +}); diff --git a/x-pack/test/case_api_integration/common/services.ts b/x-pack/test/case_api_integration/common/services.ts new file mode 100644 index 0000000000000..a927a31469bab --- /dev/null +++ b/x-pack/test/case_api_integration/common/services.ts @@ -0,0 +1,7 @@ +/* + * 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 { services } from '../../api_integration/services'; From 74a07b46d87ede2ed7a61311cbb3b392e4aad805 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Thu, 23 Apr 2020 15:00:40 -0500 Subject: [PATCH 11/35] [DOCS] Added images to automating report generation (#64333) * [DOCS] Added images to automating report generation * Fixes image file name --- docs/images/report-automate-csv.png | Bin 0 -> 54061 bytes docs/images/report-automate-pdf.png | Bin 0 -> 52835 bytes .../automating-report-generation.asciidoc | 42 +++++++++++------- 3 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 docs/images/report-automate-csv.png create mode 100644 docs/images/report-automate-pdf.png diff --git a/docs/images/report-automate-csv.png b/docs/images/report-automate-csv.png new file mode 100644 index 0000000000000000000000000000000000000000..fba77821ae29f708ec545470684600033dfadc97 GIT binary patch literal 54061 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 y&yN7Oe*5_R@+)nhvOoWY>k5UyDpM literal 0 HcmV?d00001 diff --git a/docs/images/report-automate-pdf.png b/docs/images/report-automate-pdf.png new file mode 100644 index 0000000000000000000000000000000000000000..f96eebe6fe02d5cf3572378e47b8f17ecbe9773b GIT binary patch literal 52835 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⪼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 u)7z Date: Thu, 23 Apr 2020 21:14:51 +0100 Subject: [PATCH 12/35] chore(NA): add file-loader into jest moduleNameMapper (#64330) Co-authored-by: Elastic Machine --- src/dev/jest/config.js | 1 + x-pack/dev-tools/jest/create_jest_config.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 7da14e0dfe51b..43a2cbd78c502 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -63,6 +63,7 @@ export default { '/src/dev/jest/mocks/file_mock.js', '\\.(css|less|scss)$': '/src/dev/jest/mocks/style_mock.js', '\\.ace\\.worker.js$': '/src/dev/jest/mocks/ace_worker_module_mock.js', + '^(!!)?file-loader!': '/src/dev/jest/mocks/file_mock.js', }, setupFiles: [ '/src/dev/jest/setup/babel_polyfill.js', diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index 3068cdd0daa5b..af5ace8e3cd3b 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -32,6 +32,7 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { '^test_utils/enzyme_helpers': `${xPackKibanaDirectory}/test_utils/enzyme_helpers.tsx`, '^test_utils/find_test_subject': `${xPackKibanaDirectory}/test_utils/find_test_subject.ts`, '^test_utils/stub_web_worker': `${xPackKibanaDirectory}/test_utils/stub_web_worker.ts`, + '^(!!)?file-loader!': fileMockPath, }, coverageDirectory: '/../target/kibana-coverage/jest', coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html'], From 9ad8b8f35f4c27eafd5793eb283d48b5ad41d53b Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 23 Apr 2020 14:14:13 -0700 Subject: [PATCH 13/35] [Ingest] Data streams list page (#64134) * Clean up fleet setup request/response typings * Add data stream model and list route, handler, and request/response types * Initial pass at data streams list * Table styling fixes * Fix types, fix field names * Change forEach to map --- .../ingest_manager/common/constants/routes.ts | 8 +- .../ingest_manager/common/services/routes.ts | 7 + .../common/types/models/data_stream.ts | 15 + .../common/types/models/index.ts | 1 + .../types/rest_spec/data_stream.ts} | 9 +- .../common/types/rest_spec/fleet_setup.ts | 10 - .../common/types/rest_spec/index.ts | 1 + .../ingest_manager/constants/index.ts | 1 + .../hooks/use_request/data_stream.ts | 15 + .../ingest_manager/hooks/use_request/index.ts | 1 + .../applications/ingest_manager/index.tsx | 9 +- .../ingest_manager/layouts/default.tsx | 8 +- .../sections/agent_config/list_page/index.tsx | 1 - .../sections/data_stream/index.tsx | 20 ++ .../sections/data_stream/list_page/index.tsx | 283 ++++++++++++++++++ .../ingest_manager/sections/index.tsx | 3 +- .../ingest_manager/services/index.ts | 1 + .../ingest_manager/types/index.ts | 3 + .../ingest_manager/server/constants/index.ts | 1 + .../plugins/ingest_manager/server/plugin.ts | 2 + .../server/routes/data_streams/handlers.ts | 125 ++++++++ .../server/routes/data_streams/index.ts | 20 ++ .../ingest_manager/server/routes/index.ts | 1 + .../server/routes/setup/handlers.ts | 2 +- .../server/routes/setup/index.ts | 5 +- .../ingest_manager/server/types/index.tsx | 1 + .../server/types/rest_spec/index.ts | 1 - 27 files changed, 527 insertions(+), 27 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/common/types/models/data_stream.ts rename x-pack/plugins/ingest_manager/{server/types/rest_spec/fleet_setup.ts => common/types/rest_spec/data_stream.ts} (59%) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/data_stream.ts create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx create mode 100644 x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts create mode 100644 x-pack/plugins/ingest_manager/server/routes/data_streams/index.ts diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index 616195b32f266..98ca52651a2ae 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -5,9 +5,10 @@ */ // Base API paths export const API_ROOT = `/api/ingest_manager`; +export const EPM_API_ROOT = `${API_ROOT}/epm`; +export const DATA_STREAM_API_ROOT = `${API_ROOT}/data_streams`; export const DATASOURCE_API_ROOT = `${API_ROOT}/datasources`; export const AGENT_CONFIG_API_ROOT = `${API_ROOT}/agent_configs`; -export const EPM_API_ROOT = `${API_ROOT}/epm`; export const FLEET_API_ROOT = `${API_ROOT}/fleet`; // EPM API routes @@ -23,6 +24,11 @@ export const EPM_API_ROUTES = { CATEGORIES_PATTERN: `${EPM_API_ROOT}/categories`, }; +// Data stream API routes +export const DATA_STREAM_API_ROUTES = { + LIST_PATTERN: `${DATA_STREAM_API_ROOT}`, +}; + // Datasource API routes export const DATASOURCE_API_ROUTES = { LIST_PATTERN: `${DATASOURCE_API_ROOT}`, diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts index f2343b1039151..46b76d886f3cd 100644 --- a/x-pack/plugins/ingest_manager/common/services/routes.ts +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -8,6 +8,7 @@ import { EPM_API_ROUTES, DATASOURCE_API_ROUTES, AGENT_CONFIG_API_ROUTES, + DATA_STREAM_API_ROUTES, FLEET_SETUP_API_ROUTES, AGENT_API_ROUTES, ENROLLMENT_API_KEY_ROUTES, @@ -88,6 +89,12 @@ export const agentConfigRouteService = { }, }; +export const dataStreamRouteService = { + getListPath: () => { + return DATA_STREAM_API_ROUTES.LIST_PATTERN; + }, +}; + export const fleetSetupRouteService = { getFleetSetupPath: () => FLEET_SETUP_API_ROUTES.INFO_PATTERN, postFleetSetupPath: () => FLEET_SETUP_API_ROUTES.CREATE_PATTERN, diff --git a/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts b/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts new file mode 100644 index 0000000000000..7da9bbad1b170 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts @@ -0,0 +1,15 @@ +/* + * 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 interface DataStream { + index: string; + dataset: string; + namespace: string; + type: string; + package: string; + last_activity: string; + size_in_bytes: number; +} diff --git a/x-pack/plugins/ingest_manager/common/types/models/index.ts b/x-pack/plugins/ingest_manager/common/types/models/index.ts index 579b510e52daa..f73ab7af636a9 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/index.ts @@ -7,6 +7,7 @@ export * from './agent'; export * from './agent_config'; export * from './datasource'; +export * from './data_stream'; export * from './output'; export * from './epm'; export * from './enrollment_api_key'; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/data_stream.ts similarity index 59% rename from x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.ts rename to x-pack/plugins/ingest_manager/common/types/rest_spec/data_stream.ts index 2244bcd44043f..24f8110562bfc 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/data_stream.ts @@ -3,11 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { DataStream } from '../models'; -export const GetFleetSetupRequestSchema = {}; - -export const CreateFleetSetupRequestSchema = {}; - -export interface CreateFleetSetupResponse { - isInitialized: boolean; +export interface GetDataStreamsResponse { + data_streams: DataStream[]; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts index dc1d748a8743a..c4ba8ee595acf 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts @@ -4,16 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface GetFleetSetupRequest {} - -export interface CreateFleetSetupRequest { - body: { - fleet_enroll_username: string; - fleet_enroll_password: string; - }; -} - export interface CreateFleetSetupResponse { isInitialized: boolean; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts index abe1bc8e3eddb..c1805023f497a 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts @@ -5,6 +5,7 @@ */ export * from './common'; export * from './datasource'; +export * from './data_stream'; export * from './agent'; export * from './agent_config'; export * from './fleet_setup'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts index 282ea8dbee3a2..619d03651dd96 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts @@ -12,6 +12,7 @@ export const EPM_LIST_INSTALLED_PACKAGES_PATH = `${EPM_PATH}/installed`; export const EPM_DETAIL_VIEW_PATH = `${EPM_PATH}/detail/:pkgkey/:panel?`; export const AGENT_CONFIG_PATH = '/configs'; export const AGENT_CONFIG_DETAILS_PATH = `${AGENT_CONFIG_PATH}/`; +export const DATA_STREAM_PATH = '/data-streams'; export const FLEET_PATH = '/fleet'; export const FLEET_AGENTS_PATH = `${FLEET_PATH}/agents`; export const FLEET_AGENT_DETAIL_PATH = `${FLEET_AGENTS_PATH}/`; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/data_stream.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/data_stream.ts new file mode 100644 index 0000000000000..9acf4b1e17449 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/data_stream.ts @@ -0,0 +1,15 @@ +/* + * 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 { useRequest } from './use_request'; +import { dataStreamRouteService } from '../../services'; +import { GetDataStreamsResponse } from '../../types'; + +export const useGetDataStreams = () => { + return useRequest({ + path: dataStreamRouteService.getListPath(), + method: 'get', + }); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts index 5014049407e65..084aba9a34309 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts @@ -6,6 +6,7 @@ export { setHttpClient, sendRequest, useRequest } from './use_request'; export * from './agent_config'; export * from './datasource'; +export * from './data_stream'; export * from './agents'; export * from './enrollment_api_keys'; export * from './epm'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index f7c2805c6ea7c..6485862830d8a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -16,10 +16,10 @@ import { IngestManagerConfigType, IngestManagerStartDeps, } from '../../plugin'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from './constants'; +import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from './constants'; import { DefaultLayout, WithoutHeaderLayout } from './layouts'; import { Loading, Error } from './components'; -import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp } from './sections'; +import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp, DataStreamApp } from './sections'; import { CoreContext, DepsContext, ConfigContext, setHttpClient, useConfig } from './hooks'; import { PackageInstallProvider } from './sections/epm/hooks'; import { sendSetup } from './hooks/use_request/setup'; @@ -98,6 +98,11 @@ const IngestManagerRoutes = ({ ...rest }) => { + + + + + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index 345fd535b8ecc..f1f9063de72f0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Section } from '../sections'; import { AlphaMessaging } from '../components'; import { useLink, useConfig } from '../hooks'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from '../constants'; +import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../constants'; interface Props { section?: Section; @@ -76,6 +76,12 @@ export const DefaultLayout: React.FunctionComponent = ({ section, childre defaultMessage="Fleet" /> + + + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx index 0498e814440c7..1ea162252c741 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx @@ -191,7 +191,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Name', }), width: '20%', - // FIXME: use version once available - see: https://github.com/elastic/kibana/issues/56750 render: (name: string, agentConfig: AgentConfig) => ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx new file mode 100644 index 0000000000000..7b0641e66fd43 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx @@ -0,0 +1,20 @@ +/* + * 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 from 'react'; +import { HashRouter as Router, Route, Switch } from 'react-router-dom'; +import { DataStreamListPage } from './list_page'; + +export const DataStreamApp: React.FunctionComponent = () => { + return ( + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx new file mode 100644 index 0000000000000..d7a3e933f3bb5 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx @@ -0,0 +1,283 @@ +/* + * 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, { useMemo } from 'react'; +import { + EuiBadge, + EuiButton, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiEmptyPrompt, + EuiInMemoryTable, + EuiTableActionsColumnType, + EuiTableFieldDataColumnType, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; +import { DataStream } from '../../../types'; +import { WithHeaderLayout } from '../../../layouts'; +import { useGetDataStreams, useStartDeps, usePagination } from '../../../hooks'; + +const DataStreamListPageLayout: React.FunctionComponent = ({ children }) => ( + + + +

+ +

+
+
+ + +

+ +

+
+
+
+ } + > + {children} + +); + +export const DataStreamListPage: React.FunctionComponent<{}> = () => { + const { + data: { fieldFormats }, + } = useStartDeps(); + + const { pagination, pageSizeOptions } = usePagination(); + + // Fetch agent configs + const { isLoading, data: dataStreamsData, sendRequest } = useGetDataStreams(); + + // Some configs retrieved, set up table props + const columns = useMemo(() => { + const cols: Array< + EuiTableFieldDataColumnType | EuiTableActionsColumnType + > = [ + { + field: 'dataset', + sortable: true, + width: '25%', + truncateText: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.datasetColumnTitle', { + defaultMessage: 'Dataset', + }), + }, + { + field: 'type', + sortable: true, + truncateText: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.typeColumnTitle', { + defaultMessage: 'Type', + }), + }, + { + field: 'namespace', + sortable: true, + truncateText: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.namespaceColumnTitle', { + defaultMessage: 'Namespace', + }), + render: (namespace: string) => { + return namespace ? {namespace} : ''; + }, + }, + { + field: 'package', + sortable: true, + truncateText: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.integrationColumnTitle', { + defaultMessage: 'Integration', + }), + }, + { + field: 'last_activity', + sortable: true, + width: '25%', + dataType: 'date', + name: i18n.translate('xpack.ingestManager.dataStreamList.lastActivityColumnTitle', { + defaultMessage: 'Last activity', + }), + render: (date: DataStream['last_activity']) => { + try { + const formatter = fieldFormats.getInstance('date'); + return formatter.convert(date); + } catch (e) { + return ; + } + }, + }, + { + field: 'size_in_bytes', + sortable: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.sizeColumnTitle', { + defaultMessage: 'Size', + }), + render: (size: DataStream['size_in_bytes']) => { + try { + const formatter = fieldFormats.getInstance('bytes'); + return formatter.convert(size); + } catch (e) { + return `${size}b`; + } + }, + }, + ]; + return cols; + }, [fieldFormats]); + + const emptyPrompt = useMemo( + () => ( + + + + } + /> + ), + [] + ); + + const filterOptions: { [key: string]: string[] } = { + dataset: [], + type: [], + namespace: [], + package: [], + }; + + if (dataStreamsData && dataStreamsData.data_streams.length) { + dataStreamsData.data_streams.forEach(stream => { + const { dataset, type, namespace, package: pkg } = stream; + if (!filterOptions.dataset.includes(dataset)) { + filterOptions.dataset.push(dataset); + } + if (!filterOptions.type.includes(type)) { + filterOptions.type.push(type); + } + if (!filterOptions.namespace.includes(namespace)) { + filterOptions.namespace.push(namespace); + } + if (!filterOptions.package.includes(pkg)) { + filterOptions.package.push(pkg); + } + }); + } + + return ( + + + ) : dataStreamsData && !dataStreamsData.data_streams.length ? ( + emptyPrompt + ) : ( + + ) + } + items={dataStreamsData ? dataStreamsData.data_streams : []} + itemId="index" + columns={columns} + pagination={{ + initialPageSize: pagination.pageSize, + pageSizeOptions, + }} + sorting={true} + search={{ + toolsRight: [ + sendRequest()}> + + , + ], + box: { + placeholder: i18n.translate( + 'xpack.ingestManager.dataStreamList.searchPlaceholderTitle', + { + defaultMessage: 'Filter data streams', + } + ), + incremental: true, + }, + filters: [ + { + type: 'field_value_selection', + field: 'dataset', + name: i18n.translate('xpack.ingestManager.dataStreamList.datasetColumnTitle', { + defaultMessage: 'Dataset', + }), + multiSelect: 'or', + options: filterOptions.dataset.map(option => ({ + value: option, + name: option, + })), + }, + { + type: 'field_value_selection', + field: 'type', + name: i18n.translate('xpack.ingestManager.dataStreamList.typeColumnTitle', { + defaultMessage: 'Type', + }), + multiSelect: 'or', + options: filterOptions.type.map(option => ({ + value: option, + name: option, + })), + }, + { + type: 'field_value_selection', + field: 'namespace', + name: i18n.translate('xpack.ingestManager.dataStreamList.namespaceColumnTitle', { + defaultMessage: 'Namespace', + }), + multiSelect: 'or', + options: filterOptions.namespace.map(option => ({ + value: option, + name: option, + })), + }, + { + type: 'field_value_selection', + field: 'package', + name: i18n.translate('xpack.ingestManager.dataStreamList.integrationColumnTitle', { + defaultMessage: 'Integration', + }), + multiSelect: 'or', + options: filterOptions.package.map(option => ({ + value: option, + name: option, + })), + }, + ], + }} + /> + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx index c691bb609d435..1f46c4cc820cb 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx @@ -6,6 +6,7 @@ export { IngestManagerOverview } from './overview'; export { EPMApp } from './epm'; export { AgentConfigApp } from './agent_config'; +export { DataStreamApp } from './data_stream'; export { FleetApp } from './fleet'; -export type Section = 'overview' | 'epm' | 'agent_config' | 'fleet'; +export type Section = 'overview' | 'epm' | 'agent_config' | 'fleet' | 'data_stream'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts index 5ebd1300baf65..53dbe295718c5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts @@ -9,6 +9,7 @@ export { getFlattenedObject } from '../../../../../../../src/core/utils'; export { agentConfigRouteService, datasourceRouteService, + dataStreamRouteService, fleetSetupRouteService, agentRouteService, enrollmentAPIKeyRouteService, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 75194d3397f90..8ca1495a94071 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -17,6 +17,7 @@ export { DatasourceInput, DatasourceInputStream, DatasourceConfigRecordEntry, + DataStream, // API schemas - Agent Config GetAgentConfigsResponse, GetAgentConfigsResponseItem, @@ -30,6 +31,8 @@ export { // API schemas - Datasource CreateDatasourceRequest, CreateDatasourceResponse, + // API schemas - Data Streams + GetDataStreamsResponse, // API schemas - Agents GetAgentsResponse, GetAgentsRequest, diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts index 6ac92ca5d2a91..b2e72fefe5997 100644 --- a/x-pack/plugins/ingest_manager/server/constants/index.ts +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -12,6 +12,7 @@ export { // Routes PLUGIN_ID, EPM_API_ROUTES, + DATA_STREAM_API_ROUTES, DATASOURCE_API_ROUTES, AGENT_API_ROUTES, AGENT_CONFIG_API_ROUTES, diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index c07d2efba6756..55aea4b1a4cdd 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -33,6 +33,7 @@ import { registerEncryptedSavedObjects } from './saved_objects'; import { registerEPMRoutes, registerDatasourceRoutes, + registerDataStreamRoutes, registerAgentConfigRoutes, registerSetupRoutes, registerAgentRoutes, @@ -141,6 +142,7 @@ export class IngestManagerPlugin // Register routes registerAgentConfigRoutes(router); registerDatasourceRoutes(router); + registerDataStreamRoutes(router); // Conditional routes if (config.epm.enabled) { diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts new file mode 100644 index 0000000000000..a24518d644c4c --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts @@ -0,0 +1,125 @@ +/* + * 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 { RequestHandler } from 'src/core/server'; +import { DataStream } from '../../types'; +import { GetDataStreamsResponse } from '../../../common'; + +const DATA_STREAM_INDEX_PATTERN = 'logs-*-*,metrics-*-*'; + +export const getListHandler: RequestHandler = async (context, request, response) => { + const callCluster = context.core.elasticsearch.dataClient.callAsCurrentUser; + + try { + // Get stats (size on disk) of all potentially matching indices + const { indices: indexStats } = await callCluster('indices.stats', { + index: DATA_STREAM_INDEX_PATTERN, + metric: ['store'], + }); + + // Get all matching indices and info about each + // This returns the top 100,000 indices (as buckets) by last activity + const { + aggregations: { + index: { buckets: indexResults }, + }, + } = await callCluster('search', { + index: DATA_STREAM_INDEX_PATTERN, + body: { + size: 0, + query: { + bool: { + must: [ + { + exists: { + field: 'stream.namespace', + }, + }, + { + exists: { + field: 'stream.dataset', + }, + }, + ], + }, + }, + aggs: { + index: { + terms: { + field: '_index', + size: 100000, + order: { + last_activity: 'desc', + }, + }, + aggs: { + dataset: { + terms: { + field: 'stream.dataset', + size: 1, + }, + }, + namespace: { + terms: { + field: 'stream.namespace', + size: 1, + }, + }, + type: { + terms: { + field: 'stream.type', + size: 1, + }, + }, + package: { + terms: { + field: 'event.module', + size: 1, + }, + }, + last_activity: { + max: { + field: '@timestamp', + }, + }, + }, + }, + }, + }, + }); + + const dataStreams: DataStream[] = (indexResults as any[]).map(result => { + const { + key: indexName, + dataset: { buckets: datasetBuckets }, + namespace: { buckets: namespaceBuckets }, + type: { buckets: typeBuckets }, + package: { buckets: packageBuckets }, + last_activity: { value_as_string: lastActivity }, + } = result; + return { + index: indexName, + dataset: datasetBuckets.length ? datasetBuckets[0].key : '', + namespace: namespaceBuckets.length ? namespaceBuckets[0].key : '', + type: typeBuckets.length ? typeBuckets[0].key : '', + package: packageBuckets.length ? packageBuckets[0].key : '', + last_activity: lastActivity, + size_in_bytes: indexStats[indexName] ? indexStats[indexName].total.store.size_in_bytes : 0, + }; + }); + + const body: GetDataStreamsResponse = { + data_streams: dataStreams, + }; + return response.ok({ + body, + }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/index.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/index.ts new file mode 100644 index 0000000000000..39502eba89a6a --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { IRouter } from 'src/core/server'; +import { PLUGIN_ID, DATA_STREAM_API_ROUTES } from '../../constants'; +import { getListHandler } from './handlers'; + +export const registerRoutes = (router: IRouter) => { + // List of data streams + router.get( + { + path: DATA_STREAM_API_ROUTES.LIST_PATTERN, + validate: false, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + getListHandler + ); +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/index.ts b/x-pack/plugins/ingest_manager/server/routes/index.ts index 33d75f3ab82cd..8a186c5485024 100644 --- a/x-pack/plugins/ingest_manager/server/routes/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/index.ts @@ -5,6 +5,7 @@ */ export { registerRoutes as registerAgentConfigRoutes } from './agent_config'; export { registerRoutes as registerDatasourceRoutes } from './datasource'; +export { registerRoutes as registerDataStreamRoutes } from './data_streams'; export { registerRoutes as registerEPMRoutes } from './epm'; export { registerRoutes as registerSetupRoutes } from './setup'; export { registerRoutes as registerAgentRoutes } from './agent'; diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts index 5c66f9008e2a3..837e73b966feb 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts @@ -5,7 +5,7 @@ */ import { RequestHandler } from 'src/core/server'; import { outputService } from '../../services'; -import { CreateFleetSetupResponse } from '../../types'; +import { CreateFleetSetupResponse } from '../../../common'; import { setupIngestManager, setupFleet } from '../../services/setup'; export const getFleetSetupHandler: RequestHandler = async (context, request, response) => { diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts index a2c641503e825..edc9a0a268161 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts @@ -5,7 +5,6 @@ */ import { IRouter } from 'src/core/server'; import { PLUGIN_ID, FLEET_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../constants'; -import { GetFleetSetupRequestSchema, CreateFleetSetupRequestSchema } from '../../types'; import { getFleetSetupHandler, createFleetSetupHandler, @@ -28,7 +27,7 @@ export const registerRoutes = (router: IRouter) => { router.get( { path: FLEET_SETUP_API_ROUTES.INFO_PATTERN, - validate: GetFleetSetupRequestSchema, + validate: false, options: { tags: [`access:${PLUGIN_ID}-read`] }, }, getFleetSetupHandler @@ -38,7 +37,7 @@ export const registerRoutes = (router: IRouter) => { router.post( { path: FLEET_SETUP_API_ROUTES.CREATE_PATTERN, - validate: CreateFleetSetupRequestSchema, + validate: false, options: { tags: [`access:${PLUGIN_ID}-all`] }, }, createFleetSetupHandler diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index 105f9039f1e98..aa5496cc836b7 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -22,6 +22,7 @@ export { AgentConfig, NewAgentConfig, AgentConfigStatus, + DataStream, Output, NewOutput, OutputType, diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts index c143cd3b35f91..42b607fa1c715 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts @@ -9,5 +9,4 @@ export * from './agent'; export * from './datasource'; export * from './epm'; export * from './enrollment_api_key'; -export * from './fleet_setup'; export * from './install_script'; From 97c712758939b541c27a6518d717f099886a3ab0 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Thu, 23 Apr 2020 16:30:32 -0600 Subject: [PATCH 14/35] [Maps] Migrate Maps embeddables to NP (#63976) * Migrate maps embeddables. Clean up legacy presence * Fix type error Co-authored-by: Elastic Machine --- x-pack/legacy/plugins/maps/index.js | 1 - .../embeddable/map_embeddable_factory.ts | 30 ------------------- x-pack/plugins/maps/kibana.json | 4 ++- x-pack/plugins/maps/public/plugin.ts | 7 ++++- 4 files changed, 9 insertions(+), 33 deletions(-) delete mode 100644 x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index 8546e3712c763..d1e8892fa2c98 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -57,7 +57,6 @@ export function maps(kibana) { tilemap: _.get(mapConfig, 'tilemap', []), }; }, - embeddableFactories: ['plugins/maps/embeddable/map_embeddable_factory'], styleSheetPaths: `${__dirname}/public/index.scss`, savedObjectSchemas: { 'maps-telemetry': { diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts deleted file mode 100644 index 90b17412377f5..0000000000000 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ /dev/null @@ -1,30 +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. - */ - -/* - Maintain legacy embeddable legacy present while apps switch over - */ - -import { npSetup, npStart } from 'ui/new_platform'; -import { - bindSetupCoreAndPlugins, - bindStartCoreAndPlugins, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/plugin'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MAP_SAVED_OBJECT_TYPE } from '../../../../../plugins/maps/common/constants'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MapEmbeddableFactory } from '../../../../../plugins/maps/public/embeddable'; - -bindSetupCoreAndPlugins(npSetup.core, npSetup.plugins); -bindStartCoreAndPlugins(npStart.core, npStart.plugins); - -export * from '../../../../../plugins/maps/public/embeddable/map_embeddable_factory'; - -npSetup.plugins.embeddable.registerEmbeddableFactory( - MAP_SAVED_OBJECT_TYPE, - new MapEmbeddableFactory() -); diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 00c5e70ad6b8d..b8bad47327f22 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -5,12 +5,14 @@ "configPath": ["xpack", "maps"], "requiredPlugins": [ "inspector", + "licensing", "home", "data", "fileUpload", "uiActions", "navigation", - "visualizations" + "visualizations", + "embeddable" ], "ui": true } diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 74bd305bff963..bdcd14ea98782 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -39,11 +39,15 @@ import { getMapsVisTypeAlias } from './maps_vis_type_alias'; import { registerLayerWizards } from './layers/load_layer_wizards'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { VisualizationsSetup } from '../../../../src/plugins/visualizations/public'; +import { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; +import { MapEmbeddableFactory } from './embeddable'; +import { EmbeddableSetup } from '../../../../src/plugins/embeddable/public'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; home: HomePublicPluginSetup; visualizations: VisualizationsSetup; + embeddable: EmbeddableSetup; } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface MapsPluginStartDependencies {} @@ -102,12 +106,13 @@ export class MapsPlugin MapsPluginStartDependencies > { public setup(core: CoreSetup, plugins: MapsPluginSetupDependencies) { - const { inspector, home, visualizations } = plugins; + const { inspector, home, visualizations, embeddable } = plugins; bindSetupCoreAndPlugins(core, plugins); inspector.registerView(MapView); home.featureCatalogue.register(featureCatalogueEntry); visualizations.registerAlias(getMapsVisTypeAlias()); + embeddable.registerEmbeddableFactory(MAP_SAVED_OBJECT_TYPE, new MapEmbeddableFactory()); } public start(core: CoreStart, plugins: any) { From 9e11bcb59266599a0048dfc7defbade77728e27e Mon Sep 17 00:00:00 2001 From: Nick Peihl Date: Thu, 23 Apr 2020 15:35:50 -0700 Subject: [PATCH 15/35] Add Oil Rig Icon from @elastic/maki (#64364) --- x-pack/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/package.json b/x-pack/package.json index 7d2dcb789c2ad..2a8827a1ed75b 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -185,7 +185,7 @@ "@elastic/ems-client": "7.8.0", "@elastic/eui": "22.3.0", "@elastic/filesaver": "1.1.2", - "@elastic/maki": "6.2.0", + "@elastic/maki": "6.3.0", "@elastic/node-crypto": "1.1.1", "@elastic/numeral": "2.4.0", "@kbn/babel-preset": "1.0.0", diff --git a/yarn.lock b/yarn.lock index 79d3911919693..66385c29d29b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1377,10 +1377,10 @@ through2 "^2.0.0" update-notifier "^0.5.0" -"@elastic/maki@6.2.0": - version "6.2.0" - resolved "https://registry.yarnpkg.com/@elastic/maki/-/maki-6.2.0.tgz#d0a85aa248bdc14dca44e1f9430c0b670f65e489" - integrity sha512-QkmRNpEY4Dy6eqwDimR5X9leMgdPFjdANmpEIwEW1XVUG2U4YtB2BXhDxsnMmNTUrJUjtnjnwgwBUyg0pU0FTg== +"@elastic/maki@6.3.0": + version "6.3.0" + resolved "https://registry.yarnpkg.com/@elastic/maki/-/maki-6.3.0.tgz#09780650f1510554bef9121b9db86ce297f021f1" + integrity sha512-a2U2DaemIJaW+3nL/sN/+JScdrkoggoGHLDtRPurk2Axnpa9O9QHekmMXLO7eLK1brDpYcplqGE6hwFaMvRRUg== "@elastic/node-crypto@1.1.1": version "1.1.1" From feed406c77ab2fd177a1d2eef849734d3b1eefd3 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Thu, 23 Apr 2020 15:59:01 -0700 Subject: [PATCH 16/35] [eslint] no_restricted_paths config cleanup (#63741) Major cleanup of the no_restricted_paths rule for imports of core. For relative imports, we use eslint-module-utils/resolve which resolves to the full filesystem path. So, to support relative and absolute imports from the src alias we need to define both the directory and the index including file extension. This rule was handling both core imports, as well as imports from other plugins. Imports from other plugins are being used much more liberally allowed through the exceptions in tests. I choose to break these up, removing this exception for tests for core imports. Fixes: Absolute imports of src/core/server/mocks were not allowed in src. This was not an issue in x-pack due to the target excluding !x-pack/**/*.test.* and !x-pack/test/**/*. Non-top-level public and server imports were allowed from X-Pack tests to the previously mentioned exclusion. Signed-off-by: Tyler Smalley --- .eslintrc.js | 39 ++++++++++++------- src/core/public/http/index.ts | 1 + src/core/public/index.ts | 1 + src/core/public/public.api.md | 17 ++++++++ src/core/server/mocks.ts | 1 + .../__tests__/empty_state.test.tsx | 3 +- .../state/api/__tests__/snapshot.test.ts | 2 +- .../effects/__tests__/fetch_effect.test.ts | 2 +- .../actions/server/routes/create.test.ts | 10 ++--- .../actions/server/routes/delete.test.ts | 8 ++-- .../actions/server/routes/execute.test.ts | 12 +++--- .../plugins/actions/server/routes/get.test.ts | 8 ++-- .../actions/server/routes/get_all.test.ts | 8 ++-- .../server/routes/list_action_types.test.ts | 8 ++-- .../actions/server/routes/update.test.ts | 10 ++--- .../alerting/server/routes/create.test.ts | 8 ++-- .../alerting/server/routes/delete.test.ts | 8 ++-- .../alerting/server/routes/disable.test.ts | 4 +- .../alerting/server/routes/enable.test.ts | 4 +- .../alerting/server/routes/find.test.ts | 8 ++-- .../alerting/server/routes/get.test.ts | 8 ++-- .../server/routes/get_alert_state.test.ts | 10 ++--- .../alerting/server/routes/health.test.ts | 18 ++++----- .../server/routes/list_alert_types.test.ts | 8 ++-- .../alerting/server/routes/mute_all.test.ts | 4 +- .../server/routes/mute_instance.test.ts | 4 +- .../alerting/server/routes/unmute_all.test.ts | 4 +- .../server/routes/unmute_instance.test.ts | 4 +- .../alerting/server/routes/update.test.ts | 8 ++-- .../server/routes/update_api_key.test.ts | 4 +- .../server/event_log_service.test.ts | 2 +- .../server/event_log_start_service.test.ts | 3 +- .../event_log/server/event_logger.test.ts | 15 ++++--- .../server/lib/bounded_queue.test.ts | 2 +- .../event_log/server/routes/find.test.ts | 6 +-- .../routes/agent/actions_handlers.test.ts | 3 +- .../server/services/agents/acks.test.ts | 2 +- .../server/services/agents/actions.test.ts | 2 +- .../server/services/agents/status.test.ts | 2 +- .../can_redirect_request.test.ts | 2 +- .../rules_notification_alert_type.test.ts | 6 +-- .../notifications/types.test.ts | 6 ++- .../signals/signal_rule_alert_type.test.ts | 6 +-- .../build_validation/route_validation.test.ts | 2 +- .../task_manager/server/task_store.test.ts | 9 +++-- .../reindex/polling_service.test.ts | 2 +- .../server/routes/telemetry.test.ts | 2 +- .../__tests__/get_monitor_status.test.ts | 2 +- 48 files changed, 171 insertions(+), 137 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index b3a1274d1cbeb..81a883f3397e8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -185,31 +185,40 @@ module.exports = { zones: [ { target: [ - 'src/legacy/**/*', - 'x-pack/**/*', - '!x-pack/**/*.test.*', - '!x-pack/test/**/*', + '(src|x-pack)/legacy/**/*', '(src|x-pack)/plugins/**/(public|server)/**/*', - 'src/core/(public|server)/**/*', 'examples/**/*', ], from: [ 'src/core/public/**/*', - '!src/core/public/index.ts', - '!src/core/public/mocks.ts', - '!src/core/public/*.test.mocks.ts', + '!src/core/public/index.ts', // relative import + '!src/core/public/mocks{,.ts}', + '!src/core/server/types{,.ts}', '!src/core/public/utils/**/*', + '!src/core/public/*.test.mocks{,.ts}', 'src/core/server/**/*', - '!src/core/server/index.ts', - '!src/core/server/mocks.ts', - '!src/core/server/types.ts', - '!src/core/server/test_utils.ts', + '!src/core/server/index.ts', // relative import + '!src/core/server/mocks{,.ts}', + '!src/core/server/types{,.ts}', + '!src/core/server/test_utils', // for absolute imports until fixed in // https://github.com/elastic/kibana/issues/36096 - '!src/core/server/types', - '!src/core/server/*.test.mocks.ts', - + '!src/core/server/*.test.mocks{,.ts}', + ], + allowSameFolder: true, + errorMessage: + 'Plugins may only import from top-level public and server modules in core.', + }, + { + target: [ + '(src|x-pack)/legacy/**/*', + '(src|x-pack)/plugins/**/(public|server)/**/*', + 'examples/**/*', + '!(src|x-pack)/**/*.test.*', + '!(x-pack/)?test/**/*', + ], + from: [ '(src|x-pack)/plugins/**/(public|server)/**/*', '!(src|x-pack)/plugins/**/(public|server)/(index|mocks).{js,ts,tsx}', ], diff --git a/src/core/public/http/index.ts b/src/core/public/http/index.ts index d4aced6894526..3cd8ef6169090 100644 --- a/src/core/public/http/index.ts +++ b/src/core/public/http/index.ts @@ -18,4 +18,5 @@ */ export { HttpService } from './http_service'; +export { HttpFetchError } from './http_fetch_error'; export * from './types'; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 254cac3495599..b4f64125a03ef 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -143,6 +143,7 @@ export { export { HttpHeadersInit, HttpRequestInit, + HttpFetchError, HttpFetchOptions, HttpFetchOptionsWithPath, HttpFetchQuery, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 6d95d1bc7069c..b92bb209d2607 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -593,6 +593,23 @@ export type HandlerFunction = (context: T, ...args: any[]) => // @public export type HandlerParameters> = T extends (context: any, ...args: infer U) => any ? U : never; +// @internal (undocumented) +export class HttpFetchError extends Error implements IHttpFetchError { + constructor(message: string, name: string, request: Request, response?: Response | undefined, body?: any); + // (undocumented) + readonly body?: any; + // (undocumented) + readonly name: string; + // (undocumented) + readonly req: Request; + // (undocumented) + readonly request: Request; + // (undocumented) + readonly res?: Response; + // (undocumented) + readonly response?: Response | undefined; +} + // @public export interface HttpFetchOptions extends HttpRequestInit { asResponse?: boolean; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 3b9a39db72278..2451b98ffdf29 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -45,6 +45,7 @@ export { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service. export { httpServiceMock } from './http/http_service.mock'; export { loggingServiceMock } from './logging/logging_service.mock'; export { savedObjectsRepositoryMock } from './saved_objects/service/lib/repository.mock'; +export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { metricsServiceMock } from './metrics/metrics_service.mock'; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx index acfe2ada5b68d..6328789d03f29 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx @@ -7,8 +7,7 @@ import React from 'react'; import { EmptyStateComponent } from '../empty_state'; import { StatesIndexStatus } from '../../../../../common/runtime_types'; -import { IHttpFetchError } from '../../../../../../../../../target/types/core/public/http'; -import { HttpFetchError } from '../../../../../../../../../src/core/public/http/http_fetch_error'; +import { HttpFetchError, IHttpFetchError } from 'src/core/public'; import { mountWithRouter, shallowWithRouter } from '../../../../lib'; describe('EmptyState component', () => { diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts index 66b376c3ac36f..ff9fcd0573257 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts @@ -6,7 +6,7 @@ import { fetchSnapshotCount } from '../snapshot'; import { apiService } from '../utils'; -import { HttpFetchError } from '../../../../../../../../src/core/public/http/http_fetch_error'; +import { HttpFetchError } from 'src/core/public'; describe('snapshot API', () => { let fetchMock: jest.SpyInstance>; diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts b/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts index 0354cfeac7b07..4ec35d8cd6c6f 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts @@ -7,7 +7,7 @@ import { call, put } from 'redux-saga/effects'; import { fetchEffectFactory } from '../fetch_effect'; import { indexStatusAction } from '../../actions'; -import { HttpFetchError } from '../../../../../../../../src/core/public/http/http_fetch_error'; +import { HttpFetchError } from 'src/core/public'; import { StatesIndexStatus } from '../../../../common/runtime_types'; import { fetchIndexStatus } from '../../api'; diff --git a/x-pack/plugins/actions/server/routes/create.test.ts b/x-pack/plugins/actions/server/routes/create.test.ts index 22cf0dd7f8ace..a01c5e9c2452c 100644 --- a/x-pack/plugins/actions/server/routes/create.test.ts +++ b/x-pack/plugins/actions/server/routes/create.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { createActionRoute } from './create'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess, ActionTypeDisabledError } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -20,7 +20,7 @@ beforeEach(() => { describe('createActionRoute', () => { it('creates an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createActionRoute(router, licenseState); @@ -83,7 +83,7 @@ describe('createActionRoute', () => { it('ensures the license allows creating actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createActionRoute(router, licenseState); @@ -107,7 +107,7 @@ describe('createActionRoute', () => { it('ensures the license check prevents creating actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -135,7 +135,7 @@ describe('createActionRoute', () => { it('ensures the action type gets validated for the license', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createActionRoute(router, licenseState); diff --git a/x-pack/plugins/actions/server/routes/delete.test.ts b/x-pack/plugins/actions/server/routes/delete.test.ts index 6fb526628cb02..b0929f48d7875 100644 --- a/x-pack/plugins/actions/server/routes/delete.test.ts +++ b/x-pack/plugins/actions/server/routes/delete.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { deleteActionRoute } from './delete'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -20,7 +20,7 @@ beforeEach(() => { describe('deleteActionRoute', () => { it('deletes an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); deleteActionRoute(router, licenseState); @@ -65,7 +65,7 @@ describe('deleteActionRoute', () => { it('ensures the license allows deleting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); deleteActionRoute(router, licenseState); @@ -86,7 +86,7 @@ describe('deleteActionRoute', () => { it('ensures the license check prevents deleting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts index 3a3ed1257f576..1cd6343a39dcf 100644 --- a/x-pack/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/execute.test.ts @@ -5,7 +5,7 @@ */ import { executeActionRoute } from './execute'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { ActionExecutorContract, verifyApiAccess, ActionTypeDisabledError } from '../lib'; @@ -21,7 +21,7 @@ beforeEach(() => { describe('executeActionRoute', () => { it('executes an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const [context, req, res] = mockHandlerArguments( {}, @@ -77,7 +77,7 @@ describe('executeActionRoute', () => { it('returns a "204 NO CONTENT" when the executor returns a nullish value', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const [context, req, res] = mockHandlerArguments( {}, @@ -115,7 +115,7 @@ describe('executeActionRoute', () => { it('ensures the license allows action execution', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const [context, req, res] = mockHandlerArguments( {}, @@ -147,7 +147,7 @@ describe('executeActionRoute', () => { it('ensures the license check prevents action execution', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -183,7 +183,7 @@ describe('executeActionRoute', () => { it('ensures the action type gets validated for the license', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const [context, req, res] = mockHandlerArguments( {}, diff --git a/x-pack/plugins/actions/server/routes/get.test.ts b/x-pack/plugins/actions/server/routes/get.test.ts index f4e834a5b767c..7de4d93d91bb6 100644 --- a/x-pack/plugins/actions/server/routes/get.test.ts +++ b/x-pack/plugins/actions/server/routes/get.test.ts @@ -5,7 +5,7 @@ */ import { getActionRoute } from './get'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -21,7 +21,7 @@ beforeEach(() => { describe('getActionRoute', () => { it('gets an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getActionRoute(router, licenseState); @@ -75,7 +75,7 @@ describe('getActionRoute', () => { it('ensures the license allows getting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getActionRoute(router, licenseState); @@ -105,7 +105,7 @@ describe('getActionRoute', () => { it('ensures the license check prevents getting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); diff --git a/x-pack/plugins/actions/server/routes/get_all.test.ts b/x-pack/plugins/actions/server/routes/get_all.test.ts index 6499427b8c1a5..1422bf0965786 100644 --- a/x-pack/plugins/actions/server/routes/get_all.test.ts +++ b/x-pack/plugins/actions/server/routes/get_all.test.ts @@ -5,7 +5,7 @@ */ import { getAllActionRoute } from './get_all'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -21,7 +21,7 @@ beforeEach(() => { describe('getAllActionRoute', () => { it('get all actions with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAllActionRoute(router, licenseState); @@ -57,7 +57,7 @@ describe('getAllActionRoute', () => { it('ensures the license allows getting all actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAllActionRoute(router, licenseState); @@ -85,7 +85,7 @@ describe('getAllActionRoute', () => { it('ensures the license check prevents getting all actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); diff --git a/x-pack/plugins/actions/server/routes/list_action_types.test.ts b/x-pack/plugins/actions/server/routes/list_action_types.test.ts index 76fb636a75be7..38410f45f091d 100644 --- a/x-pack/plugins/actions/server/routes/list_action_types.test.ts +++ b/x-pack/plugins/actions/server/routes/list_action_types.test.ts @@ -5,7 +5,7 @@ */ import { listActionTypesRoute } from './list_action_types'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -21,7 +21,7 @@ beforeEach(() => { describe('listActionTypesRoute', () => { it('lists action types with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); listActionTypesRoute(router, licenseState); @@ -67,7 +67,7 @@ describe('listActionTypesRoute', () => { it('ensures the license allows listing action types', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); listActionTypesRoute(router, licenseState); @@ -105,7 +105,7 @@ describe('listActionTypesRoute', () => { it('ensures the license check prevents listing action types', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); diff --git a/x-pack/plugins/actions/server/routes/update.test.ts b/x-pack/plugins/actions/server/routes/update.test.ts index 161fb4398af1d..0c2e9b622ee57 100644 --- a/x-pack/plugins/actions/server/routes/update.test.ts +++ b/x-pack/plugins/actions/server/routes/update.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { updateActionRoute } from './update'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess, ActionTypeDisabledError } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -20,7 +20,7 @@ beforeEach(() => { describe('updateActionRoute', () => { it('updates an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateActionRoute(router, licenseState); @@ -86,7 +86,7 @@ describe('updateActionRoute', () => { it('ensures the license allows deleting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateActionRoute(router, licenseState); @@ -125,7 +125,7 @@ describe('updateActionRoute', () => { it('ensures the license check prevents deleting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -168,7 +168,7 @@ describe('updateActionRoute', () => { it('ensures the action type gets validated for the license', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateActionRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/create.test.ts b/x-pack/plugins/alerting/server/routes/create.test.ts index c6a0da2bd9191..294dd7e002d71 100644 --- a/x-pack/plugins/alerting/server/routes/create.test.ts +++ b/x-pack/plugins/alerting/server/routes/create.test.ts @@ -5,7 +5,7 @@ */ import { createAlertRoute } from './create'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -68,7 +68,7 @@ describe('createAlertRoute', () => { it('creates an alert with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createAlertRoute(router, licenseState); @@ -134,7 +134,7 @@ describe('createAlertRoute', () => { it('ensures the license allows creating alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createAlertRoute(router, licenseState); @@ -151,7 +151,7 @@ describe('createAlertRoute', () => { it('ensures the license check prevents creating alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); diff --git a/x-pack/plugins/alerting/server/routes/delete.test.ts b/x-pack/plugins/alerting/server/routes/delete.test.ts index 36bb485817c15..d6afdbd85e77a 100644 --- a/x-pack/plugins/alerting/server/routes/delete.test.ts +++ b/x-pack/plugins/alerting/server/routes/delete.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { deleteAlertRoute } from './delete'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -23,7 +23,7 @@ beforeEach(() => { describe('deleteAlertRoute', () => { it('deletes an alert with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); deleteAlertRoute(router, licenseState); @@ -66,7 +66,7 @@ describe('deleteAlertRoute', () => { it('ensures the license allows deleting alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); deleteAlertRoute(router, licenseState); @@ -85,7 +85,7 @@ describe('deleteAlertRoute', () => { it('ensures the license check prevents deleting alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); diff --git a/x-pack/plugins/alerting/server/routes/disable.test.ts b/x-pack/plugins/alerting/server/routes/disable.test.ts index 622b562ec6911..fde095e9145b6 100644 --- a/x-pack/plugins/alerting/server/routes/disable.test.ts +++ b/x-pack/plugins/alerting/server/routes/disable.test.ts @@ -5,7 +5,7 @@ */ import { disableAlertRoute } from './disable'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -23,7 +23,7 @@ beforeEach(() => { describe('disableAlertRoute', () => { it('disables an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); disableAlertRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/enable.test.ts b/x-pack/plugins/alerting/server/routes/enable.test.ts index 5a7b027e8ef54..e4e89e3f06380 100644 --- a/x-pack/plugins/alerting/server/routes/enable.test.ts +++ b/x-pack/plugins/alerting/server/routes/enable.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { enableAlertRoute } from './enable'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('enableAlertRoute', () => { it('enables an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); enableAlertRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/find.test.ts b/x-pack/plugins/alerting/server/routes/find.test.ts index 391d6df3f9931..879d9498cda03 100644 --- a/x-pack/plugins/alerting/server/routes/find.test.ts +++ b/x-pack/plugins/alerting/server/routes/find.test.ts @@ -5,7 +5,7 @@ */ import { findAlertRoute } from './find'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -24,7 +24,7 @@ beforeEach(() => { describe('findAlertRoute', () => { it('finds alerts with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); findAlertRoute(router, licenseState); @@ -95,7 +95,7 @@ describe('findAlertRoute', () => { it('ensures the license allows finding alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); findAlertRoute(router, licenseState); @@ -123,7 +123,7 @@ describe('findAlertRoute', () => { it('ensures the license check prevents finding alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); diff --git a/x-pack/plugins/alerting/server/routes/get.test.ts b/x-pack/plugins/alerting/server/routes/get.test.ts index 4506700c6d9cc..fe89c86edc2b1 100644 --- a/x-pack/plugins/alerting/server/routes/get.test.ts +++ b/x-pack/plugins/alerting/server/routes/get.test.ts @@ -5,7 +5,7 @@ */ import { getAlertRoute } from './get'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -55,7 +55,7 @@ describe('getAlertRoute', () => { it('gets an alert with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertRoute(router, licenseState); const [config, handler] = router.get.mock.calls[0]; @@ -90,7 +90,7 @@ describe('getAlertRoute', () => { it('ensures the license allows getting alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertRoute(router, licenseState); @@ -113,7 +113,7 @@ describe('getAlertRoute', () => { it('ensures the license check prevents getting alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); diff --git a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts b/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts index eb51c96b88e5e..20a420ca00986 100644 --- a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts @@ -5,10 +5,10 @@ */ import { getAlertStateRoute } from './get_alert_state'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; -import { SavedObjectsErrorHelpers } from 'src/core/server/saved_objects'; +import { SavedObjectsErrorHelpers } from 'src/core/server'; import { alertsClientMock } from '../alerts_client.mock'; const alertsClient = alertsClientMock.create(); @@ -41,7 +41,7 @@ describe('getAlertStateRoute', () => { it('gets alert state', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertStateRoute(router, licenseState); @@ -84,7 +84,7 @@ describe('getAlertStateRoute', () => { it('returns NO-CONTENT when alert exists but has no task state yet', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertStateRoute(router, licenseState); @@ -127,7 +127,7 @@ describe('getAlertStateRoute', () => { it('returns NOT-FOUND when alert is not found', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertStateRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/health.test.ts b/x-pack/plugins/alerting/server/routes/health.test.ts index 42c83a7c04deb..7c50fbf561e59 100644 --- a/x-pack/plugins/alerting/server/routes/health.test.ts +++ b/x-pack/plugins/alerting/server/routes/health.test.ts @@ -5,7 +5,7 @@ */ import { healthRoute } from './health'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { verifyApiAccess } from '../lib/license_api_access'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('healthRoute', () => { it('registers the route', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -35,7 +35,7 @@ describe('healthRoute', () => { }); it('queries the usage api', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -64,7 +64,7 @@ describe('healthRoute', () => { }); it('evaluates whether Encrypted Saved Objects is using an ephemeral encryption key', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -88,7 +88,7 @@ describe('healthRoute', () => { }); it('evaluates missing security info from the usage api to mean that the security plugin is disbled', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -112,7 +112,7 @@ describe('healthRoute', () => { }); it('evaluates missing security http info from the usage api to mean that the security plugin is disbled', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -136,7 +136,7 @@ describe('healthRoute', () => { }); it('evaluates security enabled, and missing ssl info from the usage api to mean that the user cannot generate keys', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -162,7 +162,7 @@ describe('healthRoute', () => { }); it('evaluates security enabled, SSL info present but missing http info from the usage api to mean that the user cannot generate keys', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -188,7 +188,7 @@ describe('healthRoute', () => { }); it('evaluates security and tls enabled to mean that the user can generate keys', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); diff --git a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts index 723fd86fca8b5..cc83d10fb97fd 100644 --- a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts +++ b/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts @@ -5,7 +5,7 @@ */ import { listAlertTypesRoute } from './list_alert_types'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -21,7 +21,7 @@ beforeEach(() => { describe('listAlertTypesRoute', () => { it('lists alert types with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); listAlertTypesRoute(router, licenseState); @@ -79,7 +79,7 @@ describe('listAlertTypesRoute', () => { it('ensures the license allows listing alert types', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); listAlertTypesRoute(router, licenseState); @@ -117,7 +117,7 @@ describe('listAlertTypesRoute', () => { it('ensures the license check prevents listing alert types', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); diff --git a/x-pack/plugins/alerting/server/routes/mute_all.test.ts b/x-pack/plugins/alerting/server/routes/mute_all.test.ts index 4c880e176d2df..5ef9e3694f8f4 100644 --- a/x-pack/plugins/alerting/server/routes/mute_all.test.ts +++ b/x-pack/plugins/alerting/server/routes/mute_all.test.ts @@ -5,7 +5,7 @@ */ import { muteAllAlertRoute } from './mute_all'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('muteAllAlertRoute', () => { it('mute an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); muteAllAlertRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts b/x-pack/plugins/alerting/server/routes/mute_instance.test.ts index 939864972c35d..2e6adedb76df9 100644 --- a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts +++ b/x-pack/plugins/alerting/server/routes/mute_instance.test.ts @@ -5,7 +5,7 @@ */ import { muteAlertInstanceRoute } from './mute_instance'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('muteAlertInstanceRoute', () => { it('mutes an alert instance', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); muteAlertInstanceRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts b/x-pack/plugins/alerting/server/routes/unmute_all.test.ts index cd14e9b2e7172..1756dbd3fb41d 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_all.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { unmuteAllAlertRoute } from './unmute_all'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -21,7 +21,7 @@ beforeEach(() => { describe('unmuteAllAlertRoute', () => { it('unmutes an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); unmuteAllAlertRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts b/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts index d74934f691710..9b9542c606741 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts @@ -5,7 +5,7 @@ */ import { unmuteAlertInstanceRoute } from './unmute_instance'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('unmuteAlertInstanceRoute', () => { it('unmutes an alert instance', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); unmuteAlertInstanceRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/update.test.ts b/x-pack/plugins/alerting/server/routes/update.test.ts index c3628617f861f..cd96f289b8714 100644 --- a/x-pack/plugins/alerting/server/routes/update.test.ts +++ b/x-pack/plugins/alerting/server/routes/update.test.ts @@ -5,7 +5,7 @@ */ import { updateAlertRoute } from './update'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -45,7 +45,7 @@ describe('updateAlertRoute', () => { it('updates an alert with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateAlertRoute(router, licenseState); @@ -128,7 +128,7 @@ describe('updateAlertRoute', () => { it('ensures the license allows updating alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateAlertRoute(router, licenseState); @@ -171,7 +171,7 @@ describe('updateAlertRoute', () => { it('ensures the license check prevents updating alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); diff --git a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts b/x-pack/plugins/alerting/server/routes/update_api_key.test.ts index 5e9821ac005e2..0347feb24a235 100644 --- a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts +++ b/x-pack/plugins/alerting/server/routes/update_api_key.test.ts @@ -5,7 +5,7 @@ */ import { updateApiKeyRoute } from './update_api_key'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('updateApiKeyRoute', () => { it('updates api key for an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateApiKeyRoute(router, licenseState); diff --git a/x-pack/plugins/event_log/server/event_log_service.test.ts b/x-pack/plugins/event_log/server/event_log_service.test.ts index 3b250b7462009..43883ea4e384c 100644 --- a/x-pack/plugins/event_log/server/event_log_service.test.ts +++ b/x-pack/plugins/event_log/server/event_log_service.test.ts @@ -7,7 +7,7 @@ import { IEventLogConfig } from './types'; import { EventLogService } from './event_log_service'; import { contextMock } from './es/context.mock'; -import { loggingServiceMock } from '../../../../src/core/server/logging/logging_service.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; const loggingService = loggingServiceMock.create(); const systemLogger = loggingService.get(); diff --git a/x-pack/plugins/event_log/server/event_log_start_service.test.ts b/x-pack/plugins/event_log/server/event_log_start_service.test.ts index a8d75bc6c2e5a..6db16ebadd4ce 100644 --- a/x-pack/plugins/event_log/server/event_log_start_service.test.ts +++ b/x-pack/plugins/event_log/server/event_log_start_service.test.ts @@ -7,8 +7,7 @@ import { EventLogClientService } from './event_log_start_service'; import { contextMock } from './es/context.mock'; import { KibanaRequest } from 'kibana/server'; -import { savedObjectsServiceMock } from 'src/core/server/saved_objects/saved_objects_service.mock'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock, savedObjectsServiceMock } from 'src/core/server/mocks'; jest.mock('./event_log_client'); diff --git a/x-pack/plugins/event_log/server/event_logger.test.ts b/x-pack/plugins/event_log/server/event_logger.test.ts index 673bac4f396e1..6a745931420c0 100644 --- a/x-pack/plugins/event_log/server/event_logger.test.ts +++ b/x-pack/plugins/event_log/server/event_logger.test.ts @@ -9,20 +9,20 @@ import { ECS_VERSION } from './types'; import { EventLogService } from './event_log_service'; import { EsContext } from './es/context'; import { contextMock } from './es/context.mock'; -import { loggerMock, MockedLogger } from '../../../../src/core/server/logging/logger.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; import { delay } from './lib/delay'; import { EVENT_LOGGED_PREFIX } from './event_logger'; const KIBANA_SERVER_UUID = '424-24-2424'; describe('EventLogger', () => { - let systemLogger: MockedLogger; + let systemLogger: ReturnType; let esContext: EsContext; let service: IEventLogService; let eventLogger: IEventLogger; beforeEach(() => { - systemLogger = loggerMock.create(); + systemLogger = loggingServiceMock.createLogger(); esContext = contextMock.create(); service = new EventLogService({ esContext, @@ -153,7 +153,10 @@ describe('EventLogger', () => { }); // return the next logged event; throw if not an event -async function waitForLogEvent(mockLogger: MockedLogger, waitSeconds: number = 1): Promise { +async function waitForLogEvent( + mockLogger: ReturnType, + waitSeconds: number = 1 +): Promise { const result = await waitForLog(mockLogger, waitSeconds); if (typeof result === 'string') throw new Error('expecting an event'); return result; @@ -161,7 +164,7 @@ async function waitForLogEvent(mockLogger: MockedLogger, waitSeconds: number = 1 // return the next logged message; throw if it is an event async function waitForLogMessage( - mockLogger: MockedLogger, + mockLogger: ReturnType, waitSeconds: number = 1 ): Promise { const result = await waitForLog(mockLogger, waitSeconds); @@ -171,7 +174,7 @@ async function waitForLogMessage( // return the next logged message, if it's an event log entry, parse it async function waitForLog( - mockLogger: MockedLogger, + mockLogger: ReturnType, waitSeconds: number = 1 ): Promise { const intervals = 4; diff --git a/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts b/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts index c1bb6d70879f3..dd6d15a6e4843 100644 --- a/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts +++ b/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts @@ -5,7 +5,7 @@ */ import { createBoundedQueue } from './bounded_queue'; -import { loggingServiceMock } from '../../../../../src/core/server/logging/logging_service.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; const loggingService = loggingServiceMock.create(); const logger = loggingService.get(); diff --git a/x-pack/plugins/event_log/server/routes/find.test.ts b/x-pack/plugins/event_log/server/routes/find.test.ts index 844a84dc117a9..f47df499d742f 100644 --- a/x-pack/plugins/event_log/server/routes/find.test.ts +++ b/x-pack/plugins/event_log/server/routes/find.test.ts @@ -5,7 +5,7 @@ */ import { findRoute } from './find'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockHandlerArguments, fakeEvent } from './_mock_handler_arguments'; import { eventLogClientMock } from '../event_log_client.mock'; @@ -17,7 +17,7 @@ beforeEach(() => { describe('find', () => { it('finds events with proper parameters', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); findRoute(router); @@ -56,7 +56,7 @@ describe('find', () => { }); it('supports optional pagination parameters', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); findRoute(router); diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts index 76247c338a24f..bcb9a7797f26a 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts @@ -10,8 +10,7 @@ import { RequestHandlerContext, SavedObjectsClientContract, } from 'kibana/server'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; -import { httpServerMock } from '../../../../../../src/core/server/http/http_server.mocks'; +import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; import { ActionsService } from '../../services/agents'; import { AgentAction } from '../../../common/types/models'; import { postNewAgentActionHandlerBuilder } from './actions_handlers'; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts index 62871710d9877..a8fada00e25da 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; import { SavedObjectsBulkResponse } from 'kibana/server'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; import { encryptedSavedObjectsMock } from '../../../../../plugins/encrypted_saved_objects/server/mocks'; import { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts index 29143502247aa..c739007952389 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts @@ -7,7 +7,7 @@ import { createAgentAction } from './actions'; import { SavedObject } from 'kibana/server'; import { AgentAction } from '../../../common/types/models'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; describe('test agent actions services', () => { it('should create a new action', async () => { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts index d19fe883a7780..b6de083cbe0cb 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; import { getAgentStatusById } from './status'; import { AGENT_TYPE_PERMANENT } from '../../../common/constants'; import { AgentSOAttributes } from '../../../common/types/models'; diff --git a/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts b/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts index 1c9b936692f9e..9f1d6b27aa9d7 100644 --- a/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts +++ b/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServerMock } from '../../../../../src/core/server/http/http_server.mocks'; +import { httpServerMock } from 'src/core/server/mocks'; import { canRedirectRequest } from './can_redirect_request'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts index 6244a4cc64e68..e8d778bddadc2 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { loggerMock } from 'src/core/server/logging/logger.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; import { getResult } from '../routes/__mocks__/request_responses'; import { rulesNotificationAlertType } from './rules_notification_alert_type'; import { buildSignalsSearchQuery } from './build_signals_query'; @@ -15,12 +15,12 @@ jest.mock('./build_signals_query'); describe('rules_notification_alert_type', () => { let payload: NotificationExecutorOptions; let alert: ReturnType; - let logger: ReturnType; + let logger: ReturnType; let alertServices: AlertServicesMock; beforeEach(() => { alertServices = alertsMock.createAlertServices(); - logger = loggerMock.create(); + logger = loggingServiceMock.createLogger(); payload = { alertId: '1111', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts index 4fce037b483d5..0c9ccf069b3b6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { loggerMock } from 'src/core/server/logging/logger.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; import { getNotificationResult, getResult } from '../routes/__mocks__/request_responses'; import { isAlertTypes, isNotificationAlertExecutor } from './types'; import { rulesNotificationAlertType } from './rules_notification_alert_type'; @@ -20,7 +20,9 @@ describe('types', () => { it('isNotificationAlertExecutor should return true it passed object is NotificationAlertTypeDefinition type', () => { expect( - isNotificationAlertExecutor(rulesNotificationAlertType({ logger: loggerMock.create() })) + isNotificationAlertExecutor( + rulesNotificationAlertType({ logger: loggingServiceMock.createLogger() }) + ) ).toEqual(true); }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 7eecc5cb9bad0..0c7f0839f8daf 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -5,7 +5,7 @@ */ import moment from 'moment'; -import { loggerMock } from 'src/core/server/logging/logger.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { signalRulesAlertType } from './signal_rule_alert_type'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; @@ -70,13 +70,13 @@ describe('rules_notification_alert_type', () => { }; let payload: jest.Mocked; let alert: ReturnType; - let logger: ReturnType; + let logger: ReturnType; let alertServices: AlertServicesMock; let ruleStatusService: Record; beforeEach(() => { alertServices = alertsMock.createAlertServices(); - logger = loggerMock.create(); + logger = loggingServiceMock.createLogger(); ruleStatusService = { success: jest.fn(), find: jest.fn(), diff --git a/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts b/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts index 866dbe9480ca5..8a1e4271303a6 100644 --- a/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts +++ b/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts @@ -6,7 +6,7 @@ import { buildRouteValidation } from './route_validation'; import * as rt from 'io-ts'; -import { RouteValidationResultFactory } from '../../../../../../src/core/server/http'; +import { RouteValidationResultFactory } from 'src/core/server'; describe('buildRouteValidation', () => { const schema = rt.exact( diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index 4ecefcb7984eb..8f122230d5965 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -17,9 +17,12 @@ import { TaskLifecycleResult, } from './task'; import { StoreOpts, OwnershipClaimingOpts, TaskStore, SearchOpts } from './task_store'; -import { savedObjectsRepositoryMock } from '../../../../src/core/server/mocks'; -import { SavedObjectsSerializer, SavedObjectTypeRegistry } from '../../../../src/core/server'; -import { SavedObjectsErrorHelpers } from '../../../../src/core/server/saved_objects/service/lib/errors'; +import { savedObjectsRepositoryMock } from 'src/core/server/mocks'; +import { + SavedObjectsSerializer, + SavedObjectTypeRegistry, + SavedObjectsErrorHelpers, +} from 'src/core/server'; import { asTaskClaimEvent, TaskEvent } from './task_events'; import { asOk, asErr } from './lib/result_type'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts index 4228426d62159..5301f6364529d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts @@ -6,7 +6,7 @@ import { ReindexStatus, ReindexStep } from '../../../../../../../common/types'; import { ReindexPollingService } from './polling_service'; -import { httpServiceMock } from 'src/core/public/http/http_service.mock'; +import { httpServiceMock } from 'src/core/public/mocks'; const mockClient = httpServiceMock.createSetupContract(); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts index b2b8ccf1ca57a..78b03275e0ef9 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts @@ -5,7 +5,7 @@ */ import { kibanaResponseFactory } from 'src/core/server'; -import { savedObjectsServiceMock } from 'src/core/server/saved_objects/saved_objects_service.mock'; +import { savedObjectsServiceMock } from 'src/core/server/mocks'; import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; 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 e429de9ae0d68..c94b5c96aa999 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 @@ -6,7 +6,7 @@ import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; import { getMonitorStatus } from '../get_monitor_status'; -import { ScopedClusterClient } from 'src/core/server/elasticsearch'; +import { ScopedClusterClient } from 'src/core/server'; import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; interface BucketItemCriteria { From f520bbd7547d3cb6fc114d4181c8fc1cdf174b9c Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 23 Apr 2020 16:31:26 -0700 Subject: [PATCH 17/35] Renamed ilm policy for event log so it is not prefixed with dot (#64262) * Renamed ilm policy for event log so it is not prefixed with dot * Fixed due to comments --- x-pack/plugins/event_log/server/es/names.mock.ts | 2 +- x-pack/plugins/event_log/server/es/names.test.ts | 6 ++++++ x-pack/plugins/event_log/server/es/names.ts | 5 ++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/event_log/server/es/names.mock.ts b/x-pack/plugins/event_log/server/es/names.mock.ts index 268421235b4b2..7b3d01f3baa89 100644 --- a/x-pack/plugins/event_log/server/es/names.mock.ts +++ b/x-pack/plugins/event_log/server/es/names.mock.ts @@ -10,7 +10,7 @@ const createNamesMock = () => { const mock: jest.Mocked = { base: '.kibana', alias: '.kibana-event-log-8.0.0', - ilmPolicy: '.kibana-event-log-policy', + ilmPolicy: 'kibana-event-log-policy', indexPattern: '.kibana-event-log-*', indexPatternWithVersion: '.kibana-event-log-8.0.0-*', initialIndex: '.kibana-event-log-8.0.0-000001', diff --git a/x-pack/plugins/event_log/server/es/names.test.ts b/x-pack/plugins/event_log/server/es/names.test.ts index baefd756bb1ed..bc6a4c9a52fac 100644 --- a/x-pack/plugins/event_log/server/es/names.test.ts +++ b/x-pack/plugins/event_log/server/es/names.test.ts @@ -23,4 +23,10 @@ describe('getEsNames()', () => { expect(esNames.initialIndex).toEqual(`${base}-event-log-${version}-000001`); expect(esNames.indexTemplate).toEqual(`${base}-event-log-${version}-template`); }); + + test('ilm policy name does not contain dot prefix', () => { + const base = '.XYZ'; + const esNames = getEsNames(base); + expect(esNames.ilmPolicy).toEqual('XYZ-event-log-policy'); + }); }); diff --git a/x-pack/plugins/event_log/server/es/names.ts b/x-pack/plugins/event_log/server/es/names.ts index d55d02a16fc9a..8cd56a89d3fbe 100644 --- a/x-pack/plugins/event_log/server/es/names.ts +++ b/x-pack/plugins/event_log/server/es/names.ts @@ -22,10 +22,13 @@ export interface EsNames { export function getEsNames(baseName: string): EsNames { const eventLogName = `${baseName}${EVENT_LOG_NAME_SUFFIX}`; const eventLogNameWithVersion = `${eventLogName}${EVENT_LOG_VERSION_SUFFIX}`; + const eventLogPolicyName = `${ + baseName.startsWith('.') ? baseName.substring(1) : baseName + }${EVENT_LOG_NAME_SUFFIX}-policy`; return { base: baseName, alias: eventLogNameWithVersion, - ilmPolicy: `${eventLogName}-policy`, + ilmPolicy: `${eventLogPolicyName}`, indexPattern: `${eventLogName}-*`, indexPatternWithVersion: `${eventLogNameWithVersion}-*`, initialIndex: `${eventLogNameWithVersion}-000001`, From a4e740ed3d90e584a80dd6811a1c535a92ac6ff6 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 23 Apr 2020 17:06:06 -0700 Subject: [PATCH 18/35] skip flaky suite (#62497) --- test/accessibility/apps/discover.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index 086b13ecee2b3..0168626d4a1a9 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -33,7 +33,8 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { ['geo.src', 'IN'], ]; - describe('Discover', () => { + // FLAKY: https://github.com/elastic/kibana/issues/62497 + describe.skip('Discover', () => { before(async () => { await esArchiver.load('discover'); await esArchiver.loadIfNeeded('logstash_functional'); From dd21b0b0a80ebf95dffe482ea259cabe9c1e84e6 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 23 Apr 2020 17:19:29 -0700 Subject: [PATCH 19/35] skip flaky suite (#61173) --- x-pack/test/functional/apps/security/management.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/security/management.js b/x-pack/test/functional/apps/security/management.js index 8ab84126b2b30..3bcf504c17a6f 100644 --- a/x-pack/test/functional/apps/security/management.js +++ b/x-pack/test/functional/apps/security/management.js @@ -19,7 +19,8 @@ export default function({ getService, getPageObjects }) { const browser = getService('browser'); const PageObjects = getPageObjects(['security', 'settings', 'common', 'header']); - describe('Management', function() { + // FLAKY: https://github.com/elastic/kibana/issues/61173 + describe.skip('Management', function() { this.tags(['skipFirefox']); before(async () => { From 34de1aeb8b24487009ea79e848678e623b30011f Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 24 Apr 2020 01:42:34 +0100 Subject: [PATCH 20/35] =?UTF-8?q?chore(NA):=20use=20core-js=20instead=20of?= =?UTF-8?q?=20babel-polyfill=20on=20canvas=20sha=E2=80=A6=20(#63486)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(NA): use core-js instead of babel-polyfill on canvas shareable runtime build * chore(NA): include regenerator-runtime/runtime * chore(NA): change import order between runtime and core js Co-authored-by: Elastic Machine --- x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts b/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts index b05379df6b0b1..0780ab46cd873 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import 'core-js/stable'; +import 'regenerator-runtime/runtime'; import 'whatwg-fetch'; -import 'babel-polyfill'; export * from './shareable'; From e3ee02c687410ff320ab141bf6d1e2f46d716450 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 24 Apr 2020 01:43:34 +0100 Subject: [PATCH 21/35] =?UTF-8?q?chore(NA):=20reduce=20siem=20bundle=20siz?= =?UTF-8?q?e=20using=20babel-plugin-transfor=E2=80=A6=20(#63269)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(NA): reduce siem bundle size using babel-plugin-transform-imports for lodash * fix(NA): dont use preventFullImport Co-authored-by: Elastic Machine --- packages/kbn-babel-preset/package.json | 3 ++- packages/kbn-babel-preset/webpack_preset.js | 19 +++++++++++++++++++ yarn.lock | 12 ++++++++++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/kbn-babel-preset/package.json b/packages/kbn-babel-preset/package.json index b82c8d0fac897..1a2f6941c2020 100644 --- a/packages/kbn-babel-preset/package.json +++ b/packages/kbn-babel-preset/package.json @@ -15,6 +15,7 @@ "babel-plugin-add-module-exports": "^1.0.2", "babel-plugin-filter-imports": "^3.0.0", "babel-plugin-styled-components": "^1.10.6", - "babel-plugin-transform-define": "^1.3.1" + "babel-plugin-transform-define": "^1.3.1", + "babel-plugin-transform-imports": "^2.0.0" } } diff --git a/packages/kbn-babel-preset/webpack_preset.js b/packages/kbn-babel-preset/webpack_preset.js index d76a3e9714838..2c1129f275bfe 100644 --- a/packages/kbn-babel-preset/webpack_preset.js +++ b/packages/kbn-babel-preset/webpack_preset.js @@ -42,5 +42,24 @@ module.exports = () => { }, ], ], + // NOTE: we can enable this by default for everything as soon as we only have one instance + // of lodash across the entire project. For now we are just enabling it for siem + // as they are extensively using the lodash v4 + overrides: [ + { + test: [/x-pack[\/\\]legacy[\/\\]plugins[\/\\]siem[\/\\]public/], + plugins: [ + [ + require.resolve('babel-plugin-transform-imports'), + { + 'lodash/?(((\\w*)?/?)*)': { + transform: 'lodash/${1}/${member}', + preventFullImport: false, + }, + }, + ], + ], + }, + ], }; }; diff --git a/yarn.lock b/yarn.lock index 66385c29d29b8..a17102b301bb2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1165,7 +1165,7 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@babel/types@^7.9.5": +"@babel/types@^7.4", "@babel/types@^7.9.5": version "7.9.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444" integrity sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg== @@ -7125,6 +7125,14 @@ babel-plugin-transform-define@^1.3.1: lodash "^4.17.11" traverse "0.6.6" +babel-plugin-transform-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-imports/-/babel-plugin-transform-imports-2.0.0.tgz#9e5f49f751a9d34ba8f4bb988c7e48ed2419c6b6" + integrity sha512-65ewumYJ85QiXdcB/jmiU0y0jg6eL6CdnDqQAqQ8JMOKh1E52VPG3NJzbVKWcgovUR5GBH8IWpCXQ7I8Q3wjgw== + dependencies: + "@babel/types" "^7.4" + is-valid-path "^0.1.1" + babel-plugin-transform-inline-consecutive-adds@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-consecutive-adds/-/babel-plugin-transform-inline-consecutive-adds-0.4.3.tgz#323d47a3ea63a83a7ac3c811ae8e6941faf2b0d1" @@ -17579,7 +17587,7 @@ is-valid-glob@^1.0.0: resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= -is-valid-path@0.1.1: +is-valid-path@0.1.1, is-valid-path@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-valid-path/-/is-valid-path-0.1.1.tgz#110f9ff74c37f663e1ec7915eb451f2db93ac9df" integrity sha1-EQ+f90w39mPh7HkV60UfLbk6yd8= From f37185fd8f3de1d96a55e215a949c72ac93b6370 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 23 Apr 2020 18:00:19 -0700 Subject: [PATCH 22/35] [Metrics UI] Design Refresh: Inventory View, Episode 1 (#64026) * [Metrics UI] Design Refresh: Inventory View, Episode 1 * Removing unused i18n labels * Removing obsolete code removed in previous PR Co-authored-by: Elastic Machine --- .../infra/public/components/toolbar_panel.ts | 19 ++ .../components/dropdown_button.tsx | 53 ++++ .../filter_bar.tsx} | 16 +- .../inventory_view/components/layout.tsx | 97 ++++-- .../components/nodes_overview.tsx | 294 ++++++------------ .../save_views.tsx => saved_views.tsx} | 6 +- .../components/toolbars/toolbar.tsx | 13 +- .../components/toolbars/toolbar_wrapper.tsx | 47 +-- .../components/waffle/interval_label.tsx | 31 ++ .../components/waffle/legend.tsx | 2 +- .../components/waffle/legend_controls.tsx | 2 +- .../inventory_view/components/waffle/map.tsx | 7 - .../waffle/metric_control/index.tsx | 18 +- .../components/waffle/view_switcher.tsx | 6 +- .../waffle/waffle_accounts_controls.tsx | 56 ++-- .../waffle/waffle_group_by_controls.tsx | 35 ++- .../waffle/waffle_inventory_switcher.tsx | 48 ++- .../waffle/waffle_region_controls.tsx | 55 ++-- .../pages/metrics/inventory_view/index.tsx | 4 +- .../lib/create_inventory_metric_formatter.ts | 89 ++++++ .../metrics_explorer/components/toolbar.tsx | 6 +- .../infra/public/utils/is_displayable.test.ts | 65 ---- .../infra/public/utils/is_displayable.ts | 30 -- .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - 25 files changed, 493 insertions(+), 516 deletions(-) create mode 100644 x-pack/plugins/infra/public/components/toolbar_panel.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx rename x-pack/plugins/infra/public/pages/metrics/inventory_view/{toolbar.tsx => components/filter_bar.tsx} (57%) rename x-pack/plugins/infra/public/pages/metrics/inventory_view/components/{toolbars/save_views.tsx => saved_views.tsx} (68%) create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts delete mode 100644 x-pack/plugins/infra/public/utils/is_displayable.test.ts delete mode 100644 x-pack/plugins/infra/public/utils/is_displayable.ts diff --git a/x-pack/plugins/infra/public/components/toolbar_panel.ts b/x-pack/plugins/infra/public/components/toolbar_panel.ts new file mode 100644 index 0000000000000..65cde03ec98e7 --- /dev/null +++ b/x-pack/plugins/infra/public/components/toolbar_panel.ts @@ -0,0 +1,19 @@ +/* + * 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 { EuiPanel } from '@elastic/eui'; +import { euiStyled } from '../../../observability/public'; + +export const ToolbarPanel = euiStyled(EuiPanel).attrs(() => ({ + grow: false, + paddingSize: 'none', +}))` + border-top: none; + border-right: none; + border-left: none; + border-radius: 0; + padding: ${props => `12px ${props.theme.eui.paddingSizes.m}`}; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx new file mode 100644 index 0000000000000..f0bc404dc3797 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx @@ -0,0 +1,53 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import React, { ReactNode } from 'react'; +import { withTheme, EuiTheme } from '../../../../../../observability/public'; + +interface Props { + label: string; + onClick: () => void; + theme: EuiTheme; + children: ReactNode; +} + +export const DropdownButton = withTheme(({ onClick, label, theme, children }: Props) => { + return ( + + + {label} + + + + {children} + + + + ); +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx similarity index 57% rename from x-pack/plugins/infra/public/pages/metrics/inventory_view/toolbar.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx index d6a87a0197f5f..708d5f7d75907 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx @@ -7,17 +7,13 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import { Toolbar } from '../../../components/eui/toolbar'; -import { WaffleTimeControls } from './components/waffle/waffle_time_controls'; -import { WaffleInventorySwitcher } from './components/waffle/waffle_inventory_switcher'; -import { SearchBar } from './components/search_bar'; +import { WaffleTimeControls } from './waffle/waffle_time_controls'; +import { SearchBar } from './search_bar'; +import { ToolbarPanel } from '../../../../components/toolbar_panel'; -export const SnapshotToolbar = () => ( - +export const FilterBar = () => ( + - - - @@ -25,5 +21,5 @@ export const SnapshotToolbar = () => ( - + ); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index bc8be9862fe63..a71e43874b480 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -4,20 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { useInterval } from 'react-use'; -import { euiPaletteColorBlind } from '@elastic/eui'; -import { NodesOverview } from './nodes_overview'; -import { Toolbar } from './toolbars/toolbar'; +import { euiPaletteColorBlind, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { convertIntervalToString } from '../../../../utils/convert_interval_to_string'; +import { NodesOverview, calculateBoundsFromNodes } from './nodes_overview'; import { PageContent } from '../../../../components/page'; import { useSnapshot } from '../hooks/use_snaphot'; -import { useInventoryMeta } from '../hooks/use_inventory_meta'; import { useWaffleTimeContext } from '../hooks/use_waffle_time'; import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; import { useWaffleOptionsContext } from '../hooks/use_waffle_options'; import { useSourceContext } from '../../../../containers/source'; import { InfraFormatterType, InfraWaffleMapGradientLegend } from '../../../../lib/lib'; +import { euiStyled } from '../../../../../../observability/public'; +import { Toolbar } from './toolbars/toolbar'; +import { ViewSwitcher } from './waffle/view_switcher'; +import { SavedViews } from './saved_views'; +import { IntervalLabel } from './waffle/interval_label'; +import { Legend } from './waffle/legend'; +import { createInventoryMetricFormatter } from '../lib/create_inventory_metric_formatter'; const euiVisColorPalette = euiPaletteColorBlind(); @@ -34,7 +40,6 @@ export const Layout = () => { autoBounds, boundsOverride, } = useWaffleOptionsContext(); - const { accounts, regions } = useInventoryMeta(sourceId, nodeType); const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext(); const { filterQueryAsJson, applyFilterQuery } = useWaffleFiltersContext(); const { loading, nodes, reload, interval } = useSnapshot( @@ -72,25 +77,75 @@ export const Layout = () => { isAutoReloading ? 5000 : null ); + const intervalAsString = convertIntervalToString(interval); + const dataBounds = calculateBoundsFromNodes(nodes); + const bounds = autoBounds ? dataBounds : boundsOverride; + const formatter = useCallback(createInventoryMetricFormatter(options.metric), [options.metric]); + return ( <> - - + + + + + + + + + + + + + + + + + + + + + + + + ); }; + +const MainContainer = euiStyled.div` + position: relative; + flex: 1 1 auto; +`; + +const TopActionContainer = euiStyled.div` + padding: ${props => `12px ${props.theme.eui.paddingSizes.m}`}; +`; + +const BottomActionContainer = euiStyled.div` + background-color: ${props => props.theme.eui.euiPageBackgroundColor}; + padding: ${props => props.theme.eui.paddingSizes.m} ${props => + props.theme.eui.paddingSizes.m} ${props => props.theme.eui.paddingSizes.s}; + position: absolute; + left: 0; + bottom: 4px; + right: 0; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx index afbfd2a079253..966a327f40bc1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx @@ -4,31 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { get, max, min } from 'lodash'; -import React from 'react'; +import { max, min } from 'lodash'; +import React, { useCallback } from 'react'; +import { InventoryItemType } from '../../../../../common/inventory_models/types'; import { euiStyled } from '../../../../../../observability/public'; -import { - InfraFormatterType, - InfraWaffleMapBounds, - InfraWaffleMapOptions, -} from '../../../../lib/lib'; -import { createFormatter } from '../../../../utils/formatters'; +import { InfraWaffleMapBounds, InfraWaffleMapOptions, InfraFormatter } from '../../../../lib/lib'; import { NoData } from '../../../../components/empty_states'; import { InfraLoadingPanel } from '../../../../components/loading'; import { Map } from './waffle/map'; -import { ViewSwitcher } from './waffle/view_switcher'; import { TableView } from './table_view'; -import { - SnapshotNode, - SnapshotCustomMetricInputRT, -} from '../../../../../common/http_api/snapshot_api'; -import { convertIntervalToString } from '../../../../utils/convert_interval_to_string'; -import { InventoryItemType } from '../../../../../common/inventory_models/types'; -import { createFormatterForMetric } from '../../metrics_explorer/components/helpers/create_formatter_for_metric'; +import { SnapshotNode } from '../../../../../common/http_api/snapshot_api'; export interface KueryFilterQuery { kind: 'kuery'; @@ -43,74 +30,13 @@ interface Props { reload: () => void; onDrilldown: (filter: KueryFilterQuery) => void; currentTime: number; - onViewChange: (view: string) => void; view: string; boundsOverride: InfraWaffleMapBounds; autoBounds: boolean; - interval: string; -} - -interface MetricFormatter { - formatter: InfraFormatterType; - template: string; - bounds?: { min: number; max: number }; -} - -interface MetricFormatters { - [key: string]: MetricFormatter; + formatter: InfraFormatter; } -const METRIC_FORMATTERS: MetricFormatters = { - ['count']: { formatter: InfraFormatterType.number, template: '{{value}}' }, - ['cpu']: { - formatter: InfraFormatterType.percent, - template: '{{value}}', - }, - ['memory']: { - formatter: InfraFormatterType.percent, - template: '{{value}}', - }, - ['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, - ['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, - ['logRate']: { - formatter: InfraFormatterType.abbreviatedNumber, - template: '{{value}}/s', - }, - ['diskIOReadBytes']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}/s', - }, - ['diskIOWriteBytes']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}/s', - }, - ['s3BucketSize']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}', - }, - ['s3TotalRequests']: { - formatter: InfraFormatterType.abbreviatedNumber, - template: '{{value}}', - }, - ['s3NumberOfObjects']: { - formatter: InfraFormatterType.abbreviatedNumber, - template: '{{value}}', - }, - ['s3UploadBytes']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}', - }, - ['s3DownloadBytes']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}', - }, - ['sqsOldestMessage']: { - formatter: InfraFormatterType.number, - template: '{{value}} seconds', - }, -}; - -const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds => { +export const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds => { const maxValues = nodes.map(node => node.metric.max); const minValues = nodes.map(node => node.metric.value); // if there is only one value then we need to set the bottom range to zero for min @@ -122,141 +48,97 @@ const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds = return { min: min(minValues) || 0, max: max(maxValues) || 0 }; }; -export const NodesOverview = class extends React.Component { - public static displayName = 'Waffle'; - public render() { - const { - autoBounds, - boundsOverride, - loading, - nodes, - nodeType, - reload, - view, - currentTime, - options, - interval, - } = this.props; - if (loading) { - return ( - - ); - } else if (!loading && nodes && nodes.length === 0) { - return ( - { - reload(); - }} - testString="noMetricsDataPrompt" - /> - ); - } - const dataBounds = calculateBoundsFromNodes(nodes); - const bounds = autoBounds ? dataBounds : boundsOverride; - const intervalAsString = convertIntervalToString(interval); +export const NodesOverview = ({ + autoBounds, + boundsOverride, + loading, + nodes, + nodeType, + reload, + view, + currentTime, + options, + formatter, + onDrilldown, +}: Props) => { + const handleDrilldown = useCallback( + (filter: string) => { + onDrilldown({ + kind: 'kuery', + expression: filter, + }); + return; + }, + [onDrilldown] + ); + + const noData = !loading && nodes && nodes.length === 0; + if (loading) { + return ( + + ); + } else if (noData) { return ( - - - - - - - - -

- -

-
-
-
-
- {view === 'table' ? ( - - - - ) : ( - - - - )} -
+ { + reload(); + }} + testString="noMetricsDataPrompt" + /> ); } + const dataBounds = calculateBoundsFromNodes(nodes); + const bounds = autoBounds ? dataBounds : boundsOverride; - private handleViewChange = (view: string) => this.props.onViewChange(view); - - // TODO: Change this to a real implimentation using the tickFormatter from the prototype as an example. - private formatter = (val: string | number) => { - const { metric } = this.props.options; - if (SnapshotCustomMetricInputRT.is(metric)) { - const formatter = createFormatterForMetric(metric); - return formatter(val); - } - const metricFormatter = get(METRIC_FORMATTERS, metric.type, METRIC_FORMATTERS.count); - if (val == null) { - return ''; - } - const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template); - return formatter(val); - }; - - private handleDrilldown = (filter: string) => { - this.props.onDrilldown({ - kind: 'kuery', - expression: filter, - }); - return; - }; + if (view === 'table') { + return ( + + + + ); + } + return ( + + + + ); }; -const MainContainer = euiStyled.div` - position: relative; - flex: 1 1 auto; -`; - const TableContainer = euiStyled.div` padding: ${props => props.theme.eui.paddingSizes.l}; `; -const ViewSwitcherContainer = euiStyled.div` - padding: ${props => props.theme.eui.paddingSizes.l}; -`; - const MapContainer = euiStyled.div` position: absolute; display: flex; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/save_views.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx similarity index 68% rename from x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/save_views.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx index eb40ea595662a..356f0598e00d2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/save_views.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { SavedViewsToolbarControls } from '../../../../../components/saved_views/toolbar_control'; -import { inventoryViewSavedObjectType } from '../../../../../../common/saved_objects/inventory_view'; -import { useWaffleViewState } from '../../hooks/use_waffle_view_state'; +import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control'; +import { inventoryViewSavedObjectType } from '../../../../../common/saved_objects/inventory_view'; +import { useWaffleViewState } from '../hooks/use_waffle_view_state'; export const SavedViews = () => { const { viewState, defaultViewState, onViewChange } = useWaffleViewState(); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx index 3ac9c2c189628..e8485fb812586 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx @@ -6,6 +6,7 @@ import React, { FunctionComponent } from 'react'; import { EuiFlexItem } from '@elastic/eui'; +import { useSourceContext } from '../../../../../containers/source'; import { SnapshotMetricInput, SnapshotGroupBy, @@ -19,7 +20,7 @@ import { InfraGroupByOptions } from '../../../../../lib/lib'; import { IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; import { InventoryItemType } from '../../../../../../common/inventory_models/types'; import { WaffleOptionsState } from '../../hooks/use_waffle_options'; -import { SavedViews } from './save_views'; +import { useInventoryMeta } from '../../hooks/use_inventory_meta'; export interface ToolbarProps extends Omit { @@ -45,9 +46,6 @@ const wrapToolbarItems = ( <> - - - )} @@ -56,10 +54,11 @@ const wrapToolbarItems = ( interface Props { nodeType: InventoryItemType; - regions: string[]; - accounts: InventoryCloudAccount[]; } -export const Toolbar = ({ nodeType, accounts, regions }: Props) => { + +export const Toolbar = ({ nodeType }: Props) => { + const { sourceId } = useSourceContext(); + const { accounts, regions } = useInventoryMeta(sourceId, nodeType); const ToolbarItems = findToolbar(nodeType); return wrapToolbarItems(ToolbarItems, accounts, regions); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx index 86cc0d8ee62e0..ea53122984161 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx @@ -5,14 +5,14 @@ */ import React from 'react'; -import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SnapshotMetricType } from '../../../../../../common/inventory_models/types'; -import { Toolbar } from '../../../../../components/eui/toolbar'; -import { ToolbarProps } from './toolbar'; import { fieldToName } from '../../lib/field_to_display_name'; import { useSourceContext } from '../../../../../containers/source'; import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; +import { WaffleInventorySwitcher } from '../waffle/waffle_inventory_switcher'; +import { ToolbarProps } from './toolbar'; interface Props { children: (props: Omit) => React.ReactElement; @@ -36,26 +36,27 @@ export const ToolbarWrapper = (props: Props) => { } = useWaffleOptionsContext(); const { createDerivedIndexPattern } = useSourceContext(); return ( - - - {props.children({ - createDerivedIndexPattern, - changeMetric, - changeGroupBy, - changeAccount, - changeRegion, - changeCustomOptions, - customOptions, - groupBy, - metric, - nodeType, - region, - accountId, - customMetrics, - changeCustomMetrics, - })} - - + <> + + + + {props.children({ + createDerivedIndexPattern, + changeMetric, + changeGroupBy, + changeAccount, + changeRegion, + changeCustomOptions, + customOptions, + groupBy, + metric, + nodeType, + region, + accountId, + customMetrics, + changeCustomMetrics, + })} + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx new file mode 100644 index 0000000000000..dbbfb0f49c0e9 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx @@ -0,0 +1,31 @@ +/* + * 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 from 'react'; +import { EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface Props { + intervalAsString?: string; +} + +export const IntervalLabel = ({ intervalAsString }: Props) => { + if (!intervalAsString) { + return null; + } + + return ( + +

+ +

+
+ ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx index ccb4cc71924f4..ac699f96a75a6 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx @@ -53,7 +53,7 @@ export const Legend: React.FC = ({ dataBounds, legend, bounds, formatter const LegendContainer = euiStyled.div` position: absolute; - bottom: 10px; + bottom: 0px; left: 10px; right: 10px; `; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index 6ec21ad2e1b49..30447e5244241 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx @@ -40,7 +40,7 @@ export const LegendControls = ({ autoBounds, boundsOverride, onChange, dataBound const [draftBounds, setDraftBounds] = useState(autoBounds ? dataBounds : boundsOverride); // should come from bounds prop const buttonComponent = ( = ({ } })} - ); }} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx index 08d5b3e9e0670..f91e9a4034bc2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFilterButton, EuiFilterGroup, EuiPopover } from '@elastic/eui'; +import { EuiPopover } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import React, { useState, useCallback } from 'react'; import { IFieldType } from 'src/plugins/data/public'; import { @@ -21,6 +20,7 @@ import { ModeSwitcher } from './mode_switcher'; import { MetricsEditMode } from './metrics_edit_mode'; import { CustomMetricMode } from './types'; import { SnapshotMetricType } from '../../../../../../../common/inventory_models/types'; +import { DropdownButton } from '../../dropdown_button'; interface Props { options: Array<{ text: string; value: string }>; @@ -132,17 +132,13 @@ export const WaffleMetricControls = ({ } const button = ( - - - + + {currentLabel} + ); return ( - + <> - + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx index 78a2cad9ca7ee..76756637eb69e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx @@ -28,7 +28,7 @@ export const ViewSwitcher = ({ view, onChange }: Props) => { label: i18n.translate('xpack.infra.viewSwitcher.tableViewLabel', { defaultMessage: 'Table view', }), - iconType: 'editorUnorderedList', + iconType: 'visTable', }, ]; return ( @@ -37,9 +37,11 @@ export const ViewSwitcher = ({ view, onChange }: Props) => { defaultMessage: 'Switch between table and map view', })} options={buttons} - color="primary" + color="text" + buttonSize="m" idSelected={view} onChange={onChange} + isIconOnly /> ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx index a8b0cf21bce85..3e4ff1de8291d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx @@ -4,17 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiContextMenuPanelDescriptor, - EuiFilterButton, - EuiFilterGroup, - EuiPopover, - EuiContextMenu, -} from '@elastic/eui'; +import { EuiContextMenuPanelDescriptor, EuiPopover, EuiContextMenu } from '@elastic/eui'; import React, { useCallback, useState, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { InventoryCloudAccount } from '../../../../../../common/http_api/inventory_meta_api'; +import { DropdownButton } from '../dropdown_button'; interface Props { accountId: string; @@ -63,32 +57,26 @@ export const WaffleAccountsControls = (props: Props) => { [options, accountId, changeAccount] ); + const button = ( + + {currentLabel + ? currentLabel.name + : i18n.translate('xpack.infra.waffle.accountAllTitle', { + defaultMessage: 'All', + })} + + ); + return ( - - - - - } - anchorPosition="downLeft" - panelPaddingSize="none" - closePopover={closePopover} - > - - - + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx index bc763d2cf9378..c1f406f31e85e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx @@ -9,8 +9,6 @@ import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor, - EuiFilterButton, - EuiFilterGroup, EuiPopover, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -22,6 +20,7 @@ import { CustomFieldPanel } from './custom_field_panel'; import { euiStyled } from '../../../../../../../observability/public'; import { InventoryItemType } from '../../../../../../common/inventory_models/types'; import { SnapshotGroupBy } from '../../../../../../common/http_api/snapshot_api'; +import { DropdownButton } from '../dropdown_button'; interface Props { options: Array<{ text: string; field: string; toolTipContent?: string }>; @@ -121,29 +120,31 @@ export const WaffleGroupByControls = class extends React.PureComponent o != null) // In this map the `o && o.field` is totally unnecessary but Typescript is // too stupid to realize that the filter above prevents the next map from being null - .map(o => {o && o.text}) + .map(o => ( + + {o && o.text} + + )) ) : ( ); + const button = ( - - + {buttonBody} - + ); return ( - - - - - + + + ); } diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx index 23e06823f407f..e534c97eda090 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx @@ -4,19 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiPopover, - EuiContextMenu, - EuiFilterButton, - EuiFilterGroup, - EuiContextMenuPanelDescriptor, -} from '@elastic/eui'; +import { EuiPopover, EuiContextMenu, EuiContextMenuPanelDescriptor } from '@elastic/eui'; import React, { useCallback, useState, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; import { findInventoryModel } from '../../../../../../common/inventory_models'; import { InventoryItemType } from '../../../../../../common/inventory_models/types'; import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; +import { DropdownButton } from '../dropdown_button'; const getDisplayNameForType = (type: InventoryItemType) => { const inventoryModel = findInventoryModel(type); @@ -120,27 +114,23 @@ export const WaffleInventorySwitcher: React.FC = () => { return getDisplayNameForType(nodeType); }, [nodeType]); + const button = ( + + {selectedText} + + ); + return ( - - - - - } - isOpen={isOpen} - closePopover={closePopover} - panelPaddingSize="none" - withTitle - anchorPosition="downLeft" - > - - - + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx index 671e44f42ef6a..9d759424cdc93 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx @@ -4,16 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiContextMenuPanelDescriptor, - EuiFilterButton, - EuiFilterGroup, - EuiPopover, - EuiContextMenu, -} from '@elastic/eui'; +import { EuiContextMenuPanelDescriptor, EuiPopover, EuiContextMenu } from '@elastic/eui'; import React, { useCallback, useState, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { DropdownButton } from '../dropdown_button'; interface Props { region?: string; @@ -62,32 +56,25 @@ export const WaffleRegionControls = (props: Props) => { [changeRegion, options, region] ); + const button = ( + + {currentLabel || + i18n.translate('xpack.infra.waffle.region', { + defaultMessage: 'All', + })} + + ); + return ( - - - - - } - anchorPosition="downLeft" - panelPaddingSize="none" - closePopover={closePopover} - > - - - + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx index e473aea7a1f0b..3a2c33d1c824c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx @@ -8,7 +8,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; -import { SnapshotToolbar } from './toolbar'; +import { FilterBar } from './components/filter_bar'; import { DocumentTitle } from '../../../components/document_title'; import { NoIndices } from '../../../components/empty_states/no_indices'; @@ -56,7 +56,7 @@ export const SnapshotPage = () => { ) : metricIndicesExist ? ( <> - + ) : hasFailedLoadingSource ? ( diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts new file mode 100644 index 0000000000000..acd71e5137694 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts @@ -0,0 +1,89 @@ +/* + * 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 { get } from 'lodash'; +import { createFormatter } from '../../../../utils/formatters'; +import { InfraFormatterType } from '../../../../lib/lib'; +import { + SnapshotMetricInput, + SnapshotCustomMetricInputRT, +} from '../../../../../common/http_api/snapshot_api'; +import { createFormatterForMetric } from '../../metrics_explorer/components/helpers/create_formatter_for_metric'; + +interface MetricFormatter { + formatter: InfraFormatterType; + template: string; + bounds?: { min: number; max: number }; +} + +interface MetricFormatters { + [key: string]: MetricFormatter; +} + +const METRIC_FORMATTERS: MetricFormatters = { + ['count']: { formatter: InfraFormatterType.number, template: '{{value}}' }, + ['cpu']: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + }, + ['memory']: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + }, + ['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['logRate']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}/s', + }, + ['diskIOReadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + ['diskIOWriteBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + ['s3BucketSize']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['s3TotalRequests']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + ['s3NumberOfObjects']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + ['s3UploadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['s3DownloadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['sqsOldestMessage']: { + formatter: InfraFormatterType.number, + template: '{{value}} seconds', + }, +}; + +export const createInventoryMetricFormatter = (metric: SnapshotMetricInput) => ( + val: string | number +) => { + if (SnapshotCustomMetricInputRT.is(metric)) { + const formatter = createFormatterForMetric(metric); + return formatter(val); + } + const metricFormatter = get(METRIC_FORMATTERS, metric.type, METRIC_FORMATTERS.count); + if (val == null) { + return ''; + } + const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template); + return formatter(val); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx index 81971bd31a973..6913f67bad08a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx @@ -17,7 +17,6 @@ import { MetricsExplorerTimeOptions, MetricsExplorerChartOptions, } from '../hooks/use_metrics_explorer_options'; -import { Toolbar } from '../../../../components/eui/toolbar'; import { MetricsExplorerKueryBar } from './kuery_bar'; import { MetricsExplorerMetrics } from './metrics'; import { MetricsExplorerGroupBy } from './group_by'; @@ -28,6 +27,7 @@ import { MetricExplorerViewState } from '../hooks/use_metric_explorer_state'; import { metricsExplorerViewSavedObjectType } from '../../../../../common/saved_objects/metrics_explorer_view'; import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting'; import { mapKibanaQuickRangesToDatePickerRanges } from '../../../../utils/map_timepicker_quickranges_to_datepicker_ranges'; +import { ToolbarPanel } from '../../../../components/toolbar_panel'; interface Props { derivedIndexPattern: IIndexPattern; @@ -65,7 +65,7 @@ export const MetricsExplorerToolbar = ({ const commonlyUsedRanges = mapKibanaQuickRangesToDatePickerRanges(timepickerQuickRanges); return ( - + - + ); }; diff --git a/x-pack/plugins/infra/public/utils/is_displayable.test.ts b/x-pack/plugins/infra/public/utils/is_displayable.test.ts deleted file mode 100644 index ebd5c07327e9b..0000000000000 --- a/x-pack/plugins/infra/public/utils/is_displayable.test.ts +++ /dev/null @@ -1,65 +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 { isDisplayable } from './is_displayable'; - -describe('isDisplayable()', () => { - test('field that is not displayable', () => { - const field = { - name: 'some.field', - type: 'number', - displayable: false, - }; - expect(isDisplayable(field)).toBe(false); - }); - test('field that is displayable', () => { - const field = { - name: 'some.field', - type: 'number', - displayable: true, - }; - expect(isDisplayable(field)).toBe(true); - }); - test('field that an ecs field', () => { - const field = { - name: '@timestamp', - type: 'date', - displayable: true, - }; - expect(isDisplayable(field)).toBe(true); - }); - test('field that matches same prefix', () => { - const field = { - name: 'system.network.name', - type: 'string', - displayable: true, - }; - expect(isDisplayable(field, ['system.network'])).toBe(true); - }); - test('field that does not matches same prefix', () => { - const field = { - name: 'system.load.1', - type: 'number', - displayable: true, - }; - expect(isDisplayable(field, ['system.network'])).toBe(false); - }); - test('field that is an K8s allowed field but does not match prefix', () => { - const field = { - name: 'kubernetes.namespace', - type: 'string', - displayable: true, - }; - expect(isDisplayable(field, ['kubernetes.pod'])).toBe(true); - }); - test('field that is a Prometheus allowed field but does not match prefix', () => { - const field = { - name: 'prometheus.labels.foo.bar', - type: 'string', - displayable: true, - }; - expect(isDisplayable(field, ['prometheus.metrics'])).toBe(true); - }); -}); diff --git a/x-pack/plugins/infra/public/utils/is_displayable.ts b/x-pack/plugins/infra/public/utils/is_displayable.ts deleted file mode 100644 index 534282e807036..0000000000000 --- a/x-pack/plugins/infra/public/utils/is_displayable.ts +++ /dev/null @@ -1,30 +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 { IFieldType } from 'src/plugins/data/public'; -import { startsWith, uniq } from 'lodash'; -import { getAllowedListForPrefix } from '../../common/ecs_allowed_list'; - -interface DisplayableFieldType extends IFieldType { - displayable?: boolean; -} - -const fieldStartsWith = (field: DisplayableFieldType) => (name: string) => - startsWith(field.name, name); - -export const isDisplayable = (field: DisplayableFieldType, additionalPrefixes: string[] = []) => { - // We need to start with at least one prefix, even if it's empty - const prefixes = additionalPrefixes && additionalPrefixes.length ? additionalPrefixes : ['']; - // Create a set of allowed list based on the prefixes - const allowedList = prefixes.reduce((acc, prefix) => { - return uniq([...acc, ...getAllowedListForPrefix(prefix)]); - }, [] as string[]); - // If the field is displayable and part of the allowed list or covered by the prefix - return ( - (field.displayable && prefixes.some(fieldStartsWith(field))) || - allowedList.some(fieldStartsWith(field)) - ); -}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1b77dfb168e88..3e681b8662e98 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8165,7 +8165,6 @@ "xpack.infra.viewSwitcher.mapViewLabel": "マップビュー", "xpack.infra.viewSwitcher.tableViewLabel": "表ビュー", "xpack.infra.waffle.accountAllTitle": "すべて", - "xpack.infra.waffle.accountLabel": "アカウント: {selectedAccount}", "xpack.infra.waffle.aggregationNames.avg": "{field} の平均", "xpack.infra.waffle.aggregationNames.max": "{field} の最大値", "xpack.infra.waffle.aggregationNames.min": "{field} の最小値", @@ -8201,11 +8200,8 @@ "xpack.infra.waffle.customMetrics.modeSwitcher.saveButtonAriaLabel": "カスタムメトリックの変更を保存", "xpack.infra.waffle.customMetrics.submitLabel": "保存", "xpack.infra.waffle.groupByAllTitle": "すべて", - "xpack.infra.waffle.groupByButtonLabel": "グループ分けの条件: ", - "xpack.infra.waffle.inventoryButtonLabel": "ビュー: {selectedText}", "xpack.infra.waffle.loadingDataText": "データを読み込み中", "xpack.infra.waffle.maxGroupByTooltip": "一度に選択できるグループは 2 つのみです", - "xpack.infra.waffle.metricButtonLabel": "メトリック: {selectedMetric}", "xpack.infra.waffle.metricOptions.countText": "カウント", "xpack.infra.waffle.metricOptions.cpuUsageText": "CPU 使用状況", "xpack.infra.waffle.metricOptions.diskIOReadBytes": "ディスク読み取り", @@ -8232,7 +8228,6 @@ "xpack.infra.waffle.noDataDescription": "期間またはフィルターを調整してみてください。", "xpack.infra.waffle.noDataTitle": "表示するデータがありません。", "xpack.infra.waffle.region": "すべて", - "xpack.infra.waffle.regionLabel": "地域: {selectedRegion}", "xpack.infra.waffle.savedView.createHeader": "ビューを保存", "xpack.infra.waffle.savedViews.cancel": "キャンセル", "xpack.infra.waffle.savedViews.cancelButton": "キャンセル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ce2469f29b883..8d50821bd8a58 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8168,7 +8168,6 @@ "xpack.infra.viewSwitcher.mapViewLabel": "地图视图", "xpack.infra.viewSwitcher.tableViewLabel": "表视图", "xpack.infra.waffle.accountAllTitle": "全部", - "xpack.infra.waffle.accountLabel": "帐户:{selectedAccount}", "xpack.infra.waffle.aggregationNames.avg": "“{field}”的平均值", "xpack.infra.waffle.aggregationNames.max": "“{field}”的最大值", "xpack.infra.waffle.aggregationNames.min": "“{field}”的最小值", @@ -8204,11 +8203,8 @@ "xpack.infra.waffle.customMetrics.modeSwitcher.saveButtonAriaLabel": "保存定制指标的更改", "xpack.infra.waffle.customMetrics.submitLabel": "保存", "xpack.infra.waffle.groupByAllTitle": "全部", - "xpack.infra.waffle.groupByButtonLabel": "分组依据: ", - "xpack.infra.waffle.inventoryButtonLabel": "视图:{selectedText}", "xpack.infra.waffle.loadingDataText": "正在加载数据", "xpack.infra.waffle.maxGroupByTooltip": "一次只能选择两个分组", - "xpack.infra.waffle.metricButtonLabel": "指标:{selectedMetric}", "xpack.infra.waffle.metricOptions.countText": "计数", "xpack.infra.waffle.metricOptions.cpuUsageText": "CPU 使用", "xpack.infra.waffle.metricOptions.diskIOReadBytes": "磁盘读取", @@ -8235,7 +8231,6 @@ "xpack.infra.waffle.noDataDescription": "尝试调整您的时间或筛选。", "xpack.infra.waffle.noDataTitle": "没有可显示的数据。", "xpack.infra.waffle.region": "全部", - "xpack.infra.waffle.regionLabel": "地区:{selectedRegion}", "xpack.infra.waffle.savedView.createHeader": "保存视图", "xpack.infra.waffle.savedViews.cancel": "取消", "xpack.infra.waffle.savedViews.cancelButton": "取消", From 59a946394d88db9b19817b41e05743d570f3a943 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Thu, 23 Apr 2020 23:09:17 -0400 Subject: [PATCH 23/35] [Alerting] change server log action type .log to .server-log in README (#64124) The README for actions referenced the server log action type as having an id of `.log`, but it's actually `.server-log`. --- x-pack/plugins/actions/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 82cc09f5e9eca..d6c85606edc2c 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -263,7 +263,7 @@ Kibana ships with a set of built-in action types: | Type | Id | Description | | ------------------------- | ------------- | ------------------------------------------------------------------ | -| [Server log](#server-log) | `.log` | Logs messages to the Kibana log using `server.log()` | +| [Server log](#server-log) | `.server-log` | Logs messages to the Kibana log using Kibana's logger | | [Email](#email) | `.email` | Sends an email using SMTP | | [Slack](#slack) | `.slack` | Posts a message to a slack channel | | [Index](#index) | `.index` | Indexes document(s) into Elasticsearch | From 222fba5ddc87483814c8ccf5f08d71ef80e2b0c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Fri, 24 Apr 2020 09:01:51 +0200 Subject: [PATCH 24/35] [ML] Changes transforms wizard UI text (#64150) Co-Authored-By: Walter Rafelsberger --- .../components/step_details/step_details_form.tsx | 13 ++++--------- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index 9a39616fb0989..23f482b5bc76a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -240,7 +240,6 @@ export const StepDetailsForm: FC = React.memo( ]} > setTransformId(e.target.value)} aria-label={i18n.translate( @@ -257,15 +256,12 @@ export const StepDetailsForm: FC = React.memo( label={i18n.translate('xpack.transform.stepDetailsForm.transformDescriptionLabel', { defaultMessage: 'Transform description', })} - helpText={i18n.translate( - 'xpack.transform.stepDetailsForm.transformDescriptionHelpText', - { - defaultMessage: 'Optional descriptive text.', - } - )} > setTransformDescription(e.target.value)} aria-label={i18n.translate( @@ -310,7 +306,6 @@ export const StepDetailsForm: FC = React.memo( } > setDestinationIndex(e.target.value)} aria-label={i18n.translate( diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3e681b8662e98..deb3053d28658 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15668,7 +15668,6 @@ "xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました:", "xpack.transform.stepDetailsForm.errorGettingTransformList": "既存の変換 ID の取得中にエラーが発生しました:", "xpack.transform.stepDetailsForm.indexPatternTitleError": "このタイトルのインデックスパターンが既に存在します。", - "xpack.transform.stepDetailsForm.transformDescriptionHelpText": "オプションの説明テキストです。", "xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel": "オプションの変換の説明を選択してください。", "xpack.transform.stepDetailsForm.transformDescriptionLabel": "変換の説明", "xpack.transform.stepDetailsForm.transformIdExistsError": "この ID の変換が既に存在します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8d50821bd8a58..3b757f169828c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15672,7 +15672,6 @@ "xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:", "xpack.transform.stepDetailsForm.errorGettingTransformList": "获取现有转换 ID 时发生错误:", "xpack.transform.stepDetailsForm.indexPatternTitleError": "具有此名称的索引模式已存在。", - "xpack.transform.stepDetailsForm.transformDescriptionHelpText": "(可选)描述性文本。", "xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel": "选择可选的转换描述。", "xpack.transform.stepDetailsForm.transformDescriptionLabel": "转换描述", "xpack.transform.stepDetailsForm.transformIdExistsError": "已存在具有此 ID 的转换。", From d53997742baf2b6c3e98ae48717f4be9d6cc4b96 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Fri, 24 Apr 2020 10:32:34 +0300 Subject: [PATCH 25/35] [FieldFormats] Cleanup: rename IFieldFormatType -> FieldFormatInstanceType (#64193) Co-authored-by: Elastic Machine --- ...lugins-data-public.baseformatterspublic.md | 2 +- ...plugin-plugins-data-server.plugin.setup.md | 4 +- .../constants/base_formatters.ts | 4 +- .../common/field_formats/converters/custom.ts | 4 +- .../data/common/field_formats/field_format.ts | 4 +- .../field_formats_registry.test.ts | 10 ++--- .../field_formats/field_formats_registry.ts | 40 ++++++++++--------- .../data/common/field_formats/index.ts | 2 +- .../data/common/field_formats/types.ts | 6 +-- src/plugins/data/public/public.api.md | 2 +- .../field_formats/field_formats_service.ts | 10 +++-- src/plugins/data/server/server.api.md | 2 +- 12 files changed, 48 insertions(+), 42 deletions(-) diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md index 50e8f2409ac02..ddbf1a8459d1f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md @@ -7,5 +7,5 @@ Signature: ```typescript -baseFormattersPublic: (import("../../common").IFieldFormatType | typeof DateFormat)[] +baseFormattersPublic: (import("../../common").FieldFormatInstanceType | typeof DateFormat)[] ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md index 5c2f542204079..4d7a0b3cfbbca 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md @@ -9,7 +9,7 @@ ```typescript setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { fieldFormats: { - register: (customFieldFormat: import("../common").IFieldFormatType) => number; + register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; }; search: ISearchSetup; }; @@ -26,7 +26,7 @@ setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { `{ fieldFormats: { - register: (customFieldFormat: import("../common").IFieldFormatType) => number; + register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; }; search: ISearchSetup; }` diff --git a/src/plugins/data/common/field_formats/constants/base_formatters.ts b/src/plugins/data/common/field_formats/constants/base_formatters.ts index 6befe8cea71f5..921c50571f727 100644 --- a/src/plugins/data/common/field_formats/constants/base_formatters.ts +++ b/src/plugins/data/common/field_formats/constants/base_formatters.ts @@ -17,7 +17,7 @@ * under the License. */ -import { IFieldFormatType } from '../types'; +import { FieldFormatInstanceType } from '../types'; import { BoolFormat, @@ -36,7 +36,7 @@ import { UrlFormat, } from '../converters'; -export const baseFormatters: IFieldFormatType[] = [ +export const baseFormatters: FieldFormatInstanceType[] = [ BoolFormat, BytesFormat, ColorFormat, diff --git a/src/plugins/data/common/field_formats/converters/custom.ts b/src/plugins/data/common/field_formats/converters/custom.ts index a1ce0cf3e7b54..4dd011a7feff3 100644 --- a/src/plugins/data/common/field_formats/converters/custom.ts +++ b/src/plugins/data/common/field_formats/converters/custom.ts @@ -18,9 +18,9 @@ */ import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert, FIELD_FORMAT_IDS, IFieldFormatType } from '../types'; +import { TextContextTypeConvert, FIELD_FORMAT_IDS, FieldFormatInstanceType } from '../types'; -export const createCustomFieldFormat = (convert: TextContextTypeConvert): IFieldFormatType => +export const createCustomFieldFormat = (convert: TextContextTypeConvert): FieldFormatInstanceType => class CustomFieldFormat extends FieldFormat { static id = FIELD_FORMAT_IDS.CUSTOM; diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index 49baa8c074da8..96d0024dff2a2 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -22,7 +22,7 @@ import { createCustomFieldFormat } from './converters/custom'; import { FieldFormatsGetConfigFn, FieldFormatsContentType, - IFieldFormatType, + FieldFormatInstanceType, FieldFormatConvert, FieldFormatConvertFunction, HtmlContextTypeOptions, @@ -199,7 +199,7 @@ export abstract class FieldFormat { }; } - static from(convertFn: FieldFormatConvertFunction): IFieldFormatType { + static from(convertFn: FieldFormatConvertFunction): FieldFormatInstanceType { return createCustomFieldFormat(convertFn); } diff --git a/src/plugins/data/common/field_formats/field_formats_registry.test.ts b/src/plugins/data/common/field_formats/field_formats_registry.test.ts index 0b32a62744fb1..f04524505a711 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.test.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.test.ts @@ -18,7 +18,7 @@ */ import { FieldFormatsRegistry } from './field_formats_registry'; import { BoolFormat, PercentFormat, StringFormat } from './converters'; -import { FieldFormatsGetConfigFn, IFieldFormatType } from './types'; +import { FieldFormatsGetConfigFn, FieldFormatInstanceType } from './types'; import { KBN_FIELD_TYPES } from '../../common'; const getValueOfPrivateField = (instance: any, field: string) => instance[field]; @@ -75,10 +75,10 @@ describe('FieldFormatsRegistry', () => { test('should register field formats', () => { fieldFormatsRegistry.register([StringFormat, BoolFormat]); - const registeredFieldFormatters: Map = getValueOfPrivateField( - fieldFormatsRegistry, - 'fieldFormats' - ); + const registeredFieldFormatters: Map< + string, + FieldFormatInstanceType + > = getValueOfPrivateField(fieldFormatsRegistry, 'fieldFormats'); expect(registeredFieldFormatters.size).toBe(2); diff --git a/src/plugins/data/common/field_formats/field_formats_registry.ts b/src/plugins/data/common/field_formats/field_formats_registry.ts index 15b1687e22312..b0a57ad6912a7 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.ts @@ -24,7 +24,7 @@ import { FieldFormatsGetConfigFn, FieldFormatConfig, FIELD_FORMAT_IDS, - IFieldFormatType, + FieldFormatInstanceType, FieldFormatId, IFieldFormatMetaParams, IFieldFormat, @@ -35,7 +35,7 @@ import { SerializedFieldFormat } from '../../../expressions/common/types'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../types'; export class FieldFormatsRegistry { - protected fieldFormats: Map = new Map(); + protected fieldFormats: Map = new Map(); protected defaultMap: Record = {}; protected metaParamsOptions: Record = {}; protected getConfig?: FieldFormatsGetConfigFn; @@ -47,7 +47,7 @@ export class FieldFormatsRegistry { init( getConfig: FieldFormatsGetConfigFn, metaParamsOptions: Record = {}, - defaultFieldConverters: IFieldFormatType[] = baseFormatters + defaultFieldConverters: FieldFormatInstanceType[] = baseFormatters ) { const defaultTypeMap = getConfig('format:defaultTypeMap'); this.register(defaultFieldConverters); @@ -79,23 +79,23 @@ export class FieldFormatsRegistry { * Get a derived FieldFormat class by its id. * * @param {FieldFormatId} formatId - the format id - * @return {IFieldFormatType | undefined} + * @return {FieldFormatInstanceType | undefined} */ - getType = (formatId: FieldFormatId): IFieldFormatType | undefined => { + getType = (formatId: FieldFormatId): FieldFormatInstanceType | undefined => { const fieldFormat = this.fieldFormats.get(formatId); if (fieldFormat) { const decoratedFieldFormat: any = this.fieldFormatMetaParamsDecorator(fieldFormat); if (decoratedFieldFormat) { - return decoratedFieldFormat as IFieldFormatType; + return decoratedFieldFormat as FieldFormatInstanceType; } } return undefined; }; - getTypeWithoutMetaParams = (formatId: FieldFormatId): IFieldFormatType | undefined => { + getTypeWithoutMetaParams = (formatId: FieldFormatId): FieldFormatInstanceType | undefined => { return this.fieldFormats.get(formatId); }; @@ -106,12 +106,12 @@ export class FieldFormatsRegistry { * * @param {KBN_FIELD_TYPES} fieldType * @param {ES_FIELD_TYPES[]} esTypes - Array of ES data types - * @return {IFieldFormatType | undefined} + * @return {FieldFormatInstanceType | undefined} */ getDefaultType = ( fieldType: KBN_FIELD_TYPES, esTypes: ES_FIELD_TYPES[] - ): IFieldFormatType | undefined => { + ): FieldFormatInstanceType | undefined => { const config = this.getDefaultConfig(fieldType, esTypes); return this.getType(config.id); @@ -206,14 +206,16 @@ export class FieldFormatsRegistry { * Get filtered list of field formats by format type * * @param {KBN_FIELD_TYPES} fieldType - * @return {IFieldFormatType[]} + * @return {FieldFormatInstanceType[]} */ - getByFieldType(fieldType: KBN_FIELD_TYPES): IFieldFormatType[] { + getByFieldType(fieldType: KBN_FIELD_TYPES): FieldFormatInstanceType[] { return [...this.fieldFormats.values()] - .filter((format: IFieldFormatType) => format && format.fieldType.indexOf(fieldType) !== -1) + .filter( + (format: FieldFormatInstanceType) => format && format.fieldType.indexOf(fieldType) !== -1 + ) .map( - (format: IFieldFormatType) => - this.fieldFormatMetaParamsDecorator(format) as IFieldFormatType + (format: FieldFormatInstanceType) => + this.fieldFormatMetaParamsDecorator(format) as FieldFormatInstanceType ); } @@ -238,7 +240,7 @@ export class FieldFormatsRegistry { }); } - register(fieldFormats: IFieldFormatType[]) { + register(fieldFormats: FieldFormatInstanceType[]) { fieldFormats.forEach(fieldFormat => this.fieldFormats.set(fieldFormat.id, fieldFormat)); } @@ -246,12 +248,12 @@ export class FieldFormatsRegistry { * FieldFormat decorator - provide a one way to add meta-params for all field formatters * * @private - * @param {IFieldFormatType} fieldFormat - field format type - * @return {IFieldFormatType | undefined} + * @param {FieldFormatInstanceType} fieldFormat - field format type + * @return {FieldFormatInstanceType | undefined} */ private fieldFormatMetaParamsDecorator = ( - fieldFormat: IFieldFormatType - ): IFieldFormatType | undefined => { + fieldFormat: FieldFormatInstanceType + ): FieldFormatInstanceType | undefined => { const getMetaParams = (customParams: Record) => this.buildMetaParams(customParams); if (fieldFormat) { diff --git a/src/plugins/data/common/field_formats/index.ts b/src/plugins/data/common/field_formats/index.ts index 13d3d9d73d43a..b64e115fd55ff 100644 --- a/src/plugins/data/common/field_formats/index.ts +++ b/src/plugins/data/common/field_formats/index.ts @@ -52,6 +52,6 @@ export { FieldFormatConfig, FieldFormatId, // Used in data plugin only - IFieldFormatType, + FieldFormatInstanceType, IFieldFormat, } from './types'; diff --git a/src/plugins/data/common/field_formats/types.ts b/src/plugins/data/common/field_formats/types.ts index 7c1d6a8522e52..5f11c7fe094bc 100644 --- a/src/plugins/data/common/field_formats/types.ts +++ b/src/plugins/data/common/field_formats/types.ts @@ -16,9 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - import { FieldFormat } from './field_format'; -export { FieldFormat }; /** @public **/ export type FieldFormatsContentType = 'html' | 'text'; @@ -82,10 +80,12 @@ export type IFieldFormat = PublicMethodsOf; */ export type FieldFormatId = FIELD_FORMAT_IDS | string; -export type IFieldFormatType = (new ( +/** @internal **/ +export type FieldFormatInstanceType = (new ( params?: any, getConfig?: FieldFormatsGetConfigFn ) => FieldFormat) & { + // Static properties: id: FieldFormatId; title: string; fieldType: string | string[]; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 3478d5b3e2328..f5177df022ff2 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -138,7 +138,7 @@ export class AggTypeFilters { // Warning: (ae-missing-release-tag) "baseFormattersPublic" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const baseFormattersPublic: (import("../../common").IFieldFormatType | typeof DateFormat)[]; +export const baseFormattersPublic: (import("../../common").FieldFormatInstanceType | typeof DateFormat)[]; // Warning: (ae-missing-release-tag) "BUCKET_TYPES" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/src/plugins/data/server/field_formats/field_formats_service.ts b/src/plugins/data/server/field_formats/field_formats_service.ts index 0dac64fb5dc1d..3404fe8cee9fd 100644 --- a/src/plugins/data/server/field_formats/field_formats_service.ts +++ b/src/plugins/data/server/field_formats/field_formats_service.ts @@ -17,16 +17,20 @@ * under the License. */ import { has } from 'lodash'; -import { FieldFormatsRegistry, IFieldFormatType, baseFormatters } from '../../common/field_formats'; +import { + FieldFormatsRegistry, + FieldFormatInstanceType, + baseFormatters, +} from '../../common/field_formats'; import { IUiSettingsClient } from '../../../../core/server'; import { DateFormat } from './converters'; export class FieldFormatsService { - private readonly fieldFormatClasses: IFieldFormatType[] = [DateFormat, ...baseFormatters]; + private readonly fieldFormatClasses: FieldFormatInstanceType[] = [DateFormat, ...baseFormatters]; public setup() { return { - register: (customFieldFormat: IFieldFormatType) => + register: (customFieldFormat: FieldFormatInstanceType) => this.fieldFormatClasses.push(customFieldFormat), }; } diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index f8a9a7792c492..9b673de60ca65 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -609,7 +609,7 @@ export class Plugin implements Plugin_2 { // (undocumented) setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { fieldFormats: { - register: (customFieldFormat: import("../common").IFieldFormatType) => number; + register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; }; search: ISearchSetup; }; From 321430ecad5c05bef10e3549dc3b97663cb657dd Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Fri, 24 Apr 2020 10:34:27 +0300 Subject: [PATCH 26/35] KQL removes leading zero and breaks query (#62748) * KQL removes leading zero and breaks query * correctly parse numbers started from '.' Co-authored-by: Elastic Machine --- .../es_query/kuery/ast/_generated_/kuery.js | 7 +++-- .../common/es_query/kuery/ast/ast.test.ts | 27 +++++++++++++++++++ .../data/common/es_query/kuery/ast/kuery.peg | 5 ++-- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js b/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js index f745f01873bae..fc6b706f6e01e 100644 --- a/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js +++ b/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js @@ -222,9 +222,8 @@ module.exports = (function() { if (sequence === 'true') return buildLiteralNode(true); if (sequence === 'false') return buildLiteralNode(false); if (chars.includes(wildcardSymbol)) return buildWildcardNode(sequence); - const number = Number(sequence); - const value = isNaN(number) ? sequence : number; - return buildLiteralNode(value); + const isNumberPattern = /^(-?[1-9]+\d*([.]\d+)?)$|^(-?0[.]\d*[1-9]+)$|^0$|^0.0$|^[.]\d{1,}$/ + return buildLiteralNode(isNumberPattern.test(sequence) ? Number(sequence) : sequence); }, peg$c50 = { type: "any", description: "any character" }, peg$c51 = "*", @@ -3164,4 +3163,4 @@ module.exports = (function() { SyntaxError: peg$SyntaxError, parse: peg$parse }; -})(); \ No newline at end of file +})(); diff --git a/src/plugins/data/common/es_query/kuery/ast/ast.test.ts b/src/plugins/data/common/es_query/kuery/ast/ast.test.ts index e441420760475..6a69d52d72134 100644 --- a/src/plugins/data/common/es_query/kuery/ast/ast.test.ts +++ b/src/plugins/data/common/es_query/kuery/ast/ast.test.ts @@ -278,6 +278,33 @@ describe('kuery AST API', () => { expect(fromLiteralExpression('true')).toEqual(booleanTrueLiteral); expect(fromLiteralExpression('false')).toEqual(booleanFalseLiteral); expect(fromLiteralExpression('42')).toEqual(numberLiteral); + + expect(fromLiteralExpression('.3').value).toEqual(0.3); + expect(fromLiteralExpression('.36').value).toEqual(0.36); + expect(fromLiteralExpression('.00001').value).toEqual(0.00001); + expect(fromLiteralExpression('3').value).toEqual(3); + expect(fromLiteralExpression('-4').value).toEqual(-4); + expect(fromLiteralExpression('0').value).toEqual(0); + expect(fromLiteralExpression('0.0').value).toEqual(0); + expect(fromLiteralExpression('2.0').value).toEqual(2.0); + expect(fromLiteralExpression('0.8').value).toEqual(0.8); + expect(fromLiteralExpression('790.9').value).toEqual(790.9); + expect(fromLiteralExpression('0.0001').value).toEqual(0.0001); + expect(fromLiteralExpression('96565646732345').value).toEqual(96565646732345); + + expect(fromLiteralExpression('..4').value).toEqual('..4'); + expect(fromLiteralExpression('.3text').value).toEqual('.3text'); + expect(fromLiteralExpression('text').value).toEqual('text'); + expect(fromLiteralExpression('.').value).toEqual('.'); + expect(fromLiteralExpression('-').value).toEqual('-'); + expect(fromLiteralExpression('001').value).toEqual('001'); + expect(fromLiteralExpression('00.2').value).toEqual('00.2'); + expect(fromLiteralExpression('0.0.1').value).toEqual('0.0.1'); + expect(fromLiteralExpression('3.').value).toEqual('3.'); + expect(fromLiteralExpression('--4').value).toEqual('--4'); + expect(fromLiteralExpression('-.4').value).toEqual('-.4'); + expect(fromLiteralExpression('-0').value).toEqual('-0'); + expect(fromLiteralExpression('00949').value).toEqual('00949'); }); test('should allow escaping of special characters with a backslash', () => { diff --git a/src/plugins/data/common/es_query/kuery/ast/kuery.peg b/src/plugins/data/common/es_query/kuery/ast/kuery.peg index 389b9a82d2c76..625c5069f936a 100644 --- a/src/plugins/data/common/es_query/kuery/ast/kuery.peg +++ b/src/plugins/data/common/es_query/kuery/ast/kuery.peg @@ -247,9 +247,8 @@ UnquotedLiteral if (sequence === 'true') return buildLiteralNode(true); if (sequence === 'false') return buildLiteralNode(false); if (chars.includes(wildcardSymbol)) return buildWildcardNode(sequence); - const number = Number(sequence); - const value = isNaN(number) ? sequence : number; - return buildLiteralNode(value); + const isNumberPattern = /^(-?[1-9]+\d*([.]\d+)?)$|^(-?0[.]\d*[1-9]+)$|^0$|^0.0$|^[.]\d{1,}$/ + return buildLiteralNode(isNumberPattern.test(sequence) ? Number(sequence) : sequence); } UnquotedCharacter From 6d25d78f1a443a364d6a5a6d44701442d39eb3a3 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 24 Apr 2020 10:20:35 +0200 Subject: [PATCH 27/35] remove reference to local application service in graph (#64288) --- x-pack/plugins/graph/public/_main.scss | 6 ++++++ x-pack/plugins/graph/public/application.ts | 6 +++--- x-pack/plugins/graph/public/index.scss | 2 -- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/graph/public/_main.scss b/x-pack/plugins/graph/public/_main.scss index 2559b7d1aba5c..0d17015385292 100644 --- a/x-pack/plugins/graph/public/_main.scss +++ b/x-pack/plugins/graph/public/_main.scss @@ -25,3 +25,9 @@ -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; } + +.gphAppWrapper { + display: flex; + flex-direction: column; + flex-grow: 1; +} diff --git a/x-pack/plugins/graph/public/application.ts b/x-pack/plugins/graph/public/application.ts index fee42bdbeaf3b..35ec0bb2bf6ce 100644 --- a/x-pack/plugins/graph/public/application.ts +++ b/x-pack/plugins/graph/public/application.ts @@ -96,7 +96,7 @@ export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies) }; }; -const mainTemplate = (basePath: string) => `
+const mainTemplate = (basePath: string) => `
`; @@ -107,14 +107,14 @@ const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react', 'ui.boo function mountGraphApp(appBasePath: string, element: HTMLElement) { const mountpoint = document.createElement('div'); - mountpoint.setAttribute('class', 'kbnLocalApplicationWrapper'); + mountpoint.setAttribute('class', 'gphAppWrapper'); // eslint-disable-next-line mountpoint.innerHTML = mainTemplate(appBasePath); // bootstrap angular into detached element and attach it later to // make angular-within-angular possible const $injector = angular.bootstrap(mountpoint, [moduleName]); element.appendChild(mountpoint); - element.setAttribute('class', 'kbnLocalApplicationWrapper'); + element.setAttribute('class', 'gphAppWrapper'); return $injector; } diff --git a/x-pack/plugins/graph/public/index.scss b/x-pack/plugins/graph/public/index.scss index 964ef320e4352..f4e38de3e93a4 100644 --- a/x-pack/plugins/graph/public/index.scss +++ b/x-pack/plugins/graph/public/index.scss @@ -12,5 +12,3 @@ @import './main'; @import './angular/templates/index'; @import './components/index'; -// Local application mount wrapper styles -@import 'src/legacy/core_plugins/kibana/public/local_application_service/index'; From 6bb751540b5a0d03c635abee2b5780add3dbbeab Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Fri, 24 Apr 2020 11:49:02 +0300 Subject: [PATCH 28/35] Move input_control_vis into NP (#63333) * Move input_control_vis into NP * Change config paths * Clean up --- .github/CODEOWNERS | 1 + .i18nrc.json | 2 +- .../core_plugins/input_control_vis/index.ts | 44 ------------------ .../input_control_vis/package.json | 4 -- .../input_control_vis/public/legacy.ts | 45 ------------------- src/plugins/input_control_vis/kibana.json | 8 ++++ .../input_control_fn.test.ts.snap | 0 .../__snapshots__/controls_tab.test.tsx.snap | 0 .../list_control_editor.test.tsx.snap | 0 .../__snapshots__/options_tab.test.tsx.snap | 0 .../range_control_editor.test.tsx.snap | 0 .../components/editor/_control_editor.scss | 0 .../public/components/editor/_index.scss | 0 .../components/editor/control_editor.tsx | 2 +- .../components/editor/controls_tab.test.tsx | 2 +- .../public/components/editor/controls_tab.tsx | 0 .../public/components/editor/field_select.tsx | 2 +- .../editor/index_pattern_select_form_row.tsx | 0 .../editor/list_control_editor.test.tsx | 0 .../components/editor/list_control_editor.tsx | 6 +-- .../components/editor/options_tab.test.tsx | 2 +- .../public/components/editor/options_tab.tsx | 0 .../editor/range_control_editor.test.tsx | 0 .../editor/range_control_editor.tsx | 6 +-- .../vis/__snapshots__/form_row.test.tsx.snap | 0 .../input_control_vis.test.tsx.snap | 0 .../__snapshots__/list_control.test.tsx.snap | 0 .../__snapshots__/range_control.test.tsx.snap | 0 .../public/components/vis/_index.scss | 0 .../public/components/vis/_vis.scss | 0 .../public/components/vis/form_row.test.tsx | 0 .../public/components/vis/form_row.tsx | 0 .../components/vis/input_control_vis.test.tsx | 2 - .../components/vis/input_control_vis.tsx | 4 +- .../components/vis/list_control.test.tsx | 0 .../public/components/vis/list_control.tsx | 0 .../components/vis/range_control.test.tsx | 2 - .../public/components/vis/range_control.tsx | 2 +- .../public/control/control.test.ts | 0 .../public/control/control.ts | 2 +- .../public/control/control_factory.ts | 0 .../public/control/create_search_source.ts | 2 +- .../filter_manager/filter_manager.test.ts | 8 +--- .../control/filter_manager/filter_manager.ts | 6 +-- .../phrase_filter_manager.test.ts | 6 +-- .../filter_manager/phrase_filter_manager.ts | 2 +- .../range_filter_manager.test.ts | 2 +- .../filter_manager/range_filter_manager.ts | 7 +-- .../control/list_control_factory.test.ts | 0 .../public/control/list_control_factory.ts | 12 ++--- .../control/range_control_factory.test.ts | 0 .../public/control/range_control_factory.ts | 6 +-- .../input_control_vis/public/editor_utils.ts | 0 .../input_control_vis/public/index.scss | 2 - .../input_control_vis/public/index.ts | 4 +- .../public/input_control_fn.test.ts | 4 +- .../public/input_control_fn.ts | 6 +-- .../public/input_control_vis_type.ts | 2 +- .../input_control_vis/public/lineage/index.ts | 0 .../public/lineage/lineage_map.test.ts | 0 .../public/lineage/lineage_map.ts | 0 .../public/lineage/parent_candidates.test.ts | 0 .../public/lineage/parent_candidates.ts | 0 .../input_control_vis/public/plugin.ts | 7 +-- .../public/test_utils/get_deps_mock.tsx | 0 .../test_utils/get_index_pattern_mock.ts | 0 .../test_utils/get_index_patterns_mock.ts | 0 .../test_utils/get_search_service_mock.ts | 0 .../public/test_utils/index.ts | 0 .../public/test_utils/update_component.ts | 0 .../public/vis_controller.tsx | 4 +- src/plugins/input_control_vis/server/index.ts | 30 +++++++++++++ 72 files changed, 75 insertions(+), 171 deletions(-) delete mode 100644 src/legacy/core_plugins/input_control_vis/index.ts delete mode 100644 src/legacy/core_plugins/input_control_vis/package.json delete mode 100644 src/legacy/core_plugins/input_control_vis/public/legacy.ts create mode 100644 src/plugins/input_control_vis/kibana.json rename src/{legacy/core_plugins => plugins}/input_control_vis/public/__snapshots__/input_control_fn.test.ts.snap (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/editor/__snapshots__/options_tab.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/editor/__snapshots__/range_control_editor.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/editor/_control_editor.scss (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/editor/_index.scss (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/editor/control_editor.tsx (98%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/editor/controls_tab.test.tsx (98%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/editor/controls_tab.tsx (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/editor/field_select.tsx (98%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/editor/list_control_editor.test.tsx (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/editor/list_control_editor.tsx (98%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/editor/options_tab.test.tsx (97%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/editor/options_tab.tsx (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/editor/range_control_editor.test.tsx (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/editor/range_control_editor.tsx (97%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/vis/_index.scss (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/vis/_vis.scss (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/vis/form_row.test.tsx (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/vis/form_row.tsx (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/vis/input_control_vis.test.tsx (99%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/vis/input_control_vis.tsx (97%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/vis/list_control.test.tsx (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/vis/list_control.tsx (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/vis/range_control.test.tsx (98%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/components/vis/range_control.tsx (97%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/control/control.test.ts (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/control/control.ts (98%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/control/control_factory.ts (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/control/create_search_source.ts (97%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/control/filter_manager/filter_manager.test.ts (93%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/control/filter_manager/filter_manager.ts (93%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts (98%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts (98%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts (98%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/control/filter_manager/range_filter_manager.ts (95%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/control/list_control_factory.test.ts (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/control/list_control_factory.ts (99%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/control/range_control_factory.test.ts (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/control/range_control_factory.ts (97%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/editor_utils.ts (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/index.scss (80%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/index.ts (91%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/input_control_fn.test.ts (92%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/input_control_fn.ts (93%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/input_control_vis_type.ts (96%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/lineage/index.ts (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/lineage/lineage_map.test.ts (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/lineage/lineage_map.ts (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/lineage/parent_candidates.test.ts (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/lineage/parent_candidates.ts (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/plugin.ts (92%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/test_utils/get_deps_mock.tsx (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/test_utils/get_index_pattern_mock.ts (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/test_utils/get_index_patterns_mock.ts (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/test_utils/get_search_service_mock.ts (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/test_utils/index.ts (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/test_utils/update_component.ts (100%) rename src/{legacy/core_plugins => plugins}/input_control_vis/public/vis_controller.tsx (98%) create mode 100644 src/plugins/input_control_vis/server/index.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c32bf8cbaa1c3..0d86726dca836 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -18,6 +18,7 @@ /src/plugins/vis_type_timelion/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app +/src/plugins/input_control_vis/ @elastic/kibana-app /src/plugins/visualize/ @elastic/kibana-app /src/plugins/vis_type_timeseries/ @elastic/kibana-app /src/plugins/vis_type_metric/ @elastic/kibana-app diff --git a/.i18nrc.json b/.i18nrc.json index dc1be7f5a140b..2ef92f544ad6b 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -14,7 +14,7 @@ "esUi": "src/plugins/es_ui_shared", "devTools": "src/plugins/dev_tools", "expressions": "src/plugins/expressions", - "inputControl": "src/legacy/core_plugins/input_control_vis", + "inputControl": "src/plugins/input_control_vis", "inspector": "src/plugins/inspector", "inspectorViews": "src/legacy/core_plugins/inspector_views", "interpreter": "src/legacy/core_plugins/interpreter", diff --git a/src/legacy/core_plugins/input_control_vis/index.ts b/src/legacy/core_plugins/input_control_vis/index.ts deleted file mode 100644 index 0529aa24dffd7..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/index.ts +++ /dev/null @@ -1,44 +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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const inputControlVisPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'input_control_vis', - require: ['kibana', 'elasticsearch', 'interpreter'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default inputControlVisPluginInitializer; diff --git a/src/legacy/core_plugins/input_control_vis/package.json b/src/legacy/core_plugins/input_control_vis/package.json deleted file mode 100644 index 0d52be412f2fd..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "input_control_vis", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/input_control_vis/public/legacy.ts b/src/legacy/core_plugins/input_control_vis/public/legacy.ts deleted file mode 100644 index 67299068819e8..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/legacy.ts +++ /dev/null @@ -1,45 +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 { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; - -import { plugin } from '.'; - -import { - InputControlVisPluginSetupDependencies, - InputControlVisPluginStartDependencies, -} from './plugin'; - -const setupPlugins: Readonly = { - expressions: npSetup.plugins.expressions, - data: npSetup.plugins.data, - visualizations: npSetup.plugins.visualizations, -}; - -const startPlugins: Readonly = { - expressions: npStart.plugins.expressions, - data: npStart.plugins.data, - visualizations: npStart.plugins.visualizations, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core, startPlugins); diff --git a/src/plugins/input_control_vis/kibana.json b/src/plugins/input_control_vis/kibana.json new file mode 100644 index 0000000000000..4a4ec328c1352 --- /dev/null +++ b/src/plugins/input_control_vis/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "inputControlVis", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["data", "expressions", "visualizations"] +} diff --git a/src/legacy/core_plugins/input_control_vis/public/__snapshots__/input_control_fn.test.ts.snap b/src/plugins/input_control_vis/public/__snapshots__/input_control_fn.test.ts.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/__snapshots__/input_control_fn.test.ts.snap rename to src/plugins/input_control_vis/public/__snapshots__/input_control_fn.test.ts.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap b/src/plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap rename to src/plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.tsx.snap b/src/plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.tsx.snap rename to src/plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/options_tab.test.tsx.snap b/src/plugins/input_control_vis/public/components/editor/__snapshots__/options_tab.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/options_tab.test.tsx.snap rename to src/plugins/input_control_vis/public/components/editor/__snapshots__/options_tab.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/range_control_editor.test.tsx.snap b/src/plugins/input_control_vis/public/components/editor/__snapshots__/range_control_editor.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/range_control_editor.test.tsx.snap rename to src/plugins/input_control_vis/public/components/editor/__snapshots__/range_control_editor.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/_control_editor.scss b/src/plugins/input_control_vis/public/components/editor/_control_editor.scss similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/_control_editor.scss rename to src/plugins/input_control_vis/public/components/editor/_control_editor.scss diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/_index.scss b/src/plugins/input_control_vis/public/components/editor/_index.scss similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/_index.scss rename to src/plugins/input_control_vis/public/components/editor/_index.scss diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.tsx b/src/plugins/input_control_vis/public/components/editor/control_editor.tsx similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.tsx rename to src/plugins/input_control_vis/public/components/editor/control_editor.tsx index 2bd0baea6eff8..90e875fd43432 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.tsx +++ b/src/plugins/input_control_vis/public/components/editor/control_editor.tsx @@ -34,7 +34,7 @@ import { import { RangeControlEditor } from './range_control_editor'; import { ListControlEditor } from './list_control_editor'; import { getTitle, ControlParams, CONTROL_TYPES, ControlParamsOptions } from '../../editor_utils'; -import { IIndexPattern } from '../../../../../../plugins/data/public'; +import { IIndexPattern } from '../../../../data/public'; import { InputControlVisDependencies } from '../../plugin'; interface ControlEditorUiProps { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx b/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx rename to src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx index db2af742c70bc..c66dbf968cac1 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx +++ b/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx @@ -23,7 +23,7 @@ import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { getDepsMock, getIndexPatternMock } from '../../test_utils'; import { ControlsTab, ControlsTabUiProps } from './controls_tab'; -import { Vis } from '../../../../../../plugins/visualizations/public'; +import { Vis } from '../../../../visualizations/public'; const indexPatternsMock = { get: getIndexPatternMock, diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx b/src/plugins/input_control_vis/public/components/editor/controls_tab.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx rename to src/plugins/input_control_vis/public/components/editor/controls_tab.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.tsx b/src/plugins/input_control_vis/public/components/editor/field_select.tsx similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.tsx rename to src/plugins/input_control_vis/public/components/editor/field_select.tsx index 68cca9bf6c4f2..779f3ad6b1a8a 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.tsx +++ b/src/plugins/input_control_vis/public/components/editor/field_select.tsx @@ -24,7 +24,7 @@ import { InjectedIntlProps } from 'react-intl'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { IIndexPattern, IFieldType } from '../../../../../../plugins/data/public'; +import { IIndexPattern, IFieldType } from '../../../../data/public'; interface FieldSelectUiState { isLoading: boolean; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx b/src/plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx rename to src/plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx b/src/plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx rename to src/plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.tsx b/src/plugins/input_control_vis/public/components/editor/list_control_editor.tsx similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.tsx rename to src/plugins/input_control_vis/public/components/editor/list_control_editor.tsx index 9772cb5fc2548..3da3ae9ba757c 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.tsx +++ b/src/plugins/input_control_vis/public/components/editor/list_control_editor.tsx @@ -25,11 +25,7 @@ import { EuiFormRow, EuiFieldNumber, EuiSwitch, EuiSelect } from '@elastic/eui'; import { IndexPatternSelectFormRow } from './index_pattern_select_form_row'; import { FieldSelect } from './field_select'; import { ControlParams, ControlParamsOptions } from '../../editor_utils'; -import { - IIndexPattern, - IFieldType, - IndexPatternSelect, -} from '../../../../../../plugins/data/public'; +import { IIndexPattern, IFieldType, IndexPatternSelect } from '../../../../data/public'; import { InputControlVisDependencies } from '../../plugin'; interface ListControlEditorState { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.tsx b/src/plugins/input_control_vis/public/components/editor/options_tab.test.tsx similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.tsx rename to src/plugins/input_control_vis/public/components/editor/options_tab.test.tsx index 639e3d2f68c75..c53d7abc4d659 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.tsx +++ b/src/plugins/input_control_vis/public/components/editor/options_tab.test.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { Vis } from '../../../../../../plugins/visualizations/public'; +import { Vis } from '../../../../visualizations/public'; import { OptionsTab, OptionsTabProps } from './options_tab'; describe('OptionsTab', () => { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx b/src/plugins/input_control_vis/public/components/editor/options_tab.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx rename to src/plugins/input_control_vis/public/components/editor/options_tab.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx b/src/plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx rename to src/plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx b/src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx rename to src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx index 97850879a2d38..b6b852bcfa707 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx +++ b/src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx @@ -24,11 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { IndexPatternSelectFormRow } from './index_pattern_select_form_row'; import { FieldSelect } from './field_select'; import { ControlParams, ControlParamsOptions } from '../../editor_utils'; -import { - IIndexPattern, - IFieldType, - IndexPatternSelect, -} from '../../../../../../plugins/data/public'; +import { IIndexPattern, IFieldType, IndexPatternSelect } from '../../../../data/public'; import { InputControlVisDependencies } from '../../plugin'; interface RangeControlEditorProps { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap rename to src/plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap rename to src/plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap rename to src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap rename to src/plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/_index.scss b/src/plugins/input_control_vis/public/components/vis/_index.scss similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/_index.scss rename to src/plugins/input_control_vis/public/components/vis/_index.scss diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/_vis.scss b/src/plugins/input_control_vis/public/components/vis/_vis.scss similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/_vis.scss rename to src/plugins/input_control_vis/public/components/vis/_vis.scss diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.test.tsx b/src/plugins/input_control_vis/public/components/vis/form_row.test.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.test.tsx rename to src/plugins/input_control_vis/public/components/vis/form_row.test.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.tsx b/src/plugins/input_control_vis/public/components/vis/form_row.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.tsx rename to src/plugins/input_control_vis/public/components/vis/form_row.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx b/src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx similarity index 99% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx rename to src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx index 1712f024f5b7b..b0b674ad7b6ee 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx +++ b/src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx @@ -28,8 +28,6 @@ import { InputControlVis } from './input_control_vis'; import { ListControl } from '../../control/list_control_factory'; import { RangeControl } from '../../control/range_control_factory'; -jest.mock('ui/new_platform'); - const mockListControl: ListControl = { id: 'mock-list-control', isEnabled: () => { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx b/src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx rename to src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx index e2497287f35d0..c0ef99664fdf8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx +++ b/src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx @@ -23,8 +23,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { CONTROL_TYPES } from '../../editor_utils'; import { ListControl } from '../../control/list_control_factory'; import { RangeControl } from '../../control/range_control_factory'; -import { ListControl as ListControlComponent } from '../vis/list_control'; -import { RangeControl as RangeControlComponent } from '../vis/range_control'; +import { ListControl as ListControlComponent } from './list_control'; +import { RangeControl as RangeControlComponent } from './range_control'; function isListControl(control: RangeControl | ListControl): control is ListControl { return control.type === CONTROL_TYPES.LIST; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.test.tsx b/src/plugins/input_control_vis/public/components/vis/list_control.test.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.test.tsx rename to src/plugins/input_control_vis/public/components/vis/list_control.test.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx b/src/plugins/input_control_vis/public/components/vis/list_control.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx rename to src/plugins/input_control_vis/public/components/vis/list_control.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx b/src/plugins/input_control_vis/public/components/vis/range_control.test.tsx similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx rename to src/plugins/input_control_vis/public/components/vis/range_control.test.tsx index 639616151a395..ff5d572fa21c4 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx +++ b/src/plugins/input_control_vis/public/components/vis/range_control.test.tsx @@ -23,8 +23,6 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { RangeControl, ceilWithPrecision, floorWithPrecision } from './range_control'; import { RangeControl as RangeControlClass } from '../../control/range_control_factory'; -jest.mock('ui/new_platform'); - const control: RangeControlClass = { id: 'mock-range-control', isEnabled: () => { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx b/src/plugins/input_control_vis/public/components/vis/range_control.tsx similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx rename to src/plugins/input_control_vis/public/components/vis/range_control.tsx index 0cd2a2b331980..f028feaf5f84f 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx +++ b/src/plugins/input_control_vis/public/components/vis/range_control.tsx @@ -19,7 +19,7 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; -import { ValidatedDualRange } from '../../../../../../../src/plugins/kibana_react/public'; +import { ValidatedDualRange } from '../../../../kibana_react/public'; import { FormRow } from './form_row'; import { RangeControl as RangeControlClass } from '../../control/range_control_factory'; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control.test.ts b/src/plugins/input_control_vis/public/control/control.test.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/control/control.test.ts rename to src/plugins/input_control_vis/public/control/control.test.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control.ts b/src/plugins/input_control_vis/public/control/control.ts similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/control/control.ts rename to src/plugins/input_control_vis/public/control/control.ts index 62e0090e466c0..c57b09a19ebc8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/control.ts +++ b/src/plugins/input_control_vis/public/control/control.ts @@ -22,7 +22,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Filter } from '../../../../../plugins/data/public'; +import { Filter } from 'src/plugins/data/public'; import { ControlParams, ControlParamsOptions, CONTROL_TYPES } from '../editor_utils'; import { RangeFilterManager } from './filter_manager/range_filter_manager'; import { PhraseFilterManager } from './filter_manager/phrase_filter_manager'; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control_factory.ts b/src/plugins/input_control_vis/public/control/control_factory.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/control/control_factory.ts rename to src/plugins/input_control_vis/public/control/control_factory.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts b/src/plugins/input_control_vis/public/control/create_search_source.ts similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts rename to src/plugins/input_control_vis/public/control/create_search_source.ts index 8f86232f63be7..d6772a7cba5b8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts +++ b/src/plugins/input_control_vis/public/control/create_search_source.ts @@ -23,7 +23,7 @@ import { IndexPattern, TimefilterContract, DataPublicPluginStart, -} from '../../../../../plugins/data/public'; +} from 'src/plugins/data/public'; export function createSearchSource( { create }: DataPublicPluginStart['search']['searchSource'], diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts similarity index 93% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts rename to src/plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts index 39c9d843e6bce..a9b7550be44ae 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts @@ -20,12 +20,8 @@ import expect from '@kbn/expect'; import { FilterManager } from './filter_manager'; -import { coreMock } from '../../../../../../core/public/mocks'; -import { - Filter, - IndexPattern, - FilterManager as QueryFilterManager, -} from '../../../../../../plugins/data/public'; +import { coreMock } from '../../../../../core/public/mocks'; +import { Filter, IndexPattern, FilterManager as QueryFilterManager } from '../../../../data/public'; const setupMock = coreMock.createSetup(); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.ts similarity index 93% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts rename to src/plugins/input_control_vis/public/control/filter_manager/filter_manager.ts index 90b88a56950e2..bb806b336c3e0 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.ts @@ -19,11 +19,7 @@ import _ from 'lodash'; -import { - FilterManager as QueryFilterManager, - IndexPattern, - Filter, -} from '../../../../../../plugins/data/public'; +import { FilterManager as QueryFilterManager, IndexPattern, Filter } from '../../../../data/public'; export abstract class FilterManager { constructor( diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts rename to src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts index 5be5d0157541e..6398c10b63a8c 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts @@ -19,11 +19,7 @@ import expect from '@kbn/expect'; -import { - Filter, - IndexPattern, - FilterManager as QueryFilterManager, -} from '../../../../../../plugins/data/public'; +import { Filter, IndexPattern, FilterManager as QueryFilterManager } from '../../../../data/public'; import { PhraseFilterManager } from './phrase_filter_manager'; describe('PhraseFilterManager', function() { diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts rename to src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts index 6f4a95b491907..bf167afa69bcf 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts @@ -25,7 +25,7 @@ import { esFilters, IndexPattern, FilterManager as QueryFilterManager, -} from '../../../../../../plugins/data/public'; +} from '../../../../data/public'; export class PhraseFilterManager extends FilterManager { constructor( diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts rename to src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts index c776042ea4ba6..6e66b6942e5d3 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts @@ -25,7 +25,7 @@ import { RangeFilterMeta, IndexPattern, FilterManager as QueryFilterManager, -} from '../../../../../../plugins/data/public'; +} from '../../../../data/public'; describe('RangeFilterManager', function() { const controlId = 'control1'; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts similarity index 95% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts rename to src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts index 7a6719e85961b..1a884cf267c41 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts @@ -20,12 +20,7 @@ import _ from 'lodash'; import { FilterManager } from './filter_manager'; -import { - esFilters, - RangeFilter, - RangeFilterParams, - IFieldType, -} from '../../../../../../plugins/data/public'; +import { esFilters, RangeFilter, RangeFilterParams, IFieldType } from '../../../../data/public'; interface SliderValue { min?: string | number; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.ts b/src/plugins/input_control_vis/public/control/list_control_factory.test.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.ts rename to src/plugins/input_control_vis/public/control/list_control_factory.test.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts b/src/plugins/input_control_vis/public/control/list_control_factory.ts similarity index 99% rename from src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts rename to src/plugins/input_control_vis/public/control/list_control_factory.ts index 4b2b1d751ffc7..123ef83277e0b 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts +++ b/src/plugins/input_control_vis/public/control/list_control_factory.ts @@ -19,17 +19,17 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Control, noValuesDisableMsg, noIndexPatternMsg } from './control'; -import { PhraseFilterManager } from './filter_manager/phrase_filter_manager'; -import { createSearchSource } from './create_search_source'; -import { ControlParams } from '../editor_utils'; -import { InputControlVisDependencies } from '../plugin'; import { IFieldType, TimefilterContract, SearchSourceFields, DataPublicPluginStart, -} from '../../../../../plugins/data/public'; +} from 'src/plugins/data/public'; +import { Control, noValuesDisableMsg, noIndexPatternMsg } from './control'; +import { PhraseFilterManager } from './filter_manager/phrase_filter_manager'; +import { createSearchSource } from './create_search_source'; +import { ControlParams } from '../editor_utils'; +import { InputControlVisDependencies } from '../plugin'; function getEscapedQuery(query = '') { // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators diff --git a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.ts b/src/plugins/input_control_vis/public/control/range_control_factory.test.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.ts rename to src/plugins/input_control_vis/public/control/range_control_factory.test.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts b/src/plugins/input_control_vis/public/control/range_control_factory.ts similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts rename to src/plugins/input_control_vis/public/control/range_control_factory.ts index 5f3c9994ef353..326756ad5ffc6 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts +++ b/src/plugins/input_control_vis/public/control/range_control_factory.ts @@ -20,16 +20,12 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { IFieldType, TimefilterContract, DataPublicPluginStart } from 'src/plugins/data/public'; import { Control, noValuesDisableMsg, noIndexPatternMsg } from './control'; import { RangeFilterManager } from './filter_manager/range_filter_manager'; import { createSearchSource } from './create_search_source'; import { ControlParams } from '../editor_utils'; import { InputControlVisDependencies } from '../plugin'; -import { - IFieldType, - TimefilterContract, - DataPublicPluginStart, -} from '../.../../../../../../plugins/data/public'; const minMaxAgg = (field?: IFieldType) => { const aggBody: any = {}; diff --git a/src/legacy/core_plugins/input_control_vis/public/editor_utils.ts b/src/plugins/input_control_vis/public/editor_utils.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/editor_utils.ts rename to src/plugins/input_control_vis/public/editor_utils.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/index.scss b/src/plugins/input_control_vis/public/index.scss similarity index 80% rename from src/legacy/core_plugins/input_control_vis/public/index.scss rename to src/plugins/input_control_vis/public/index.scss index ac4692494b923..42fded23d7761 100644 --- a/src/legacy/core_plugins/input_control_vis/public/index.scss +++ b/src/plugins/input_control_vis/public/index.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - // Prefix all styles with "icv" to avoid conflicts. // Examples // icvChart diff --git a/src/legacy/core_plugins/input_control_vis/public/index.ts b/src/plugins/input_control_vis/public/index.ts similarity index 91% rename from src/legacy/core_plugins/input_control_vis/public/index.ts rename to src/plugins/input_control_vis/public/index.ts index e14c2cc4b69b6..8edd3fd9996c3 100644 --- a/src/legacy/core_plugins/input_control_vis/public/index.ts +++ b/src/plugins/input_control_vis/public/index.ts @@ -17,7 +17,9 @@ * under the License. */ -import { PluginInitializerContext } from '../../../../core/public'; +import './index.scss'; + +import { PluginInitializerContext } from '../../../core/public'; import { InputControlVisPlugin as Plugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts b/src/plugins/input_control_vis/public/input_control_fn.test.ts similarity index 92% rename from src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts rename to src/plugins/input_control_vis/public/input_control_fn.test.ts index d654acefd0550..f3ea2d2d6f0ba 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts +++ b/src/plugins/input_control_vis/public/input_control_fn.test.ts @@ -18,9 +18,7 @@ */ import { createInputControlVisFn } from './input_control_fn'; - -// eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; +import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; describe('interpreter/functions#input_control_vis', () => { const fn = functionWrapper(createInputControlVisFn()); diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts b/src/plugins/input_control_vis/public/input_control_fn.ts similarity index 93% rename from src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts rename to src/plugins/input_control_vis/public/input_control_fn.ts index e779c6d344ab5..59c0e03505bb7 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts +++ b/src/plugins/input_control_vis/public/input_control_fn.ts @@ -19,11 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaDatatable, - Render, -} from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; interface Arguments { visConfig: string; diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts b/src/plugins/input_control_vis/public/input_control_vis_type.ts similarity index 96% rename from src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts rename to src/plugins/input_control_vis/public/input_control_vis_type.ts index badea68eec19f..8114dbf110f8b 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts +++ b/src/plugins/input_control_vis/public/input_control_vis_type.ts @@ -23,7 +23,7 @@ import { createInputControlVisController } from './vis_controller'; import { getControlsTab } from './components/editor/controls_tab'; import { OptionsTab } from './components/editor/options_tab'; import { InputControlVisDependencies } from './plugin'; -import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/public'; +import { defaultFeedbackMessage } from '../../kibana_utils/public'; export function createInputControlVisTypeDefinition(deps: InputControlVisDependencies) { const InputControlVisController = createInputControlVisController(deps); diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/index.ts b/src/plugins/input_control_vis/public/lineage/index.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/index.ts rename to src/plugins/input_control_vis/public/lineage/index.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.ts b/src/plugins/input_control_vis/public/lineage/lineage_map.test.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.ts rename to src/plugins/input_control_vis/public/lineage/lineage_map.test.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.ts b/src/plugins/input_control_vis/public/lineage/lineage_map.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.ts rename to src/plugins/input_control_vis/public/lineage/lineage_map.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.ts b/src/plugins/input_control_vis/public/lineage/parent_candidates.test.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.ts rename to src/plugins/input_control_vis/public/lineage/parent_candidates.test.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.ts b/src/plugins/input_control_vis/public/lineage/parent_candidates.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.ts rename to src/plugins/input_control_vis/public/lineage/parent_candidates.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/plugin.ts b/src/plugins/input_control_vis/public/plugin.ts similarity index 92% rename from src/legacy/core_plugins/input_control_vis/public/plugin.ts rename to src/plugins/input_control_vis/public/plugin.ts index b743468065430..9fc7df24c2dcc 100644 --- a/src/legacy/core_plugins/input_control_vis/public/plugin.ts +++ b/src/plugins/input_control_vis/public/plugin.ts @@ -19,11 +19,8 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { - VisualizationsSetup, - VisualizationsStart, -} from '../../../../plugins/visualizations/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/public'; import { createInputControlVisFn } from './input_control_fn'; import { createInputControlVisTypeDefinition } from './input_control_vis_type'; diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_deps_mock.tsx b/src/plugins/input_control_vis/public/test_utils/get_deps_mock.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/get_deps_mock.tsx rename to src/plugins/input_control_vis/public/test_utils/get_deps_mock.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts b/src/plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts rename to src/plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_index_patterns_mock.ts b/src/plugins/input_control_vis/public/test_utils/get_index_patterns_mock.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/get_index_patterns_mock.ts rename to src/plugins/input_control_vis/public/test_utils/get_index_patterns_mock.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_search_service_mock.ts b/src/plugins/input_control_vis/public/test_utils/get_search_service_mock.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/get_search_service_mock.ts rename to src/plugins/input_control_vis/public/test_utils/get_search_service_mock.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/index.ts b/src/plugins/input_control_vis/public/test_utils/index.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/index.ts rename to src/plugins/input_control_vis/public/test_utils/index.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/update_component.ts b/src/plugins/input_control_vis/public/test_utils/update_component.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/update_component.ts rename to src/plugins/input_control_vis/public/test_utils/update_component.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx b/src/plugins/input_control_vis/public/vis_controller.tsx similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx rename to src/plugins/input_control_vis/public/vis_controller.tsx index 818221353afbc..97506556d7e0a 100644 --- a/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx +++ b/src/plugins/input_control_vis/public/vis_controller.tsx @@ -28,8 +28,8 @@ import { ControlParams } from './editor_utils'; import { RangeControl } from './control/range_control_factory'; import { ListControl } from './control/list_control_factory'; import { InputControlVisDependencies } from './plugin'; -import { FilterManager, Filter } from '../../../../plugins/data/public'; -import { VisParams, Vis } from '../../../../plugins/visualizations/public'; +import { FilterManager, Filter } from '../../data/public'; +import { VisParams, Vis } from '../../visualizations/public'; export const createInputControlVisController = (deps: InputControlVisDependencies) => { return class InputControlVisController { diff --git a/src/plugins/input_control_vis/server/index.ts b/src/plugins/input_control_vis/server/index.ts new file mode 100644 index 0000000000000..043657ba98a3c --- /dev/null +++ b/src/plugins/input_control_vis/server/index.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginConfigDescriptor } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + +export const config: PluginConfigDescriptor = { + schema: schema.object({ enabled: schema.boolean({ defaultValue: true }) }), +}; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); From b3c70027990dce1d8a9bfd3ce659f8635f961f92 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 24 Apr 2020 12:08:19 +0100 Subject: [PATCH 29/35] [ML] Moving to kibana capabilities (#64057) * [ML] Moving to kibana capabilities * fixing types * renaming privilges * renaming privileges to capabilities * renaming resolvers * correcting admin capabilities * fixing includes * removing any types * renaming type * readding spaces * adding capabilities switcher * updating comment * removing unnecessary failing tests * adding error to log --- .../uptime/public/state/actions/ml_anomaly.ts | 4 +- .../uptime/public/state/api/ml_anomaly.ts | 4 +- .../public/state/reducers/ml_anomaly.ts | 4 +- x-pack/plugins/ml/common/license/index.ts | 9 +- .../plugins/ml/common/license/ml_license.ts | 12 +- .../plugins/ml/common/types/capabilities.ts | 66 +++ x-pack/plugins/ml/common/types/privileges.ts | 75 --- .../check_capabilities.ts} | 58 +- .../get_capabilities.ts} | 14 +- .../anomalies_table/anomalies_table.test.js | 6 +- .../anomalies_table_columns.js | 2 +- .../components/anomalies_table/links_menu.js | 2 +- .../rule_editor/rule_editor_flyout.js | 2 +- .../rule_editor/rule_editor_flyout.test.js | 2 +- .../components/rule_editor/scope_section.js | 2 +- .../rule_editor/scope_section.test.js | 2 +- .../analytics_list/action_delete.test.tsx | 4 +- .../analytics_list/action_delete.tsx | 2 +- .../analytics_list/action_start.tsx | 2 +- .../components/analytics_list/actions.tsx | 2 +- .../analytics_list/analytics_list.tsx | 2 +- .../create_analytics_button.tsx | 2 +- .../hooks/use_create_analytics_form/state.ts | 2 +- .../results_links/results_links.tsx | 2 +- .../datavisualizer/index_based/page.tsx | 2 +- .../components/job_actions/management.js | 2 +- .../job_details/datafeed_preview_tab.js | 2 +- .../multi_job_actions/actions_menu.js | 2 +- .../group_selector/group_selector.js | 2 +- .../new_job_button/new_job_button.js | 2 +- .../jobs_list_page/jobs_list_page.tsx | 4 +- .../application/overview/overview_page.tsx | 2 +- .../public/application/routing/resolvers.ts | 4 +- .../routes/datavisualizer/datavisualizer.tsx | 4 +- .../routes/datavisualizer/file_based.tsx | 4 +- .../routes/datavisualizer/index_based.tsx | 4 +- .../routes/new_job/index_or_search.tsx | 4 +- .../routing/routes/new_job/wizard.tsx | 4 +- .../application/routing/routes/overview.tsx | 4 +- .../routing/routes/settings/calendar_list.tsx | 7 +- .../routes/settings/calendar_new_edit.tsx | 7 +- .../routing/routes/settings/filter_list.tsx | 7 +- .../routes/settings/filter_list_new_edit.tsx | 7 +- .../routing/routes/settings/settings.tsx | 7 +- .../services/ml_api_service/index.ts | 11 +- .../calendars/edit/new_calendar.test.js | 6 +- .../calendars/list/calendars_list.test.js | 6 +- .../delete_filter_list_modal.test.js | 2 +- .../filter_lists/list/filter_lists.test.js | 2 +- .../settings/filter_lists/list/table.test.js | 2 +- .../forecasting_modal/run_controls.js | 2 +- .../capabilities/__mocks__/ml_capabilities.ts | 29 + .../lib/capabilities/capabilities_switcher.ts | 58 ++ .../capabilities/check_capabilities.test.ts | 255 +++++++++ .../lib/capabilities/check_capabilities.ts | 47 ++ .../index.ts | 3 +- .../upgrade.ts | 0 .../__mocks__/call_with_request.ts | 154 ------ .../check_privileges/check_privileges.test.ts | 515 ------------------ .../lib/check_privileges/check_privileges.ts | 269 --------- .../server/lib/check_privileges/privileges.ts | 37 -- x-pack/plugins/ml/server/plugin.ts | 34 +- x-pack/plugins/ml/server/routes/apidoc.json | 3 +- .../plugins/ml/server/routes/job_service.ts | 31 +- x-pack/plugins/ml/server/routes/system.ts | 26 +- .../shared_services/providers/system.ts | 31 +- .../server/shared_services/shared_services.ts | 13 +- x-pack/plugins/ml/server/types.ts | 6 + 68 files changed, 690 insertions(+), 1215 deletions(-) create mode 100644 x-pack/plugins/ml/common/types/capabilities.ts delete mode 100644 x-pack/plugins/ml/common/types/privileges.ts rename x-pack/plugins/ml/public/application/{privilege/check_privilege.ts => capabilities/check_capabilities.ts} (70%) rename x-pack/plugins/ml/public/application/{privilege/get_privileges.ts => capabilities/get_capabilities.ts} (68%) create mode 100644 x-pack/plugins/ml/server/lib/capabilities/__mocks__/ml_capabilities.ts create mode 100644 x-pack/plugins/ml/server/lib/capabilities/capabilities_switcher.ts create mode 100644 x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts create mode 100644 x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts rename x-pack/plugins/ml/server/lib/{check_privileges => capabilities}/index.ts (65%) rename x-pack/plugins/ml/server/lib/{check_privileges => capabilities}/upgrade.ts (100%) delete mode 100644 x-pack/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts delete mode 100644 x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts delete mode 100644 x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts delete mode 100644 x-pack/plugins/ml/server/lib/check_privileges/privileges.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts index 2e83490b71b54..67c75314a7305 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts @@ -6,7 +6,7 @@ import { createAction } from 'redux-actions'; import { createAsyncAction } from './utils'; -import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges'; +import { MlCapabilitiesResponse } from '../../../../../../plugins/ml/common/types/capabilities'; import { AnomaliesTableRecord } from '../../../../../../plugins/ml/common/types/anomalies'; import { CreateMLJobSuccess, @@ -27,7 +27,7 @@ export const createMLJobAction = createAsyncAction< CreateMLJobSuccess | null >('CREATE_ML_JOB'); -export const getMLCapabilitiesAction = createAsyncAction( +export const getMLCapabilitiesAction = createAsyncAction( 'GET_ML_CAPABILITIES' ); diff --git a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts index f10745a50f56a..16b90e9921428 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts @@ -8,7 +8,7 @@ import moment from 'moment'; import { apiService } from './utils'; import { AnomalyRecords, AnomalyRecordsParams } from '../actions'; import { API_URLS, ML_JOB_ID, ML_MODULE_ID } from '../../../common/constants'; -import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges'; +import { MlCapabilitiesResponse } from '../../../../../../plugins/ml/common/types/capabilities'; import { CreateMLJobSuccess, DeleteJobResults, @@ -20,7 +20,7 @@ import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_r export const getMLJobId = (monitorId: string) => `${monitorId}_${ML_JOB_ID}`.toLowerCase(); -export const getMLCapabilities = async (): Promise => { +export const getMLCapabilities = async (): Promise => { return await apiService.get(API_URLS.ML_CAPABILITIES); }; diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts index 9d59d39634327..df5e825c3488b 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts @@ -17,7 +17,7 @@ import { import { getAsyncInitialState, handleAsyncAction } from './utils'; import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; import { AsyncInitialState } from './types'; -import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges'; +import { MlCapabilitiesResponse } from '../../../../../../plugins/ml/common/types/capabilities'; import { CreateMLJobSuccess, DeleteJobResults } from '../actions/types'; import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_recognizer'; @@ -26,7 +26,7 @@ export interface MLJobState { createJob: AsyncInitialState; deleteJob: AsyncInitialState; anomalies: AsyncInitialState; - mlCapabilities: AsyncInitialState; + mlCapabilities: AsyncInitialState; } const initialState: MLJobState = { diff --git a/x-pack/plugins/ml/common/license/index.ts b/x-pack/plugins/ml/common/license/index.ts index e901a9545897b..e87986a26a3bd 100644 --- a/x-pack/plugins/ml/common/license/index.ts +++ b/x-pack/plugins/ml/common/license/index.ts @@ -4,4 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -export { MlLicense, LicenseStatus, MINIMUM_FULL_LICENSE, MINIMUM_LICENSE } from './ml_license'; +export { + MlLicense, + LicenseStatus, + MINIMUM_FULL_LICENSE, + MINIMUM_LICENSE, + isFullLicense, + isMinimumLicense, +} from './ml_license'; diff --git a/x-pack/plugins/ml/common/license/ml_license.ts b/x-pack/plugins/ml/common/license/ml_license.ts index 2a60887310447..25b5b4992b227 100644 --- a/x-pack/plugins/ml/common/license/ml_license.ts +++ b/x-pack/plugins/ml/common/license/ml_license.ts @@ -38,8 +38,8 @@ export class MlLicense { this._isSecurityEnabled = securityIsEnabled; this._hasLicenseExpired = this._license.status === 'expired'; this._isMlEnabled = this._license.getFeature(PLUGIN_ID).isEnabled; - this._isMinimumLicense = this._license.check(PLUGIN_ID, MINIMUM_LICENSE).state === 'valid'; - this._isFullLicense = this._license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === 'valid'; + this._isMinimumLicense = isMinimumLicense(this._license); + this._isFullLicense = isFullLicense(this._license); if (this._initialized === false && postInitFunctions !== undefined) { postInitFunctions.forEach(f => f(this)); @@ -74,3 +74,11 @@ export class MlLicense { return this._isFullLicense; } } + +export function isFullLicense(license: ILicense) { + return license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === 'valid'; +} + +export function isMinimumLicense(license: ILicense) { + return license.check(PLUGIN_ID, MINIMUM_LICENSE).state === 'valid'; +} diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts new file mode 100644 index 0000000000000..2a449c95faa5b --- /dev/null +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -0,0 +1,66 @@ +/* + * 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 { KibanaRequest } from 'kibana/server'; + +export const userMlCapabilities = { + // Anomaly Detection + canGetJobs: false, + canGetDatafeeds: false, + // Calendars + canGetCalendars: false, + // File Data Visualizer + canFindFileStructure: false, + // Filters + canGetFilters: false, + // Data Frame Analytics + canGetDataFrameAnalytics: false, +}; + +export const adminMlCapabilities = { + // Anomaly Detection + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canForecastJob: false, + canStartStopDatafeed: false, + canUpdateJob: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + // Calendars + canCreateCalendar: false, + canDeleteCalendar: false, + // Filters + canCreateFilter: false, + canDeleteFilter: false, + // Data Frame Analytics + canDeleteDataFrameAnalytics: false, + canCreateDataFrameAnalytics: false, + canStartStopDataFrameAnalytics: false, +}; + +export type UserMlCapabilities = typeof userMlCapabilities; +export type AdminMlCapabilities = typeof adminMlCapabilities; +export type MlCapabilities = UserMlCapabilities & AdminMlCapabilities; + +export const basicLicenseMlCapabilities = ['canFindFileStructure'] as Array; + +export function getDefaultCapabilities(): MlCapabilities { + return { + ...userMlCapabilities, + ...adminMlCapabilities, + }; +} + +export interface MlCapabilitiesResponse { + capabilities: MlCapabilities; + upgradeInProgress: boolean; + isPlatinumOrTrialLicense: boolean; + mlFeatureEnabledInSpace: boolean; +} + +export type ResolveMlCapabilities = (request: KibanaRequest) => Promise; diff --git a/x-pack/plugins/ml/common/types/privileges.ts b/x-pack/plugins/ml/common/types/privileges.ts deleted file mode 100644 index d9089c751b81b..0000000000000 --- a/x-pack/plugins/ml/common/types/privileges.ts +++ /dev/null @@ -1,75 +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. - */ -// - -export interface Privileges { - // Anomaly Detection - canGetJobs: boolean; - canCreateJob: boolean; - canDeleteJob: boolean; - canOpenJob: boolean; - canCloseJob: boolean; - canForecastJob: boolean; - canGetDatafeeds: boolean; - canStartStopDatafeed: boolean; - canUpdateJob: boolean; - canUpdateDatafeed: boolean; - canPreviewDatafeed: boolean; - // Calendars - canGetCalendars: boolean; - canCreateCalendar: boolean; - canDeleteCalendar: boolean; - // Filters - canGetFilters: boolean; - canCreateFilter: boolean; - canDeleteFilter: boolean; - // File Data Visualizer - canFindFileStructure: boolean; - // Data Frame Analytics - canGetDataFrameAnalytics: boolean; - canDeleteDataFrameAnalytics: boolean; - canCreateDataFrameAnalytics: boolean; - canStartStopDataFrameAnalytics: boolean; -} - -export function getDefaultPrivileges(): Privileges { - return { - // Anomaly Detection - canGetJobs: false, - canCreateJob: false, - canDeleteJob: false, - canOpenJob: false, - canCloseJob: false, - canForecastJob: false, - canGetDatafeeds: false, - canStartStopDatafeed: false, - canUpdateJob: false, - canUpdateDatafeed: false, - canPreviewDatafeed: false, - // Calendars - canGetCalendars: false, - canCreateCalendar: false, - canDeleteCalendar: false, - // Filters - canGetFilters: false, - canCreateFilter: false, - canDeleteFilter: false, - // File Data Visualizer - canFindFileStructure: false, - // Data Frame Analytics - canGetDataFrameAnalytics: false, - canDeleteDataFrameAnalytics: false, - canCreateDataFrameAnalytics: false, - canStartStopDataFrameAnalytics: false, - }; -} - -export interface PrivilegesResponse { - capabilities: Privileges; - upgradeInProgress: boolean; - isPlatinumOrTrialLicense: boolean; - mlFeatureEnabledInSpace: boolean; -} diff --git a/x-pack/plugins/ml/public/application/privilege/check_privilege.ts b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts similarity index 70% rename from x-pack/plugins/ml/public/application/privilege/check_privilege.ts rename to x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts index 4de8c6eb703ff..1ca176d8d09ce 100644 --- a/x-pack/plugins/ml/public/application/privilege/check_privilege.ts +++ b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts @@ -8,19 +8,19 @@ import { i18n } from '@kbn/i18n'; import { hasLicenseExpired } from '../license'; -import { Privileges, getDefaultPrivileges } from '../../../common/types/privileges'; -import { getPrivileges, getManageMlPrivileges } from './get_privileges'; +import { MlCapabilities, getDefaultCapabilities } from '../../../common/types/capabilities'; +import { getCapabilities, getManageMlCapabilities } from './get_capabilities'; import { ACCESS_DENIED_PATH } from '../management/management_urls'; -let privileges: Privileges = getDefaultPrivileges(); -// manage_ml requires all monitor and admin cluster privileges: https://github.com/elastic/elasticsearch/blob/664a29c8905d8ce9ba8c18aa1ed5c5de93a0eabc/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java#L53 -export function checkGetManagementMlJobs() { +let _capabilities: MlCapabilities = getDefaultCapabilities(); + +export function checkGetManagementMlJobsResolver() { return new Promise<{ mlFeatureEnabledInSpace: boolean }>((resolve, reject) => { - getManageMlPrivileges().then( + getManageMlCapabilities().then( ({ capabilities, isPlatinumOrTrialLicense, mlFeatureEnabledInSpace }) => { - privileges = capabilities; - // Loop through all privileges to ensure they are all set to true. - const isManageML = Object.values(privileges).every(p => p === true); + _capabilities = capabilities; + // Loop through all capabilities to ensure they are all set to true. + const isManageML = Object.values(_capabilities).every(p => p === true); if (isManageML === true && isPlatinumOrTrialLicense === true) { return resolve({ mlFeatureEnabledInSpace }); @@ -33,17 +33,17 @@ export function checkGetManagementMlJobs() { }); } -export function checkGetJobsPrivilege(): Promise { +export function checkGetJobsCapabilitiesResolver(): Promise { return new Promise((resolve, reject) => { - getPrivileges().then(({ capabilities, isPlatinumOrTrialLicense }) => { - privileges = capabilities; + getCapabilities().then(({ capabilities, isPlatinumOrTrialLicense }) => { + _capabilities = capabilities; // the minimum privilege for using ML with a platinum or trial license is being able to get the transforms list. - // all other functionality is controlled by the return privileges object. + // all other functionality is controlled by the return capabilities object. // if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect, // allow the promise to resolve as the separate license check will redirect then user to // a basic feature - if (privileges.canGetJobs || isPlatinumOrTrialLicense === false) { - return resolve(privileges); + if (_capabilities.canGetJobs || isPlatinumOrTrialLicense === false) { + return resolve(_capabilities); } else { window.location.href = '#/access-denied'; return reject(); @@ -52,15 +52,15 @@ export function checkGetJobsPrivilege(): Promise { }); } -export function checkCreateJobsPrivilege(): Promise { +export function checkCreateJobsCapabilitiesResolver(): Promise { return new Promise((resolve, reject) => { - getPrivileges().then(({ capabilities, isPlatinumOrTrialLicense }) => { - privileges = capabilities; + getCapabilities().then(({ capabilities, isPlatinumOrTrialLicense }) => { + _capabilities = capabilities; // if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect, // allow the promise to resolve as the separate license check will redirect then user to // a basic feature - if (privileges.canCreateJob || isPlatinumOrTrialLicense === false) { - return resolve(privileges); + if (_capabilities.canCreateJob || isPlatinumOrTrialLicense === false) { + return resolve(_capabilities); } else { // if the user has no permission to create a job, // redirect them back to the Transforms Management page @@ -71,14 +71,14 @@ export function checkCreateJobsPrivilege(): Promise { }); } -export function checkFindFileStructurePrivilege(): Promise { +export function checkFindFileStructurePrivilegeResolver(): Promise { return new Promise((resolve, reject) => { - getPrivileges().then(({ capabilities }) => { - privileges = capabilities; + getCapabilities().then(({ capabilities }) => { + _capabilities = capabilities; // the minimum privilege for using ML with a basic license is being able to use the datavisualizer. - // all other functionality is controlled by the return privileges object - if (privileges.canFindFileStructure) { - return resolve(privileges); + // all other functionality is controlled by the return _capabilities object + if (_capabilities.canFindFileStructure) { + return resolve(_capabilities); } else { window.location.href = '#/access-denied'; return reject(); @@ -89,14 +89,14 @@ export function checkFindFileStructurePrivilege(): Promise { // check the privilege type and the license to see whether a user has permission to access a feature. // takes the name of the privilege variable as specified in get_privileges.js -export function checkPermission(privilegeType: keyof Privileges) { +export function checkPermission(capability: keyof MlCapabilities) { const licenseHasExpired = hasLicenseExpired(); - return privileges[privilegeType] === true && licenseHasExpired !== true; + return _capabilities[capability] === true && licenseHasExpired !== true; } // create the text for the button's tooltips if the user's license has // expired or if they don't have the privilege to press that button -export function createPermissionFailureMessage(privilegeType: keyof Privileges) { +export function createPermissionFailureMessage(privilegeType: keyof MlCapabilities) { let message = ''; const licenseHasExpired = hasLicenseExpired(); if (licenseHasExpired) { diff --git a/x-pack/plugins/ml/public/application/privilege/get_privileges.ts b/x-pack/plugins/ml/public/application/capabilities/get_capabilities.ts similarity index 68% rename from x-pack/plugins/ml/public/application/privilege/get_privileges.ts rename to x-pack/plugins/ml/public/application/capabilities/get_capabilities.ts index a3811779333d9..c9561ed254544 100644 --- a/x-pack/plugins/ml/public/application/privilege/get_privileges.ts +++ b/x-pack/plugins/ml/public/application/capabilities/get_capabilities.ts @@ -7,12 +7,12 @@ import { ml } from '../services/ml_api_service'; import { setUpgradeInProgress } from '../services/upgrade_service'; -import { PrivilegesResponse } from '../../../common/types/privileges'; +import { MlCapabilitiesResponse } from '../../../common/types/capabilities'; -export function getPrivileges(): Promise { +export function getCapabilities(): Promise { return new Promise((resolve, reject) => { - ml.checkMlPrivileges() - .then((resp: PrivilegesResponse) => { + ml.checkMlCapabilities() + .then((resp: MlCapabilitiesResponse) => { if (resp.upgradeInProgress === true) { setUpgradeInProgress(true); } @@ -24,10 +24,10 @@ export function getPrivileges(): Promise { }); } -export function getManageMlPrivileges(): Promise { +export function getManageMlCapabilities(): Promise { return new Promise((resolve, reject) => { - ml.checkManageMLPrivileges() - .then((resp: PrivilegesResponse) => { + ml.checkManageMLCapabilities() + .then((resp: MlCapabilitiesResponse) => { if (resp.upgradeInProgress === true) { setUpgradeInProgress(true); } diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js index b881bfe4f1fe6..7696143a9ce71 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js @@ -8,14 +8,14 @@ import React from 'react'; import mockAnomaliesTableData from '../../explorer/__mocks__/mock_anomalies_table_data.json'; import { getColumns } from './anomalies_table_columns'; -jest.mock('../../privilege/check_privilege', () => ({ +jest.mock('../../capabilities/check_capabilities', () => ({ checkPermission: () => false, })); jest.mock('../../license', () => ({ hasLicenseExpired: () => false, })); -jest.mock('../../privilege/get_privileges', () => ({ - getPrivileges: () => {}, +jest.mock('../../capabilities/get_capabilities', () => ({ + getCapabilities: () => {}, })); jest.mock('../../services/field_format_service', () => ({ getFieldFormat: () => {}, diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js index 0c6c959927140..8f79ce4a6c08a 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js @@ -23,7 +23,7 @@ import { DetectorCell } from './detector_cell'; import { EntityCell } from '../entity_cell'; import { InfluencersCell } from './influencers_cell'; import { LinksMenu } from './links_menu'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { mlFieldFormatService } from '../../services/field_format_service'; import { isRuleSupported } from '../../../../common/util/anomaly_utils'; import { formatValue } from '../../formatters/format_value'; diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js index 2a34f12330a75..5d2b8e35fbc0f 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -17,7 +17,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; import { isRuleSupported } from '../../../../common/util/anomaly_utils'; import { parseInterval } from '../../../../common/util/parse_interval'; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js index f8868ec099985..f259a1f1ffb02 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js @@ -30,7 +30,7 @@ import { import { DetectorDescriptionList } from './components/detector_description_list'; import { ActionsSection } from './actions_section'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { ConditionsSection } from './conditions_section'; import { ScopeSection } from './scope_section'; import { SelectRuleAction } from './select_rule_action'; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js index 5c43c558a3333..3e8a17eeb8617 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js @@ -45,7 +45,7 @@ jest.mock('../../services/job_service', () => ({ }, })); jest.mock('../../services/ml_api_service', () => 'ml'); -jest.mock('../../privilege/check_privilege', () => ({ +jest.mock('../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js index 8dca6db96d45b..48e0da72f067c 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js @@ -14,7 +14,7 @@ import React from 'react'; import { EuiCallOut, EuiCheckbox, EuiLink, EuiSpacer, EuiTitle } from '@elastic/eui'; import { ScopeExpression } from './scope_expression'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { getScopeFieldDefaults } from './utils'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.test.js b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.test.js index 3d7861fc7de0a..189fb6a35d3e6 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.test.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.test.js @@ -11,7 +11,7 @@ jest.mock('../../services/job_service.js', () => 'mlJobService'); // The mock is hoisted to the top, so need to prefix the mock function // with 'mock' so it can be used lazily. const mockCheckPermission = jest.fn(() => true); -jest.mock('../../privilege/check_privilege', () => ({ +jest.mock('../../capabilities/check_capabilities', () => ({ checkPermission: privilege => mockCheckPermission(privilege), })); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx index 6d1db5033863b..3e5224b76329e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx @@ -7,13 +7,13 @@ import React from 'react'; import { render } from '@testing-library/react'; -import * as CheckPrivilige from '../../../../../privilege/check_privilege'; +import * as CheckPrivilige from '../../../../../capabilities/check_capabilities'; import { DeleteAction } from './action_delete'; import mockAnalyticsListItem from './__mocks__/analytics_list_item.json'; -jest.mock('../../../../../privilege/check_privilege', () => ({ +jest.mock('../../../../../capabilities/check_capabilities', () => ({ checkPermission: jest.fn(() => false), createPermissionFailureMessage: jest.fn(), })); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx index 47fc84cf450c0..2923938ae68ac 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx @@ -19,7 +19,7 @@ import { deleteAnalytics } from '../../services/analytics_service'; import { checkPermission, createPermissionFailureMessage, -} from '../../../../../privilege/check_privilege'; +} from '../../../../../capabilities/check_capabilities'; import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx index 40664a1413845..bbbda85c45e49 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx @@ -19,7 +19,7 @@ import { startAnalytics } from '../../services/analytics_service'; import { checkPermission, createPermissionFailureMessage, -} from '../../../../../privilege/check_privilege'; +} from '../../../../../capabilities/check_capabilities'; import { DataFrameAnalyticsListRow, isCompletedAnalyticsJob } from './common'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index 4e19df9ae22a8..72514c91ff58b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -12,7 +12,7 @@ import { DeepReadonly } from '../../../../../../../common/types/common'; import { checkPermission, createPermissionFailureMessage, -} from '../../../../../privilege/check_privilege'; +} from '../../../../../capabilities/check_capabilities'; import { getAnalysisType, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index d2e5f582d23f6..58bc75bd7309b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { DataFrameAnalyticsId, useRefreshAnalyticsList } from '../../../../common'; -import { checkPermission } from '../../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { getTaskStateBadge } from './columns'; import { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx index e5054e8a6ad2c..3c8c3c3b3aa55 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { createPermissionFailureMessage } from '../../../../../privilege/check_privilege'; +import { createPermissionFailureMessage } from '../../../../../capabilities/check_capabilities'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; export const CreateAnalyticsButton: FC = props => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index e121268e65e86..70840a442f6f6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -6,7 +6,7 @@ import { EuiComboBoxOptionOption } from '@elastic/eui'; import { DeepPartial, DeepReadonly } from '../../../../../../../common/types/common'; -import { checkPermission } from '../../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../../ml_nodes_check'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx index dddf64ce2cfd3..c05ca63df3a75 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui'; import { ml } from '../../../../services/ml_api_service'; import { isFullLicense } from '../../../../license'; -import { checkPermission } from '../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import { useMlKibana } from '../../../../contexts/kibana'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 3a37274edbc16..86ffc4a2614b9 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -33,7 +33,7 @@ import { NavigationMenu } from '../../components/navigation_menu'; import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; import { isFullLicense } from '../../license'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../ml_nodes_check/check_ml_nodes'; import { FullTimeRangeSelector } from '../../components/full_time_range_selector'; import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js index 92ad842686b05..bb4bed93f922e 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { checkPermission } from '../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import { getIndexPatternNames } from '../../../../util/index_utils'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js index 216c416f30a6b..9406f1b3456cf 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js @@ -10,7 +10,7 @@ import React, { Component } from 'react'; import { EuiSpacer, EuiCallOut, EuiLoadingSpinner } from '@elastic/eui'; import { mlJobService } from '../../../../services/job_service'; -import { checkPermission } from '../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; import { ML_DATA_PREVIEW_COUNT } from '../../../../../../common/util/job_utils'; import { MLJobEditor } from '../ml_job_editor'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js index a5509c0f79a36..2e530a66cd83d 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { checkPermission } from '../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js index 5f91ba9b6f107..e7b6e3a771a85 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js @@ -22,7 +22,7 @@ import { import { cloneDeep } from 'lodash'; import { ml } from '../../../../../services/ml_api_service'; -import { checkPermission } from '../../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { GroupList } from './group_list'; import { NewGroupInput } from './new_group_input'; import { mlMessageBarService } from '../../../../../components/messagebar'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js index 1297ca5b9afd1..fdffa8b38ae04 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { checkPermission } from '../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import React from 'react'; diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index dca1235a96cb6..4a41f3e45001d 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -20,7 +20,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { checkGetManagementMlJobs } from '../../../../privilege/check_privilege'; +import { checkGetManagementMlJobsResolver } from '../../../../capabilities/check_capabilities'; import { getDocLinks } from '../../../../util/dependency_cache'; // @ts-ignore undeclared module @@ -75,7 +75,7 @@ export const JobsListPage: FC<{ I18nContext: CoreStart['i18n']['Context'] }> = ( const check = async () => { try { - const checkPrivilege = await checkGetManagementMlJobs(); + const checkPrivilege = await checkGetManagementMlJobsResolver(); setInitialized(true); setIsMlEnabledInSpace(checkPrivilege.mlFeatureEnabledInSpace); } catch (e) { diff --git a/x-pack/plugins/ml/public/application/overview/overview_page.tsx b/x-pack/plugins/ml/public/application/overview/overview_page.tsx index 7c995c8a568d7..9a852c491ee27 100644 --- a/x-pack/plugins/ml/public/application/overview/overview_page.tsx +++ b/x-pack/plugins/ml/public/application/overview/overview_page.tsx @@ -6,7 +6,7 @@ import React, { Fragment, FC } from 'react'; import { EuiFlexGroup, EuiPage, EuiPageBody } from '@elastic/eui'; -import { checkPermission } from '../privilege/check_privilege'; +import { checkPermission } from '../capabilities/check_capabilities'; import { mlNodesAvailable } from '../ml_nodes_check/check_ml_nodes'; import { NavigationMenu } from '../components/navigation_menu'; import { OverviewSideBar } from './components/sidebar'; diff --git a/x-pack/plugins/ml/public/application/routing/resolvers.ts b/x-pack/plugins/ml/public/application/routing/resolvers.ts index 776f0727389dd..958221df8a636 100644 --- a/x-pack/plugins/ml/public/application/routing/resolvers.ts +++ b/x-pack/plugins/ml/public/application/routing/resolvers.ts @@ -6,7 +6,7 @@ import { loadIndexPatterns, loadSavedSearches } from '../util/index_utils'; import { checkFullLicense } from '../license'; -import { checkGetJobsPrivilege } from '../privilege/check_privilege'; +import { checkGetJobsCapabilitiesResolver } from '../capabilities/check_capabilities'; import { getMlNodeCount } from '../ml_nodes_check/check_ml_nodes'; import { loadMlServerInfo } from '../services/ml_server_info'; @@ -28,6 +28,6 @@ export const basicResolvers = ({ indexPatterns }: BasicResolverDependencies): Re getMlNodeCount, loadMlServerInfo, loadIndexPatterns: () => loadIndexPatterns(indexPatterns), - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, loadSavedSearches, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx index d257a9c080c35..fc2d517b2edb1 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx @@ -16,7 +16,7 @@ import { useResolver } from '../../use_resolver'; import { DatavisualizerSelector } from '../../../datavisualizer'; import { checkBasicLicense } from '../../../license'; -import { checkFindFileStructurePrivilege } from '../../../privilege/check_privilege'; +import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; const breadcrumbs = [ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB]; @@ -30,7 +30,7 @@ export const selectorRoute: MlRoute = { const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkBasicLicense, - checkFindFileStructurePrivilege, + checkFindFileStructurePrivilege: checkFindFileStructurePrivilegeResolver, }); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx index 174b3e3b4b338..1115a38870821 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx @@ -17,7 +17,7 @@ import { useResolver } from '../../use_resolver'; import { FileDataVisualizerPage } from '../../../datavisualizer/file_based'; import { checkBasicLicense } from '../../../license'; -import { checkFindFileStructurePrivilege } from '../../../privilege/check_privilege'; +import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; import { loadIndexPatterns } from '../../../util/index_utils'; import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -43,7 +43,7 @@ const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkFindFileStructurePrivilege, + checkFindFileStructurePrivilege: checkFindFileStructurePrivilegeResolver, }); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx index a3dbc9f97124c..1ec73fced82fe 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -12,7 +12,7 @@ import { useResolver } from '../../use_resolver'; import { Page } from '../../../datavisualizer/index_based'; import { checkBasicLicense } from '../../../license'; -import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; +import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -39,7 +39,7 @@ const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver(index, savedSearchId, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, checkMlNodesAvailable, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index 9411b415e4e4d..b630b09b1a46d 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -13,7 +13,7 @@ import { Page, preConfiguredJobRedirect } from '../../../jobs/new_job/pages/inde import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; import { checkBasicLicense } from '../../../license'; import { loadIndexPatterns } from '../../../util/index_utils'; -import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; +import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; enum MODE { @@ -71,7 +71,7 @@ const PageWrapper: FC = ({ nextStepPath, deps, mode }) = const dataVizResolvers = { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, checkMlNodesAvailable, }; diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx index b1256e21888d9..1c91d7e94b241 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -15,7 +15,7 @@ import { Page } from '../../../jobs/new_job/pages/new_job'; import { JOB_TYPE } from '../../../../../common/constants/new_job'; import { mlJobService } from '../../../services/job_service'; import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service'; -import { checkCreateJobsPrivilege } from '../../../privilege/check_privilege'; +import { checkCreateJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; interface WizardPageProps extends PageProps { @@ -115,7 +115,7 @@ const PageWrapper: FC = ({ location, jobType, deps }) => { const { index, savedSearchId }: Record = parse(location.search, { sort: false }); const { context, results } = useResolver(index, savedSearchId, deps.config, { ...basicResolvers(deps), - privileges: checkCreateJobsPrivilege, + privileges: checkCreateJobsCapabilitiesResolver, jobCaps: () => loadNewJobCapabilities(index, savedSearchId, deps.indexPatterns), existingJobsAndGroups: mlJobService.getJobAndGroupIds, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx index ccb99985cb70c..9b08bbf35c448 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx @@ -13,7 +13,7 @@ import { useResolver } from '../use_resolver'; import { OverviewPage } from '../../overview'; import { checkFullLicense } from '../../license'; -import { checkGetJobsPrivilege } from '../../privilege/check_privilege'; +import { checkGetJobsCapabilitiesResolver } from '../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../ml_nodes_check'; import { loadMlServerInfo } from '../../services/ml_server_info'; import { useTimefilter } from '../../contexts/kibana'; @@ -38,7 +38,7 @@ export const overviewRoute: MlRoute = { const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, getMlNodeCount, loadMlServerInfo, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx index 9d5c4e9c0b0a0..e015a3292acc4 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx @@ -17,7 +17,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { CalendarsList } from '../../../settings/calendars'; import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -42,7 +45,7 @@ export const calendarListRoute: MlRoute = { const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, getMlNodeCount, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx index bf039e3bd2354..ebd58120853a9 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx @@ -17,7 +17,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { NewCalendar } from '../../../settings/calendars'; import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -74,7 +77,7 @@ const PageWrapper: FC = ({ location, mode, deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, checkMlNodesAvailable, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx index 6839ad833cb06..25bded1a52db1 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx @@ -17,7 +17,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { FilterLists } from '../../../settings/filter_lists'; @@ -43,7 +46,7 @@ export const filterListRoute: MlRoute = { const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, getMlNodeCount, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx index 7b8bd6c3c81ac..2f4ccecf2f1a2 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx @@ -17,7 +17,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { EditFilterList } from '../../../settings/filter_lists'; import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -74,7 +77,7 @@ const PageWrapper: FC = ({ location, mode, deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, checkMlNodesAvailable, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx index 10ccc0987fe5d..7cb943c091c4e 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx @@ -16,7 +16,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { Settings } from '../../../settings'; import { ML_BREADCRUMB, SETTINGS } from '../../breadcrumbs'; @@ -32,7 +35,7 @@ export const settingsRoute: MlRoute = { const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, getMlNodeCount, }); diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index e160126833801..6e3fd08e90e38 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -15,7 +15,7 @@ import { jobs } from './jobs'; import { fileDatavisualizer } from './datavisualizer'; import { MlServerDefaults, MlServerLimits } from '../../../../common/types/ml_server_info'; -import { PrivilegesResponse } from '../../../../common/types/privileges'; +import { MlCapabilitiesResponse } from '../../../../common/types/capabilities'; import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars'; import { Job, @@ -300,18 +300,17 @@ export const ml = { }); }, - checkMlPrivileges() { - return http({ + checkMlCapabilities() { + return http({ path: `${basePath()}/ml_capabilities`, method: 'GET', }); }, - checkManageMLPrivileges() { - return http({ + checkManageMLCapabilities() { + return http({ path: `${basePath()}/ml_capabilities`, method: 'GET', - query: { ignoreSpaces: true }, }); }, diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js index 7e2d6814c0b23..7f5ade64e7f14 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js @@ -7,15 +7,15 @@ jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () =>
, })); -jest.mock('../../../privilege/check_privilege', () => ({ +jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); jest.mock('../../../license', () => ({ hasLicenseExpired: () => false, isFullLicense: () => false, })); -jest.mock('../../../privilege/get_privileges', () => ({ - getPrivileges: () => {}, +jest.mock('../../../capabilities/get_capabilities', () => ({ + getCapabilities: () => {}, })); jest.mock('../../../ml_nodes_check/check_ml_nodes', () => ({ mlNodesAvailable: () => true, diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js index 8750927ac1ee7..b2fce2c1474cb 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js @@ -13,15 +13,15 @@ import { CalendarsList } from './calendars_list'; jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () =>
, })); -jest.mock('../../../privilege/check_privilege', () => ({ +jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); jest.mock('../../../license', () => ({ hasLicenseExpired: () => false, isFullLicense: () => false, })); -jest.mock('../../../privilege/get_privileges', () => ({ - getPrivileges: () => {}, +jest.mock('../../../capabilities/get_capabilities', () => ({ + getCapabilities: () => {}, })); jest.mock('../../../ml_nodes_check/check_ml_nodes', () => ({ mlNodesAvailable: () => true, diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js index 8b1ab853676cf..0266bc2a55318 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js @@ -8,7 +8,7 @@ // The mock is hoisted to the top, so need to prefix the mock function // with 'mock' so it can be used lazily. const mockCheckPermission = jest.fn(() => true); -jest.mock('../../../../privilege/check_privilege', () => ({ +jest.mock('../../../../capabilities/check_capabilities', () => ({ checkPermission: privilege => mockCheckPermission(privilege), })); jest.mock('../../../../services/ml_api_service', () => 'ml'); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js index dbc815b5fc099..c1bcee4acdd37 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js @@ -12,7 +12,7 @@ import { FilterLists } from './filter_lists'; jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () =>
, })); -jest.mock('../../../privilege/check_privilege', () => ({ +jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.test.js b/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.test.js index a5c1c872bfa5a..29b1185ddd4ab 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.test.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.test.js @@ -6,7 +6,7 @@ // Create a mock for the privilege check used within the table to // enable/disable the 'New Filter' button. -jest.mock('../../../privilege/check_privilege', () => ({ +jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); jest.mock('../../../services/ml_api_service', () => 'ml'); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js index c04ac40b962d5..7dd06268f7f8d 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js @@ -32,7 +32,7 @@ import { mlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { checkPermission, createPermissionFailureMessage, -} from '../../../privilege/check_privilege'; +} from '../../../capabilities/check_capabilities'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/ml/server/lib/capabilities/__mocks__/ml_capabilities.ts b/x-pack/plugins/ml/server/lib/capabilities/__mocks__/ml_capabilities.ts new file mode 100644 index 0000000000000..8e350b8382276 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/__mocks__/ml_capabilities.ts @@ -0,0 +1,29 @@ +/* + * 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 { + adminMlCapabilities, + userMlCapabilities, + MlCapabilities, + getDefaultCapabilities, +} from '../../../../common/types/capabilities'; + +export function getAdminCapabilities() { + const caps: any = {}; + Object.keys(adminMlCapabilities).forEach(k => { + caps[k] = true; + }); + return { ...getUserCapabilities(), ...caps } as MlCapabilities; +} + +export function getUserCapabilities() { + const caps: any = {}; + Object.keys(userMlCapabilities).forEach(k => { + caps[k] = true; + }); + + return { ...getDefaultCapabilities(), ...caps } as MlCapabilities; +} diff --git a/x-pack/plugins/ml/server/lib/capabilities/capabilities_switcher.ts b/x-pack/plugins/ml/server/lib/capabilities/capabilities_switcher.ts new file mode 100644 index 0000000000000..aef22debf3642 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/capabilities_switcher.ts @@ -0,0 +1,58 @@ +/* + * 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 { cloneDeep } from 'lodash'; +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { CapabilitiesSwitcher, CoreSetup, Logger } from 'src/core/server'; +import { ILicense } from '../../../../licensing/common/types'; +import { isFullLicense, isMinimumLicense } from '../../../common/license'; +import { MlCapabilities, basicLicenseMlCapabilities } from '../../../common/types/capabilities'; + +export const setupCapabilitiesSwitcher = ( + coreSetup: CoreSetup, + license$: Observable, + logger: Logger +) => { + coreSetup.capabilities.registerSwitcher(getSwitcher(license$, logger)); +}; + +function getSwitcher(license$: Observable, logger: Logger): CapabilitiesSwitcher { + return async (request, capabilities) => { + const isAnonymousRequest = !request.route.options.authRequired; + + if (isAnonymousRequest) { + return capabilities; + } + + try { + const license = await license$.pipe(take(1)).toPromise(); + + // full license, leave capabilities as they were + if (isFullLicense(license)) { + return capabilities; + } + + const mlCaps = capabilities.ml as MlCapabilities; + const originalCapabilities = cloneDeep(mlCaps); + + // not full licence, switch off all capabilities + Object.keys(mlCaps).forEach(k => { + mlCaps[k as keyof MlCapabilities] = false; + }); + + // for a basic license, reapply the original capabilities for the basic license features + if (isMinimumLicense(license)) { + basicLicenseMlCapabilities.forEach(c => (mlCaps[c] = originalCapabilities[c])); + } + + return capabilities; + } catch (e) { + logger.debug(`Error updating capabilities for ML based on licensing: ${e}`); + return capabilities; + } + }; +} diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts new file mode 100644 index 0000000000000..5093801d2d184 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts @@ -0,0 +1,255 @@ +/* + * 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 { getAdminCapabilities, getUserCapabilities } from './__mocks__/ml_capabilities'; +import { capabilitiesProvider } from './check_capabilities'; +import { MlLicense } from '../../../common/license'; +import { getDefaultCapabilities } from '../../../common/types/capabilities'; + +const mlLicense = { + isSecurityEnabled: () => true, + isFullLicense: () => true, +} as MlLicense; + +const mlLicenseBasic = { + isSecurityEnabled: () => true, + isFullLicense: () => false, +} as MlLicense; + +const mlIsEnabled = async () => true; +const mlIsNotEnabled = async () => false; + +const callWithRequestNonUpgrade = async () => ({ upgrade_mode: false }); +const callWithRequestUpgrade = async () => ({ upgrade_mode: true }); + +describe('check_capabilities', () => { + describe('getCapabilities() - right number of capabilities', () => { + test('kibana capabilities count', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getAdminCapabilities(), + mlLicense, + mlIsEnabled + ); + const { capabilities } = await getCapabilities(); + const count = Object.keys(capabilities).length; + expect(count).toBe(22); + done(); + }); + }); + + describe('getCapabilities() with security', () => { + test('ml_user capabilities only', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getUserCapabilities(), + mlLicense, + mlIsEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(false); + expect(mlFeatureEnabledInSpace).toBe(true); + expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canCreateJob).toBe(false); + expect(capabilities.canDeleteJob).toBe(false); + expect(capabilities.canOpenJob).toBe(false); + expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canStartStopDatafeed).toBe(false); + expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canUpdateDatafeed).toBe(false); + expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); + + test('full capabilities', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getAdminCapabilities(), + mlLicense, + mlIsEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(false); + expect(mlFeatureEnabledInSpace).toBe(true); + expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canCreateJob).toBe(true); + expect(capabilities.canDeleteJob).toBe(true); + expect(capabilities.canOpenJob).toBe(true); + expect(capabilities.canCloseJob).toBe(true); + expect(capabilities.canForecastJob).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canStartStopDatafeed).toBe(true); + expect(capabilities.canUpdateJob).toBe(true); + expect(capabilities.canUpdateDatafeed).toBe(true); + expect(capabilities.canPreviewDatafeed).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canCreateCalendar).toBe(true); + expect(capabilities.canDeleteCalendar).toBe(true); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canCreateFilter).toBe(true); + expect(capabilities.canDeleteFilter).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(true); + expect(capabilities.canCreateDataFrameAnalytics).toBe(true); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(true); + done(); + }); + + test('upgrade in progress with full capabilities', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestUpgrade, + getAdminCapabilities(), + mlLicense, + mlIsEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(true); + expect(mlFeatureEnabledInSpace).toBe(true); + expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canCreateJob).toBe(false); + expect(capabilities.canDeleteJob).toBe(false); + expect(capabilities.canOpenJob).toBe(false); + expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canStartStopDatafeed).toBe(false); + expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canUpdateDatafeed).toBe(false); + expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); + + test('upgrade in progress with partial capabilities', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestUpgrade, + getUserCapabilities(), + mlLicense, + mlIsEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(true); + expect(mlFeatureEnabledInSpace).toBe(true); + expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canCreateJob).toBe(false); + expect(capabilities.canDeleteJob).toBe(false); + expect(capabilities.canOpenJob).toBe(false); + expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canStartStopDatafeed).toBe(false); + expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canUpdateDatafeed).toBe(false); + expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); + + test('full capabilities, ml disabled in space', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getDefaultCapabilities(), + mlLicense, + mlIsNotEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(false); + expect(mlFeatureEnabledInSpace).toBe(false); + expect(capabilities.canGetJobs).toBe(false); + expect(capabilities.canCreateJob).toBe(false); + expect(capabilities.canDeleteJob).toBe(false); + expect(capabilities.canOpenJob).toBe(false); + expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(false); + expect(capabilities.canStartStopDatafeed).toBe(false); + expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canUpdateDatafeed).toBe(false); + expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetCalendars).toBe(false); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canGetFilters).toBe(false); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canFindFileStructure).toBe(false); + expect(capabilities.canGetDataFrameAnalytics).toBe(false); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); + }); + + test('full capabilities, basic license, ml disabled in space', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getDefaultCapabilities(), + mlLicenseBasic, + mlIsNotEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(false); + expect(mlFeatureEnabledInSpace).toBe(false); + expect(capabilities.canGetJobs).toBe(false); + expect(capabilities.canCreateJob).toBe(false); + expect(capabilities.canDeleteJob).toBe(false); + expect(capabilities.canOpenJob).toBe(false); + expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(false); + expect(capabilities.canStartStopDatafeed).toBe(false); + expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canUpdateDatafeed).toBe(false); + expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetCalendars).toBe(false); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canGetFilters).toBe(false); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canFindFileStructure).toBe(false); + expect(capabilities.canGetDataFrameAnalytics).toBe(false); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); +}); diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts new file mode 100644 index 0000000000000..a2ad83c5522de --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts @@ -0,0 +1,47 @@ +/* + * 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 { IScopedClusterClient } from 'kibana/server'; +import { + MlCapabilities, + adminMlCapabilities, + MlCapabilitiesResponse, +} from '../../../common/types/capabilities'; +import { upgradeCheckProvider } from './upgrade'; +import { MlLicense } from '../../../common/license'; + +export function capabilitiesProvider( + callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'], + capabilities: MlCapabilities, + mlLicense: MlLicense, + isMlEnabledInSpace: () => Promise +) { + const { isUpgradeInProgress } = upgradeCheckProvider(callAsCurrentUser); + async function getCapabilities(): Promise { + const upgradeInProgress = await isUpgradeInProgress(); + const isPlatinumOrTrialLicense = mlLicense.isFullLicense(); + const mlFeatureEnabledInSpace = await isMlEnabledInSpace(); + + if (upgradeInProgress === true) { + // if an upgrade is in progress, set all admin capabilities to false + disableAdminPrivileges(capabilities); + } + + return { + capabilities, + upgradeInProgress, + isPlatinumOrTrialLicense, + mlFeatureEnabledInSpace, + }; + } + return { getCapabilities }; +} + +function disableAdminPrivileges(capabilities: MlCapabilities) { + Object.keys(adminMlCapabilities).forEach(k => { + capabilities[k as keyof MlCapabilities] = false; + }); +} diff --git a/x-pack/plugins/ml/server/lib/check_privileges/index.ts b/x-pack/plugins/ml/server/lib/capabilities/index.ts similarity index 65% rename from x-pack/plugins/ml/server/lib/check_privileges/index.ts rename to x-pack/plugins/ml/server/lib/capabilities/index.ts index 67b435116aa00..b73c6b87f6839 100644 --- a/x-pack/plugins/ml/server/lib/check_privileges/index.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { privilegesProvider, MlCapabilities } from './check_privileges'; +export { capabilitiesProvider } from './check_capabilities'; +export { setupCapabilitiesSwitcher } from './capabilities_switcher'; diff --git a/x-pack/plugins/ml/server/lib/check_privileges/upgrade.ts b/x-pack/plugins/ml/server/lib/capabilities/upgrade.ts similarity index 100% rename from x-pack/plugins/ml/server/lib/check_privileges/upgrade.ts rename to x-pack/plugins/ml/server/lib/capabilities/upgrade.ts diff --git a/x-pack/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts b/x-pack/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts deleted file mode 100644 index ef82003ec80d6..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts +++ /dev/null @@ -1,154 +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. - */ - -export function callWithRequestProvider(testType: string) { - switch (testType) { - case 'partialPrivileges': - return partialPrivileges; - case 'fullPrivileges': - return fullPrivileges; - case 'upgradeWithFullPrivileges': - return upgradeWithFullPrivileges; - case 'upgradeWithPartialPrivileges': - return upgradeWithPartialPrivileges; - - default: - return fullPrivileges; - } -} - -const fullPrivileges = async (action: string, params?: any) => { - switch (action) { - case 'ml.privilegeCheck': - return { - username: 'test2', - has_all_requested: false, - cluster: fullClusterPrivileges, - index: {}, - application: {}, - }; - case 'ml.info': - return { upgrade_mode: false }; - - default: - break; - } -}; - -const partialPrivileges = async (action: string, params?: any) => { - switch (action) { - case 'ml.privilegeCheck': - return { - username: 'test2', - has_all_requested: false, - cluster: partialClusterPrivileges, - index: {}, - application: {}, - }; - case 'ml.info': - return { upgrade_mode: false }; - - default: - break; - } -}; - -const upgradeWithFullPrivileges = async (action: string, params?: any) => { - switch (action) { - case 'ml.privilegeCheck': - return { - username: 'elastic', - has_all_requested: true, - cluster: fullClusterPrivileges, - index: {}, - application: {}, - }; - case 'ml.info': - return { upgrade_mode: true }; - - default: - break; - } -}; - -const upgradeWithPartialPrivileges = async (action: string, params?: any) => { - switch (action) { - case 'ml.privilegeCheck': - return { - username: 'test2', - has_all_requested: false, - cluster: partialClusterPrivileges, - index: {}, - application: {}, - }; - case 'ml.info': - return { upgrade_mode: true }; - - default: - break; - } -}; - -const fullClusterPrivileges = { - 'cluster:admin/xpack/ml/datafeeds/delete': true, - 'cluster:admin/xpack/ml/datafeeds/update': true, - 'cluster:admin/xpack/ml/job/forecast': true, - 'cluster:monitor/xpack/ml/job/stats/get': true, - 'cluster:admin/xpack/ml/filters/delete': true, - 'cluster:admin/xpack/ml/datafeeds/preview': true, - 'cluster:admin/xpack/ml/datafeeds/start': true, - 'cluster:admin/xpack/ml/filters/put': true, - 'cluster:admin/xpack/ml/datafeeds/stop': true, - 'cluster:monitor/xpack/ml/calendars/get': true, - 'cluster:admin/xpack/ml/filters/get': true, - 'cluster:monitor/xpack/ml/datafeeds/get': true, - 'cluster:admin/xpack/ml/filters/update': true, - 'cluster:admin/xpack/ml/calendars/events/post': true, - 'cluster:admin/xpack/ml/job/close': true, - 'cluster:monitor/xpack/ml/datafeeds/stats/get': true, - 'cluster:admin/xpack/ml/calendars/jobs/update': true, - 'cluster:admin/xpack/ml/calendars/put': true, - 'cluster:admin/xpack/ml/calendars/events/delete': true, - 'cluster:admin/xpack/ml/datafeeds/put': true, - 'cluster:admin/xpack/ml/job/open': true, - 'cluster:admin/xpack/ml/job/delete': true, - 'cluster:monitor/xpack/ml/job/get': true, - 'cluster:admin/xpack/ml/job/put': true, - 'cluster:admin/xpack/ml/job/update': true, - 'cluster:admin/xpack/ml/calendars/delete': true, - 'cluster:monitor/xpack/ml/findfilestructure': true, -}; - -// the same as ml_user role -const partialClusterPrivileges = { - 'cluster:admin/xpack/ml/datafeeds/delete': false, - 'cluster:admin/xpack/ml/datafeeds/update': false, - 'cluster:admin/xpack/ml/job/forecast': false, - 'cluster:monitor/xpack/ml/job/stats/get': true, - 'cluster:admin/xpack/ml/filters/delete': false, - 'cluster:admin/xpack/ml/datafeeds/preview': false, - 'cluster:admin/xpack/ml/datafeeds/start': false, - 'cluster:admin/xpack/ml/filters/put': false, - 'cluster:admin/xpack/ml/datafeeds/stop': false, - 'cluster:monitor/xpack/ml/calendars/get': true, - 'cluster:admin/xpack/ml/filters/get': false, - 'cluster:monitor/xpack/ml/datafeeds/get': true, - 'cluster:admin/xpack/ml/filters/update': false, - 'cluster:admin/xpack/ml/calendars/events/post': false, - 'cluster:admin/xpack/ml/job/close': false, - 'cluster:monitor/xpack/ml/datafeeds/stats/get': true, - 'cluster:admin/xpack/ml/calendars/jobs/update': false, - 'cluster:admin/xpack/ml/calendars/put': false, - 'cluster:admin/xpack/ml/calendars/events/delete': false, - 'cluster:admin/xpack/ml/datafeeds/put': false, - 'cluster:admin/xpack/ml/job/open': false, - 'cluster:admin/xpack/ml/job/delete': false, - 'cluster:monitor/xpack/ml/job/get': true, - 'cluster:admin/xpack/ml/job/put': false, - 'cluster:admin/xpack/ml/job/update': false, - 'cluster:admin/xpack/ml/calendars/delete': false, - 'cluster:monitor/xpack/ml/findfilestructure': true, -}; diff --git a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts deleted file mode 100644 index d8435e9026250..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts +++ /dev/null @@ -1,515 +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 { callWithRequestProvider } from './__mocks__/call_with_request'; -import { privilegesProvider } from './check_privileges'; -import { mlPrivileges } from './privileges'; -import { MlLicense } from '../../../common/license'; - -const mlLicenseWithSecurity = { - isSecurityEnabled: () => true, - isFullLicense: () => true, -} as MlLicense; - -const mlLicenseWithOutSecurity = { - isSecurityEnabled: () => false, - isFullLicense: () => true, -} as MlLicense; - -const mlLicenseWithOutSecurityBasicLicense = { - isSecurityEnabled: () => false, - isFullLicense: () => false, -} as MlLicense; - -const mlLicenseWithSecurityBasicLicense = { - isSecurityEnabled: () => true, - isFullLicense: () => false, -} as MlLicense; - -const mlIsEnabled = async () => true; -const mlIsNotEnabled = async () => false; - -describe('check_privileges', () => { - describe('getPrivileges() - right number of capabilities', () => { - test('es capabilities count', async done => { - const count = mlPrivileges.cluster.length; - expect(count).toBe(27); - done(); - }); - - test('kibana capabilities count', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities } = await getPrivileges(); - const count = Object.keys(capabilities).length; - expect(count).toBe(22); - done(); - }); - }); - - describe('getPrivileges() with security', () => { - test('ml_user capabilities only', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('full capabilities', async done => { - const callWithRequest = callWithRequestProvider('fullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(true); - expect(capabilities.canDeleteJob).toBe(true); - expect(capabilities.canOpenJob).toBe(true); - expect(capabilities.canCloseJob).toBe(true); - expect(capabilities.canForecastJob).toBe(true); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(true); - expect(capabilities.canUpdateJob).toBe(true); - expect(capabilities.canUpdateDatafeed).toBe(true); - expect(capabilities.canPreviewDatafeed).toBe(true); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(true); - expect(capabilities.canDeleteCalendar).toBe(true); - expect(capabilities.canGetFilters).toBe(true); - expect(capabilities.canCreateFilter).toBe(true); - expect(capabilities.canDeleteFilter).toBe(true); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(true); - expect(capabilities.canCreateDataFrameAnalytics).toBe(true); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(true); - done(); - }); - - test('upgrade in progress with full capabilities', async done => { - const callWithRequest = callWithRequestProvider('upgradeWithFullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(true); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('upgrade in progress with partial capabilities', async done => { - const callWithRequest = callWithRequestProvider('upgradeWithPartialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(true); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('ml_user capabilities with security with basic license', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurityBasicLicense, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('full user with security with basic license', async done => { - const callWithRequest = callWithRequestProvider('fullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurityBasicLicense, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('full capabilities, ml disabled in space', async done => { - const callWithRequest = callWithRequestProvider('fullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsNotEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(false); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - }); - - describe('getPrivileges() without security', () => { - test('ml_user capabilities only', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(true); - expect(capabilities.canDeleteJob).toBe(true); - expect(capabilities.canOpenJob).toBe(true); - expect(capabilities.canCloseJob).toBe(true); - expect(capabilities.canForecastJob).toBe(true); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(true); - expect(capabilities.canUpdateJob).toBe(true); - expect(capabilities.canUpdateDatafeed).toBe(true); - expect(capabilities.canPreviewDatafeed).toBe(true); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(true); - expect(capabilities.canDeleteCalendar).toBe(true); - expect(capabilities.canGetFilters).toBe(true); - expect(capabilities.canCreateFilter).toBe(true); - expect(capabilities.canDeleteFilter).toBe(true); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(true); - expect(capabilities.canCreateDataFrameAnalytics).toBe(true); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(true); - done(); - }); - - test('upgrade in progress with full capabilities', async done => { - const callWithRequest = callWithRequestProvider('upgradeWithFullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(true); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('upgrade in progress with partial capabilities', async done => { - const callWithRequest = callWithRequestProvider('upgradeWithPartialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(true); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('ml_user capabilities without security with basic license', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurityBasicLicense, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('full user without security with basic license', async done => { - const callWithRequest = callWithRequestProvider('fullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurityBasicLicense, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('ml_user capabilities only, ml disabled in space', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurity, - mlIsNotEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(false); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - }); -}); diff --git a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts deleted file mode 100644 index df61ad0111a03..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts +++ /dev/null @@ -1,269 +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 { IScopedClusterClient } from 'kibana/server'; -import { Privileges, getDefaultPrivileges } from '../../../common/types/privileges'; -import { upgradeCheckProvider } from './upgrade'; -import { MlLicense } from '../../../common/license'; - -import { mlPrivileges } from './privileges'; - -type ClusterPrivilege = Record; - -export interface MlCapabilities { - capabilities: Privileges; - upgradeInProgress: boolean; - isPlatinumOrTrialLicense: boolean; - mlFeatureEnabledInSpace: boolean; -} - -export function privilegesProvider( - callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'], - mlLicense: MlLicense, - isMlEnabledInSpace: () => Promise, - ignoreSpaces: boolean = false -) { - const { isUpgradeInProgress } = upgradeCheckProvider(callAsCurrentUser); - async function getPrivileges(): Promise { - // get the default privileges, forced to be false. - const privileges = getDefaultPrivileges(); - - const upgradeInProgress = await isUpgradeInProgress(); - const isSecurityEnabled = mlLicense.isSecurityEnabled(); - - const isPlatinumOrTrialLicense = mlLicense.isFullLicense(); - const mlFeatureEnabledInSpace = await isMlEnabledInSpace(); - - const setGettingPrivileges = isPlatinumOrTrialLicense - ? setFullGettingPrivileges - : setBasicGettingPrivileges; - - const setActionPrivileges = isPlatinumOrTrialLicense - ? setFullActionPrivileges - : setBasicActionPrivileges; - - if (mlFeatureEnabledInSpace === false && ignoreSpaces === false) { - // if ML isn't enabled in the current space, - // return with the default privileges (all false) - return { - capabilities: privileges, - upgradeInProgress, - isPlatinumOrTrialLicense, - mlFeatureEnabledInSpace, - }; - } - - if (isSecurityEnabled === false) { - if (upgradeInProgress === true) { - // if security is disabled and an upgrade in is progress, - // force all "getting" privileges to be true - // leaving all "setting" privileges to be the default false - setGettingPrivileges({}, privileges, true); - } else { - // if no upgrade is in progress, - // get all privileges forced to true - setGettingPrivileges({}, privileges, true); - setActionPrivileges({}, privileges, true); - } - } else { - // security enabled - // load all ml privileges for this user. - const { cluster } = await callAsCurrentUser('ml.privilegeCheck', { body: mlPrivileges }); - setGettingPrivileges(cluster, privileges); - if (upgradeInProgress === false) { - // if an upgrade is in progress, don't apply the "setting" - // privileges. leave them to be the default false. - setActionPrivileges(cluster, privileges); - } - } - return { - capabilities: privileges, - upgradeInProgress, - isPlatinumOrTrialLicense, - mlFeatureEnabledInSpace, - }; - } - return { getPrivileges }; -} - -function setFullGettingPrivileges( - cluster: ClusterPrivilege = {}, - privileges: Privileges, - forceTrue = false -) { - // Anomaly Detection - if ( - forceTrue || - (cluster['cluster:monitor/xpack/ml/job/get'] && - cluster['cluster:monitor/xpack/ml/job/stats/get']) - ) { - privileges.canGetJobs = true; - } - - if ( - forceTrue || - (cluster['cluster:monitor/xpack/ml/datafeeds/get'] && - cluster['cluster:monitor/xpack/ml/datafeeds/stats/get']) - ) { - privileges.canGetDatafeeds = true; - } - - // Calendars - if (forceTrue || cluster['cluster:monitor/xpack/ml/calendars/get']) { - privileges.canGetCalendars = true; - } - - // Filters - if (forceTrue || cluster['cluster:admin/xpack/ml/filters/get']) { - privileges.canGetFilters = true; - } - - // File Data Visualizer - if (forceTrue || cluster['cluster:monitor/xpack/ml/findfilestructure']) { - privileges.canFindFileStructure = true; - } - - // Data Frame Analytics - if ( - forceTrue || - (cluster['cluster:monitor/xpack/ml/job/get'] && - cluster['cluster:monitor/xpack/ml/job/stats/get']) - ) { - privileges.canGetDataFrameAnalytics = true; - } -} - -function setFullActionPrivileges( - cluster: ClusterPrivilege = {}, - privileges: Privileges, - forceTrue = false -) { - // Anomaly Detection - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/put'] && - cluster['cluster:admin/xpack/ml/job/open'] && - cluster['cluster:admin/xpack/ml/datafeeds/put']) - ) { - privileges.canCreateJob = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/job/update']) { - privileges.canUpdateJob = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/job/open']) { - privileges.canOpenJob = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/job/close']) { - privileges.canCloseJob = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/job/forecast']) { - privileges.canForecastJob = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/delete'] && - cluster['cluster:admin/xpack/ml/datafeeds/delete']) - ) { - privileges.canDeleteJob = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/open'] && - cluster['cluster:admin/xpack/ml/datafeeds/start'] && - cluster['cluster:admin/xpack/ml/datafeeds/stop']) - ) { - privileges.canStartStopDatafeed = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/datafeeds/update']) { - privileges.canUpdateDatafeed = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/datafeeds/preview']) { - privileges.canPreviewDatafeed = true; - } - - // Calendars - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/calendars/put'] && - cluster['cluster:admin/xpack/ml/calendars/jobs/update'] && - cluster['cluster:admin/xpack/ml/calendars/events/post']) - ) { - privileges.canCreateCalendar = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/calendars/delete'] && - cluster['cluster:admin/xpack/ml/calendars/events/delete']) - ) { - privileges.canDeleteCalendar = true; - } - - // Filters - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/filters/put'] && - cluster['cluster:admin/xpack/ml/filters/update']) - ) { - privileges.canCreateFilter = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/filters/delete']) { - privileges.canDeleteFilter = true; - } - - // Data Frame Analytics - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/put'] && - cluster['cluster:admin/xpack/ml/job/open'] && - cluster['cluster:admin/xpack/ml/datafeeds/put']) - ) { - privileges.canCreateDataFrameAnalytics = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/delete'] && - cluster['cluster:admin/xpack/ml/datafeeds/delete']) - ) { - privileges.canDeleteDataFrameAnalytics = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/open'] && - cluster['cluster:admin/xpack/ml/datafeeds/start'] && - cluster['cluster:admin/xpack/ml/datafeeds/stop']) - ) { - privileges.canStartStopDataFrameAnalytics = true; - } -} - -function setBasicGettingPrivileges( - cluster: ClusterPrivilege = {}, - privileges: Privileges, - forceTrue = false -) { - // File Data Visualizer - if (forceTrue || cluster['cluster:monitor/xpack/ml/findfilestructure']) { - privileges.canFindFileStructure = true; - } -} - -function setBasicActionPrivileges( - cluster: ClusterPrivilege = {}, - privileges: Privileges, - forceTrue = false -) {} diff --git a/x-pack/plugins/ml/server/lib/check_privileges/privileges.ts b/x-pack/plugins/ml/server/lib/check_privileges/privileges.ts deleted file mode 100644 index 9fcd28dd68105..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/privileges.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. - */ - -export const mlPrivileges = { - cluster: [ - 'cluster:monitor/xpack/ml/job/get', - 'cluster:monitor/xpack/ml/job/stats/get', - 'cluster:monitor/xpack/ml/datafeeds/get', - 'cluster:monitor/xpack/ml/datafeeds/stats/get', - 'cluster:monitor/xpack/ml/calendars/get', - 'cluster:admin/xpack/ml/job/put', - 'cluster:admin/xpack/ml/job/delete', - 'cluster:admin/xpack/ml/job/update', - 'cluster:admin/xpack/ml/job/open', - 'cluster:admin/xpack/ml/job/close', - 'cluster:admin/xpack/ml/job/forecast', - 'cluster:admin/xpack/ml/datafeeds/put', - 'cluster:admin/xpack/ml/datafeeds/delete', - 'cluster:admin/xpack/ml/datafeeds/start', - 'cluster:admin/xpack/ml/datafeeds/stop', - 'cluster:admin/xpack/ml/datafeeds/update', - 'cluster:admin/xpack/ml/datafeeds/preview', - 'cluster:admin/xpack/ml/calendars/put', - 'cluster:admin/xpack/ml/calendars/delete', - 'cluster:admin/xpack/ml/calendars/jobs/update', - 'cluster:admin/xpack/ml/calendars/events/post', - 'cluster:admin/xpack/ml/calendars/events/delete', - 'cluster:admin/xpack/ml/filters/put', - 'cluster:admin/xpack/ml/filters/get', - 'cluster:admin/xpack/ml/filters/update', - 'cluster:admin/xpack/ml/filters/delete', - 'cluster:monitor/xpack/ml/findfilestructure', - ], -}; diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index c7add12be142c..67e80a3bc44c0 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -7,14 +7,18 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, + CoreStart, Plugin, IScopedClusterClient, + KibanaRequest, Logger, PluginInitializerContext, ICustomClusterClient, + CapabilitiesStart, } from 'kibana/server'; import { PluginsSetup, RouteInitialization } from './types'; import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app'; +import { MlCapabilities } from '../common/types/capabilities'; import { elasticsearchJsPlugin } from './client/elasticsearch_ml'; import { initMlTelemetry } from './lib/telemetry'; @@ -41,6 +45,8 @@ import { systemRoutes } from './routes/system'; import { MlLicense } from '../common/license'; import { MlServerLicense } from './lib/license'; import { createSharedServices, SharedServices } from './shared_services'; +import { userMlCapabilities, adminMlCapabilities } from '../common/types/capabilities'; +import { setupCapabilitiesSwitcher } from './lib/capabilities'; declare module 'kibana/server' { interface RequestHandlerContext { @@ -59,6 +65,7 @@ export class MlServerPlugin implements Plugin initSampleDataSets(mlLicense, plugins), ]); + // initialize capabilities switcher to add license filter to ml capabilities + setupCapabilitiesSwitcher(coreSetup, plugins.licensing.license$, this.log); + // Can access via router's handler function 'context' parameter - context.ml.mlClient const mlClient = coreSetup.elasticsearch.createClient(PLUGIN_ID, { plugins: [elasticsearchJsPlugin], @@ -132,6 +145,14 @@ export class MlServerPlugin implements Plugin { + if (this.capabilities === null) { + return null; + } + const capabilities = await this.capabilities.resolveCapabilities(request); + return capabilities.ml as MlCapabilities; + }; + annotationRoutes(routeInit, plugins.security); calendars(routeInit); dataFeedRoutes(routeInit); @@ -144,24 +165,27 @@ export class MlServerPlugin implements Plugin { try { - const ignoreSpaces = request.query && request.query.ignoreSpaces === 'true'; // if spaces is disabled force isMlEnabledInSpace to be true const { isMlEnabledInSpace } = spaces !== undefined ? spacesUtilsProvider(spaces, (request as unknown) as Request) : { isMlEnabledInSpace: async () => true }; - const { getPrivileges } = privilegesProvider( + const mlCapabilities = await resolveMlCapabilities(request); + if (mlCapabilities === null) { + return response.customError(wrapError(new Error('resolveMlCapabilities is not defined'))); + } + + const { getCapabilities } = capabilitiesProvider( context.ml!.mlClient.callAsCurrentUser, + mlCapabilities, mlLicense, - isMlEnabledInSpace, - ignoreSpaces + isMlEnabledInSpace ); return response.ok({ - body: await getPrivileges(), + body: await getCapabilities(), }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/ml/server/shared_services/providers/system.ts b/x-pack/plugins/ml/server/shared_services/providers/system.ts index cedf18e80906f..698ac8e6261e5 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/system.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/system.ts @@ -4,23 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { APICaller } from 'kibana/server'; +import { APICaller, KibanaRequest } from 'kibana/server'; import { SearchResponse, SearchParams } from 'elasticsearch'; import { MlServerLicense } from '../../lib/license'; import { CloudSetup } from '../../../../cloud/server'; import { LicenseCheck } from '../license_checks'; -import { spacesUtilsProvider, RequestFacade } from '../../lib/spaces_utils'; +import { spacesUtilsProvider } from '../../lib/spaces_utils'; import { SpacesPluginSetup } from '../../../../spaces/server'; -import { privilegesProvider, MlCapabilities } from '../../lib/check_privileges'; +import { capabilitiesProvider } from '../../lib/capabilities'; import { MlInfoResponse } from '../../../common/types/ml_server_info'; import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import { MlCapabilitiesResponse, ResolveMlCapabilities } from '../../../common/types/capabilities'; export interface MlSystemProvider { mlSystemProvider( callAsCurrentUser: APICaller, - request: RequestFacade + request: KibanaRequest ): { - mlCapabilities(ignoreSpaces?: boolean): Promise; + mlCapabilities(): Promise; mlInfo(): Promise; mlSearch(searchParams: SearchParams): Promise>; }; @@ -31,12 +32,13 @@ export function getMlSystemProvider( isFullLicense: LicenseCheck, mlLicense: MlServerLicense, spaces: SpacesPluginSetup | undefined, - cloud: CloudSetup | undefined + cloud: CloudSetup | undefined, + resolveMlCapabilities: ResolveMlCapabilities ): MlSystemProvider { return { - mlSystemProvider(callAsCurrentUser: APICaller, request: RequestFacade) { + mlSystemProvider(callAsCurrentUser: APICaller, request: KibanaRequest) { return { - mlCapabilities(ignoreSpaces?: boolean) { + async mlCapabilities() { isMinimumLicense(); const { isMlEnabledInSpace } = @@ -44,13 +46,18 @@ export function getMlSystemProvider( ? spacesUtilsProvider(spaces, request) : { isMlEnabledInSpace: async () => true }; - const { getPrivileges } = privilegesProvider( + const mlCapabilities = await resolveMlCapabilities(request); + if (mlCapabilities === null) { + throw new Error('resolveMlCapabilities is not defined'); + } + + const { getCapabilities } = capabilitiesProvider( callAsCurrentUser, + mlCapabilities, mlLicense, - isMlEnabledInSpace, - ignoreSpaces + isMlEnabledInSpace ); - return getPrivileges(); + return getCapabilities(); }, async mlInfo(): Promise { isMinimumLicense(); diff --git a/x-pack/plugins/ml/server/shared_services/shared_services.ts b/x-pack/plugins/ml/server/shared_services/shared_services.ts index f08eb5c23b272..f2d20a72444be 100644 --- a/x-pack/plugins/ml/server/shared_services/shared_services.ts +++ b/x-pack/plugins/ml/server/shared_services/shared_services.ts @@ -17,6 +17,7 @@ import { AnomalyDetectorsProvider, getAnomalyDetectorsProvider, } from './providers/anomaly_detectors'; +import { ResolveMlCapabilities } from '../../common/types/capabilities'; export type SharedServices = JobServiceProvider & AnomalyDetectorsProvider & @@ -27,14 +28,22 @@ export type SharedServices = JobServiceProvider & export function createSharedServices( mlLicense: MlServerLicense, spaces: SpacesPluginSetup | undefined, - cloud: CloudSetup + cloud: CloudSetup, + resolveMlCapabilities: ResolveMlCapabilities ): SharedServices { const { isFullLicense, isMinimumLicense } = licenseChecks(mlLicense); return { ...getJobServiceProvider(isFullLicense), ...getAnomalyDetectorsProvider(isFullLicense), - ...getMlSystemProvider(isMinimumLicense, isFullLicense, mlLicense, spaces, cloud), + ...getMlSystemProvider( + isMinimumLicense, + isFullLicense, + mlLicense, + spaces, + cloud, + resolveMlCapabilities + ), ...getModulesProvider(isFullLicense), ...getResultsServiceProvider(isFullLicense), }; diff --git a/x-pack/plugins/ml/server/types.ts b/x-pack/plugins/ml/server/types.ts index ff4d07bd79e42..d4cd61a7fa4f7 100644 --- a/x-pack/plugins/ml/server/types.ts +++ b/x-pack/plugins/ml/server/types.ts @@ -13,6 +13,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../features/serve import { LicensingPluginSetup } from '../../licensing/server'; import { SpacesPluginSetup } from '../../spaces/server'; import { MlServerLicense } from './lib/license'; +import { ResolveMlCapabilities } from '../common/types/capabilities'; export interface LicenseCheckResult { isAvailable: boolean; @@ -26,6 +27,11 @@ export interface LicenseCheckResult { export interface SystemRouteDeps { cloud: CloudSetup; spaces?: SpacesPluginSetup; + resolveMlCapabilities: ResolveMlCapabilities; +} + +export interface JobServiceRouteDeps { + resolveMlCapabilities: ResolveMlCapabilities; } export interface PluginsSetup { From 4eb971c8c39f0ca5f3a443e996cdfa27676c1294 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 24 Apr 2020 13:38:27 +0200 Subject: [PATCH 30/35] [ML] EuiDataGrid ml/transform components. (#63447) Cleanup and consolidation of code related to EuiDataGrid. The transform wizard source and pivot preview table as well as the data frame analytics results pages now share a common code base related to data grid tables. To avoid tight coupling of components and hooks, the hooks are not within the common data grid component. Instead the hooks need to be used on the outer wrapping component and the results will be passed as props to the data grid component. This allows us to pass data from different data sources (transform index source, pivot previews, analytics results) into a shared component. --- .../components/data_grid}/common.test.ts | 60 +-- .../components/data_grid/common.ts | 287 ++++++++++++ .../components/data_grid/data_grid.tsx | 181 ++++++++ .../application/components/data_grid/index.ts | 23 + .../application/components/data_grid/types.ts | 98 ++++ .../components/data_grid/use_data_grid.ts | 112 +++++ .../data_frame_analytics/common/constants.ts | 10 + .../data_frame_analytics/common/data_grid.ts | 23 - .../data_frame_analytics/common/fields.ts | 426 +++++------------- .../common/get_index_data.ts | 68 +++ .../common/get_index_fields.ts | 49 ++ .../data_frame_analytics/common/index.ts | 15 +- .../common/use_results_view_config.ts | 104 +++++ .../classification_exploration.tsx | 181 +------- .../classification_exploration_data_grid.tsx | 135 ------ .../use_explore_data.ts | 292 ------------ .../error_callout/error_callout.tsx | 37 +- .../exploration_data_grid.tsx | 161 ------- .../exploration_page_wrapper.tsx | 76 ++++ .../exploration_page_wrapper}/index.ts | 2 +- .../exploration_results_table.tsx} | 117 ++--- .../exploration_results_table}/index.ts | 2 +- .../use_exploration_results.ts | 72 +++ .../exploration_title/exploration_title.tsx | 15 + .../components/exploration_title}/index.ts | 2 +- .../index.ts | 2 +- .../job_config_error_callout.tsx | 47 ++ .../outlier_exploration}/common.test.ts | 0 .../outlier_exploration}/common.ts | 15 +- .../outlier_exploration.tsx | 151 ++----- .../use_outlier_data.test.ts | 33 ++ .../outlier_exploration/use_outlier_data.ts | 117 +++++ .../regression_exploration/evaluate_panel.tsx | 2 +- .../regression_exploration.tsx | 171 +------ .../regression_exploration_data_grid.tsx | 135 ------ .../regression_exploration/results_table.tsx | 233 ---------- .../use_explore_data.ts | 291 ------------ .../use_explore_data/use_explore_data.ts | 267 ----------- .../analytics_list/action_clone.tsx | 3 +- .../public/__mocks__/shared_imports.ts | 20 +- .../public/app/common/data_grid.test.ts | 93 ++++ .../transform/public/app/common/data_grid.ts | 25 +- .../transform/public/app/common/index.ts | 6 +- .../app/components/pivot_preview/common.ts | 60 --- .../pivot_preview/pivot_preview.test.tsx | 55 --- .../pivot_preview/pivot_preview.tsx | 345 -------------- .../use_pivot_preview_data.test.tsx | 69 --- .../pivot_preview/use_pivot_preview_data.ts | 91 ---- .../public/app/hooks/use_index_data.test.tsx | 85 ++++ .../public/app/hooks/use_index_data.ts | 111 +++++ .../public/app/hooks/use_pivot_data.ts | 240 ++++++++++ .../__snapshots__/expanded_row.test.tsx.snap | 71 --- .../source_index_preview/common.test.ts | 35 -- .../components/source_index_preview/common.ts | 17 - .../expanded_row.test.tsx | 46 -- .../source_index_preview/expanded_row.tsx | 22 - .../source_index_preview.test.tsx | 38 -- .../source_index_preview.tsx | 293 ------------ .../use_source_index_data.test.tsx | 43 -- .../use_source_index_data.ts | 143 ------ .../step_define/step_define_form.tsx | 62 ++- .../step_define/step_define_summary.tsx | 51 ++- .../expanded_row_preview_pane.tsx | 26 +- .../transform/public/shared_imports.ts | 15 + .../translations/translations/ja-JP.json | 28 -- .../translations/translations/zh-CN.json | 28 -- .../apps/transform/creation_index_pattern.ts | 16 +- .../apps/transform/creation_saved_search.ts | 14 +- .../machine_learning/data_frame_analytics.ts | 4 +- .../services/transform_ui/wizard.ts | 16 +- 70 files changed, 2234 insertions(+), 3949 deletions(-) rename x-pack/plugins/{transform/public/app/components/pivot_preview => ml/public/application/components/data_grid}/common.test.ts (52%) create mode 100644 x-pack/plugins/ml/public/application/components/data_grid/common.ts create mode 100644 x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx create mode 100644 x-pack/plugins/ml/public/application/components/data_grid/index.ts create mode 100644 x-pack/plugins/ml/public/application/components/data_grid/types.ts create mode 100644 x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/common/data_grid.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration_data_grid.tsx delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/exploration_data_grid.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx rename x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/{hooks/use_explore_data => components/exploration_page_wrapper}/index.ts (73%) rename x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/{classification_exploration/results_table.tsx => exploration_results_table/exploration_results_table.tsx} (57%) rename x-pack/plugins/{transform/public/app/sections/create_transform/components/source_index_preview => ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table}/index.ts (77%) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/exploration_title.tsx rename x-pack/plugins/{transform/public/app/components/pivot_preview => ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title}/index.ts (81%) rename x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/{exploration_data_grid => job_config_error_callout}/index.ts (78%) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx rename x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/{hooks/use_explore_data => components/outlier_exploration}/common.test.ts (100%) rename x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/{hooks/use_explore_data => components/outlier_exploration}/common.ts (51%) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.test.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts create mode 100644 x-pack/plugins/transform/public/app/common/data_grid.test.ts delete mode 100644 x-pack/plugins/transform/public/app/components/pivot_preview/common.ts delete mode 100644 x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx delete mode 100644 x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx delete mode 100644 x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.test.tsx delete mode 100644 x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts create mode 100644 x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx create mode 100644 x-pack/plugins/transform/public/app/hooks/use_index_data.ts create mode 100644 x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts delete mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap delete mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts delete mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts delete mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx delete mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx delete mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx delete mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx delete mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx delete mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts similarity index 52% rename from x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts rename to x-pack/plugins/ml/public/application/components/data_grid/common.test.ts index 172256ddb5cee..4bb670ad02dfc 100644 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts @@ -6,16 +6,7 @@ import { EuiDataGridSorting } from '@elastic/eui'; -import { - getPreviewRequestBody, - PivotAggsConfig, - PivotGroupByConfig, - PIVOT_SUPPORTED_AGGS, - PIVOT_SUPPORTED_GROUP_BY_AGGS, - SimpleQuery, -} from '../../common'; - -import { multiColumnSortFactory, getPivotPreviewDevConsoleStatement } from './common'; +import { multiColumnSortFactory } from './common'; describe('Transform: Define Pivot Common', () => { test('multiColumnSortFactory()', () => { @@ -65,53 +56,4 @@ describe('Transform: Define Pivot Common', () => { { s: 'a', n: 1 }, ]); }); - - test('getPivotPreviewDevConsoleStatement()', () => { - const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, - }; - const groupBy: PivotGroupByConfig = { - agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, - field: 'the-group-by-field', - aggName: 'the-group-by-agg-name', - dropDownName: 'the-group-by-drop-down-name', - }; - const agg: PivotAggsConfig = { - agg: PIVOT_SUPPORTED_AGGS.AVG, - field: 'the-agg-field', - aggName: 'the-agg-agg-name', - dropDownName: 'the-agg-drop-down-name', - }; - const request = getPreviewRequestBody('the-index-pattern-title', query, [groupBy], [agg]); - const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request); - - expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview -{ - "source": { - "index": [ - "the-index-pattern-title" - ] - }, - "pivot": { - "group_by": { - "the-group-by-agg-name": { - "terms": { - "field": "the-group-by-field" - } - } - }, - "aggregations": { - "the-agg-agg-name": { - "avg": { - "field": "the-agg-field" - } - } - } - } -} -`); - }); }); diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts new file mode 100644 index 0000000000000..d141b68b5d03f --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -0,0 +1,287 @@ +/* + * 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 moment from 'moment-timezone'; +import { useEffect, useMemo } from 'react'; + +import { + EuiDataGridCellValueElementProps, + EuiDataGridSorting, + EuiDataGridStyle, +} from '@elastic/eui'; + +import { + IndexPattern, + IFieldType, + ES_FIELD_TYPES, + KBN_FIELD_TYPES, +} from '../../../../../../../src/plugins/data/public'; + +import { + BASIC_NUMERICAL_TYPES, + EXTENDED_NUMERICAL_TYPES, +} from '../../data_frame_analytics/common/fields'; + +import { + FEATURE_IMPORTANCE, + FEATURE_INFLUENCE, + OUTLIER_SCORE, +} from '../../data_frame_analytics/common/constants'; +import { formatHumanReadableDateTimeSeconds } from '../../util/date_utils'; +import { getNestedProperty } from '../../util/object_utils'; +import { mlFieldFormatService } from '../../services/field_format_service'; + +import { DataGridItem, IndexPagination, RenderCellValue } from './types'; + +export const INIT_MAX_COLUMNS = 20; + +export const euiDataGridStyle: EuiDataGridStyle = { + border: 'all', + fontSize: 's', + cellPadding: 's', + stripes: false, + rowHover: 'none', + header: 'shade', +}; + +export const euiDataGridToolbarSettings = { + showColumnSelector: true, + showStyleSelector: false, + showSortSelector: true, + showFullScreenSelector: false, +}; + +export const getFieldsFromKibanaIndexPattern = (indexPattern: IndexPattern): string[] => { + const allFields = indexPattern.fields.map(f => f.name); + const indexPatternFields: string[] = allFields.filter(f => { + if (indexPattern.metaFields.includes(f)) { + return false; + } + + const fieldParts = f.split('.'); + const lastPart = fieldParts.pop(); + if (lastPart === 'keyword' && allFields.includes(fieldParts.join('.'))) { + return false; + } + + return true; + }); + + return indexPatternFields; +}; + +export interface FieldTypes { + [key: string]: ES_FIELD_TYPES; +} + +export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, resultsField: string) => { + return Object.keys(fieldTypes).map(field => { + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + const isSortable = true; + const type = fieldTypes[field]; + + const isNumber = + type !== undefined && (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); + if (isNumber) { + schema = 'numeric'; + } + + switch (type) { + case 'date': + schema = 'datetime'; + break; + case 'geo_point': + schema = 'json'; + break; + case 'boolean': + schema = 'boolean'; + break; + } + + if ( + field === `${resultsField}.${OUTLIER_SCORE}` || + field.includes(`${resultsField}.${FEATURE_INFLUENCE}`) + ) { + schema = 'numeric'; + } + + if (field.includes(`${resultsField}.${FEATURE_IMPORTANCE}`)) { + schema = 'json'; + } + + return { id: field, schema, isSortable }; + }); +}; + +export const getDataGridSchemaFromKibanaFieldType = (field: IFieldType | undefined) => { + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + + switch (field?.type) { + case KBN_FIELD_TYPES.BOOLEAN: + schema = 'boolean'; + break; + case KBN_FIELD_TYPES.DATE: + schema = 'datetime'; + break; + case KBN_FIELD_TYPES.GEO_POINT: + case KBN_FIELD_TYPES.GEO_SHAPE: + schema = 'json'; + break; + case KBN_FIELD_TYPES.NUMBER: + schema = 'numeric'; + break; + } + + return schema; +}; + +export const useRenderCellValue = ( + indexPattern: IndexPattern | undefined, + pagination: IndexPagination, + tableItems: DataGridItem[], + resultsField?: string, + cellPropsCallback?: ( + columnId: string, + cellValue: any, + fullItem: Record, + setCellProps: EuiDataGridCellValueElementProps['setCellProps'] + ) => void +): RenderCellValue => { + const renderCellValue: RenderCellValue = useMemo(() => { + return ({ + rowIndex, + columnId, + setCellProps, + }: { + rowIndex: number; + columnId: string; + setCellProps: EuiDataGridCellValueElementProps['setCellProps']; + }) => { + const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + const fullItem = tableItems[adjustedRowIndex]; + + if (fullItem === undefined) { + return null; + } + + if (indexPattern === undefined) { + return null; + } + + let format: any; + + if (indexPattern !== undefined) { + format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); + } + + function getCellValue(cId: string) { + if (cId.includes(`.${FEATURE_INFLUENCE}.`) && resultsField !== undefined) { + const results = getNestedProperty(tableItems[adjustedRowIndex], resultsField, null); + return results[cId.replace(`${resultsField}.`, '')]; + } + + return tableItems.hasOwnProperty(adjustedRowIndex) + ? getNestedProperty(tableItems[adjustedRowIndex], cId, null) + : null; + } + + const cellValue = getCellValue(columnId); + + // React by default doesn't all us to use a hook in a callback. + // However, this one will be passed on to EuiDataGrid and its docs + // recommend wrapping `setCellProps` in a `useEffect()` hook + // so we're ignoring the linting rule here. + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + if (typeof cellPropsCallback === 'function') { + cellPropsCallback(columnId, cellValue, fullItem, setCellProps); + } + }, [columnId, cellValue]); + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + if (cellValue === undefined || cellValue === null) { + return null; + } + + if (format !== undefined) { + return format.convert(cellValue, 'text'); + } + + if (typeof cellValue === 'string' || cellValue === null) { + return cellValue; + } + + const field = indexPattern.fields.getByName(columnId); + if (field?.type === KBN_FIELD_TYPES.DATE) { + return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); + } + + if (typeof cellValue === 'boolean') { + return cellValue ? 'true' : 'false'; + } + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + return cellValue; + }; + }, [indexPattern?.fields, pagination.pageIndex, pagination.pageSize, tableItems]); + return renderCellValue; +}; + +/** + * Helper to sort an array of objects based on an EuiDataGrid sorting configuration. + * `sortFn()` is recursive to support sorting on multiple columns. + * + * @param sortingColumns - The EUI data grid sorting configuration + * @returns The sorting function which can be used with an array's sort() function. + */ +export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['columns']) => { + const isString = (arg: any): arg is string => { + return typeof arg === 'string'; + }; + + const sortFn = (a: any, b: any, sortingColumnIndex = 0): number => { + const sort = sortingColumns[sortingColumnIndex]; + const aValue = getNestedProperty(a, sort.id, null); + const bValue = getNestedProperty(b, sort.id, null); + + if (typeof aValue === 'number' && typeof bValue === 'number') { + if (aValue < bValue) { + return sort.direction === 'asc' ? -1 : 1; + } + if (aValue > bValue) { + return sort.direction === 'asc' ? 1 : -1; + } + } + + if (isString(aValue) && isString(bValue)) { + if (aValue.localeCompare(bValue) === -1) { + return sort.direction === 'asc' ? -1 : 1; + } + if (aValue.localeCompare(bValue) === 1) { + return sort.direction === 'asc' ? 1 : -1; + } + } + + if (sortingColumnIndex + 1 < sortingColumns.length) { + return sortFn(a, b, sortingColumnIndex + 1); + } + + return 0; + }; + + return sortFn; +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx new file mode 100644 index 0000000000000..a5b301902cc75 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -0,0 +1,181 @@ +/* + * 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, { useEffect, FC } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { + EuiButtonIcon, + EuiCallOut, + EuiCodeBlock, + EuiCopy, + EuiDataGrid, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; + +import { CoreSetup } from 'src/core/public'; + +import { INDEX_STATUS } from '../../data_frame_analytics/common'; + +import { euiDataGridStyle, euiDataGridToolbarSettings } from './common'; +import { UseIndexDataReturnType } from './types'; + +export const DataGridTitle: FC<{ title: string }> = ({ title }) => ( + + {title} + +); + +interface PropsWithoutHeader extends UseIndexDataReturnType { + dataTestSubj: string; + toastNotifications: CoreSetup['notifications']['toasts']; +} + +interface PropsWithHeader extends PropsWithoutHeader { + copyToClipboard: string; + copyToClipboardDescription: string; + title: string; +} + +function isWithHeader(arg: any): arg is PropsWithHeader { + return typeof arg?.title === 'string' && arg?.title !== ''; +} + +type Props = PropsWithHeader | PropsWithoutHeader; + +export const DataGrid: FC = props => { + const { + columns, + dataTestSubj, + errorMessage, + invalidSortingColumnns, + noDataMessage, + onChangeItemsPerPage, + onChangePage, + onSort, + pagination, + setVisibleColumns, + renderCellValue, + rowCount, + sortingColumns, + status, + tableItems: data, + toastNotifications, + visibleColumns, + } = props; + + useEffect(() => { + if (invalidSortingColumnns.length > 0) { + invalidSortingColumnns.forEach(columnId => { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataGrid.invalidSortingColumnError', { + defaultMessage: `The column '{columnId}' cannot be used for sorting.`, + values: { columnId }, + }) + ); + }); + } + }, [invalidSortingColumnns, toastNotifications]); + + if (status === INDEX_STATUS.LOADED && data.length === 0) { + return ( +
+ {isWithHeader(props) && } + +

+ {i18n.translate('xpack.ml.dataGrid.IndexNoDataCalloutBody', { + defaultMessage: + 'The query for the index returned no results. Please make sure you have sufficient permissions, the index contains documents and your query is not too restrictive.', + })} +

+
+
+ ); + } + + if (noDataMessage !== '') { + return ( +
+ {isWithHeader(props) && } + +

{noDataMessage}

+
+
+ ); + } + + return ( +
+ {isWithHeader(props) && ( + + + + + + + {(copy: () => void) => ( + + )} + + + + )} + {status === INDEX_STATUS.ERROR && ( +
+ + + {errorMessage} + + + +
+ )} + +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts new file mode 100644 index 0000000000000..2472878d1b0c1 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { + getDataGridSchemasFromFieldTypes, + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + multiColumnSortFactory, + useRenderCellValue, +} from './common'; +export { useDataGrid } from './use_data_grid'; +export { DataGrid } from './data_grid'; +export { + DataGridItem, + EsSorting, + RenderCellValue, + SearchResponse7, + UseDataGridReturnType, + UseIndexDataReturnType, +} from './types'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts new file mode 100644 index 0000000000000..5fa038edf7815 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/types.ts @@ -0,0 +1,98 @@ +/* + * 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 { Dispatch, SetStateAction } from 'react'; +import { SearchResponse } from 'elasticsearch'; + +import { EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; + +import { Dictionary } from '../../../../common/types/common'; + +import { INDEX_STATUS } from '../../data_frame_analytics/common/analytics'; + +export type ColumnId = string; +export type DataGridItem = Record; + +export type IndexPagination = Pick; + +export type OnChangeItemsPerPage = (pageSize: any) => void; +export type OnChangePage = (pageIndex: any) => void; +export type OnSort = ( + sc: Array<{ + id: string; + direction: 'asc' | 'desc'; + }> +) => void; + +export type RenderCellValue = ({ + rowIndex, + columnId, + setCellProps, +}: { + rowIndex: number; + columnId: string; + setCellProps: any; +}) => any; + +export type EsSorting = Dictionary<{ + order: 'asc' | 'desc'; +}>; + +// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. +export interface SearchResponse7 extends SearchResponse { + hits: SearchResponse['hits'] & { + total: { + value: number; + relation: string; + }; + }; +} + +export interface UseIndexDataReturnType + extends Pick< + UseDataGridReturnType, + | 'errorMessage' + | 'invalidSortingColumnns' + | 'noDataMessage' + | 'onChangeItemsPerPage' + | 'onChangePage' + | 'onSort' + | 'pagination' + | 'setPagination' + | 'setVisibleColumns' + | 'rowCount' + | 'sortingColumns' + | 'status' + | 'tableItems' + | 'visibleColumns' + > { + columns: EuiDataGridColumn[]; + renderCellValue: RenderCellValue; +} + +export interface UseDataGridReturnType { + errorMessage: string; + invalidSortingColumnns: ColumnId[]; + noDataMessage: string; + onChangeItemsPerPage: OnChangeItemsPerPage; + onChangePage: OnChangePage; + onSort: OnSort; + pagination: IndexPagination; + resetPagination: () => void; + rowCount: number; + setErrorMessage: Dispatch>; + setNoDataMessage: Dispatch>; + setPagination: Dispatch>; + setRowCount: Dispatch>; + setSortingColumns: Dispatch>; + setStatus: Dispatch>; + setTableItems: Dispatch>; + setVisibleColumns: Dispatch>; + sortingColumns: EuiDataGridSorting['columns']; + status: INDEX_STATUS; + tableItems: DataGridItem[]; + visibleColumns: ColumnId[]; +} diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts new file mode 100644 index 0000000000000..c7c4f46031b6e --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts @@ -0,0 +1,112 @@ +/* + * 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 { useCallback, useEffect, useState } from 'react'; + +import { EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; + +import { INDEX_STATUS } from '../../data_frame_analytics/common'; + +import { INIT_MAX_COLUMNS } from './common'; +import { + ColumnId, + DataGridItem, + IndexPagination, + OnChangeItemsPerPage, + OnChangePage, + OnSort, + UseDataGridReturnType, +} from './types'; + +export const useDataGrid = ( + columns: EuiDataGridColumn[], + defaultPageSize = 5, + defaultVisibleColumnsCount = INIT_MAX_COLUMNS, + defaultVisibleColumnsFilter?: (id: string) => boolean +): UseDataGridReturnType => { + const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: defaultPageSize }; + + const [noDataMessage, setNoDataMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const [status, setStatus] = useState(INDEX_STATUS.UNUSED); + const [rowCount, setRowCount] = useState(0); + const [tableItems, setTableItems] = useState([]); + const [pagination, setPagination] = useState(defaultPagination); + const [sortingColumns, setSortingColumns] = useState([]); + + const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback(pageSize => { + setPagination(p => { + const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); + return { pageIndex, pageSize }; + }); + }, []); + + const onChangePage: OnChangePage = useCallback( + pageIndex => setPagination(p => ({ ...p, pageIndex })), + [] + ); + + const resetPagination = () => setPagination(defaultPagination); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState([]); + + const columnIds = columns.map(c => c.id); + const filteredColumnIds = + defaultVisibleColumnsFilter !== undefined + ? columnIds.filter(defaultVisibleColumnsFilter) + : columnIds; + const defaultVisibleColumns = filteredColumnIds.splice(0, defaultVisibleColumnsCount); + + useEffect(() => { + setVisibleColumns(defaultVisibleColumns); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [defaultVisibleColumns.join()]); + + const [invalidSortingColumnns, setInvalidSortingColumnns] = useState([]); + + const onSort: OnSort = useCallback( + sc => { + // Check if an unsupported column type for sorting was selected. + const updatedInvalidSortingColumnns = sc.reduce((arr, current) => { + const columnType = columns.find(dgc => dgc.id === current.id); + if (columnType?.schema === 'json') { + arr.push(current.id); + } + return arr; + }, []); + setInvalidSortingColumnns(updatedInvalidSortingColumnns); + if (updatedInvalidSortingColumnns.length === 0) { + setSortingColumns(sc); + } + }, + [columns] + ); + + return { + errorMessage, + invalidSortingColumnns, + noDataMessage, + onChangeItemsPerPage, + onChangePage, + onSort, + pagination, + resetPagination, + rowCount, + setErrorMessage, + setNoDataMessage, + setPagination, + setRowCount, + setSortingColumns, + setStatus, + setTableItems, + setVisibleColumns, + sortingColumns, + status, + tableItems, + visibleColumns, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts new file mode 100644 index 0000000000000..51b2918012c8d --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts @@ -0,0 +1,10 @@ +/* + * 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 const DEFAULT_RESULTS_FIELD = 'ml'; +export const FEATURE_IMPORTANCE = 'feature_importance'; +export const FEATURE_INFLUENCE = 'feature_influence'; +export const OUTLIER_SCORE = 'outlier_score'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/data_grid.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/data_grid.ts deleted file mode 100644 index 2b6d733837562..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/data_grid.ts +++ /dev/null @@ -1,23 +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 { EuiDataGridStyle } from '@elastic/eui'; - -export const euiDataGridStyle: EuiDataGridStyle = { - border: 'all', - fontSize: 's', - cellPadding: 's', - stripes: false, - rowHover: 'none', - header: 'shade', -}; - -export const euiDataGridToolbarSettings = { - showColumnSelector: true, - showStyleSelector: false, - showSortSelector: true, - showFullScreenSelector: false, -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts index f165669bdd674..8423bc1b94a09 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -4,18 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getNestedProperty } from '../../util/object_utils'; import { - DataFrameAnalyticsConfig, getNumTopFeatureImportanceValues, getPredictedFieldName, getDependentVar, getPredictionFieldName, + isClassificationAnalysis, + isOutlierAnalysis, + isRegressionAnalysis, + DataFrameAnalyticsConfig, } from './analytics'; import { Field } from '../../../../common/types/fields'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; import { newJobCapsService } from '../../services/new_job_capabilities_service'; +import { FEATURE_IMPORTANCE, FEATURE_INFLUENCE, OUTLIER_SCORE } from './constants'; + export type EsId = string; export type EsDocSource = Record; export type EsFieldName = string; @@ -42,7 +46,7 @@ export const EXTENDED_NUMERICAL_TYPES = new Set([ ES_FIELD_TYPES.SCALED_FLOAT, ]); -const ML__ID_COPY = 'ml__id_copy'; +export const ML__ID_COPY = 'ml__id_copy'; export const isKeywordAndTextType = (fieldName: string): boolean => { const { fields } = newJobCapsService; @@ -64,32 +68,61 @@ export const isKeywordAndTextType = (fieldName: string): boolean => { }; // Used to sort columns: +// - Anchor on the left ml.outlier_score, ml.is_training, , // - string based columns are moved to the left -// - followed by the outlier_score column -// - feature_influence fields get moved next to the corresponding field column +// - feature_influence/feature_importance fields get moved next to the corresponding field column // - overall fields get sorted alphabetically -export const sortColumns = (obj: EsDocSource, resultsField: string) => (a: string, b: string) => { - const typeofA = typeof obj[a]; - const typeofB = typeof obj[b]; +export const sortExplorationResultsFields = ( + a: string, + b: string, + jobConfig: DataFrameAnalyticsConfig +) => { + const resultsField = jobConfig.dest.results_field; - if (typeofA !== 'string' && typeofB === 'string') { - return 1; - } - if (typeofA === 'string' && typeofB !== 'string') { - return -1; - } - if (typeofA === 'string' && typeofB === 'string') { - return a.localeCompare(b); - } + if (isOutlierAnalysis(jobConfig.analysis)) { + if (a === `${resultsField}.${OUTLIER_SCORE}`) { + return -1; + } - if (a === `${resultsField}.outlier_score`) { - return -1; + if (b === `${resultsField}.${OUTLIER_SCORE}`) { + return 1; + } } - if (b === `${resultsField}.outlier_score`) { - return 1; + if (isClassificationAnalysis(jobConfig.analysis) || isRegressionAnalysis(jobConfig.analysis)) { + const dependentVariable = getDependentVar(jobConfig.analysis); + const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); + + if (a === `${resultsField}.is_training`) { + return -1; + } + if (b === `${resultsField}.is_training`) { + return 1; + } + if (a === predictedField) { + return -1; + } + if (b === predictedField) { + return 1; + } + if (a === dependentVariable || a === dependentVariable.replace(/\.keyword$/, '')) { + return -1; + } + if (b === dependentVariable || b === dependentVariable.replace(/\.keyword$/, '')) { + return 1; + } + + if (a === `${resultsField}.prediction_probability`) { + return -1; + } + if (b === `${resultsField}.prediction_probability`) { + return 1; + } } + const typeofA = typeof a; + const typeofB = typeof b; + const tokensA = a.split('.'); const prefixA = tokensA[0]; const tokensB = b.split('.'); @@ -109,91 +142,6 @@ export const sortColumns = (obj: EsDocSource, resultsField: string) => (a: strin return a.localeCompare(tokensB.join('.')); } - return a.localeCompare(b); -}; - -export const sortRegressionResultsFields = ( - a: string, - b: string, - jobConfig: DataFrameAnalyticsConfig -) => { - const dependentVariable = getDependentVar(jobConfig.analysis); - const resultsField = jobConfig.dest.results_field; - const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); - if (a === `${resultsField}.is_training`) { - return -1; - } - if (b === `${resultsField}.is_training`) { - return 1; - } - if (a === predictedField) { - return -1; - } - if (b === predictedField) { - return 1; - } - if (a === dependentVariable || a === dependentVariable.replace(/\.keyword$/, '')) { - return -1; - } - if (b === dependentVariable || b === dependentVariable.replace(/\.keyword$/, '')) { - return 1; - } - - if (a === `${resultsField}.prediction_probability`) { - return -1; - } - if (b === `${resultsField}.prediction_probability`) { - return 1; - } - - return a.localeCompare(b); -}; - -// Used to sort columns: -// Anchor on the left ml.is_training, , -export const sortRegressionResultsColumns = ( - obj: EsDocSource, - jobConfig: DataFrameAnalyticsConfig -) => (a: string, b: string) => { - const dependentVariable = getDependentVar(jobConfig.analysis); - const resultsField = jobConfig.dest.results_field; - const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); - - const typeofA = typeof obj[a]; - const typeofB = typeof obj[b]; - - if (a === `${resultsField}.is_training`) { - return -1; - } - - if (b === `${resultsField}.is_training`) { - return 1; - } - - if (a === predictedField) { - return -1; - } - - if (b === predictedField) { - return 1; - } - - if (a === dependentVariable) { - return -1; - } - - if (b === dependentVariable) { - return 1; - } - - if (a === `${resultsField}.prediction_probability`) { - return -1; - } - - if (b === `${resultsField}.prediction_probability`) { - return 1; - } - if (typeofA !== 'string' && typeofB === 'string') { return 1; } @@ -204,44 +152,9 @@ export const sortRegressionResultsColumns = ( return a.localeCompare(b); } - const tokensA = a.split('.'); - const prefixA = tokensA[0]; - const tokensB = b.split('.'); - const prefixB = tokensB[0]; - - if (prefixA === resultsField && tokensA.length > 1 && prefixB !== resultsField) { - tokensA.shift(); - tokensA.shift(); - if (tokensA.join('.') === b) return 1; - return tokensA.join('.').localeCompare(b); - } - - if (prefixB === resultsField && tokensB.length > 1 && prefixA !== resultsField) { - tokensB.shift(); - tokensB.shift(); - if (tokensB.join('.') === a) return -1; - return a.localeCompare(tokensB.join('.')); - } - return a.localeCompare(b); }; -export function getFlattenedFields(obj: EsDocSource, resultsField: string): EsFieldName[] { - const flatDocFields: EsFieldName[] = []; - const newDocFields = Object.keys(obj); - newDocFields.forEach(f => { - const fieldValue = getNestedProperty(obj, f); - if (typeof fieldValue !== 'object' || fieldValue === null || Array.isArray(fieldValue)) { - flatDocFields.push(f); - } else { - const innerFields = getFlattenedFields(fieldValue, resultsField); - const flattenedFields = innerFields.map(d => `${f}.${d}`); - flatDocFields.push(...flattenedFields); - } - }); - return flatDocFields.filter(f => f !== ML__ID_COPY); -} - export const getDefaultFieldsFromJobCaps = ( fields: Field[], jobConfig: DataFrameAnalyticsConfig, @@ -259,49 +172,72 @@ export const getDefaultFieldsFromJobCaps = ( return fieldsObj; } - const dependentVariable = getDependentVar(jobConfig.analysis); - const type = newJobCapsService.getFieldById(dependentVariable)?.type; - const predictionFieldName = getPredictionFieldName(jobConfig.analysis); - const numTopFeatureImportanceValues = getNumTopFeatureImportanceValues(jobConfig.analysis); // default is 'ml' const resultsField = jobConfig.dest.results_field; - const defaultPredictionField = `${dependentVariable}_prediction`; - const predictedField = `${resultsField}.${ - predictionFieldName ? predictionFieldName : defaultPredictionField - }`; - const featureImportanceFields = []; - - if ((numTopFeatureImportanceValues ?? 0) > 0) { - featureImportanceFields.push({ - id: `${resultsField}.feature_importance`, - name: `${resultsField}.feature_importance`, - type: KBN_FIELD_TYPES.NUMBER, - }); + const featureInfluenceFields = []; + const allFields: any = []; + let type: ES_FIELD_TYPES | undefined; + let predictedField: string | undefined; + + if (isOutlierAnalysis(jobConfig.analysis)) { + // Only need to add these fields if we didn't use dest index pattern to get the fields + if (needsDestIndexFields === true) { + allFields.push({ + id: `${resultsField}.${OUTLIER_SCORE}`, + name: `${resultsField}.${OUTLIER_SCORE}`, + type: KBN_FIELD_TYPES.NUMBER, + }); + + featureInfluenceFields.push( + ...fields + .filter(d => !jobConfig.analyzed_fields.excludes.includes(d.id)) + .map(d => ({ + id: `${resultsField}.${FEATURE_INFLUENCE}.${d.id}`, + name: `${resultsField}.${FEATURE_INFLUENCE}.${d.name}`, + type: KBN_FIELD_TYPES.NUMBER, + })) + ); + } } - let allFields: any = []; - // Only need to add these fields if we didn't use dest index pattern to get the fields - if (needsDestIndexFields === true) { - allFields.push( - { - id: `${resultsField}.is_training`, - name: `${resultsField}.is_training`, - type: ES_FIELD_TYPES.BOOLEAN, - }, - { id: predictedField, name: predictedField, type } - ); + if (isClassificationAnalysis(jobConfig.analysis) || isRegressionAnalysis(jobConfig.analysis)) { + const dependentVariable = getDependentVar(jobConfig.analysis); + type = newJobCapsService.getFieldById(dependentVariable)?.type; + const predictionFieldName = getPredictionFieldName(jobConfig.analysis); + const numTopFeatureImportanceValues = getNumTopFeatureImportanceValues(jobConfig.analysis); + + const defaultPredictionField = `${dependentVariable}_prediction`; + predictedField = `${resultsField}.${ + predictionFieldName ? predictionFieldName : defaultPredictionField + }`; + + if ((numTopFeatureImportanceValues ?? 0) > 0 && needsDestIndexFields === true) { + featureImportanceFields.push({ + id: `${resultsField}.${FEATURE_IMPORTANCE}`, + name: `${resultsField}.${FEATURE_IMPORTANCE}`, + type: KBN_FIELD_TYPES.UNKNOWN, + }); + } + + // Only need to add these fields if we didn't use dest index pattern to get the fields + if (needsDestIndexFields === true) { + allFields.push( + { + id: `${resultsField}.is_training`, + name: `${resultsField}.is_training`, + type: ES_FIELD_TYPES.BOOLEAN, + }, + { id: predictedField, name: predictedField, type } + ); + } } - allFields.push(...fields, ...featureImportanceFields); + allFields.push(...fields, ...featureImportanceFields, ...featureInfluenceFields); allFields.sort(({ name: a }: { name: string }, { name: b }: { name: string }) => - sortRegressionResultsFields(a, b, jobConfig) + sortExplorationResultsFields(a, b, jobConfig) ); - // Remove feature_importance fields provided by dest index since feature_importance is an array the path is not valid - if (needsDestIndexFields === false) { - allFields = allFields.filter((field: any) => !field.name.includes('.feature_importance.')); - } let selectedFields = allFields.filter( (field: any) => field.name === predictedField || !field.name.includes('.keyword') @@ -317,145 +253,3 @@ export const getDefaultFieldsFromJobCaps = ( depVarType: type, }; }; - -export const getDefaultClassificationFields = ( - docs: EsDoc[], - jobConfig: DataFrameAnalyticsConfig -): EsFieldName[] => { - if (docs.length === 0) { - return []; - } - const resultsField = jobConfig.dest.results_field; - const newDocFields = getFlattenedFields(docs[0]._source, resultsField); - return newDocFields - .filter(k => { - if (k === `${resultsField}.is_training`) { - return true; - } - // predicted value of dependent variable - if (k === getPredictedFieldName(resultsField, jobConfig.analysis, true)) { - return true; - } - // actual value of dependent variable - if (k === getDependentVar(jobConfig.analysis)) { - return true; - } - - if (k === `${resultsField}.prediction_probability`) { - return true; - } - - if (k.split('.')[0] === resultsField) { - return false; - } - - return docs.some(row => row._source[k] !== null); - }) - .sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)) - .slice(0, DEFAULT_REGRESSION_COLUMNS); -}; - -export const getDefaultRegressionFields = ( - docs: EsDoc[], - jobConfig: DataFrameAnalyticsConfig -): EsFieldName[] => { - const resultsField = jobConfig.dest.results_field; - if (docs.length === 0) { - return []; - } - - const newDocFields = getFlattenedFields(docs[0]._source, resultsField); - return newDocFields - .filter(k => { - if (k === `${resultsField}.is_training`) { - return true; - } - // predicted value of dependent variable - if (k === getPredictedFieldName(resultsField, jobConfig.analysis)) { - return true; - } - // actual value of dependent variable - if (k === getDependentVar(jobConfig.analysis)) { - return true; - } - if (k.split('.')[0] === resultsField) { - return false; - } - - return docs.some(row => row._source[k] !== null); - }) - .sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)) - .slice(0, DEFAULT_REGRESSION_COLUMNS); -}; - -export const getDefaultSelectableFields = (docs: EsDoc[], resultsField: string): EsFieldName[] => { - if (docs.length === 0) { - return []; - } - - const newDocFields = getFlattenedFields(docs[0]._source, resultsField); - return newDocFields.filter(k => { - if (k === `${resultsField}.outlier_score`) { - return true; - } - if (k.split('.')[0] === resultsField) { - return false; - } - - return docs.some(row => row._source[k] !== null); - }); -}; - -export const toggleSelectedFieldSimple = ( - selectedFields: EsFieldName[], - column: EsFieldName -): EsFieldName[] => { - const index = selectedFields.indexOf(column); - - if (index === -1) { - selectedFields.push(column); - } else { - selectedFields.splice(index, 1); - } - return selectedFields; -}; -// Fields starting with 'ml' or custom result name not included in newJobCapsService fields so -// need to recreate the field with correct type and add to selected fields -export const toggleSelectedField = ( - selectedFields: Field[], - column: EsFieldName, - resultsField: string, - depVarType?: ES_FIELD_TYPES -): Field[] => { - const index = selectedFields.map(field => field.name).indexOf(column); - if (index === -1) { - const columnField = newJobCapsService.getFieldById(column); - if (columnField !== null) { - selectedFields.push(columnField); - } else { - const resultFieldPattern = `^${resultsField}\.`; - const regex = new RegExp(resultFieldPattern); - const isResultField = column.match(regex) !== null; - let newField; - - if (isResultField && column.includes('is_training')) { - newField = { - id: column, - name: column, - type: ES_FIELD_TYPES.BOOLEAN, - }; - } else if (isResultField && depVarType !== undefined) { - newField = { - id: column, - name: column, - type: depVarType, - }; - } - - if (newField) selectedFields.push(newField); - } - } else { - selectedFields.splice(index, 1); - } - return selectedFields; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts new file mode 100644 index 0000000000000..87b8c15aeaa78 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.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 { getErrorMessage } from '../../../../common/util/errors'; + +import { EsSorting, SearchResponse7, UseDataGridReturnType } from '../../components/data_grid'; +import { ml } from '../../services/ml_api_service'; + +import { isKeywordAndTextType } from '../common/fields'; +import { SavedSearchQuery } from '../../contexts/ml'; + +import { DataFrameAnalyticsConfig, INDEX_STATUS } from './analytics'; + +export const getIndexData = async ( + jobConfig: DataFrameAnalyticsConfig | undefined, + dataGrid: UseDataGridReturnType, + searchQuery: SavedSearchQuery +) => { + if (jobConfig !== undefined) { + const { + pagination, + setErrorMessage, + setRowCount, + setStatus, + setTableItems, + sortingColumns, + } = dataGrid; + + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + try { + const sort: EsSorting = sortingColumns + .map(column => { + const { id } = column; + column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; + return column; + }) + .reduce((s, column) => { + s[column.id] = { order: column.direction }; + return s; + }, {} as EsSorting); + + const { pageIndex, pageSize } = pagination; + const resp: SearchResponse7 = await ml.esSearch({ + index: jobConfig.dest.index, + body: { + query: searchQuery, + from: pageIndex * pageSize, + size: pageSize, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, + }); + + setRowCount(resp.hits.total.value); + + const docs = resp.hits.hits.map(d => d._source); + setTableItems(docs); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setStatus(INDEX_STATUS.ERROR); + } + } +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts new file mode 100644 index 0000000000000..12ae4a586e949 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts @@ -0,0 +1,49 @@ +/* + * 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 { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; + +import { newJobCapsService } from '../../services/new_job_capabilities_service'; + +import { getDefaultFieldsFromJobCaps, DataFrameAnalyticsConfig } from '../common'; + +export interface FieldTypes { + [key: string]: ES_FIELD_TYPES; +} + +export const getIndexFields = ( + jobConfig: DataFrameAnalyticsConfig | undefined, + needsDestIndexFields: boolean +) => { + const { fields } = newJobCapsService; + if (jobConfig !== undefined) { + const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( + fields, + jobConfig, + needsDestIndexFields + ); + + const types: FieldTypes = {}; + const allFields: string[] = []; + + docFields.forEach(field => { + types[field.id] = field.type; + allFields.push(field.id); + }); + + return { + defaultSelectedFields: defaultSelected.map(field => field.id), + fieldTypes: types, + tableFields: allFields, + }; + } else { + return { + defaultSelectedFields: [], + fieldTypes: {}, + tableFields: [], + }; + } +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index 7b76faf613ce8..400902c152c9e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -30,16 +30,8 @@ export { } from './analytics'; export { - getDefaultSelectableFields, - getDefaultRegressionFields, - getDefaultClassificationFields, getDefaultFieldsFromJobCaps, - getFlattenedFields, - sortColumns, - sortRegressionResultsColumns, - sortRegressionResultsFields, - toggleSelectedField, - toggleSelectedFieldSimple, + sortExplorationResultsFields, EsId, EsDoc, EsDocSource, @@ -47,4 +39,7 @@ export { MAX_COLUMNS, } from './fields'; -export { euiDataGridStyle, euiDataGridToolbarSettings } from './data_grid'; +export { getIndexData } from './get_index_data'; +export { getIndexFields } from './get_index_fields'; + +export { useResultsViewConfig } from './use_results_view_config'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts new file mode 100644 index 0000000000000..0bc9e78207596 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts @@ -0,0 +1,104 @@ +/* + * 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 { useEffect, useState } from 'react'; + +import { IndexPattern } from '../../../../../../../src/plugins/data/public'; + +import { getErrorMessage } from '../../../../common/util/errors'; + +import { getIndexPatternIdFromName } from '../../util/index_utils'; +import { ml } from '../../services/ml_api_service'; +import { newJobCapsService } from '../../services/new_job_capabilities_service'; +import { useMlContext } from '../../contexts/ml'; + +import { DataFrameAnalyticsConfig } from '../common'; + +import { isGetDataFrameAnalyticsStatsResponseOk } from '../pages/analytics_management/services/analytics_service/get_analytics'; +import { DATA_FRAME_TASK_STATE } from '../pages/analytics_management/components/analytics_list/common'; + +export const useResultsViewConfig = (jobId: string) => { + const mlContext = useMlContext(); + const [indexPattern, setIndexPattern] = useState(undefined); + const [isInitialized, setIsInitialized] = useState(false); + const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); + const [jobConfig, setJobConfig] = useState(undefined); + const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState( + undefined + ); + const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); + const [jobStatus, setJobStatus] = useState(undefined); + + // get analytics configuration, index pattern and field caps + useEffect(() => { + (async function() { + setIsLoadingJobConfig(false); + + try { + const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); + const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); + const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) + ? analyticsStats.data_frame_analytics[0] + : undefined; + + if (stats !== undefined && stats.state) { + setJobStatus(stats.state); + } + + if ( + Array.isArray(analyticsConfigs.data_frame_analytics) && + analyticsConfigs.data_frame_analytics.length > 0 + ) { + const jobConfigUpdate = analyticsConfigs.data_frame_analytics[0]; + + try { + const destIndex = Array.isArray(jobConfigUpdate.dest.index) + ? jobConfigUpdate.dest.index[0] + : jobConfigUpdate.dest.index; + const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; + let indexP: IndexPattern | undefined; + + try { + indexP = await mlContext.indexPatterns.get(destIndexPatternId); + } catch (e) { + indexP = undefined; + } + + if (indexP === undefined) { + const sourceIndex = jobConfigUpdate.source.index[0]; + const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; + indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); + } + + if (indexP !== undefined) { + await newJobCapsService.initializeFromIndexPattern(indexP, false, false); + setJobConfig(analyticsConfigs.data_frame_analytics[0]); + setIndexPattern(indexP); + setIsInitialized(true); + setIsLoadingJobConfig(false); + } + } catch (e) { + setJobCapsServiceErrorMessage(getErrorMessage(e)); + setIsLoadingJobConfig(false); + } + } + } catch (e) { + setJobConfigErrorMessage(getErrorMessage(e)); + setIsLoadingJobConfig(false); + } + })(); + }, []); + + return { + indexPattern, + isInitialized, + isLoadingJobConfig, + jobCapsServiceErrorMessage, + jobConfig, + jobConfigErrorMessage, + jobStatus, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx index 5c151166829ab..ccac9a697210b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -4,183 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useState, useEffect } from 'react'; -import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { ml } from '../../../../../services/ml_api_service'; -import { DataFrameAnalyticsConfig } from '../../../../common'; -import { EvaluatePanel } from './evaluate_panel'; -import { ResultsTable } from './results_table'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; -import { LoadingPanel } from '../loading_panel'; -import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { useMlContext } from '../../../../../contexts/ml'; -import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; +import React, { FC } from 'react'; -export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( - - - {i18n.translate('xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle', { - defaultMessage: 'Destination index for classification job ID {jobId}', - values: { jobId }, - })} - - -); +import { i18n } from '@kbn/i18n'; -const jobConfigErrorTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError', - { - defaultMessage: - 'Unable to fetch results. An error occurred loading the job configuration data.', - } -); +import { ExplorationPageWrapper } from '../exploration_page_wrapper'; -const jobCapsErrorTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError', - { - defaultMessage: "Unable to fetch results. An error occurred loading the index's field data.", - } -); +import { EvaluatePanel } from './evaluate_panel'; interface Props { jobId: string; } export const ClassificationExploration: FC = ({ jobId }) => { - const [jobConfig, setJobConfig] = useState(undefined); - const [jobStatus, setJobStatus] = useState(undefined); - const [indexPattern, setIndexPattern] = useState(undefined); - const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); - const [isInitialized, setIsInitialized] = useState(false); - const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); - const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState( - undefined - ); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const mlContext = useMlContext(); - - const loadJobConfig = async () => { - setIsLoadingJobConfig(true); - try { - const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); - const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); - const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) - ? analyticsStats.data_frame_analytics[0] - : undefined; - - if (stats !== undefined && stats.state) { - setJobStatus(stats.state); - } - - if ( - Array.isArray(analyticsConfigs.data_frame_analytics) && - analyticsConfigs.data_frame_analytics.length > 0 - ) { - setJobConfig(analyticsConfigs.data_frame_analytics[0]); - setIsLoadingJobConfig(false); - } else { - setJobConfigErrorMessage( - i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage', - { - defaultMessage: 'No results found.', - } - ) - ); - } - } catch (e) { - if (e.message !== undefined) { - setJobConfigErrorMessage(e.message); - } else { - setJobConfigErrorMessage(JSON.stringify(e)); - } - setIsLoadingJobConfig(false); - } - }; - - useEffect(() => { - loadJobConfig(); - }, []); - - const initializeJobCapsService = async () => { - if (jobConfig !== undefined) { - try { - const destIndex = Array.isArray(jobConfig.dest.index) - ? jobConfig.dest.index[0] - : jobConfig.dest.index; - const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; - let indexP: IndexPattern | undefined; - - try { - indexP = await mlContext.indexPatterns.get(destIndexPatternId); - } catch (e) { - indexP = undefined; - } - - if (indexP === undefined) { - const sourceIndex = jobConfig.source.index[0]; - const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); - } - - if (indexP !== undefined) { - setIndexPattern(indexP); - await newJobCapsService.initializeFromIndexPattern(indexP, false, false); - } - setIsInitialized(true); - } catch (e) { - if (e.message !== undefined) { - setJobCapsServiceErrorMessage(e.message); - } else { - setJobCapsServiceErrorMessage(JSON.stringify(e)); - } - } - } - }; - - useEffect(() => { - initializeJobCapsService(); - }, [jobConfig && jobConfig.id]); - - if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { - return ( - - - - -

{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}

-
-
- ); - } - return ( - - {isLoadingJobConfig === true && jobConfig === undefined && } - {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && ( - + - {isLoadingJobConfig === true && jobConfig === undefined && } - {isLoadingJobConfig === false && - jobConfig !== undefined && - indexPattern !== undefined && - isInitialized === true && ( - - )} - + EvaluatePanel={EvaluatePanel} + /> ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration_data_grid.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration_data_grid.tsx deleted file mode 100644 index 424fc002795ca..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration_data_grid.tsx +++ /dev/null @@ -1,135 +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 React, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common'; - -import { mlFieldFormatService } from '../../../../../services/field_format_service'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -type Pagination = Pick; -type TableItem = Record; - -interface ExplorationDataGridProps { - colorRange?: (d: number) => string; - columns: any[]; - indexPattern: IndexPattern; - pagination: Pagination; - resultsField: string; - rowCount: number; - selectedFields: string[]; - setPagination: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - tableItems: TableItem[]; -} - -export const ClassificationExplorationDataGrid: FC = ({ - columns, - indexPattern, - pagination, - resultsField, - rowCount, - selectedFields, - setPagination, - setSelectedFields, - setSortingColumns, - sortingColumns, - tableItems, -}) => { - const renderCellValue = useMemo(() => { - return ({ rowIndex, columnId }: { rowIndex: number; columnId: string; setCellProps: any }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const fullItem = tableItems[adjustedRowIndex]; - - if (fullItem === undefined) { - return null; - } - - let format: any; - - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); - } - - const cellValue = - fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined - ? fullItem[columnId] - : null; - - if (format !== undefined) { - return format.convert(cellValue, 'text'); - } - - if (typeof cellValue === 'string' || cellValue === null) { - return cellValue; - } - - if (typeof cellValue === 'boolean') { - return cellValue ? 'true' : 'false'; - } - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - return cellValue; - }; - }, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - return ( - - ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts deleted file mode 100644 index c8809ca5e471b..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts +++ /dev/null @@ -1,292 +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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { SearchResponse } from 'elasticsearch'; -import { cloneDeep } from 'lodash'; - -import { SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; - -import { ml } from '../../../../../services/ml_api_service'; -import { getNestedProperty } from '../../../../../util/object_utils'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { isKeywordAndTextType } from '../../../../common/fields'; -import { Dictionary } from '../../../../../../../common/types/common'; -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { - defaultSearchQuery, - ResultsSearchQuery, - isResultsSearchBoolQuery, - LoadExploreDataArg, -} from '../../../../common/analytics'; - -import { - getDefaultFieldsFromJobCaps, - getDependentVar, - getFlattenedFields, - getPredictedFieldName, - DataFrameAnalyticsConfig, - EsFieldName, - INDEX_STATUS, -} from '../../../../common'; -import { SavedSearchQuery } from '../../../../../contexts/ml'; - -export type TableItem = Record; -type Pagination = Pick; - -export interface UseExploreDataReturnType { - errorMessage: string; - fieldTypes: { [key: string]: ES_FIELD_TYPES }; - pagination: Pagination; - rowCount: number; - searchQuery: SavedSearchQuery; - selectedFields: EsFieldName[]; - setFilterByIsTraining: Dispatch>; - setPagination: Dispatch>; - setSearchQuery: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - status: INDEX_STATUS; - tableFields: string[]; - tableItems: TableItem[]; -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -export const useExploreData = ( - jobConfig: DataFrameAnalyticsConfig, - needsDestIndexFields: boolean -): UseExploreDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); - const [tableFields, setTableFields] = useState([]); - const [tableItems, setTableItems] = useState([]); - const [fieldTypes, setFieldTypes] = useState<{ [key: string]: ES_FIELD_TYPES }>({}); - const [rowCount, setRowCount] = useState(0); - - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const [filterByIsTraining, setFilterByIsTraining] = useState(undefined); - const [sortingColumns, setSortingColumns] = useState([]); - - const predictedFieldName = getPredictedFieldName( - jobConfig.dest.results_field, - jobConfig.analysis - ); - const dependentVariable = getDependentVar(jobConfig.analysis); - - const getDefaultSelectedFields = () => { - const { fields } = newJobCapsService; - if (selectedFields.length === 0 && jobConfig !== undefined) { - const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( - fields, - jobConfig, - needsDestIndexFields - ); - - const types: { [key: string]: ES_FIELD_TYPES } = {}; - const allFields: string[] = []; - - docFields.forEach(field => { - types[field.id] = field.type; - allFields.push(field.id); - }); - - setFieldTypes(types); - setSelectedFields(defaultSelected.map(field => field.id)); - setTableFields(allFields); - } - }; - - const loadExploreData = async ({ - filterByIsTraining: isTraining, - searchQuery: incomingQuery, - }: LoadExploreDataArg) => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const resultsField = jobConfig.dest.results_field; - const searchQueryClone: ResultsSearchQuery = cloneDeep(incomingQuery); - let query: ResultsSearchQuery; - const { pageIndex, pageSize } = pagination; - // If filterByIsTraining is defined - add that in to the final query - const trainingQuery = - isTraining !== undefined - ? { - term: { [`${resultsField}.is_training`]: { value: isTraining } }, - } - : undefined; - - if (JSON.stringify(incomingQuery) === JSON.stringify(defaultSearchQuery)) { - const existsQuery = { - exists: { - field: resultsField, - }, - }; - - query = { - bool: { - must: [existsQuery], - }, - }; - - if (trainingQuery !== undefined && isResultsSearchBoolQuery(query)) { - query.bool.must.push(trainingQuery); - } - } else if (isResultsSearchBoolQuery(searchQueryClone)) { - if (searchQueryClone.bool.must === undefined) { - searchQueryClone.bool.must = []; - } - - searchQueryClone.bool.must.push({ - exists: { - field: resultsField, - }, - }); - - if (trainingQuery !== undefined) { - searchQueryClone.bool.must.push(trainingQuery); - } - - query = searchQueryClone; - } else { - query = searchQueryClone; - } - - const sort: EsSorting = sortingColumns - .map(column => { - const { id } = column; - column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; - return column; - }) - .reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const resp: SearchResponse7 = await ml.esSearch({ - index: jobConfig.dest.index, - body: { - query, - from: pageIndex * pageSize, - size: pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }); - - setRowCount(resp.hits.total.value); - - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(INDEX_STATUS.LOADED); - return; - } - - // Create a version of the doc's source with flattened field names. - // This avoids confusion later on if a field name has dots in its name - // or is a nested fields when displaying it via EuiInMemoryTable. - const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); - const transformedTableItems = docs.map(doc => { - const item: TableItem = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - if (item[ff] === undefined) { - const parts = ff.split('.'); - if (parts[0] === resultsField && parts.length >= 2) { - parts.shift(); - if (doc._source[resultsField] !== undefined) { - item[ff] = doc._source[resultsField][parts.join('.')]; - } - } - } - }); - return item; - }); - - setTableItems(transformedTableItems); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e)); - } - setTableItems([]); - setStatus(INDEX_STATUS.ERROR); - } - } - }; - - useEffect(() => { - getDefaultSelectedFields(); - }, [jobConfig && jobConfig.id]); - - // By default set sorting to descending on the prediction field (`_prediction`). - useEffect(() => { - const sortByField = isKeywordAndTextType(dependentVariable) - ? `${predictedFieldName}.keyword` - : predictedFieldName; - const direction = SORT_DIRECTION.DESC; - - setSortingColumns([{ id: sortByField, direction }]); - }, [jobConfig && jobConfig.id]); - - useEffect(() => { - loadExploreData({ filterByIsTraining, searchQuery }); - }, [ - filterByIsTraining, - jobConfig && jobConfig.id, - pagination, - searchQuery, - selectedFields, - sortingColumns, - ]); - - return { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setFilterByIsTraining, - setPagination, - setSelectedFields, - setSortingColumns, - setSearchQuery, - sortingColumns, - status, - tableItems, - tableFields, - }; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx index 9765192f0e446..839587c47289a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx @@ -15,7 +15,7 @@ interface Props { export const ErrorCallout: FC = ({ error }) => { let errorCallout = ( = ({ error }) => { if (error.includes('index_not_found')) { errorCallout = (

- {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.noIndexCalloutBody', { + {i18n.translate('xpack.ml.dataframe.analytics.errorCallout.noIndexCalloutBody', { defaultMessage: 'The query for the index returned no results. Please make sure the destination index exists and contains documents.', })} @@ -46,16 +46,13 @@ export const ErrorCallout: FC = ({ error }) => { // Job was started but no results have been written yet errorCallout = (

- {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody', { + {i18n.translate('xpack.ml.dataframe.analytics.errorCallout.noDataCalloutBody', { defaultMessage: 'The query for the index returned no results. Please make sure the job has completed and the index contains documents.', })} @@ -66,22 +63,16 @@ export const ErrorCallout: FC = ({ error }) => { // query bar syntax is incorrect errorCallout = (

- {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorBody', - { - defaultMessage: - 'The query syntax is invalid and returned no results. Please check the query syntax and try again.', - } - )} + {i18n.translate('xpack.ml.dataframe.analytics.errorCallout.queryParsingErrorBody', { + defaultMessage: + 'The query syntax is invalid and returned no results. Please check the query syntax and try again.', + })}

); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/exploration_data_grid.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/exploration_data_grid.tsx deleted file mode 100644 index e88bc1cd06f95..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/exploration_data_grid.tsx +++ /dev/null @@ -1,161 +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 React, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common'; - -import { mlFieldFormatService } from '../../../../../services/field_format_service'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -const FEATURE_INFLUENCE = 'feature_influence'; -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -type Pagination = Pick; -type TableItem = Record; - -interface ExplorationDataGridProps { - colorRange: (d: number) => string; - columns: any[]; - indexPattern: IndexPattern; - pagination: Pagination; - resultsField: string; - rowCount: number; - selectedFields: string[]; - setPagination: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - tableItems: TableItem[]; -} - -export const ExplorationDataGrid: FC = ({ - colorRange, - columns, - indexPattern, - pagination, - resultsField, - rowCount, - selectedFields, - setPagination, - setSelectedFields, - setSortingColumns, - sortingColumns, - tableItems, -}) => { - const renderCellValue = useMemo(() => { - return ({ - rowIndex, - columnId, - setCellProps, - }: { - rowIndex: number; - columnId: string; - setCellProps: any; - }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const fullItem = tableItems[adjustedRowIndex]; - - if (fullItem === undefined) { - return null; - } - - let format: any; - - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); - } - - const cellValue = - fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined - ? fullItem[columnId] - : null; - - const split = columnId.split('.'); - let backgroundColor; - - // column with feature values get color coded by its corresponding influencer value - if (fullItem[`${resultsField}.${FEATURE_INFLUENCE}.${columnId}`] !== undefined) { - backgroundColor = colorRange(fullItem[`${resultsField}.${FEATURE_INFLUENCE}.${columnId}`]); - } - - // column with influencer values get color coded by its own value - if (split.length > 2 && split[0] === resultsField && split[1] === FEATURE_INFLUENCE) { - backgroundColor = colorRange(cellValue); - } - - if (backgroundColor !== undefined) { - setCellProps({ - style: { backgroundColor }, - }); - } - - if (format !== undefined) { - return format.convert(cellValue, 'text'); - } - - if (typeof cellValue === 'string' || cellValue === null) { - return cellValue; - } - - if (typeof cellValue === 'boolean') { - return cellValue ? 'true' : 'false'; - } - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - return cellValue; - }; - }, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - return ( - - ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx new file mode 100644 index 0000000000000..1986c486974c9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx @@ -0,0 +1,76 @@ +/* + * 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, { FC, useState } from 'react'; + +import { EuiSpacer } from '@elastic/eui'; + +import { useResultsViewConfig, DataFrameAnalyticsConfig } from '../../../../common'; +import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; + +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; + +import { ExplorationResultsTable } from '../exploration_results_table'; +import { JobConfigErrorCallout } from '../job_config_error_callout'; +import { LoadingPanel } from '../loading_panel'; + +export interface EvaluatePanelProps { + jobConfig: DataFrameAnalyticsConfig; + jobStatus?: DATA_FRAME_TASK_STATE; + searchQuery: ResultsSearchQuery; +} + +interface Props { + jobId: string; + title: string; + EvaluatePanel: FC; +} + +export const ExplorationPageWrapper: FC = ({ jobId, title, EvaluatePanel }) => { + const { + indexPattern, + isInitialized, + isLoadingJobConfig, + jobCapsServiceErrorMessage, + jobConfig, + jobConfigErrorMessage, + jobStatus, + } = useResultsViewConfig(jobId); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + + if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { + return ( + + ); + } + + return ( + <> + {isLoadingJobConfig === true && jobConfig === undefined && } + {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && ( + + )} + + {isLoadingJobConfig === true && jobConfig === undefined && } + {isLoadingJobConfig === false && + jobConfig !== undefined && + indexPattern !== undefined && + isInitialized === true && ( + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/index.ts similarity index 73% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/index.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/index.ts index dd896ca02f7f7..bf294a3cd08c9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { useExploreData, TableItem } from './use_explore_data'; +export { EvaluatePanelProps, ExplorationPageWrapper } from './exploration_page_wrapper'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx similarity index 57% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index bf63dfe68fe9e..24e5785c6e808 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useEffect } from 'react'; +import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiCallOut, @@ -12,17 +12,15 @@ import { EuiFlexItem, EuiFormRow, EuiPanel, - EuiProgress, EuiSpacer, EuiText, } from '@elastic/eui'; import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; -import { - BASIC_NUMERICAL_TYPES, - EXTENDED_NUMERICAL_TYPES, - sortRegressionResultsFields, -} from '../../../../common/fields'; + +import { DataGrid } from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; import { DataFrameAnalyticsConfig, @@ -33,20 +31,20 @@ import { } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { useExploreData } from './use_explore_data'; // TableItem -import { ExplorationTitle } from './classification_exploration'; -import { ClassificationExplorationDataGrid } from './classification_exploration_data_grid'; +import { ExplorationTitle } from '../exploration_title'; import { ExplorationQueryBar } from '../exploration_query_bar'; +import { useExplorationResults } from './use_exploration_results'; + const showingDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText', + 'xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText', { defaultMessage: 'Showing documents for which predictions exist', } ); const showingFirstDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText', + 'xpack.ml.dataframe.analytics.explorationResults.firstDocumentsShownHelpText', { defaultMessage: 'Showing first {searchSize} documents for which predictions exist', values: { searchSize: SEARCH_SIZE }, @@ -58,71 +56,22 @@ interface Props { jobConfig: DataFrameAnalyticsConfig; jobStatus?: DATA_FRAME_TASK_STATE; setEvaluateSearchQuery: React.Dispatch>; + title: string; } -export const ResultsTable: FC = React.memo( - ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery }) => { - const needsDestIndexFields = indexPattern && indexPattern.title === jobConfig.source.index[0]; - const resultsField = jobConfig.dest.results_field; - const { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - status, - tableFields, - tableItems, - } = useExploreData(jobConfig, needsDestIndexFields); +export const ExplorationResultsTable: FC = React.memo( + ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery, title }) => { + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); useEffect(() => { setEvaluateSearchQuery(searchQuery); }, [JSON.stringify(searchQuery)]); - const columns = tableFields - .sort((a: any, b: any) => sortRegressionResultsFields(a, b, jobConfig)) - .map((field: any) => { - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - let isSortable = true; - const type = fieldTypes[field]; - const isNumber = - type !== undefined && - (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); - - if (isNumber) { - schema = 'numeric'; - } - - switch (type) { - case 'date': - schema = 'datetime'; - break; - case 'geo_point': - schema = 'json'; - break; - case 'boolean': - schema = 'boolean'; - break; - } + const classificationData = useExplorationResults(indexPattern, jobConfig, searchQuery); + const docFieldsCount = classificationData.columns.length; + const { columns, errorMessage, status, tableItems, visibleColumns } = classificationData; - if (field === `${resultsField}.feature_importance`) { - isSortable = false; - } - - return { id: field, schema, isSortable }; - }); - - const docFieldsCount = tableFields.length; - - if (jobConfig === undefined) { + if (jobConfig === undefined || classificationData === undefined) { return null; } // if it's a searchBar syntax error leave the table visible so they can try again @@ -131,7 +80,7 @@ export const ResultsTable: FC = React.memo( - + {jobStatus !== undefined && ( @@ -156,13 +105,13 @@ export const ResultsTable: FC = React.memo( - + {jobStatus !== undefined && ( @@ -177,11 +126,11 @@ export const ResultsTable: FC = React.memo( {docFieldsCount > MAX_COLUMNS && ( {i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.fieldSelection', + 'xpack.ml.dataframe.analytics.explorationResults.fieldSelection', { defaultMessage: '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', - values: { selectedFieldsLength: selectedFields.length, docFieldsCount }, + values: { selectedFieldsLength: visibleColumns.length, docFieldsCount }, } )} @@ -190,10 +139,6 @@ export const ResultsTable: FC = React.memo( - {status === INDEX_STATUS.LOADING && } - {status !== INDEX_STATUS.LOADING && ( - - )} {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( @@ -213,18 +158,10 @@ export const ResultsTable: FC = React.memo( - diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/index.ts similarity index 77% rename from x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/index.ts index a13e678813a00..19308640c8b02 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { SourceIndexPreview } from './source_index_preview'; +export { ExplorationResultsTable } from './exploration_results_table'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts new file mode 100644 index 0000000000000..6f9dc694d8172 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts @@ -0,0 +1,72 @@ +/* + * 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 { useEffect } from 'react'; + +import { EuiDataGridColumn } from '@elastic/eui'; + +import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; + +import { + getDataGridSchemasFromFieldTypes, + useDataGrid, + useRenderCellValue, + UseIndexDataReturnType, +} from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; + +import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; +import { DEFAULT_RESULTS_FIELD, FEATURE_IMPORTANCE } from '../../../../common/constants'; +import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields'; + +export const useExplorationResults = ( + indexPattern: IndexPattern | undefined, + jobConfig: DataFrameAnalyticsConfig | undefined, + searchQuery: SavedSearchQuery +): UseIndexDataReturnType => { + const needsDestIndexFields = + indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; + + const columns: EuiDataGridColumn[] = []; + + if (jobConfig !== undefined) { + const resultsField = jobConfig.dest.results_field; + const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); + columns.push( + ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => + sortExplorationResultsFields(a.id, b.id, jobConfig) + ) + ); + } + + const dataGrid = useDataGrid( + columns, + 25, + // reduce default selected rows from 20 to 8 for performance reasons. + 8, + // by default, hide feature-importance columns and the doc id copy + d => !d.includes(`.${FEATURE_IMPORTANCE}.`) && d !== ML__ID_COPY + ); + + useEffect(() => { + getIndexData(jobConfig, dataGrid, searchQuery); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); + + const renderCellValue = useRenderCellValue( + indexPattern, + dataGrid.pagination, + dataGrid.tableItems, + jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD + ); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/exploration_title.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/exploration_title.tsx new file mode 100644 index 0000000000000..f06c88c73df71 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/exploration_title.tsx @@ -0,0 +1,15 @@ +/* + * 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, { FC } from 'react'; + +import { EuiTitle } from '@elastic/eui'; + +export const ExplorationTitle: FC<{ title: string }> = ({ title }) => ( + + {title} + +); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/index.ts similarity index 81% rename from x-pack/plugins/transform/public/app/components/pivot_preview/index.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/index.ts index 049e73d6309fc..b34e61b3b5e76 100644 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { PivotPreview } from './pivot_preview'; +export { ExplorationTitle } from './exploration_title'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/index.ts similarity index 78% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/index.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/index.ts index ea89e91de5046..a5991f4325d12 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ExplorationDataGrid } from './exploration_data_grid'; +export { JobConfigErrorCallout } from './job_config_error_callout'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx new file mode 100644 index 0000000000000..945d6654067c0 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx @@ -0,0 +1,47 @@ +/* + * 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, { FC } from 'react'; + +import { EuiCallOut, EuiPanel, EuiSpacer } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { ExplorationTitle } from '../exploration_title'; + +const jobConfigErrorTitle = i18n.translate('xpack.ml.dataframe.analytics.jobConfig.errorTitle', { + defaultMessage: 'Unable to fetch results. An error occurred loading the job configuration data.', +}); + +const jobCapsErrorTitle = i18n.translate('xpack.ml.dataframe.analytics.jobCaps.errorTitle', { + defaultMessage: "Unable to fetch results. An error occurred loading the index's field data.", +}); + +interface Props { + jobCapsServiceErrorMessage: string | undefined; + jobConfigErrorMessage: string | undefined; + title: string; +} + +export const JobConfigErrorCallout: FC = ({ + jobCapsServiceErrorMessage, + jobConfigErrorMessage, + title, +}) => { + return ( + + + + +

{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}

+
+
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.test.ts similarity index 100% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.test.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.test.ts diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts similarity index 51% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts index 48db8e34c934e..bfd3dd33995aa 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts @@ -4,9 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DataFrameAnalyticsConfig } from '../../../../common'; +import { DataGridItem } from '../../../../../components/data_grid'; -const OUTLIER_SCORE = 'outlier_score'; +import { DataFrameAnalyticsConfig } from '../../../../common'; +import { FEATURE_INFLUENCE, OUTLIER_SCORE } from '../../../../common/constants'; export const getOutlierScoreFieldName = (jobConfig: DataFrameAnalyticsConfig) => `${jobConfig.dest.results_field}.${OUTLIER_SCORE}`; + +export const getFeatureCount = (resultsField: string, tableItems: DataGridItem[] = []) => { + if (tableItems.length === 0) { + return 0; + } + + return Object.keys(tableItems[0]).filter(key => + key.includes(`${resultsField}.${FEATURE_INFLUENCE}.`) + ).length; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index fdcb7d9d237e3..0154f92576c4a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { useState, FC } from 'react'; import { i18n } from '@kbn/i18n'; @@ -15,127 +15,51 @@ import { EuiHorizontalRule, EuiPanel, EuiSpacer, - EuiTitle, } from '@elastic/eui'; import { useColorRange, - ColorRangeLegend, COLOR_RANGE, COLOR_RANGE_SCALE, } from '../../../../../components/color_range_legend'; +import { ColorRangeLegend } from '../../../../../components/color_range_legend'; +import { DataGrid } from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; -import { sortColumns, INDEX_STATUS, defaultSearchQuery } from '../../../../common'; +import { defaultSearchQuery, useResultsViewConfig, INDEX_STATUS } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; -import { useExploreData, TableItem } from '../../hooks/use_explore_data'; - -import { ExplorationDataGrid } from '../exploration_data_grid'; import { ExplorationQueryBar } from '../exploration_query_bar'; +import { ExplorationTitle } from '../exploration_title'; -const FEATURE_INFLUENCE = 'feature_influence'; +import { getFeatureCount } from './common'; +import { useOutlierData } from './use_outlier_data'; -const ExplorationTitle: FC<{ jobId: string }> = ({ jobId }) => ( - - - {i18n.translate('xpack.ml.dataframe.analytics.exploration.jobIdTitle', { - defaultMessage: 'Outlier detection job ID {jobId}', - values: { jobId }, - })} - - -); +export type TableItem = Record; interface ExplorationProps { jobId: string; } -const getFeatureCount = (resultsField: string, tableItems: TableItem[] = []) => { - if (tableItems.length === 0) { - return 0; - } - - return Object.keys(tableItems[0]).filter(key => - key.includes(`${resultsField}.${FEATURE_INFLUENCE}.`) - ).length; -}; - export const OutlierExploration: FC = React.memo(({ jobId }) => { - const { - errorMessage, - indexPattern, - jobConfig, - jobStatus, - pagination, - searchQuery, - selectedFields, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - rowCount, - status, - tableFields, - tableItems, - } = useExploreData(jobId); - - const columns = []; - - if ( - jobConfig !== undefined && - indexPattern !== undefined && - selectedFields.length > 0 && - tableItems.length > 0 - ) { - const resultsField = jobConfig.dest.results_field; - const removePrefix = new RegExp(`^${resultsField}\.${FEATURE_INFLUENCE}\.`, 'g'); - columns.push( - ...tableFields.sort(sortColumns(tableItems[0], resultsField)).map(id => { - const idWithoutPrefix = id.replace(removePrefix, ''); - const field = indexPattern.fields.getByName(idWithoutPrefix); - - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - - switch (field?.type) { - case 'date': - schema = 'datetime'; - break; - case 'geo_point': - schema = 'json'; - break; - case 'number': - schema = 'numeric'; - break; - } - - if (id === `${resultsField}.outlier_score`) { - schema = 'numeric'; - } - - return { id, schema }; - }) - ); - } + const explorationTitle = i18n.translate('xpack.ml.dataframe.analytics.exploration.jobIdTitle', { + defaultMessage: 'Outlier detection job ID {jobId}', + values: { jobId }, + }); - const colorRange = useColorRange( - COLOR_RANGE.BLUE, - COLOR_RANGE_SCALE.INFLUENCER, - jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, tableItems) : 1 - ); + const { indexPattern, jobConfig, jobStatus } = useResultsViewConfig(jobId); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + const outlierData = useOutlierData(indexPattern, jobConfig, searchQuery); - if (jobConfig === undefined || indexPattern === undefined) { - return null; - } + const { columns, errorMessage, status, tableItems } = outlierData; // if it's a searchBar syntax error leave the table visible so they can try again if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { return ( - + = React.memo(({ jobId }) = ); } - let tableError = - status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception') - ? errorMessage - : undefined; - - if (status === INDEX_STATUS.LOADED && tableItems.length === 0 && tableError === undefined) { - tableError = i18n.translate('xpack.ml.dataframe.analytics.exploration.noDataCalloutBody', { - defaultMessage: - 'The query for the index returned no results. Please make sure the index contains documents and your query is not too restrictive.', - }); - } + const colorRange = useColorRange( + COLOR_RANGE.BLUE, + COLOR_RANGE_SCALE.INFLUENCER, + jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, tableItems) : 1 + ); return ( @@ -170,7 +88,7 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = gutterSize="s" > - + {jobStatus !== undefined && ( @@ -179,7 +97,7 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = )}
- {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( + {(columns.length > 0 || searchQuery !== defaultSearchQuery) && indexPattern !== undefined && ( <> @@ -200,19 +118,10 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = {columns.length > 0 && tableItems.length > 0 && ( - )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.test.ts new file mode 100644 index 0000000000000..91a5ba2db6908 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { DataFrameAnalyticsConfig } from '../../../../common'; + +import { getOutlierScoreFieldName } from './common'; + +describe('Data Frame Analytics: common utils', () => { + test('getOutlierScoreFieldName()', () => { + const jobConfig: DataFrameAnalyticsConfig = { + id: 'the-id', + analysis: { outlier_detection: {} }, + dest: { + index: 'the-dest-index', + results_field: 'the-results-field', + }, + source: { + index: 'the-source-index', + }, + analyzed_fields: { includes: [], excludes: [] }, + model_memory_limit: '50mb', + create_time: 1234, + version: '1.0.0', + }; + + const outlierScoreFieldName = getOutlierScoreFieldName(jobConfig); + + expect(outlierScoreFieldName).toMatch('the-results-field.outlier_score'); + }); +}); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts new file mode 100644 index 0000000000000..0d06bc0d43307 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -0,0 +1,117 @@ +/* + * 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 { useEffect } from 'react'; + +import { EuiDataGridColumn } from '@elastic/eui'; + +import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; + +import { + useColorRange, + COLOR_RANGE, + COLOR_RANGE_SCALE, +} from '../../../../../components/color_range_legend'; +import { + getDataGridSchemasFromFieldTypes, + useDataGrid, + useRenderCellValue, + UseIndexDataReturnType, +} from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; + +import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; +import { DEFAULT_RESULTS_FIELD, FEATURE_INFLUENCE } from '../../../../common/constants'; +import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields'; + +import { getFeatureCount, getOutlierScoreFieldName } from './common'; + +export const useOutlierData = ( + indexPattern: IndexPattern | undefined, + jobConfig: DataFrameAnalyticsConfig | undefined, + searchQuery: SavedSearchQuery +): UseIndexDataReturnType => { + const needsDestIndexFields = + indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; + + const columns: EuiDataGridColumn[] = []; + + if (jobConfig !== undefined && indexPattern !== undefined) { + const resultsField = jobConfig.dest.results_field; + const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); + columns.push( + ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => + sortExplorationResultsFields(a.id, b.id, jobConfig) + ) + ); + } + + const dataGrid = useDataGrid( + columns, + 25, + // reduce default selected rows from 20 to 8 for performance reasons. + 8, + // by default, hide feature-influence columns and the doc id copy + d => !d.includes(`.${FEATURE_INFLUENCE}.`) && d !== ML__ID_COPY + ); + + // initialize sorting: reverse sort on outlier score column + useEffect(() => { + if (jobConfig !== undefined) { + dataGrid.setSortingColumns([{ id: getOutlierScoreFieldName(jobConfig), direction: 'desc' }]); + } + }, [jobConfig && jobConfig.id]); + + useEffect(() => { + getIndexData(jobConfig, dataGrid, searchQuery); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); + + const colorRange = useColorRange( + COLOR_RANGE.BLUE, + COLOR_RANGE_SCALE.INFLUENCER, + jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, dataGrid.tableItems) : 1 + ); + + const renderCellValue = useRenderCellValue( + indexPattern, + dataGrid.pagination, + dataGrid.tableItems, + jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD, + (columnId, cellValue, fullItem, setCellProps) => { + const resultsField = jobConfig?.dest.results_field ?? ''; + + const split = columnId.split('.'); + let backgroundColor; + + // column with feature values get color coded by its corresponding influencer value + if ( + fullItem[resultsField] !== undefined && + fullItem[resultsField][`${FEATURE_INFLUENCE}.${columnId}`] !== undefined + ) { + backgroundColor = colorRange(fullItem[resultsField][`${FEATURE_INFLUENCE}.${columnId}`]); + } + + // column with influencer values get color coded by its own value + if (split.length > 2 && split[0] === resultsField && split[1] === FEATURE_INFLUENCE) { + backgroundColor = colorRange(cellValue); + } + + if (backgroundColor !== undefined) { + setCellProps({ + style: { backgroundColor }, + }); + } + } + ); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index 6ef6666be5ec6..f6e8e0047671f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -235,7 +235,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) href={`${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-regression-evaluation`} > {i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.regressionDocsLink', + 'xpack.ml.dataframe.analytics.regressionExploration.regressionDocsLink', { defaultMessage: 'Regression evaluation docs ', } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index bfeca76a2b1c7..36d91f6f41d44 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -4,174 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useState, useEffect } from 'react'; -import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { ml } from '../../../../../services/ml_api_service'; -import { DataFrameAnalyticsConfig } from '../../../../common'; -import { EvaluatePanel } from './evaluate_panel'; -import { ResultsTable } from './results_table'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; -import { LoadingPanel } from '../loading_panel'; -import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; -import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { useMlContext } from '../../../../../contexts/ml'; -import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; +import React, { FC } from 'react'; -export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( - - - {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle', { - defaultMessage: 'Destination index for regression job ID {jobId}', - values: { jobId }, - })} - - -); +import { i18n } from '@kbn/i18n'; -const jobConfigErrorTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError', - { - defaultMessage: - 'Unable to fetch results. An error occurred loading the job configuration data.', - } -); +import { ExplorationPageWrapper } from '../exploration_page_wrapper'; -const jobCapsErrorTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError', - { - defaultMessage: "Unable to fetch results. An error occurred loading the index's field data.", - } -); +import { EvaluatePanel } from './evaluate_panel'; interface Props { jobId: string; } export const RegressionExploration: FC = ({ jobId }) => { - const [jobConfig, setJobConfig] = useState(undefined); - const [jobStatus, setJobStatus] = useState(undefined); - const [indexPattern, setIndexPattern] = useState(undefined); - const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); - const [isInitialized, setIsInitialized] = useState(false); - const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); - const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState( - undefined - ); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const mlContext = useMlContext(); - - const loadJobConfig = async () => { - setIsLoadingJobConfig(true); - try { - const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); - const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); - const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) - ? analyticsStats.data_frame_analytics[0] - : undefined; - - if (stats !== undefined && stats.state) { - setJobStatus(stats.state); - } - - if ( - Array.isArray(analyticsConfigs.data_frame_analytics) && - analyticsConfigs.data_frame_analytics.length > 0 - ) { - setJobConfig(analyticsConfigs.data_frame_analytics[0]); - setIsLoadingJobConfig(false); - } - } catch (e) { - if (e.message !== undefined) { - setJobConfigErrorMessage(e.message); - } else { - setJobConfigErrorMessage(JSON.stringify(e)); - } - setIsLoadingJobConfig(false); - } - }; - - useEffect(() => { - loadJobConfig(); - }, []); - - const initializeJobCapsService = async () => { - if (jobConfig !== undefined) { - try { - const destIndex = Array.isArray(jobConfig.dest.index) - ? jobConfig.dest.index[0] - : jobConfig.dest.index; - const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; - let indexP: IIndexPattern | undefined; - - try { - indexP = await mlContext.indexPatterns.get(destIndexPatternId); - } catch (e) { - indexP = undefined; - } - - if (indexP === undefined) { - const sourceIndex = jobConfig.source.index[0]; - const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); - } - - if (indexP !== undefined) { - setIndexPattern(indexP); - await newJobCapsService.initializeFromIndexPattern(indexP, false, false); - } - setIsInitialized(true); - } catch (e) { - if (e.message !== undefined) { - setJobCapsServiceErrorMessage(e.message); - } else { - setJobCapsServiceErrorMessage(JSON.stringify(e)); - } - } - } - }; - - useEffect(() => { - initializeJobCapsService(); - }, [jobConfig && jobConfig.id]); - - if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { - return ( - - - - -

{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}

-
-
- ); - } - return ( - - {isLoadingJobConfig === true && jobConfig === undefined && } - {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && ( - - )} - - {isLoadingJobConfig === true && jobConfig === undefined && } - {isLoadingJobConfig === false && - jobConfig !== undefined && - indexPattern !== undefined && - isInitialized === true && ( - - )} - + ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx deleted file mode 100644 index 0fcb1ed600719..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx +++ /dev/null @@ -1,135 +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 React, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common'; - -import { mlFieldFormatService } from '../../../../../services/field_format_service'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -type Pagination = Pick; -type TableItem = Record; - -interface ExplorationDataGridProps { - colorRange?: (d: number) => string; - columns: any[]; - indexPattern: IndexPattern; - pagination: Pagination; - resultsField: string; - rowCount: number; - selectedFields: string[]; - setPagination: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - tableItems: TableItem[]; -} - -export const RegressionExplorationDataGrid: FC = ({ - columns, - indexPattern, - pagination, - resultsField, - rowCount, - selectedFields, - setPagination, - setSelectedFields, - setSortingColumns, - sortingColumns, - tableItems, -}) => { - const renderCellValue = useMemo(() => { - return ({ rowIndex, columnId }: { rowIndex: number; columnId: string; setCellProps: any }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const fullItem = tableItems[adjustedRowIndex]; - - if (fullItem === undefined) { - return null; - } - - let format: any; - - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); - } - - const cellValue = - fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined - ? fullItem[columnId] - : null; - - if (format !== undefined) { - return format.convert(cellValue, 'text'); - } - - if (typeof cellValue === 'string' || cellValue === null) { - return cellValue; - } - - if (typeof cellValue === 'boolean') { - return cellValue ? 'true' : 'false'; - } - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - return cellValue; - }; - }, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - return ( - - ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx deleted file mode 100644 index 43fa50b2e4df5..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ /dev/null @@ -1,233 +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 React, { Fragment, FC, useEffect } from 'react'; - -import { i18n } from '@kbn/i18n'; -import { - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiPanel, - EuiProgress, - EuiSpacer, - EuiText, -} from '@elastic/eui'; - -import { - BASIC_NUMERICAL_TYPES, - EXTENDED_NUMERICAL_TYPES, - sortRegressionResultsFields, -} from '../../../../common/fields'; - -import { - DataFrameAnalyticsConfig, - MAX_COLUMNS, - INDEX_STATUS, - SEARCH_SIZE, - defaultSearchQuery, -} from '../../../../common'; -import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -import { useExploreData } from './use_explore_data'; -import { ExplorationTitle } from './regression_exploration'; -import { RegressionExplorationDataGrid } from './regression_exploration_data_grid'; -import { ExplorationQueryBar } from '../exploration_query_bar'; - -const showingDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText', - { - defaultMessage: 'Showing documents for which predictions exist', - } -); - -const showingFirstDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.firstDocumentsShownHelpText', - { - defaultMessage: 'Showing first {searchSize} documents for which predictions exist', - values: { searchSize: SEARCH_SIZE }, - } -); - -interface Props { - indexPattern: IndexPattern; - jobConfig: DataFrameAnalyticsConfig; - jobStatus?: DATA_FRAME_TASK_STATE; - setEvaluateSearchQuery: React.Dispatch>; -} - -export const ResultsTable: FC = React.memo( - ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery }) => { - const needsDestIndexFields = indexPattern && indexPattern.title === jobConfig.source.index[0]; - const resultsField = jobConfig.dest.results_field; - const { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - status, - tableFields, - tableItems, - } = useExploreData(jobConfig, needsDestIndexFields); - - useEffect(() => { - setEvaluateSearchQuery(searchQuery); - }, [JSON.stringify(searchQuery)]); - - const columns = tableFields - .sort((a: any, b: any) => sortRegressionResultsFields(a, b, jobConfig)) - .map((field: any) => { - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - let isSortable = true; - const type = fieldTypes[field]; - const isNumber = - type !== undefined && - (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); - - if (isNumber) { - schema = 'numeric'; - } - - switch (type) { - case 'date': - schema = 'datetime'; - break; - case 'geo_point': - schema = 'json'; - break; - case 'boolean': - schema = 'boolean'; - break; - } - - if (field === `${resultsField}.feature_importance`) { - isSortable = false; - } - - return { id: field, schema, isSortable }; - }); - - const docFieldsCount = tableFields.length; - - if (jobConfig === undefined) { - return null; - } - // if it's a searchBar syntax error leave the table visible so they can try again - if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { - return ( - - - - - - {jobStatus !== undefined && ( - - {getTaskStateBadge(jobStatus)} - - )} - - -

{errorMessage}

-
-
- ); - } - - return ( - - - - - - - - {jobStatus !== undefined && ( - - {getTaskStateBadge(jobStatus)} - - )} - - - - - - {docFieldsCount > MAX_COLUMNS && ( - - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.fieldSelection', - { - defaultMessage: - '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', - values: { selectedFieldsLength: selectedFields.length, docFieldsCount }, - } - )} - - )} - - - - - {status === INDEX_STATUS.LOADING && } - {status !== INDEX_STATUS.LOADING && ( - - )} - {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( - - - - - - - - - - - - - - - - - )} - - ); - } -); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts deleted file mode 100644 index 978aafd10de11..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts +++ /dev/null @@ -1,291 +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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { SearchResponse } from 'elasticsearch'; -import { cloneDeep } from 'lodash'; - -import { ml } from '../../../../../services/ml_api_service'; -import { getNestedProperty } from '../../../../../util/object_utils'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; -import { SavedSearchQuery } from '../../../../../contexts/ml'; - -import { - getDefaultFieldsFromJobCaps, - getDependentVar, - getFlattenedFields, - getPredictedFieldName, - DataFrameAnalyticsConfig, - EsFieldName, - INDEX_STATUS, -} from '../../../../common'; -import { Dictionary } from '../../../../../../../common/types/common'; -import { isKeywordAndTextType } from '../../../../common/fields'; -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { - LoadExploreDataArg, - defaultSearchQuery, - ResultsSearchQuery, - isResultsSearchBoolQuery, -} from '../../../../common/analytics'; - -export type TableItem = Record; -type Pagination = Pick; - -export interface UseExploreDataReturnType { - errorMessage: string; - fieldTypes: { [key: string]: ES_FIELD_TYPES }; - pagination: Pagination; - rowCount: number; - searchQuery: SavedSearchQuery; - selectedFields: EsFieldName[]; - setFilterByIsTraining: Dispatch>; - setPagination: Dispatch>; - setSearchQuery: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - status: INDEX_STATUS; - tableFields: string[]; - tableItems: TableItem[]; -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -export const useExploreData = ( - jobConfig: DataFrameAnalyticsConfig, - needsDestIndexFields: boolean -): UseExploreDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); - const [tableFields, setTableFields] = useState([]); - const [tableItems, setTableItems] = useState([]); - const [fieldTypes, setFieldTypes] = useState<{ [key: string]: ES_FIELD_TYPES }>({}); - const [rowCount, setRowCount] = useState(0); - - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const [filterByIsTraining, setFilterByIsTraining] = useState(undefined); - const [sortingColumns, setSortingColumns] = useState([]); - - const predictedFieldName = getPredictedFieldName( - jobConfig.dest.results_field, - jobConfig.analysis - ); - const dependentVariable = getDependentVar(jobConfig.analysis); - - const getDefaultSelectedFields = () => { - const { fields } = newJobCapsService; - if (selectedFields.length === 0 && jobConfig !== undefined) { - const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( - fields, - jobConfig, - needsDestIndexFields - ); - - const types: { [key: string]: ES_FIELD_TYPES } = {}; - const allFields: string[] = []; - - docFields.forEach(field => { - types[field.id] = field.type; - allFields.push(field.id); - }); - - setFieldTypes(types); - setSelectedFields(defaultSelected.map(field => field.id)); - setTableFields(allFields); - } - }; - - const loadExploreData = async ({ - filterByIsTraining: isTraining, - searchQuery: incomingQuery, - }: LoadExploreDataArg) => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const resultsField = jobConfig.dest.results_field; - const searchQueryClone: ResultsSearchQuery = cloneDeep(incomingQuery); - let query: ResultsSearchQuery; - const { pageIndex, pageSize } = pagination; - // If filterByIsTraining is defined - add that in to the final query - const trainingQuery = - isTraining !== undefined - ? { - term: { [`${resultsField}.is_training`]: { value: isTraining } }, - } - : undefined; - - if (JSON.stringify(incomingQuery) === JSON.stringify(defaultSearchQuery)) { - const existsQuery = { - exists: { - field: resultsField, - }, - }; - - query = { - bool: { - must: [existsQuery], - }, - }; - - if (trainingQuery !== undefined && isResultsSearchBoolQuery(query)) { - query.bool.must.push(trainingQuery); - } - } else if (isResultsSearchBoolQuery(searchQueryClone)) { - if (searchQueryClone.bool.must === undefined) { - searchQueryClone.bool.must = []; - } - - searchQueryClone.bool.must.push({ - exists: { - field: resultsField, - }, - }); - - if (trainingQuery !== undefined) { - searchQueryClone.bool.must.push(trainingQuery); - } - - query = searchQueryClone; - } else { - query = searchQueryClone; - } - - const sort: EsSorting = sortingColumns - .map(column => { - const { id } = column; - column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; - return column; - }) - .reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const resp: SearchResponse7 = await ml.esSearch({ - index: jobConfig.dest.index, - body: { - query, - from: pageIndex * pageSize, - size: pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }); - - setRowCount(resp.hits.total.value); - - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(INDEX_STATUS.LOADED); - return; - } - - // Create a version of the doc's source with flattened field names. - // This avoids confusion later on if a field name has dots in its name - // or is a nested fields when displaying it via EuiInMemoryTable. - const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); - const transformedTableItems = docs.map(doc => { - const item: TableItem = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - if (item[ff] === undefined) { - const parts = ff.split('.'); - if (parts[0] === resultsField && parts.length >= 2) { - parts.shift(); - if (doc._source[resultsField] !== undefined) { - item[ff] = doc._source[resultsField][parts.join('.')]; - } - } - } - }); - return item; - }); - - setTableItems(transformedTableItems); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e)); - } - setTableItems([]); - setStatus(INDEX_STATUS.ERROR); - } - } - }; - - useEffect(() => { - getDefaultSelectedFields(); - }, [jobConfig && jobConfig.id]); - - // By default set sorting to descending on the prediction field (`_prediction`). - useEffect(() => { - const sortByField = isKeywordAndTextType(dependentVariable) - ? `${predictedFieldName}.keyword` - : predictedFieldName; - const direction = SORT_DIRECTION.DESC; - - setSortingColumns([{ id: sortByField, direction }]); - }, [jobConfig && jobConfig.id]); - - useEffect(() => { - loadExploreData({ filterByIsTraining, searchQuery }); - }, [ - filterByIsTraining, - jobConfig && jobConfig.id, - pagination, - searchQuery, - selectedFields, - sortingColumns, - ]); - - return { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setFilterByIsTraining, - setPagination, - setSelectedFields, - setSortingColumns, - setSearchQuery, - sortingColumns, - status, - tableItems, - tableFields, - }; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts deleted file mode 100644 index a0a9eb8312499..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts +++ /dev/null @@ -1,267 +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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import { SearchResponse } from 'elasticsearch'; - -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -import { Dictionary } from '../../../../../../../common/types/common'; - -import { SavedSearchQuery } from '../../../../../contexts/ml'; -import { ml } from '../../../../../services/ml_api_service'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; -import { getNestedProperty } from '../../../../../util/object_utils'; -import { useMlContext } from '../../../../../contexts/ml'; -import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; - -import { - getDefaultSelectableFields, - getFlattenedFields, - DataFrameAnalyticsConfig, - EsFieldName, - INDEX_STATUS, - MAX_COLUMNS, - defaultSearchQuery, -} from '../../../../common'; -import { isKeywordAndTextType } from '../../../../common/fields'; - -import { getOutlierScoreFieldName } from './common'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; - -export type TableItem = Record; - -type Pagination = Pick; - -interface UseExploreDataReturnType { - errorMessage: string; - indexPattern: IndexPattern | undefined; - jobConfig: DataFrameAnalyticsConfig | undefined; - jobStatus: DATA_FRAME_TASK_STATE | undefined; - pagination: Pagination; - searchQuery: SavedSearchQuery; - selectedFields: EsFieldName[]; - setJobConfig: Dispatch>; - setPagination: Dispatch>; - setSearchQuery: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - rowCount: number; - sortingColumns: EuiDataGridSorting['columns']; - status: INDEX_STATUS; - tableFields: string[]; - tableItems: TableItem[]; -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -export const useExploreData = (jobId: string): UseExploreDataReturnType => { - const mlContext = useMlContext(); - - const [indexPattern, setIndexPattern] = useState(undefined); - const [jobConfig, setJobConfig] = useState(undefined); - const [jobStatus, setJobStatus] = useState(undefined); - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); - const [tableFields, setTableFields] = useState([]); - const [tableItems, setTableItems] = useState([]); - const [rowCount, setRowCount] = useState(0); - - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const [sortingColumns, setSortingColumns] = useState([]); - - // get analytics configuration - useEffect(() => { - (async function() { - const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); - const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); - const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) - ? analyticsStats.data_frame_analytics[0] - : undefined; - - if (stats !== undefined && stats.state) { - setJobStatus(stats.state); - } - - if ( - Array.isArray(analyticsConfigs.data_frame_analytics) && - analyticsConfigs.data_frame_analytics.length > 0 - ) { - setJobConfig(analyticsConfigs.data_frame_analytics[0]); - } - })(); - }, []); - - // get index pattern and field caps - useEffect(() => { - (async () => { - if (jobConfig !== undefined) { - try { - const destIndex = Array.isArray(jobConfig.dest.index) - ? jobConfig.dest.index[0] - : jobConfig.dest.index; - const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; - let indexP: IndexPattern | undefined; - - try { - indexP = await mlContext.indexPatterns.get(destIndexPatternId); - } catch (e) { - indexP = undefined; - } - - if (indexP === undefined) { - const sourceIndex = jobConfig.source.index[0]; - const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); - } - - if (indexP !== undefined) { - setIndexPattern(indexP); - await newJobCapsService.initializeFromIndexPattern(indexP, false, false); - } - } catch (e) { - // eslint-disable-next-line - console.log('Error loading index field data', e); - } - } - })(); - }, [jobConfig && jobConfig.id]); - - // initialize sorting: reverse sort on outlier score column - useEffect(() => { - if (jobConfig !== undefined) { - setSortingColumns([{ id: getOutlierScoreFieldName(jobConfig), direction: 'desc' }]); - } - }, [jobConfig && jobConfig.id]); - - // update data grid data - useEffect(() => { - (async () => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const resultsField = jobConfig.dest.results_field; - - const sort: EsSorting = sortingColumns - .map(column => { - const { id } = column; - column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; - return column; - }) - .reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const { pageIndex, pageSize } = pagination; - const resp: SearchResponse7 = await ml.esSearch({ - index: jobConfig.dest.index, - body: { - query: searchQuery, - from: pageIndex * pageSize, - size: pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }); - - setRowCount(resp.hits.total.value); - - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(INDEX_STATUS.LOADED); - return; - } - - if (selectedFields.length === 0) { - const newSelectedFields = getDefaultSelectableFields(docs, resultsField); - setSelectedFields(newSelectedFields.sort().splice(0, MAX_COLUMNS)); - } - - // Create a version of the doc's source with flattened field names. - // This avoids confusion later on if a field name has dots in its name - // or is a nested fields when displaying it via EuiInMemoryTable. - const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); - const transformedTableItems = docs.map(doc => { - const item: TableItem = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - if (item[ff] === undefined) { - const parts = ff.split('.'); - if (parts[0] === resultsField && parts.length >= 2) { - parts.shift(); - if (doc._source[resultsField] !== undefined) { - item[ff] = doc._source[resultsField][parts.join('.')]; - } - } - } - }); - return item; - }); - - setTableFields(flattenedFields); - setTableItems(transformedTableItems); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e)); - } - setTableItems([]); - setStatus(INDEX_STATUS.ERROR); - } - } - })(); - }, [jobConfig && jobConfig.id, pagination, searchQuery, selectedFields, sortingColumns]); - - return { - errorMessage, - indexPattern, - jobConfig, - jobStatus, - pagination, - rowCount, - searchQuery, - selectedFields, - setJobConfig, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - status, - tableFields, - tableItems, - }; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx index eb1871c98764b..8c65af1d92959 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { DeepReadonly } from '../../../../../../../common/types/common'; import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common'; import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics'; +import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants'; import { CreateAnalyticsFormProps, DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES, @@ -214,7 +215,7 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo }, results_field: { optional: true, - defaultValue: 'ml', + defaultValue: DEFAULT_RESULTS_FIELD, }, }, model_memory_limit: { diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts index 0db11ffa061c0..9d8106a1366d6 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +// actual mocks export const expandLiteralStrings = jest.fn(); export const XJsonMode = jest.fn(); export const useRequest = jest.fn(() => ({ @@ -11,5 +12,20 @@ export const useRequest = jest.fn(() => ({ error: null, data: undefined, })); -export { mlInMemoryTableBasicFactory } from '../../../ml/public/application/components/ml_in_memory_table'; -export const SORT_DIRECTION = { ASC: 'asc' }; + +// just passing through the reimports +export { getErrorMessage } from '../../../ml/common/util/errors'; +export { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + multiColumnSortFactory, + useDataGrid, + useRenderCellValue, + DataGrid, + EsSorting, + RenderCellValue, + SearchResponse7, + UseDataGridReturnType, + UseIndexDataReturnType, +} from '../../../ml/public/application/components/data_grid'; +export { INDEX_STATUS } from '../../../ml/public/application/data_frame_analytics/common'; diff --git a/x-pack/plugins/transform/public/app/common/data_grid.test.ts b/x-pack/plugins/transform/public/app/common/data_grid.test.ts new file mode 100644 index 0000000000000..0e5ecb5d3b214 --- /dev/null +++ b/x-pack/plugins/transform/public/app/common/data_grid.test.ts @@ -0,0 +1,93 @@ +/* + * 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 { + getPreviewRequestBody, + PivotAggsConfig, + PivotGroupByConfig, + PIVOT_SUPPORTED_AGGS, + PIVOT_SUPPORTED_GROUP_BY_AGGS, + SimpleQuery, +} from '../common'; + +import { getIndexDevConsoleStatement, getPivotPreviewDevConsoleStatement } from './data_grid'; + +describe('Transform: Data Grid', () => { + test('getPivotPreviewDevConsoleStatement()', () => { + const query: SimpleQuery = { + query_string: { + query: '*', + default_operator: 'AND', + }, + }; + const groupBy: PivotGroupByConfig = { + agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, + field: 'the-group-by-field', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', + }; + const agg: PivotAggsConfig = { + agg: PIVOT_SUPPORTED_AGGS.AVG, + field: 'the-agg-field', + aggName: 'the-agg-agg-name', + dropDownName: 'the-agg-drop-down-name', + }; + const request = getPreviewRequestBody('the-index-pattern-title', query, [groupBy], [agg]); + const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request); + + expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview +{ + "source": { + "index": [ + "the-index-pattern-title" + ] + }, + "pivot": { + "group_by": { + "the-group-by-agg-name": { + "terms": { + "field": "the-group-by-field" + } + } + }, + "aggregations": { + "the-agg-agg-name": { + "avg": { + "field": "the-agg-field" + } + } + } + } +} +`); + }); +}); + +describe('Transform: Index Preview Common', () => { + test('getIndexDevConsoleStatement()', () => { + const query: SimpleQuery = { + query_string: { + query: '*', + default_operator: 'AND', + }, + }; + const indexPreviewDevConsoleStatement = getIndexDevConsoleStatement( + query, + 'the-index-pattern-title' + ); + + expect(indexPreviewDevConsoleStatement).toBe(`GET the-index-pattern-title/_search +{ + "query": { + "query_string": { + "query": "*", + "default_operator": "AND" + } + } +} +`); + }); +}); diff --git a/x-pack/plugins/transform/public/app/common/data_grid.ts b/x-pack/plugins/transform/public/app/common/data_grid.ts index 0e9cceefb3156..cf9ba5d6f5853 100644 --- a/x-pack/plugins/transform/public/app/common/data_grid.ts +++ b/x-pack/plugins/transform/public/app/common/data_grid.ts @@ -4,22 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiDataGridStyle } from '@elastic/eui'; +import { PivotQuery } from './request'; +import { PreviewRequestBody } from './transform'; export const INIT_MAX_COLUMNS = 20; -export const euiDataGridStyle: EuiDataGridStyle = { - border: 'all', - fontSize: 's', - cellPadding: 's', - stripes: false, - rowHover: 'highlight', - header: 'shade', +export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => { + return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`; }; -export const euiDataGridToolbarSettings = { - showColumnSelector: true, - showStyleSelector: false, - showSortSelector: true, - showFullScreenSelector: false, +export const getIndexDevConsoleStatement = (query: PivotQuery, indexPatternTitle: string) => { + return `GET ${indexPatternTitle}/_search\n${JSON.stringify( + { + query, + }, + null, + 2 + )}\n`; }; diff --git a/x-pack/plugins/transform/public/app/common/index.ts b/x-pack/plugins/transform/public/app/common/index.ts index daeddaa801828..009c8c7a2a9f5 100644 --- a/x-pack/plugins/transform/public/app/common/index.ts +++ b/x-pack/plugins/transform/public/app/common/index.ts @@ -5,7 +5,11 @@ */ export { AggName, isAggName } from './aggregations'; -export { euiDataGridStyle, euiDataGridToolbarSettings, INIT_MAX_COLUMNS } from './data_grid'; +export { + getIndexDevConsoleStatement, + getPivotPreviewDevConsoleStatement, + INIT_MAX_COLUMNS, +} from './data_grid'; export { getDefaultSelectableFields, getFlattenedFields, diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts deleted file mode 100644 index 498c3a3ac60af..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts +++ /dev/null @@ -1,60 +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 { EuiDataGridSorting } from '@elastic/eui'; - -import { getNestedProperty } from '../../../../common/utils/object_utils'; - -import { PreviewRequestBody } from '../../common'; - -/** - * Helper to sort an array of objects based on an EuiDataGrid sorting configuration. - * `sortFn()` is recursive to support sorting on multiple columns. - * - * @param sortingColumns - The EUI data grid sorting configuration - * @returns The sorting function which can be used with an array's sort() function. - */ -export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['columns']) => { - const isString = (arg: any): arg is string => { - return typeof arg === 'string'; - }; - - const sortFn = (a: any, b: any, sortingColumnIndex = 0): number => { - const sort = sortingColumns[sortingColumnIndex]; - const aValue = getNestedProperty(a, sort.id, null); - const bValue = getNestedProperty(b, sort.id, null); - - if (typeof aValue === 'number' && typeof bValue === 'number') { - if (aValue < bValue) { - return sort.direction === 'asc' ? -1 : 1; - } - if (aValue > bValue) { - return sort.direction === 'asc' ? 1 : -1; - } - } - - if (isString(aValue) && isString(bValue)) { - if (aValue.localeCompare(bValue) === -1) { - return sort.direction === 'asc' ? -1 : 1; - } - if (aValue.localeCompare(bValue) === 1) { - return sort.direction === 'asc' ? 1 : -1; - } - } - - if (sortingColumnIndex + 1 < sortingColumns.length) { - return sortFn(a, b, sortingColumnIndex + 1); - } - - return 0; - }; - - return sortFn; -}; - -export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => { - return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`; -}; diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx deleted file mode 100644 index 5ed50eaab46ba..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx +++ /dev/null @@ -1,55 +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 React from 'react'; -import { render, wait } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; - -import { - getPivotQuery, - PivotAggsConfig, - PivotGroupByConfig, - PIVOT_SUPPORTED_AGGS, - PIVOT_SUPPORTED_GROUP_BY_AGGS, -} from '../../common'; - -import { PivotPreview } from './pivot_preview'; - -jest.mock('../../../shared_imports'); -jest.mock('../../../app/app_dependencies'); - -describe('Transform: ', () => { - // Using the async/await wait()/done() pattern to avoid act() errors. - test('Minimal initialization', async done => { - // Arrange - const groupBy: PivotGroupByConfig = { - agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, - field: 'the-group-by-field', - aggName: 'the-group-by-agg-name', - dropDownName: 'the-group-by-drop-down-name', - }; - const agg: PivotAggsConfig = { - agg: PIVOT_SUPPORTED_AGGS.AVG, - field: 'the-agg-field', - aggName: 'the-group-by-agg-name', - dropDownName: 'the-group-by-drop-down-name', - }; - const props = { - aggs: { 'the-agg-name': agg }, - groupBy: { 'the-group-by-name': groupBy }, - indexPatternTitle: 'the-index-pattern-title', - query: getPivotQuery('the-query'), - }; - - const { getByText } = render(); - - // Act - // Assert - expect(getByText('Transform pivot preview')).toBeInTheDocument(); - await wait(); - done(); - }); -}); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx deleted file mode 100644 index c50df0366d698..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx +++ /dev/null @@ -1,345 +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 moment from 'moment-timezone'; -import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { - EuiButtonIcon, - EuiCallOut, - EuiCodeBlock, - EuiCopy, - EuiDataGrid, - EuiDataGridSorting, - EuiFlexGroup, - EuiFlexItem, - EuiProgress, - EuiTitle, -} from '@elastic/eui'; - -import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; - -import { dictionaryToArray } from '../../../../common/types/common'; -import { formatHumanReadableDateTimeSeconds } from '../../../../common/utils/date_utils'; -import { getNestedProperty } from '../../../../common/utils/object_utils'; - -import { - euiDataGridStyle, - euiDataGridToolbarSettings, - EsFieldName, - PreviewRequestBody, - PivotAggsConfigDict, - PivotGroupByConfig, - PivotGroupByConfigDict, - PivotQuery, - INIT_MAX_COLUMNS, -} from '../../common'; -import { SearchItems } from '../../hooks/use_search_items'; - -import { getPivotPreviewDevConsoleStatement, multiColumnSortFactory } from './common'; -import { PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data'; - -function sortColumns(groupByArr: PivotGroupByConfig[]) { - return (a: string, b: string) => { - // make sure groupBy fields are always most left columns - if (groupByArr.some(d => d.aggName === a) && groupByArr.some(d => d.aggName === b)) { - return a.localeCompare(b); - } - if (groupByArr.some(d => d.aggName === a)) { - return -1; - } - if (groupByArr.some(d => d.aggName === b)) { - return 1; - } - return a.localeCompare(b); - }; -} - -interface PreviewTitleProps { - previewRequest: PreviewRequestBody; -} - -const PreviewTitle: FC = ({ previewRequest }) => { - const euiCopyText = i18n.translate('xpack.transform.pivotPreview.copyClipboardTooltip', { - defaultMessage: 'Copy Dev Console statement of the pivot preview to the clipboard.', - }); - - return ( - - - - - {i18n.translate('xpack.transform.pivotPreview.PivotPreviewTitle', { - defaultMessage: 'Transform pivot preview', - })} - - - - - - {(copy: () => void) => ( - - )} - - - - ); -}; - -interface ErrorMessageProps { - message: string; -} - -const ErrorMessage: FC = ({ message }) => ( - - {message} - -); - -interface PivotPreviewProps { - aggs: PivotAggsConfigDict; - groupBy: PivotGroupByConfigDict; - indexPatternTitle: SearchItems['indexPattern']['title']; - query: PivotQuery; - showHeader?: boolean; -} - -const defaultPagination = { pageIndex: 0, pageSize: 5 }; - -export const PivotPreview: FC = React.memo( - ({ aggs, groupBy, indexPatternTitle, query, showHeader = true }) => { - const { - previewData: data, - previewMappings, - errorMessage, - previewRequest, - status, - } = usePivotPreviewData(indexPatternTitle, query, aggs, groupBy); - const groupByArr = dictionaryToArray(groupBy); - - // Filters mapping properties of type `object`, which get returned for nested field parents. - const columnKeys = Object.keys(previewMappings.properties).filter( - key => previewMappings.properties[key].type !== 'object' - ); - columnKeys.sort(sortColumns(groupByArr)); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState([]); - - useEffect(() => { - setVisibleColumns(columnKeys.splice(0, INIT_MAX_COLUMNS)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [columnKeys.join()]); - - const [pagination, setPagination] = useState(defaultPagination); - - // Reset pagination if data changes. This is to avoid ending up with an empty table - // when for example the user selected a page that is not available with the updated data. - useEffect(() => { - setPagination(defaultPagination); - }, [data.length]); - - // EuiDataGrid State - const dataGridColumns = columnKeys.map(id => { - const field = previewMappings.properties[id]; - - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - - switch (field?.type) { - case ES_FIELD_TYPES.GEO_POINT: - case ES_FIELD_TYPES.GEO_SHAPE: - schema = 'json'; - break; - case ES_FIELD_TYPES.BOOLEAN: - schema = 'boolean'; - break; - case ES_FIELD_TYPES.DATE: - case ES_FIELD_TYPES.DATE_NANOS: - schema = 'datetime'; - break; - case ES_FIELD_TYPES.BYTE: - case ES_FIELD_TYPES.DOUBLE: - case ES_FIELD_TYPES.FLOAT: - case ES_FIELD_TYPES.HALF_FLOAT: - case ES_FIELD_TYPES.INTEGER: - case ES_FIELD_TYPES.LONG: - case ES_FIELD_TYPES.SCALED_FLOAT: - case ES_FIELD_TYPES.SHORT: - schema = 'numeric'; - break; - // keep schema undefined for text based columns - case ES_FIELD_TYPES.KEYWORD: - case ES_FIELD_TYPES.TEXT: - break; - } - - return { id, schema }; - }); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - // Sorting config - const [sortingColumns, setSortingColumns] = useState([]); - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - if (sortingColumns.length > 0) { - data.sort(multiColumnSortFactory(sortingColumns)); - } - - const pageData = data.slice( - pagination.pageIndex * pagination.pageSize, - (pagination.pageIndex + 1) * pagination.pageSize - ); - - const renderCellValue = useMemo(() => { - return ({ - rowIndex, - columnId, - setCellProps, - }: { - rowIndex: number; - columnId: string; - setCellProps: any; - }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const cellValue = pageData.hasOwnProperty(adjustedRowIndex) - ? getNestedProperty(pageData[adjustedRowIndex], columnId, null) - : null; - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - if (cellValue === undefined || cellValue === null) { - return null; - } - - if ( - [ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.DATE_NANOS].includes( - previewMappings.properties[columnId].type - ) - ) { - return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); - } - - if (previewMappings.properties[columnId].type === ES_FIELD_TYPES.BOOLEAN) { - return cellValue ? 'true' : 'false'; - } - - return cellValue; - }; - }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappings.properties]); - - if (status === PIVOT_PREVIEW_STATUS.ERROR) { - return ( -
- - - - -
- ); - } - - if (data.length === 0) { - let noDataMessage = i18n.translate( - 'xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', - { - defaultMessage: - 'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.', - } - ); - - const aggsArr = dictionaryToArray(aggs); - if (aggsArr.length === 0 || groupByArr.length === 0) { - noDataMessage = i18n.translate( - 'xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody', - { - defaultMessage: 'Please choose at least one group-by field and aggregation.', - } - ); - } - - return ( -
- - -

{noDataMessage}

-
-
- ); - } - - if (columnKeys.length === 0) { - return null; - } - - return ( -
- {showHeader && ( - <> - -
- {status === PIVOT_PREVIEW_STATUS.LOADING && } - {status !== PIVOT_PREVIEW_STATUS.LOADING && ( - - )} -
- - )} - {dataGridColumns.length > 0 && data.length > 0 && ( - - )} -
- ); - } -); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.test.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.test.tsx deleted file mode 100644 index 8d09d06b1c731..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.test.tsx +++ /dev/null @@ -1,69 +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 React, { FC } from 'react'; -import ReactDOM from 'react-dom'; - -import { SimpleQuery } from '../../common'; -import { - PIVOT_PREVIEW_STATUS, - usePivotPreviewData, - UsePivotPreviewDataReturnType, -} from './use_pivot_preview_data'; - -jest.mock('../../hooks/use_api'); - -type Callback = () => void; -interface TestHookProps { - callback: Callback; -} - -const TestHook: FC = ({ callback }) => { - callback(); - return null; -}; - -const testHook = (callback: Callback) => { - const container = document.createElement('div'); - document.body.appendChild(container); - ReactDOM.render(, container); -}; - -const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, -}; - -let pivotPreviewObj: UsePivotPreviewDataReturnType; - -describe('usePivotPreviewData', () => { - test('indexPattern not defined', () => { - testHook(() => { - pivotPreviewObj = usePivotPreviewData('the-title', query, {}, {}); - }); - - expect(pivotPreviewObj.errorMessage).toBe(''); - expect(pivotPreviewObj.status).toBe(PIVOT_PREVIEW_STATUS.UNUSED); - expect(pivotPreviewObj.previewData).toEqual([]); - }); - - test('indexPattern set triggers loading', () => { - testHook(() => { - pivotPreviewObj = usePivotPreviewData('the-title', query, {}, {}); - }); - - expect(pivotPreviewObj.errorMessage).toBe(''); - // ideally this should be LOADING instead of UNUSED but jest/enzyme/hooks doesn't - // trigger that state upate yet. - expect(pivotPreviewObj.status).toBe(PIVOT_PREVIEW_STATUS.UNUSED); - expect(pivotPreviewObj.previewData).toEqual([]); - }); - - // TODO add more tests to check data retrieved via `api.esSearch()`. - // This needs more investigation in regards to jest/enzyme's React Hooks support. -}); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts deleted file mode 100644 index 83fa7ba189ff0..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts +++ /dev/null @@ -1,91 +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 { useEffect, useState } from 'react'; - -import { dictionaryToArray } from '../../../../common/types/common'; -import { useApi } from '../../hooks/use_api'; - -import { IndexPattern } from '../../../../../../../src/plugins/data/public'; - -import { - getPreviewRequestBody, - PreviewRequestBody, - PivotAggsConfigDict, - PivotGroupByConfigDict, - PivotQuery, - PreviewData, - PreviewMappings, -} from '../../common'; - -export enum PIVOT_PREVIEW_STATUS { - UNUSED, - LOADING, - LOADED, - ERROR, -} - -export interface UsePivotPreviewDataReturnType { - errorMessage: string; - status: PIVOT_PREVIEW_STATUS; - previewData: PreviewData; - previewMappings: PreviewMappings; - previewRequest: PreviewRequestBody; -} - -export const usePivotPreviewData = ( - indexPatternTitle: IndexPattern['title'], - query: PivotQuery, - aggs: PivotAggsConfigDict, - groupBy: PivotGroupByConfigDict -): UsePivotPreviewDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(PIVOT_PREVIEW_STATUS.UNUSED); - const [previewData, setPreviewData] = useState([]); - const [previewMappings, setPreviewMappings] = useState({ properties: {} }); - const api = useApi(); - - const aggsArr = dictionaryToArray(aggs); - const groupByArr = dictionaryToArray(groupBy); - - const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr); - - const getPreviewData = async () => { - if (aggsArr.length === 0 || groupByArr.length === 0) { - setPreviewData([]); - return; - } - - setErrorMessage(''); - setStatus(PIVOT_PREVIEW_STATUS.LOADING); - - try { - const resp = await api.getTransformsPreview(previewRequest); - setPreviewData(resp.preview); - setPreviewMappings(resp.generated_dest_index.mappings); - setStatus(PIVOT_PREVIEW_STATUS.LOADED); - } catch (e) { - setErrorMessage(JSON.stringify(e, null, 2)); - setPreviewData([]); - setPreviewMappings({ properties: {} }); - setStatus(PIVOT_PREVIEW_STATUS.ERROR); - } - }; - - useEffect(() => { - getPreviewData(); - // custom comparison - /* eslint-disable react-hooks/exhaustive-deps */ - }, [ - indexPatternTitle, - JSON.stringify(aggsArr), - JSON.stringify(groupByArr), - JSON.stringify(query), - /* eslint-enable react-hooks/exhaustive-deps */ - ]); - - return { errorMessage, status, previewData, previewMappings, previewRequest }; -}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx new file mode 100644 index 0000000000000..4ca536e3c115d --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx @@ -0,0 +1,85 @@ +/* + * 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 from 'react'; + +import { render, wait } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import '@testing-library/jest-dom/extend-expect'; + +import { CoreSetup } from 'src/core/public'; + +import { DataGrid, UseIndexDataReturnType, INDEX_STATUS } from '../../shared_imports'; + +import { SimpleQuery } from '../common'; + +import { SearchItems } from './use_search_items'; +import { useIndexData } from './use_index_data'; + +jest.mock('../../shared_imports'); +jest.mock('../app_dependencies'); +jest.mock('./use_api'); + +const query: SimpleQuery = { + query_string: { + query: '*', + default_operator: 'AND', + }, +}; + +describe('Transform: useIndexData()', () => { + test('indexPattern set triggers loading', async done => { + const { result, waitForNextUpdate } = renderHook(() => + useIndexData( + ({ + id: 'the-id', + title: 'the-title', + fields: [], + } as unknown) as SearchItems['indexPattern'], + query + ) + ); + const IndexObj: UseIndexDataReturnType = result.current; + + await waitForNextUpdate(); + + expect(IndexObj.errorMessage).toBe(''); + expect(IndexObj.status).toBe(INDEX_STATUS.LOADING); + expect(IndexObj.tableItems).toEqual([]); + done(); + }); +}); + +describe('Transform: with useIndexData()', () => { + // Using the async/await wait()/done() pattern to avoid act() errors. + test('Minimal initialization', async done => { + // Arrange + const indexPattern = { + title: 'the-index-pattern-title', + fields: [] as any[], + } as SearchItems['indexPattern']; + + const Wrapper = () => { + const props = { + ...useIndexData(indexPattern, { match_all: {} }), + copyToClipboard: 'the-copy-to-clipboard-code', + copyToClipboardDescription: 'the-copy-to-clipboard-description', + dataTestSubj: 'the-data-test-subj', + title: 'the-index-preview-title', + toastNotifications: {} as CoreSetup['notifications']['toasts'], + }; + + return ; + }; + const { getByText } = render(); + + // Act + // Assert + expect(getByText('the-index-preview-title')).toBeInTheDocument(); + await wait(); + done(); + }); +}); diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts new file mode 100644 index 0000000000000..ec5a4d244c152 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -0,0 +1,111 @@ +/* + * 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 { useEffect } from 'react'; + +import { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + getErrorMessage, + useDataGrid, + useRenderCellValue, + EsSorting, + SearchResponse7, + UseIndexDataReturnType, + INDEX_STATUS, +} from '../../shared_imports'; + +import { isDefaultQuery, matchAllQuery, PivotQuery } from '../common'; + +import { SearchItems } from './use_search_items'; +import { useApi } from './use_api'; + +type IndexSearchResponse = SearchResponse7; + +export const useIndexData = ( + indexPattern: SearchItems['indexPattern'], + query: PivotQuery +): UseIndexDataReturnType => { + const api = useApi(); + + const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern); + + // EuiDataGrid State + const columns = [ + ...indexPatternFields.map(id => { + const field = indexPattern.fields.getByName(id); + const schema = getDataGridSchemaFromKibanaFieldType(field); + return { id, schema }; + }), + ]; + + const dataGrid = useDataGrid(columns); + + const { + pagination, + resetPagination, + setErrorMessage, + setRowCount, + setStatus, + setTableItems, + sortingColumns, + tableItems, + } = dataGrid; + + useEffect(() => { + resetPagination(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(query)]); + + const getIndexData = async function() { + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + const sort: EsSorting = sortingColumns.reduce((s, column) => { + s[column.id] = { order: column.direction }; + return s; + }, {} as EsSorting); + + const esSearchRequest = { + index: indexPattern.title, + body: { + // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. + query: isDefaultQuery(query) ? matchAllQuery : query, + from: pagination.pageIndex * pagination.pageSize, + size: pagination.pageSize, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, + }; + + try { + const resp: IndexSearchResponse = await api.esSearch(esSearchRequest); + + const docs = resp.hits.hits.map(d => d._source); + + setRowCount(resp.hits.total.value); + setTableItems(docs); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setStatus(INDEX_STATUS.ERROR); + } + }; + + useEffect(() => { + getIndexData(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); + + const renderCellValue = useRenderCellValue(indexPattern, pagination, tableItems); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts new file mode 100644 index 0000000000000..ff7ca5d42b5f7 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts @@ -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 moment from 'moment-timezone'; +import { useEffect, useMemo, useState } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; + +import { dictionaryToArray } from '../../../common/types/common'; +import { formatHumanReadableDateTimeSeconds } from '../../../common/utils/date_utils'; +import { getNestedProperty } from '../../../common/utils/object_utils'; + +import { + getErrorMessage, + multiColumnSortFactory, + useDataGrid, + RenderCellValue, + UseIndexDataReturnType, + INDEX_STATUS, +} from '../../shared_imports'; + +import { + getPreviewRequestBody, + PivotAggsConfigDict, + PivotGroupByConfigDict, + PivotGroupByConfig, + PivotQuery, + PreviewMappings, +} from '../common'; + +import { SearchItems } from './use_search_items'; +import { useApi } from './use_api'; + +function sortColumns(groupByArr: PivotGroupByConfig[]) { + return (a: string, b: string) => { + // make sure groupBy fields are always most left columns + if (groupByArr.some(d => d.aggName === a) && groupByArr.some(d => d.aggName === b)) { + return a.localeCompare(b); + } + if (groupByArr.some(d => d.aggName === a)) { + return -1; + } + if (groupByArr.some(d => d.aggName === b)) { + return 1; + } + return a.localeCompare(b); + }; +} + +export const usePivotData = ( + indexPatternTitle: SearchItems['indexPattern']['title'], + query: PivotQuery, + aggs: PivotAggsConfigDict, + groupBy: PivotGroupByConfigDict +): UseIndexDataReturnType => { + const [previewMappings, setPreviewMappings] = useState({ properties: {} }); + const api = useApi(); + + const aggsArr = dictionaryToArray(aggs); + const groupByArr = dictionaryToArray(groupBy); + + // Filters mapping properties of type `object`, which get returned for nested field parents. + const columnKeys = Object.keys(previewMappings.properties).filter( + key => previewMappings.properties[key].type !== 'object' + ); + columnKeys.sort(sortColumns(groupByArr)); + + // EuiDataGrid State + const columns = columnKeys.map(id => { + const field = previewMappings.properties[id]; + + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + + switch (field?.type) { + case ES_FIELD_TYPES.GEO_POINT: + case ES_FIELD_TYPES.GEO_SHAPE: + schema = 'json'; + break; + case ES_FIELD_TYPES.BOOLEAN: + schema = 'boolean'; + break; + case ES_FIELD_TYPES.DATE: + case ES_FIELD_TYPES.DATE_NANOS: + schema = 'datetime'; + break; + case ES_FIELD_TYPES.BYTE: + case ES_FIELD_TYPES.DOUBLE: + case ES_FIELD_TYPES.FLOAT: + case ES_FIELD_TYPES.HALF_FLOAT: + case ES_FIELD_TYPES.INTEGER: + case ES_FIELD_TYPES.LONG: + case ES_FIELD_TYPES.SCALED_FLOAT: + case ES_FIELD_TYPES.SHORT: + schema = 'numeric'; + break; + // keep schema undefined for text based columns + case ES_FIELD_TYPES.KEYWORD: + case ES_FIELD_TYPES.TEXT: + break; + } + + return { id, schema }; + }); + + const dataGrid = useDataGrid(columns); + + const { + pagination, + resetPagination, + setErrorMessage, + setNoDataMessage, + setRowCount, + setStatus, + setTableItems, + sortingColumns, + tableItems, + } = dataGrid; + + const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr); + + const getPreviewData = async () => { + if (aggsArr.length === 0 || groupByArr.length === 0) { + setTableItems([]); + setRowCount(0); + setNoDataMessage( + i18n.translate('xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody', { + defaultMessage: 'Please choose at least one group-by field and aggregation.', + }) + ); + return; + } + + setErrorMessage(''); + setNoDataMessage(''); + setStatus(INDEX_STATUS.LOADING); + + try { + const resp = await api.getTransformsPreview(previewRequest); + setTableItems(resp.preview); + setRowCount(resp.preview.length); + setPreviewMappings(resp.generated_dest_index.mappings); + setStatus(INDEX_STATUS.LOADED); + + if (resp.preview.length === 0) { + setNoDataMessage( + i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', { + defaultMessage: + 'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.', + }) + ); + } + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setTableItems([]); + setRowCount(0); + setPreviewMappings({ properties: {} }); + setStatus(INDEX_STATUS.ERROR); + } + }; + + useEffect(() => { + resetPagination(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(query)]); + + useEffect(() => { + getPreviewData(); + // custom comparison + /* eslint-disable react-hooks/exhaustive-deps */ + }, [ + indexPatternTitle, + JSON.stringify(aggsArr), + JSON.stringify(groupByArr), + JSON.stringify(query), + /* eslint-enable react-hooks/exhaustive-deps */ + ]); + + if (sortingColumns.length > 0) { + tableItems.sort(multiColumnSortFactory(sortingColumns)); + } + + const pageData = tableItems.slice( + pagination.pageIndex * pagination.pageSize, + (pagination.pageIndex + 1) * pagination.pageSize + ); + + const renderCellValue: RenderCellValue = useMemo(() => { + return ({ + rowIndex, + columnId, + setCellProps, + }: { + rowIndex: number; + columnId: string; + setCellProps: any; + }) => { + const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + const cellValue = pageData.hasOwnProperty(adjustedRowIndex) + ? getNestedProperty(pageData[adjustedRowIndex], columnId, null) + : null; + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + if (cellValue === undefined || cellValue === null) { + return null; + } + + if ( + [ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.DATE_NANOS].includes( + previewMappings.properties[columnId].type + ) + ) { + return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); + } + + if (previewMappings.properties[columnId].type === ES_FIELD_TYPES.BOOLEAN) { + return cellValue ? 'true' : 'false'; + } + + return cellValue; + }; + }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappings.properties]); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap deleted file mode 100644 index b668c7d8e4a69..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap +++ /dev/null @@ -1,71 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Test against strings, objects and arrays. 1`] = ` - - - - name - : - - - - the-name -    - - - - - nested.inner1 - : - - - - the-inner-1 -    - - - - - nested.inner2 - : - - - - the-inner-2 -    - - - - - arrayString - : - - - - ["the-array-string-1","the-array-string-2"] -    - - - - - arrayObject - : - - - - [{"object1":"the-object-1"},{"object2":"the-objects-2"}] -    - - - -`; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts deleted file mode 100644 index d3bf81bba2e56..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts +++ /dev/null @@ -1,35 +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 { SimpleQuery } from '../../../../common'; - -import { getSourceIndexDevConsoleStatement } from './common'; - -describe('Transform: Source Index Preview Common', () => { - test('getSourceIndexDevConsoleStatement()', () => { - const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, - }; - const sourceIndexPreviewDevConsoleStatement = getSourceIndexDevConsoleStatement( - query, - 'the-index-pattern-title' - ); - - expect(sourceIndexPreviewDevConsoleStatement).toBe(`GET the-index-pattern-title/_search -{ - "query": { - "query_string": { - "query": "*", - "default_operator": "AND" - } - } -} -`); - }); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts deleted file mode 100644 index c34675463bf8b..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.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 { PivotQuery } from '../../../../common'; - -export const getSourceIndexDevConsoleStatement = (query: PivotQuery, indexPatternTitle: string) => { - return `GET ${indexPatternTitle}/_search\n${JSON.stringify( - { - query, - }, - null, - 2 - )}\n`; -}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx deleted file mode 100644 index ddd1a1482fd35..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx +++ /dev/null @@ -1,46 +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 { shallow } from 'enzyme'; -import React from 'react'; - -import { getNestedProperty } from '../../../../../../common/utils/object_utils'; -import { getFlattenedFields } from '../../../../common'; - -import { ExpandedRow } from './expanded_row'; - -describe('Transform: ', () => { - test('Test against strings, objects and arrays.', () => { - const source = { - name: 'the-name', - nested: { - inner1: 'the-inner-1', - inner2: 'the-inner-2', - }, - arrayString: ['the-array-string-1', 'the-array-string-2'], - arrayObject: [{ object1: 'the-object-1' }, { object2: 'the-objects-2' }], - } as Record; - - const flattenedSource = getFlattenedFields(source).reduce((p, c) => { - p[c] = getNestedProperty(source, c); - if (p[c] === undefined) { - p[c] = source[`"${c}"`]; - } - return p; - }, {} as Record); - - const props = { - item: { - _id: 'the-id', - _source: flattenedSource, - }, - }; - - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx deleted file mode 100644 index 9b83a3e5da8a8..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx +++ /dev/null @@ -1,22 +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 React from 'react'; - -import { EuiBadge, EuiText } from '@elastic/eui'; - -import { EsDoc } from '../../../../common'; - -export const ExpandedRow: React.FC<{ item: EsDoc }> = ({ item }) => ( - - {Object.entries(item._source).map(([k, value]) => ( - - {k}: - {typeof value === 'string' ? value : JSON.stringify(value)}   - - ))} - -); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx deleted file mode 100644 index 32f6ff9490a0f..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx +++ /dev/null @@ -1,38 +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 React from 'react'; -import { render, wait } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; - -import { getPivotQuery } from '../../../../common'; -import { SearchItems } from '../../../../hooks/use_search_items'; - -import { SourceIndexPreview } from './source_index_preview'; - -jest.mock('../../../../../shared_imports'); -jest.mock('../../../../../app/app_dependencies'); - -describe('Transform: ', () => { - // Using the async/await wait()/done() pattern to avoid act() errors. - test('Minimal initialization', async done => { - // Arrange - const props = { - indexPattern: { - title: 'the-index-pattern-title', - fields: [] as any[], - } as SearchItems['indexPattern'], - query: getPivotQuery('the-query'), - }; - const { getByText } = render(); - - // Act - // Assert - expect(getByText(`Source index ${props.indexPattern.title}`)).toBeInTheDocument(); - await wait(); - done(); - }); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx deleted file mode 100644 index bcdeb7ddb0d36..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx +++ /dev/null @@ -1,293 +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 moment from 'moment-timezone'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { - EuiButtonIcon, - EuiCallOut, - EuiCodeBlock, - EuiCopy, - EuiDataGrid, - EuiFlexGroup, - EuiFlexItem, - EuiProgress, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; - -import { KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/common'; - -import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils'; -import { getNestedProperty } from '../../../../../../common/utils/object_utils'; - -import { - euiDataGridStyle, - euiDataGridToolbarSettings, - EsFieldName, - PivotQuery, - INIT_MAX_COLUMNS, -} from '../../../../common'; -import { SearchItems } from '../../../../hooks/use_search_items'; -import { useToastNotifications } from '../../../../app_dependencies'; - -import { getSourceIndexDevConsoleStatement } from './common'; -import { SOURCE_INDEX_STATUS, useSourceIndexData } from './use_source_index_data'; - -interface SourceIndexPreviewTitle { - indexPatternTitle: string; -} -const SourceIndexPreviewTitle: React.FC = ({ indexPatternTitle }) => ( - - - {i18n.translate('xpack.transform.sourceIndexPreview.sourceIndexPatternTitle', { - defaultMessage: 'Source index {indexPatternTitle}', - values: { indexPatternTitle }, - })} - - -); - -interface Props { - indexPattern: SearchItems['indexPattern']; - query: PivotQuery; -} - -export const SourceIndexPreview: React.FC = React.memo(({ indexPattern, query }) => { - const toastNotifications = useToastNotifications(); - const allFields = indexPattern.fields.map(f => f.name); - const indexPatternFields: string[] = allFields.filter(f => { - if (indexPattern.metaFields.includes(f)) { - return false; - } - - const fieldParts = f.split('.'); - const lastPart = fieldParts.pop(); - if (lastPart === 'keyword' && allFields.includes(fieldParts.join('.'))) { - return false; - } - - return true; - }); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState([]); - - useEffect(() => { - setVisibleColumns(indexPatternFields.splice(0, INIT_MAX_COLUMNS)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [indexPatternFields.join()]); - - const { - errorMessage, - pagination, - setPagination, - setSortingColumns, - rowCount, - sortingColumns, - status, - tableItems: data, - } = useSourceIndexData(indexPattern, query); - - // EuiDataGrid State - const dataGridColumns = [ - ...indexPatternFields.map(id => { - const field = indexPattern.fields.getByName(id); - - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - - switch (field?.type) { - case KBN_FIELD_TYPES.BOOLEAN: - schema = 'boolean'; - break; - case KBN_FIELD_TYPES.DATE: - schema = 'datetime'; - break; - case KBN_FIELD_TYPES.GEO_POINT: - case KBN_FIELD_TYPES.GEO_SHAPE: - schema = 'json'; - break; - case KBN_FIELD_TYPES.NUMBER: - schema = 'numeric'; - break; - } - - return { id, schema }; - }), - ]; - - const onSort = useCallback( - (sc: Array<{ id: string; direction: 'asc' | 'desc' }>) => { - // Check if an unsupported column type for sorting was selected. - const invalidSortingColumnns = sc.reduce((arr, current) => { - const columnType = dataGridColumns.find(dgc => dgc.id === current.id); - if (columnType?.schema === 'json') { - arr.push(current.id); - } - return arr; - }, []); - if (invalidSortingColumnns.length === 0) { - setSortingColumns(sc); - } else { - invalidSortingColumnns.forEach(columnId => { - toastNotifications.addDanger( - i18n.translate('xpack.transform.sourceIndexPreview.invalidSortingColumnError', { - defaultMessage: `The column '{columnId}' cannot be used for sorting.`, - values: { columnId }, - }) - ); - }); - } - }, - [dataGridColumns, setSortingColumns, toastNotifications] - ); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const renderCellValue = useMemo(() => { - return ({ - rowIndex, - columnId, - setCellProps, - }: { - rowIndex: number; - columnId: string; - setCellProps: any; - }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const cellValue = data.hasOwnProperty(adjustedRowIndex) - ? getNestedProperty(data[adjustedRowIndex], columnId, null) - : null; - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - if (cellValue === undefined || cellValue === null) { - return null; - } - - const field = indexPattern.fields.getByName(columnId); - if (field?.type === KBN_FIELD_TYPES.DATE) { - return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); - } - - if (field?.type === KBN_FIELD_TYPES.BOOLEAN) { - return cellValue ? 'true' : 'false'; - } - - return cellValue; - }; - }, [data, indexPattern.fields, pagination.pageIndex, pagination.pageSize]); - - if (status === SOURCE_INDEX_STATUS.LOADED && data.length === 0) { - return ( -
- - -

- {i18n.translate('xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody', { - defaultMessage: - 'The query for the source index returned no results. Please make sure you have sufficient permissions, the index contains documents and your query is not too restrictive.', - })} -

-
-
- ); - } - - const euiCopyText = i18n.translate('xpack.transform.sourceIndexPreview.copyClipboardTooltip', { - defaultMessage: 'Copy Dev Console statement of the source index preview to the clipboard.', - }); - - return ( -
- - - - - - - {(copy: () => void) => ( - - )} - - - -
- {status === SOURCE_INDEX_STATUS.LOADING && } - {status !== SOURCE_INDEX_STATUS.LOADING && ( - - )} -
- {status === SOURCE_INDEX_STATUS.ERROR && ( -
- - - {errorMessage} - - - -
- )} - -
- ); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx deleted file mode 100644 index 5a1d8a8db5b42..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx +++ /dev/null @@ -1,43 +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 { renderHook } from '@testing-library/react-hooks'; -import '@testing-library/jest-dom/extend-expect'; - -import { SimpleQuery } from '../../../../common'; -import { - SOURCE_INDEX_STATUS, - useSourceIndexData, - UseSourceIndexDataReturnType, -} from './use_source_index_data'; - -jest.mock('../../../../hooks/use_api'); - -const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, -}; - -describe('useSourceIndexData', () => { - test('indexPattern set triggers loading', async done => { - const { result, waitForNextUpdate } = renderHook(() => - useSourceIndexData({ id: 'the-id', title: 'the-title', fields: [] }, query) - ); - const sourceIndexObj: UseSourceIndexDataReturnType = result.current; - - await waitForNextUpdate(); - - expect(sourceIndexObj.errorMessage).toBe(''); - expect(sourceIndexObj.status).toBe(SOURCE_INDEX_STATUS.LOADING); - expect(sourceIndexObj.tableItems).toEqual([]); - done(); - }); - - // TODO add more tests to check data retrieved via `api.esSearch()`. - // This needs more investigation in regards to jest's React Hooks support. -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts deleted file mode 100644 index 5301a3c168a51..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts +++ /dev/null @@ -1,143 +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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; - -import { SearchResponse } from 'elasticsearch'; - -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { IIndexPattern } from 'src/plugins/data/public'; - -import { Dictionary } from '../../../../../../common/types/common'; - -import { isDefaultQuery, matchAllQuery, EsDocSource, PivotQuery } from '../../../../common'; -import { useApi } from '../../../../hooks/use_api'; - -export enum SOURCE_INDEX_STATUS { - UNUSED, - LOADING, - LOADED, - ERROR, -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -interface ErrorResponse { - request: Dictionary; - response: Dictionary; - body: { - statusCode: number; - error: string; - message: string; - }; - name: string; - req: Dictionary; - res: Dictionary; -} - -const isErrorResponse = (arg: any): arg is ErrorResponse => { - return arg?.body?.error !== undefined && arg?.body?.message !== undefined; -}; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -type SourceIndexSearchResponse = SearchResponse7; - -type SourceIndexPagination = Pick; -const defaultPagination: SourceIndexPagination = { pageIndex: 0, pageSize: 5 }; - -export interface UseSourceIndexDataReturnType { - errorMessage: string; - pagination: SourceIndexPagination; - setPagination: Dispatch>; - setSortingColumns: Dispatch>; - rowCount: number; - sortingColumns: EuiDataGridSorting['columns']; - status: SOURCE_INDEX_STATUS; - tableItems: EsDocSource[]; -} - -export const useSourceIndexData = ( - indexPattern: IIndexPattern, - query: PivotQuery -): UseSourceIndexDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(SOURCE_INDEX_STATUS.UNUSED); - const [pagination, setPagination] = useState(defaultPagination); - const [sortingColumns, setSortingColumns] = useState([]); - const [rowCount, setRowCount] = useState(0); - const [tableItems, setTableItems] = useState([]); - const api = useApi(); - - useEffect(() => { - setPagination(defaultPagination); - }, [query]); - - const getSourceIndexData = async function() { - setErrorMessage(''); - setStatus(SOURCE_INDEX_STATUS.LOADING); - - const sort: EsSorting = sortingColumns.reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const esSearchRequest = { - index: indexPattern.title, - body: { - // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. - query: isDefaultQuery(query) ? matchAllQuery : query, - from: pagination.pageIndex * pagination.pageSize, - size: pagination.pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }; - - try { - const resp: SourceIndexSearchResponse = await api.esSearch(esSearchRequest); - - const docs = resp.hits.hits.map(d => d._source); - - setRowCount(resp.hits.total.value); - setTableItems(docs); - setStatus(SOURCE_INDEX_STATUS.LOADED); - } catch (e) { - if (isErrorResponse(e)) { - setErrorMessage(`${e.body.error}: ${e.body.message}`); - } else { - setErrorMessage(JSON.stringify(e, null, 2)); - } - setStatus(SOURCE_INDEX_STATUS.ERROR); - } - }; - - useEffect(() => { - getSourceIndexData(); - // custom comparison - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); - return { - errorMessage, - pagination, - setPagination, - setSortingColumns, - rowCount, - sortingColumns, - status, - tableItems, - }; -}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 320e405b5d437..0e6e2c1a38d0e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -35,19 +35,19 @@ import { import { useXJsonMode } from '../../../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; -import { PivotPreview } from '../../../../components/pivot_preview'; +import { DataGrid } from '../../../../../shared_imports'; + +import { + getIndexDevConsoleStatement, + getPivotPreviewDevConsoleStatement, +} from '../../../../common/data_grid'; import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; import { SavedSearchQuery, SearchItems } from '../../../../hooks/use_search_items'; +import { useIndexData } from '../../../../hooks/use_index_data'; +import { usePivotData } from '../../../../hooks/use_pivot_data'; import { useToastNotifications } from '../../../../app_dependencies'; -import { TransformPivotConfig } from '../../../../common'; import { dictionaryToArray, Dictionary } from '../../../../../../common/types/common'; -import { DropDown } from '../aggregation_dropdown'; -import { AggListForm } from '../aggregation_list'; -import { GroupByListForm } from '../group_by_list'; -import { SourceIndexPreview } from '../source_index_preview'; -import { SwitchModal } from './switch_modal'; - import { getPivotQuery, getPreviewRequestBody, @@ -61,11 +61,17 @@ import { PivotGroupByConfig, PivotGroupByConfigDict, PivotSupportedGroupByAggs, + TransformPivotConfig, PIVOT_SUPPORTED_AGGS, PIVOT_SUPPORTED_GROUP_BY_AGGS, } from '../../../../common'; +import { DropDown } from '../aggregation_dropdown'; +import { AggListForm } from '../aggregation_list'; +import { GroupByListForm } from '../group_by_list'; + import { getPivotDropdownOptions } from './common'; +import { SwitchModal } from './switch_modal'; export interface StepDefineExposedState { aggList: PivotAggsConfigDict; @@ -296,7 +302,6 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, return; } } catch (e) { - console.log('Invalid syntax', JSON.stringify(e, null, 2)); // eslint-disable-line no-console setErrorMessage({ query: query.query as string, message: e.message }); } }; @@ -593,6 +598,9 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, /* eslint-enable react-hooks/exhaustive-deps */ ]); + const indexPreviewProps = useIndexData(indexPattern, pivotQuery); + const pivotPreviewProps = usePivotData(indexPattern.title, pivotQuery, aggList, groupByList); + // TODO This should use the actual value of `indices.query.bool.max_clause_count` const maxIndexFields = 1024; const numIndexFields = indexPattern.fields.length; @@ -973,13 +981,37 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, - + - diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index f31514e67003b..b9021f4ee5b11 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -17,8 +17,19 @@ import { EuiText, } from '@elastic/eui'; -import { getPivotQuery, isDefaultQuery, isMatchAllQuery } from '../../../../common'; -import { PivotPreview } from '../../../../components/pivot_preview'; +import { dictionaryToArray } from '../../../../../../common/types/common'; + +import { DataGrid } from '../../../../../shared_imports'; + +import { useToastNotifications } from '../../../../app_dependencies'; +import { + getPivotQuery, + getPivotPreviewDevConsoleStatement, + getPreviewRequestBody, + isDefaultQuery, + isMatchAllQuery, +} from '../../../../common'; +import { usePivotData } from '../../../../hooks/use_pivot_data'; import { SearchItems } from '../../../../hooks/use_search_items'; import { AggListSummary } from '../aggregation_list'; @@ -35,8 +46,25 @@ export const StepDefineSummary: FC = ({ formState: { searchString, searchQuery, groupByList, aggList }, searchItems, }) => { + const toastNotifications = useToastNotifications(); + const pivotAggsArr = dictionaryToArray(aggList); + const pivotGroupByArr = dictionaryToArray(groupByList); const pivotQuery = getPivotQuery(searchQuery); + const previewRequest = getPreviewRequestBody( + searchItems.indexPattern.title, + pivotQuery, + pivotGroupByArr, + pivotAggsArr + ); + + const pivotPreviewProps = usePivotData( + searchItems.indexPattern.title, + pivotQuery, + aggList, + groupByList + ); + return ( @@ -117,11 +145,20 @@ export const StepDefineSummary: FC = ({ - diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index eaaedc2eb77ce..e183712b390cf 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -6,37 +6,39 @@ import React, { FC } from 'react'; -import { SearchItems } from '../../../../hooks/use_search_items'; +import { DataGrid } from '../../../../../shared_imports'; +import { useToastNotifications } from '../../../../app_dependencies'; import { getPivotQuery, TransformPivotConfig } from '../../../../common'; +import { usePivotData } from '../../../../hooks/use_pivot_data'; +import { SearchItems } from '../../../../hooks/use_search_items'; import { applyTransformConfigToDefineState, getDefaultStepDefineState, } from '../../../create_transform/components/step_define/'; -import { PivotPreview } from '../../../../components/pivot_preview'; -interface Props { +interface ExpandedRowPreviewPaneProps { transformConfig: TransformPivotConfig; } -export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { - const previewConfig = applyTransformConfigToDefineState( +export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { + const toastNotifications = useToastNotifications(); + const { aggList, groupByList, searchQuery } = applyTransformConfigToDefineState( getDefaultStepDefineState({} as SearchItems), transformConfig ); - + const pivotQuery = getPivotQuery(searchQuery); const indexPatternTitle = Array.isArray(transformConfig.source.index) ? transformConfig.source.index.join(',') : transformConfig.source.index; + const pivotPreviewProps = usePivotData(indexPatternTitle, pivotQuery, aggList, groupByList); return ( - ); }; diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index 494b6db6aafe0..bcd8e53e3d191 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -17,3 +17,18 @@ export { } from '../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; export { getErrorMessage } from '../../ml/common/util/errors'; + +export { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + multiColumnSortFactory, + useDataGrid, + useRenderCellValue, + DataGrid, + EsSorting, + RenderCellValue, + SearchResponse7, + UseDataGridReturnType, + UseIndexDataReturnType, +} from '../../ml/public/application/components/data_grid'; +export { INDEX_STATUS } from '../../ml/public/application/data_frame_analytics/common'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index deb3053d28658..87e8e2e7a41e2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9443,14 +9443,8 @@ "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixLabel": "分類混同行列", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixPredictedLabel": "予測されたラベル", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTooltip": "マルチクラス混同行列には、分析が実際のクラスで正しくデータポイントを分類した発生数と、別のクラスで誤分類した発生数が含まれます。", - "xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText": "予測があるドキュメントを示す", "xpack.ml.dataframe.analytics.classificationExploration.evaluateJobIdTitle": "分類ジョブID {jobId}の評価", - "xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText": "予測がある最初の{searchSize}のドキュメントを示す", "xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました", - "xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError": "結果を取得できません。インデックスのフィールドデータの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError": "結果を取得できません。ジョブ構成データの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage": "結果が見つかりませんでした。", - "xpack.ml.dataframe.analytics.classificationExploration.regressionDocsLink": "回帰評価ドキュメント ", "xpack.ml.dataframe.analytics.classificationExploration.showActions": "アクションを表示", "xpack.ml.dataframe.analytics.classificationExploration.showAllColumns": "すべての列を表示", "xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle": "分類ジョブID {jobId}のデスティネーションインデックス", @@ -9525,31 +9519,17 @@ "xpack.ml.dataframe.analytics.create.startDataFrameAnalyticsSuccessMessage": "データフレーム分析 {jobId} の開始リクエストが受け付けられました。", "xpack.ml.dataframe.analytics.create.trainingPercentLabel": "トレーニングパーセンテージ", "xpack.ml.dataframe.analytics.exploration.colorRangeLegendTitle": "機能影響スコア", - "xpack.ml.dataframe.analytics.exploration.dataGridAriaLabel": "外れ値検出結果表", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeLabel": "実験的", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeTooltipContent": "データフレーム分析は実験段階の機能です。フィードバックをお待ちしています。", "xpack.ml.dataframe.analytics.exploration.indexError": "インデックスデータの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.exploration.jobIdTitle": "外れ値検出ジョブID {jobId}", - "xpack.ml.dataframe.analytics.exploration.noDataCalloutBody": "インデックスのクエリが結果を返しませんでした。インデックスにドキュメントが含まれていて、クエリ要件が妥当であることを確認してください。", "xpack.ml.dataframe.analytics.exploration.title": "分析の探索", - "xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText": "予測があるドキュメントを示す", - "xpack.ml.dataframe.analytics.regressionExploration.evaluateError": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.evaluateJobIdTitle": "回帰ジョブID {jobId}の評価", - "xpack.ml.dataframe.analytics.regressionExploration.fieldSelection": "{docFieldsCount, number} 件中 showing {selectedFieldsLength, number} 件の{docFieldsCount, plural, one {フィールド} other {フィールド}}", - "xpack.ml.dataframe.analytics.regressionExploration.firstDocumentsShownHelpText": "予測がある最初の{searchSize}のドキュメントを示す", - "xpack.ml.dataframe.analytics.regressionExploration.generalError": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "一般化エラー", "xpack.ml.dataframe.analytics.regressionExploration.indexError": "インデックスデータの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError": "結果を取得できません。インデックスのフィールドデータの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError": "結果を取得できません。ジョブ構成データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "平均二乗エラー", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent": "回帰分析モデルの実行の効果を測定します。真値と予測値の間の差異の二乗平均合計。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody": "インデックスのクエリが結果を返しませんでした。ジョブが完了済みで、インデックスにドキュメントがあることを確認してください。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle": "空のインデックスクエリ結果。", - "xpack.ml.dataframe.analytics.regressionExploration.noIndexCalloutBody": "インデックスのクエリが結果を返しませんでした。デスティネーションインデックスが存在し、ドキュメントがあることを確認してください。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorBody": "クエリ構文が無効であり、結果を返しませんでした。クエリ構文を確認し、再試行してください。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorMessage": "クエリをパースできません。", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredText": "R の二乗", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent": "適合度を表します。モデルによる観察された結果の複製の効果を測定します。", "xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle": "回帰ジョブID {jobId}のデスティネーションインデックス", @@ -15567,19 +15547,11 @@ "xpack.transform.newTransform.searchSelection.savedObjectType.indexPattern": "インデックスパターン", "xpack.transform.newTransform.searchSelection.savedObjectType.search": "保存検索", "xpack.transform.pivotPreview.copyClipboardTooltip": "ピボットプレビューの開発コンソールステートメントをクリップボードにコピーします。", - "xpack.transform.pivotPreview.PivotPreviewError": "ピボットプレビューの読み込み中にエラーが発生しました。", "xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody": "group-by フィールドと集約を 1 つ以上選んでください。", "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody": "プレビューリクエストはデータを返しませんでした。オプションのクエリがデータを返し、グループ分け基準により使用されるフィールドと集約フィールドに値が存在することを確認してください。", - "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutTitle": "ピボットプレビューを利用できません", "xpack.transform.pivotPreview.PivotPreviewTitle": "ピボットプレビューを変換", "xpack.transform.progress": "進捗", "xpack.transform.sourceIndex": "ソースインデックス", - "xpack.transform.sourceIndexPreview.copyClipboardTooltip": "ソースインデックスプレビューの開発コンソールステートメントをクリップボードにコピーします。", - "xpack.transform.sourceIndexPreview.invalidSortingColumnError": "列「{columnId}」は並べ替えに使用できません。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody": "ソースインデックスのクエリが結果を返しませんでした。インデックスにドキュメントが含まれていて、クエリ要件が妥当であることを確認してください。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutTitle": "ソースインデックスクエリの結果がありません", - "xpack.transform.sourceIndexPreview.sourceIndexPatternError": "ソースインデックスデータの読み込み中にエラーが発生しました。", - "xpack.transform.sourceIndexPreview.sourceIndexPatternTitle": "ソースインデックス {indexPatternTitle}", "xpack.transform.statsBar.batchTransformsLabel": "一斉", "xpack.transform.statsBar.continuousTransformsLabel": "連続", "xpack.transform.statsBar.failedTransformsLabel": "失敗", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3b757f169828c..4a5ed3291a915 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9446,14 +9446,8 @@ "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixLabel": "分类混淆矩阵", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixPredictedLabel": "预测标签", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTooltip": "多类混淆矩阵包含分析使用数据点的实际类正确分类数据点的次数以及分析使用其他类错误分类这些数据点的次数", - "xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText": "正在显示有相关预测存在的文档", "xpack.ml.dataframe.analytics.classificationExploration.evaluateJobIdTitle": "分类作业 ID {jobId} 的评估", - "xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText": "正在显示有相关预测存在的前 {searchSize} 个文档", "xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估", - "xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError": "无法提取结果。加载索引的字段数据时发生错误。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError": "无法提取结果。加载作业配置数据时发生错误。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage": "未找到结果。", - "xpack.ml.dataframe.analytics.classificationExploration.regressionDocsLink": "回归评估文档 ", "xpack.ml.dataframe.analytics.classificationExploration.showActions": "显示操作", "xpack.ml.dataframe.analytics.classificationExploration.showAllColumns": "显示所有列", "xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle": "分类作业 ID {jobId} 的目标索引", @@ -9528,31 +9522,17 @@ "xpack.ml.dataframe.analytics.create.startDataFrameAnalyticsSuccessMessage": "数据帧分析 {jobId} 启动请求已确认。", "xpack.ml.dataframe.analytics.create.trainingPercentLabel": "训练百分比", "xpack.ml.dataframe.analytics.exploration.colorRangeLegendTitle": "功能影响分数", - "xpack.ml.dataframe.analytics.exploration.dataGridAriaLabel": "离群值检测结果表", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeLabel": "实验性", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeTooltipContent": "数据帧分析为实验功能。我们很乐意听取您的反馈意见。", "xpack.ml.dataframe.analytics.exploration.indexError": "加载索引数据时出错。", "xpack.ml.dataframe.analytics.exploration.jobIdTitle": "离群值检测作业 ID {jobId}", - "xpack.ml.dataframe.analytics.exploration.noDataCalloutBody": "该索引的查询未返回结果。请确保索引包含文档且您的查询限制不过于严格。", "xpack.ml.dataframe.analytics.exploration.title": "分析浏览", - "xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText": "正在显示有相关预测存在的文档", - "xpack.ml.dataframe.analytics.regressionExploration.evaluateError": "加载数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.evaluateJobIdTitle": "回归作业 ID {jobId} 的评估", - "xpack.ml.dataframe.analytics.regressionExploration.fieldSelection": "已选择 {docFieldsCount, number} 个{docFieldsCount, plural, one {字段} other {字段}}中的 {selectedFieldsLength, number} 个", - "xpack.ml.dataframe.analytics.regressionExploration.firstDocumentsShownHelpText": "正在显示有相关预测存在的前 {searchSize} 个文档", - "xpack.ml.dataframe.analytics.regressionExploration.generalError": "加载数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "泛化误差", "xpack.ml.dataframe.analytics.regressionExploration.indexError": "加载索引数据时出错。", - "xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError": "无法提取结果。加载索引的字段数据时发生错误。", - "xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError": "无法提取结果。加载作业配置数据时发生错误。", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "均方误差", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent": "度量回归分析模型的表现。真实值与预测值之差的平均平方和。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody": "该索引的查询未返回结果。请确保作业已完成且索引包含文档。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle": "空的索引查询结果。", - "xpack.ml.dataframe.analytics.regressionExploration.noIndexCalloutBody": "该索引的查询未返回结果。请确保目标索引存在且包含文档。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorBody": "查询语法无效,未返回任何结果。请检查查询语法并重试。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorMessage": "无法解析查询。", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredText": "R 平方", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent": "表示拟合优度。度量模型复制被观察结果的优良性。", "xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle": "回归作业 ID {jobId} 的目标索引", @@ -15571,19 +15551,11 @@ "xpack.transform.newTransform.searchSelection.savedObjectType.indexPattern": "索引模式", "xpack.transform.newTransform.searchSelection.savedObjectType.search": "已保存搜索", "xpack.transform.pivotPreview.copyClipboardTooltip": "将透视预览的开发控制台语句复制到剪贴板。", - "xpack.transform.pivotPreview.PivotPreviewError": "加载数据透视表预览时出错。", "xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody": "请至少选择一个分组依据字段和聚合。", "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody": "预览请求未返回任何数据。请确保可选查询返回数据且存在分组依据和聚合字段使用的字段的值。", - "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutTitle": "数据透视表预览不可用", "xpack.transform.pivotPreview.PivotPreviewTitle": "转换数据透视表预览", "xpack.transform.progress": "进度", "xpack.transform.sourceIndex": "源索引", - "xpack.transform.sourceIndexPreview.copyClipboardTooltip": "将源索引预览的开发控制台语句复制到剪贴板。", - "xpack.transform.sourceIndexPreview.invalidSortingColumnError": "列“{columnId}”无法用于排序。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody": "源索引的查询未返回结果。请确保索引包含文档且您的查询限制不过于严格。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutTitle": "源索引查询结果为空。", - "xpack.transform.sourceIndexPreview.sourceIndexPatternError": "加载源索引数据时出错。", - "xpack.transform.sourceIndexPreview.sourceIndexPatternTitle": "源索引 {indexPatternTitle}", "xpack.transform.statsBar.batchTransformsLabel": "批量", "xpack.transform.statsBar.continuousTransformsLabel": "连续", "xpack.transform.statsBar.failedTransformsLabel": "失败", diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index bea6b814ee8a3..dbdbca0368146 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -90,7 +90,7 @@ export default function({ getService }: FtrProviderContext) { mode: 'batch', progress: '100', }, - sourcePreview: { + indexPreview: { columns: 20, rows: 5, }, @@ -144,7 +144,7 @@ export default function({ getService }: FtrProviderContext) { mode: 'batch', progress: '100', }, - sourcePreview: { + indexPreview: { columns: 20, rows: 5, }, @@ -180,14 +180,14 @@ export default function({ getService }: FtrProviderContext) { await transform.wizard.assertDefineStepActive(); }); - it('loads the source index preview', async () => { - await transform.wizard.assertSourceIndexPreviewLoaded(); + it('loads the index preview', async () => { + await transform.wizard.assertIndexPreviewLoaded(); }); - it('shows the source index preview', async () => { - await transform.wizard.assertSourceIndexPreview( - testData.expected.sourcePreview.columns, - testData.expected.sourcePreview.rows + it('shows the index preview', async () => { + await transform.wizard.assertIndexPreview( + testData.expected.indexPreview.columns, + testData.expected.indexPreview.rows ); }); diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index 993bd3a79abbc..fd5673de0d7a7 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -65,7 +65,7 @@ export default function({ getService }: FtrProviderContext) { progress: '100', }, sourceIndex: 'ft_farequote', - sourcePreview: { + indexPreview: { column: 2, values: ['ASA'], }, @@ -101,14 +101,14 @@ export default function({ getService }: FtrProviderContext) { await transform.wizard.assertDefineStepActive(); }); - it('loads the source index preview', async () => { - await transform.wizard.assertSourceIndexPreviewLoaded(); + it('loads the index preview', async () => { + await transform.wizard.assertIndexPreviewLoaded(); }); - it('shows the filtered source index preview', async () => { - await transform.wizard.assertSourceIndexPreviewColumnValues( - testData.expected.sourcePreview.column, - testData.expected.sourcePreview.values + it('shows the filtered index preview', async () => { + await transform.wizard.assertIndexPreviewColumnValues( + testData.expected.indexPreview.column, + testData.expected.indexPreview.values ); }); diff --git a/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts b/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts index bada1d42b564a..bd7d76e34b447 100644 --- a/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts +++ b/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts @@ -41,7 +41,7 @@ export function MachineLearningDataFrameAnalyticsProvider( }, async assertRegressionTablePanelExists() { - await testSubjects.existOrFail('mlDFAnalyticsRegressionExplorationTablePanel'); + await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); }, async assertClassificationEvaluatePanelElementsExists() { @@ -50,7 +50,7 @@ export function MachineLearningDataFrameAnalyticsProvider( }, async assertClassificationTablePanelExists() { - await testSubjects.existOrFail('mlDFAnalyticsClassificationExplorationTablePanel'); + await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); }, async assertOutlierTablePanelExists() { diff --git a/x-pack/test/functional/services/transform_ui/wizard.ts b/x-pack/test/functional/services/transform_ui/wizard.ts index ba9096a372d9a..e63af493438d6 100644 --- a/x-pack/test/functional/services/transform_ui/wizard.ts +++ b/x-pack/test/functional/services/transform_ui/wizard.ts @@ -52,8 +52,8 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await this.assertDetailsSummaryExists(); }, - async assertSourceIndexPreviewExists(subSelector?: string) { - let selector = 'transformSourceIndexPreview'; + async assertIndexPreviewExists(subSelector?: string) { + let selector = 'transformIndexPreview'; if (subSelector !== undefined) { selector = `${selector} ${subSelector}`; } else { @@ -62,8 +62,8 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail(selector); }, - async assertSourceIndexPreviewLoaded() { - await this.assertSourceIndexPreviewExists('loaded'); + async assertIndexPreviewLoaded() { + await this.assertIndexPreviewExists('loaded'); }, async assertPivotPreviewExists(subSelector?: string) { @@ -124,10 +124,10 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { }); }, - async assertSourceIndexPreview(columns: number, rows: number) { + async assertIndexPreview(columns: number, rows: number) { await retry.tryForTime(2000, async () => { // get a 2D array of rows and cell values - const rowsData = await this.parseEuiDataGrid('transformSourceIndexPreview'); + const rowsData = await this.parseEuiDataGrid('transformIndexPreview'); expect(rowsData).to.length( rows, @@ -143,8 +143,8 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { }); }, - async assertSourceIndexPreviewColumnValues(column: number, values: string[]) { - await this.assertEuiDataGridColumnValues('transformSourceIndexPreview', column, values); + async assertIndexPreviewColumnValues(column: number, values: string[]) { + await this.assertEuiDataGridColumnValues('transformIndexPreview', column, values); }, async assertPivotPreviewColumnValues(column: number, values: string[]) { From 66074f9042be279a3719e397d232c372fe9afc19 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Fri, 24 Apr 2020 14:56:59 +0300 Subject: [PATCH 31/35] Index pattern management UI -> TypeScript and New Platform Ready (edit_index_pattern) (#64184) * draft * Converted edit_index_pattern to React. Created 'tab' component. * Some fixes * returned state_container * Fixed tests and translation * Some refactoring * Fixed tests * rermove unused translations * update snapshots * Some refactoring Co-authored-by: Elastic Machine Co-authored-by: Matt Kime --- .../edit_index_pattern.html | 151 +----- .../edit_index_pattern/edit_index_pattern.js | 511 ------------------ .../edit_index_pattern/edit_index_pattern.tsx | 238 ++++++++ .../edit_index_pattern_state_container.ts | 5 +- .../edit_index_pattern/edit_sections.js | 80 --- .../edit_index_pattern/field_controls.html | 19 - .../edit_index_pattern/index.js | 157 +++++- .../index_header/index_header.tsx | 2 +- .../indexed_fields_table.tsx | 10 +- .../scripted_field_table.test.tsx.snap | 10 +- .../scripted_fields_table.tsx | 11 +- .../edit_index_pattern/tabs/index.ts | 20 + .../edit_index_pattern/tabs/tabs.tsx | 267 +++++++++ .../edit_index_pattern/tabs/utils.ts | 146 +++++ test/functional/page_objects/settings_page.ts | 20 +- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 17 files changed, 861 insertions(+), 792 deletions(-) delete mode 100644 src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.tsx delete mode 100644 src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_sections.js delete mode 100644 src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/field_controls.html create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/index.ts create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/tabs.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html index 0376df6bbdc58..0bf7c7f0bdfbe 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html @@ -6,156 +6,7 @@ role="region" aria-label="{{::'kbn.management.editIndexPattern.detailsAria' | i18n: { defaultMessage: 'Index pattern details' } }}" > - -
- -
-

- - - - - - - - - - - - - - - {{tag.name}} - - - - -

- -
- -
-

- - - - - -

-
- -
- - -
-
- - -
- -
-
- -
-
-
- - -
- -
- - -
-
-
- - -
-
- -
- -
- -
- -
-
- - -
-
- -
- -
-
+
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js deleted file mode 100644 index 3239a17f109e4..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js +++ /dev/null @@ -1,511 +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 _ from 'lodash'; -import { HashRouter } from 'react-router-dom'; -import { IndexHeader } from './index_header'; -import { CreateEditField } from './create_edit_field'; -import { docTitle } from 'ui/doc_title'; -import { KbnUrlProvider } from 'ui/url'; -import { IndicesEditSectionsProvider } from './edit_sections'; -import { fatalError, toastNotifications } from 'ui/notify'; -import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors'; -import uiRoutes from 'ui/routes'; -import { uiModules } from 'ui/modules'; -import template from './edit_index_pattern.html'; -import createEditFieldtemplate from './create_edit_field.html'; -import { fieldWildcardMatcher } from '../../../../../../../../plugins/kibana_utils/public'; -import { subscribeWithScope } from '../../../../../../../../plugins/kibana_legacy/public'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { SourceFiltersTable } from './source_filters_table'; -import { IndexedFieldsTable } from './indexed_fields_table'; -import { ScriptedFieldsTable } from './scripted_fields_table'; -import { i18n } from '@kbn/i18n'; -import { I18nContext } from 'ui/i18n'; -import { npStart } from 'ui/new_platform'; -import { - getEditBreadcrumbs, - getEditFieldBreadcrumbs, - getCreateFieldBreadcrumbs, -} from '../breadcrumbs'; -import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS, TAB_SOURCE_FILTERS } from './constants'; -import { createEditIndexPatternPageStateContainer } from './edit_index_pattern_state_container'; - -const REACT_SOURCE_FILTERS_DOM_ELEMENT_ID = 'reactSourceFiltersTable'; -const REACT_INDEXED_FIELDS_DOM_ELEMENT_ID = 'reactIndexedFieldsTable'; -const REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID = 'reactScriptedFieldsTable'; -const REACT_INDEX_HEADER_DOM_ELEMENT_ID = 'reactIndexHeader'; - -const EDIT_FIELD_PATH = '/management/kibana/index_patterns/{{indexPattern.id}}/field/{{name}}'; - -function updateSourceFiltersTable($scope) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_SOURCE_FILTERS_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - { - $scope.editSections = $scope.editSectionsProvider( - $scope.indexPattern, - $scope.fieldFilter, - $scope.indexPatternListProvider - ); - $scope.refreshFilters(); - $scope.$apply(); - }} - /> - , - node - ); - }); -} - -function destroySourceFiltersTable() { - const node = document.getElementById(REACT_SOURCE_FILTERS_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -function updateScriptedFieldsTable($scope) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - { - $scope.kbnUrl.changePath(EDIT_FIELD_PATH, field); - $scope.$apply(); - }, - getRouteHref: (obj, route) => $scope.kbnUrl.getRouteHref(obj, route), - }} - onRemoveField={() => { - $scope.editSections = $scope.editSectionsProvider( - $scope.indexPattern, - $scope.fieldFilter, - $scope.indexPatternListProvider - ); - $scope.refreshFilters(); - $scope.$apply(); - }} - /> - , - node - ); - }); -} - -function destroyScriptedFieldsTable() { - const node = document.getElementById(REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -function updateIndexedFieldsTable($scope) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_INDEXED_FIELDS_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - { - $scope.kbnUrl.changePath(EDIT_FIELD_PATH, field); - $scope.$apply(); - }, - getFieldInfo: $scope.getFieldInfo, - }} - /> - , - node - ); - }); -} - -function destroyIndexedFieldsTable() { - const node = document.getElementById(REACT_INDEXED_FIELDS_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -function destroyIndexHeader() { - const node = document.getElementById(REACT_INDEX_HEADER_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -function renderIndexHeader($scope, config) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_INDEX_HEADER_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - - , - node - ); - }); -} - -function handleTabChange($scope, newTab) { - destroyIndexedFieldsTable(); - destroySourceFiltersTable(); - destroyScriptedFieldsTable(); - updateTables($scope, newTab); -} - -function updateTables($scope, currentTab) { - switch (currentTab) { - case TAB_SCRIPTED_FIELDS: - return updateScriptedFieldsTable($scope); - case TAB_INDEXED_FIELDS: - return updateIndexedFieldsTable($scope); - case TAB_SOURCE_FILTERS: - return updateSourceFiltersTable($scope); - } -} - -uiRoutes.when('/management/kibana/index_patterns/:indexPatternId', { - template, - k7Breadcrumbs: getEditBreadcrumbs, - resolve: { - indexPattern: function($route, Promise, redirectWhenMissing) { - const { indexPatterns } = npStart.plugins.data; - return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( - redirectWhenMissing('/management/kibana/index_patterns') - ); - }, - }, -}); - -uiModules - .get('apps/management') - .controller('managementIndexPatternsEdit', function( - $scope, - $location, - $route, - Promise, - config, - Private - ) { - const { - startSyncingState, - stopSyncingState, - setCurrentTab, - getCurrentTab, - state$, - } = createEditIndexPatternPageStateContainer({ - useHashedUrl: config.get('state:storeInSessionStorage'), - defaultTab: TAB_INDEXED_FIELDS, - }); - - $scope.getCurrentTab = getCurrentTab; - $scope.setCurrentTab = setCurrentTab; - - const stateChangedSub = subscribeWithScope( - $scope, - state$, - { - next: ({ tab }) => { - handleTabChange($scope, tab); - }, - }, - fatalError - ); - - handleTabChange($scope, getCurrentTab()); // setup initial tab depending on initial tab state - - startSyncingState(); // starts syncing state between state container and url - - const destroyState = () => { - stateChangedSub.unsubscribe(); - stopSyncingState(); - }; - - $scope.fieldWildcardMatcher = (...args) => - fieldWildcardMatcher(...args, config.get('metaFields')); - $scope.editSectionsProvider = Private(IndicesEditSectionsProvider); - $scope.kbnUrl = Private(KbnUrlProvider); - $scope.indexPattern = $route.current.locals.indexPattern; - $scope.indexPatternListProvider = npStart.plugins.indexPatternManagement.list; - $scope.indexPattern.tags = npStart.plugins.indexPatternManagement.list.getIndexPatternTags( - $scope.indexPattern, - $scope.indexPattern.id === config.get('defaultIndex') - ); - $scope.getFieldInfo = npStart.plugins.indexPatternManagement.list.getFieldInfo; - docTitle.change($scope.indexPattern.title); - - const otherPatterns = _.filter($route.current.locals.indexPatterns, pattern => { - return pattern.id !== $scope.indexPattern.id; - }); - - $scope.$watch('indexPattern.fields', function() { - $scope.editSections = $scope.editSectionsProvider( - $scope.indexPattern, - $scope.fieldFilter, - npStart.plugins.indexPatternManagement.list - ); - $scope.refreshFilters(); - $scope.fields = $scope.indexPattern.getNonScriptedFields(); - }); - - $scope.refreshFilters = function() { - const indexedFieldTypes = []; - const scriptedFieldLanguages = []; - $scope.indexPattern.fields.forEach(field => { - if (field.scripted) { - scriptedFieldLanguages.push(field.lang); - } else { - indexedFieldTypes.push(field.type); - } - }); - - $scope.indexedFieldTypes = _.unique(indexedFieldTypes); - $scope.scriptedFieldLanguages = _.unique(scriptedFieldLanguages); - }; - - $scope.changeFilter = function(filter, val) { - $scope[filter] = val || ''; // null causes filter to check for null explicitly - }; - - $scope.$watchCollection('indexPattern.fields', function() { - $scope.conflictFields = $scope.indexPattern.fields.filter(field => field.type === 'conflict'); - }); - - $scope.refreshFields = function() { - const confirmMessage = i18n.translate('kbn.management.editIndexPattern.refreshLabel', { - defaultMessage: 'This action resets the popularity counter of each field.', - }); - const confirmModalOptions = { - confirmButtonText: i18n.translate('kbn.management.editIndexPattern.refreshButton', { - defaultMessage: 'Refresh', - }), - title: i18n.translate('kbn.management.editIndexPattern.refreshHeader', { - defaultMessage: 'Refresh field list?', - }), - }; - - npStart.core.overlays - .openConfirm(confirmMessage, confirmModalOptions) - .then(async isConfirmed => { - if (isConfirmed) { - await $scope.indexPattern.init(true); - $scope.fields = $scope.indexPattern.getNonScriptedFields(); - } - }); - }; - - $scope.removePattern = function() { - function doRemove() { - if ($scope.indexPattern.id === config.get('defaultIndex')) { - config.remove('defaultIndex'); - - if (otherPatterns.length) { - config.set('defaultIndex', otherPatterns[0].id); - } - } - - Promise.resolve($scope.indexPattern.destroy()) - .then(function() { - $location.url('/management/kibana/index_patterns'); - }) - .catch(fatalError); - } - - const confirmModalOptions = { - confirmButtonText: i18n.translate('kbn.management.editIndexPattern.deleteButton', { - defaultMessage: 'Delete', - }), - title: i18n.translate('kbn.management.editIndexPattern.deleteHeader', { - defaultMessage: 'Delete index pattern?', - }), - }; - - npStart.core.overlays.openConfirm('', confirmModalOptions).then(isConfirmed => { - if (isConfirmed) { - doRemove(); - } - }); - }; - - $scope.setDefaultPattern = function() { - config.set('defaultIndex', $scope.indexPattern.id); - }; - - $scope.setIndexPatternsTimeField = function(field) { - if (field.type !== 'date') { - const errorMessage = i18n.translate('kbn.management.editIndexPattern.notDateErrorMessage', { - defaultMessage: 'That field is a {fieldType} not a date.', - values: { fieldType: field.type }, - }); - toastNotifications.addDanger(errorMessage); - return; - } - $scope.indexPattern.timeFieldName = field.name; - return $scope.indexPattern.save(); - }; - - $scope.$watch('fieldFilter', () => { - $scope.editSections = $scope.editSectionsProvider( - $scope.indexPattern, - $scope.fieldFilter, - npStart.plugins.indexPatternManagement.list - ); - - if ($scope.fieldFilter === undefined) { - return; - } - - updateTables($scope, getCurrentTab()); - }); - - $scope.$watch('indexedFieldTypeFilter', () => { - if ($scope.indexedFieldTypeFilter !== undefined && getCurrentTab() === TAB_INDEXED_FIELDS) { - updateIndexedFieldsTable($scope); - } - }); - - $scope.$watch('scriptedFieldLanguageFilter', () => { - if ( - $scope.scriptedFieldLanguageFilter !== undefined && - getCurrentTab() === TAB_SCRIPTED_FIELDS - ) { - updateScriptedFieldsTable($scope); - } - }); - - $scope.$on('$destroy', () => { - destroyIndexedFieldsTable(); - destroyScriptedFieldsTable(); - destroySourceFiltersTable(); - destroyIndexHeader(); - destroyState(); - }); - - renderIndexHeader($scope, config); - }); - -// routes for create edit field. Will be removed after migartion all component to react. -const REACT_FIELD_EDITOR_ID = 'reactFieldEditor'; -const renderCreateEditField = ($scope, $route, getConfig, $http, fieldFormatEditors) => { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_FIELD_EDITOR_ID); - if (!node) { - return; - } - - render( - - - - - , - node - ); - }); -}; - -const destroyCreateEditField = () => { - const node = document.getElementById(REACT_FIELD_EDITOR_ID); - node && unmountComponentAtNode(node); -}; - -uiRoutes - .when('/management/kibana/index_patterns/:indexPatternId/field/:fieldName*', { - mode: 'edit', - k7Breadcrumbs: getEditFieldBreadcrumbs, - }) - .when('/management/kibana/index_patterns/:indexPatternId/create-field/', { - mode: 'create', - k7Breadcrumbs: getCreateFieldBreadcrumbs, - }) - .defaults(/management\/kibana\/index_patterns\/[^\/]+\/(field|create-field)(\/|$)/, { - template: createEditFieldtemplate, - mapBreadcrumbs($route, breadcrumbs) { - const { indexPattern } = $route.current.locals; - return breadcrumbs.map(crumb => { - if (crumb.id !== indexPattern.id) { - return crumb; - } - - return { - ...crumb, - display: indexPattern.title, - }; - }); - }, - resolve: { - indexPattern: function($route, Promise, redirectWhenMissing) { - const { indexPatterns } = npStart.plugins.data; - return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( - redirectWhenMissing('/management/kibana/index_patterns') - ); - }, - }, - controllerAs: 'fieldSettings', - controller: function FieldEditorPageController($scope, $route, $http, Private, config) { - const getConfig = (...args) => config.get(...args); - const fieldFormatEditors = Private(RegistryFieldFormatEditorsProvider); - - renderCreateEditField($scope, $route, getConfig, $http, fieldFormatEditors); - - $scope.$on('$destroy', () => { - destroyCreateEditField(); - }); - }, - }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.tsx new file mode 100644 index 0000000000000..e869ac84c2db2 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.tsx @@ -0,0 +1,238 @@ +/* + * 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 { filter } from 'lodash'; +import React, { useEffect, useState, useCallback } from 'react'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiBadge, + EuiText, + EuiLink, + EuiIcon, + EuiCallOut, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { IndexPattern, IndexPatternField } from '../../../../../../../../plugins/data/public'; +import { + ChromeDocTitle, + NotificationsStart, + OverlayStart, +} from '../../../../../../../../core/public'; +import { IndexPatternManagementStart } from '../../../../../../../../plugins/index_pattern_management/public'; +import { Tabs } from './tabs'; +import { IndexHeader } from './index_header'; + +interface EditIndexPatternProps extends RouteComponentProps { + indexPattern: IndexPattern; + indexPatterns: IndexPattern[]; + config: Record; + services: { + notifications: NotificationsStart; + docTitle: ChromeDocTitle; + overlays: OverlayStart; + indexPatternManagement: IndexPatternManagementStart; + }; +} + +const mappingAPILink = i18n.translate( + 'kbn.management.editIndexPattern.timeFilterLabel.mappingAPILink', + { + defaultMessage: 'Mapping API', + } +); + +const mappingConflictHeader = i18n.translate( + 'kbn.management.editIndexPattern.mappingConflictHeader', + { + defaultMessage: 'Mapping conflict', + } +); + +const confirmMessage = i18n.translate('kbn.management.editIndexPattern.refreshLabel', { + defaultMessage: 'This action resets the popularity counter of each field.', +}); + +const confirmModalOptionsRefresh = { + confirmButtonText: i18n.translate('kbn.management.editIndexPattern.refreshButton', { + defaultMessage: 'Refresh', + }), + title: i18n.translate('kbn.management.editIndexPattern.refreshHeader', { + defaultMessage: 'Refresh field list?', + }), +}; + +const confirmModalOptionsDelete = { + confirmButtonText: i18n.translate('kbn.management.editIndexPattern.deleteButton', { + defaultMessage: 'Delete', + }), + title: i18n.translate('kbn.management.editIndexPattern.deleteHeader', { + defaultMessage: 'Delete index pattern?', + }), +}; + +export const EditIndexPattern = withRouter( + ({ indexPattern, indexPatterns, config, services, history, location }: EditIndexPatternProps) => { + const [fields, setFields] = useState(indexPattern.getNonScriptedFields()); + const [conflictedFields, setConflictedFields] = useState( + indexPattern.fields.filter(field => field.type === 'conflict') + ); + const [defaultIndex, setDefaultIndex] = useState(config.get('defaultIndex')); + const [tags, setTags] = useState([]); + + useEffect(() => { + setFields(indexPattern.getNonScriptedFields()); + setConflictedFields(indexPattern.fields.filter(field => field.type === 'conflict')); + }, [indexPattern, indexPattern.fields]); + + useEffect(() => { + const indexPatternTags = + services.indexPatternManagement.list.getIndexPatternTags( + indexPattern, + indexPattern.id === defaultIndex + ) || []; + setTags(indexPatternTags); + }, [defaultIndex, indexPattern, services.indexPatternManagement.list]); + + const setDefaultPattern = useCallback(() => { + config.set('defaultIndex', indexPattern.id); + setDefaultIndex(indexPattern.id || ''); + }, [config, indexPattern.id]); + + const refreshFields = () => { + services.overlays + .openConfirm(confirmMessage, confirmModalOptionsRefresh) + .then(async isConfirmed => { + if (isConfirmed) { + await indexPattern.init(true); + setFields(indexPattern.getNonScriptedFields()); + } + }); + }; + + const removePattern = () => { + function doRemove() { + if (indexPattern.id === defaultIndex) { + config.remove('defaultIndex'); + const otherPatterns = filter(indexPatterns, pattern => { + return pattern.id !== indexPattern.id; + }); + + if (otherPatterns.length) { + config.set('defaultIndex', otherPatterns[0].id); + } + } + + Promise.resolve(indexPattern.destroy()).then(function() { + history.push('/management/kibana/index_patterns'); + }); + } + + services.overlays.openConfirm('', confirmModalOptionsDelete).then(isConfirmed => { + if (isConfirmed) { + doRemove(); + } + }); + }; + + const timeFilterHeader = i18n.translate('kbn.management.editIndexPattern.timeFilterHeader', { + defaultMessage: "Time Filter field name: '{timeFieldName}'", + values: { timeFieldName: indexPattern.timeFieldName }, + }); + + const mappingConflictLabel = i18n.translate( + 'kbn.management.editIndexPattern.mappingConflictLabel', + { + defaultMessage: + '{conflictFieldsLength, plural, one {A field is} other {# fields are}} defined as several types (string, integer, etc) across the indices that match this pattern. You may still be able to use these conflict fields in parts of Kibana, but they will be unavailable for functions that require Kibana to know their type. Correcting this issue will require reindexing your data.', + values: { conflictFieldsLength: conflictedFields.length }, + } + ); + + services.docTitle.change(indexPattern.title); + + const showTagsSection = Boolean(indexPattern.timeFieldName || (tags && tags.length > 0)); + + return ( + + + + + {showTagsSection && ( + + {Boolean(indexPattern.timeFieldName) && ( + + {timeFilterHeader} + + )} + {tags.map((tag: any) => ( + + {tag.name} + + ))} + + )} + + +

+ {indexPattern.title} }} + />{' '} + + {mappingAPILink} + + +

+
+ {conflictedFields.length > 0 && ( + +

{mappingConflictLabel}

+
+ )} +
+ + + +
+ ); + } +); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts index 473417a7aabd6..5723a596f95d5 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts @@ -25,7 +25,7 @@ import { } from '../../../../../../../../plugins/kibana_utils/public'; interface IEditIndexPatternState { - tab: string; // TODO: type those 3 tabs with enum, when edit_index_pattern.js migrated to ts + tab: string; } /** @@ -38,7 +38,6 @@ export function createEditIndexPatternPageStateContainer({ defaultTab: string; useHashedUrl: boolean; }) { - // until angular is used as shell - use hash history const history = createHashHistory(); // query param to store app state at const stateStorageKey = '_a'; @@ -78,12 +77,10 @@ export function createEditIndexPatternPageStateContainer({ // makes sure initial url is the same as initial state (this is not really required) kbnUrlStateStorage.set(stateStorageKey, stateContainer.getState(), { replace: true }); - // expose api needed for Controller return { startSyncingState: start, stopSyncingState: stop, setCurrentTab: (newTab: string) => stateContainer.transitions.setTab(newTab), getCurrentTab: () => stateContainer.selectors.tab(), - state$: stateContainer.state$, }; } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_sections.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_sections.js deleted file mode 100644 index f0220b2f798e5..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_sections.js +++ /dev/null @@ -1,80 +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 _ from 'lodash'; -import { i18n } from '@kbn/i18n'; - -function filterBy(items, key, filter) { - const lowercaseFilter = (filter || '').toLowerCase(); - return items.filter(item => item[key].toLowerCase().includes(lowercaseFilter)); -} - -function getCounts(fields, sourceFilters, fieldFilter = '') { - const fieldCount = _.countBy(filterBy(fields, 'name', fieldFilter), function(field) { - return field.scripted ? 'scripted' : 'indexed'; - }); - - _.defaults(fieldCount, { - indexed: 0, - scripted: 0, - sourceFilters: sourceFilters ? filterBy(sourceFilters, 'value', fieldFilter).length : 0, - }); - - return fieldCount; -} - -export function IndicesEditSectionsProvider() { - return function(indexPattern, fieldFilter, indexPatternListProvider) { - const totalCount = getCounts(indexPattern.fields, indexPattern.sourceFilters); - const filteredCount = getCounts(indexPattern.fields, indexPattern.sourceFilters, fieldFilter); - - const editSections = []; - - editSections.push({ - title: i18n.translate('kbn.management.editIndexPattern.tabs.fieldsHeader', { - defaultMessage: 'Fields', - }), - index: 'indexedFields', - count: filteredCount.indexed, - totalCount: totalCount.indexed, - }); - - if (indexPatternListProvider.areScriptedFieldsEnabled(indexPattern)) { - editSections.push({ - title: i18n.translate('kbn.management.editIndexPattern.tabs.scriptedHeader', { - defaultMessage: 'Scripted fields', - }), - index: 'scriptedFields', - count: filteredCount.scripted, - totalCount: totalCount.scripted, - }); - } - - editSections.push({ - title: i18n.translate('kbn.management.editIndexPattern.tabs.sourceHeader', { - defaultMessage: 'Source filters', - }), - index: 'sourceFilters', - count: filteredCount.sourceFilters, - totalCount: totalCount.sourceFilters, - }); - - return editSections; - }; -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/field_controls.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/field_controls.html deleted file mode 100644 index c14dcd3f3a8d5..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/field_controls.html +++ /dev/null @@ -1,19 +0,0 @@ -
- - - - - -
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js index 6beaee60b3788..e05aea3678fe2 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js @@ -17,4 +17,159 @@ * under the License. */ -import './edit_index_pattern'; +import React from 'react'; +import { HashRouter } from 'react-router-dom'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors'; +import uiRoutes from 'ui/routes'; +import { uiModules } from 'ui/modules'; +import { I18nContext } from 'ui/i18n'; +import { npStart } from 'ui/new_platform'; +import template from './edit_index_pattern.html'; +import createEditFieldtemplate from './create_edit_field.html'; +import { + getEditBreadcrumbs, + getEditFieldBreadcrumbs, + getCreateFieldBreadcrumbs, +} from '../breadcrumbs'; +import { EditIndexPattern } from './edit_index_pattern'; +import { CreateEditField } from './create_edit_field'; + +const REACT_EDIT_INDEX_PATTERN_DOM_ELEMENT_ID = 'reactEditIndexPattern'; + +function destroyEditIndexPattern() { + const node = document.getElementById(REACT_EDIT_INDEX_PATTERN_DOM_ELEMENT_ID); + node && unmountComponentAtNode(node); +} + +function renderEditIndexPattern($scope, config, $route) { + $scope.$$postDigest(() => { + const node = document.getElementById(REACT_EDIT_INDEX_PATTERN_DOM_ELEMENT_ID); + if (!node) { + return; + } + + render( + + + + + , + node + ); + }); +} + +uiRoutes.when('/management/kibana/index_patterns/:indexPatternId', { + template, + k7Breadcrumbs: getEditBreadcrumbs, + resolve: { + indexPattern: function($route, Promise, redirectWhenMissing) { + const { indexPatterns } = npStart.plugins.data; + return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( + redirectWhenMissing('/management/kibana/index_patterns') + ); + }, + }, +}); + +uiModules + .get('apps/management') + .controller('managementIndexPatternsEdit', function($scope, $route, config) { + $scope.$on('$destroy', () => { + destroyEditIndexPattern(); + }); + + renderEditIndexPattern($scope, config, $route); + }); + +// routes for create edit field. Will be removed after migartion all component to react. +const REACT_FIELD_EDITOR_ID = 'reactFieldEditor'; +const renderCreateEditField = ($scope, $route, getConfig, $http, fieldFormatEditors) => { + $scope.$$postDigest(() => { + const node = document.getElementById(REACT_FIELD_EDITOR_ID); + if (!node) { + return; + } + + render( + + + + + , + node + ); + }); +}; + +const destroyCreateEditField = () => { + const node = document.getElementById(REACT_FIELD_EDITOR_ID); + node && unmountComponentAtNode(node); +}; + +uiRoutes + .when('/management/kibana/index_patterns/:indexPatternId/field/:fieldName*', { + mode: 'edit', + k7Breadcrumbs: getEditFieldBreadcrumbs, + }) + .when('/management/kibana/index_patterns/:indexPatternId/create-field/', { + mode: 'create', + k7Breadcrumbs: getCreateFieldBreadcrumbs, + }) + .defaults(/management\/kibana\/index_patterns\/[^\/]+\/(field|create-field)(\/|$)/, { + template: createEditFieldtemplate, + mapBreadcrumbs($route, breadcrumbs) { + const { indexPattern } = $route.current.locals; + return breadcrumbs.map(crumb => { + if (crumb.id !== indexPattern.id) { + return crumb; + } + + return { + ...crumb, + display: indexPattern.title, + }; + }); + }, + resolve: { + indexPattern: function($route, Promise, redirectWhenMissing) { + const { indexPatterns } = npStart.plugins.data; + return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( + redirectWhenMissing('/management/kibana/index_patterns') + ); + }, + }, + controllerAs: 'fieldSettings', + controller: function FieldEditorPageController($scope, $route, $http, Private, config) { + const getConfig = (...args) => config.get(...args); + const fieldFormatEditors = Private(RegistryFieldFormatEditorsProvider); + + renderCreateEditField($scope, $route, getConfig, $http, fieldFormatEditors); + + $scope.$on('$destroy', () => { + destroyCreateEditField(); + }); + }, + }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx index deac85d9a32e9..a06671ef6a470 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx @@ -92,7 +92,7 @@ export function IndexHeader({ diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 7c2bb565615d7..c69063967b1e2 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -19,7 +19,11 @@ import React, { Component } from 'react'; import { createSelector } from 'reselect'; -import { IndexPatternField, IIndexPattern } from '../../../../../../../../../plugins/data/public'; +import { + IndexPatternField, + IIndexPattern, + IFieldType, +} from '../../../../../../../../../plugins/data/public'; import { Table } from './components/table'; import { getFieldFormat } from './lib'; import { IndexedFieldItem } from './types'; @@ -31,7 +35,7 @@ interface IndexedFieldsTableProps { indexedFieldTypeFilter?: string; helpers: { redirectToRoute: (obj: any) => void; - getFieldInfo: (indexPattern: IIndexPattern, field: string) => string[]; + getFieldInfo: (indexPattern: IIndexPattern, field: IFieldType) => string[]; }; fieldWildcardMatcher: (filters: any[]) => (val: any) => boolean; } @@ -76,7 +80,7 @@ export class IndexedFieldsTable extends Component< indexPattern: field.indexPattern, format: getFieldFormat(indexPattern, field.name), excluded: fieldWildcardMatch ? fieldWildcardMatch(field.name) : false, - info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field.name), + info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field), }; })) || [] diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap index 569b75c848c52..202b09ddc6066 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap @@ -3,7 +3,7 @@ exports[`ScriptedFieldsTable should filter based on the lang filter 1`] = `
void; } @@ -136,14 +136,19 @@ export class ScriptedFieldsTable extends Component< }; render() { - const { helpers, indexPattern } = this.props; + const { indexPattern } = this.props; const { fieldToDelete, deprecatedLangsInUse } = this.state; const items = this.getFilteredItems(); return ( <> -
+
{ + indexPattern: IndexPattern; + config: Record; + fields: IndexPatternField[]; + services: { + indexPatternManagement: IndexPatternManagementStart; + }; +} + +const filterAriaLabel = i18n.translate('kbn.management.editIndexPattern.fields.filterAria', { + defaultMessage: 'Filter', +}); + +const filterPlaceholder = i18n.translate( + 'kbn.management.editIndexPattern.fields.filterPlaceholder', + { + defaultMessage: 'Filter', + } +); + +export function Tabs({ config, indexPattern, fields, services, history, location }: TabsProps) { + const [fieldFilter, setFieldFilter] = useState(''); + const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState(''); + const [scriptedFieldLanguageFilter, setScriptedFieldLanguageFilter] = useState(''); + const [indexedFieldTypes, setIndexedFieldType] = useState([]); + const [scriptedFieldLanguages, setScriptedFieldLanguages] = useState([]); + const [syncingStateFunc, setSyncingStateFunc] = useState({ + getCurrentTab: () => TAB_INDEXED_FIELDS, + }); + + const refreshFilters = useCallback(() => { + const tempIndexedFieldTypes: string[] = []; + const tempScriptedFieldLanguages: string[] = []; + indexPattern.fields.forEach(field => { + if (field.scripted) { + if (field.lang) { + tempScriptedFieldLanguages.push(field.lang); + } + } else { + tempIndexedFieldTypes.push(field.type); + } + }); + + setIndexedFieldType(convertToEuiSelectOption(tempIndexedFieldTypes, 'indexedFiledTypes')); + setScriptedFieldLanguages( + convertToEuiSelectOption(tempScriptedFieldLanguages, 'scriptedFieldLanguages') + ); + }, [indexPattern]); + + useEffect(() => { + refreshFilters(); + }, [indexPattern, indexPattern.fields, refreshFilters]); + + const fieldWildcardMatcherDecorated = useCallback( + (filters: string[]) => fieldWildcardMatcher(filters, config.get('metaFields')), + [config] + ); + + const getFilterSection = useCallback( + (type: string) => { + return ( + + + setFieldFilter(e.target.value)} + data-test-subj="indexPatternFieldFilter" + aria-label={filterAriaLabel} + /> + + {type === TAB_INDEXED_FIELDS && indexedFieldTypes.length > 0 && ( + + setIndexedFieldTypeFilter(e.target.value)} + data-test-subj="indexedFieldTypeFilterDropdown" + /> + + )} + {type === TAB_SCRIPTED_FIELDS && scriptedFieldLanguages.length > 0 && ( + + setScriptedFieldLanguageFilter(e.target.value)} + data-test-subj="scriptedFieldLanguageFilterDropdown" + /> + + )} + + ); + }, + [ + fieldFilter, + indexedFieldTypeFilter, + indexedFieldTypes, + scriptedFieldLanguageFilter, + scriptedFieldLanguages, + ] + ); + + const getContent = useCallback( + (type: string) => { + switch (type) { + case TAB_INDEXED_FIELDS: + return ( + + + {getFilterSection(type)} + + { + history.push(getPath(field)); + }, + getFieldInfo: services.indexPatternManagement.list.getFieldInfo, + }} + /> + + ); + case TAB_SCRIPTED_FIELDS: + return ( + + + {getFilterSection(type)} + + { + history.push(getPath(field)); + }, + }} + onRemoveField={refreshFilters} + /> + + ); + case TAB_SOURCE_FILTERS: + return ( + + + {getFilterSection(type)} + + + + ); + } + }, + [ + fieldFilter, + fieldWildcardMatcherDecorated, + fields, + getFilterSection, + history, + indexPattern, + indexedFieldTypeFilter, + refreshFilters, + scriptedFieldLanguageFilter, + services.indexPatternManagement.list.getFieldInfo, + ] + ); + + const euiTabs: EuiTabbedContentTab[] = useMemo( + () => + getTabs(indexPattern, fieldFilter, services.indexPatternManagement.list).map( + (tab: Pick) => { + return { + ...tab, + content: getContent(tab.id), + }; + } + ), + [fieldFilter, getContent, indexPattern, services.indexPatternManagement.list] + ); + + const [selectedTabId, setSelectedTabId] = useState(euiTabs[0].id); + + useEffect(() => { + const { + startSyncingState, + stopSyncingState, + setCurrentTab, + getCurrentTab, + } = createEditIndexPatternPageStateContainer({ + useHashedUrl: config.get('state:storeInSessionStorage'), + defaultTab: TAB_INDEXED_FIELDS, + }); + + startSyncingState(); + setSyncingStateFunc({ + setCurrentTab, + getCurrentTab, + }); + setSelectedTabId(getCurrentTab()); + + return () => { + stopSyncingState(); + }; + }, [config]); + + return ( + tab.id === selectedTabId)} + onTabClick={tab => { + setSelectedTabId(tab.id); + syncingStateFunc.setCurrentTab(tab.id); + }} + /> + ); +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts new file mode 100644 index 0000000000000..bdb1436c37efb --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts @@ -0,0 +1,146 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Dictionary, countBy, defaults, unique } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { IndexPattern, IndexPatternField } from '../../../../../../../../../plugins/data/public'; +import { IndexPatternManagementStart } from '../../../../../../../../../plugins/index_pattern_management/public'; +import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS, TAB_SOURCE_FILTERS } from '../constants'; + +function filterByName(items: IndexPatternField[], filter: string) { + const lowercaseFilter = (filter || '').toLowerCase(); + return items.filter(item => item.name.toLowerCase().includes(lowercaseFilter)); +} + +function getCounts( + fields: IndexPatternField[], + sourceFilters: { + excludes: string[]; + }, + fieldFilter = '' +) { + const fieldCount = countBy(filterByName(fields, fieldFilter), function(field) { + return field.scripted ? 'scripted' : 'indexed'; + }); + + defaults(fieldCount, { + indexed: 0, + scripted: 0, + sourceFilters: sourceFilters.excludes + ? sourceFilters.excludes.filter(value => + value.toLowerCase().includes(fieldFilter.toLowerCase()) + ).length + : 0, + }); + + return fieldCount; +} + +function getTitle(type: string, filteredCount: Dictionary, totalCount: Dictionary) { + let title = ''; + switch (type) { + case 'indexed': + title = i18n.translate('kbn.management.editIndexPattern.tabs.fieldsHeader', { + defaultMessage: 'Fields', + }); + break; + case 'scripted': + title = i18n.translate('kbn.management.editIndexPattern.tabs.scriptedHeader', { + defaultMessage: 'Scripted fields', + }); + break; + case 'sourceFilters': + title = i18n.translate('kbn.management.editIndexPattern.tabs.sourceHeader', { + defaultMessage: 'Source filters', + }); + break; + } + const count = ` (${ + filteredCount[type] === totalCount[type] + ? filteredCount[type] + : filteredCount[type] + ' / ' + totalCount[type] + })`; + return title + count; +} + +export function getTabs( + indexPattern: IndexPattern, + fieldFilter: string, + indexPatternListProvider: IndexPatternManagementStart['list'] +) { + const totalCount = getCounts(indexPattern.fields, indexPattern.getSourceFiltering()); + const filteredCount = getCounts( + indexPattern.fields, + indexPattern.getSourceFiltering(), + fieldFilter + ); + + const tabs = []; + + tabs.push({ + name: getTitle('indexed', filteredCount, totalCount), + id: TAB_INDEXED_FIELDS, + }); + + if (indexPatternListProvider.areScriptedFieldsEnabled(indexPattern)) { + tabs.push({ + name: getTitle('scripted', filteredCount, totalCount), + id: TAB_SCRIPTED_FIELDS, + }); + } + + tabs.push({ + name: getTitle('sourceFilters', filteredCount, totalCount), + id: TAB_SOURCE_FILTERS, + }); + + return tabs; +} + +export function getPath(field: IndexPatternField) { + return `/management/kibana/index_patterns/${field.indexPattern?.id}/field/${field.name}`; +} + +const allTypesDropDown = i18n.translate('kbn.management.editIndexPattern.fields.allTypesDropDown', { + defaultMessage: 'All field types', +}); + +const allLangsDropDown = i18n.translate('kbn.management.editIndexPattern.fields.allLangsDropDown', { + defaultMessage: 'All languages', +}); + +export function convertToEuiSelectOption(options: string[], type: string) { + const euiOptions = + options.length > 0 + ? [ + { + value: '', + text: type === 'scriptedFieldLanguages' ? allLangsDropDown : allTypesDropDown, + }, + ] + : []; + return euiOptions.concat( + unique(options).map(option => { + return { + value: option, + text: option, + }; + }) + ); +} diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 6dcd017335c85..8864eda3823ef 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -206,15 +206,17 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async getFieldsTabCount() { return retry.try(async () => { - const text = await testSubjects.getVisibleText('tab-count-indexedFields'); - return text.replace(/\((.*)\)/, '$1'); + const indexedFieldsTab = await find.byCssSelector('#indexedFields .euiTab__content'); + const text = await indexedFieldsTab.getVisibleText(); + return text.split(/[()]/)[1]; }); } async getScriptedFieldsTabCount() { return await retry.try(async () => { - const theText = await testSubjects.getVisibleText('tab-count-scriptedFields'); - return theText.replace(/\((.*)\)/, '$1'); + const scriptedFieldsTab = await find.byCssSelector('#scriptedFields .euiTab__content'); + const text = await scriptedFieldsTab.getVisibleText(); + return text.split(/[()]/)[1]; }); } @@ -241,13 +243,13 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async setFieldTypeFilter(type: string) { await find.clickByCssSelector( - 'select[data-test-subj="indexedFieldTypeFilterDropdown"] > option[label="' + type + '"]' + 'select[data-test-subj="indexedFieldTypeFilterDropdown"] > option[value="' + type + '"]' ); } async setScriptedFieldLanguageFilter(language: string) { await find.clickByCssSelector( - 'select[data-test-subj="scriptedFieldLanguageFilterDropdown"] > option[label="' + + 'select[data-test-subj="scriptedFieldLanguageFilterDropdown"] > option[value="' + language + '"]' ); @@ -412,17 +414,17 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async clickFieldsTab() { log.debug('click Fields tab'); - await testSubjects.click('tab-indexFields'); + await find.clickByCssSelector('#indexedFields'); } async clickScriptedFieldsTab() { log.debug('click Scripted Fields tab'); - await testSubjects.click('tab-scriptedFields'); + await find.clickByCssSelector('#scriptedFields'); } async clickSourceFiltersTab() { log.debug('click Source Filters tab'); - await testSubjects.click('tab-sourceFilters'); + await find.clickByCssSelector('#sourceFilters'); } async editScriptedField(name: string) { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 87e8e2e7a41e2..909a736220d17 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2186,10 +2186,8 @@ "kbn.management.createIndexPatternHeader": "{indexPatternName} の作成", "kbn.management.createIndexPatternLabel": "Kibana は、可視化などを目的に Elasticsearch インデックスからデータを取得するために、インデックスパターンを使用します。", "kbn.management.editIndexPattern.deleteButton": "削除", - "kbn.management.editIndexPattern.deleteFieldButton": "削除", "kbn.management.editIndexPattern.deleteHeader": "インデックスパターンを削除しますか?", "kbn.management.editIndexPattern.detailsAria": "インデックスパターンの詳細", - "kbn.management.editIndexPattern.editFieldButton": "編集", "kbn.management.editIndexPattern.fields.allLangsDropDown": "すべての言語", "kbn.management.editIndexPattern.fields.allTypesDropDown": "すべてのフィールドタイプ", "kbn.management.editIndexPattern.fields.filterAria": "フィルター", @@ -2215,7 +2213,6 @@ "kbn.management.editIndexPattern.fields.table.typeHeader": "タイプ", "kbn.management.editIndexPattern.mappingConflictHeader": "マッピングの矛盾", "kbn.management.editIndexPattern.mappingConflictLabel": "{conflictFieldsLength, plural, one {フィールドが} other {# フィールドが}}このパターンと一致するインデックスの間で異なるタイプ (文字列、整数など) に定義されています。これらの矛盾したフィールドは Kibana の一部で使用できますが、Kibana がタイプを把握しなければならない機能には使用できません。この問題を修正するにはデータのレンダリングが必要です。", - "kbn.management.editIndexPattern.notDateErrorMessage": "このフィールドは日付ではなく {fieldType} です。", "kbn.management.editIndexPattern.refreshAria": "フィールドリストを再度読み込みます", "kbn.management.editIndexPattern.refreshButton": "更新", "kbn.management.editIndexPattern.refreshHeader": "フィールドリストを更新しますか?", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4a5ed3291a915..0ebef9f8f1ef9 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2187,10 +2187,8 @@ "kbn.management.createIndexPatternHeader": "创建 {indexPatternName}", "kbn.management.createIndexPatternLabel": "Kibana 使用索引模式从 Elasticsearch 索引中检索数据,以实现诸如可视化等功能。", "kbn.management.editIndexPattern.deleteButton": "删除", - "kbn.management.editIndexPattern.deleteFieldButton": "删除", "kbn.management.editIndexPattern.deleteHeader": "删除索引模式?", "kbn.management.editIndexPattern.detailsAria": "索引模式详细信息", - "kbn.management.editIndexPattern.editFieldButton": "编辑", "kbn.management.editIndexPattern.fields.allLangsDropDown": "所有语言", "kbn.management.editIndexPattern.fields.allTypesDropDown": "所有字段类型", "kbn.management.editIndexPattern.fields.filterAria": "筛选", @@ -2216,7 +2214,6 @@ "kbn.management.editIndexPattern.fields.table.typeHeader": "类型", "kbn.management.editIndexPattern.mappingConflictHeader": "映射冲突", "kbn.management.editIndexPattern.mappingConflictLabel": "匹配此模式的各个索引中{conflictFieldsLength, plural, one {一个字段已} other {# 个字段已}}定义为若干类型(字符串、整数等)。您仍能够在 Kibana 的各个部分中使用这些冲突类型,但它们将无法用于需要 Kibana 知道其类型的函数。要解决此问题,需要重新索引您的数据。", - "kbn.management.editIndexPattern.notDateErrorMessage": "该字段是{fieldType},不是日期。", "kbn.management.editIndexPattern.refreshAria": "重新加载字段列表", "kbn.management.editIndexPattern.refreshButton": "刷新", "kbn.management.editIndexPattern.refreshHeader": "刷新字段列表?", From 6d099d0a4f9c78c105285f4b7c261f24d993c954 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Fri, 24 Apr 2020 14:55:43 +0200 Subject: [PATCH 32/35] Migrate graph_workspace saved object registration to Kibana platform (#64157) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Migrate graph_workspace saved object registration to Kibana platform * Remove legacy plugin 🎉 --- src/dev/precommit_hook/casing_check_config.js | 2 - x-pack/.i18nrc.json | 2 +- x-pack/index.js | 2 - x-pack/legacy/plugins/graph/index.ts | 32 ----- x-pack/legacy/plugins/graph/mappings.json | 31 ----- x-pack/legacy/plugins/graph/migrations.js | 41 ------- .../legacy/plugins/graph/migrations.test.js | 102 ---------------- x-pack/plugins/graph/server/plugin.ts | 2 + .../server/saved_objects/graph_workspace.ts | 43 +++++++ .../graph/server/saved_objects/index.ts | 6 + .../server/saved_objects/migrations.test.ts | 111 ++++++++++++++++++ .../graph/server/saved_objects/migrations.ts | 40 +++++++ 12 files changed, 203 insertions(+), 211 deletions(-) delete mode 100644 x-pack/legacy/plugins/graph/index.ts delete mode 100644 x-pack/legacy/plugins/graph/mappings.json delete mode 100644 x-pack/legacy/plugins/graph/migrations.js delete mode 100644 x-pack/legacy/plugins/graph/migrations.test.js create mode 100644 x-pack/plugins/graph/server/saved_objects/graph_workspace.ts create mode 100644 x-pack/plugins/graph/server/saved_objects/index.ts create mode 100644 x-pack/plugins/graph/server/saved_objects/migrations.test.ts create mode 100644 x-pack/plugins/graph/server/saved_objects/migrations.ts diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index a75a6997a8cb2..fc95288eabed8 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -159,8 +159,6 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'webpackShims/elasticsearch-browser.js', 'webpackShims/moment-timezone.js', 'webpackShims/ui-bootstrap.js', - 'x-pack/legacy/plugins/graph/public/graphClientWorkspace.js', - 'x-pack/legacy/plugins/graph/public/angular-venn-simple.js', 'x-pack/legacy/plugins/index_management/public/lib/editSettings.js', 'x-pack/legacy/plugins/license_management/public/store/reducers/licenseManagement.js', 'x-pack/legacy/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js', diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 8e5563e4ff674..a8e50e017102f 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -15,7 +15,7 @@ "xpack.endpoint": "plugins/endpoint", "xpack.features": "plugins/features", "xpack.fileUpload": "plugins/file_upload", - "xpack.graph": ["legacy/plugins/graph", "plugins/graph"], + "xpack.graph": ["plugins/graph"], "xpack.grokDebugger": "plugins/grokdebugger", "xpack.idxMgmt": "plugins/index_management", "xpack.indexLifecycleMgmt": "plugins/index_lifecycle_management", diff --git a/x-pack/index.js b/x-pack/index.js index 43ae5c3e5c5dd..82a35f1710361 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -5,7 +5,6 @@ */ import { xpackMain } from './legacy/plugins/xpack_main'; -import { graph } from './legacy/plugins/graph'; import { monitoring } from './legacy/plugins/monitoring'; import { reporting } from './legacy/plugins/reporting'; import { security } from './legacy/plugins/security'; @@ -32,7 +31,6 @@ import { triggersActionsUI } from './legacy/plugins/triggers_actions_ui'; module.exports = function(kibana) { return [ xpackMain(kibana), - graph(kibana), monitoring(kibana), reporting(kibana), spaces(kibana), diff --git a/x-pack/legacy/plugins/graph/index.ts b/x-pack/legacy/plugins/graph/index.ts deleted file mode 100644 index 5c7f8fa46c18b..0000000000000 --- a/x-pack/legacy/plugins/graph/index.ts +++ /dev/null @@ -1,32 +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. - */ - -// @ts-ignore -import migrations from './migrations'; -import mappings from './mappings.json'; -import { LegacyPluginInitializer } from '../../../../src/legacy/plugin_discovery/types'; - -export const graph: LegacyPluginInitializer = kibana => { - return new kibana.Plugin({ - id: 'graph', - configPrefix: 'xpack.graph', - require: ['kibana', 'elasticsearch', 'xpack_main'], - uiExports: { - mappings, - migrations, - }, - - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - canEditDrillDownUrls: Joi.boolean().default(true), - savePolicy: Joi.string() - .valid(['config', 'configAndDataWithConsent', 'configAndData', 'none']) - .default('configAndData'), - }).default(); - }, - }); -}; diff --git a/x-pack/legacy/plugins/graph/mappings.json b/x-pack/legacy/plugins/graph/mappings.json deleted file mode 100644 index f1950c459eee5..0000000000000 --- a/x-pack/legacy/plugins/graph/mappings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - } -} diff --git a/x-pack/legacy/plugins/graph/migrations.js b/x-pack/legacy/plugins/graph/migrations.js deleted file mode 100644 index 0cefe6217b45d..0000000000000 --- a/x-pack/legacy/plugins/graph/migrations.js +++ /dev/null @@ -1,41 +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 { get } from 'lodash'; - -export default { - 'graph-workspace': { - '7.0.0': doc => { - // Set new "references" attribute - doc.references = doc.references || []; - // Migrate index pattern - const wsState = get(doc, 'attributes.wsState'); - if (typeof wsState !== 'string') { - return doc; - } - let state; - try { - state = JSON.parse(JSON.parse(wsState)); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - return doc; - } - const { indexPattern } = state; - if (!indexPattern) { - return doc; - } - state.indexPatternRefName = 'indexPattern_0'; - delete state.indexPattern; - doc.attributes.wsState = JSON.stringify(JSON.stringify(state)); - doc.references.push({ - name: 'indexPattern_0', - type: 'index-pattern', - id: indexPattern, - }); - return doc; - }, - }, -}; diff --git a/x-pack/legacy/plugins/graph/migrations.test.js b/x-pack/legacy/plugins/graph/migrations.test.js deleted file mode 100644 index 93162d94857ce..0000000000000 --- a/x-pack/legacy/plugins/graph/migrations.test.js +++ /dev/null @@ -1,102 +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 migrations from './migrations'; - -describe('graph-workspace', () => { - describe('7.0.0', () => { - const migration = migrations['graph-workspace']['7.0.0']; - - test('returns doc on empty object', () => { - expect(migration({})).toMatchInlineSnapshot(` -Object { - "references": Array [], -} -`); - }); - - test('returns doc when wsState is not a string', () => { - const doc = { - id: '1', - attributes: { - wsState: true, - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "wsState": true, - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('returns doc when wsState is not valid JSON', () => { - const doc = { - id: '1', - attributes: { - wsState: '123abc', - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "wsState": "123abc", - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('returns doc when "indexPattern" is missing from wsState', () => { - const doc = { - id: '1', - attributes: { - wsState: JSON.stringify(JSON.stringify({ foo: true })), - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "wsState": "\\"{\\\\\\"foo\\\\\\":true}\\"", - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('extract "indexPattern" attribute from doc', () => { - const doc = { - id: '1', - attributes: { - wsState: JSON.stringify(JSON.stringify({ foo: true, indexPattern: 'pattern*' })), - bar: true, - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "bar": true, - "wsState": "\\"{\\\\\\"foo\\\\\\":true,\\\\\\"indexPatternRefName\\\\\\":\\\\\\"indexPattern_0\\\\\\"}\\"", - }, - "id": "1", - "references": Array [ - Object { - "id": "pattern*", - "name": "indexPattern_0", - "type": "index-pattern", - }, - ], -} -`); - }); - }); -}); diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts index a169953d5a10b..141d5d0ea8db4 100644 --- a/x-pack/plugins/graph/server/plugin.ts +++ b/x-pack/plugins/graph/server/plugin.ts @@ -13,6 +13,7 @@ import { registerExploreRoute } from './routes/explore'; import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; import { registerSampleData } from './sample_data'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { graphWorkspace } from './saved_objects'; export class GraphPlugin implements Plugin { private licenseState: LicenseState | null = null; @@ -32,6 +33,7 @@ export class GraphPlugin implements Plugin { const licenseState = new LicenseState(); licenseState.start(licensing.license$); this.licenseState = licenseState; + core.savedObjects.registerType(graphWorkspace); if (home) { registerSampleData(home.sampleData, licenseState); diff --git a/x-pack/plugins/graph/server/saved_objects/graph_workspace.ts b/x-pack/plugins/graph/server/saved_objects/graph_workspace.ts new file mode 100644 index 0000000000000..8e8cb64aac1b9 --- /dev/null +++ b/x-pack/plugins/graph/server/saved_objects/graph_workspace.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 { SavedObjectsType } from 'kibana/server'; +import { graphMigrations } from './migrations'; + +export const graphWorkspace: SavedObjectsType = { + name: 'graph-workspace', + namespaceType: 'single', + hidden: false, + migrations: graphMigrations, + mappings: { + properties: { + description: { + type: 'text', + }, + kibanaSavedObjectMeta: { + properties: { + searchSourceJSON: { + type: 'text', + }, + }, + }, + numLinks: { + type: 'integer', + }, + numVertices: { + type: 'integer', + }, + title: { + type: 'text', + }, + version: { + type: 'integer', + }, + wsState: { + type: 'text', + }, + }, + }, +}; diff --git a/x-pack/plugins/graph/server/saved_objects/index.ts b/x-pack/plugins/graph/server/saved_objects/index.ts new file mode 100644 index 0000000000000..67d1501950175 --- /dev/null +++ b/x-pack/plugins/graph/server/saved_objects/index.ts @@ -0,0 +1,6 @@ +/* + * 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 { graphWorkspace } from './graph_workspace'; diff --git a/x-pack/plugins/graph/server/saved_objects/migrations.test.ts b/x-pack/plugins/graph/server/saved_objects/migrations.test.ts new file mode 100644 index 0000000000000..ecf1f3ca3b69e --- /dev/null +++ b/x-pack/plugins/graph/server/saved_objects/migrations.test.ts @@ -0,0 +1,111 @@ +/* + * 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 { graphMigrations } from './migrations'; +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; + +describe('graph-workspace', () => { + describe('7.0.0', () => { + const migration = graphMigrations['7.0.0']; + + test('returns doc on empty object', () => { + expect(migration({} as SavedObjectUnsanitizedDoc)).toMatchInlineSnapshot(` + Object { + "references": Array [], + } + `); + }); + + test('returns doc when wsState is not a string', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: true, + }, + }; + expect(migration(doc)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "wsState": true, + }, + "id": "1", + "references": Array [], + "type": "graph-workspace", + } + `); + }); + + test('returns doc when wsState is not valid JSON', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: '123abc', + }, + }; + expect(migration(doc)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "wsState": "123abc", + }, + "id": "1", + "references": Array [], + "type": "graph-workspace", + } + `); + }); + + test('returns doc when "indexPattern" is missing from wsState', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: JSON.stringify(JSON.stringify({ foo: true })), + }, + }; + expect(migration(doc)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "wsState": "\\"{\\\\\\"foo\\\\\\":true}\\"", + }, + "id": "1", + "references": Array [], + "type": "graph-workspace", + } + `); + }); + + test('extract "indexPattern" attribute from doc', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: JSON.stringify(JSON.stringify({ foo: true, indexPattern: 'pattern*' })), + bar: true, + }, + }; + const migratedDoc = migration(doc); + expect(migratedDoc).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "bar": true, + "wsState": "\\"{\\\\\\"foo\\\\\\":true,\\\\\\"indexPatternRefName\\\\\\":\\\\\\"indexPattern_0\\\\\\"}\\"", + }, + "id": "1", + "references": Array [ + Object { + "id": "pattern*", + "name": "indexPattern_0", + "type": "index-pattern", + }, + ], + "type": "graph-workspace", + } + `); + }); + }); +}); diff --git a/x-pack/plugins/graph/server/saved_objects/migrations.ts b/x-pack/plugins/graph/server/saved_objects/migrations.ts new file mode 100644 index 0000000000000..e77d2ea0fb7c9 --- /dev/null +++ b/x-pack/plugins/graph/server/saved_objects/migrations.ts @@ -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 { get } from 'lodash'; +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; + +export const graphMigrations = { + '7.0.0': (doc: SavedObjectUnsanitizedDoc) => { + // Set new "references" attribute + doc.references = doc.references || []; + // Migrate index pattern + const wsState = get(doc, 'attributes.wsState'); + if (typeof wsState !== 'string') { + return doc; + } + let state; + try { + state = JSON.parse(JSON.parse(wsState)); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return doc; + } + const { indexPattern } = state; + if (!indexPattern) { + return doc; + } + state.indexPatternRefName = 'indexPattern_0'; + delete state.indexPattern; + doc.attributes.wsState = JSON.stringify(JSON.stringify(state)); + doc.references.push({ + name: 'indexPattern_0', + type: 'index-pattern', + id: indexPattern, + }); + return doc; + }, +}; From 0dcefe8dbcf22dd7b437f24548c9b1b6eaffea7c Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 24 Apr 2020 09:01:23 -0400 Subject: [PATCH 33/35] [Fleet] Fix agent status count to not include unenrolled agents (#64106) --- x-pack/plugins/ingest_manager/server/services/agents/status.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.ts index 001b6d01f078e..fce989cea3248 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.ts @@ -67,7 +67,7 @@ export async function getAgentStatusForConfig( AgentStatusKueryHelper.buildKueryForOfflineAgents(), ].map(kuery => listAgents(soClient, { - showInactive: true, + showInactive: false, perPage: 0, page: 1, kuery: configId From 9bda6bde838d71f00c0f54dfb5f26f4ac9b2ba2a Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Fri, 24 Apr 2020 16:20:32 +0300 Subject: [PATCH 34/35] Move ensureDefaultIndexPattern into data plugin (#63100) * Move ensure_default_index_pattern into data plugin * Update docs to include ensureDefaultIndexPattern * Fix translations * Move helper into index_patterns service * Update docs * Remove mock * Add mock Co-authored-by: Elastic Machine --- .../kibana/public/discover/kibana_services.ts | 6 +- .../discover/np_ready/angular/discover.js | 3 +- .../create_index_pattern_wizard.test.tsx.snap | 1 + .../public/application/legacy_app.js | 9 +- .../ensure_default_index_pattern.tsx | 98 +++++++++++++++++++ .../index_patterns/index_patterns.test.ts | 6 +- .../index_patterns/index_patterns.ts | 15 +-- src/plugins/data/public/mocks.ts | 1 + src/plugins/data/public/plugin.ts | 2 +- .../history/ensure_default_index_pattern.tsx | 98 ------------------- .../kibana_utils/public/history/index.ts | 1 - src/plugins/kibana_utils/public/index.ts | 2 +- .../saved_objects_table.test.tsx.snap | 1 + .../public/application/legacy_app.js | 20 ++-- .../translations/translations/ja-JP.json | 2 +- .../translations/translations/zh-CN.json | 2 +- 16 files changed, 133 insertions(+), 134 deletions(-) create mode 100644 src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx delete mode 100644 src/plugins/kibana_utils/public/history/ensure_default_index_pattern.tsx diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 3aa552b1da07d..77664e87a3279 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -59,11 +59,7 @@ export const [getUrlTracker, setUrlTracker] = createGetterSetter<{ export const getHistory = _.once(() => createHashHistory()); export const { getRequestInspectorStats, getResponseInspectorStats, tabifyAggResponse } = search; -export { - unhashUrl, - redirectWhenMissing, - ensureDefaultIndexPattern, -} from '../../../../../plugins/kibana_utils/public'; +export { unhashUrl, redirectWhenMissing } from '../../../../../plugins/kibana_utils/public'; export { formatMsg, formatStack, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index 567cfda45cc0d..c1de704d1c00a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -49,7 +49,6 @@ import { subscribeWithScope, tabifyAggResponse, getAngularModule, - ensureDefaultIndexPattern, redirectWhenMissing, } from '../../kibana_services'; @@ -118,7 +117,7 @@ app.config($routeProvider => { savedObjects: function($route, Promise) { const history = getHistory(); const savedSearchId = $route.current.params.id; - return ensureDefaultIndexPattern(core, data, history).then(() => { + return data.indexPatterns.ensureDefaultIndexPattern(history).then(() => { const { appStateContainer } = getState({ history }); const { index } = appStateContainer.getState(); return Promise.props({ diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap index 09a06bd8827ce..ed65db10e0acb 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap @@ -149,6 +149,7 @@ exports[`CreateIndexPatternWizard renders time field step when step is set to 2 indexPatternsService={ Object { "clearCache": [MockFunction], + "ensureDefaultIndexPattern": [MockFunction], "get": [MockFunction], "make": [Function], } diff --git a/src/plugins/dashboard/public/application/legacy_app.js b/src/plugins/dashboard/public/application/legacy_app.js index 10243dbf2f979..31225530b10b9 100644 --- a/src/plugins/dashboard/public/application/legacy_app.js +++ b/src/plugins/dashboard/public/application/legacy_app.js @@ -28,7 +28,6 @@ import { initDashboardAppDirective } from './dashboard_app'; import { createDashboardEditUrl, DashboardConstants } from '../dashboard_constants'; import { createKbnUrlStateStorage, - ensureDefaultIndexPattern, redirectWhenMissing, InvalidJSONProperty, SavedObjectNotFound, @@ -138,7 +137,7 @@ export function initDashboardApp(app, deps) { }, resolve: { dash: function($route, history) { - return ensureDefaultIndexPattern(deps.core, deps.data, history).then(() => { + return deps.data.indexPatterns.ensureDefaultIndexPattern(history).then(() => { const savedObjectsClient = deps.savedObjectsClient; const title = $route.current.params.title; if (title) { @@ -173,7 +172,8 @@ export function initDashboardApp(app, deps) { requireUICapability: 'dashboard.createNew', resolve: { dash: history => - ensureDefaultIndexPattern(deps.core, deps.data, history) + deps.data.indexPatterns + .ensureDefaultIndexPattern(history) .then(() => deps.savedDashboards.get()) .catch( redirectWhenMissing({ @@ -194,7 +194,8 @@ export function initDashboardApp(app, deps) { dash: function($route, history) { const id = $route.current.params.id; - return ensureDefaultIndexPattern(deps.core, deps.data, history) + return deps.data.indexPatterns + .ensureDefaultIndexPattern(history) .then(() => deps.savedDashboards.get(id)) .then(savedDashboard => { deps.chrome.recentlyAccessed.add( diff --git a/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx b/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx new file mode 100644 index 0000000000000..6b71739862f62 --- /dev/null +++ b/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx @@ -0,0 +1,98 @@ +/* + * 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 { contains } from 'lodash'; +import React from 'react'; +import { History } from 'history'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut } from '@elastic/eui'; +import { CoreStart } from 'kibana/public'; +import { toMountPoint } from '../../../../kibana_react/public'; +import { IndexPatternsContract } from './index_patterns'; + +export type EnsureDefaultIndexPattern = (history: History) => Promise | undefined; + +export const createEnsureDefaultIndexPattern = (core: CoreStart) => { + let bannerId: string; + let timeoutId: NodeJS.Timeout | undefined; + + /** + * Checks whether a default index pattern is set and exists and defines + * one otherwise. + * + * If there are no index patterns, redirect to management page and show + * banner. In this case the promise returned from this function will never + * resolve to wait for the URL change to happen. + */ + return async function ensureDefaultIndexPattern(this: IndexPatternsContract, history: History) { + const patterns = await this.getIds(); + let defaultId = core.uiSettings.get('defaultIndex'); + let defined = !!defaultId; + const exists = contains(patterns, defaultId); + + if (defined && !exists) { + core.uiSettings.remove('defaultIndex'); + defaultId = defined = false; + } + + if (defined) { + return; + } + + // If there is any index pattern created, set the first as default + if (patterns.length >= 1) { + defaultId = patterns[0]; + core.uiSettings.set('defaultIndex', defaultId); + } else { + const canManageIndexPatterns = core.application.capabilities.management.kibana.index_patterns; + const redirectTarget = canManageIndexPatterns ? '/management/kibana/index_pattern' : '/home'; + + if (timeoutId) { + clearTimeout(timeoutId); + } + + // Avoid being hostile to new users who don't have an index pattern setup yet + // give them a friendly info message instead of a terse error message + bannerId = core.overlays.banners.replace( + bannerId, + toMountPoint( + + ) + ); + + // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around + timeoutId = setTimeout(() => { + core.overlays.banners.remove(bannerId); + timeoutId = undefined; + }, 15000); + + history.push(redirectTarget); + + // return never-resolving promise to stop resolving and wait for the url change + return new Promise(() => {}); + } + }; +}; diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts index c429431b632bd..cf1f83d0e28cb 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts @@ -21,9 +21,9 @@ import { IndexPatternsService } from './index_patterns'; import { SavedObjectsClientContract, - IUiSettingsClient, HttpSetup, SavedObjectsFindResponsePublic, + CoreStart, } from 'kibana/public'; jest.mock('./index_pattern', () => { @@ -61,10 +61,10 @@ describe('IndexPatterns', () => { }) as Promise> ); - const uiSettings = {} as IUiSettingsClient; + const core = {} as CoreStart; const http = {} as HttpSetup; - indexPatterns = new IndexPatternsService(uiSettings, savedObjectsClient, http); + indexPatterns = new IndexPatternsService(core, savedObjectsClient, http); }); test('does cache gets for the same id', async () => { diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts index acce5ed57683c..b5d66a6aab60a 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts @@ -22,11 +22,16 @@ import { SimpleSavedObject, IUiSettingsClient, HttpStart, + CoreStart, } from 'src/core/public'; import { createIndexPatternCache } from './_pattern_cache'; import { IndexPattern } from './index_pattern'; import { IndexPatternsApiClient, GetFieldsOptions } from './index_patterns_api_client'; +import { + createEnsureDefaultIndexPattern, + EnsureDefaultIndexPattern, +} from './ensure_default_index_pattern'; const indexPatternCache = createIndexPatternCache(); @@ -37,15 +42,13 @@ export class IndexPatternsService { private savedObjectsClient: SavedObjectsClientContract; private savedObjectsCache?: Array>> | null; private apiClient: IndexPatternsApiClient; + ensureDefaultIndexPattern: EnsureDefaultIndexPattern; - constructor( - config: IUiSettingsClient, - savedObjectsClient: SavedObjectsClientContract, - http: HttpStart - ) { + constructor(core: CoreStart, savedObjectsClient: SavedObjectsClientContract, http: HttpStart) { this.apiClient = new IndexPatternsApiClient(http); - this.config = config; + this.config = core.uiSettings; this.savedObjectsClient = savedObjectsClient; + this.ensureDefaultIndexPattern = createEnsureDefaultIndexPattern(core); } private async refreshSavedObjectsCache() { diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index a2df754786a68..ba1df89c41358 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -57,6 +57,7 @@ const createStartContract = (): Start => { SearchBar: jest.fn(), }, indexPatterns: ({ + ensureDefaultIndexPattern: jest.fn(), make: () => ({ fieldsFetcher: { fetchForWildcard: jest.fn(), diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 924fcd6730f93..f3a88287313a0 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -160,7 +160,7 @@ export class DataPublicPlugin implements Plugin= 1) { - defaultId = patterns[0]; - core.uiSettings.set('defaultIndex', defaultId); - } else { - const canManageIndexPatterns = core.application.capabilities.management.kibana.index_patterns; - const redirectTarget = canManageIndexPatterns ? '/management/kibana/index_pattern' : '/home'; - - if (timeoutId) { - clearTimeout(timeoutId); - } - - // Avoid being hostile to new users who don't have an index pattern setup yet - // give them a friendly info message instead of a terse error message - bannerId = core.overlays.banners.replace( - bannerId, - toMountPoint( - - ) - ); - - // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around - timeoutId = setTimeout(() => { - core.overlays.banners.remove(bannerId); - timeoutId = undefined; - }, 15000); - - history.push(redirectTarget); - - // return never-resolving promise to stop resolving and wait for the url change - return new Promise(() => {}); - } -} diff --git a/src/plugins/kibana_utils/public/history/index.ts b/src/plugins/kibana_utils/public/history/index.ts index 1a73bbb6b04a1..bb13ea09f928a 100644 --- a/src/plugins/kibana_utils/public/history/index.ts +++ b/src/plugins/kibana_utils/public/history/index.ts @@ -19,4 +19,3 @@ export { removeQueryParam } from './remove_query_param'; export { redirectWhenMissing } from './redirect_when_missing'; -export { ensureDefaultIndexPattern } from './ensure_default_index_pattern'; diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 2f139050e994a..c634322b23d0b 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -74,7 +74,7 @@ export { StartSyncStateFnType, StopSyncStateFnType, } from './state_sync'; -export { removeQueryParam, redirectWhenMissing, ensureDefaultIndexPattern } from './history'; +export { removeQueryParam, redirectWhenMissing } from './history'; export { applyDiff } from './state_management/utils/diff_object'; /** dummy plugin, we just want kibanaUtils to have its own bundle */ diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index 563ce87b82ae5..fb3d6efa63826 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -238,6 +238,7 @@ exports[`SavedObjectsTable import should show the flyout 1`] = ` indexPatterns={ Object { "clearCache": [MockFunction], + "ensureDefaultIndexPattern": [MockFunction], "get": [MockFunction], "make": [Function], } diff --git a/src/plugins/visualize/public/application/legacy_app.js b/src/plugins/visualize/public/application/legacy_app.js index 7c5e3ce9408f0..c7cc11c1f3ff5 100644 --- a/src/plugins/visualize/public/application/legacy_app.js +++ b/src/plugins/visualize/public/application/legacy_app.js @@ -21,11 +21,7 @@ import { find } from 'lodash'; import { i18n } from '@kbn/i18n'; import { createHashHistory } from 'history'; -import { - createKbnUrlStateStorage, - redirectWhenMissing, - ensureDefaultIndexPattern, -} from '../../../kibana_utils/public'; +import { createKbnUrlStateStorage, redirectWhenMissing } from '../../../kibana_utils/public'; import { createSavedSearchesLoader } from '../../../discover/public'; import editorTemplate from './editor/editor.html'; @@ -127,7 +123,7 @@ export function initVisualizeApp(app, deps) { controllerAs: 'listingController', resolve: { createNewVis: () => false, - hasDefaultIndex: history => ensureDefaultIndexPattern(deps.core, deps.data, history), + hasDefaultIndex: history => deps.data.indexPatterns.ensureDefaultIndexPattern(history), }, }) .when(VisualizeConstants.WIZARD_STEP_1_PAGE_PATH, { @@ -138,7 +134,7 @@ export function initVisualizeApp(app, deps) { controllerAs: 'listingController', resolve: { createNewVis: () => true, - hasDefaultIndex: history => ensureDefaultIndexPattern(deps.core, deps.data, history), + hasDefaultIndex: history => deps.data.indexPatterns.ensureDefaultIndexPattern(history), }, }) .when(VisualizeConstants.CREATE_PATH, { @@ -147,7 +143,7 @@ export function initVisualizeApp(app, deps) { k7Breadcrumbs: getCreateBreadcrumbs, resolve: { resolved: function($route, history) { - const { core, data, savedVisualizations, visualizations, toastNotifications } = deps; + const { data, savedVisualizations, visualizations, toastNotifications } = deps; const visTypes = visualizations.all(); const visType = find(visTypes, { name: $route.current.params.type }); const shouldHaveIndex = visType.requiresSearch && visType.options.showIndexSelection; @@ -164,7 +160,8 @@ export function initVisualizeApp(app, deps) { ); } - return ensureDefaultIndexPattern(core, data, history) + return data.indexPatterns + .ensureDefaultIndexPattern(history) .then(() => savedVisualizations.get($route.current.params)) .then(getResolvedResults(deps)) .catch( @@ -183,9 +180,10 @@ export function initVisualizeApp(app, deps) { k7Breadcrumbs: getEditBreadcrumbs, resolve: { resolved: function($route, history) { - const { chrome, core, data, savedVisualizations, toastNotifications } = deps; + const { chrome, data, savedVisualizations, toastNotifications } = deps; - return ensureDefaultIndexPattern(core, data, history) + return data.indexPatterns + .ensureDefaultIndexPattern(history) .then(() => savedVisualizations.get($route.current.params.id)) .then(savedVis => { chrome.recentlyAccessed.add(savedVis.getFullPath(), savedVis.title, savedVis.id); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 909a736220d17..5b55c7a15f1b3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -671,6 +671,7 @@ "data.functions.esaggs.inspector.dataRequest.description": "このリクエストは Elasticsearch にクエリし、ビジュアライゼーション用のデータを取得します。", "data.functions.esaggs.inspector.dataRequest.title": "データ", "data.indexPatterns.fetchFieldErrorTitle": "インデックスパターンのフィールド取得中にエラーが発生 {title} (ID: {id})", + "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。", "data.indexPatterns.unableWriteLabel": "インデックスパターンを書き込めません!このインデックスパターンへの最新の変更を取得するには、ページを更新してください。", "data.indexPatterns.unknownFieldErrorMessage": "インデックスパターン「{title}」のフィールド「{name}」が不明なフィールドタイプを使用しています。", "data.indexPatterns.unknownFieldHeader": "不明なフィールドタイプ {type}", @@ -2428,7 +2429,6 @@ "kibana_legacy.paginate.size.allDropDownOptionLabel": "すべて", "kibana_utils.defaultFeedbackMessage": "フィードバックがありますか?{link} で問題を報告してください。", "kibana_utils.history.savedObjectIsMissingNotificationMessage": "保存されたオブジェクトがありません", - "kibana_utils.indexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。", "kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage": "URL を完全に復元できません。共有機能を使用していることを確認してください。", "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "セッションがいっぱいで安全に削除できるアイテムが見つからないため、Kibana は履歴アイテムを保存できません。\n\nこれは大抵新規タブに移動することで解決されますが、より大きな問題が原因である可能性もあります。このメッセージが定期的に表示される場合は、{gitHubIssuesUrl} で問題を報告してください。", "kibana-react.dualRangeControl.mustSetBothErrorMessage": "下と上の値の両方を設定する必要があります", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0ebef9f8f1ef9..1758588d01ba8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -671,6 +671,7 @@ "data.functions.esaggs.inspector.dataRequest.description": "此请求将查询 Elasticsearch 以获取用于可视化的数据。", "data.functions.esaggs.inspector.dataRequest.title": "数据", "data.indexPatterns.fetchFieldErrorTitle": "提取索引模式 {title} (ID: {id}) 的字段时出错", + "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "若要在 Kibana 中可视化和浏览数据,您需要创建索引模式,以从 Elasticsearch 检索数据。", "data.indexPatterns.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。", "data.indexPatterns.unknownFieldErrorMessage": "indexPattern “{title}” 中的字段 “{name}” 使用未知字段类型。", "data.indexPatterns.unknownFieldHeader": "未知字段类型 {type}", @@ -2429,7 +2430,6 @@ "kibana_legacy.paginate.size.allDropDownOptionLabel": "全部", "kibana_utils.defaultFeedbackMessage": "想反馈?请在 {link} 中创建问题。", "kibana_utils.history.savedObjectIsMissingNotificationMessage": "已保存对象缺失", - "kibana_utils.indexPattern.bannerLabel": "若要在 Kibana 中可视化和浏览数据,您需要创建索引模式,以从 Elasticsearch 检索数据。", "kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage": "无法完全还原 URL,请确保使用共享功能。", "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "Kibana 无法将历史记录项存储在您的会话中,因为其已满,另外,似乎没有任何可安全删除的项目。\n\n通常,这可以通过移到全新的选项卡来解决,但这种情况可能是由更大的问题造成。如果您定期看到这个消息,请在 {gitHubIssuesUrl} 报告问题。", "kibana-react.dualRangeControl.mustSetBothErrorMessage": "下限值和上限值都须设置", From 2ace269a260829c7603f5925c304826363e000c8 Mon Sep 17 00:00:00 2001 From: Maryia Lapata Date: Fri, 24 Apr 2020 16:25:08 +0300 Subject: [PATCH 35/35] [NP] Vega migration (#63849) * Vega migartion * Move mocha tests to legacy * Fix TS * Update .i18nrc.json * Move mocks to vis_type_vega * Fix issue with babel and vega deps * Update mocha test * Mock services * Update vega_request_handler.ts * don't parse vega-lib/build/vega.js Co-authored-by: Elastic Machine Co-authored-by: spalger --- .i18nrc.json | 2 +- .../src/worker/webpack.config.ts | 5 +- .../__tests__/vis_type_vega}/vega_graph.hjson | 0 .../vis_type_vega}/vega_image_512.png | Bin .../vis_type_vega}/vega_map_image_256.png | Bin .../vis_type_vega}/vega_map_test.hjson | 0 .../vis_type_vega}/vega_tooltip_test.hjson | 0 .../vis_type_vega}/vega_visualization.js | 38 +++++++----- .../vis_type_vega}/vegalite_graph.hjson | 0 .../vis_type_vega}/vegalite_image_256.png | Bin .../vis_type_vega}/vegalite_image_512.png | Bin .../core_plugins/vis_type_vega/index.ts | 55 ------------------ .../core_plugins/vis_type_vega/package.json | 6 -- .../vis_type_vega/public/index.ts | 25 -------- .../vis_type_vega/public/legacy.ts | 38 ------------ .../ui/public/new_platform/new_platform.ts | 2 - .../ui/ui_exports/ui_export_defaults.js | 1 + src/plugins/vis_type_vega/kibana.json | 3 +- .../public/__mocks__/services.ts | 8 +-- .../vis_type_vega/public/_vega_editor.scss | 0 .../vis_type_vega/public/_vega_vis.scss | 0 .../vis_type_vega/public/components/index.ts | 0 .../public/components/vega_actions_menu.tsx | 0 .../public/components/vega_help_menu.tsx | 0 .../public/components/vega_vis_editor.tsx | 0 .../public/data_model/ems_file_parser.js | 0 .../public/data_model/es_query_parser.js | 0 .../public/data_model/es_query_parser.test.js | 0 .../public/data_model/search_cache.js | 0 .../public/data_model/search_cache.test.js | 0 .../public/data_model/time_cache.js | 0 .../public/data_model/time_cache.test.js | 0 .../public/data_model/url_parser.js | 0 .../vis_type_vega/public/data_model/utils.js | 0 .../public/data_model/vega_parser.js | 0 .../public/data_model/vega_parser.test.js | 0 .../vis_type_vega/public/default.spec.hjson | 0 .../vis_type_vega/public/index.scss | 2 - src/plugins/vis_type_vega/public/index.ts | 20 ++----- .../vis_type_vega/public/plugin.ts | 23 ++++---- .../vis_type_vega/public/services.ts | 7 +-- .../vis_type_vega/public/vega_fn.ts | 6 +- .../public/vega_request_handler.ts | 15 +++-- .../vis_type_vega/public/vega_type.ts | 4 +- .../public/vega_view/vega_base_view.js | 2 +- .../public/vega_view/vega_map_layer.js | 2 +- .../public/vega_view/vega_map_view.js | 2 +- .../public/vega_view/vega_tooltip.js | 0 .../public/vega_view/vega_view.js | 0 .../public/vega_visualization.js | 0 50 files changed, 72 insertions(+), 194 deletions(-) rename src/legacy/core_plugins/{vis_type_vega/public/__tests__ => kibana/public/__tests__/vis_type_vega}/vega_graph.hjson (100%) rename src/legacy/core_plugins/{vis_type_vega/public/__tests__ => kibana/public/__tests__/vis_type_vega}/vega_image_512.png (100%) rename src/legacy/core_plugins/{vis_type_vega/public/__tests__ => kibana/public/__tests__/vis_type_vega}/vega_map_image_256.png (100%) rename src/legacy/core_plugins/{vis_type_vega/public/__tests__ => kibana/public/__tests__/vis_type_vega}/vega_map_test.hjson (100%) rename src/legacy/core_plugins/{vis_type_vega/public/__tests__ => kibana/public/__tests__/vis_type_vega}/vega_tooltip_test.hjson (100%) rename src/legacy/core_plugins/{vis_type_vega/public/__tests__ => kibana/public/__tests__/vis_type_vega}/vega_visualization.js (86%) rename src/legacy/core_plugins/{vis_type_vega/public/__tests__ => kibana/public/__tests__/vis_type_vega}/vegalite_graph.hjson (100%) rename src/legacy/core_plugins/{vis_type_vega/public/__tests__ => kibana/public/__tests__/vis_type_vega}/vegalite_image_256.png (100%) rename src/legacy/core_plugins/{vis_type_vega/public/__tests__ => kibana/public/__tests__/vis_type_vega}/vegalite_image_512.png (100%) delete mode 100644 src/legacy/core_plugins/vis_type_vega/index.ts delete mode 100644 src/legacy/core_plugins/vis_type_vega/package.json delete mode 100644 src/legacy/core_plugins/vis_type_vega/public/index.ts delete mode 100644 src/legacy/core_plugins/vis_type_vega/public/legacy.ts rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/__mocks__/services.ts (87%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/_vega_editor.scss (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/_vega_vis.scss (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/components/index.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/components/vega_actions_menu.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/components/vega_help_menu.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/components/vega_vis_editor.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/data_model/ems_file_parser.js (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/data_model/es_query_parser.js (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/data_model/es_query_parser.test.js (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/data_model/search_cache.js (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/data_model/search_cache.test.js (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/data_model/time_cache.js (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/data_model/time_cache.test.js (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/data_model/url_parser.js (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/data_model/utils.js (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/data_model/vega_parser.js (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/data_model/vega_parser.test.js (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/default.spec.hjson (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/index.scss (78%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/plugin.ts (77%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/services.ts (86%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/vega_fn.ts (94%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/vega_request_handler.ts (84%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/vega_type.ts (92%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/vega_view/vega_base_view.js (99%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/vega_view/vega_map_layer.js (94%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/vega_view/vega_map_view.js (98%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/vega_view/vega_tooltip.js (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/vega_view/vega_view.js (100%) rename src/{legacy/core_plugins => plugins}/vis_type_vega/public/vega_visualization.js (100%) diff --git a/.i18nrc.json b/.i18nrc.json index 2ef92f544ad6b..35ce745234346 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -51,7 +51,7 @@ "visTypeTable": "src/plugins/vis_type_table", "visTypeTagCloud": "src/plugins/vis_type_tagcloud", "visTypeTimeseries": ["src/legacy/core_plugins/vis_type_timeseries", "src/plugins/vis_type_timeseries"], - "visTypeVega": "src/legacy/core_plugins/vis_type_vega", + "visTypeVega": "src/plugins/vis_type_vega", "visTypeVislib": "src/legacy/core_plugins/vis_type_vislib", "visTypeXy": "src/legacy/core_plugins/vis_type_xy", "visualizations": "src/plugins/visualizations", diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 3d52006fd2f35..7411e2df1b613 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -138,9 +138,12 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { module: { // no parse rules for a few known large packages which have no require() statements + // or which have require() statements that should be ignored because the file is + // already bundled with all its necessary depedencies noParse: [ /[\///]node_modules[\///]elasticsearch-browser[\///]/, - /[\///]node_modules[\///]lodash[\///]index\.js/, + /[\///]node_modules[\///]lodash[\///]index\.js$/, + /[\///]node_modules[\///]vega-lib[\///]build[\///]vega\.js$/, ], rules: [ diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_graph.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_graph.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_graph.hjson rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_graph.hjson diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_image_512.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_image_512.png similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_image_512.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_image_512.png diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_map_image_256.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_image_256.png similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_map_image_256.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_image_256.png diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_map_test.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_test.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_map_test.hjson rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_test.hjson diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_tooltip_test.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_tooltip_test.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_tooltip_test.hjson rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_tooltip_test.hjson diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js similarity index 86% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js index 6412d8a569b2a..21b7ea7dbf4c3 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js @@ -21,7 +21,9 @@ import Bluebird from 'bluebird'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import $ from 'jquery'; -import { createVegaVisualization } from '../vega_visualization'; +// Will be replaced with new path when tests are moved +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createVegaVisualization } from '../../../../../../plugins/vis_type_vega/public/vega_visualization'; import { ImageComparator } from 'test_utils/image_comparator'; import vegaliteGraph from '!!raw-loader!./vegalite_graph.hjson'; @@ -35,24 +37,34 @@ import vegaTooltipGraph from '!!raw-loader!./vega_tooltip_test.hjson'; import vegaMapGraph from '!!raw-loader!./vega_map_test.hjson'; import vegaMapImage256 from './vega_map_image_256.png'; +// Will be replaced with new path when tests are moved +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { VegaParser } from '../../../../../../plugins/vis_type_vega/public/data_model/vega_parser'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { SearchCache } from '../../../../../../plugins/vis_type_vega/public/data_model/search_cache'; -import { VegaParser } from '../data_model/vega_parser'; -import { SearchCache } from '../data_model/search_cache'; - -import { createVegaTypeDefinition } from '../vega_type'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createVegaTypeDefinition } from '../../../../../../plugins/vis_type_vega/public/vega_type'; // TODO This is an integration test and thus requires a running platform. When moving to the new platform, // this test has to be migrated to the newly created integration test environment. // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { npStart } from 'ui/new_platform'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { BaseVisType } from '../../../../../plugins/visualizations/public/vis_types/base_vis_type'; +import { BaseVisType } from '../../../../../../plugins/visualizations/public/vis_types/base_vis_type'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ExprVis } from '../../../../../plugins/visualizations/public/expressions/vis'; -import { setInjectedVars } from '../services'; +import { ExprVis } from '../../../../../../plugins/visualizations/public/expressions/vis'; + +import { + setInjectedVars, + setData, + setSavedObjects, + setNotifications, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/vis_type_vega/public/services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setInjectedVarFunc } from '../../../../../plugins/maps_legacy/public/kibana_services'; +import { setInjectedVarFunc } from '../../../../../../plugins/maps_legacy/public/kibana_services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ServiceSettings } from '../../../../../plugins/maps_legacy/public/map/service_settings'; +import { ServiceSettings } from '../../../../../../plugins/maps_legacy/public/map/service_settings'; const THRESHOLD = 0.1; const PIXEL_DIFF = 30; @@ -70,6 +82,9 @@ describe('VegaVisualizations', () => { enableExternalUrls: true, esShardTimeout: 10000, }); + setData(npStart.plugins.data); + setSavedObjects(npStart.core.savedObjects); + setNotifications(npStart.core.notifications); beforeEach(ngMock.module('kibana')); beforeEach( @@ -111,9 +126,6 @@ describe('VegaVisualizations', () => { timefilter: {}, }, }, - __LEGACY: { - esClient: npStart.plugins.data.search.__LEGACY.esClient, - }, }, }, }; diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_graph.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_graph.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_graph.hjson rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_graph.hjson diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_256.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_256.png similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_256.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_256.png diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_512.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_512.png similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_512.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_512.png diff --git a/src/legacy/core_plugins/vis_type_vega/index.ts b/src/legacy/core_plugins/vis_type_vega/index.ts deleted file mode 100644 index ac7e407ca9e4d..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/index.ts +++ /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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const vegaPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'vis_type_vega', - deprecations: ({ rename }: { rename: any }) => [ - rename('vega.enabled', 'vis_type_vega.enabled'), - ], - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - enableExternalUrls: Joi.boolean().default(false), - }).default(); - }, - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => { - const serverConfig = server.config(); - const mapConfig: Record = serverConfig.get('map'); - - return { - emsTileLayerId: mapConfig.emsTileLayerId, - }; - }, - }, - init: (server: Legacy.Server) => ({}), - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default vegaPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_vega/package.json b/src/legacy/core_plugins/vis_type_vega/package.json deleted file mode 100644 index acd6f3da128ab..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "author": "Yuri Astrakhan", - "name": "vega", - "version": "kibana" -} - diff --git a/src/legacy/core_plugins/vis_type_vega/public/index.ts b/src/legacy/core_plugins/vis_type_vega/public/index.ts deleted file mode 100644 index 34ca0e72190e4..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/index.ts +++ /dev/null @@ -1,25 +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 { PluginInitializerContext } from '../../../../core/public'; -import { VegaPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} diff --git a/src/legacy/core_plugins/vis_type_vega/public/legacy.ts b/src/legacy/core_plugins/vis_type_vega/public/legacy.ts deleted file mode 100644 index 450af4a6f253e..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/legacy.ts +++ /dev/null @@ -1,38 +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 { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; -import { VegaPluginSetupDependencies, VegaPluginStartDependencies } from './plugin'; -import { plugin } from '.'; - -const setupPlugins: Readonly = { - ...npSetup.plugins, - visualizations: npSetup.plugins.visualizations, - mapsLegacy: npSetup.plugins.mapsLegacy, -}; - -const startPlugins: Readonly = { - ...npStart.plugins, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core, startPlugins); diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 5ae2e2348aaa1..a15c7cce5511d 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -59,7 +59,6 @@ import { NavigationPublicPluginSetup, NavigationPublicPluginStart, } from '../../../../plugins/navigation/public'; -import { VisTypeVegaSetup } from '../../../../plugins/vis_type_vega/public'; import { DiscoverSetup, DiscoverStart } from '../../../../plugins/discover/public'; import { SavedObjectsManagementPluginSetup, @@ -88,7 +87,6 @@ export interface PluginsSetup { usageCollection: UsageCollectionSetup; advancedSettings: AdvancedSettingsSetup; management: ManagementSetup; - visTypeVega: VisTypeVegaSetup; discover: DiscoverSetup; visualizations: VisualizationsSetup; telemetry?: TelemetryPluginSetup; diff --git a/src/legacy/ui/ui_exports/ui_export_defaults.js b/src/legacy/ui/ui_exports/ui_export_defaults.js index bb246d97bfe4e..35e1f8b7d2127 100644 --- a/src/legacy/ui/ui_exports/ui_export_defaults.js +++ b/src/legacy/ui/ui_exports/ui_export_defaults.js @@ -24,6 +24,7 @@ export const UI_EXPORT_DEFAULTS = { webpackNoParseRules: [ /node_modules[\/\\](angular|elasticsearch-browser)[\/\\]/, /node_modules[\/\\](mocha|moment)[\/\\]/, + /node_modules[\/\\]vega-lib[\/\\]build[\/\\]vega\.js$/, ], webpackAliases: { diff --git a/src/plugins/vis_type_vega/kibana.json b/src/plugins/vis_type_vega/kibana.json index 6bfd6c9536df4..f1f82e7f5b7ad 100644 --- a/src/plugins/vis_type_vega/kibana.json +++ b/src/plugins/vis_type_vega/kibana.json @@ -2,5 +2,6 @@ "id": "visTypeVega", "version": "kibana", "server": true, - "ui": true + "ui": true, + "requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions"] } diff --git a/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts b/src/plugins/vis_type_vega/public/__mocks__/services.ts similarity index 87% rename from src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts rename to src/plugins/vis_type_vega/public/__mocks__/services.ts index b2f3e5b2241e6..1bf051232e4c9 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts +++ b/src/plugins/vis_type_vega/public/__mocks__/services.ts @@ -17,11 +17,11 @@ * under the License. */ -import { createGetterSetter } from '../../../../../plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../../plugins/data/public'; +import { createGetterSetter } from '../../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../../data/public'; import { IUiSettingsClient, NotificationsStart, SavedObjectsStart } from 'kibana/public'; -import { dataPluginMock } from '../../../../../plugins/data/public/mocks'; -import { coreMock } from '../../../../../core/public/mocks'; +import { dataPluginMock } from '../../../data/public/mocks'; +import { coreMock } from '../../../../core/public/mocks'; export const [getData, setData] = createGetterSetter('Data'); setData(dataPluginMock.createStartContract()); diff --git a/src/legacy/core_plugins/vis_type_vega/public/_vega_editor.scss b/src/plugins/vis_type_vega/public/_vega_editor.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/_vega_editor.scss rename to src/plugins/vis_type_vega/public/_vega_editor.scss diff --git a/src/legacy/core_plugins/vis_type_vega/public/_vega_vis.scss b/src/plugins/vis_type_vega/public/_vega_vis.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/_vega_vis.scss rename to src/plugins/vis_type_vega/public/_vega_vis.scss diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/index.ts b/src/plugins/vis_type_vega/public/components/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/components/index.ts rename to src/plugins/vis_type_vega/public/components/index.ts diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/vega_actions_menu.tsx b/src/plugins/vis_type_vega/public/components/vega_actions_menu.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/components/vega_actions_menu.tsx rename to src/plugins/vis_type_vega/public/components/vega_actions_menu.tsx diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/vega_help_menu.tsx b/src/plugins/vis_type_vega/public/components/vega_help_menu.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/components/vega_help_menu.tsx rename to src/plugins/vis_type_vega/public/components/vega_help_menu.tsx diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx b/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx rename to src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/ems_file_parser.js b/src/plugins/vis_type_vega/public/data_model/ems_file_parser.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/ems_file_parser.js rename to src/plugins/vis_type_vega/public/data_model/ems_file_parser.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.js b/src/plugins/vis_type_vega/public/data_model/es_query_parser.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.js rename to src/plugins/vis_type_vega/public/data_model/es_query_parser.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js b/src/plugins/vis_type_vega/public/data_model/es_query_parser.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js rename to src/plugins/vis_type_vega/public/data_model/es_query_parser.test.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.js b/src/plugins/vis_type_vega/public/data_model/search_cache.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.js rename to src/plugins/vis_type_vega/public/data_model/search_cache.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js b/src/plugins/vis_type_vega/public/data_model/search_cache.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js rename to src/plugins/vis_type_vega/public/data_model/search_cache.test.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.js b/src/plugins/vis_type_vega/public/data_model/time_cache.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.js rename to src/plugins/vis_type_vega/public/data_model/time_cache.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js b/src/plugins/vis_type_vega/public/data_model/time_cache.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js rename to src/plugins/vis_type_vega/public/data_model/time_cache.test.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/url_parser.js b/src/plugins/vis_type_vega/public/data_model/url_parser.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/url_parser.js rename to src/plugins/vis_type_vega/public/data_model/url_parser.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/utils.js b/src/plugins/vis_type_vega/public/data_model/utils.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/utils.js rename to src/plugins/vis_type_vega/public/data_model/utils.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.js rename to src/plugins/vis_type_vega/public/data_model/vega_parser.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js rename to src/plugins/vis_type_vega/public/data_model/vega_parser.test.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/default.spec.hjson b/src/plugins/vis_type_vega/public/default.spec.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/default.spec.hjson rename to src/plugins/vis_type_vega/public/default.spec.hjson diff --git a/src/legacy/core_plugins/vis_type_vega/public/index.scss b/src/plugins/vis_type_vega/public/index.scss similarity index 78% rename from src/legacy/core_plugins/vis_type_vega/public/index.scss rename to src/plugins/vis_type_vega/public/index.scss index 1ab2119d481a0..78d9eb61999f7 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/index.scss +++ b/src/plugins/vis_type_vega/public/index.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - // Prefix all styles with "vga" to avoid conflicts. // Examples // vgaChart diff --git a/src/plugins/vis_type_vega/public/index.ts b/src/plugins/vis_type_vega/public/index.ts index 71f3474f8217e..78878d38e1674 100644 --- a/src/plugins/vis_type_vega/public/index.ts +++ b/src/plugins/vis_type_vega/public/index.ts @@ -19,20 +19,8 @@ import { PluginInitializerContext } from 'kibana/public'; import { ConfigSchema } from '../config'; +import { VegaPlugin as Plugin } from './plugin'; -export const plugin = (initializerContext: PluginInitializerContext) => ({ - setup() { - return { - /** - * The configuration is temporarily exposed to allow the legacy vega plugin to consume - * the setting. Once the vega plugin is migrated completely, this will become an implementation - * detail. - * @deprecated - */ - config: initializerContext.config.get(), - }; - }, - start() {}, -}); - -export type VisTypeVegaSetup = ReturnType['setup']>; +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts similarity index 77% rename from src/legacy/core_plugins/vis_type_vega/public/plugin.ts rename to src/plugins/vis_type_vega/public/plugin.ts index 9fa77d28fbbfa..c312705194cde 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { Plugin as DataPublicPlugin } from '../../../../plugins/data/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { Plugin as DataPublicPlugin } from '../../data/public'; +import { VisualizationsSetup } from '../../visualizations/public'; import { setNotifications, setData, @@ -30,8 +30,10 @@ import { import { createVegaFn } from './vega_fn'; import { createVegaTypeDefinition } from './vega_type'; -import { VisTypeVegaSetup } from '../../../../plugins/vis_type_vega/public'; -import { IServiceSettings } from '../../../../plugins/maps_legacy/public'; +import { IServiceSettings } from '../../maps_legacy/public'; +import { ConfigSchema } from '../config'; + +import './index.scss'; /** @internal */ export interface VegaVisualizationDependencies { @@ -47,7 +49,6 @@ export interface VegaPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; data: ReturnType; - visTypeVega: VisTypeVegaSetup; mapsLegacy: any; } @@ -58,18 +59,18 @@ export interface VegaPluginStartDependencies { /** @internal */ export class VegaPlugin implements Plugin, void> { - initializerContext: PluginInitializerContext; + initializerContext: PluginInitializerContext; - constructor(initializerContext: PluginInitializerContext) { + constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; } public async setup( core: CoreSetup, - { data, expressions, visualizations, visTypeVega, mapsLegacy }: VegaPluginSetupDependencies + { data, expressions, visualizations, mapsLegacy }: VegaPluginSetupDependencies ) { setInjectedVars({ - enableExternalUrls: visTypeVega.config.enableExternalUrls, + enableExternalUrls: this.initializerContext.config.get().enableExternalUrls, esShardTimeout: core.injectedMetadata.getInjectedVar('esShardTimeout') as number, emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true), }); diff --git a/src/legacy/core_plugins/vis_type_vega/public/services.ts b/src/plugins/vis_type_vega/public/services.ts similarity index 86% rename from src/legacy/core_plugins/vis_type_vega/public/services.ts rename to src/plugins/vis_type_vega/public/services.ts index 88e0e0098bf8c..e349cfbdc0024 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/services.ts +++ b/src/plugins/vis_type_vega/public/services.ts @@ -18,10 +18,9 @@ */ import { SavedObjectsStart } from 'kibana/public'; -import { NotificationsStart } from 'src/core/public'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; -import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; -import { IUiSettingsClient } from '../../../../core/public'; +import { NotificationsStart, IUiSettingsClient } from 'src/core/public'; +import { DataPublicPluginStart } from '../../data/public'; +import { createGetterSetter } from '../../kibana_utils/public'; export const [getData, setData] = createGetterSetter('Data'); diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts similarity index 94% rename from src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts rename to src/plugins/vis_type_vega/public/vega_fn.ts index 2a0da81a31a96..6d45e043f7cee 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -19,11 +19,7 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaContext, - Render, -} from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, KibanaContext, Render } from '../../expressions/public'; import { VegaVisualizationDependencies } from './plugin'; import { createVegaRequestHandler } from './vega_request_handler'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts b/src/plugins/vis_type_vega/public/vega_request_handler.ts similarity index 84% rename from src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts rename to src/plugins/vis_type_vega/public/vega_request_handler.ts index f63efc0007c3b..196e8fdcbafda 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/plugins/vis_type_vega/public/vega_request_handler.ts @@ -17,9 +17,7 @@ * under the License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getSearchService } from '../../../../plugins/data/public/services'; -import { Filter, esQuery, TimeRange, Query } from '../../../../plugins/data/public'; +import { Filter, esQuery, TimeRange, Query } from '../../data/public'; // @ts-ignore import { VegaParser } from './data_model/vega_parser'; @@ -30,6 +28,7 @@ import { TimeCache } from './data_model/time_cache'; import { VegaVisualizationDependencies } from './plugin'; import { VisParams } from './vega_fn'; +import { getData } from './services'; interface VegaRequestHandlerParams { query: Query; @@ -43,12 +42,18 @@ export function createVegaRequestHandler({ core: { uiSettings }, serviceSettings, }: VegaVisualizationDependencies) { - const { esClient } = getSearchService().__LEGACY; - const searchCache = new SearchCache(esClient, { max: 10, maxAge: 4 * 1000 }); + let searchCache: SearchCache | undefined; const { timefilter } = data.query.timefilter; const timeCache = new TimeCache(timefilter, 3 * 1000); return ({ timeRange, filters, query, visParams }: VegaRequestHandlerParams) => { + if (!searchCache) { + searchCache = new SearchCache(getData().search.__LEGACY.esClient, { + max: 10, + maxAge: 4 * 1000, + }); + } + timeCache.setTimeRange(timeRange); const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts similarity index 92% rename from src/legacy/core_plugins/vis_type_vega/public/vega_type.ts rename to src/plugins/vis_type_vega/public/vega_type.ts index f56d7682efc6f..c864553c118b9 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -18,10 +18,10 @@ */ import { i18n } from '@kbn/i18n'; -import { DefaultEditorSize } from '../../../../plugins/vis_default_editor/public'; +import { DefaultEditorSize } from '../../vis_default_editor/public'; import { VegaVisualizationDependencies } from './plugin'; import { VegaVisEditor } from './components'; -import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/public'; +import { defaultFeedbackMessage } from '../../kibana_utils/public'; import { createVegaRequestHandler } from './vega_request_handler'; // @ts-ignore diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js similarity index 99% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_base_view.js rename to src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index c90f059ff7c94..be98d2b69ad87 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -26,7 +26,7 @@ import { Utils } from '../data_model/utils'; import { VISUALIZATION_COLORS } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TooltipHandler } from './vega_tooltip'; -import { esFilters } from '../../../../../plugins/data/public'; +import { esFilters } from '../../../data/public'; import { getEnableExternalUrls } from '../services'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js similarity index 94% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js rename to src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js index d43eb9c3351ea..8e4009eab8488 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js @@ -19,7 +19,7 @@ import L from 'leaflet'; import 'leaflet-vega'; -import { KibanaMapLayer } from '../../../../../plugins/maps_legacy/public'; +import { KibanaMapLayer } from '../../../maps_legacy/public'; export class VegaMapLayer extends KibanaMapLayer { constructor(spec, options) { diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js similarity index 98% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js rename to src/plugins/vis_type_vega/public/vega_view/vega_map_view.js index 03aef29dc5739..bd6652a597203 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js @@ -21,7 +21,7 @@ import * as vega from 'vega-lib'; import { i18n } from '@kbn/i18n'; import { VegaBaseView } from './vega_base_view'; import { VegaMapLayer } from './vega_map_layer'; -import { KibanaMap } from '../../../../../plugins/maps_legacy/public'; +import { KibanaMap } from '../../../maps_legacy/public'; import { getEmsTileLayerId, getUISettings } from '../services'; export class VegaMapView extends VegaBaseView { diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_tooltip.js b/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_tooltip.js rename to src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_view.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_view.js rename to src/plugins/vis_type_vega/public/vega_view/vega_view.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_visualization.js b/src/plugins/vis_type_vega/public/vega_visualization.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/vega_visualization.js rename to src/plugins/vis_type_vega/public/vega_visualization.js