From dd8eb62d9aad1e4e279fc338a242a8a39d0422b6 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 20 Feb 2020 12:05:08 +0100 Subject: [PATCH 01/43] Test NP logging config reload on SIGHUP (#57681) * TSify reload config test and simplify logic * get rid of mutable config in tests * increase timeouts * address comments --- .../reload_logging_config/kibana.test.yml | 3 + .../kibana_log_console.test.yml | 22 ++ .../kibana_log_file.test.yml | 22 ++ .../reload_logging_config.test.js | 224 --------------- .../reload_logging_config.test.ts | 263 ++++++++++++++++++ 5 files changed, 310 insertions(+), 224 deletions(-) create mode 100644 src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_console.test.yml create mode 100644 src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_file.test.yml delete mode 100644 src/cli/serve/integration_tests/reload_logging_config.test.js create mode 100644 src/cli/serve/integration_tests/reload_logging_config.test.ts diff --git a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml index 23f33940283c0d..594c2efc8adc92 100644 --- a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml +++ b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml @@ -1,4 +1,5 @@ server: + autoListen: false port: 8274 logging: json: true @@ -6,3 +7,5 @@ optimize: enabled: false plugins: initialize: false +migrations: + skip: true diff --git a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_console.test.yml b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_console.test.yml new file mode 100644 index 00000000000000..33dd4787efad92 --- /dev/null +++ b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_console.test.yml @@ -0,0 +1,22 @@ +server: + autoListen: false + port: 8274 +logging: + loggers: + - context: root + appenders: + - console + level: debug + appenders: + console: + kind: console + layout: + kind: json + root: + level: debug +optimize: + enabled: false +plugins: + initialize: false +migrations: + skip: true diff --git a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_file.test.yml b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_file.test.yml new file mode 100644 index 00000000000000..f5148899ff8542 --- /dev/null +++ b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_file.test.yml @@ -0,0 +1,22 @@ +server: + autoListen: false + port: 8274 +logging: + loggers: + - context: root + appenders: + - file + level: debug + appenders: + file: + kind: file + layout: + kind: pattern + root: + level: debug +optimize: + enabled: false +plugins: + initialize: false +migrations: + skip: true diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.js b/src/cli/serve/integration_tests/reload_logging_config.test.js deleted file mode 100644 index 82d514877aff64..00000000000000 --- a/src/cli/serve/integration_tests/reload_logging_config.test.js +++ /dev/null @@ -1,224 +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 { spawn } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; -import del from 'del'; - -import { safeDump } from 'js-yaml'; -import { - createMapStream, - createSplitStream, - createPromiseFromStreams, -} from '../../../legacy/utils/streams'; -import { getConfigFromFiles } from '../../../core/server/config/read_config'; - -const testConfigFile = follow('__fixtures__/reload_logging_config/kibana.test.yml'); -const kibanaPath = follow('../../../../scripts/kibana.js'); - -const second = 1000; -const minute = second * 60; - -const tempDir = path.join(os.tmpdir(), 'kbn-reload-test'); - -function follow(file) { - return path.relative(process.cwd(), path.resolve(__dirname, file)); -} - -function setLoggingJson(enabled) { - const conf = getConfigFromFiles([testConfigFile]); - conf.logging = conf.logging || {}; - conf.logging.json = enabled; - - const yaml = safeDump(conf); - - fs.writeFileSync(testConfigFile, yaml); -} - -describe('Server logging configuration', function() { - let child; - let isJson; - - beforeEach(() => { - isJson = true; - setLoggingJson(true); - - fs.mkdirSync(tempDir, { recursive: true }); - }); - - afterEach(() => { - isJson = true; - setLoggingJson(true); - - if (child !== undefined) { - child.kill(); - child = undefined; - } - - del.sync(tempDir, { force: true }); - }); - - const isWindows = /^win/.test(process.platform); - if (isWindows) { - it('SIGHUP is not a feature of Windows.', () => { - // nothing to do for Windows - }); - } else { - it( - 'should be reloadable via SIGHUP process signaling', - async function() { - expect.assertions(3); - - child = spawn( - process.execPath, - [kibanaPath, '--config', testConfigFile, '--oss', '--verbose'], - { - stdio: 'pipe', - } - ); - - let sawJson = false; - let sawNonjson = false; - - const [exitCode] = await Promise.all([ - Promise.race([ - new Promise(r => child.once('exit', r)).then(code => (code === null ? 0 : code)), - - new Promise(r => child.once('error', r)).then(err => { - throw new Error( - `error in child process while attempting to reload config. ${err.stack || - err.message || - err}` - ); - }), - ]), - - createPromiseFromStreams([ - child.stdout, - createSplitStream('\n'), - createMapStream(async line => { - if (!line) { - // skip empty lines - return; - } - - if (isJson) { - const data = JSON.parse(line); - sawJson = true; - - // We know the sighup handler will be registered before - // root.setup() is called - if (data.message.includes('setting up root')) { - isJson = false; - setLoggingJson(false); - - // Reload logging config. We give it a little bit of time to just make - // sure the process sighup handler is registered. - await new Promise(r => setTimeout(r, 100)); - child.kill('SIGHUP'); - } - } else if (line.startsWith('{')) { - // We have told Kibana to stop logging json, but it hasn't completed - // the switch yet, so we ignore before switching over. - } else { - // Kibana has successfully stopped logging json, so kill the server. - sawNonjson = true; - - child && child.kill(); - child = undefined; - } - }), - ]), - ]); - - expect(exitCode).toEqual(0); - expect(sawJson).toEqual(true); - expect(sawNonjson).toEqual(true); - }, - minute - ); - - it( - 'should recreate file handler on SIGHUP', - function(done) { - expect.hasAssertions(); - - const logPath = path.resolve(tempDir, 'kibana.log'); - const logPathArchived = path.resolve(tempDir, 'kibana_archive.log'); - - function watchFileUntil(path, matcher, timeout) { - return new Promise((resolve, reject) => { - const timeoutHandle = setTimeout(() => { - fs.unwatchFile(path); - reject(`watchFileUntil timed out for "${matcher}"`); - }, timeout); - - fs.watchFile(path, () => { - try { - const contents = fs.readFileSync(path); - - if (matcher.test(contents)) { - clearTimeout(timeoutHandle); - fs.unwatchFile(path); - resolve(contents); - } - } catch (e) { - // noop - } - }); - }); - } - - child = spawn(process.execPath, [ - kibanaPath, - '--oss', - '--config', - testConfigFile, - '--logging.dest', - logPath, - '--plugins.initialize', - 'false', - '--logging.json', - 'false', - '--verbose', - ]); - - watchFileUntil(logPath, /starting server/, 2 * minute) - .then(() => { - // once the server is running, archive the log file and issue SIGHUP - fs.renameSync(logPath, logPathArchived); - child.kill('SIGHUP'); - }) - .then(() => - watchFileUntil(logPath, /Reloaded logging configuration due to SIGHUP/, 10 * second) - ) - .then(contents => { - const lines = contents.toString().split('\n'); - // should be the first line of the new log file - expect(lines[0]).toMatch(/Reloaded logging configuration due to SIGHUP/); - child.kill(); - }) - .then(done, done); - }, - 3 * minute - ); - } -}); diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.ts b/src/cli/serve/integration_tests/reload_logging_config.test.ts new file mode 100644 index 00000000000000..2def3569828d34 --- /dev/null +++ b/src/cli/serve/integration_tests/reload_logging_config.test.ts @@ -0,0 +1,263 @@ +/* + * 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 Child from 'child_process'; +import Fs from 'fs'; +import Path from 'path'; +import Os from 'os'; +import Del from 'del'; + +import * as Rx from 'rxjs'; +import { map, filter, take } from 'rxjs/operators'; +import { safeDump } from 'js-yaml'; + +import { getConfigFromFiles } from '../../../core/server/config/read_config'; + +const legacyConfig = follow('__fixtures__/reload_logging_config/kibana.test.yml'); +const configFileLogConsole = follow( + '__fixtures__/reload_logging_config/kibana_log_console.test.yml' +); +const configFileLogFile = follow('__fixtures__/reload_logging_config/kibana_log_file.test.yml'); + +const kibanaPath = follow('../../../../scripts/kibana.js'); + +const second = 1000; +const minute = second * 60; + +const tempDir = Path.join(Os.tmpdir(), 'kbn-reload-test'); + +function follow(file: string) { + return Path.relative(process.cwd(), Path.resolve(__dirname, file)); +} + +function watchFileUntil(path: string, matcher: RegExp, timeout: number) { + return new Promise((resolve, reject) => { + const timeoutHandle = setTimeout(() => { + Fs.unwatchFile(path); + reject(`watchFileUntil timed out for "${matcher}"`); + }, timeout); + + Fs.watchFile(path, () => { + try { + const contents = Fs.readFileSync(path, 'utf-8'); + + if (matcher.test(contents)) { + clearTimeout(timeoutHandle); + Fs.unwatchFile(path); + resolve(contents); + } + } catch (e) { + // noop + } + }); + }); +} + +function containsJsonOnly(content: string[]) { + return content.every(line => line.startsWith('{')); +} + +function createConfigManager(configPath: string) { + return { + modify(fn: (input: Record) => Record) { + const oldContent = getConfigFromFiles([configPath]); + const yaml = safeDump(fn(oldContent)); + Fs.writeFileSync(configPath, yaml); + }, + }; +} + +describe('Server logging configuration', function() { + let child: Child.ChildProcess; + beforeEach(() => { + Fs.mkdirSync(tempDir, { recursive: true }); + }); + + afterEach(async () => { + if (child !== undefined) { + child.kill(); + // wait for child to be killed otherwise jest complains that process not finished + await new Promise(res => setTimeout(res, 1000)); + } + Del.sync(tempDir, { force: true }); + }); + + const isWindows = /^win/.test(process.platform); + if (isWindows) { + it('SIGHUP is not a feature of Windows.', () => { + // nothing to do for Windows + }); + } else { + describe('legacy logging', () => { + it( + 'should be reloadable via SIGHUP process signaling', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(legacyConfig, configFilePath); + + child = Child.spawn(process.execPath, [ + kibanaPath, + '--oss', + '--config', + configFilePath, + '--verbose', + ]); + + const message$ = Rx.fromEvent(child.stdout, 'data').pipe( + map(messages => + String(messages) + .split('\n') + .filter(Boolean) + ) + ); + + await message$ + .pipe( + // We know the sighup handler will be registered before this message logged + filter(messages => messages.some(m => m.includes('setting up root'))), + take(1) + ) + .toPromise(); + + const lastMessage = await message$.pipe(take(1)).toPromise(); + expect(containsJsonOnly(lastMessage)).toBe(true); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.json = false; + return oldConfig; + }); + + child.kill('SIGHUP'); + + await message$ + .pipe( + filter(messages => !containsJsonOnly(messages)), + take(1) + ) + .toPromise(); + }, + minute + ); + + it( + 'should recreate file handle on SIGHUP', + async function() { + const logPath = Path.resolve(tempDir, 'kibana.log'); + const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log'); + + child = Child.spawn(process.execPath, [ + kibanaPath, + '--oss', + '--config', + legacyConfig, + '--logging.dest', + logPath, + '--verbose', + ]); + + await watchFileUntil(logPath, /setting up root/, 30 * second); + // once the server is running, archive the log file and issue SIGHUP + Fs.renameSync(logPath, logPathArchived); + child.kill('SIGHUP'); + + await watchFileUntil( + logPath, + /Reloaded logging configuration due to SIGHUP/, + 30 * second + ); + }, + minute + ); + }); + + describe('platform logging', () => { + it( + 'should be reloadable via SIGHUP process signaling', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(configFileLogConsole, configFilePath); + + child = Child.spawn(process.execPath, [kibanaPath, '--oss', '--config', configFilePath]); + + const message$ = Rx.fromEvent(child.stdout, 'data').pipe( + map(messages => + String(messages) + .split('\n') + .filter(Boolean) + ) + ); + + await message$ + .pipe( + // We know the sighup handler will be registered before this message logged + filter(messages => messages.some(m => m.includes('setting up root'))), + take(1) + ) + .toPromise(); + + const lastMessage = await message$.pipe(take(1)).toPromise(); + expect(containsJsonOnly(lastMessage)).toBe(true); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.appenders.console.layout.kind = 'pattern'; + return oldConfig; + }); + child.kill('SIGHUP'); + + await message$ + .pipe( + filter(messages => !containsJsonOnly(messages)), + take(1) + ) + .toPromise(); + }, + 30 * second + ); + it( + 'should recreate file handle on SIGHUP', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(configFileLogFile, configFilePath); + + const logPath = Path.resolve(tempDir, 'kibana.log'); + const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log'); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.appenders.file.path = logPath; + return oldConfig; + }); + + child = Child.spawn(process.execPath, [kibanaPath, '--oss', '--config', configFilePath]); + + await watchFileUntil(logPath, /setting up root/, 30 * second); + // once the server is running, archive the log file and issue SIGHUP + Fs.renameSync(logPath, logPathArchived); + child.kill('SIGHUP'); + + await watchFileUntil( + logPath, + /Reloaded logging configuration due to SIGHUP/, + 30 * second + ); + }, + minute + ); + }); + } +}); From d6aff77485ce28bc7d50a5f4bac513024c70ea57 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Thu, 20 Feb 2020 13:12:04 +0100 Subject: [PATCH 02/43] remove NODE_ENV=test (#58037) Co-authored-by: Elastic Machine --- .ci/Jenkinsfile_coverage | 6 +----- test/scripts/jenkins_unit.sh | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index e40cc584dc376c..fa1e141be93eaa 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -13,11 +13,7 @@ stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a ]) { parallel([ 'kibana-intake-agent': { - withEnv([ - 'NODE_ENV=test' // Needed for jest tests only - ]) { - kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh')() - } + kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh')() }, 'x-pack-intake-agent': { withEnv([ diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index fe67594ad8ac2d..a9751003e84258 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -5,7 +5,6 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]] ; then "$(FORCE_COLOR=0 yarn bin)/grunt" jenkins:unit --dev; else - echo "NODE_ENV=$NODE_ENV" echo " -> Running jest tests with coverage" node scripts/jest --ci --verbose --coverage echo "" From b07aa096060266e16366766d716fd3d5c7b1a30c Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 20 Feb 2020 13:25:02 +0100 Subject: [PATCH 03/43] [Watcher] Refactor to use client from `RequestHandlerContext` (#57834) * Remove elasticsearch setup service from route deps Removed the callWithRequestFactory entirely. This setup was introducing a pattern where route handlers were not pulling the ES client fromt the route handler context. For this refactor we need to extend the route handler context with watcher specific client actions and so we also extend RequestHandlerContext globally. In this commit we also update the types for params, query and body schema on each route to avoid using any everwhere. * Add generic types to license wrapper Adding to the license wrapper made it a transparent wrapper from a type perspective so we can remove the need to eplicitly set RequestHandler on the handler. Also cleaned up a variable name "response" -> "searchResults" Also removed elasticsearch from the RouteDependencies type. Co-authored-by: Elastic Machine --- x-pack/plugins/watcher/server/index.ts | 2 + .../server/lib/call_with_request_factory.ts | 28 ---- .../fetch_all_from_scroll.ts | 29 ++-- .../license_pre_routing_factory.ts | 6 +- x-pack/plugins/watcher/server/plugin.ts | 31 +++- .../routes/api/indices/register_get_route.ts | 96 ++++++------ .../api/license/register_refresh_route.ts | 10 +- .../routes/api/register_list_fields_route.ts | 64 ++++---- .../routes/api/register_load_history_route.ts | 82 +++++----- .../api/settings/register_load_route.ts | 33 ++--- .../action/register_acknowledge_route.ts | 72 +++++---- .../api/watch/register_activate_route.ts | 75 +++++----- .../api/watch/register_deactivate_route.ts | 76 +++++----- .../routes/api/watch/register_delete_route.ts | 55 ++++--- .../api/watch/register_execute_route.ts | 88 +++++------ .../api/watch/register_history_route.ts | 103 ++++++------- .../routes/api/watch/register_load_route.ts | 82 +++++----- .../routes/api/watch/register_save_route.ts | 140 +++++++++--------- .../api/watch/register_visualize_route.ts | 68 +++++---- .../api/watches/register_delete_route.ts | 42 +++--- .../routes/api/watches/register_list_route.ts | 95 ++++++------ x-pack/plugins/watcher/server/types.ts | 4 +- 22 files changed, 633 insertions(+), 648 deletions(-) delete mode 100644 x-pack/plugins/watcher/server/lib/call_with_request_factory.ts diff --git a/x-pack/plugins/watcher/server/index.ts b/x-pack/plugins/watcher/server/index.ts index 51eb7bfa543fe1..356be781fb1944 100644 --- a/x-pack/plugins/watcher/server/index.ts +++ b/x-pack/plugins/watcher/server/index.ts @@ -6,4 +6,6 @@ import { PluginInitializerContext } from 'kibana/server'; import { WatcherServerPlugin } from './plugin'; +export { WatcherContext } from './plugin'; + export const plugin = (ctx: PluginInitializerContext) => new WatcherServerPlugin(ctx); diff --git a/x-pack/plugins/watcher/server/lib/call_with_request_factory.ts b/x-pack/plugins/watcher/server/lib/call_with_request_factory.ts deleted file mode 100644 index 4884c75436c246..00000000000000 --- a/x-pack/plugins/watcher/server/lib/call_with_request_factory.ts +++ /dev/null @@ -1,28 +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 { ElasticsearchServiceSetup } from 'kibana/server'; -import { once } from 'lodash'; -import { elasticsearchJsPlugin } from './elasticsearch_js_plugin'; - -const callWithRequest = once((elasticsearchService: ElasticsearchServiceSetup) => { - const config = { plugins: [elasticsearchJsPlugin] }; - return elasticsearchService.createClient('watcher', config); -}); - -export const callWithRequestFactory = ( - elasticsearchService: ElasticsearchServiceSetup, - request: any -) => { - return (...args: any[]) => { - return ( - callWithRequest(elasticsearchService) - .asScoped(request) - // @ts-ignore - .callAsCurrentUser(...args) - ); - }; -}; diff --git a/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts b/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts index de01bd59655040..8e8ca369dd02b4 100644 --- a/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts +++ b/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts @@ -4,24 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; import { ES_SCROLL_SETTINGS } from '../../../common/constants'; -export function fetchAllFromScroll(response: any, callWithRequest: any, hits: any[] = []) { - const newHits = get(response, 'hits.hits', []); - const scrollId = get(response, '_scroll_id'); +export function fetchAllFromScroll( + searchResuls: any, + dataClient: IScopedClusterClient, + hits: any[] = [] +): Promise { + const newHits = get(searchResuls, 'hits.hits', []); + const scrollId = get(searchResuls, '_scroll_id'); if (newHits.length > 0) { hits.push(...newHits); - return callWithRequest('scroll', { - body: { - scroll: ES_SCROLL_SETTINGS.KEEPALIVE, - scroll_id: scrollId, - }, - }).then((innerResponse: any) => { - return fetchAllFromScroll(innerResponse, callWithRequest, hits); - }); + return dataClient + .callAsCurrentUser('scroll', { + body: { + scroll: ES_SCROLL_SETTINGS.KEEPALIVE, + scroll_id: scrollId, + }, + }) + .then((innerResponse: any) => { + return fetchAllFromScroll(innerResponse, dataClient, hits); + }); } return Promise.resolve(hits); diff --git a/x-pack/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts index d010a239527254..1b2476fc78b452 100644 --- a/x-pack/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts +++ b/x-pack/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts @@ -12,13 +12,13 @@ import { } from 'kibana/server'; import { RouteDependencies } from '../../types'; -export const licensePreRoutingFactory = ( +export const licensePreRoutingFactory = ( { getLicenseStatus }: RouteDependencies, - handler: RequestHandler + handler: RequestHandler ) => { return function licenseCheck( ctx: RequestHandlerContext, - request: KibanaRequest, + request: KibanaRequest, response: KibanaResponseFactory ) { const licenseStatus = getLicenseStatus(); diff --git a/x-pack/plugins/watcher/server/plugin.ts b/x-pack/plugins/watcher/server/plugin.ts index 1f7b3823609eca..51d85c2001bd28 100644 --- a/x-pack/plugins/watcher/server/plugin.ts +++ b/x-pack/plugins/watcher/server/plugin.ts @@ -3,7 +3,20 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; + +declare module 'kibana/server' { + interface RequestHandlerContext { + watcher?: WatcherContext; + } +} + +import { + CoreSetup, + IScopedClusterClient, + Logger, + Plugin, + PluginInitializerContext, +} from 'kibana/server'; import { PLUGIN } from '../common/constants'; import { Dependencies, LicenseStatus, RouteDependencies } from './types'; import { LICENSE_CHECK_STATE } from '../../licensing/server'; @@ -15,6 +28,11 @@ import { registerWatchesRoutes } from './routes/api/watches'; import { registerWatchRoutes } from './routes/api/watch'; import { registerListFieldsRoute } from './routes/api/register_list_fields_route'; import { registerLoadHistoryRoute } from './routes/api/register_load_history_route'; +import { elasticsearchJsPlugin } from './lib/elasticsearch_js_plugin'; + +export interface WatcherContext { + client: IScopedClusterClient; +} export class WatcherServerPlugin implements Plugin { log: Logger; @@ -31,15 +49,20 @@ export class WatcherServerPlugin implements Plugin { { http, elasticsearch: elasticsearchService }: CoreSetup, { licensing }: Dependencies ) { - const elasticsearch = await elasticsearchService.adminClient; const router = http.createRouter(); const routeDependencies: RouteDependencies = { - elasticsearch, - elasticsearchService, router, getLicenseStatus: () => this.licenseStatus, }; + const config = { plugins: [elasticsearchJsPlugin] }; + const watcherESClient = elasticsearchService.createClient('watcher', config); + http.registerRouteHandlerContext('watcher', (ctx, request) => { + return { + client: watcherESClient.asScoped(request), + }; + }); + registerListFieldsRoute(routeDependencies); registerLoadHistoryRoute(routeDependencies); registerIndicesRoutes(routeDependencies); diff --git a/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts b/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts index 30607b82e32950..df6f62135baeb6 100644 --- a/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts @@ -5,13 +5,14 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { reduce, size } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +const bodySchema = schema.object({ pattern: schema.string() }, { allowUnknowns: true }); + function getIndexNamesFromAliasesResponse(json: Record) { return reduce( json, @@ -26,67 +27,66 @@ function getIndexNamesFromAliasesResponse(json: Record) { ); } -function getIndices(callWithRequest: any, pattern: string, limit = 10) { - return callWithRequest('indices.getAlias', { - index: pattern, - ignore: [404], - }).then((aliasResult: any) => { - if (aliasResult.status !== 404) { - const indicesFromAliasResponse = getIndexNamesFromAliasesResponse(aliasResult); - return indicesFromAliasResponse.slice(0, limit); - } - - const params = { +function getIndices(dataClient: IScopedClusterClient, pattern: string, limit = 10) { + return dataClient + .callAsCurrentUser('indices.getAlias', { index: pattern, ignore: [404], - body: { - size: 0, // no hits - aggs: { - indices: { - terms: { - field: '_index', - size: limit, + }) + .then((aliasResult: any) => { + if (aliasResult.status !== 404) { + const indicesFromAliasResponse = getIndexNamesFromAliasesResponse(aliasResult); + return indicesFromAliasResponse.slice(0, limit); + } + + const params = { + index: pattern, + ignore: [404], + body: { + size: 0, // no hits + aggs: { + indices: { + terms: { + field: '_index', + size: limit, + }, }, }, }, - }, - }; + }; - return callWithRequest('search', params).then((response: any) => { - if (response.status === 404 || !response.aggregations) { - return []; - } - return response.aggregations.indices.buckets.map((bucket: any) => bucket.key); + return dataClient.callAsCurrentUser('search', params).then((response: any) => { + if (response.status === 404 || !response.aggregations) { + return []; + } + return response.aggregations.indices.buckets.map((bucket: any) => bucket.key); + }); }); - }); } export function registerGetRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const { pattern } = request.body; - - try { - const indices = await getIndices(callWithRequest, pattern); - return response.ok({ body: { indices } }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.post( { path: '/api/watcher/indices', validate: { - body: schema.object({}, { allowUnknowns: true }), + body: bodySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { pattern } = request.body; + + try { + const indices = await getIndices(ctx.watcher!.client, pattern); + return response.ok({ body: { indices } }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/license/register_refresh_route.ts b/x-pack/plugins/watcher/server/routes/api/license/register_refresh_route.ts index a61fd16e8be4ad..bd537cd6d21ab5 100644 --- a/x-pack/plugins/watcher/server/routes/api/license/register_refresh_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/license/register_refresh_route.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandler } from 'kibana/server'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; /* @@ -13,16 +12,15 @@ it needs to make a round-trip to the kibana server. This refresh endpoint is pro for when the client needs to check the license, but doesn't need to pull data from the server for any reason, i.e., when adding a new watch. */ -export function registerRefreshRoute(deps: RouteDependencies) { - const handler: RequestHandler = (ctx, request, response) => { - return response.ok({ body: { success: true } }); - }; +export function registerRefreshRoute(deps: RouteDependencies) { deps.router.get( { path: '/api/watcher/license/refresh', validate: false, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, (ctx, request, response) => { + return response.ok({ body: { success: true } }); + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts b/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts index 7c47379b875892..d72e5ad2f817de 100644 --- a/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts @@ -5,15 +5,18 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../lib/is_es_error'; // @ts-ignore import { Fields } from '../../models/fields/index'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; import { RouteDependencies } from '../../types'; -function fetchFields(callWithRequest: any, indexes: string[]) { +const bodySchema = schema.object({ + indexes: schema.arrayOf(schema.string()), +}); + +function fetchFields(dataClient: IScopedClusterClient, indexes: string[]) { const params = { index: indexes, fields: ['*'], @@ -22,44 +25,39 @@ function fetchFields(callWithRequest: any, indexes: string[]) { ignore: 404, }; - return callWithRequest('fieldCaps', params); + return dataClient.callAsCurrentUser('fieldCaps', params); } export function registerListFieldsRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const { indexes } = request.body; - - try { - const fieldsResponse = await fetchFields(callWithRequest, indexes); - const json = fieldsResponse.status === 404 ? { fields: [] } : fieldsResponse; - const fields = Fields.fromUpstreamJson(json); - return response.ok({ body: fields.downstreamJson }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ - statusCode: e.statusCode, - body: { - message: e.message, - }, - }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.post( { path: '/api/watcher/fields', validate: { - body: schema.object({ - indexes: schema.arrayOf(schema.string()), - }), + body: bodySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { indexes } = request.body; + + try { + const fieldsResponse = await fetchFields(ctx.watcher!.client, indexes); + const json = fieldsResponse.status === 404 ? { fields: [] } : fieldsResponse; + const fields = Fields.fromUpstreamJson(json); + return response.ok({ body: fields.downstreamJson }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: { + message: e.message, + }, + }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts b/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts index 1be8477df79bc7..8c9068123ce8d7 100644 --- a/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts @@ -6,8 +6,7 @@ import { schema } from '@kbn/config-schema'; import { get } from 'lodash'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../lib/is_es_error'; import { INDEX_NAMES } from '../../../common/constants'; import { RouteDependencies } from '../../types'; @@ -15,8 +14,12 @@ import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory' // @ts-ignore import { WatchHistoryItem } from '../../models/watch_history_item/index'; -function fetchHistoryItem(callWithRequest: any, watchHistoryItemId: string) { - return callWithRequest('search', { +const paramsSchema = schema.object({ + id: schema.string(), +}); + +function fetchHistoryItem(dataClient: IScopedClusterClient, watchHistoryItemId: string) { + return dataClient.callAsCurrentUser('search', { index: INDEX_NAMES.WATCHER_HISTORY, body: { query: { @@ -29,49 +32,44 @@ function fetchHistoryItem(callWithRequest: any, watchHistoryItemId: string) { } export function registerLoadHistoryRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const id = request.params.id; - - try { - const responseFromES = await fetchHistoryItem(callWithRequest, id); - const hit = get(responseFromES, 'hits.hits[0]'); - if (!hit) { - return response.notFound({ body: `Watch History Item with id = ${id} not found` }); - } - const watchHistoryItemJson = get(hit, '_source'); - const watchId = get(hit, '_source.watch_id'); - const json = { - id, - watchId, - watchHistoryItemJson, - includeDetails: true, - }; - - const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); - return response.ok({ - body: { watchHistoryItem: watchHistoryItem.downstreamJson }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.get( { path: '/api/watcher/history/{id}', validate: { - params: schema.object({ - id: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const id = request.params.id; + + try { + const responseFromES = await fetchHistoryItem(ctx.watcher!.client, id); + const hit = get(responseFromES, 'hits.hits[0]'); + if (!hit) { + return response.notFound({ body: `Watch History Item with id = ${id} not found` }); + } + const watchHistoryItemJson = get(hit, '_source'); + const watchId = get(hit, '_source.watch_id'); + const json = { + id, + watchId, + watchHistoryItemJson, + includeDetails: true, + }; + + const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); + return response.ok({ + body: { watchHistoryItem: watchHistoryItem.downstreamJson }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts b/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts index 6c70c2d0d07b6a..fe9dd32735692d 100644 --- a/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IClusterClient, RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../../lib/is_es_error'; // @ts-ignore import { Settings } from '../../../models/settings/index'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; -function fetchClusterSettings(client: IClusterClient) { +function fetchClusterSettings(client: IScopedClusterClient) { return client.callAsInternalUser('cluster.getSettings', { includeDefaults: true, filterPath: '**.xpack.notification', @@ -19,25 +19,24 @@ function fetchClusterSettings(client: IClusterClient) { } export function registerLoadRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - try { - const settings = await fetchClusterSettings(deps.elasticsearch); - return response.ok({ body: Settings.fromUpstreamJson(settings).downstreamJson }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; deps.router.get( { path: '/api/watcher/settings', validate: false, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + try { + const settings = await fetchClusterSettings(ctx.watcher!.client); + return response.ok({ body: Settings.fromUpstreamJson(settings).downstreamJson }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts index 08eec7456e3a57..9e024a63b82c57 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts @@ -6,60 +6,58 @@ import { schema } from '@kbn/config-schema'; import { get } from 'lodash'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../../../lib/is_es_error'; // @ts-ignore import { WatchStatus } from '../../../../models/watch_status/index'; import { RouteDependencies } from '../../../../types'; import { licensePreRoutingFactory } from '../../../../lib/license_pre_routing_factory'; -function acknowledgeAction(callWithRequest: any, watchId: string, actionId: string) { - return callWithRequest('watcher.ackWatch', { +const paramsSchema = schema.object({ + watchId: schema.string(), + actionId: schema.string(), +}); + +function acknowledgeAction(dataClient: IScopedClusterClient, watchId: string, actionId: string) { + return dataClient.callAsCurrentUser('watcher.ackWatch', { id: watchId, action: actionId, }); } export function registerAcknowledgeRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const { watchId, actionId } = request.params; - - try { - const hit = await acknowledgeAction(callWithRequest, watchId, actionId); - const watchStatusJson = get(hit, 'status'); - const json = { - id: watchId, - watchStatusJson, - }; - - const watchStatus = WatchStatus.fromUpstreamJson(json); - return response.ok({ - body: { watchStatus: watchStatus.downstreamJson }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; - return response.customError({ statusCode: e.statusCode, body }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.put( { path: '/api/watcher/watch/{watchId}/action/{actionId}/acknowledge', validate: { - params: schema.object({ - watchId: schema.string(), - actionId: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { watchId, actionId } = request.params; + + try { + const hit = await acknowledgeAction(ctx.watcher!.client, watchId, actionId); + const watchStatusJson = get(hit, 'status'); + const json = { + id: watchId, + watchStatusJson, + }; + + const watchStatus = WatchStatus.fromUpstreamJson(json); + return response.ok({ + body: { watchStatus: watchStatus.downstreamJson }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts index fdc20854ed8c22..1afeeb4e80efbf 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts @@ -5,62 +5,59 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; // @ts-ignore import { WatchStatus } from '../../../models/watch_status/index'; -function activateWatch(callWithRequest: any, watchId: string) { - return callWithRequest('watcher.activateWatch', { +function activateWatch(dataClient: IScopedClusterClient, watchId: string) { + return dataClient.callAsCurrentUser('watcher.activateWatch', { id: watchId, }); } -export function registerActivateRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - const { watchId } = request.params; - - try { - const hit = await activateWatch(callWithRequest, watchId); - const watchStatusJson = get(hit, 'status'); - const json = { - id: watchId, - watchStatusJson, - }; - - const watchStatus = WatchStatus.fromUpstreamJson(json); - return response.ok({ - body: { - watchStatus: watchStatus.downstreamJson, - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; - return response.customError({ statusCode: e.statusCode, body }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; +const paramsSchema = schema.object({ + watchId: schema.string(), +}); +export function registerActivateRoute(deps: RouteDependencies) { deps.router.put( { path: '/api/watcher/watch/{watchId}/activate', validate: { - params: schema.object({ - watchId: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { watchId } = request.params; + + try { + const hit = await activateWatch(ctx.watcher!.client, watchId); + const watchStatusJson = get(hit, 'status'); + const json = { + id: watchId, + watchStatusJson, + }; + + const watchStatus = WatchStatus.fromUpstreamJson(json); + return response.ok({ + body: { + watchStatus: watchStatus.downstreamJson, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts index 08d99f42df0540..3171d8ee2e1e55 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts @@ -3,63 +3,61 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; // @ts-ignore import { WatchStatus } from '../../../models/watch_status/index'; -function deactivateWatch(callWithRequest: any, watchId: string) { - return callWithRequest('watcher.deactivateWatch', { +const paramsSchema = schema.object({ + watchId: schema.string(), +}); + +function deactivateWatch(dataClient: IScopedClusterClient, watchId: string) { + return dataClient.callAsCurrentUser('watcher.deactivateWatch', { id: watchId, }); } export function registerDeactivateRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - const { watchId } = request.params; - - try { - const hit = await deactivateWatch(callWithRequest, watchId); - const watchStatusJson = get(hit, 'status'); - const json = { - id: watchId, - watchStatusJson, - }; - - const watchStatus = WatchStatus.fromUpstreamJson(json); - return response.ok({ - body: { - watchStatus: watchStatus.downstreamJson, - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; - return response.customError({ statusCode: e.statusCode, body }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.put( { path: '/api/watcher/watch/{watchId}/deactivate', validate: { - params: schema.object({ - watchId: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { watchId } = request.params; + + try { + const hit = await deactivateWatch(ctx.watcher!.client, watchId); + const watchStatusJson = get(hit, 'status'); + const json = { + id: watchId, + watchStatusJson, + }; + + const watchStatus = WatchStatus.fromUpstreamJson(json); + return response.ok({ + body: { + watchStatus: watchStatus.downstreamJson, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts index 6e95cf959bc9ca..bfdf328550bbea 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts @@ -5,49 +5,46 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; -function deleteWatch(callWithRequest: any, watchId: string) { - return callWithRequest('watcher.deleteWatch', { +const paramsSchema = schema.object({ + watchId: schema.string(), +}); + +function deleteWatch(dataClient: IScopedClusterClient, watchId: string) { + return dataClient.callAsCurrentUser('watcher.deleteWatch', { id: watchId, }); } export function registerDeleteRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - const { watchId } = request.params; - - try { - return response.ok({ - body: await deleteWatch(callWithRequest, watchId), - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; - return response.customError({ statusCode: e.statusCode, body }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.delete( { path: '/api/watcher/watch/{watchId}', validate: { - params: schema.object({ - watchId: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { watchId } = request.params; + + try { + return response.ok({ + body: await deleteWatch(ctx.watcher!.client, watchId), + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts index fef6d07317da5c..7aaa77c05a5f06 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts @@ -5,9 +5,8 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; @@ -19,60 +18,63 @@ import { Watch } from '../../../models/watch/index'; // @ts-ignore import { WatchHistoryItem } from '../../../models/watch_history_item/index'; -function executeWatch(callWithRequest: any, executeDetails: any, watchJson: any) { +const bodySchema = schema.object({ + executeDetails: schema.object({}, { allowUnknowns: true }), + watch: schema.object({}, { allowUnknowns: true }), +}); + +function executeWatch(dataClient: IScopedClusterClient, executeDetails: any, watchJson: any) { const body = executeDetails; body.watch = watchJson; - return callWithRequest('watcher.executeWatch', { + return dataClient.callAsCurrentUser('watcher.executeWatch', { body, }); } export function registerExecuteRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const executeDetails = ExecuteDetails.fromDownstreamJson(request.body.executeDetails); - const watch = Watch.fromDownstreamJson(request.body.watch); - - try { - const hit = await executeWatch(callWithRequest, executeDetails.upstreamJson, watch.watchJson); - const id = get(hit, '_id'); - const watchHistoryItemJson = get(hit, 'watch_record'); - const watchId = get(hit, 'watch_record.watch_id'); - const json = { - id, - watchId, - watchHistoryItemJson, - includeDetails: true, - }; - - const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); - return response.ok({ - body: { - watchHistoryItem: watchHistoryItem.downstreamJson, - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.put( { path: '/api/watcher/watch/execute', validate: { - body: schema.object({ - executeDetails: schema.object({}, { allowUnknowns: true }), - watch: schema.object({}, { allowUnknowns: true }), - }), + body: bodySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const executeDetails = ExecuteDetails.fromDownstreamJson(request.body.executeDetails); + const watch = Watch.fromDownstreamJson(request.body.watch); + + try { + const hit = await executeWatch( + ctx.watcher!.client, + executeDetails.upstreamJson, + watch.watchJson + ); + const id = get(hit, '_id'); + const watchHistoryItemJson = get(hit, 'watch_record'); + const watchId = get(hit, 'watch_record.watch_id'); + const json = { + id, + watchId, + watchHistoryItemJson, + includeDetails: true, + }; + + const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); + return response.ok({ + body: { + watchHistoryItem: watchHistoryItem.downstreamJson, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts index 7f0f1ac8d66a31..b64c28e114b72c 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts @@ -5,9 +5,8 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll'; import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../common/constants'; import { isEsError } from '../../../lib/is_es_error'; @@ -16,7 +15,15 @@ import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_facto // @ts-ignore import { WatchHistoryItem } from '../../../models/watch_history_item/index'; -function fetchHistoryItems(callWithRequest: any, watchId: any, startTime: any) { +const paramsSchema = schema.object({ + watchId: schema.string(), +}); + +const querySchema = schema.object({ + startTime: schema.string(), +}); + +function fetchHistoryItems(dataClient: IScopedClusterClient, watchId: any, startTime: any) { const params: any = { index: INDEX_NAMES.WATCHER_HISTORY, scroll: ES_SCROLL_SETTINGS.KEEPALIVE, @@ -37,61 +44,57 @@ function fetchHistoryItems(callWithRequest: any, watchId: any, startTime: any) { params.body.query.bool.must.push(timeRangeQuery); } - return callWithRequest('search', params).then((response: any) => - fetchAllFromScroll(response, callWithRequest) - ); + return dataClient + .callAsCurrentUser('search', params) + .then((response: any) => fetchAllFromScroll(response, dataClient)); } export function registerHistoryRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const { watchId } = request.params; - const { startTime } = request.query; - - try { - const hits = await fetchHistoryItems(callWithRequest, watchId, startTime); - const watchHistoryItems = hits.map((hit: any) => { - const id = get(hit, '_id'); - const watchHistoryItemJson = get(hit, '_source'); - - const opts = { includeDetails: false }; - return WatchHistoryItem.fromUpstreamJson( - { - id, - watchId, - watchHistoryItemJson, - }, - opts - ); - }); - - return response.ok({ - body: { - watchHistoryItems: watchHistoryItems.map( - (watchHistoryItem: any) => watchHistoryItem.downstreamJson - ), - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.get( { path: '/api/watcher/watch/{watchId}/history', validate: { - params: schema.object({ - watchId: schema.string(), - }), + params: paramsSchema, + query: querySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { watchId } = request.params; + const { startTime } = request.query; + + try { + const hits = await fetchHistoryItems(ctx.watcher!.client, watchId, startTime); + const watchHistoryItems = hits.map((hit: any) => { + const id = get(hit, '_id'); + const watchHistoryItemJson = get(hit, '_source'); + + const opts = { includeDetails: false }; + return WatchHistoryItem.fromUpstreamJson( + { + id, + watchId, + watchHistoryItemJson, + }, + opts + ); + }); + + return response.ok({ + body: { + watchHistoryItems: watchHistoryItems.map( + (watchHistoryItem: any) => watchHistoryItem.downstreamJson + ), + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts index 91d71cd737121f..6363054921333f 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts @@ -5,65 +5,63 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; // @ts-ignore import { Watch } from '../../../models/watch/index'; import { RouteDependencies } from '../../../types'; -function fetchWatch(callWithRequest: any, watchId: string) { - return callWithRequest('watcher.getWatch', { +const paramsSchema = schema.object({ + id: schema.string(), +}); + +function fetchWatch(dataClient: IScopedClusterClient, watchId: string) { + return dataClient.callAsCurrentUser('watcher.getWatch', { id: watchId, }); } export function registerLoadRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - const id = request.params.id; - - try { - const hit = await fetchWatch(callWithRequest, id); - const watchJson = get(hit, 'watch'); - const watchStatusJson = get(hit, 'status'); - const json = { - id, - watchJson, - watchStatusJson, - }; - - const watch = Watch.fromUpstreamJson(json, { - throwExceptions: { - Action: false, - }, - }); - return response.ok({ - body: { watch: watch.downstreamJson }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - const body = e.statusCode === 404 ? `Watch with id = ${id} not found` : e; - return response.customError({ statusCode: e.statusCode, body }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; deps.router.get( { path: '/api/watcher/watch/{id}', validate: { - params: schema.object({ - id: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const id = request.params.id; + + try { + const hit = await fetchWatch(ctx.watcher!.client, id); + const watchJson = get(hit, 'watch'); + const watchStatusJson = get(hit, 'status'); + const json = { + id, + watchJson, + watchStatusJson, + }; + + const watch = Watch.fromUpstreamJson(json, { + throwExceptions: { + Action: false, + }, + }); + return response.ok({ + body: { watch: watch.downstreamJson }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${id} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts index 7986424e6229a7..572790f12a5f8a 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts @@ -5,98 +5,104 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { i18n } from '@kbn/i18n'; import { WATCH_TYPES } from '../../../../common/constants'; import { serializeJsonWatch, serializeThresholdWatch } from '../../../../common/lib/serialization'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; -function fetchWatch(callWithRequest: any, watchId: string) { - return callWithRequest('watcher.getWatch', { +const paramsSchema = schema.object({ + id: schema.string(), +}); + +const bodySchema = schema.object( + { + type: schema.string(), + isNew: schema.boolean(), + }, + { allowUnknowns: true } +); + +function fetchWatch(dataClient: IScopedClusterClient, watchId: string) { + return dataClient.callAsCurrentUser('watcher.getWatch', { id: watchId, }); } -function saveWatch(callWithRequest: any, id: string, body: any) { - return callWithRequest('watcher.putWatch', { +function saveWatch(dataClient: IScopedClusterClient, id: string, body: any) { + return dataClient.callAsCurrentUser('watcher.putWatch', { id, body, }); } export function registerSaveRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const { id } = request.params; - const { type, isNew, ...watchConfig } = request.body; + deps.router.put( + { + path: '/api/watcher/watch/{id}', + validate: { + params: paramsSchema, + body: bodySchema, + }, + }, + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { id } = request.params; + const { type, isNew, ...watchConfig } = request.body; - // For new watches, verify watch with the same ID doesn't already exist - if (isNew) { - try { - const existingWatch = await fetchWatch(callWithRequest, id); - if (existingWatch.found) { - return response.conflict({ - body: { - message: i18n.translate('xpack.watcher.saveRoute.duplicateWatchIdErrorMessage', { - defaultMessage: "There is already a watch with ID '{watchId}'.", - values: { - watchId: id, - }, - }), - }, - }); - } - } catch (e) { - const es404 = isEsError(e) && e.statusCode === 404; - if (!es404) { - return response.internalError({ body: e }); + // For new watches, verify watch with the same ID doesn't already exist + if (isNew) { + try { + const existingWatch = await fetchWatch(ctx.watcher!.client, id); + if (existingWatch.found) { + return response.conflict({ + body: { + message: i18n.translate('xpack.watcher.saveRoute.duplicateWatchIdErrorMessage', { + defaultMessage: "There is already a watch with ID '{watchId}'.", + values: { + watchId: id, + }, + }), + }, + }); + } + } catch (e) { + const es404 = isEsError(e) && e.statusCode === 404; + if (!es404) { + return response.internalError({ body: e }); + } + // Else continue... } - // Else continue... } - } - let serializedWatch; + let serializedWatch; - switch (type) { - case WATCH_TYPES.JSON: - const { name, watch } = watchConfig; - serializedWatch = serializeJsonWatch(name, watch); - break; + switch (type) { + case WATCH_TYPES.JSON: + const { name, watch } = watchConfig as any; + serializedWatch = serializeJsonWatch(name, watch); + break; - case WATCH_TYPES.THRESHOLD: - serializedWatch = serializeThresholdWatch(watchConfig); - break; - } - - try { - // Create new watch - return response.ok({ - body: await saveWatch(callWithRequest, id, serializedWatch), - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); + case WATCH_TYPES.THRESHOLD: + serializedWatch = serializeThresholdWatch(watchConfig); + break; } - // Case: default - return response.internalError({ body: e }); - } - }; + try { + // Create new watch + return response.ok({ + body: await saveWatch(ctx.watcher!.client, id, serializedWatch), + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } - deps.router.put( - { - path: '/api/watcher/watch/{id}', - validate: { - params: schema.object({ - id: schema.string(), - }), - body: schema.object({}, { allowUnknowns: true }), - }, - }, - licensePreRoutingFactory(deps, handler) + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts index f2110bcc0ebdbc..200b35953b6f23 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts @@ -5,8 +5,7 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; @@ -16,7 +15,12 @@ import { Watch } from '../../../models/watch/index'; // @ts-ignore import { VisualizeOptions } from '../../../models/visualize_options/index'; -function fetchVisualizeData(callWithRequest: any, index: any, body: any) { +const bodySchema = schema.object({ + watch: schema.object({}, { allowUnknowns: true }), + options: schema.object({}, { allowUnknowns: true }), +}); + +function fetchVisualizeData(dataClient: IScopedClusterClient, index: any, body: any) { const params = { index, body, @@ -25,46 +29,40 @@ function fetchVisualizeData(callWithRequest: any, index: any, body: any) { ignore: [404], }; - return callWithRequest('search', params); + return dataClient.callAsCurrentUser('search', params); } export function registerVisualizeRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const watch = Watch.fromDownstreamJson(request.body.watch); - const options = VisualizeOptions.fromDownstreamJson(request.body.options); - const body = watch.getVisualizeQuery(options); - - try { - const hits = await fetchVisualizeData(callWithRequest, watch.index, body); - const visualizeData = watch.formatVisualizeData(hits); - - return response.ok({ - body: { - visualizeData, - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.post( { path: '/api/watcher/watch/visualize', validate: { - body: schema.object({ - watch: schema.object({}, { allowUnknowns: true }), - options: schema.object({}, { allowUnknowns: true }), - }), + body: bodySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const watch = Watch.fromDownstreamJson(request.body.watch); + const options = VisualizeOptions.fromDownstreamJson(request.body.options); + const body = watch.getVisualizeQuery(options); + + try { + const hits = await fetchVisualizeData(ctx.watcher!.client, watch.index, body); + const visualizeData = watch.formatVisualizeData(hits); + + return response.ok({ + body: { + visualizeData, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts b/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts index 2ac824529f9a6d..71e0a77bff9729 100644 --- a/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts @@ -5,16 +5,20 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; -function deleteWatches(callWithRequest: any, watchIds: string[]) { +const bodySchema = schema.object({ + watchIds: schema.arrayOf(schema.string()), +}); + +function deleteWatches(dataClient: IScopedClusterClient, watchIds: string[]) { const deletePromises = watchIds.map(watchId => { - return callWithRequest('watcher.deleteWatch', { - id: watchId, - }) + return dataClient + .callAsCurrentUser('watcher.deleteWatch', { + id: watchId, + }) .then((success: Array<{ _id: string }>) => ({ success })) .catch((error: Array<{ _id: string }>) => ({ error })); }); @@ -22,7 +26,7 @@ function deleteWatches(callWithRequest: any, watchIds: string[]) { return Promise.all(deletePromises).then(results => { const errors: Error[] = []; const successes: boolean[] = []; - results.forEach(({ success, error }) => { + results.forEach(({ success, error }: { success?: any; error?: any }) => { if (success) { successes.push(success._id); } else if (error) { @@ -38,26 +42,20 @@ function deleteWatches(callWithRequest: any, watchIds: string[]) { } export function registerDeleteRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - try { - const results = await deleteWatches(callWithRequest, request.body.watchIds); - return response.ok({ body: { results } }); - } catch (e) { - return response.internalError({ body: e }); - } - }; - deps.router.post( { path: '/api/watcher/watches/delete', validate: { - body: schema.object({ - watchIds: schema.arrayOf(schema.string()), - }), + body: bodySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + try { + const results = await deleteWatches(ctx.watcher!.client, request.body.watchIds); + return response.ok({ body: { results } }); + } catch (e) { + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts b/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts index fcbdf688a2ab40..5e823a0a8d2de6 100644 --- a/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll'; import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../common/constants'; import { isEsError } from '../../../lib/is_es_error'; @@ -15,7 +14,7 @@ import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_facto // @ts-ignore import { Watch } from '../../../models/watch/index'; -function fetchWatches(callWithRequest: any) { +function fetchWatches(dataClient: IScopedClusterClient) { const params = { index: INDEX_NAMES.WATCHES, scroll: ES_SCROLL_SETTINGS.KEEPALIVE, @@ -25,62 +24,58 @@ function fetchWatches(callWithRequest: any) { ignore: [404], }; - return callWithRequest('search', params).then((response: any) => - fetchAllFromScroll(response, callWithRequest) - ); + return dataClient + .callAsCurrentUser('search', params) + .then((response: any) => fetchAllFromScroll(response, dataClient)); } export function registerListRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - try { - const hits = await fetchWatches(callWithRequest); - const watches = hits.map((hit: any) => { - const id = get(hit, '_id'); - const watchJson = get(hit, '_source'); - const watchStatusJson = get(hit, '_source.status'); + deps.router.get( + { + path: '/api/watcher/watches', + validate: false, + }, + licensePreRoutingFactory(deps, async (ctx, request, response) => { + try { + const hits = await fetchWatches(ctx.watcher!.client); + const watches = hits.map((hit: any) => { + const id = get(hit, '_id'); + const watchJson = get(hit, '_source'); + const watchStatusJson = get(hit, '_source.status'); - return Watch.fromUpstreamJson( - { - id, - watchJson, - watchStatusJson, - }, - { - throwExceptions: { - Action: false, + return Watch.fromUpstreamJson( + { + id, + watchJson, + watchStatusJson, }, - } - ); - }); + { + throwExceptions: { + Action: false, + }, + } + ); + }); - return response.ok({ - body: { - watches: watches.map((watch: any) => watch.downstreamJson), - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ - statusCode: e.statusCode, + return response.ok({ body: { - message: e.message, + watches: watches.map((watch: any) => watch.downstreamJson), }, }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: { + message: e.message, + }, + }); + } - deps.router.get( - { - path: '/api/watcher/watches', - validate: false, - }, - licensePreRoutingFactory(deps, handler) + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/types.ts b/x-pack/plugins/watcher/server/types.ts index d9f2d3c3b1e7a6..dd941054114a84 100644 --- a/x-pack/plugins/watcher/server/types.ts +++ b/x-pack/plugins/watcher/server/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter, ElasticsearchServiceSetup, IClusterClient } from 'kibana/server'; +import { IRouter } from 'kibana/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { XPackMainPlugin } from '../../../legacy/plugins/xpack_main/server/xpack_main'; @@ -24,8 +24,6 @@ export interface ServerShim { export interface RouteDependencies { router: IRouter; getLicenseStatus: () => LicenseStatus; - elasticsearchService: ElasticsearchServiceSetup; - elasticsearch: IClusterClient; } export interface LicenseStatus { From 16eb81628f7a56af2e826cf97d8a7e2db77910fb Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Thu, 20 Feb 2020 13:31:04 +0100 Subject: [PATCH 04/43] [ML] Transform functional tests - bootstrap transform job for clone test (#57992) This PR adds transform bootstrapping to the functional test transform.api service and demonstrates the usage in a new cloning test file, which will be completed as part of the transform cloning PR. --- .../test/functional/apps/transform/cloning.ts | 65 +++++++++++++ .../test/functional/apps/transform/index.ts | 1 + .../functional/services/transform_ui/api.ts | 91 +++++++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 x-pack/test/functional/apps/transform/cloning.ts diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts new file mode 100644 index 00000000000000..f06dc0a14a383d --- /dev/null +++ b/x-pack/test/functional/apps/transform/cloning.ts @@ -0,0 +1,65 @@ +/* + * 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'; +import { TransformPivotConfig } from '../../../../legacy/plugins/transform/public/app/common'; + +function getTransformConfig(): TransformPivotConfig { + const date = Date.now(); + return { + id: `ec_2_${date}`, + source: { index: ['ecommerce'] }, + pivot: { + group_by: { category: { terms: { field: 'category.keyword' } } }, + aggregations: { 'products.base_price.avg': { avg: { field: 'products.base_price' } } }, + }, + description: + 'ecommerce batch transform with avg(products.base_price) grouped by terms(category.keyword)', + dest: { index: `user-ec_2_${date}` }, + }; +} + +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('cloning', function() { + this.tags(['smoke']); + const transformConfig = getTransformConfig(); + + before(async () => { + await esArchiver.load('ml/ecommerce'); + await transform.api.createAndRunTransform(transformConfig); + await transform.securityUI.loginAsTransformPowerUser(); + }); + + after(async () => { + await esArchiver.unload('ml/ecommerce'); + await transform.api.deleteIndices(transformConfig.dest.index); + await transform.api.cleanTransformIndices(); + }); + + const testDataList = [ + { + suiteTitle: 'batch transform with terms group and avg agg', + expected: {}, + }, + ]; + + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function() { + after(async () => { + // await transform.api.deleteIndices(); + }); + + it('loads the home page', async () => { + await transform.navigation.navigateTo(); + await transform.management.assertTransformListPageExists(); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 66a55105b3ca8e..60b72f122f1131 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -23,5 +23,6 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./creation_index_pattern')); loadTestFile(require.resolve('./creation_saved_search')); + loadTestFile(require.resolve('./cloning')); }); } diff --git a/x-pack/test/functional/services/transform_ui/api.ts b/x-pack/test/functional/services/transform_ui/api.ts index a6756e5940d72b..6a4a1dfff6ea16 100644 --- a/x-pack/test/functional/services/transform_ui/api.ts +++ b/x-pack/test/functional/services/transform_ui/api.ts @@ -7,10 +7,17 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { + TRANSFORM_STATE, + TransformPivotConfig, + TransformStats, +} from '../../../../legacy/plugins/transform/public/app/common'; + export function TransformAPIProvider({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const log = getService('log'); const retry = getService('retry'); + const esSupertest = getService('esSupertest'); return { async deleteIndices(indices: string) { @@ -39,5 +46,89 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { async cleanTransformIndices() { await this.deleteIndices('.transform-*'); }, + + async getTransformStats(transformId: string): Promise { + log.debug(`Fetching transform stats for transform ${transformId}`); + const statsResponse = await esSupertest + .get(`/_transform/${transformId}/_stats`) + .expect(200) + .then((res: any) => res.body); + + expect(statsResponse.transforms).to.have.length(1); + return statsResponse.transforms[0]; + }, + + async getTransformState(transformId: string): Promise { + const stats = await this.getTransformStats(transformId); + const state: TRANSFORM_STATE = stats.state; + + return state; + }, + + async waitForTransformState(transformId: string, expectedState: TRANSFORM_STATE) { + await retry.waitForWithTimeout( + `transform state to be ${expectedState}`, + 2 * 60 * 1000, + async () => { + const state = await this.getTransformState(transformId); + if (state === expectedState) { + return true; + } else { + throw new Error(`expected transform state to be ${expectedState} but got ${state}`); + } + } + ); + }, + + async waitForBatchTransformToComplete(transformId: string) { + await retry.waitForWithTimeout(`batch transform to complete`, 2 * 60 * 1000, async () => { + const stats = await this.getTransformStats(transformId); + if (stats.state === TRANSFORM_STATE.STOPPED && stats.checkpointing.last.checkpoint === 1) { + return true; + } else { + throw new Error( + `expected batch transform to be stopped with last checkpoint = 1 (got status: '${stats.state}', checkpoint: '${stats.checkpointing.last.checkpoint}')` + ); + } + }); + }, + + async getTransform(transformId: string) { + return await esSupertest.get(`/_transform/${transformId}`).expect(200); + }, + + async createTransform(transformConfig: TransformPivotConfig) { + const transformId = transformConfig.id; + log.debug(`Creating transform with id '${transformId}'...`); + await esSupertest + .put(`/_transform/${transformId}`) + .send(transformConfig) + .expect(200); + + await retry.waitForWithTimeout(`'${transformId}' to be created`, 5 * 1000, async () => { + if (await this.getTransform(transformId)) { + return true; + } else { + throw new Error(`expected transform '${transformId}' to be created`); + } + }); + }, + + async startTransform(transformId: string) { + log.debug(`Starting transform '${transformId}' ...`); + await esSupertest.post(`/_transform/${transformId}/_start`).expect(200); + }, + + async createAndRunTransform(transformConfig: TransformPivotConfig) { + await this.createTransform(transformConfig); + await this.startTransform(transformConfig.id); + if (transformConfig.sync === undefined) { + // batch mode + await this.waitForBatchTransformToComplete(transformConfig.id); + } else { + // continuous mode + await this.waitForTransformState(transformConfig.id, TRANSFORM_STATE.STARTED); + } + }, }; } From ed2ca68d796fbb99d219336cd3dddc6ac8d71ba7 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Thu, 20 Feb 2020 15:40:00 +0300 Subject: [PATCH 05/43] [ui/agg_response/tabify] -> TypeScript & Jest & Shim (#57660) * [ui/agg_response/tabify] -> TypeScript & Jest & Shim [ui/agg_response/tabify] -> TypeScript & Jest & Shim Part of #57394 * fix CI * move tabify into new folder * TypeScript _bucket.js * rename _buckets -> bucket * fix CI * tabify.test.js -> tabify.test.ts * tabify.js -> tabify.ts * fix JEST * Update src/legacy/core_plugins/data/public/search/tabify/types.ts Co-Authored-By: Luke Elmers * fake_hierarchical_data.js -> fake_hierarchical_data.ts * TimeRange -> TabbedRangeFilterParams Co-authored-by: Elastic Machine Co-authored-by: Luke Elmers --- ...ical_data.js => fake_hierarchical_data.ts} | 16 +- src/legacy/core_plugins/data/public/index.ts | 2 + .../data/public/search/aggs/agg_configs.ts | 6 +- .../data/public/search/aggs/agg_params.ts | 4 +- .../data/public/search/aggs/index.ts | 3 + .../data/public/search/expressions/esaggs.ts | 3 +- .../core_plugins/data/public/search/index.ts | 1 + .../public/search/tabify/buckets.test.ts} | 112 +++++----- .../data/public/search/tabify/buckets.ts | 135 ++++++++++++ .../public/search/tabify/get_columns.test.ts | 191 +++++++++++++++++ .../data/public/search/tabify/get_columns.ts} | 4 +- .../data/public/search/tabify/index.ts} | 1 + .../search/tabify/response_writer.test.ts | 170 +++++++++++++++ .../public/search/tabify/response_writer.ts | 88 ++++++++ .../data/public/search/tabify/tabify.test.ts | 172 +++++++++++++++ .../data/public/search/tabify/tabify.ts | 173 +++++++++++++++ .../data/public/search/tabify/types.ts} | 18 +- .../data/public/search/utils/types.ts | 6 + .../kibana/public/discover/kibana_services.ts | 3 +- .../public/agg_table/__tests__/agg_table.js | 12 +- .../agg_table/__tests__/agg_table_group.js | 6 +- .../vis_type_table/public/legacy_imports.ts | 4 +- .../vis_type_vislib/public/legacy_imports.ts | 4 +- .../__tests__/visualizations/pie_chart.js | 6 +- src/legacy/ui/public/agg_response/index.js | 2 +- .../tabify/__tests__/_get_columns.js | 199 ------------------ .../tabify/__tests__/_integration.js | 175 --------------- .../tabify/__tests__/_response_writer.js | 186 ---------------- .../ui/public/agg_response/tabify/_buckets.js | 123 ----------- .../agg_response/tabify/_response_writer.js | 97 --------- .../ui/public/agg_response/tabify/tabify.js | 134 ------------ .../es_geo_grid_source/es_geo_grid_source.js | 2 +- 32 files changed, 1053 insertions(+), 1005 deletions(-) rename src/fixtures/{fake_hierarchical_data.js => fake_hierarchical_data.ts} (98%) rename src/legacy/{ui/public/agg_response/tabify/__tests__/_buckets.js => core_plugins/data/public/search/tabify/buckets.test.ts} (66%) create mode 100644 src/legacy/core_plugins/data/public/search/tabify/buckets.ts create mode 100644 src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts rename src/legacy/{ui/public/agg_response/tabify/_get_columns.ts => core_plugins/data/public/search/tabify/get_columns.ts} (96%) rename src/legacy/{ui/public/agg_response/tabify/index.js => core_plugins/data/public/search/tabify/index.ts} (94%) create mode 100644 src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts create mode 100644 src/legacy/core_plugins/data/public/search/tabify/response_writer.ts create mode 100644 src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts create mode 100644 src/legacy/core_plugins/data/public/search/tabify/tabify.ts rename src/legacy/{ui/public/agg_response/tabify/__tests__/tabify.js => core_plugins/data/public/search/tabify/types.ts} (69%) delete mode 100644 src/legacy/ui/public/agg_response/tabify/__tests__/_get_columns.js delete mode 100644 src/legacy/ui/public/agg_response/tabify/__tests__/_integration.js delete mode 100644 src/legacy/ui/public/agg_response/tabify/__tests__/_response_writer.js delete mode 100644 src/legacy/ui/public/agg_response/tabify/_buckets.js delete mode 100644 src/legacy/ui/public/agg_response/tabify/_response_writer.js delete mode 100644 src/legacy/ui/public/agg_response/tabify/tabify.js diff --git a/src/fixtures/fake_hierarchical_data.js b/src/fixtures/fake_hierarchical_data.ts similarity index 98% rename from src/fixtures/fake_hierarchical_data.js rename to src/fixtures/fake_hierarchical_data.ts index b4ae02a487049f..4480caae39664a 100644 --- a/src/fixtures/fake_hierarchical_data.js +++ b/src/fixtures/fake_hierarchical_data.ts @@ -17,16 +17,14 @@ * under the License. */ -const data = {}; - -data.metricOnly = { +export const metricOnly = { hits: { total: 1000, hits: [], max_score: 0 }, aggregations: { agg_1: { value: 412032 }, }, }; -data.threeTermBuckets = { +export const threeTermBuckets = { hits: { total: 1000, hits: [], max_score: 0 }, aggregations: { agg_2: { @@ -129,7 +127,7 @@ data.threeTermBuckets = { }, }; -data.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = { +export const oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = { hits: { total: 1000, hits: [], max_score: 0 }, aggregations: { agg_3: { @@ -520,7 +518,7 @@ data.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = { }, }; -data.oneRangeBucket = { +export const oneRangeBucket = { took: 35, timed_out: false, _shards: { @@ -555,7 +553,7 @@ data.oneRangeBucket = { }, }; -data.oneFilterBucket = { +export const oneFilterBucket = { took: 11, timed_out: false, _shards: { @@ -582,7 +580,7 @@ data.oneFilterBucket = { }, }; -data.oneHistogramBucket = { +export const oneHistogramBucket = { took: 37, timed_out: false, _shards: { @@ -632,5 +630,3 @@ data.oneHistogramBucket = { }, }, }; - -export default data; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 50120292a627a6..ce46f534141f4f 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -81,4 +81,6 @@ export { // search_source getRequestInspectorStats, getResponseInspectorStats, + tabifyAggResponse, + tabifyGetColumns, } from './search'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts index 7e7e4944b00da9..8e091ed5f21ae0 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts @@ -27,7 +27,7 @@ */ import _ from 'lodash'; -import { AggConfig, AggConfigOptions } from './agg_config'; +import { AggConfig, AggConfigOptions, IAggConfig } from './agg_config'; import { Schema } from './schemas'; import { AggGroupNames } from './agg_groups'; import { @@ -63,7 +63,7 @@ export class AggConfigs { public schemas: any; public timeRange?: TimeRange; - aggs: AggConfig[]; + aggs: IAggConfig[]; constructor(indexPattern: IndexPattern, configStates = [] as any, schemas?: any) { configStates = AggConfig.ensureIds(configStates); @@ -74,7 +74,7 @@ export class AggConfigs { configStates.forEach((params: any) => this.createAggConfig(params)); - if (this.schemas) { + if (schemas) { this.initializeDefaultsFromSchemas(schemas); } } diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_params.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_params.ts index 34727ff4614b95..551cb81529a0a3 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_params.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_params.ts @@ -76,7 +76,9 @@ export const writeParams = < aggs?: IAggConfigs, locals?: Record ) => { - const output = { params: {} as Record }; + const output: Record = { + params: {} as Record, + }; locals = locals || {}; params.forEach(param => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/index.ts b/src/legacy/core_plugins/data/public/search/aggs/index.ts index 0fef7f38aae742..0bdb92b8de65e8 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/index.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/index.ts @@ -50,3 +50,6 @@ export { isValidJson, isValidInterval } from './utils'; export { BUCKET_TYPES } from './buckets/bucket_agg_types'; export { METRIC_TYPES } from './metrics/metric_agg_types'; export { ISchemas, Schema, Schemas } from './schemas'; + +// types +export { IAggConfig, IAggConfigs } from './types'; diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index 9aee7124c95211..302527e4ed549f 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -39,8 +39,7 @@ import { import { buildTabularInspectorData } from './build_tabular_inspector_data'; import { calculateObjectHash } from '../../../../visualizations/public'; -// @ts-ignore -import { tabifyAggResponse } from '../../../../../ui/public/agg_response/tabify/tabify'; +import { tabifyAggResponse } from '../../../../../core_plugins/data/public'; import { PersistedState } from '../../../../../ui/public/persisted_state'; import { Adapters } from '../../../../../../plugins/inspector/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths diff --git a/src/legacy/core_plugins/data/public/search/index.ts b/src/legacy/core_plugins/data/public/search/index.ts index 90e191b769a8df..96d2825559da26 100644 --- a/src/legacy/core_plugins/data/public/search/index.ts +++ b/src/legacy/core_plugins/data/public/search/index.ts @@ -20,3 +20,4 @@ export * from './aggs'; export { getRequestInspectorStats, getResponseInspectorStats } from './utils'; export { serializeAggConfig } from './expressions/utils'; +export { tabifyAggResponse, tabifyGetColumns } from './tabify'; diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/_buckets.js b/src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts similarity index 66% rename from src/legacy/ui/public/agg_response/tabify/__tests__/_buckets.js rename to src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts index b85b45d3c5820a..ef2748102623ab 100644 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/_buckets.js +++ b/src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts @@ -17,31 +17,36 @@ * under the License. */ -import expect from '@kbn/expect'; -import { TabifyBuckets } from '../_buckets'; +import { TabifyBuckets } from './buckets'; +import { AggGroupNames } from '../aggs'; -describe('Buckets wrapper', function() { - function test(aggResp, count, keys) { - it('reads the length', function() { +jest.mock('ui/new_platform'); + +describe('Buckets wrapper', () => { + const check = (aggResp: any, count: number, keys: string[]) => { + test('reads the length', () => { const buckets = new TabifyBuckets(aggResp); - expect(buckets).to.have.length(count); + expect(buckets).toHaveLength(count); }); - it('iterates properly, passing in the key', function() { + test('iterates properly, passing in the key', () => { const buckets = new TabifyBuckets(aggResp); - const keysSent = []; - buckets.forEach(function(bucket, key) { - keysSent.push(key); + const keysSent: any[] = []; + + buckets.forEach((bucket, key) => { + if (key) { + keysSent.push(key); + } }); - expect(keysSent).to.have.length(count); - expect(keysSent).to.eql(keys); + expect(keysSent).toHaveLength(count); + expect(keysSent).toEqual(keys); }); - } + }; - describe('with object style buckets', function() { - const aggResp = { - buckets: { + describe('with object style buckets', () => { + let aggResp: any = { + [AggGroupNames.Buckets]: { '0-100': {}, '100-200': {}, '200-300': {}, @@ -51,11 +56,11 @@ describe('Buckets wrapper', function() { const count = 3; const keys = ['0-100', '100-200', '200-300']; - test(aggResp, count, keys); + check(aggResp, count, keys); - it('should accept filters agg queries with strings', () => { - const aggResp = { - buckets: { + test('should accept filters agg queries with strings', () => { + aggResp = { + [AggGroupNames.Buckets]: { 'response:200': {}, 'response:404': {}, }, @@ -75,15 +80,17 @@ describe('Buckets wrapper', function() { }; const buckets = new TabifyBuckets(aggResp, aggParams); - expect(buckets).to.have.length(2); + + expect(buckets).toHaveLength(2); + buckets._keys.forEach(key => { - expect(key).to.be.a('string'); + expect(typeof key).toBe('string'); }); }); - it('should accept filters agg queries with query_string queries', () => { - const aggResp = { - buckets: { + test('should accept filters agg queries with query_string queries', () => { + aggResp = { + [AggGroupNames.Buckets]: { 'response:200': {}, 'response:404': {}, }, @@ -103,15 +110,17 @@ describe('Buckets wrapper', function() { }; const buckets = new TabifyBuckets(aggResp, aggParams); - expect(buckets).to.have.length(2); + + expect(buckets).toHaveLength(2); + buckets._keys.forEach(key => { - expect(key).to.be.a('string'); + expect(typeof key).toBe('string'); }); }); - it('should accept filters agg queries with query dsl queries', () => { - const aggResp = { - buckets: { + test('should accept filters agg queries with query dsl queries', () => { + aggResp = { + [AggGroupNames.Buckets]: { '{match_all: {}}': {}, }, }; @@ -126,16 +135,18 @@ describe('Buckets wrapper', function() { }; const buckets = new TabifyBuckets(aggResp, aggParams); - expect(buckets).to.have.length(1); + + expect(buckets).toHaveLength(1); + buckets._keys.forEach(key => { - expect(key).to.be.a('string'); + expect(typeof key).toBe('string'); }); }); }); - describe('with array style buckets', function() { + describe('with array style buckets', () => { const aggResp = { - buckets: [ + [AggGroupNames.Buckets]: [ { key: '0-100', value: {} }, { key: '100-200', value: {} }, { key: '200-300', value: {} }, @@ -145,23 +156,24 @@ describe('Buckets wrapper', function() { const count = 3; const keys = ['0-100', '100-200', '200-300']; - test(aggResp, count, keys); + check(aggResp, count, keys); }); - describe('with single bucket aggregations (filter)', function() { - it('creates single bucket from agg content', function() { + describe('with single bucket aggregations (filter)', () => { + test('creates single bucket from agg content', () => { const aggResp = { single_bucket: {}, doc_count: 5, }; const buckets = new TabifyBuckets(aggResp); - expect(buckets).to.have.length(1); + + expect(buckets).toHaveLength(1); }); }); - describe('drop_partial option', function() { + describe('drop_partial option', () => { const aggResp = { - buckets: [ + [AggGroupNames.Buckets]: [ { key: 0, value: {} }, { key: 100, value: {} }, { key: 200, value: {} }, @@ -169,7 +181,7 @@ describe('Buckets wrapper', function() { ], }; - it('drops partial buckets when enabled', function() { + test('drops partial buckets when enabled', () => { const aggParams = { drop_partials: true, field: { @@ -182,10 +194,11 @@ describe('Buckets wrapper', function() { name: 'date', }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); - expect(buckets).to.have.length(1); + + expect(buckets).toHaveLength(1); }); - it('keeps partial buckets when disabled', function() { + test('keeps partial buckets when disabled', () => { const aggParams = { drop_partials: false, field: { @@ -198,10 +211,11 @@ describe('Buckets wrapper', function() { name: 'date', }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); - expect(buckets).to.have.length(4); + + expect(buckets).toHaveLength(4); }); - it('keeps aligned buckets when enabled', function() { + test('keeps aligned buckets when enabled', () => { const aggParams = { drop_partials: true, field: { @@ -214,10 +228,11 @@ describe('Buckets wrapper', function() { name: 'date', }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); - expect(buckets).to.have.length(3); + + expect(buckets).toHaveLength(3); }); - it('does not drop buckets for non-timerange fields', function() { + test('does not drop buckets for non-timerange fields', () => { const aggParams = { drop_partials: true, field: { @@ -230,7 +245,8 @@ describe('Buckets wrapper', function() { name: 'date', }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); - expect(buckets).to.have.length(4); + + expect(buckets).toHaveLength(4); }); }); }); diff --git a/src/legacy/core_plugins/data/public/search/tabify/buckets.ts b/src/legacy/core_plugins/data/public/search/tabify/buckets.ts new file mode 100644 index 00000000000000..8078136299f8c6 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/buckets.ts @@ -0,0 +1,135 @@ +/* + * 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 { get, isPlainObject, keys, findKey } from 'lodash'; +import moment from 'moment'; +import { IAggConfig } from '../aggs'; +import { TabbedRangeFilterParams } from './types'; +import { AggResponseBucket } from '../types'; + +type AggParams = IAggConfig['params'] & { + drop_partials: boolean; + ranges: TabbedRangeFilterParams[]; +}; + +const isRangeEqual = (range1: TabbedRangeFilterParams, range2: TabbedRangeFilterParams) => + range1?.from === range2?.from && range1?.to === range2?.to; + +export class TabifyBuckets { + length: number; + objectMode: boolean; + buckets: any; + _keys: any[] = []; + + constructor(aggResp: any, aggParams?: AggParams, timeRange?: TabbedRangeFilterParams) { + if (aggResp && aggResp.buckets) { + this.buckets = aggResp.buckets; + } else if (aggResp) { + // Some Bucket Aggs only return a single bucket (like filter). + // In those instances, the aggResp is the content of the single bucket. + this.buckets = [aggResp]; + } else { + this.buckets = []; + } + + this.objectMode = isPlainObject(this.buckets); + + if (this.objectMode) { + this._keys = keys(this.buckets); + this.length = this._keys.length; + } else { + this.length = this.buckets.length; + } + + if (this.length && aggParams) { + this.orderBucketsAccordingToParams(aggParams); + if (aggParams.drop_partials) { + this.dropPartials(aggParams, timeRange); + } + } + } + + forEach(fn: (bucket: any, key: any) => void) { + const buckets = this.buckets; + + if (this.objectMode) { + this._keys.forEach(key => { + fn(buckets[key], key); + }); + } else { + buckets.forEach((bucket: AggResponseBucket) => { + fn(bucket, bucket.key); + }); + } + } + + private orderBucketsAccordingToParams(params: AggParams) { + if (params.filters && this.objectMode) { + this._keys = params.filters.map((filter: any) => { + const query = get(filter, 'input.query.query_string.query', filter.input.query); + const queryString = typeof query === 'string' ? query : JSON.stringify(query); + + return filter.label || queryString || '*'; + }); + } else if (params.ranges && this.objectMode) { + this._keys = params.ranges.map((range: TabbedRangeFilterParams) => + findKey(this.buckets, (el: TabbedRangeFilterParams) => isRangeEqual(el, range)) + ); + } else if (params.ranges && params.field.type !== 'date') { + let ranges = params.ranges; + if (params.ipRangeType) { + ranges = params.ipRangeType === 'mask' ? ranges.mask : ranges.fromTo; + } + this.buckets = ranges.map((range: any) => { + if (range.mask) { + return this.buckets.find((el: AggResponseBucket) => el.key === range.mask); + } + + return this.buckets.find((el: TabbedRangeFilterParams) => isRangeEqual(el, range)); + }); + } + } + + // dropPartials should only be called if the aggParam setting is enabled, + // and the agg field is the same as the Time Range. + private dropPartials(params: AggParams, timeRange?: TabbedRangeFilterParams) { + if ( + !timeRange || + this.buckets.length <= 1 || + this.objectMode || + params.field.name !== timeRange.name + ) { + return; + } + + const interval = this.buckets[1].key - this.buckets[0].key; + + this.buckets = this.buckets.filter((bucket: AggResponseBucket) => { + if (moment(bucket.key).isBefore(timeRange.gte)) { + return false; + } + if (moment(bucket.key + interval).isAfter(timeRange.lte)) { + return false; + } + return true; + }); + + this.length = this.buckets.length; + } +} diff --git a/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts b/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts new file mode 100644 index 00000000000000..0328e87d8b8329 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts @@ -0,0 +1,191 @@ +/* + * 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 { tabifyGetColumns, AggColumn } from './get_columns'; +import { AggConfigs, AggGroupNames, Schemas } from '../aggs'; + +jest.mock('ui/new_platform'); + +describe('get columns', () => { + const createAggConfigs = (aggs: any[] = []) => { + const field = { + name: '@timestamp', + }; + + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + return new AggConfigs( + indexPattern, + aggs, + new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + min: 1, + defaults: [{ schema: 'metric', type: 'count' }], + }, + ]).all + ); + }; + + test('should inject a count metric if no aggs exist', () => { + const columns = tabifyGetColumns(createAggConfigs().aggs, true); + + expect(columns).toHaveLength(1); + expect(columns[0]).toHaveProperty('aggConfig'); + expect(columns[0].aggConfig.type).toHaveProperty('name', 'count'); + }); + + test('should inject a count metric if only buckets exist', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + ]).aggs, + true + ); + + expect(columns).toHaveLength(2); + expect(columns[1]).toHaveProperty('aggConfig'); + expect(columns[1].aggConfig.type).toHaveProperty('name', 'count'); + }); + + test('should inject the metric after each bucket if the vis is hierarchical', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + ]).aggs, + false + ); + + expect(columns).toHaveLength(8); + + columns.forEach((column, i) => { + expect(column).toHaveProperty('aggConfig'); + expect(column.aggConfig.type).toHaveProperty('name', i % 2 ? 'count' : 'date_histogram'); + }); + }); + + test('should inject the multiple metrics after each bucket if the vis is hierarchical', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { type: 'sum', schema: 'metric', params: { field: 'bytes' } }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + ]).aggs, + false + ); + + function checkColumns(column: AggColumn, i: number) { + expect(column).toHaveProperty('aggConfig'); + + switch (i) { + case 0: + expect(column.aggConfig.type).toHaveProperty('name', 'date_histogram'); + break; + case 1: + expect(column.aggConfig.type).toHaveProperty('name', 'avg'); + break; + case 2: + expect(column.aggConfig.type).toHaveProperty('name', 'sum'); + break; + } + } + + expect(columns).toHaveLength(12); + + for (let i = 0; i < columns.length; i += 3) { + columns.slice(i, i + 3).forEach(checkColumns); + } + }); + + test('should put all metrics at the end of the columns if the vis is not hierarchical', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '20s' }, + }, + { type: 'sum', schema: 'metric', params: { field: '@timestamp' } }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + ]).aggs, + false + ); + + expect(columns.map(c => c.name)).toEqual([ + '@timestamp per 20 seconds', + 'Sum of @timestamp', + '@timestamp per 10 seconds', + 'Sum of @timestamp', + ]); + }); +}); diff --git a/src/legacy/ui/public/agg_response/tabify/_get_columns.ts b/src/legacy/core_plugins/data/public/search/tabify/get_columns.ts similarity index 96% rename from src/legacy/ui/public/agg_response/tabify/_get_columns.ts rename to src/legacy/core_plugins/data/public/search/tabify/get_columns.ts index 4144d5be16012a..54f09f6c6364fa 100644 --- a/src/legacy/ui/public/agg_response/tabify/_get_columns.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/get_columns.ts @@ -18,7 +18,7 @@ */ import { groupBy } from 'lodash'; -import { IAggConfig } from '../../agg_types'; +import { IAggConfig } from '../aggs'; export interface AggColumn { aggConfig: IAggConfig; @@ -40,7 +40,7 @@ const getColumn = (agg: IAggConfig, i: number): AggColumn => { * @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates * @param {boolean} minimalColumns - setting to true will only return a column for the last bucket/metric instead of one for each level */ -export function tabifyGetColumns(aggs: IAggConfig[], minimalColumns: boolean) { +export function tabifyGetColumns(aggs: IAggConfig[], minimalColumns: boolean): AggColumn[] { // pick the columns if (minimalColumns) { return aggs.map((agg, i) => getColumn(agg, i)); diff --git a/src/legacy/ui/public/agg_response/tabify/index.js b/src/legacy/core_plugins/data/public/search/tabify/index.ts similarity index 94% rename from src/legacy/ui/public/agg_response/tabify/index.js rename to src/legacy/core_plugins/data/public/search/tabify/index.ts index f14ca647e4b32a..be8d64510033c4 100644 --- a/src/legacy/ui/public/agg_response/tabify/index.js +++ b/src/legacy/core_plugins/data/public/search/tabify/index.ts @@ -18,3 +18,4 @@ */ export { tabifyAggResponse } from './tabify'; +export { tabifyGetColumns } from './get_columns'; diff --git a/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts b/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts new file mode 100644 index 00000000000000..f5df0a683ca00c --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts @@ -0,0 +1,170 @@ +/* + * 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 { TabbedAggResponseWriter } from './response_writer'; +import { AggConfigs, AggGroupNames, Schemas, BUCKET_TYPES } from '../aggs'; + +import { TabbedResponseWriterOptions } from './types'; + +jest.mock('ui/new_platform'); + +describe('TabbedAggResponseWriter class', () => { + let responseWriter: TabbedAggResponseWriter; + + const splitAggConfig = [ + { + type: BUCKET_TYPES.TERMS, + params: { + field: 'geo.src', + }, + }, + ]; + + const twoSplitsAggConfig = [ + { + type: BUCKET_TYPES.TERMS, + params: { + field: 'geo.src', + }, + }, + { + type: BUCKET_TYPES.TERMS, + params: { + field: 'machine.os.raw', + }, + }, + ]; + + const createResponseWritter = (aggs: any[] = [], opts?: Partial) => { + const field = { + name: 'geo.src', + }; + + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + return new TabbedAggResponseWriter( + new AggConfigs( + indexPattern, + aggs, + new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + min: 1, + defaults: [{ schema: 'metric', type: 'count' }], + }, + ]).all + ), + { + metricsAtAllLevels: false, + partialRows: false, + ...opts, + } + ); + }; + + describe('Constructor', () => { + beforeEach(() => { + responseWriter = createResponseWritter(twoSplitsAggConfig); + }); + + test('generates columns', () => { + expect(responseWriter.columns.length).toEqual(3); + }); + + test('correctly generates columns with metricsAtAllLevels set to true', () => { + const minimalColumnsResponseWriter = createResponseWritter(twoSplitsAggConfig, { + metricsAtAllLevels: true, + }); + + expect(minimalColumnsResponseWriter.columns.length).toEqual(4); + }); + + describe('row()', () => { + beforeEach(() => { + responseWriter = createResponseWritter(splitAggConfig); + }); + + test('adds the row to the array', () => { + responseWriter.bucketBuffer = [{ id: 'col-0', value: 'US' }]; + responseWriter.metricBuffer = [{ id: 'col-1', value: 5 }]; + + responseWriter.row(); + + expect(responseWriter.rows.length).toEqual(1); + expect(responseWriter.rows[0]).toEqual({ 'col-0': 'US', 'col-1': 5 }); + }); + + test("doesn't add an empty row", () => { + responseWriter.row(); + + expect(responseWriter.rows.length).toEqual(0); + }); + }); + + describe('response()', () => { + beforeEach(() => { + responseWriter = createResponseWritter(splitAggConfig); + }); + + test('produces correct response', () => { + responseWriter.bucketBuffer = [ + { id: 'col-0-1', value: 'US' }, + { id: 'col-1-2', value: 5 }, + ]; + responseWriter.row(); + + const response = responseWriter.response(); + + expect(response).toHaveProperty('rows'); + expect(response.rows).toEqual([{ 'col-0-1': 'US', 'col-1-2': 5 }]); + expect(response).toHaveProperty('columns'); + expect(response.columns.length).toEqual(2); + expect(response.columns[0]).toHaveProperty('id', 'col-0-1'); + expect(response.columns[0]).toHaveProperty('name', 'geo.src: Descending'); + expect(response.columns[0]).toHaveProperty('aggConfig'); + expect(response.columns[1]).toHaveProperty('id', 'col-1-2'); + expect(response.columns[1]).toHaveProperty('name', 'Count'); + expect(response.columns[1]).toHaveProperty('aggConfig'); + }); + + test('produces correct response for no data', () => { + const response = responseWriter.response(); + + expect(response).toHaveProperty('rows'); + expect(response.rows.length).toBe(0); + expect(response).toHaveProperty('columns'); + expect(response.columns.length).toEqual(2); + expect(response.columns[0]).toHaveProperty('id', 'col-0-1'); + expect(response.columns[0]).toHaveProperty('name', 'geo.src: Descending'); + expect(response.columns[0]).toHaveProperty('aggConfig'); + expect(response.columns[1]).toHaveProperty('id', 'col-1-2'); + expect(response.columns[1]).toHaveProperty('name', 'Count'); + expect(response.columns[1]).toHaveProperty('aggConfig'); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/search/tabify/response_writer.ts b/src/legacy/core_plugins/data/public/search/tabify/response_writer.ts new file mode 100644 index 00000000000000..4c4578e505b71a --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/response_writer.ts @@ -0,0 +1,88 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { IAggConfigs } from '../aggs/agg_configs'; +import { AggColumn, tabifyGetColumns } from './get_columns'; + +import { TabbedResponseWriterOptions } from './types'; + +interface TabbedAggColumn { + id: string; + value: string | number; +} + +type TabbedAggRow = Record; + +/** + * Writer class that collects information about an aggregation response and + * produces a table, or a series of tables. + */ +export class TabbedAggResponseWriter { + columns: AggColumn[]; + rows: TabbedAggRow[] = []; + bucketBuffer: TabbedAggColumn[] = []; + metricBuffer: TabbedAggColumn[] = []; + + private readonly partialRows: boolean; + + /** + * @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates + * @param {boolean} metricsAtAllLevels - setting to true will produce metrics for every bucket + * @param {boolean} partialRows - setting to true will not remove rows with missing values + */ + constructor( + aggs: IAggConfigs, + { metricsAtAllLevels = false, partialRows = false }: Partial + ) { + this.partialRows = partialRows; + + this.columns = tabifyGetColumns(aggs.getResponseAggs(), !metricsAtAllLevels); + this.rows = []; + } + + /** + * Create a new row by reading the row buffer and bucketBuffer + */ + row() { + const rowBuffer: TabbedAggRow = {}; + + this.bucketBuffer.forEach(bucket => { + rowBuffer[bucket.id] = bucket.value; + }); + + this.metricBuffer.forEach(metric => { + rowBuffer[metric.id] = metric.value; + }); + + const isPartialRow = + this.partialRows && !this.columns.every(column => rowBuffer.hasOwnProperty(column.id)); + + if (!isEmpty(rowBuffer) && !isPartialRow) { + this.rows.push(rowBuffer); + } + } + + response() { + return { + columns: this.columns, + rows: this.rows, + }; + } +} diff --git a/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts b/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts new file mode 100644 index 00000000000000..13fe7719b0a856 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts @@ -0,0 +1,172 @@ +/* + * 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 { IndexPattern } from '../../../../../../plugins/data/public'; +import { tabifyAggResponse } from './tabify'; +import { IAggConfig, IAggConfigs, AggGroupNames, Schemas, AggConfigs } from '../aggs'; +import { metricOnly, threeTermBuckets } from 'fixtures/fake_hierarchical_data'; + +jest.mock('ui/new_platform'); + +describe('tabifyAggResponse Integration', () => { + const createAggConfigs = (aggs: IAggConfig[] = []) => { + const field = { + name: '@timestamp', + }; + + const indexPattern = ({ + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as unknown) as IndexPattern; + + return new AggConfigs( + indexPattern, + aggs, + new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + min: 1, + defaults: [{ schema: 'metric', type: 'count' }], + }, + ]).all + ); + }; + + const mockAggConfig = (agg: any): IAggConfig => (agg as unknown) as IAggConfig; + + test('transforms a simple response properly', () => { + const aggConfigs = createAggConfigs(); + + const resp = tabifyAggResponse(aggConfigs, metricOnly, { + metricsAtAllLevels: true, + }); + + expect(resp).toHaveProperty('rows'); + expect(resp).toHaveProperty('columns'); + + expect(resp.rows).toHaveLength(1); + expect(resp.columns).toHaveLength(1); + + expect(resp.rows[0]).toEqual({ 'col-0-1': 1000 }); + expect(resp.columns[0]).toHaveProperty('aggConfig', aggConfigs.aggs[0]); + }); + + describe('transforms a complex response', () => { + let esResp: typeof threeTermBuckets; + let aggConfigs: IAggConfigs; + let avg: IAggConfig; + let ext: IAggConfig; + let src: IAggConfig; + let os: IAggConfig; + + beforeEach(() => { + aggConfigs = createAggConfigs([ + mockAggConfig({ type: 'avg', schema: 'metric', params: { field: '@timestamp' } }), + mockAggConfig({ type: 'terms', schema: 'split', params: { field: '@timestamp' } }), + mockAggConfig({ type: 'terms', schema: 'segment', params: { field: '@timestamp' } }), + mockAggConfig({ type: 'terms', schema: 'segment', params: { field: '@timestamp' } }), + ]); + + [avg, ext, src, os] = aggConfigs.aggs; + + esResp = threeTermBuckets; + esResp.aggregations.agg_2.buckets[1].agg_3.buckets[0].agg_4.buckets = []; + }); + + // check that the columns of a table are formed properly + function expectColumns(table: ReturnType, aggs: IAggConfig[]) { + expect(table.columns).toHaveLength(aggs.length); + + aggs.forEach((agg, i) => { + expect(table.columns[i]).toHaveProperty('aggConfig', agg); + }); + } + + // check that a row has expected values + function expectRow( + row: Record, + asserts: Array<(val: string | number) => void> + ) { + expect(typeof row).toBe('object'); + + asserts.forEach((assert, i: number) => { + if (row[`col-${i}`]) { + assert(row[`col-${i}`]); + } + }); + } + + // check for two character country code + function expectCountry(val: string | number) { + expect(typeof val).toBe('string'); + expect(val).toHaveLength(2); + } + + // check for an OS term + function expectExtension(val: string | number) { + expect(val).toMatch(/^(js|png|html|css|jpg)$/); + } + + // check for an OS term + function expectOS(val: string | number) { + expect(val).toMatch(/^(win|mac|linux)$/); + } + + // check for something like an average bytes result + function expectAvgBytes(val: string | number) { + expect(typeof val).toBe('number'); + expect(val === 0 || val > 1000).toBeDefined(); + } + + test('for non-hierarchical vis', () => { + // the default for a non-hierarchical vis is to display + // only complete rows, and only put the metrics at the end. + + const tabbed = tabifyAggResponse(aggConfigs, esResp, { metricsAtAllLevels: false }); + + expectColumns(tabbed, [ext, src, os, avg]); + + tabbed.rows.forEach(row => { + expectRow(row, [expectExtension, expectCountry, expectOS, expectAvgBytes]); + }); + }); + + test('for hierarchical vis', () => { + const tabbed = tabifyAggResponse(aggConfigs, esResp, { metricsAtAllLevels: true }); + + expectColumns(tabbed, [ext, avg, src, avg, os, avg]); + + tabbed.rows.forEach(row => { + expectRow(row, [ + expectExtension, + expectAvgBytes, + expectCountry, + expectAvgBytes, + expectOS, + expectAvgBytes, + ]); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/search/tabify/tabify.ts b/src/legacy/core_plugins/data/public/search/tabify/tabify.ts new file mode 100644 index 00000000000000..078d3f7f72759b --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/tabify.ts @@ -0,0 +1,173 @@ +/* + * 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 { get } from 'lodash'; +import { TabbedAggResponseWriter } from './response_writer'; +import { TabifyBuckets } from './buckets'; +import { TabbedResponseWriterOptions, TabbedRangeFilterParams } from './types'; +import { AggResponseBucket } from '../types'; +import { IAggConfigs, AggGroupNames } from '../aggs'; + +/** + * Sets up the ResponseWriter and kicks off bucket collection. + */ +export function tabifyAggResponse( + aggConfigs: IAggConfigs, + esResponse: Record, + respOpts?: Partial +) { + /** + * read an aggregation from a bucket, which *might* be found at key (if + * the response came in object form), and will recurse down the aggregation + * tree and will pass the read values to the ResponseWriter. + */ + function collectBucket( + aggs: IAggConfigs, + write: TabbedAggResponseWriter, + bucket: AggResponseBucket, + key: string, + aggScale: number + ) { + const column = write.columns.shift(); + + if (column) { + const agg = column.aggConfig; + const aggInfo = agg.write(aggs); + aggScale *= aggInfo.metricScale || 1; + + switch (agg.type.type) { + case AggGroupNames.Buckets: + const aggBucket = get(bucket, agg.id); + const tabifyBuckets = new TabifyBuckets(aggBucket, agg.params, timeRange); + + if (tabifyBuckets.length) { + tabifyBuckets.forEach((subBucket, tabifyBucketKey) => { + // if the bucket doesn't have value don't add it to the row + // we don't want rows like: { column1: undefined, column2: 10 } + const bucketValue = agg.getKey(subBucket, tabifyBucketKey); + const hasBucketValue = typeof bucketValue !== 'undefined'; + + if (hasBucketValue) { + write.bucketBuffer.push({ id: column.id, value: bucketValue }); + } + + collectBucket( + aggs, + write, + subBucket, + agg.getKey(subBucket, tabifyBucketKey), + aggScale + ); + + if (hasBucketValue) { + write.bucketBuffer.pop(); + } + }); + } else if (respOpts?.partialRows) { + // we don't have any buckets, but we do have metrics at this + // level, then pass all the empty buckets and jump back in for + // the metrics. + write.columns.unshift(column); + passEmptyBuckets(aggs, write, bucket, key, aggScale); + write.columns.shift(); + } else { + // we don't have any buckets, and we don't have isHierarchical + // data, so no metrics, just try to write the row + write.row(); + } + break; + case AggGroupNames.Metrics: + let value = agg.getValue(bucket); + // since the aggregation could be a non integer (such as a max date) + // only do the scaling calculation if it is needed. + if (aggScale !== 1) { + value *= aggScale; + } + write.metricBuffer.push({ id: column.id, value }); + + if (!write.columns.length) { + // row complete + write.row(); + } else { + // process the next agg at this same level + collectBucket(aggs, write, bucket, key, aggScale); + } + + write.metricBuffer.pop(); + + break; + } + + write.columns.unshift(column); + } + } + + // write empty values for each bucket agg, then write + // the metrics from the initial bucket using collectBucket() + function passEmptyBuckets( + aggs: IAggConfigs, + write: TabbedAggResponseWriter, + bucket: AggResponseBucket, + key: string, + aggScale: number + ) { + const column = write.columns.shift(); + + if (column) { + const agg = column.aggConfig; + + switch (agg.type.type) { + case AggGroupNames.Metrics: + // pass control back to collectBucket() + write.columns.unshift(column); + collectBucket(aggs, write, bucket, key, aggScale); + return; + + case AggGroupNames.Buckets: + passEmptyBuckets(aggs, write, bucket, key, aggScale); + } + + write.columns.unshift(column); + } + } + + const write = new TabbedAggResponseWriter(aggConfigs, respOpts || {}); + const topLevelBucket: AggResponseBucket = { + ...esResponse.aggregations, + doc_count: esResponse.hits.total, + }; + + let timeRange: TabbedRangeFilterParams | undefined; + + // Extract the time range object if provided + if (respOpts && respOpts.timeRange) { + const [timeRangeKey] = Object.keys(respOpts.timeRange); + + if (timeRangeKey) { + timeRange = { + name: timeRangeKey, + ...respOpts.timeRange[timeRangeKey], + }; + } + } + + collectBucket(aggConfigs, write, topLevelBucket, '', 1); + + return write.response(); +} diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/tabify.js b/src/legacy/core_plugins/data/public/search/tabify/types.ts similarity index 69% rename from src/legacy/ui/public/agg_response/tabify/__tests__/tabify.js rename to src/legacy/core_plugins/data/public/search/tabify/types.ts index 38ed5408b603e5..3a02a2b64f0c33 100644 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/tabify.js +++ b/src/legacy/core_plugins/data/public/search/tabify/types.ts @@ -17,8 +17,16 @@ * under the License. */ -import './_get_columns'; -import './_buckets'; -import './_response_writer'; -import './_integration'; -describe('Tabify Agg Response', function() {}); +import { RangeFilterParams } from '../../../../../../plugins/data/public'; + +/** @internal **/ +export interface TabbedRangeFilterParams extends RangeFilterParams { + name: string; +} + +/** @internal **/ +export interface TabbedResponseWriterOptions { + metricsAtAllLevels: boolean; + partialRows: boolean; + timeRange?: { [key: string]: RangeFilterParams }; +} diff --git a/src/legacy/core_plugins/data/public/search/utils/types.ts b/src/legacy/core_plugins/data/public/search/utils/types.ts index 305f27a86b398c..e0afe99aa81fac 100644 --- a/src/legacy/core_plugins/data/public/search/utils/types.ts +++ b/src/legacy/core_plugins/data/public/search/utils/types.ts @@ -31,3 +31,9 @@ export interface RequestInspectorStats { hits?: InspectorStat; requestTime?: InspectorStat; } + +export interface AggResponseBucket { + key_as_string: string; + key: number; + doc_count: number; +} 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 b0bb17ce1ac7f7..91b5c7f13dc954 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -58,8 +58,7 @@ export { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; // @ts-ignore export { timezoneProvider } from 'ui/vis/lib/timezone'; -// @ts-ignore -export { tabifyAggResponse } from 'ui/agg_response/tabify'; +export { tabifyAggResponse } from '../../../data/public'; export { unhashUrl } from '../../../../../plugins/kibana_utils/public'; export { migrateLegacyQuery, diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js index 0dbff60613cb08..9fe7920588cd24 100644 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js +++ b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js @@ -21,7 +21,11 @@ import $ from 'jquery'; import moment from 'moment'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import fixtures from 'fixtures/fake_hierarchical_data'; +import { + metricOnly, + threeTermBuckets, + oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative, +} from 'fixtures/fake_hierarchical_data'; import sinon from 'sinon'; import { tabifyAggResponse, npStart } from '../../legacy_imports'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; @@ -44,7 +48,7 @@ describe('Table Vis - AggTable Directive', function() { const init = () => { const vis1 = new visualizationsStart.Vis(indexPattern, 'table'); - tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, fixtures.metricOnly); + tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, metricOnly); const vis2 = new visualizationsStart.Vis(indexPattern, { type: 'table', @@ -61,7 +65,7 @@ describe('Table Vis - AggTable Directive', function() { vis2.aggs.aggs.forEach(function(agg, i) { agg.id = 'agg_' + (i + 1); }); - tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.aggs, fixtures.threeTermBuckets, { + tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.aggs, threeTermBuckets, { metricsAtAllLevels: true, }); @@ -94,7 +98,7 @@ describe('Table Vis - AggTable Directive', function() { tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = tabifyAggResponse( vis3.aggs, - fixtures.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative + oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative ); }; diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js index f6ae41b024b7de..79d4d7c40d3559 100644 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js +++ b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js @@ -20,7 +20,7 @@ import $ from 'jquery'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import fixtures from 'fixtures/fake_hierarchical_data'; +import { metricOnly, threeTermBuckets } from 'fixtures/fake_hierarchical_data'; import { tabifyAggResponse, npStart } from '../../legacy_imports'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { getAngularModule } from '../../get_inner_angular'; @@ -36,7 +36,7 @@ describe('Table Vis - AggTableGroup Directive', function() { const init = () => { const vis1 = new visualizationsStart.Vis(indexPattern, 'table'); - tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, fixtures.metricOnly); + tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, metricOnly); const vis2 = new visualizationsStart.Vis(indexPattern, { type: 'pie', @@ -50,7 +50,7 @@ describe('Table Vis - AggTableGroup Directive', function() { vis2.aggs.aggs.forEach(function(agg, i) { agg.id = 'agg_' + (i + 1); }); - tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.aggs, fixtures.threeTermBuckets); + tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.aggs, threeTermBuckets); }; const initLocalAngular = () => { diff --git a/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts index cb44814897bcfc..90929150de9c39 100644 --- a/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts @@ -24,9 +24,7 @@ export { IAggConfig, AggGroupNames, Schemas } from 'ui/agg_types'; export { PaginateDirectiveProvider } from 'ui/directives/paginate'; // @ts-ignore export { PaginateControlsDirectiveProvider } from 'ui/directives/paginate'; -export { tabifyGetColumns } from 'ui/agg_response/tabify/_get_columns'; -// @ts-ignore -export { tabifyAggResponse } from 'ui/agg_response/tabify'; +export { tabifyAggResponse, tabifyGetColumns } from '../../data/public'; export { configureAppAngularModule, KbnAccessibleClickProvider, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts index 9c79be98a320c3..1c8e679f7d61f3 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts @@ -19,10 +19,8 @@ export { AggType, AggGroupNames, IAggConfig, IAggType, Schemas } from 'ui/agg_types'; export { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; -// @ts-ignore -export { tabifyAggResponse } from 'ui/agg_response/tabify'; +export { tabifyAggResponse, tabifyGetColumns } from '../../data/public'; // @ts-ignore export { buildHierarchicalData } from 'ui/agg_response/hierarchical/build_hierarchical_data'; // @ts-ignore export { buildPointSeriesData } from 'ui/agg_response/point_series/point_series'; -export { tabifyGetColumns } from '../../../ui/public/agg_response/tabify/_get_columns'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js index 534a5231037745..9c9c5a84f046cc 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js @@ -22,7 +22,7 @@ import _ from 'lodash'; import $ from 'jquery'; import expect from '@kbn/expect'; -import fixtures from 'fixtures/fake_hierarchical_data'; +import { threeTermBuckets } from 'fixtures/fake_hierarchical_data'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { start as visualizationsStart } from '../../../../../visualizations/public/np_ready/public/legacy'; @@ -147,7 +147,7 @@ describe('No global chart settings', function() { }); beforeEach(async () => { - const table1 = tabifyAggResponse(stubVis1.aggs, fixtures.threeTermBuckets, { + const table1 = tabifyAggResponse(stubVis1.aggs, threeTermBuckets, { metricsAtAllLevels: true, }); data1 = await responseHandler(table1, rowAggDimensions); @@ -234,7 +234,7 @@ describe('Vislib PieChart Class Test Suite', function() { }); beforeEach(async () => { - const table = tabifyAggResponse(stubVis.aggs, fixtures.threeTermBuckets, { + const table = tabifyAggResponse(stubVis.aggs, threeTermBuckets, { metricsAtAllLevels: true, }); data = await responseHandler(table, dataDimensions); diff --git a/src/legacy/ui/public/agg_response/index.js b/src/legacy/ui/public/agg_response/index.js index 41d45d1a06ca43..139a124356de21 100644 --- a/src/legacy/ui/public/agg_response/index.js +++ b/src/legacy/ui/public/agg_response/index.js @@ -19,7 +19,7 @@ import { buildHierarchicalData } from './hierarchical/build_hierarchical_data'; import { buildPointSeriesData } from './point_series/point_series'; -import { tabifyAggResponse } from './tabify/tabify'; +import { tabifyAggResponse } from '../../../core_plugins/data/public'; export const aggResponseIndex = { hierarchical: buildHierarchicalData, diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/_get_columns.js b/src/legacy/ui/public/agg_response/tabify/__tests__/_get_columns.js deleted file mode 100644 index 3eb41c03050d03..00000000000000 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/_get_columns.js +++ /dev/null @@ -1,199 +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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { tabifyGetColumns } from '../_get_columns'; -import { start as visualizationsStart } from '../../../../../core_plugins/visualizations/public/np_ready/public/legacy'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -describe('get columns', function() { - let indexPattern; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private) { - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - }) - ); - - it('should inject a count metric if no aggs exist', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'pie', - }); - while (vis.aggs.length) vis.aggs.pop(); - const columns = tabifyGetColumns( - vis.getAggConfig().getResponseAggs(), - null, - vis.isHierarchical() - ); - - expect(columns).to.have.length(1); - expect(columns[0]).to.have.property('aggConfig'); - expect(columns[0].aggConfig.type).to.have.property('name', 'count'); - }); - - it('should inject a count metric if only buckets exist', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'pie', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - ], - }); - - const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), !vis.isHierarchical()); - - expect(columns).to.have.length(2); - expect(columns[1]).to.have.property('aggConfig'); - expect(columns[1].aggConfig.type).to.have.property('name', 'count'); - }); - - it('should inject the metric after each bucket if the vis is hierarchical', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'pie', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - ], - }); - - const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), !vis.isHierarchical()); - - expect(columns).to.have.length(8); - columns.forEach(function(column, i) { - expect(column).to.have.property('aggConfig'); - expect(column.aggConfig.type).to.have.property('name', i % 2 ? 'count' : 'date_histogram'); - }); - }); - - it('should inject the multiple metrics after each bucket if the vis is hierarchical', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'pie', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { type: 'sum', schema: 'metric', params: { field: 'bytes' } }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - ], - }); - - const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), !vis.isHierarchical()); - - function checkColumns(column, i) { - expect(column).to.have.property('aggConfig'); - switch (i) { - case 0: - expect(column.aggConfig.type).to.have.property('name', 'date_histogram'); - break; - case 1: - expect(column.aggConfig.type).to.have.property('name', 'avg'); - break; - case 2: - expect(column.aggConfig.type).to.have.property('name', 'sum'); - break; - } - } - - expect(columns).to.have.length(12); - for (let i = 0; i < columns.length; i += 3) { - columns.slice(i, i + 3).forEach(checkColumns); - } - }); - - it('should put all metrics at the end of the columns if the vis is not hierarchical', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { type: 'sum', schema: 'metric', params: { field: 'bytes' } }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - ], - }); - - const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), !vis.isHierarchical()); - expect(columns).to.have.length(6); - - // sum should be last - expect(columns.pop().aggConfig.type).to.have.property('name', 'sum'); - // avg should be before that - expect(columns.pop().aggConfig.type).to.have.property('name', 'avg'); - // the rest are date_histograms - while (columns.length) { - expect(columns.pop().aggConfig.type).to.have.property('name', 'date_histogram'); - } - }); -}); diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/_integration.js b/src/legacy/ui/public/agg_response/tabify/__tests__/_integration.js deleted file mode 100644 index f3f2e20149acfc..00000000000000 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/_integration.js +++ /dev/null @@ -1,175 +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 fixtures from 'fixtures/fake_hierarchical_data'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { tabifyAggResponse } from '../tabify'; -import { start as visualizationsStart } from '../../../../../core_plugins/visualizations/public/np_ready/public/legacy'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; - -describe('tabifyAggResponse Integration', function() { - let indexPattern; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private) { - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - }) - ); - - function normalizeIds(vis) { - vis.aggs.aggs.forEach(function(agg, i) { - agg.id = 'agg_' + (i + 1); - }); - } - - it('transforms a simple response properly', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [], - }); - normalizeIds(vis); - - const resp = tabifyAggResponse(vis.getAggConfig(), fixtures.metricOnly, { - metricsAtAllLevels: vis.isHierarchical(), - }); - - expect(resp) - .to.have.property('rows') - .and.property('columns'); - expect(resp.rows).to.have.length(1); - expect(resp.columns).to.have.length(1); - - expect(resp.rows[0]).to.eql({ 'col-0-agg_1': 1000 }); - expect(resp.columns[0]).to.have.property('aggConfig', vis.aggs[0]); - }); - - describe('transforms a complex response', function() { - this.slow(1000); - - let vis; - let avg; - let ext; - let src; - let os; - let esResp; - - beforeEach(function() { - vis = new visualizationsStart.Vis(indexPattern, { - type: 'pie', - aggs: [ - { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, - { type: 'terms', schema: 'split', params: { field: 'extension' } }, - { type: 'terms', schema: 'segment', params: { field: 'geo.src' } }, - { type: 'terms', schema: 'segment', params: { field: 'machine.os' } }, - ], - }); - normalizeIds(vis); - - avg = vis.aggs[0]; - ext = vis.aggs[1]; - src = vis.aggs[2]; - os = vis.aggs[3]; - - esResp = _.cloneDeep(fixtures.threeTermBuckets); - // remove the buckets for css in MX - esResp.aggregations.agg_2.buckets[1].agg_3.buckets[0].agg_4.buckets = []; - }); - - // check that the columns of a table are formed properly - function expectColumns(table, aggs) { - expect(table.columns) - .to.be.an('array') - .and.have.length(aggs.length); - aggs.forEach(function(agg, i) { - expect(table.columns[i]).to.have.property('aggConfig', agg); - }); - } - - // check that a row has expected values - function expectRow(row, asserts) { - expect(row).to.be.an('object'); - asserts.forEach(function(assert, i) { - if (row[`col-${i}`]) { - assert(row[`col-${i}`]); - } - }); - } - - // check for two character country code - function expectCountry(val) { - expect(val).to.be.a('string'); - expect(val).to.have.length(2); - } - - // check for an OS term - function expectExtension(val) { - expect(val).to.match(/^(js|png|html|css|jpg)$/); - } - - // check for an OS term - function expectOS(val) { - expect(val).to.match(/^(win|mac|linux)$/); - } - - // check for something like an average bytes result - function expectAvgBytes(val) { - expect(val).to.be.a('number'); - expect(val === 0 || val > 1000).to.be.ok(); - } - - it('for non-hierarchical vis', function() { - // the default for a non-hierarchical vis is to display - // only complete rows, and only put the metrics at the end. - - const tabbed = tabifyAggResponse(vis.getAggConfig(), esResp, { metricsAtAllLevels: false }); - - expectColumns(tabbed, [ext, src, os, avg]); - - tabbed.rows.forEach(function(row) { - expectRow(row, [expectExtension, expectCountry, expectOS, expectAvgBytes]); - }); - }); - - it('for hierarchical vis', function() { - // since we have partialRows we expect that one row will have some empty - // values, and since the vis is hierarchical and we are NOT using - // minimalColumns we should expect the partial row to be completely after - // the existing bucket and it's metric - - vis.isHierarchical = _.constant(true); - const tabbed = tabifyAggResponse(vis.getAggConfig(), esResp, { metricsAtAllLevels: true }); - - expectColumns(tabbed, [ext, avg, src, avg, os, avg]); - - tabbed.rows.forEach(function(row) { - expectRow(row, [ - expectExtension, - expectAvgBytes, - expectCountry, - expectAvgBytes, - expectOS, - expectAvgBytes, - ]); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/_response_writer.js b/src/legacy/ui/public/agg_response/tabify/__tests__/_response_writer.js deleted file mode 100644 index b0c0f2f3d91002..00000000000000 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/_response_writer.js +++ /dev/null @@ -1,186 +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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { TabbedAggResponseWriter } from '../_response_writer'; -import { start as visualizationsStart } from '../../../../../core_plugins/visualizations/public/np_ready/public/legacy'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; - -describe('TabbedAggResponseWriter class', function() { - let Private; - let indexPattern; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function($injector) { - Private = $injector.get('Private'); - - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - }) - ); - - const splitAggConfig = [ - { - type: 'terms', - params: { - field: 'geo.src', - }, - }, - ]; - - const twoSplitsAggConfig = [ - { - type: 'terms', - params: { - field: 'geo.src', - }, - }, - { - type: 'terms', - params: { - field: 'machine.os.raw', - }, - }, - ]; - - const createResponseWritter = (aggs = [], opts = {}) => { - const vis = new visualizationsStart.Vis(indexPattern, { type: 'histogram', aggs: aggs }); - return new TabbedAggResponseWriter(vis.getAggConfig(), opts); - }; - - describe('Constructor', function() { - let responseWriter; - beforeEach(() => { - responseWriter = createResponseWritter(twoSplitsAggConfig); - }); - - it('creates aggStack', () => { - expect(responseWriter.aggStack.length).to.eql(3); - }); - - it('generates columns', () => { - expect(responseWriter.columns.length).to.eql(3); - }); - - it('correctly generates columns with metricsAtAllLevels set to true', () => { - const minimalColumnsResponseWriter = createResponseWritter(twoSplitsAggConfig, { - metricsAtAllLevels: true, - }); - expect(minimalColumnsResponseWriter.columns.length).to.eql(4); - }); - - describe('sets timeRange', function() { - it("to the first nested object's range", function() { - const vis = new visualizationsStart.Vis(indexPattern, { type: 'histogram', aggs: [] }); - const range = { - gte: 0, - lte: 100, - }; - - const writer = new TabbedAggResponseWriter(vis.getAggConfig(), { - timeRange: { - '@timestamp': range, - }, - }); - - expect(writer.timeRange.gte).to.be(range.gte); - expect(writer.timeRange.lte).to.be(range.lte); - expect(writer.timeRange.name).to.be('@timestamp'); - }); - - it('to undefined if no nested object', function() { - const vis = new visualizationsStart.Vis(indexPattern, { type: 'histogram', aggs: [] }); - - const writer = new TabbedAggResponseWriter(vis.getAggConfig(), { - timeRange: {}, - }); - expect(writer).to.have.property('timeRange', undefined); - }); - }); - }); - - describe('row()', function() { - let responseWriter; - - beforeEach(() => { - responseWriter = createResponseWritter(splitAggConfig, { partialRows: true }); - }); - - it('adds the row to the array', () => { - responseWriter.rowBuffer['col-0'] = 'US'; - responseWriter.rowBuffer['col-1'] = 5; - responseWriter.row(); - expect(responseWriter.rows.length).to.eql(1); - expect(responseWriter.rows[0]).to.eql({ 'col-0': 'US', 'col-1': 5 }); - }); - - it('correctly handles bucketBuffer', () => { - responseWriter.bucketBuffer.push({ id: 'col-0', value: 'US' }); - responseWriter.rowBuffer['col-1'] = 5; - responseWriter.row(); - expect(responseWriter.rows.length).to.eql(1); - expect(responseWriter.rows[0]).to.eql({ 'col-0': 'US', 'col-1': 5 }); - }); - - it("doesn't add an empty row", () => { - responseWriter.row(); - expect(responseWriter.rows.length).to.eql(0); - }); - }); - - describe('response()', () => { - let responseWriter; - - beforeEach(() => { - responseWriter = createResponseWritter(splitAggConfig); - }); - - it('produces correct response', () => { - responseWriter.rowBuffer['col-0-1'] = 'US'; - responseWriter.rowBuffer['col-1-2'] = 5; - responseWriter.row(); - const response = responseWriter.response(); - expect(response).to.have.property('rows'); - expect(response.rows).to.eql([{ 'col-0-1': 'US', 'col-1-2': 5 }]); - expect(response).to.have.property('columns'); - expect(response.columns.length).to.equal(2); - expect(response.columns[0]).to.have.property('id', 'col-0-1'); - expect(response.columns[0]).to.have.property('name', 'geo.src: Descending'); - expect(response.columns[0]).to.have.property('aggConfig'); - expect(response.columns[1]).to.have.property('id', 'col-1-2'); - expect(response.columns[1]).to.have.property('name', 'Count'); - expect(response.columns[1]).to.have.property('aggConfig'); - }); - - it('produces correct response for no data', () => { - const response = responseWriter.response(); - expect(response).to.have.property('rows'); - expect(response.rows.length).to.be(0); - expect(response).to.have.property('columns'); - expect(response.columns.length).to.equal(2); - expect(response.columns[0]).to.have.property('id', 'col-0-1'); - expect(response.columns[0]).to.have.property('name', 'geo.src: Descending'); - expect(response.columns[0]).to.have.property('aggConfig'); - expect(response.columns[1]).to.have.property('id', 'col-1-2'); - expect(response.columns[1]).to.have.property('name', 'Count'); - expect(response.columns[1]).to.have.property('aggConfig'); - }); - }); -}); diff --git a/src/legacy/ui/public/agg_response/tabify/_buckets.js b/src/legacy/ui/public/agg_response/tabify/_buckets.js deleted file mode 100644 index 7180a056ab0ca0..00000000000000 --- a/src/legacy/ui/public/agg_response/tabify/_buckets.js +++ /dev/null @@ -1,123 +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 moment from 'moment'; - -function TabifyBuckets(aggResp, aggParams, timeRange) { - if (_.has(aggResp, 'buckets')) { - this.buckets = aggResp.buckets; - } else if (aggResp) { - // Some Bucket Aggs only return a single bucket (like filter). - // In those instances, the aggResp is the content of the single bucket. - this.buckets = [aggResp]; - } else { - this.buckets = []; - } - - this.objectMode = _.isPlainObject(this.buckets); - if (this.objectMode) { - this._keys = _.keys(this.buckets); - this.length = this._keys.length; - } else { - this.length = this.buckets.length; - } - - if (this.length && aggParams) { - this._orderBucketsAccordingToParams(aggParams); - if (aggParams.drop_partials) { - this._dropPartials(aggParams, timeRange); - } - } -} - -TabifyBuckets.prototype.forEach = function(fn) { - const buckets = this.buckets; - - if (this.objectMode) { - this._keys.forEach(function(key) { - fn(buckets[key], key); - }); - } else { - buckets.forEach(function(bucket) { - fn(bucket, bucket.key); - }); - } -}; - -TabifyBuckets.prototype._isRangeEqual = function(range1, range2) { - return ( - _.get(range1, 'from', null) === _.get(range2, 'from', null) && - _.get(range1, 'to', null) === _.get(range2, 'to', null) - ); -}; - -TabifyBuckets.prototype._orderBucketsAccordingToParams = function(params) { - if (params.filters && this.objectMode) { - this._keys = params.filters.map(filter => { - const query = _.get(filter, 'input.query.query_string.query', filter.input.query); - const queryString = typeof query === 'string' ? query : JSON.stringify(query); - return filter.label || queryString || '*'; - }); - } else if (params.ranges && this.objectMode) { - this._keys = params.ranges.map(range => { - return _.findKey(this.buckets, el => this._isRangeEqual(el, range)); - }); - } else if (params.ranges && params.field.type !== 'date') { - let ranges = params.ranges; - if (params.ipRangeType) { - ranges = params.ipRangeType === 'mask' ? ranges.mask : ranges.fromTo; - } - this.buckets = ranges.map(range => { - if (range.mask) { - return this.buckets.find(el => el.key === range.mask); - } - return this.buckets.find(el => this._isRangeEqual(el, range)); - }); - } -}; - -// dropPartials should only be called if the aggParam setting is enabled, -// and the agg field is the same as the Time Range. -TabifyBuckets.prototype._dropPartials = function(params, timeRange) { - if ( - !timeRange || - this.buckets.length <= 1 || - this.objectMode || - params.field.name !== timeRange.name - ) { - return; - } - - const interval = this.buckets[1].key - this.buckets[0].key; - - this.buckets = this.buckets.filter(bucket => { - if (moment(bucket.key).isBefore(timeRange.gte)) { - return false; - } - if (moment(bucket.key + interval).isAfter(timeRange.lte)) { - return false; - } - return true; - }); - - this.length = this.buckets.length; -}; - -export { TabifyBuckets }; diff --git a/src/legacy/ui/public/agg_response/tabify/_response_writer.js b/src/legacy/ui/public/agg_response/tabify/_response_writer.js deleted file mode 100644 index 85586c7ca7fdaf..00000000000000 --- a/src/legacy/ui/public/agg_response/tabify/_response_writer.js +++ /dev/null @@ -1,97 +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 { toArray } from 'lodash'; -import { tabifyGetColumns } from './_get_columns'; - -/** - * Writer class that collects information about an aggregation response and - * produces a table, or a series of tables. - * - * @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates - * @param {boolean} metricsAtAllLevels - setting to true will produce metrics for every bucket - * @param {boolean} partialRows - setting to true will not remove rows with missing values - * @param {Object} timeRange - time range object, if provided - */ -function TabbedAggResponseWriter( - aggs, - { metricsAtAllLevels = false, partialRows = false, timeRange } = {} -) { - // Private - this._removePartialRows = !partialRows; - - // Public - this.rowBuffer = {}; - this.bucketBuffer = []; - this.metricBuffer = []; - this.aggs = aggs; - this.partialRows = partialRows; - this.columns = tabifyGetColumns(aggs.getResponseAggs(), !metricsAtAllLevels); - this.aggStack = [...this.columns]; - this.rows = []; - // Extract the time range object if provided - if (timeRange) { - const timeRangeKey = Object.keys(timeRange)[0]; - this.timeRange = timeRange[timeRangeKey]; - if (this.timeRange) { - this.timeRange.name = timeRangeKey; - } - } -} - -TabbedAggResponseWriter.prototype.isPartialRow = function(row) { - return !this.columns.map(column => row.hasOwnProperty(column.id)).every(c => c === true); -}; - -/** - * Create a new row by reading the row buffer and bucketBuffer - */ -TabbedAggResponseWriter.prototype.row = function() { - this.bucketBuffer.forEach(bucket => { - this.rowBuffer[bucket.id] = bucket.value; - }); - - this.metricBuffer.forEach(metric => { - this.rowBuffer[metric.id] = metric.value; - }); - - if ( - !toArray(this.rowBuffer).length || - (this._removePartialRows && this.isPartialRow(this.rowBuffer)) - ) { - return; - } - - this.rows.push(this.rowBuffer); - this.rowBuffer = {}; -}; - -/** - * Get the actual response - * - * @return {object} - the final table - */ -TabbedAggResponseWriter.prototype.response = function() { - return { - columns: this.columns, - rows: this.rows, - }; -}; - -export { TabbedAggResponseWriter }; diff --git a/src/legacy/ui/public/agg_response/tabify/tabify.js b/src/legacy/ui/public/agg_response/tabify/tabify.js deleted file mode 100644 index 8316055cb15cc9..00000000000000 --- a/src/legacy/ui/public/agg_response/tabify/tabify.js +++ /dev/null @@ -1,134 +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 { TabbedAggResponseWriter } from './_response_writer'; -import { TabifyBuckets } from './_buckets'; - -/** - * Sets up the ResponseWriter and kicks off bucket collection. - * - * @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates - * @param {Object} esResponse - response that came back from Elasticsearch - * @param {Object} respOpts - options object for the ResponseWriter with params set by Courier - * @param {boolean} respOpts.metricsAtAllLevels - setting to true will produce metrics for every bucket - * @param {boolean} respOpts.partialRows - setting to true will not remove rows with missing values - * @param {Object} respOpts.timeRange - time range object, if provided - */ -export function tabifyAggResponse(aggs, esResponse, respOpts = {}) { - const write = new TabbedAggResponseWriter(aggs, respOpts); - - const topLevelBucket = _.assign({}, esResponse.aggregations, { - doc_count: esResponse.hits.total, - }); - - collectBucket(write, topLevelBucket, '', 1); - - return write.response(); -} - -/** - * read an aggregation from a bucket, which *might* be found at key (if - * the response came in object form), and will recurse down the aggregation - * tree and will pass the read values to the ResponseWriter. - * - * @param {object} bucket - a bucket from the aggResponse - * @param {undefined|string} key - the key where the bucket was found - * @returns {undefined} - */ -function collectBucket(write, bucket, key, aggScale) { - const column = write.aggStack.shift(); - const agg = column.aggConfig; - const aggInfo = agg.write(write.aggs); - aggScale *= aggInfo.metricScale || 1; - - switch (agg.type.type) { - case 'buckets': - const buckets = new TabifyBuckets(bucket[agg.id], agg.params, write.timeRange); - if (buckets.length) { - buckets.forEach(function(subBucket, key) { - // if the bucket doesn't have value don't add it to the row - // we don't want rows like: { column1: undefined, column2: 10 } - const bucketValue = agg.getKey(subBucket, key); - const hasBucketValue = typeof bucketValue !== 'undefined'; - if (hasBucketValue) { - write.bucketBuffer.push({ id: column.id, value: bucketValue }); - } - collectBucket(write, subBucket, agg.getKey(subBucket, key), aggScale); - if (hasBucketValue) { - write.bucketBuffer.pop(); - } - }); - } else if (write.partialRows) { - // we don't have any buckets, but we do have metrics at this - // level, then pass all the empty buckets and jump back in for - // the metrics. - write.aggStack.unshift(column); - passEmptyBuckets(write, bucket, key, aggScale); - write.aggStack.shift(); - } else { - // we don't have any buckets, and we don't have isHierarchical - // data, so no metrics, just try to write the row - write.row(); - } - break; - case 'metrics': - let value = agg.getValue(bucket); - // since the aggregation could be a non integer (such as a max date) - // only do the scaling calculation if it is needed. - if (aggScale !== 1) { - value *= aggScale; - } - write.metricBuffer.push({ id: column.id, value: value }); - - if (!write.aggStack.length) { - // row complete - write.row(); - } else { - // process the next agg at this same level - collectBucket(write, bucket, key, aggScale); - } - - write.metricBuffer.pop(); - - break; - } - - write.aggStack.unshift(column); -} - -// write empty values for each bucket agg, then write -// the metrics from the initial bucket using collectBucket() -function passEmptyBuckets(write, bucket, key, aggScale) { - const column = write.aggStack.shift(); - const agg = column.aggConfig; - - switch (agg.type.type) { - case 'metrics': - // pass control back to collectBucket() - write.aggStack.unshift(column); - collectBucket(write, bucket, key, aggScale); - return; - - case 'buckets': - passEmptyBuckets(write, bucket, key, aggScale); - } - - write.aggStack.unshift(column); -} diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index cb8b43a6c312b2..0912e5a9f1283d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js @@ -11,7 +11,7 @@ import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import { HeatmapLayer } from '../../heatmap_layer'; import { VectorLayer } from '../../vector_layer'; import { AggConfigs, Schemas } from 'ui/agg_types'; -import { tabifyAggResponse } from 'ui/agg_response/tabify'; +import { tabifyAggResponse } from '../../../../../../../../src/legacy/core_plugins/data/public'; import { convertToGeoJson } from './convert_to_geojson'; import { VectorStyle } from '../../styles/vector/vector_style'; import { From 39c835af605e9c832cde2d2a40200ef8283a46f8 Mon Sep 17 00:00:00 2001 From: Sebastian Grodzicki Date: Thu, 20 Feb 2020 14:05:21 +0100 Subject: [PATCH 06/43] Add owners for monitoring plugin (#57987) --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 56db8d3793f57c..bea10a1c8b31c8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -75,6 +75,7 @@ /x-pack/plugins/ingest_manager/ @elastic/ingest /x-pack/legacy/plugins/ingest_manager/ @elastic/ingest /x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest +/x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui # Machine Learning /x-pack/legacy/plugins/ml/ @elastic/ml-ui From 47bb7c61b67c1370a73fe247f4248326b8f696b4 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 20 Feb 2020 09:13:49 -0500 Subject: [PATCH 07/43] update estimateBucketSpan schema and api test (#58004) --- .../new_platform/job_validation_schema.ts | 1 + .../apis/ml/bucket_span_estimator.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/x-pack/legacy/plugins/ml/server/new_platform/job_validation_schema.ts b/x-pack/legacy/plugins/ml/server/new_platform/job_validation_schema.ts index 1cc6e8a97ffc05..5917ec50884d80 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/job_validation_schema.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/job_validation_schema.ts @@ -11,6 +11,7 @@ export const estimateBucketSpanSchema = schema.object({ aggTypes: schema.arrayOf(schema.nullable(schema.string())), duration: schema.object({ start: schema.number(), end: schema.number() }), fields: schema.arrayOf(schema.nullable(schema.string())), + filters: schema.maybe(schema.arrayOf(schema.any())), index: schema.string(), query: schema.any(), splitField: schema.maybe(schema.string()), diff --git a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts b/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts index 47afe7553fe62b..1c7245234b089f 100644 --- a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts +++ b/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts @@ -69,6 +69,23 @@ export default ({ getService }: FtrProviderContext) => { responseBody: { name: '3h', ms: 10800000 }, }, }, + { + testTitleSuffix: 'with 1 field, 1 agg, no split, and empty filters', + user: USER.ML_POWERUSER, + requestBody: { + aggTypes: ['avg'], + duration: { start: 1560297859000, end: 1562975136000 }, + fields: ['taxless_total_price'], + filters: [], + index: 'ecommerce', + query: { bool: { must: [{ match_all: {} }] } }, + timeField: 'order_date', + }, + expected: { + responseCode: 200, + responseBody: { name: '15m', ms: 900000 }, + }, + }, ]; describe('bucket span estimator', function() { From 5cfc5ef9fffb3f39fedc06b495aecc881d1b731a Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Thu, 20 Feb 2020 09:14:11 -0500 Subject: [PATCH 08/43] Reverting grouped nav UI (#57724) * reverting grouped nav ui * removing Management name change --- docs/management/advanced-options.asciidoc | 3 - src/core/public/chrome/chrome_service.tsx | 1 - .../__snapshots__/nav_drawer.test.tsx.snap | 5283 ----------------- src/core/public/chrome/ui/header/header.tsx | 12 +- src/core/public/chrome/ui/header/index.ts | 1 - .../chrome/ui/header/nav_drawer.test.tsx | 103 - .../public/chrome/ui/header/nav_drawer.tsx | 118 +- src/legacy/core_plugins/kibana/index.js | 2 +- .../home/np_ready/components/home.test.js | 2 +- .../kibana/public/management/index.js | 2 +- .../kibana/ui_setting_defaults.js | 19 - .../ui/public/management/breadcrumbs.ts | 2 +- .../management_sidebar_nav.tsx | 3 +- .../public/legacy/sections_register.js | 4 +- .../management/public/management_app.tsx | 3 +- src/plugins/management/public/plugin.ts | 4 +- .../dashboard/create_and_add_embeddables.js | 1 - .../apps/management/_index_pattern_filter.js | 2 +- test/functional/page_objects/header_page.js | 2 +- test/functional/page_objects/settings_page.ts | 7 - .../core_plugins/application_status.ts | 6 +- .../test_suites/core_plugins/applications.ts | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../advanced_settings_security.ts | 8 +- .../advanced_settings_spaces.ts | 3 +- .../apps/apm/feature_controls/apm_security.ts | 4 +- .../apps/apm/feature_controls/apm_spaces.ts | 3 +- .../feature_controls/canvas_security.ts | 4 +- .../canvas/feature_controls/canvas_spaces.ts | 3 +- .../feature_controls/dashboard_security.ts | 4 +- .../feature_controls/dashboard_spaces.ts | 9 +- .../dashboard_mode/dashboard_view_mode.js | 9 +- .../feature_controls/dev_tools_security.ts | 4 +- .../feature_controls/dev_tools_spaces.ts | 9 +- .../feature_controls/discover_security.ts | 4 +- .../feature_controls/discover_spaces.ts | 2 - .../graph/feature_controls/graph_security.ts | 4 +- .../graph/feature_controls/graph_spaces.ts | 3 +- .../index_patterns_security.ts | 8 +- .../feature_controls/index_patterns_spaces.ts | 3 +- .../infrastructure_security.ts | 4 +- .../feature_controls/infrastructure_spaces.ts | 9 +- .../infra/feature_controls/logs_security.ts | 4 +- .../infra/feature_controls/logs_spaces.ts | 9 +- .../feature_controls/ml_security.ts | 3 +- .../feature_controls/ml_spaces.ts | 3 +- .../maps/feature_controls/maps_security.ts | 6 +- .../feature_controls/monitoring_security.ts | 3 +- .../feature_controls/monitoring_spaces.ts | 3 +- .../feature_controls/spaces_security.ts | 5 +- .../feature_controls/timelion_security.ts | 4 +- .../feature_controls/timelion_spaces.ts | 9 +- .../feature_controls/uptime_security.ts | 4 +- .../uptime/feature_controls/uptime_spaces.ts | 3 +- .../feature_controls/visualize_security.ts | 4 +- .../feature_controls/visualize_spaces.ts | 9 +- 57 files changed, 79 insertions(+), 5671 deletions(-) delete mode 100644 src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap delete mode 100644 src/core/public/chrome/ui/header/nav_drawer.test.tsx diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index ec626677d09025..80c9053dc5ae6f 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -70,9 +70,6 @@ into the document when displaying it. `metrics:max_buckets`:: The maximum numbers of buckets that a single data source can return. This might arise when the user selects a short interval (for example, 1s) for a long time period (1 year). -`pageNavigation`:: The style of navigation menu for Kibana. -Choices are Individual, the legacy style where every plugin is represented in the nav, -and Grouped, a new format that bundles related plugins together in nested navigation. `query:allowLeadingWildcards`:: Allows a wildcard (*) as the first character in a query clause. Only applies when experimental query features are enabled in the query bar. To disallow leading wildcards in Lucene queries, diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 6ab9fe158742a6..2b0b115ce068ee 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -193,7 +193,6 @@ export class ChromeService { recentlyAccessed$={recentlyAccessed.get$()} navControlsLeft$={navControls.getLeft$()} navControlsRight$={navControls.getRight$()} - navSetting$={uiSettings.get$('pageNavigation')} /> ), diff --git a/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap deleted file mode 100644 index cf3b48f237286b..00000000000000 --- a/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap +++ /dev/null @@ -1,5283 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` - - - - - - - -`; - -exports[`NavDrawer Advanced setting set to grouped renders individual items if there are less than 7 1`] = ` - - - - - - - -`; - -exports[`NavDrawer Advanced setting set to grouped renders individual items if there is only 1 category 1`] = ` - - - - - - - -`; - -exports[`NavDrawer Advanced setting set to individual renders individual items 1`] = ` - - - - - - - -`; diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index c3cefd180b16f6..c9a583f39b30cb 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -42,7 +42,7 @@ import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { ChromeHelpExtension } from '../../chrome_service'; import { HeaderBadge } from './header_badge'; -import { NavSetting, OnIsLockedUpdate } from './'; +import { OnIsLockedUpdate } from './'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; import { HeaderHelpMenu } from './header_help_menu'; import { HeaderNavControls } from './header_nav_controls'; @@ -69,7 +69,6 @@ export interface HeaderProps { navControlsRight$: Rx.Observable; basePath: HttpStart['basePath']; isLocked?: boolean; - navSetting$: Rx.Observable; onIsLockedUpdate?: OnIsLockedUpdate; } @@ -81,7 +80,6 @@ interface State { forceNavigation: boolean; navControlsLeft: readonly ChromeNavControl[]; navControlsRight: readonly ChromeNavControl[]; - navSetting: NavSetting; currentAppId: string | undefined; } @@ -100,7 +98,6 @@ export class Header extends Component { forceNavigation: false, navControlsLeft: [], navControlsRight: [], - navSetting: 'grouped', currentAppId: '', }; } @@ -116,8 +113,7 @@ export class Header extends Component { Rx.combineLatest( this.props.navControlsLeft$, this.props.navControlsRight$, - this.props.application.currentAppId$, - this.props.navSetting$ + this.props.application.currentAppId$ ) ).subscribe({ next: ([ @@ -126,7 +122,7 @@ export class Header extends Component { forceNavigation, navLinks, recentlyAccessed, - [navControlsLeft, navControlsRight, currentAppId, navSetting], + [navControlsLeft, navControlsRight, currentAppId], ]) => { this.setState({ appTitle, @@ -136,7 +132,6 @@ export class Header extends Component { recentlyAccessed, navControlsLeft, navControlsRight, - navSetting, currentAppId, }); }, @@ -225,7 +220,6 @@ export class Header extends Component { void; diff --git a/src/core/public/chrome/ui/header/nav_drawer.test.tsx b/src/core/public/chrome/ui/header/nav_drawer.test.tsx deleted file mode 100644 index 7272935b93a520..00000000000000 --- a/src/core/public/chrome/ui/header/nav_drawer.test.tsx +++ /dev/null @@ -1,103 +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 { cloneDeep } from 'lodash'; -import { mount } from 'enzyme'; -import React from 'react'; -import { NavSetting } from './'; -import { ChromeNavLink } from '../../../'; -import { AppCategory } from 'src/core/types'; -import { DEFAULT_APP_CATEGORIES } from '../../../../utils'; -import { NavDrawer } from './nav_drawer'; -import { euiNavLink } from './nav_link'; - -const { analyze, management, observability, security } = DEFAULT_APP_CATEGORIES; -const mockIBasePath = { - get: () => '/app', - prepend: () => '/app', - remove: () => '/app', -}; - -const getMockProps = (chromeNavLinks: ChromeNavLink[], navSetting: NavSetting = 'grouped') => ({ - navSetting, - navLinks: chromeNavLinks.map(link => - euiNavLink(link, true, undefined, mockIBasePath, () => Promise.resolve()) - ), - chromeNavLinks, - recentlyAccessedItems: [], - basePath: mockIBasePath, -}); - -const makeLink = (id: string, order: number, category?: AppCategory) => ({ - id, - category, - order, - title: id, - baseUrl: `http://localhost:5601/app/${id}`, - legacy: true, -}); - -const getMockChromeNavLink = () => - cloneDeep([ - makeLink('discover', 100, analyze), - makeLink('siem', 500, security), - makeLink('metrics', 600, observability), - makeLink('monitoring', 800, management), - makeLink('visualize', 200, analyze), - makeLink('dashboard', 300, analyze), - makeLink('canvas', 400, { label: 'customCategory' }), - makeLink('logs', 700, observability), - ]); - -describe('NavDrawer', () => { - describe('Advanced setting set to individual', () => { - it('renders individual items', () => { - const component = mount( - - ); - expect(component).toMatchSnapshot(); - }); - }); - describe('Advanced setting set to grouped', () => { - it('renders individual items if there are less than 7', () => { - const links = getMockChromeNavLink().slice(0, 5); - const component = mount(); - expect(component).toMatchSnapshot(); - }); - it('renders individual items if there is only 1 category', () => { - // management doesn't count as a category - const navLinks = [ - makeLink('discover', 100, analyze), - makeLink('siem', 500, analyze), - makeLink('metrics', 600, analyze), - makeLink('monitoring', 800, analyze), - makeLink('visualize', 200, analyze), - makeLink('dashboard', 300, management), - makeLink('canvas', 400, management), - makeLink('logs', 700, management), - ]; - const component = mount(); - expect(component).toMatchSnapshot(); - }); - it('renders grouped items', () => { - const component = mount(); - expect(component).toMatchSnapshot(); - }); - }); -}); diff --git a/src/core/public/chrome/ui/header/nav_drawer.tsx b/src/core/public/chrome/ui/header/nav_drawer.tsx index dbb68d5dd3901e..c57faec1e428d7 100644 --- a/src/core/public/chrome/ui/header/nav_drawer.tsx +++ b/src/core/public/chrome/ui/header/nav_drawer.tsx @@ -18,39 +18,16 @@ */ import React from 'react'; -import { groupBy, sortBy } from 'lodash'; import { i18n } from '@kbn/i18n'; // @ts-ignore import { EuiNavDrawer, EuiHorizontalRule, EuiNavDrawerGroup } from '@elastic/eui'; -import { NavSetting, OnIsLockedUpdate } from './'; +import { OnIsLockedUpdate } from './'; import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../../..'; -import { AppCategory } from '../../../../types'; import { HttpStart } from '../../../http'; import { NavLink } from './nav_link'; import { RecentLinks } from './recent_links'; -function getAllCategories(allCategorizedLinks: Record) { - const allCategories = {} as Record; - - for (const [key, value] of Object.entries(allCategorizedLinks)) { - allCategories[key] = value[0].category; - } - - return allCategories; -} - -function getOrderedCategories( - mainCategories: Record, - categoryDictionary: ReturnType -) { - return sortBy( - Object.keys(mainCategories), - categoryName => categoryDictionary[categoryName]?.order - ); -} - export interface Props { - navSetting: NavSetting; isLocked?: boolean; onIsLockedUpdate?: OnIsLockedUpdate; navLinks: NavLink[]; @@ -60,26 +37,9 @@ export interface Props { } function navDrawerRenderer( - { - navSetting, - isLocked, - onIsLockedUpdate, - navLinks, - chromeNavLinks, - recentlyAccessedItems, - basePath, - }: Props, + { isLocked, onIsLockedUpdate, navLinks, chromeNavLinks, recentlyAccessedItems, basePath }: Props, ref: React.Ref ) { - const disableGroupedNavSetting = navSetting === 'individual'; - const groupedNavLinks = groupBy(navLinks, link => link?.category?.label); - const { undefined: unknowns, ...allCategorizedLinks } = groupedNavLinks; - const { Management: management, ...mainCategories } = allCategorizedLinks; - const categoryDictionary = getAllCategories(allCategorizedLinks); - const orderedCategories = getOrderedCategories(mainCategories, categoryDictionary); - const showUngroupedNav = - disableGroupedNavSetting || navLinks.length < 7 || Object.keys(mainCategories).length === 1; - return ( - {showUngroupedNav ? ( - - ) : ( - <> - { - const category = categoryDictionary[categoryName]!; - const links = mainCategories[categoryName]; - - if (links.length === 1) { - return { - ...links[0], - label: category.label, - iconType: category.euiIconType || links[0].iconType, - }; - } - - return { - 'data-test-subj': 'navDrawerCategory', - iconType: category.euiIconType, - label: category.label, - flyoutMenu: { - title: category.label, - listItems: sortBy(links, 'order').map(link => { - link['data-test-subj'] = 'navDrawerFlyoutLink'; - return link; - }), - }, - }; - }), - ...sortBy(unknowns, 'order'), - ]} - /> - - { - link['data-test-subj'] = 'navDrawerFlyoutLink'; - return link; - }), - }, - }, - ]} - /> - - )} + ); } diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 8e6bae0b588bc0..221133a17d59ab 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -115,7 +115,7 @@ export default function(kibana) { { id: 'kibana:stack_management', title: i18n.translate('kbn.managementTitle', { - defaultMessage: 'Stack Management', + defaultMessage: 'Management', }), order: 9003, url: `${kbnBaseUrl}#/management`, diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js index b0d94711be7b62..db24cb3e3c1b7e 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js @@ -131,7 +131,7 @@ describe('home', () => { test('should not render directory entry when showOnHomePage is false', async () => { const directoryEntry = { id: 'stack-management', - title: 'Stack Management', + title: 'Management', description: 'Your center console for managing the Elastic Stack.', icon: 'managementApp', path: 'management_landing_page', diff --git a/src/legacy/core_plugins/kibana/public/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js index 2cba9fab7be222..6a36391c56b5c3 100644 --- a/src/legacy/core_plugins/kibana/public/management/index.js +++ b/src/legacy/core_plugins/kibana/public/management/index.js @@ -69,7 +69,7 @@ export function updateLandingPage(version) {

diff --git a/src/legacy/core_plugins/kibana/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/ui_setting_defaults.js index 744ede891b84a3..f92694eabe58d1 100644 --- a/src/legacy/core_plugins/kibana/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/ui_setting_defaults.js @@ -1174,24 +1174,5 @@ export function getUiSettingDefaults() { category: ['accessibility'], requiresPageReload: true, }, - pageNavigation: { - name: i18n.translate('kbn.advancedSettings.pageNavigationName', { - defaultMessage: 'Side nav style', - }), - value: 'grouped', - description: i18n.translate('kbn.advancedSettings.pageNavigationDesc', { - defaultMessage: 'Change the style of navigation', - }), - type: 'select', - options: ['grouped', 'individual'], - optionLabels: { - grouped: i18n.translate('kbn.advancedSettings.pageNavigationGrouped', { - defaultMessage: 'Grouped', - }), - individual: i18n.translate('kbn.advancedSettings.pageNavigationIndividual', { - defaultMessage: 'Individual', - }), - }, - }, }; } diff --git a/src/legacy/ui/public/management/breadcrumbs.ts b/src/legacy/ui/public/management/breadcrumbs.ts index 936e99caff565f..e6156b6639ac47 100644 --- a/src/legacy/ui/public/management/breadcrumbs.ts +++ b/src/legacy/ui/public/management/breadcrumbs.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; export const MANAGEMENT_BREADCRUMB = Object.freeze({ text: i18n.translate('common.ui.stackManagement.breadcrumb', { - defaultMessage: 'Stack Management', + defaultMessage: 'Management', }), href: '#/management', }); diff --git a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx index 69ba813d2347ed..01a98eb0ddb1f8 100644 --- a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx +++ b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx @@ -168,8 +168,7 @@ export class ManagementSidebarNav extends React.Component<

{i18n.translate('management.nav.label', { - // todo - defaultMessage: 'Stack Management', + defaultMessage: 'Management', })}

diff --git a/src/plugins/management/public/legacy/sections_register.js b/src/plugins/management/public/legacy/sections_register.js index ca35db56c340be..63d919377f89ea 100644 --- a/src/plugins/management/public/legacy/sections_register.js +++ b/src/plugins/management/public/legacy/sections_register.js @@ -27,8 +27,7 @@ export class LegacyManagementAdapter { 'management', { display: i18n.translate('management.displayName', { - // todo - defaultMessage: 'Stack Management', + defaultMessage: 'Management', }), }, capabilities @@ -36,7 +35,6 @@ export class LegacyManagementAdapter { this.main.register('data', { display: i18n.translate('management.connectDataDisplayName', { - // todo defaultMessage: 'Connect Data', }), order: 0, diff --git a/src/plugins/management/public/management_app.tsx b/src/plugins/management/public/management_app.tsx index 02b3ea306c23d8..705d98eaaf2ffd 100644 --- a/src/plugins/management/public/management_app.tsx +++ b/src/plugins/management/public/management_app.tsx @@ -64,8 +64,7 @@ export class ManagementApp { coreStart.chrome.setBreadcrumbs([ { text: i18n.translate('management.breadcrumb', { - // todo - defaultMessage: 'Stack Management', + defaultMessage: 'Management', }), href: '#/management', }, diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index df2398412dac25..1c9e1d5c895509 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -36,8 +36,8 @@ export class ManagementPlugin implements Plugin { - before(async () => { - await PageObjects.settings.setNavType('individual'); - }); - beforeEach(async () => { await PageObjects.common.navigateToApp('app_status_start'); }); diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts index 6567837f653095..f50d4605325560 100644 --- a/test/plugin_functional/test_suites/core_plugins/applications.ts +++ b/test/plugin_functional/test_suites/core_plugins/applications.ts @@ -122,7 +122,7 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider }); it('can navigate from NP apps to legacy apps', async () => { - await appsMenu.clickLink('Stack Management'); + await appsMenu.clickLink('Management'); await loadingScreenShown(); await testSubjects.existOrFail('managementNav'); }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b171863f26c21c..47bf2ae634048e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1449,7 +1449,6 @@ "kbn.management.indexPatterns.listBreadcrumb": "インデックスパターン", "kbn.management.indexPatternTable.createBtn": "インデックスパターンの作成", "kbn.management.indexPatternTable.title": "インデックスパターン", - "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "インデックス、インデックスパターン、保存されたオブジェクト、Kibana の設定、その他を管理します。", "kbn.management.landing.text": "すべてのツールの一覧は、左のメニューにあります。", "kbn.management.objects.confirmModalOptions.deleteButtonLabel": "削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 050e9bd40f58df..a94a602e48d9ba 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1449,7 +1449,6 @@ "kbn.management.indexPatterns.listBreadcrumb": "索引模式", "kbn.management.indexPatternTable.createBtn": "创建索引模式", "kbn.management.indexPatternTable.title": "索引模式", - "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "管理您的索引、索引模式、已保存对象、Kibana 设置等等。", "kbn.management.landing.text": "在左侧菜单中可找到完整工具列表", "kbn.management.objects.confirmModalOptions.deleteButtonLabel": "删除", diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index 261a8533876195..0d4c6b2c876666 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -55,7 +55,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { expectSpaceSelector: false, } ); - await kibanaServer.uiSettings.replace({ pageNavigation: 'individual' }); + await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); }); @@ -69,7 +69,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Stack Management']); + expect(navLinks).to.eql(['Management']); }); it(`allows settings to be changed`, async () => { @@ -125,7 +125,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Stack Management']); + expect(navLinks).to.eql(['Management']); }); it(`does not allow settings to be changed`, async () => { @@ -176,7 +176,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover', 'Management']); }); it(`does not allow navigation to advanced settings; redirects to management home`, async () => { diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts index 53202089e8961c..fc4f385df36942 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -41,9 +41,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Stack Management'); + expect(navLinks).to.contain('Management'); }); it(`allows settings to be changed`, async () => { diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts index 7c9c9f9c8c155a..e2d5efac4644cc 100644 --- a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts @@ -60,7 +60,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows apm navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['APM', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['APM', 'Management']); }); it('can navigate to APM app', async () => { @@ -109,7 +109,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows apm navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['APM', 'Stack Management']); + expect(navLinks).to.eql(['APM', 'Management']); }); it('can navigate to APM app', async () => { diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts index 474240b201face..1ac1784e0e05db 100644 --- a/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security', 'settings']); + const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -30,7 +30,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('APM'); }); diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts index 71c10bd8248be8..d0e37ec8e3f359 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -66,7 +66,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows canvas navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Canvas', 'Stack Management']); + expect(navLinks).to.eql(['Canvas', 'Management']); }); it(`landing page shows "Create new workpad" button`, async () => { @@ -142,7 +142,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows canvas navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Canvas', 'Stack Management']); + expect(navLinks).to.eql(['Canvas', 'Management']); }); it(`landing page shows disabled "Create new workpad" button`, async () => { diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts index 5395f125bbd22b..28b572401892b5 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector', 'settings']); + const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); describe('spaces feature controls', function() { @@ -40,7 +40,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Canvas'); }); diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts index bfffefaecd94ce..b966d37becc3f0 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts @@ -77,7 +77,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows dashboard navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Dashboard', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['Dashboard', 'Management']); }); it(`landing page shows "Create new Dashboard" button`, async () => { @@ -261,7 +261,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows dashboard navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Dashboard', 'Stack Management']); + expect(navLinks).to.eql(['Dashboard', 'Management']); }); it(`landing page doesn't show "Create new Dashboard" button`, async () => { diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts index 1f4f0f33a061e6..5ab26e41890965 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts @@ -14,13 +14,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const config = getService('config'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'dashboard', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); @@ -50,7 +44,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Dashboard'); }); diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js index b521c47585d58b..b9c0b0095b96b7 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js @@ -37,10 +37,7 @@ export default function({ getService, getPageObjects }) { log.debug('Dashboard View Mode:initTests'); await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('dashboard_view_mode'); - await kibanaServer.uiSettings.replace({ - defaultIndex: 'logstash-*', - pageNavigation: 'individual', - }); + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); await browser.setWindowSize(1600, 1000); await PageObjects.common.navigateToApp('discover'); @@ -200,7 +197,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.security.forceLogout(); await PageObjects.security.login('mixeduser', '123456'); - if (await appsMenu.linkExists('Stack Management')) { + if (await appsMenu.linkExists('Management')) { throw new Error('Expected management nav link to not be shown'); } }); @@ -209,7 +206,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.security.forceLogout(); await PageObjects.security.login('mysuperuser', '123456'); - if (!(await appsMenu.linkExists('Stack Management'))) { + if (!(await appsMenu.linkExists('Management'))) { throw new Error('Expected management nav link to be shown'); } }); diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts index 162bf23c294906..3d17d235b7f4fe 100644 --- a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts @@ -64,7 +64,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Dev Tools navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Dev Tools', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['Dev Tools', 'Management']); }); describe('console', () => { @@ -145,7 +145,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it(`shows 'Dev Tools' navlink`, async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Dev Tools', 'Stack Management']); + expect(navLinks).to.eql(['Dev Tools', 'Management']); }); describe('console', () => { diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts index 561b7f64eb77d6..d1eddfe89c59ed 100644 --- a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts @@ -10,13 +10,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const config = getService('config'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'dashboard', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); const grokDebugger = getService('grokDebugger'); @@ -47,7 +41,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Dev Tools'); }); diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 53d0872b810fe1..87ae5231d10312 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -83,7 +83,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows discover navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Discover', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['Discover', 'Management']); }); it('shows save button', async () => { @@ -170,7 +170,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows discover navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover', 'Management']); }); it(`doesn't show save button`, async () => { diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index ba7d4077b27409..4bedc757f0b575 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -16,7 +16,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { 'timePicker', 'security', 'spaceSelector', - 'settings', ]); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -51,7 +50,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Discover'); }); diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts index 37de93a0a7e910..a2b062e6ef84fb 100644 --- a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts @@ -64,7 +64,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows graph navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Graph', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['Graph', 'Management']); }); it('landing page shows "Create new graph" button', async () => { @@ -127,7 +127,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows graph navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Graph', 'Stack Management']); + expect(navLinks).to.eql(['Graph', 'Management']); }); it('does not show a "Create new Workspace" button', async () => { diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts index d0d0232b5a8b14..a0b0d5bef96680 100644 --- a/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'graph', 'security', 'error', 'settings']); + const PageObjects = getPageObjects(['common', 'graph', 'security', 'error']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -34,7 +34,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Graph'); }); diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts index 0783767d8f1521..d72c9b970204a4 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts @@ -71,7 +71,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Stack Management']); + expect(navLinks).to.eql(['Management']); }); it(`index pattern listing shows create button`, async () => { @@ -114,7 +114,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { } ); - await kibanaServer.uiSettings.replace({ pageNavigation: 'individual' }); + await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); }); @@ -125,7 +125,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Stack Management']); + expect(navLinks).to.eql(['Management']); }); it(`index pattern listing doesn't show create button`, async () => { @@ -177,7 +177,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover', 'Management']); }); it(`doesn't show Index Patterns in management side-nav`, async () => { diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts index d4422e94d10cf5..7d9bee37bbbc4e 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts @@ -41,9 +41,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Stack Management'); + expect(navLinks).to.contain('Management'); }); it(`index pattern listing shows create button`, async () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts index ede77b7d9afa7f..bf35d4dc06aa2b 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts @@ -61,7 +61,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows metrics navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Metrics', 'Stack Management']); + expect(navLinks).to.eql(['Metrics', 'Management']); }); describe('infrastructure landing page without data', () => { @@ -177,7 +177,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows metrics navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Metrics', 'Stack Management']); + expect(navLinks).to.eql(['Metrics', 'Management']); }); describe('infrastructure landing page without data', () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts index 3bbcc1aa7043c0..37056c7f17ca11 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts @@ -9,13 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'infraHome', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'infraHome', 'security', 'spaceSelector']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -47,7 +41,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Metrics'); }); diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts index 48ad4e90fd413f..e5a6e27a0fadbe 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts @@ -58,7 +58,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Logs', 'Stack Management']); + expect(navLinks).to.eql(['Logs', 'Management']); }); describe('logs landing page without data', () => { @@ -121,7 +121,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Logs', 'Stack Management']); + expect(navLinks).to.eql(['Logs', 'Management']); }); describe('logs landing page without data', () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts index 0094d227514c00..985131113c5350 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts @@ -9,13 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'infraHome', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'infraHome', 'security', 'spaceSelector']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -42,7 +36,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Logs'); }); diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts index c25c1bfe4b7318..8fb6f21c778d3f 100644 --- a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts @@ -10,7 +10,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const security = getService('security'); const appsMenu = getService('appsMenu'); - const PageObjects = getPageObjects(['common', 'security', 'settings']); + const PageObjects = getPageObjects(['common', 'security']); describe('security', () => { before(async () => { @@ -94,7 +94,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); await PageObjects.security.login('machine_learning_user', 'machine_learning_user-password'); - await PageObjects.settings.setNavType('individual'); }); after(async () => { diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts index c633852a2da0a7..fc94688e98811b 100644 --- a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error', 'settings']); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error']); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); @@ -39,7 +39,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Machine Learning'); }); diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts index ece162cbd96cc7..804ad5725edfd3 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts @@ -66,7 +66,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows maps navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Maps', 'Stack Management']); + expect(navLinks).to.eql(['Maps', 'Management']); }); it(`allows a map to be created`, async () => { @@ -153,7 +153,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Maps navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Maps', 'Stack Management']); + expect(navLinks).to.eql(['Maps', 'Management']); }); it(`does not show create new button`, async () => { @@ -248,7 +248,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('does not show Maps navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover', 'Management']); }); it(`returns a 404`, async () => { diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts index 130aefb3cae2ac..d985da42ab5eda 100644 --- a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts @@ -10,7 +10,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const security = getService('security'); const appsMenu = getService('appsMenu'); - const PageObjects = getPageObjects(['common', 'security', 'settings']); + const PageObjects = getPageObjects(['common', 'security']); describe('security', () => { before(async () => { @@ -97,7 +97,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows monitoring navlink', async () => { - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Stack Monitoring'); }); diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts index 0465cbcf54541c..9e306b074d2140 100644 --- a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error', 'settings']); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error']); const appsMenu = getService('appsMenu'); const find = getService('find'); @@ -41,7 +41,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Stack Monitoring'); }); diff --git a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts index 9ca314ba5ec183..49b684a37079e8 100644 --- a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts +++ b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts @@ -16,7 +16,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { describe('security feature controls', () => { before(async () => { await esArchiver.load('empty_kibana'); - await PageObjects.settings.setNavType('individual'); }); after(async () => { @@ -57,7 +56,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Stack Management'); + expect(navLinks).to.contain('Management'); }); it(`displays Spaces management section`, async () => { @@ -135,7 +134,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Stack Management'); + expect(navLinks).to.contain('Management'); }); it(`doesn't display Spaces management section`, async () => { diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts index 62483a10552e34..dea45f161e4510 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts @@ -60,7 +60,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows timelion navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Timelion', 'Stack Management']); + expect(navLinks).to.eql(['Timelion', 'Management']); }); it(`allows a timelion sheet to be created`, async () => { @@ -112,7 +112,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows timelion navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Timelion', 'Stack Management']); + expect(navLinks).to.eql(['Timelion', 'Management']); }); it(`does not allow a timelion sheet to be created`, async () => { diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts index 7e0fe731301a64..fb203a23359bdd 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts @@ -9,13 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'timelion', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'timelion', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); describe('timelion', () => { @@ -44,7 +38,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Timelion'); }); diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts index 4ff82484db91c4..a004f8db66823c 100644 --- a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts @@ -64,7 +64,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows uptime navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Uptime', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['Uptime', 'Management']); }); it('can navigate to Uptime app', async () => { @@ -115,7 +115,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows uptime navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Uptime', 'Stack Management']); + expect(navLinks).to.eql(['Uptime', 'Management']); }); it('can navigate to Uptime app', async () => { diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts index c3dcb1b27771fb..77c5b323340bf8 100644 --- a/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security', 'settings']); + const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -30,7 +30,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Uptime'); }); diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index 1876f460383263..e5b6512d1c1b07 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -76,7 +76,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Visualize', 'Stack Management']); + expect(navLinks).to.eql(['Visualize', 'Management']); }); it(`landing page shows "Create new Visualization" button`, async () => { @@ -200,7 +200,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Visualize', 'Stack Management']); + expect(navLinks).to.eql(['Visualize', 'Management']); }); it(`landing page shows "Create new Visualization" button`, async () => { diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts index b1cb156caad90d..4f12dd16247f62 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts @@ -11,13 +11,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const config = getService('config'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'visualize', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'visualize', 'security', 'spaceSelector']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -47,7 +41,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Visualize'); }); From 3cba05128e21772307bce491cebbedcd34ff6b2b Mon Sep 17 00:00:00 2001 From: Maggie Ghamry <46542915+maggieghamry@users.noreply.github.com> Date: Thu, 20 Feb 2020 10:12:19 -0500 Subject: [PATCH 09/43] [Canvas] Adds argument to open all links in new tab within markdown element (#57017) * Adds toggle to open links in new tab within markdown element * Updating test for markdown function * Switch to using Markdown Component * Update ui.ts Update to change toggle verbiage per Ryan's request, and reuse the Kibana Markdown per Corey's help and recommendation. Still working on updating the styles (consulting Ryan) * Update toggle.js Update to prevent text for "Open links in new tab from wrapping" - the example from the horizontal bar chart is quite differently, and reads from "axisConfig" - when I changed the argType to "axisConfig", the layout was the same, but I'll need some input on which specific styles to add to the "ToggleArgInput" - I think this is where the style updates need to occur to get the toggle to stay on the same line, but may be wrong. * Update ui.ts Update to original message string per Ryan's feedback, now that there is no wrapping * Update to UI styles per Ryan's feedback * updating message per Ryan's request * Update ui.ts update to fix internationalization issues Co-authored-by: Corey Robertson --- .../functions/browser/markdown.test.js | 29 ++++++++++++++++- .../functions/browser/markdown.ts | 8 +++++ .../renderers/markdown/index.js | 14 +++------ .../canvas_plugin_src/uis/arguments/toggle.js | 31 +++++++++++-------- .../canvas_plugin_src/uis/views/markdown.js | 11 +++++++ .../canvas/i18n/functions/dict/markdown.ts | 4 +++ x-pack/legacy/plugins/canvas/i18n/ui.ts | 12 +++++++ 7 files changed, 86 insertions(+), 23 deletions(-) diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.test.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.test.js index f9912c270a46a0..27ea290fb4dccd 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.test.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.test.js @@ -19,7 +19,7 @@ describe('markdown', () => { }); describe('args', () => { - describe('expression', () => { + describe('content', () => { it('sets the content to all strings in expression concatenated', () => { const result = fn(null, { content: ['# this ', 'is ', 'some ', 'markdown'], @@ -54,5 +54,32 @@ describe('markdown', () => { // TODO: write test when using an instance of the interpreter // it("defaults to the expression '{font}'", () => {}); }); + describe('openLinksInNewTab', () => { + it('sets the value of openLinksInNewTab to true ', () => { + const result = fn(null, { + content: ['some ', 'markdown'], + openLinksInNewTab: true, + }); + + expect(result.value).toHaveProperty('openLinksInNewTab', true); + }); + + it('sets the value of openLinksInNewTab to false ', () => { + const result = fn(null, { + content: ['some ', 'markdown'], + openLinksInNewTab: false, + }); + + expect(result.value).toHaveProperty('openLinksInNewTab', false); + }); + + it('defaults the value of openLinksInNewTab to false ', () => { + const result = fn(null, { + content: ['some ', 'markdown'], + }); + + expect(result.value).toHaveProperty('openLinksInNewTab', false); + }); + }); }); }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts index 95859feeed5f3e..e94b9f201a1746 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts @@ -19,11 +19,13 @@ type Context = Datatable | null; interface Arguments { content: string[]; font: Style; + openLinksInNewTab: boolean; } interface Return { content: string; font: Style; + openLinksInNewTab: boolean; } export function markdown(): ExpressionFunctionDefinition< @@ -53,6 +55,11 @@ export function markdown(): ExpressionFunctionDefinition< help: argHelp.font, default: '{font}', }, + openLinksInNewTab: { + types: ['boolean'], + help: argHelp.openLinksInNewTab, + default: false, + }, }, fn: (input, args) => { const compileFunctions = args.content.map(str => @@ -71,6 +78,7 @@ export function markdown(): ExpressionFunctionDefinition< value: { content: compileFunctions.map(fn => fn(ctx)).join(''), font: args.font, + openLinksInNewTab: args.openLinksInNewTab, }, }; }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/markdown/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/markdown/index.js index 82c63d5e7d5295..c1bfd7c99ac413 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/markdown/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/markdown/index.js @@ -6,33 +6,29 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import Markdown from 'markdown-it'; import { RendererStrings } from '../../../i18n'; +import { Markdown } from '../../../../../../../src/legacy/core_plugins/kibana_react/public'; const { markdown: strings } = RendererStrings; -const md = new Markdown(); - export const markdown = () => ({ name: 'markdown', displayName: strings.getDisplayName(), help: strings.getHelpDescription(), reuseDomNode: true, render(domNode, config, handlers) { - const html = { __html: md.render(String(config.content)) }; const fontStyle = config.font ? config.font.spec : {}; - /* eslint-disable react/no-danger */ ReactDOM.render( -
, domNode, () => handlers.done() ); - /* eslint-enable */ handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js index bcad4678e0b6a8..299f96ff1b4e8a 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js @@ -19,18 +19,21 @@ const ToggleArgInput = ({ onValueChange, argValue, argId, renderError, typeInsta return null; } return ( - - - +
+ + + +
); }; @@ -38,6 +41,8 @@ ToggleArgInput.propTypes = { onValueChange: PropTypes.func.isRequired, argValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.string, PropTypes.object]).isRequired, argId: PropTypes.string.isRequired, + labelValue: PropTypes.string, + showLabelValue: PropTypes.bool, renderError: PropTypes.func.isRequired, }; @@ -45,6 +50,6 @@ export const toggle = () => ({ name: 'toggle', displayName: strings.getDisplayName(), help: strings.getHelp(), - simpleTemplate: templateFromReactComponent(ToggleArgInput), + template: templateFromReactComponent(ToggleArgInput), default: 'false', }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/markdown.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/markdown.js index 1c46bc6dd57c2a..edae739ee0d3da 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/markdown.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/markdown.js @@ -29,5 +29,16 @@ export const markdown = () => ({ name: 'font', argType: 'font', }, + { + name: 'openLinksInNewTab', + displayName: strings.getOpenLinksInNewTabDisplayName(), + help: strings.getOpenLinksInNewTabHelp(), + label: strings.getOpenLinksInNewTabLabelName(), + argType: 'toggle', + default: false, + options: { + labelValue: 'Open all links in a new tab', + }, + }, ], }); diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/dict/markdown.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/markdown.ts index d5271e14436e23..aa2845ba4ec3a0 100644 --- a/x-pack/legacy/plugins/canvas/i18n/functions/dict/markdown.ts +++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/markdown.ts @@ -37,5 +37,9 @@ export const help: FunctionHelp> = { fontWeight: 'font-weight', }, }), + openLinksInNewTab: i18n.translate('xpack.canvas.functions.markdown.args.openLinkHelpText', { + defaultMessage: + 'A true/false value for opening links in a new tab. Default value is false. Setting to true will open all links in a new tab.', + }), }, }; diff --git a/x-pack/legacy/plugins/canvas/i18n/ui.ts b/x-pack/legacy/plugins/canvas/i18n/ui.ts index 323a6c97fd967d..5b94cb0435b31f 100644 --- a/x-pack/legacy/plugins/canvas/i18n/ui.ts +++ b/x-pack/legacy/plugins/canvas/i18n/ui.ts @@ -628,6 +628,18 @@ export const ViewStrings = { markdown: MARKDOWN, }, }), + getOpenLinksInNewTabDisplayName: () => + i18n.translate('xpack.canvas.uis.views.openLinksInNewTabTitle', { + defaultMessage: 'Markdown link settings', + }), + getOpenLinksInNewTabLabelName: () => + i18n.translate('xpack.canvas.uis.views.openLinksInNewTabLabel', { + defaultMessage: 'Open all links in a new tab', + }), + getOpenLinksInNewTabHelp: () => + i18n.translate('xpack.canvas.uis.views.openLinksInNewTabHelpLabel', { + defaultMessage: 'Set links to open in new tab', + }), }, Metric: { getDisplayName: () => From 602aca338780c20269e59d113d5d63f6840b2797 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 20 Feb 2020 10:39:43 -0500 Subject: [PATCH 10/43] [SIEM] Detections container/rules unit tests (#58055) * add unit test for rules api * add unit test for useFetchIndexPatterns * fix useFetchIndexPatterns and add unit test for usePersistRule * add more unit test for container/rules * review Co-authored-by: Elastic Machine --- .../detection_engine/rules/__mocks__/api.ts | 81 ++ .../detection_engine/rules/api.test.ts | 860 ++++++++++++++++++ .../rules/fetch_index_patterns.test.tsx | 460 ++++++++++ .../rules/fetch_index_patterns.tsx | 2 +- .../containers/detection_engine/rules/mock.ts | 139 +++ .../rules/persist_rule.test.tsx | 44 + .../detection_engine/rules/persist_rule.tsx | 5 +- .../detection_engine/rules/types.ts | 2 +- .../rules/use_pre_packaged_rules.test.tsx | 267 ++++++ .../rules/use_pre_packaged_rules.tsx | 8 +- .../detection_engine/rules/use_rule.test.tsx | 84 ++ .../detection_engine/rules/use_rule.tsx | 5 +- .../rules/use_rule_status.test.tsx | 65 ++ .../rules/use_rule_status.tsx | 5 +- .../detection_engine/rules/use_rules.test.tsx | 216 +++++ .../detection_engine/rules/use_rules.tsx | 7 +- .../detection_engine/rules/use_tags.test.tsx | 29 + .../detection_engine/rules/use_tags.tsx | 4 +- .../plugins/siem/public/mock/hook_wrapper.tsx | 1 + 19 files changed, 2267 insertions(+), 17 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/mock.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.ts new file mode 100644 index 00000000000000..9f37f3fecd5082 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.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 { + AddRulesProps, + NewRule, + PrePackagedRulesStatusResponse, + BasicFetchProps, + RuleStatusResponse, + Rule, + FetchRuleProps, + FetchRulesResponse, + FetchRulesProps, +} from '../types'; +import { ruleMock, savedRuleMock, rulesMock } from '../mock'; + +export const addRule = async ({ rule, signal }: AddRulesProps): Promise => + Promise.resolve(ruleMock); + +export const getPrePackagedRulesStatus = async ({ + signal, +}: { + signal: AbortSignal; +}): Promise => + Promise.resolve({ + rules_custom_installed: 33, + rules_installed: 12, + rules_not_installed: 0, + rules_not_updated: 0, + }); + +export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise => + Promise.resolve(true); + +export const getRuleStatusById = async ({ + id, + signal, +}: { + id: string; + signal: AbortSignal; +}): Promise => + Promise.resolve({ + myOwnRuleID: { + current_status: { + alert_id: 'alertId', + status_date: 'mm/dd/yyyyTHH:MM:sssz', + status: 'succeeded', + last_failure_at: null, + last_success_at: 'mm/dd/yyyyTHH:MM:sssz', + last_failure_message: null, + last_success_message: 'it is a success', + }, + failures: [], + }, + }); + +export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise => + Promise.resolve(savedRuleMock); + +export const fetchRules = async ({ + filterOptions = { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: false, + showElasticRules: false, + tags: [], + }, + pagination = { + page: 1, + perPage: 20, + total: 0, + }, + signal, +}: FetchRulesProps): Promise => Promise.resolve(rulesMock); + +export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise => + Promise.resolve(['elastic', 'love', 'quality', 'code']); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts new file mode 100644 index 00000000000000..b348678e789f88 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts @@ -0,0 +1,860 @@ +/* + * 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 { KibanaServices } from '../../../lib/kibana'; +import { + addRule, + fetchRules, + fetchRuleById, + enableRules, + deleteRules, + duplicateRules, + createPrepackagedRules, + importRules, + exportRules, + getRuleStatusById, + fetchTags, + getPrePackagedRulesStatus, +} from './api'; +import { ruleMock, rulesMock } from './mock'; +import { ToasterErrors } from '../../../components/ml/api/throw_if_not_ok'; + +const abortCtrl = new AbortController(); +const mockKibanaServices = KibanaServices.get as jest.Mock; +jest.mock('../../../lib/kibana'); + +const mockfetchSuccess = (body: unknown, fetchMock?: jest.Mock) => { + if (fetchMock) { + mockKibanaServices.mockImplementation(() => ({ + http: { + fetch: fetchMock, + }, + })); + } else { + mockKibanaServices.mockImplementation(() => ({ + http: { + fetch: () => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body, + }), + }, + })); + } +}; + +const mockfetchError = () => { + mockKibanaServices.mockImplementation(() => ({ + http: { + fetch: () => ({ + response: { + ok: false, + text: () => + JSON.stringify({ + message: 'super mega error, it is not that bad', + }), + }, + body: null, + }), + }, + })); +}; + +describe('Detections Rules API', () => { + const fetchMock = jest.fn(); + describe('addRule', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url, body', async () => { + mockfetchSuccess(null, fetchMock); + + await addRule({ rule: ruleMock, signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { + asResponse: true, + body: + '{"description":"some desc","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf","language":"kuery","risk_score":75,"name":"Test rule","query":"user.email: \'root@elastic.co\'","references":[],"severity":"high","tags":["APM"],"to":"now","type":"query","threat":[]}', + method: 'POST', + signal: abortCtrl.signal, + }); + }); + test('happy path', async () => { + mockfetchSuccess(ruleMock); + const ruleResp = await addRule({ rule: ruleMock, signal: abortCtrl.signal }); + expect(ruleResp).toEqual(ruleMock); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await addRule({ rule: ruleMock, signal: abortCtrl.signal }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('fetchRules', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: rulesMock, + })); + }); + + test('check parameter url, query without any options', async () => { + mockfetchSuccess(null, fetchMock); + + await fetchRules({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + asResponse: true, + method: 'GET', + query: { + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('check parameter url, query with a filter', async () => { + mockfetchSuccess(null, fetchMock); + + await fetchRules({ + filterOptions: { + filter: 'hello world', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: false, + showElasticRules: false, + tags: [], + }, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + asResponse: true, + method: 'GET', + query: { + filter: 'alert.attributes.name: hello world', + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('check parameter url, query with showCustomRules', async () => { + mockfetchSuccess(null, fetchMock); + + await fetchRules({ + filterOptions: { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: true, + showElasticRules: false, + tags: [], + }, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + asResponse: true, + method: 'GET', + query: { + filter: 'alert.attributes.tags: "__internal_immutable:false"', + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('check parameter url, query with showElasticRules', async () => { + mockfetchSuccess(null, fetchMock); + + await fetchRules({ + filterOptions: { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: false, + showElasticRules: true, + tags: [], + }, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + asResponse: true, + method: 'GET', + query: { + filter: 'alert.attributes.tags: "__internal_immutable:true"', + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('check parameter url, query with tags', async () => { + mockfetchSuccess(null, fetchMock); + + await fetchRules({ + filterOptions: { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: false, + showElasticRules: false, + tags: ['hello', 'world'], + }, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + asResponse: true, + method: 'GET', + query: { + filter: 'alert.attributes.tags: hello AND alert.attributes.tags: world', + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('check parameter url, query with all options', async () => { + mockfetchSuccess(null, fetchMock); + + await fetchRules({ + filterOptions: { + filter: 'ruleName', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: true, + showElasticRules: true, + tags: ['hello', 'world'], + }, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + asResponse: true, + method: 'GET', + query: { + filter: + 'alert.attributes.name: ruleName AND alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags: hello AND alert.attributes.tags: world', + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + mockfetchSuccess(rulesMock); + + const rulesResp = await fetchRules({ signal: abortCtrl.signal }); + expect(rulesResp).toEqual(rulesMock); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await fetchRules({ signal: abortCtrl.signal }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('fetchRuleById', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url, query', async () => { + mockfetchSuccess(null, fetchMock); + + await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { + asResponse: true, + query: { + id: 'mySuperRuleId', + }, + method: 'GET', + signal: abortCtrl.signal, + }); + }); + test('happy path', async () => { + mockfetchSuccess(ruleMock); + const ruleResp = await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); + expect(ruleResp).toEqual(ruleMock); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('enableRules', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url, body when enabling rules', async () => { + mockfetchSuccess(null, fetchMock); + + await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: true }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_update', { + asResponse: true, + body: '[{"id":"mySuperRuleId","enabled":true},{"id":"mySuperRuleId_II","enabled":true}]', + method: 'PATCH', + }); + }); + test('check parameter url, body when disabling rules', async () => { + mockfetchSuccess(null, fetchMock); + + await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: false }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_update', { + asResponse: true, + body: '[{"id":"mySuperRuleId","enabled":false},{"id":"mySuperRuleId_II","enabled":false}]', + method: 'PATCH', + }); + }); + test('happy path', async () => { + mockfetchSuccess(rulesMock.data); + const ruleResp = await enableRules({ + ids: ['mySuperRuleId', 'mySuperRuleId_II'], + enabled: true, + }); + expect(ruleResp).toEqual(rulesMock.data); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: true }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('deleteRules', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url, body when deleting rules', async () => { + mockfetchSuccess(null, fetchMock); + + await deleteRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'] }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_delete', { + asResponse: true, + body: '[{"id":"mySuperRuleId"},{"id":"mySuperRuleId_II"}]', + method: 'DELETE', + }); + }); + test('happy path', async () => { + mockfetchSuccess(ruleMock); + const ruleResp = await deleteRules({ + ids: ['mySuperRuleId', 'mySuperRuleId_II'], + }); + expect(ruleResp).toEqual(ruleMock); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await deleteRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'] }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('duplicateRules', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url, body when duplicating rules', async () => { + mockfetchSuccess(null, fetchMock); + + await duplicateRules({ rules: rulesMock.data }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_create', { + asResponse: true, + body: + '[{"description":"Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":73,"name":"Credential Dumping - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection","filters":[],"references":[],"severity":"high","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"version":1},{"description":"Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":47,"name":"Adversary Behavior - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:rules_engine_event","filters":[],"references":[],"severity":"medium","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"version":1}]', + method: 'POST', + }); + }); + test('happy path', async () => { + mockfetchSuccess(rulesMock.data); + const ruleResp = await duplicateRules({ rules: rulesMock.data }); + expect(ruleResp).toEqual(rulesMock.data); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await duplicateRules({ rules: rulesMock.data }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('createPrepackagedRules', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url when creating pre-packaged rules', async () => { + mockfetchSuccess(null, fetchMock); + + await createPrepackagedRules({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/prepackaged', { + asResponse: true, + signal: abortCtrl.signal, + method: 'PUT', + }); + }); + test('happy path', async () => { + mockfetchSuccess(true); + const resp = await createPrepackagedRules({ signal: abortCtrl.signal }); + expect(resp).toEqual(true); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await createPrepackagedRules({ signal: abortCtrl.signal }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('importRules', () => { + const fileToImport: File = { + lastModified: 33, + name: 'fileToImport', + size: 89, + type: 'json', + arrayBuffer: jest.fn(), + slice: jest.fn(), + stream: jest.fn(), + text: jest.fn(), + } as File; + const formData = new FormData(); + formData.append('file', fileToImport); + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url, body and query when importing rules', async () => { + mockfetchSuccess(null, fetchMock); + await importRules({ fileToImport, signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_import', { + asResponse: true, + signal: abortCtrl.signal, + method: 'POST', + body: formData, + headers: { + 'Content-Type': undefined, + }, + query: { + overwrite: false, + }, + }); + }); + + test('check parameter url, body and query when importing rules with overwrite', async () => { + mockfetchSuccess(null, fetchMock); + + await importRules({ fileToImport, overwrite: true, signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_import', { + asResponse: true, + signal: abortCtrl.signal, + method: 'POST', + body: formData, + headers: { + 'Content-Type': undefined, + }, + query: { + overwrite: true, + }, + }); + }); + + test('happy path', async () => { + mockfetchSuccess({ + success: true, + success_count: 33, + errors: [], + }); + const resp = await importRules({ fileToImport, signal: abortCtrl.signal }); + expect(resp).toEqual({ + success: true, + success_count: 33, + errors: [], + }); + }); + + test('unhappy path', async () => { + mockfetchError(); + try { + await importRules({ fileToImport, signal: abortCtrl.signal }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('exportRules', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + + test('check parameter url, body and query when exporting rules', async () => { + mockfetchSuccess(null, fetchMock); + await exportRules({ + ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'], + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { + asResponse: true, + signal: abortCtrl.signal, + method: 'POST', + body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', + query: { + exclude_export_details: false, + file_name: 'rules_export.ndjson', + }, + }); + }); + + test('check parameter url, body and query when exporting rules with excludeExportDetails', async () => { + mockfetchSuccess(null, fetchMock); + await exportRules({ + excludeExportDetails: true, + ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'], + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { + asResponse: true, + signal: abortCtrl.signal, + method: 'POST', + body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', + query: { + exclude_export_details: true, + file_name: 'rules_export.ndjson', + }, + }); + }); + + test('check parameter url, body and query when exporting rules with fileName', async () => { + mockfetchSuccess(null, fetchMock); + await exportRules({ + filename: 'myFileName.ndjson', + ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'], + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { + asResponse: true, + signal: abortCtrl.signal, + method: 'POST', + body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', + query: { + exclude_export_details: false, + file_name: 'myFileName.ndjson', + }, + }); + }); + + test('check parameter url, body and query when exporting rules with all options', async () => { + mockfetchSuccess(null, fetchMock); + await exportRules({ + excludeExportDetails: true, + filename: 'myFileName.ndjson', + ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'], + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { + asResponse: true, + signal: abortCtrl.signal, + method: 'POST', + body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', + query: { + exclude_export_details: true, + file_name: 'myFileName.ndjson', + }, + }); + }); + + test('happy path', async () => { + const blob: Blob = { + size: 89, + type: 'json', + arrayBuffer: jest.fn(), + slice: jest.fn(), + stream: jest.fn(), + text: jest.fn(), + } as Blob; + mockfetchSuccess(blob); + const resp = await exportRules({ + ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'], + signal: abortCtrl.signal, + }); + expect(resp).toEqual(blob); + }); + + test('unhappy path', async () => { + mockfetchError(); + try { + await exportRules({ + ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'], + signal: abortCtrl.signal, + }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('getRuleStatusById', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url, query', async () => { + mockfetchSuccess(null, fetchMock); + + await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find_statuses', { + asResponse: true, + query: { + ids: '["mySuperRuleId"]', + }, + method: 'GET', + signal: abortCtrl.signal, + }); + }); + test('happy path', async () => { + const statusMock = { + myRule: { + current_status: { + alert_id: 'alertId', + status_date: 'mm/dd/yyyyTHH:MM:sssz', + status: 'succeeded', + last_failure_at: null, + last_success_at: 'mm/dd/yyyyTHH:MM:sssz', + last_failure_message: null, + last_success_message: 'it is a success', + }, + failures: [], + }, + }; + mockfetchSuccess(statusMock); + const ruleResp = await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); + expect(ruleResp).toEqual(statusMock); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('fetchTags', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url when fetching tags', async () => { + mockfetchSuccess(null, fetchMock); + + await fetchTags({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/tags', { + asResponse: true, + signal: abortCtrl.signal, + method: 'GET', + }); + }); + test('happy path', async () => { + mockfetchSuccess(['hello', 'tags']); + const resp = await fetchTags({ signal: abortCtrl.signal }); + expect(resp).toEqual(['hello', 'tags']); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await fetchTags({ signal: abortCtrl.signal }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('getPrePackagedRulesStatus', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url when fetching tags', async () => { + mockfetchSuccess(null, fetchMock); + + await getPrePackagedRulesStatus({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/prepackaged/_status', { + asResponse: true, + signal: abortCtrl.signal, + method: 'GET', + }); + }); + test('happy path', async () => { + const prePackagesRulesStatus = { + rules_custom_installed: 33, + rules_installed: 12, + rules_not_installed: 0, + rules_not_updated: 2, + }; + mockfetchSuccess(prePackagesRulesStatus); + const resp = await getPrePackagedRulesStatus({ signal: abortCtrl.signal }); + expect(resp).toEqual(prePackagesRulesStatus); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await getPrePackagedRulesStatus({ signal: abortCtrl.signal }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx new file mode 100644 index 00000000000000..cad78ac565903a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx @@ -0,0 +1,460 @@ +/* + * 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, act } from '@testing-library/react-hooks'; + +import { defaultIndexPattern } from '../../../../default_index_pattern'; +import { useApolloClient } from '../../../utils/apollo_context'; +import { mocksSource } from '../../source/mock'; + +import { useFetchIndexPatterns, Return } from './fetch_index_patterns'; + +const mockUseApolloClient = useApolloClient as jest.Mock; +jest.mock('../../../utils/apollo_context'); + +describe('useFetchIndexPatterns', () => { + beforeEach(() => { + mockUseApolloClient.mockClear(); + }); + test('happy path', async () => { + await act(async () => { + mockUseApolloClient.mockImplementation(() => ({ + query: () => Promise.resolve(mocksSource[0].result), + })); + const { result, waitForNextUpdate } = renderHook(() => + useFetchIndexPatterns(defaultIndexPattern) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual([ + { + browserFields: { + base: { + fields: { + '@timestamp': { + category: 'base', + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: '@timestamp', + searchable: true, + type: 'date', + aggregatable: true, + }, + }, + }, + agent: { + fields: { + 'agent.ephemeral_id': { + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'agent.hostname': { + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'agent.id': { + category: 'agent', + description: + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', + example: '8a4f500d', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'agent.name': { + category: 'agent', + description: + 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', + example: 'foo', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.name', + searchable: true, + type: 'string', + aggregatable: true, + }, + }, + }, + auditd: { + fields: { + 'auditd.data.a0': { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a0', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'auditd.data.a1': { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a1', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'auditd.data.a2': { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a2', + searchable: true, + type: 'string', + aggregatable: true, + }, + }, + }, + client: { + fields: { + 'client.address': { + category: 'client', + description: + 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.address', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'client.bytes': { + category: 'client', + description: 'Bytes sent from the client to the server.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.bytes', + searchable: true, + type: 'number', + aggregatable: true, + }, + 'client.domain': { + category: 'client', + description: 'Client domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.domain', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'client.geo.country_iso_code': { + category: 'client', + description: 'Country ISO code.', + example: 'CA', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.geo.country_iso_code', + searchable: true, + type: 'string', + aggregatable: true, + }, + }, + }, + cloud: { + fields: { + 'cloud.account.id': { + category: 'cloud', + description: + 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', + example: '666777888999', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.account.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'cloud.availability_zone': { + category: 'cloud', + description: 'Availability zone in which this host is running.', + example: 'us-east-1c', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.availability_zone', + searchable: true, + type: 'string', + aggregatable: true, + }, + }, + }, + container: { + fields: { + 'container.id': { + category: 'container', + description: 'Unique container id.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'container.image.name': { + category: 'container', + description: 'Name of the image the container was built on.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.name', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'container.image.tag': { + category: 'container', + description: 'Container image tag.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.tag', + searchable: true, + type: 'string', + aggregatable: true, + }, + }, + }, + destination: { + fields: { + 'destination.address': { + category: 'destination', + description: + 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.address', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'destination.bytes': { + category: 'destination', + description: 'Bytes sent from the destination to the source.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.bytes', + searchable: true, + type: 'number', + aggregatable: true, + }, + 'destination.domain': { + category: 'destination', + description: 'Destination domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.domain', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'destination.ip': { + aggregatable: true, + category: 'destination', + description: + 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.ip', + searchable: true, + type: 'ip', + }, + 'destination.port': { + aggregatable: true, + category: 'destination', + description: 'Port of the destination.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.port', + searchable: true, + type: 'long', + }, + }, + }, + source: { + fields: { + 'source.ip': { + aggregatable: true, + category: 'source', + description: + 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.ip', + searchable: true, + type: 'ip', + }, + 'source.port': { + aggregatable: true, + category: 'source', + description: 'Port of the source.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.port', + searchable: true, + type: 'long', + }, + }, + }, + event: { + fields: { + 'event.end': { + aggregatable: true, + category: 'event', + description: + 'event.end contains the date when the event ended or when the activity was last observed.', + example: null, + format: '', + indexes: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + name: 'event.end', + searchable: true, + type: 'date', + }, + }, + }, + }, + isLoading: false, + indices: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + indicesExists: true, + indexPatterns: { + fields: [ + { name: '@timestamp', searchable: true, type: 'date', aggregatable: true }, + { name: 'agent.ephemeral_id', searchable: true, type: 'string', aggregatable: true }, + { name: 'agent.hostname', searchable: true, type: 'string', aggregatable: true }, + { name: 'agent.id', searchable: true, type: 'string', aggregatable: true }, + { name: 'agent.name', searchable: true, type: 'string', aggregatable: true }, + { name: 'auditd.data.a0', searchable: true, type: 'string', aggregatable: true }, + { name: 'auditd.data.a1', searchable: true, type: 'string', aggregatable: true }, + { name: 'auditd.data.a2', searchable: true, type: 'string', aggregatable: true }, + { name: 'client.address', searchable: true, type: 'string', aggregatable: true }, + { name: 'client.bytes', searchable: true, type: 'number', aggregatable: true }, + { name: 'client.domain', searchable: true, type: 'string', aggregatable: true }, + { + name: 'client.geo.country_iso_code', + searchable: true, + type: 'string', + aggregatable: true, + }, + { name: 'cloud.account.id', searchable: true, type: 'string', aggregatable: true }, + { + name: 'cloud.availability_zone', + searchable: true, + type: 'string', + aggregatable: true, + }, + { name: 'container.id', searchable: true, type: 'string', aggregatable: true }, + { + name: 'container.image.name', + searchable: true, + type: 'string', + aggregatable: true, + }, + { name: 'container.image.tag', searchable: true, type: 'string', aggregatable: true }, + { name: 'destination.address', searchable: true, type: 'string', aggregatable: true }, + { name: 'destination.bytes', searchable: true, type: 'number', aggregatable: true }, + { name: 'destination.domain', searchable: true, type: 'string', aggregatable: true }, + { name: 'destination.ip', searchable: true, type: 'ip', aggregatable: true }, + { name: 'destination.port', searchable: true, type: 'long', aggregatable: true }, + { name: 'source.ip', searchable: true, type: 'ip', aggregatable: true }, + { name: 'source.port', searchable: true, type: 'long', aggregatable: true }, + { name: 'event.end', searchable: true, type: 'date', aggregatable: true }, + ], + title: 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*', + }, + }, + result.current[1], + ]); + }); + }); + + test('unhappy path', async () => { + await act(async () => { + mockUseApolloClient.mockImplementation(() => ({ + query: () => Promise.reject(new Error('Something went wrong')), + })); + const { result, waitForNextUpdate } = renderHook(() => + useFetchIndexPatterns(defaultIndexPattern) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual([ + { + browserFields: {}, + indexPatterns: { + fields: [], + title: '', + }, + indices: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + indicesExists: false, + isLoading: false, + }, + result.current[1], + ]); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx index d376a1d6ad178a..b7ad41b8ba1bb2 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -29,7 +29,7 @@ interface FetchIndexPatternReturn { indexPatterns: IIndexPattern; } -type Return = [FetchIndexPatternReturn, Dispatch>]; +export type Return = [FetchIndexPatternReturn, Dispatch>]; export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => { const apolloClient = useApolloClient(); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/mock.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/mock.ts new file mode 100644 index 00000000000000..51526c0ab99494 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/mock.ts @@ -0,0 +1,139 @@ +/* + * 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 { NewRule, FetchRulesResponse, Rule } from './types'; + +export const ruleMock: NewRule = { + description: 'some desc', + enabled: true, + false_positives: [], + filters: [], + from: 'now-360s', + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + interval: '5m', + rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf', + language: 'kuery', + risk_score: 75, + name: 'Test rule', + query: "user.email: 'root@elastic.co'", + references: [], + severity: 'high', + tags: ['APM'], + to: 'now', + type: 'query', + threat: [], +}; + +export const savedRuleMock: Rule = { + created_at: 'mm/dd/yyyyTHH:MM:sssz', + created_by: 'mockUser', + description: 'some desc', + enabled: true, + false_positives: [], + filters: [], + from: 'now-360s', + id: '12345678987654321', + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + interval: '5m', + immutable: false, + rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf', + language: 'kuery', + risk_score: 75, + name: 'Test rule', + max_signals: 100, + query: "user.email: 'root@elastic.co'", + references: [], + severity: 'high', + tags: ['APM'], + to: 'now', + type: 'query', + threat: [], + updated_at: 'mm/dd/yyyyTHH:MM:sssz', + updated_by: 'mockUser', +}; + +export const rulesMock: FetchRulesResponse = { + page: 1, + perPage: 2, + total: 2, + data: [ + { + created_at: '2020-02-14T19:49:28.178Z', + updated_at: '2020-02-14T19:49:28.320Z', + created_by: 'elastic', + description: + 'Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.', + enabled: false, + false_positives: [], + from: 'now-660s', + id: '80c59768-8e1f-400e-908e-7b25c4ce29c3', + immutable: true, + index: ['endgame-*'], + interval: '10m', + rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e', + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 73, + name: 'Credential Dumping - Detected - Elastic Endpoint', + query: + 'event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection', + filters: [], + references: [], + severity: 'high', + updated_by: 'elastic', + tags: ['Elastic', 'Endpoint'], + to: 'now', + type: 'query', + threat: [], + version: 1, + }, + { + created_at: '2020-02-14T19:49:28.189Z', + updated_at: '2020-02-14T19:49:28.326Z', + created_by: 'elastic', + description: + 'Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.', + enabled: false, + false_positives: [], + from: 'now-660s', + id: '2e846086-bd64-4dbc-9c56-42b46b5b2c8c', + immutable: true, + index: ['endgame-*'], + interval: '10m', + rule_id: '77a3c3df-8ec4-4da4-b758-878f551dee69', + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 47, + name: 'Adversary Behavior - Detected - Elastic Endpoint', + query: 'event.kind:alert and event.module:endgame and event.action:rules_engine_event', + filters: [], + references: [], + severity: 'medium', + updated_by: 'elastic', + tags: ['Elastic', 'Endpoint'], + to: 'now', + type: 'query', + threat: [], + version: 1, + }, + ], +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx new file mode 100644 index 00000000000000..1bf21623992e61 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx @@ -0,0 +1,44 @@ +/* + * 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, act } from '@testing-library/react-hooks'; + +import { usePersistRule, ReturnPersistRule } from './persist_rule'; +import { ruleMock } from './mock'; + +jest.mock('./api'); + +describe('usePersistRule', () => { + test('init', async () => { + const { result } = renderHook(() => usePersistRule()); + + expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]); + }); + + test('saving rule with isLoading === true', async () => { + await act(async () => { + const { result, rerender, waitForNextUpdate } = renderHook(() => + usePersistRule() + ); + await waitForNextUpdate(); + result.current[1](ruleMock); + rerender(); + expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]); + }); + }); + + test('saved rule with isSaved === true', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePersistRule() + ); + await waitForNextUpdate(); + result.current[1](ruleMock); + await waitForNextUpdate(); + expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx index ea03c34ec31ba8..e720a1e70f1532 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx @@ -18,9 +18,9 @@ interface PersistRuleReturn { isSaved: boolean; } -type Return = [PersistRuleReturn, Dispatch]; +export type ReturnPersistRule = [PersistRuleReturn, Dispatch]; -export const usePersistRule = (): Return => { +export const usePersistRule = (): ReturnPersistRule => { const [rule, setRule] = useState(null); const [isSaved, setIsSaved] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -35,7 +35,6 @@ export const usePersistRule = (): Return => { try { setIsLoading(true); await persistRule({ rule, signal: abortCtrl.signal }); - if (isSubscribed) { setIsSaved(true); } diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index 0aaffb7b86b284..ff49bb8a8c3a24 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -64,7 +64,6 @@ export const RuleSchema = t.intersection([ language: t.string, name: t.string, max_signals: t.number, - meta: MetaRule, query: t.string, references: t.array(t.string), risk_score: t.number, @@ -80,6 +79,7 @@ export const RuleSchema = t.intersection([ t.partial({ last_failure_at: t.string, last_failure_message: t.string, + meta: MetaRule, output_index: t.string, saved_id: t.string, status: t.string, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx new file mode 100644 index 00000000000000..426a1ab9238dc4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx @@ -0,0 +1,267 @@ +/* + * 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, act } from '@testing-library/react-hooks'; +import { ReturnPrePackagedRules, usePrePackagedRules } from './use_pre_packaged_rules'; +import * as api from './api'; + +jest.mock('./api'); + +describe('usePersistRule', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: null, + hasIndexWrite: null, + hasManageApiKey: null, + isAuthenticated: null, + hasEncryptionKey: null, + isSignalIndexExists: null, + }) + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + createPrePackagedRules: null, + loading: true, + loadingCreatePrePackagedRules: false, + refetchPrePackagedRulesStatus: null, + rulesCustomInstalled: null, + rulesInstalled: null, + rulesNotInstalled: null, + rulesNotUpdated: null, + }); + }); + }); + + test('fetch getPrePackagedRulesStatus', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: null, + hasIndexWrite: null, + hasManageApiKey: null, + isAuthenticated: null, + hasEncryptionKey: null, + isSignalIndexExists: null, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + createPrePackagedRules: result.current.createPrePackagedRules, + loading: false, + loadingCreatePrePackagedRules: false, + refetchPrePackagedRulesStatus: result.current.refetchPrePackagedRulesStatus, + rulesCustomInstalled: 33, + rulesInstalled: 12, + rulesNotInstalled: 0, + rulesNotUpdated: 0, + }); + }); + }); + + test('happy path to createPrePackagedRules', async () => { + const spyOnCreatePrepackagedRules = jest.spyOn(api, 'createPrepackagedRules'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + hasManageApiKey: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + let resp = null; + if (result.current.createPrePackagedRules) { + resp = await result.current.createPrePackagedRules(); + } + expect(resp).toEqual(true); + expect(spyOnCreatePrepackagedRules).toHaveBeenCalled(); + expect(result.current).toEqual({ + createPrePackagedRules: result.current.createPrePackagedRules, + loading: false, + loadingCreatePrePackagedRules: false, + refetchPrePackagedRulesStatus: result.current.refetchPrePackagedRulesStatus, + rulesCustomInstalled: 33, + rulesInstalled: 12, + rulesNotInstalled: 0, + rulesNotUpdated: 0, + }); + }); + }); + + test('unhappy path to createPrePackagedRules', async () => { + const spyOnCreatePrepackagedRules = jest.spyOn(api, 'createPrepackagedRules'); + spyOnCreatePrepackagedRules.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + hasManageApiKey: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + let resp = null; + if (result.current.createPrePackagedRules) { + resp = await result.current.createPrePackagedRules(); + } + expect(resp).toEqual(false); + expect(spyOnCreatePrepackagedRules).toHaveBeenCalled(); + }); + }); + + test('can NOT createPrePackagedRules because canUserCrud === false', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: false, + hasIndexWrite: true, + hasManageApiKey: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + let resp = null; + if (result.current.createPrePackagedRules) { + resp = await result.current.createPrePackagedRules(); + } + expect(resp).toEqual(false); + }); + }); + + test('can NOT createPrePackagedRules because hasIndexWrite === false', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: false, + hasManageApiKey: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + let resp = null; + if (result.current.createPrePackagedRules) { + resp = await result.current.createPrePackagedRules(); + } + expect(resp).toEqual(false); + }); + }); + + test('can NOT createPrePackagedRules because hasManageApiKey === false', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + hasManageApiKey: false, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + let resp = null; + if (result.current.createPrePackagedRules) { + resp = await result.current.createPrePackagedRules(); + } + expect(resp).toEqual(false); + }); + }); + + test('can NOT createPrePackagedRules because isAuthenticated === false', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + hasManageApiKey: true, + isAuthenticated: false, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + let resp = null; + if (result.current.createPrePackagedRules) { + resp = await result.current.createPrePackagedRules(); + } + expect(resp).toEqual(false); + }); + }); + + test('can NOT createPrePackagedRules because hasEncryptionKey === false', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + hasManageApiKey: true, + isAuthenticated: true, + hasEncryptionKey: false, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + let resp = null; + if (result.current.createPrePackagedRules) { + resp = await result.current.createPrePackagedRules(); + } + expect(resp).toEqual(false); + }); + }); + + test('can NOT createPrePackagedRules because isSignalIndexExists === false', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + hasManageApiKey: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: false, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + let resp = null; + if (result.current.createPrePackagedRules) { + resp = await result.current.createPrePackagedRules(); + } + expect(resp).toEqual(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx index d77d6283692a24..04d7e3ef67da40 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -13,7 +13,7 @@ import * as i18n from './translations'; type Func = () => void; export type CreatePreBuiltRules = () => Promise; -interface Return { +export interface ReturnPrePackagedRules { createPrePackagedRules: null | CreatePreBuiltRules; loading: boolean; loadingCreatePrePackagedRules: boolean; @@ -50,10 +50,10 @@ export const usePrePackagedRules = ({ isAuthenticated, hasEncryptionKey, isSignalIndexExists, -}: UsePrePackagedRuleProps): Return => { +}: UsePrePackagedRuleProps): ReturnPrePackagedRules => { const [rulesStatus, setRuleStatus] = useState< Pick< - Return, + ReturnPrePackagedRules, | 'createPrePackagedRules' | 'refetchPrePackagedRulesStatus' | 'rulesCustomInstalled' @@ -167,6 +167,8 @@ export const usePrePackagedRules = ({ }, 300); timeoutId = reFetch(); } + } else { + resolve(false); } } catch (error) { if (isSubscribed) { diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx new file mode 100644 index 00000000000000..e0bf2c49073700 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx @@ -0,0 +1,84 @@ +/* + * 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, act } from '@testing-library/react-hooks'; +import { useRule, ReturnRule } from './use_rule'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useRule', () => { + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useRule('myOwnRuleID') + ); + await waitForNextUpdate(); + expect(result.current).toEqual([true, null]); + }); + }); + + test('fetch rule', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useRule('myOwnRuleID') + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual([ + false, + { + created_at: 'mm/dd/yyyyTHH:MM:sssz', + created_by: 'mockUser', + description: 'some desc', + enabled: true, + false_positives: [], + filters: [], + from: 'now-360s', + id: '12345678987654321', + immutable: false, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + interval: '5m', + language: 'kuery', + name: 'Test rule', + max_signals: 100, + query: "user.email: 'root@elastic.co'", + references: [], + risk_score: 75, + rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf', + severity: 'high', + tags: ['APM'], + threat: [], + to: 'now', + type: 'query', + updated_at: 'mm/dd/yyyyTHH:MM:sssz', + updated_by: 'mockUser', + }, + ]); + }); + }); + + test('fetch a new rule', async () => { + const spyOnfetchRuleById = jest.spyOn(api, 'fetchRuleById'); + await act(async () => { + const { rerender, waitForNextUpdate } = renderHook(id => useRule(id), { + initialProps: 'myOwnRuleID', + }); + await waitForNextUpdate(); + await waitForNextUpdate(); + rerender('newRuleId'); + await waitForNextUpdate(); + expect(spyOnfetchRuleById).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx index 22ba86cd09f743..ab08bd39688ce5 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx @@ -12,7 +12,7 @@ import { fetchRuleById } from './api'; import * as i18n from './translations'; import { Rule } from './types'; -type Return = [boolean, Rule | null]; +export type ReturnRule = [boolean, Rule | null]; /** * Hook for using to get a Rule from the Detection Engine API @@ -20,7 +20,7 @@ type Return = [boolean, Rule | null]; * @param id desired Rule ID's (not rule_id) * */ -export const useRule = (id: string | undefined): Return => { +export const useRule = (id: string | undefined): ReturnRule => { const [rule, setRule] = useState(null); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); @@ -36,7 +36,6 @@ export const useRule = (id: string | undefined): Return => { id: idToFetch, signal: abortCtrl.signal, }); - if (isSubscribed) { setRule(ruleResponse); } diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx new file mode 100644 index 00000000000000..25011adcfe98ba --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx @@ -0,0 +1,65 @@ +/* + * 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, act } from '@testing-library/react-hooks'; +import { useRuleStatus, ReturnRuleStatus } from './use_rule_status'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useRuleStatus', () => { + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useRuleStatus('myOwnRuleID') + ); + await waitForNextUpdate(); + expect(result.current).toEqual([true, null, null]); + }); + }); + + test('fetch rule status', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useRuleStatus('myOwnRuleID') + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual([ + false, + { + current_status: { + alert_id: 'alertId', + last_failure_at: null, + last_failure_message: null, + last_success_at: 'mm/dd/yyyyTHH:MM:sssz', + last_success_message: 'it is a success', + status: 'succeeded', + status_date: 'mm/dd/yyyyTHH:MM:sssz', + }, + failures: [], + }, + result.current[2], + ]); + }); + }); + + test('re-fetch rule status', async () => { + const spyOngetRuleStatusById = jest.spyOn(api, 'getRuleStatusById'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useRuleStatus('myOwnRuleID') + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + if (result.current[2]) { + result.current[2]('myOwnRuleID'); + } + await waitForNextUpdate(); + expect(spyOngetRuleStatusById).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx index 466c2cddac97d1..fcf95ac061ba3c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx @@ -13,7 +13,7 @@ import * as i18n from './translations'; import { RuleStatus } from './types'; type Func = (ruleId: string) => void; -type Return = [boolean, RuleStatus | null, Func | null]; +export type ReturnRuleStatus = [boolean, RuleStatus | null, Func | null]; /** * Hook for using to get a Rule from the Detection Engine API @@ -21,7 +21,7 @@ type Return = [boolean, RuleStatus | null, Func | null]; * @param id desired Rule ID's (not rule_id) * */ -export const useRuleStatus = (id: string | undefined | null): Return => { +export const useRuleStatus = (id: string | undefined | null): ReturnRuleStatus => { const [ruleStatus, setRuleStatus] = useState(null); const fetchRuleStatus = useRef(null); const [loading, setLoading] = useState(true); @@ -34,6 +34,7 @@ export const useRuleStatus = (id: string | undefined | null): Return => { const fetchData = async (idToFetch: string) => { try { setLoading(true); + const ruleStatusResponse = await getRuleStatusById({ id: idToFetch, signal: abortCtrl.signal, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx new file mode 100644 index 00000000000000..b369d3a50730d0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx @@ -0,0 +1,216 @@ +/* + * 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, act } from '@testing-library/react-hooks'; +import { useRules, ReturnRules } from './use_rules'; +import * as api from './api'; +import { PaginationOptions, FilterOptions } from '.'; + +jest.mock('./api'); + +describe('useRules', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + [PaginationOptions, FilterOptions], + ReturnRules + >(props => + useRules( + { + page: 1, + perPage: 10, + total: 100, + }, + { + filter: '', + sortField: 'created_at', + sortOrder: 'desc', + } + ) + ); + await waitForNextUpdate(); + expect(result.current).toEqual([ + true, + { + data: [], + page: 1, + perPage: 20, + total: 0, + }, + null, + ]); + }); + }); + + test('fetch rules', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + [PaginationOptions, FilterOptions], + ReturnRules + >(() => + useRules( + { + page: 1, + perPage: 10, + total: 100, + }, + { + filter: '', + sortField: 'created_at', + sortOrder: 'desc', + } + ) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual([ + false, + { + data: [ + { + created_at: '2020-02-14T19:49:28.178Z', + created_by: 'elastic', + description: + 'Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.', + enabled: false, + false_positives: [], + filters: [], + from: 'now-660s', + id: '80c59768-8e1f-400e-908e-7b25c4ce29c3', + immutable: true, + index: ['endgame-*'], + interval: '10m', + language: 'kuery', + max_signals: 100, + name: 'Credential Dumping - Detected - Elastic Endpoint', + output_index: '.siem-signals-default', + query: + 'event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection', + references: [], + risk_score: 73, + rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e', + severity: 'high', + tags: ['Elastic', 'Endpoint'], + threat: [], + to: 'now', + type: 'query', + updated_at: '2020-02-14T19:49:28.320Z', + updated_by: 'elastic', + version: 1, + }, + { + created_at: '2020-02-14T19:49:28.189Z', + created_by: 'elastic', + description: + 'Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.', + enabled: false, + false_positives: [], + filters: [], + from: 'now-660s', + id: '2e846086-bd64-4dbc-9c56-42b46b5b2c8c', + immutable: true, + index: ['endgame-*'], + interval: '10m', + language: 'kuery', + max_signals: 100, + name: 'Adversary Behavior - Detected - Elastic Endpoint', + output_index: '.siem-signals-default', + query: + 'event.kind:alert and event.module:endgame and event.action:rules_engine_event', + references: [], + risk_score: 47, + rule_id: '77a3c3df-8ec4-4da4-b758-878f551dee69', + severity: 'medium', + tags: ['Elastic', 'Endpoint'], + threat: [], + to: 'now', + type: 'query', + updated_at: '2020-02-14T19:49:28.326Z', + updated_by: 'elastic', + version: 1, + }, + ], + page: 1, + perPage: 2, + total: 2, + }, + result.current[2], + ]); + }); + }); + + test('re-fetch rules', async () => { + const spyOnfetchRules = jest.spyOn(api, 'fetchRules'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + [PaginationOptions, FilterOptions], + ReturnRules + >(id => + useRules( + { + page: 1, + perPage: 10, + total: 100, + }, + { + filter: '', + sortField: 'created_at', + sortOrder: 'desc', + } + ) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + if (result.current[2]) { + result.current[2](); + } + await waitForNextUpdate(); + expect(spyOnfetchRules).toHaveBeenCalledTimes(2); + }); + }); + + test('fetch rules if props changes', async () => { + const spyOnfetchRules = jest.spyOn(api, 'fetchRules'); + await act(async () => { + const { rerender, waitForNextUpdate } = renderHook< + [PaginationOptions, FilterOptions], + ReturnRules + >(args => useRules(args[0], args[1]), { + initialProps: [ + { + page: 1, + perPage: 10, + total: 100, + }, + { + filter: '', + sortField: 'created_at', + sortOrder: 'desc', + }, + ], + }); + await waitForNextUpdate(); + await waitForNextUpdate(); + rerender([ + { + page: 1, + perPage: 10, + total: 100, + }, + { + filter: 'hello world', + sortField: 'created_at', + sortOrder: 'desc', + }, + ]); + await waitForNextUpdate(); + expect(spyOnfetchRules).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx index af6e437255acdd..301a68dc6f445f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx @@ -13,7 +13,7 @@ import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import * as i18n from './translations'; type Func = () => void; -type Return = [boolean, FetchRulesResponse, Func | null]; +export type ReturnRules = [boolean, FetchRulesResponse, Func | null]; /** * Hook for using the list of Rules from the Detection Engine API @@ -21,7 +21,10 @@ type Return = [boolean, FetchRulesResponse, Func | null]; * @param pagination desired pagination options (e.g. page/perPage) * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) */ -export const useRules = (pagination: PaginationOptions, filterOptions: FilterOptions): Return => { +export const useRules = ( + pagination: PaginationOptions, + filterOptions: FilterOptions +): ReturnRules => { const [rules, setRules] = useState({ page: 1, perPage: 20, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx new file mode 100644 index 00000000000000..4a796efa5b0cb2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx @@ -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 { renderHook, act } from '@testing-library/react-hooks'; +import { useTags, ReturnTags } from './use_tags'; + +jest.mock('./api'); + +describe('useTags', () => { + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useTags()); + await waitForNextUpdate(); + expect(result.current).toEqual([true, []]); + }); + }); + + test('fetch tags', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useTags()); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual([false, ['elastic', 'love', 'quality', 'code']]); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx index 1c961d530422a5..196d4b14205610 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx @@ -10,13 +10,13 @@ import { fetchTags } from './api'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import * as i18n from './translations'; -type Return = [boolean, string[]]; +export type ReturnTags = [boolean, string[]]; /** * Hook for using the list of Tags from the Detection Engine API * */ -export const useTags = (): Return => { +export const useTags = (): ReturnTags => { const [tags, setTags] = useState([]); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); diff --git a/x-pack/legacy/plugins/siem/public/mock/hook_wrapper.tsx b/x-pack/legacy/plugins/siem/public/mock/hook_wrapper.tsx index 292ddc036dcafd..70c76de01e95a1 100644 --- a/x-pack/legacy/plugins/siem/public/mock/hook_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/mock/hook_wrapper.tsx @@ -12,6 +12,7 @@ interface HookWrapperProps { // eslint-disable-next-line @typescript-eslint/no-explicit-any hookProps?: any; } + export const HookWrapper = ({ hook, hookProps }: HookWrapperProps) => { const myHook = hook ? (hookProps ? hook(hookProps) : hook()) : null; return
{JSON.stringify(myHook)}
; From 783663fa523b70329c3094e2703613f9a68ebfa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 20 Feb 2020 10:46:28 -0500 Subject: [PATCH 11/43] Skip flaky alert details test (#58120) * Skip flaky test * Skip suite * Skip suite --- .../functional_with_es_ssl/apps/triggers_actions_ui/details.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 3db4731f0adfb6..86fc3d6cd6a6c0 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -18,7 +18,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const alerting = getService('alerting'); const retry = getService('retry'); - describe('Alert Details', function() { + // FLAKY: https://github.com/elastic/kibana/issues/57426 + describe.skip('Alert Details', function() { describe('Header', function() { const testRunUuid = uuid.v4(); before(async () => { From 289b2fa611503ab198339f2bae26966a1aeffdfa Mon Sep 17 00:00:00 2001 From: Rachel Shen Date: Thu, 20 Feb 2020 08:59:44 -0700 Subject: [PATCH 12/43] Clarify Precision function in Timelion Kibana (#58031) * Closes issue 26100 Co-authored-by: Elastic Machine --- src/plugins/timelion/server/series_functions/precision.js | 4 ++-- x-pack/plugins/translations/translations/ja-JP.json | 2 -- x-pack/plugins/translations/translations/zh-CN.json | 2 -- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/plugins/timelion/server/series_functions/precision.js b/src/plugins/timelion/server/series_functions/precision.js index 756fb067f2335c..71c15fdd46fddc 100644 --- a/src/plugins/timelion/server/series_functions/precision.js +++ b/src/plugins/timelion/server/series_functions/precision.js @@ -32,12 +32,12 @@ export default new Chainable('precision', { name: 'precision', types: ['number'], help: i18n.translate('timelion.help.functions.precision.args.precisionHelpText', { - defaultMessage: 'Number of digits to round each value to', + defaultMessage: 'The number of digits to truncate each value to', }), }, ], help: i18n.translate('timelion.help.functions.precisionHelpText', { - defaultMessage: 'number of digits to round the decimal portion of the value to', + defaultMessage: 'The number of digits to truncate the decimal portion of the value to', }), fn: async function precisionFn(args) { await alter(args, function(eachSeries, precision) { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 47bf2ae634048e..380e78cf6fa194 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2697,8 +2697,6 @@ "timelion.help.functions.points.args.symbolHelpText": "点のシンボルです。{validSymbols} の 1 つ", "timelion.help.functions.points.args.weightHelpText": "点の周りの太さです", "timelion.help.functions.pointsHelpText": "数列を点として表示します", - "timelion.help.functions.precision.args.precisionHelpText": "各値を四捨五入する桁数です", - "timelion.help.functions.precisionHelpText": "値の小数点以下の四捨五入する桁数です", "timelion.help.functions.props.args.globalHelpText": "各数列に対し、seriesList にプロップを設定します", "timelion.help.functions.propsHelpText": "数列に任意のプロパティを設定するため、自己責任で行ってください。例: {example}", "timelion.help.functions.quandl.args.codeHelpText": "プロットする Quandl コードです。これらは quandl.com に掲載されています。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a94a602e48d9ba..23e822821fea4e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2697,8 +2697,6 @@ "timelion.help.functions.points.args.symbolHelpText": "点符号。以下选项之一:{validSymbols}", "timelion.help.functions.points.args.weightHelpText": "围绕点的线条粗细", "timelion.help.functions.pointsHelpText": "将序列显示为点", - "timelion.help.functions.precision.args.precisionHelpText": "将每个值舍入到的小数位数", - "timelion.help.functions.precisionHelpText": "将值的小数部分舍入到的小数位数", "timelion.help.functions.props.args.globalHelpText": "在 seriesList 与每个序列上设置属性", "timelion.help.functions.propsHelpText": "在序列上可设置任意属性,但请自担风险。例如 {example}", "timelion.help.functions.quandl.args.codeHelpText": "要绘图的 quandl 代码。可以在 quandl.com 找到这些内容。", From c3001c4469e015f5942d5fdaa6a37872129f4aa4 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Thu, 20 Feb 2020 08:27:53 -0800 Subject: [PATCH 13/43] Add filter for ILM phase to Index Management (revert #45486) (#57402) --- .../extend_index_management.test.js.snap | 24 +++++++++++++ .../np_ready/extend_index_management/index.js | 34 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap b/x-pack/legacy/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap index 92aaa171551a0d..74c3e7408fe7cd 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap +++ b/x-pack/legacy/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap @@ -63,6 +63,30 @@ Array [ ], "type": "field_value_selection", }, + Object { + "field": "ilm.phase", + "multiSelect": "or", + "name": "Lifecycle phase", + "options": Array [ + Object { + "value": "hot", + "view": "Hot", + }, + Object { + "value": "warm", + "view": "Warm", + }, + Object { + "value": "cold", + "view": "Cold", + }, + Object { + "value": "delete", + "view": "Delete", + }, + ], + "type": "field_value_selection", + }, ] `; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js index 6958c4ecce0cc6..0e662b78b2c180 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js @@ -200,6 +200,40 @@ export const ilmFilterExtension = indices => { }, ], }, + { + type: 'field_value_selection', + field: 'ilm.phase', + name: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.lifecyclePhaseLabel', { + defaultMessage: 'Lifecycle phase', + }), + multiSelect: 'or', + options: [ + { + value: 'hot', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.hotLabel', { + defaultMessage: 'Hot', + }), + }, + { + value: 'warm', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.warmLabel', { + defaultMessage: 'Warm', + }), + }, + { + value: 'cold', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.coldLabel', { + defaultMessage: 'Cold', + }), + }, + { + value: 'delete', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.deleteLabel', { + defaultMessage: 'Delete', + }), + }, + ], + }, ]; } }; From dfd19596e1aa39af9d9ea6acc30316d8823b20a9 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Thu, 20 Feb 2020 10:40:35 -0600 Subject: [PATCH 14/43] Fix browser date format (#57714) * fix browser date formatter --- .../constants/base_formatters.ts | 2 -- .../common/field_formats/converters/index.ts | 1 - .../field_formats/field_formats_registry.ts | 4 +++ .../data/common/field_formats/index.ts | 1 - .../data/common/field_formats/types.ts | 1 + src/plugins/data/common/types.ts | 1 + .../data/public/field_formats/constants.ts | 23 +++++++++++++ .../field_formats/converters/date.test.ts | 0 .../field_formats/converters/date.ts | 9 +++-- .../public/field_formats/converters/index.ts | 20 +++++++++++ .../field_formats_service.test.ts | 34 +++++++++++++++++++ .../field_formats/field_formats_service.ts | 19 +++++++---- .../data/public/field_formats/index.ts | 2 ++ src/plugins/data/public/index.ts | 4 ++- src/plugins/data/public/mocks.ts | 1 + .../query_string_input.test.tsx.snap | 6 ++++ .../field_formats/converters/date_server.ts | 6 ++-- .../server/field_formats/converters/index.ts | 20 +++++++++++ .../field_formats_service.test.ts | 34 +++++++++++++++++++ .../field_formats/field_formats_service.ts | 3 +- src/plugins/data/server/index.ts | 4 --- src/test_utils/public/stub_field_formats.ts | 3 +- 22 files changed, 174 insertions(+), 24 deletions(-) create mode 100644 src/plugins/data/public/field_formats/constants.ts rename src/plugins/data/{common => public}/field_formats/converters/date.test.ts (100%) rename src/plugins/data/{common => public}/field_formats/converters/date.ts (92%) create mode 100644 src/plugins/data/public/field_formats/converters/index.ts create mode 100644 src/plugins/data/public/field_formats/field_formats_service.test.ts rename src/plugins/data/{common => server}/field_formats/converters/date_server.ts (95%) create mode 100644 src/plugins/data/server/field_formats/converters/index.ts create mode 100644 src/plugins/data/server/field_formats/field_formats_service.test.ts 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 95aedd02d16d65..6befe8cea71f54 100644 --- a/src/plugins/data/common/field_formats/constants/base_formatters.ts +++ b/src/plugins/data/common/field_formats/constants/base_formatters.ts @@ -23,7 +23,6 @@ import { BoolFormat, BytesFormat, ColorFormat, - DateFormat, DateNanosFormat, DurationFormat, IpFormat, @@ -41,7 +40,6 @@ export const baseFormatters: IFieldFormatType[] = [ BoolFormat, BytesFormat, ColorFormat, - DateFormat, DateNanosFormat, DurationFormat, IpFormat, diff --git a/src/plugins/data/common/field_formats/converters/index.ts b/src/plugins/data/common/field_formats/converters/index.ts index f7e50539b44d8e..cc9fae7fc9965d 100644 --- a/src/plugins/data/common/field_formats/converters/index.ts +++ b/src/plugins/data/common/field_formats/converters/index.ts @@ -19,7 +19,6 @@ export { UrlFormat } from './url'; export { BytesFormat } from './bytes'; -export { DateFormat } from './date_server'; export { DateNanosFormat } from './date_nanos'; export { RelativeDateFormat } from './relative_date'; export { DurationFormat } from './duration'; 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 9fe9a31307b6ac..9fdf1ad9c80fbf 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.ts @@ -95,6 +95,10 @@ export class FieldFormatsRegistry { return undefined; }; + getTypeWithoutMetaParams = (formatId: FieldFormatId): IFieldFormatType | undefined => { + return this.fieldFormats.get(formatId); + }; + /** * Get the default FieldFormat type (class) for * a field type, using the format:defaultTypeMap. diff --git a/src/plugins/data/common/field_formats/index.ts b/src/plugins/data/common/field_formats/index.ts index d7858966f2620a..13d3d9d73d43a6 100644 --- a/src/plugins/data/common/field_formats/index.ts +++ b/src/plugins/data/common/field_formats/index.ts @@ -27,7 +27,6 @@ export { BoolFormat, BytesFormat, ColorFormat, - DateFormat, DateNanosFormat, DurationFormat, IpFormat, diff --git a/src/plugins/data/common/field_formats/types.ts b/src/plugins/data/common/field_formats/types.ts index 24aa92c67b6941..0c16d9f1ac8bf7 100644 --- a/src/plugins/data/common/field_formats/types.ts +++ b/src/plugins/data/common/field_formats/types.ts @@ -18,6 +18,7 @@ */ import { FieldFormat } from './field_format'; +export { FieldFormat }; /** @public **/ export type FieldFormatsContentType = 'html' | 'text'; diff --git a/src/plugins/data/common/types.ts b/src/plugins/data/common/types.ts index be0d3230b3a0e0..93629c3dbaf626 100644 --- a/src/plugins/data/common/types.ts +++ b/src/plugins/data/common/types.ts @@ -21,3 +21,4 @@ export * from './timefilter/types'; export * from './query/types'; export * from './kbn_field_types/types'; export * from './index_patterns/types'; +export { TextContextTypeConvert, IFieldFormatMetaParams } from './field_formats/types'; diff --git a/src/plugins/data/public/field_formats/constants.ts b/src/plugins/data/public/field_formats/constants.ts new file mode 100644 index 00000000000000..a5c2b4e3799083 --- /dev/null +++ b/src/plugins/data/public/field_formats/constants.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { baseFormatters } from '../../common'; +import { DateFormat } from './converters/date'; + +export const baseFormattersPublic = [DateFormat, ...baseFormatters]; diff --git a/src/plugins/data/common/field_formats/converters/date.test.ts b/src/plugins/data/public/field_formats/converters/date.test.ts similarity index 100% rename from src/plugins/data/common/field_formats/converters/date.test.ts rename to src/plugins/data/public/field_formats/converters/date.test.ts diff --git a/src/plugins/data/common/field_formats/converters/date.ts b/src/plugins/data/public/field_formats/converters/date.ts similarity index 92% rename from src/plugins/data/common/field_formats/converters/date.ts rename to src/plugins/data/public/field_formats/converters/date.ts index 3888df051b1187..3e1efdc69dec82 100644 --- a/src/plugins/data/common/field_formats/converters/date.ts +++ b/src/plugins/data/public/field_formats/converters/date.ts @@ -20,9 +20,12 @@ import { i18n } from '@kbn/i18n'; import { memoize, noop } from 'lodash'; import moment from 'moment'; -import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; -import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; +import { + FieldFormat, + KBN_FIELD_TYPES, + TextContextTypeConvert, + FIELD_FORMAT_IDS, +} from '../../../common'; export class DateFormat extends FieldFormat { static id = FIELD_FORMAT_IDS.DATE; diff --git a/src/plugins/data/public/field_formats/converters/index.ts b/src/plugins/data/public/field_formats/converters/index.ts new file mode 100644 index 00000000000000..c51111092becad --- /dev/null +++ b/src/plugins/data/public/field_formats/converters/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { DateFormat } from './date'; diff --git a/src/plugins/data/public/field_formats/field_formats_service.test.ts b/src/plugins/data/public/field_formats/field_formats_service.test.ts new file mode 100644 index 00000000000000..e066af28f4699c --- /dev/null +++ b/src/plugins/data/public/field_formats/field_formats_service.test.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FieldFormatsService } from './field_formats_service'; +import { coreMock } from '../../../../../src/core/public/mocks'; +import { DateFormat } from './converters/date'; + +describe('FieldFormatService', () => { + test('DateFormat is public version', () => { + const mockCore = coreMock.createSetup(); + const service = new FieldFormatsService(); + service.setup(mockCore); + const fieldFormatsRegistry = service.start(); + const DateFormatFromRegsitry = fieldFormatsRegistry.getTypeWithoutMetaParams('date'); + + expect(DateFormatFromRegsitry).toEqual(DateFormat); + }); +}); diff --git a/src/plugins/data/public/field_formats/field_formats_service.ts b/src/plugins/data/public/field_formats/field_formats_service.ts index 785bedf9b35d34..22c7e90c06130b 100644 --- a/src/plugins/data/public/field_formats/field_formats_service.ts +++ b/src/plugins/data/public/field_formats/field_formats_service.ts @@ -18,9 +18,10 @@ */ import { CoreSetup } from 'src/core/public'; -import { FieldFormatsRegistry } from '../../common/field_formats'; +import { FieldFormatsRegistry } from '../../common'; import { deserializeFieldFormat } from './utils/deserialize'; import { FormatFactory } from '../../common/field_formats/utils'; +import { baseFormattersPublic } from './constants'; export class FieldFormatsService { private readonly fieldFormatsRegistry: FieldFormatsRegistry = new FieldFormatsRegistry(); @@ -34,13 +35,17 @@ export class FieldFormatsService { const getConfig = core.uiSettings.get.bind(core.uiSettings); - this.fieldFormatsRegistry.init(getConfig, { - parsedUrl: { - origin: window.location.origin, - pathname: window.location.pathname, - basePath: core.http.basePath.get(), + this.fieldFormatsRegistry.init( + getConfig, + { + parsedUrl: { + origin: window.location.origin, + pathname: window.location.pathname, + basePath: core.http.basePath.get(), + }, }, - }); + baseFormattersPublic + ); return this.fieldFormatsRegistry as FieldFormatsSetup; } diff --git a/src/plugins/data/public/field_formats/index.ts b/src/plugins/data/public/field_formats/index.ts index 4550a5781535f2..015d5b39561bb5 100644 --- a/src/plugins/data/public/field_formats/index.ts +++ b/src/plugins/data/public/field_formats/index.ts @@ -18,3 +18,5 @@ */ export { FieldFormatsService, FieldFormatsSetup, FieldFormatsStart } from './field_formats_service'; +export { DateFormat } from './converters'; +export { baseFormattersPublic } from './constants'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index cdc4167f545af5..cbd4bfd348797d 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -156,7 +156,6 @@ import { BoolFormat, BytesFormat, ColorFormat, - DateFormat, DateNanosFormat, DurationFormat, IpFormat, @@ -171,6 +170,9 @@ import { serializeFieldFormat, } from '../common/field_formats'; +import { DateFormat } from './field_formats'; +export { baseFormattersPublic } from './field_formats'; + // Field formats helpers namespace: export const fieldFormats = { FieldFormat, diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 2d5cc72597ec4b..a2a1a2424fc90e 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -50,6 +50,7 @@ const fieldFormatsMock: IFieldFormatsRegistry = { register: jest.fn(), parseDefaultTypeMap: jest.fn(), deserialize: jest.fn(), + getTypeWithoutMetaParams: jest.fn(), }; const createSetupContract = (): Setup => { diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index 06e56aaf3eb0a6..93af543fba1a8b 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -184,6 +184,7 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "getInstance": [MockFunction], "getType": [MockFunction], "getTypeNameByEsTypes": [MockFunction], + "getTypeWithoutMetaParams": [MockFunction], "init": [MockFunction], "parseDefaultTypeMap": [MockFunction], "register": [MockFunction], @@ -839,6 +840,7 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "getInstance": [MockFunction], "getType": [MockFunction], "getTypeNameByEsTypes": [MockFunction], + "getTypeWithoutMetaParams": [MockFunction], "init": [MockFunction], "parseDefaultTypeMap": [MockFunction], "register": [MockFunction], @@ -1476,6 +1478,7 @@ exports[`QueryStringInput Should pass the query language to the language switche "getInstance": [MockFunction], "getType": [MockFunction], "getTypeNameByEsTypes": [MockFunction], + "getTypeWithoutMetaParams": [MockFunction], "init": [MockFunction], "parseDefaultTypeMap": [MockFunction], "register": [MockFunction], @@ -2128,6 +2131,7 @@ exports[`QueryStringInput Should pass the query language to the language switche "getInstance": [MockFunction], "getType": [MockFunction], "getTypeNameByEsTypes": [MockFunction], + "getTypeWithoutMetaParams": [MockFunction], "init": [MockFunction], "parseDefaultTypeMap": [MockFunction], "register": [MockFunction], @@ -2765,6 +2769,7 @@ exports[`QueryStringInput Should render the given query 1`] = ` "getInstance": [MockFunction], "getType": [MockFunction], "getTypeNameByEsTypes": [MockFunction], + "getTypeWithoutMetaParams": [MockFunction], "init": [MockFunction], "parseDefaultTypeMap": [MockFunction], "register": [MockFunction], @@ -3417,6 +3422,7 @@ exports[`QueryStringInput Should render the given query 1`] = ` "getInstance": [MockFunction], "getType": [MockFunction], "getTypeNameByEsTypes": [MockFunction], + "getTypeWithoutMetaParams": [MockFunction], "init": [MockFunction], "parseDefaultTypeMap": [MockFunction], "register": [MockFunction], diff --git a/src/plugins/data/common/field_formats/converters/date_server.ts b/src/plugins/data/server/field_formats/converters/date_server.ts similarity index 95% rename from src/plugins/data/common/field_formats/converters/date_server.ts rename to src/plugins/data/server/field_formats/converters/date_server.ts index 216af133bb5f52..f4e62962591963 100644 --- a/src/plugins/data/common/field_formats/converters/date_server.ts +++ b/src/plugins/data/server/field_formats/converters/date_server.ts @@ -20,14 +20,14 @@ import { i18n } from '@kbn/i18n'; import { memoize, noop } from 'lodash'; import moment from 'moment-timezone'; -import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; -import { FieldFormat } from '../field_format'; import { + FieldFormat, + KBN_FIELD_TYPES, TextContextTypeConvert, FIELD_FORMAT_IDS, FieldFormatsGetConfigFn, IFieldFormatMetaParams, -} from '../types'; +} from '../../../common'; export class DateFormat extends FieldFormat { static id = FIELD_FORMAT_IDS.DATE; diff --git a/src/plugins/data/server/field_formats/converters/index.ts b/src/plugins/data/server/field_formats/converters/index.ts new file mode 100644 index 00000000000000..f5c69df9728699 --- /dev/null +++ b/src/plugins/data/server/field_formats/converters/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { DateFormat } from './date_server'; diff --git a/src/plugins/data/server/field_formats/field_formats_service.test.ts b/src/plugins/data/server/field_formats/field_formats_service.test.ts new file mode 100644 index 00000000000000..2e7ce0fa435a76 --- /dev/null +++ b/src/plugins/data/server/field_formats/field_formats_service.test.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FieldFormatsService } from './field_formats_service'; +import { DateFormat } from './converters/date_server'; +import { coreMock } from '../../../../core/server/mocks'; + +describe('FieldFormatService', () => { + test('DateFormat is server version', async () => { + const service = new FieldFormatsService(); + const fieldFormatsService = await service.start(); + const uiSettings = coreMock.createStart().uiSettings.asScopedToClient({} as any); + const fieldFormatsRegistry = await fieldFormatsService.fieldFormatServiceFactory(uiSettings); + const DateFormatFromRegsitry = fieldFormatsRegistry.getTypeWithoutMetaParams('date'); + + expect(DateFormatFromRegsitry).toEqual(DateFormat); + }); +}); 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 a31e5927ab8001..0dac64fb5dc1d2 100644 --- a/src/plugins/data/server/field_formats/field_formats_service.ts +++ b/src/plugins/data/server/field_formats/field_formats_service.ts @@ -19,9 +19,10 @@ import { has } from 'lodash'; import { FieldFormatsRegistry, IFieldFormatType, baseFormatters } from '../../common/field_formats'; import { IUiSettingsClient } from '../../../../core/server'; +import { DateFormat } from './converters'; export class FieldFormatsService { - private readonly fieldFormatClasses: IFieldFormatType[] = baseFormatters; + private readonly fieldFormatClasses: IFieldFormatType[] = [DateFormat, ...baseFormatters]; public setup() { return { diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 3ee98a318de352..40d367138b60d7 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -83,7 +83,6 @@ import { BoolFormat, BytesFormat, ColorFormat, - DateFormat, DateNanosFormat, DurationFormat, IpFormat, @@ -101,13 +100,10 @@ import { export const fieldFormats = { FieldFormatsRegistry, FieldFormat, - serializeFieldFormat, - BoolFormat, BytesFormat, ColorFormat, - DateFormat, DateNanosFormat, DurationFormat, IpFormat, diff --git a/src/test_utils/public/stub_field_formats.ts b/src/test_utils/public/stub_field_formats.ts index 5a20823134ebd5..589e93fd600c2c 100644 --- a/src/test_utils/public/stub_field_formats.ts +++ b/src/test_utils/public/stub_field_formats.ts @@ -19,12 +19,13 @@ import { CoreSetup } from 'kibana/public'; import { DataPublicPluginStart, fieldFormats } from '../../plugins/data/public'; import { deserializeFieldFormat } from '../../plugins/data/public/field_formats/utils/deserialize'; +import { baseFormattersPublic } from '../../plugins/data/public'; export const getFieldFormatsRegistry = (core: CoreSetup) => { const fieldFormatsRegistry = new fieldFormats.FieldFormatsRegistry(); const getConfig = core.uiSettings.get.bind(core.uiSettings); - fieldFormatsRegistry.init(getConfig, {}); + fieldFormatsRegistry.init(getConfig, {}, baseFormattersPublic); fieldFormatsRegistry.deserialize = deserializeFieldFormat.bind( fieldFormatsRegistry as DataPublicPluginStart['fieldFormats'] From 5953e62a726bacd5b49976a3e0863e1d2cb5aa06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez?= Date: Thu, 20 Feb 2020 17:57:00 +0100 Subject: [PATCH 15/43] [Logs UI] Fix column reordering in settings page (#58104) * Ensure only one element has scroll Apparently having multiple elements with scroll confuses the `react-beautiful-dnd` mechanism to determine the position of the elements. Adding `overflowY` to the app root fixes it. * Fix upper bound check for log column reordering Co-authored-by: Elastic Machine --- x-pack/plugins/infra/public/apps/start_app.tsx | 1 + .../log_columns_configuration_form_state.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/public/apps/start_app.tsx b/x-pack/plugins/infra/public/apps/start_app.tsx index 300d97d3c45b17..66e699abd22b41 100644 --- a/x-pack/plugins/infra/public/apps/start_app.tsx +++ b/x-pack/plugins/infra/public/apps/start_app.tsx @@ -79,6 +79,7 @@ export async function startApp( // expected. element.style.height = '100%'; element.style.display = 'flex'; + element.style.overflowY = 'hidden'; // Prevent having scroll within a container having scroll. It messes up with drag-n-drop elements element.className += ` ${CONTAINER_CLASSNAME}`; ReactDOM.render(, element); diff --git a/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_form_state.tsx b/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_form_state.tsx index c5398cf79ef430..0b6a92ed98507f 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_form_state.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_form_state.tsx @@ -108,7 +108,7 @@ export const useLogColumnsConfigurationFormState = ({ const moveLogColumn = useCallback( (sourceIndex, destinationIndex) => { - if (destinationIndex >= 0 && sourceIndex < formState.logColumns.length - 1) { + if (destinationIndex >= 0 && sourceIndex <= formState.logColumns.length - 1) { const newLogColumns = [...formState.logColumns]; newLogColumns.splice(destinationIndex, 0, newLogColumns.splice(sourceIndex, 1)[0]); setFormStateChanges(changes => ({ From 857f9f8379632c74f10b64f16d375ba2cd1ddd66 Mon Sep 17 00:00:00 2001 From: Maryia Lapata Date: Thu, 20 Feb 2020 21:20:23 +0300 Subject: [PATCH 16/43] [NP] Move ui/saved_objects to NP (#57452) * Move saved_objects to NP * Update path for imports * Remove ui/saved_objects * Update i18n IDs * Convert test * Replace Bluebird to Promise; fix unit tests * Mock openConfirm in test * Add kibana.json * Check unit test * Update unit tests --- src/core/MIGRATION.md | 2 +- .../simple_saved_object.test.ts} | 35 +- .../kibana/public/dashboard/legacy_imports.ts | 2 - .../public/dashboard/np_ready/application.ts | 2 +- .../np_ready/dashboard_app_controller.tsx | 3 +- .../dashboard/np_ready/lib/save_dashboard.ts | 2 +- .../saved_dashboard/saved_dashboard.ts | 7 +- .../saved_dashboard/saved_dashboards.ts | 6 +- .../discover/saved_searches/_saved_search.ts | 7 +- .../discover/saved_searches/saved_searches.ts | 7 +- .../management/saved_object_registry.ts | 2 +- .../render.test.js | 1 + .../saved_object_save_as_checkbox.html | 2 +- .../timelion/public/services/_saved_sheet.ts | 6 +- .../timelion/public/services/saved_sheets.ts | 2 +- .../public/embeddable/visualize_embeddable.ts | 2 +- .../public/saved_visualizations/_saved_vis.ts | 7 +- .../saved_visualizations.ts | 6 +- src/plugins/data/public/mocks.ts | 1 + .../query_string_input.test.tsx.snap | 6 + src/plugins/saved_objects/kibana.json | 7 + .../saved_objects/public}/constants.ts | 11 +- src/plugins/saved_objects/public/index.ts | 6 + src/plugins/saved_objects/public/plugin.ts | 25 + .../saved_object}/helpers/apply_es_resp.ts | 8 +- .../helpers/build_saved_object.ts | 4 +- .../helpers/check_for_duplicate_title.ts | 4 +- .../helpers/confirm_modal_promise.tsx | 11 +- .../saved_object}/helpers/create_source.ts | 17 +- .../display_duplicate_title_confirm_modal.ts | 17 +- .../helpers/find_object_by_title.test.ts} | 28 +- .../helpers/find_object_by_title.ts | 8 +- .../helpers/hydrate_index_pattern.ts | 4 +- .../helpers/initialize_saved_object.ts | 2 +- .../helpers/parse_search_source.ts | 6 +- .../helpers/save_saved_object.ts | 4 +- .../helpers/serialize_saved_object.ts | 4 +- .../helpers/string_utils.test.ts | 1 + .../saved_object}/helpers/string_utils.ts | 0 .../public/saved_object}/index.ts | 2 +- .../public/saved_object/saved_object.test.ts} | 569 +++++++++--------- .../public/saved_object}/saved_object.ts | 2 +- .../saved_object}/saved_object_loader.ts | 2 +- .../saved_objects/public}/types.ts | 6 +- .../plugins/graph/public/legacy_imports.ts | 2 - .../services/persistence/saved_workspace.ts | 4 +- .../persistence/saved_workspace_loader.ts | 2 +- .../plugins/graph/public/types/persistence.ts | 2 +- .../services/gis_map_saved_object_loader.js | 2 +- .../public/angular/services/saved_gis_map.js | 2 +- .../translations/translations/ja-JP.json | 14 +- .../translations/translations/zh-CN.json | 14 +- 52 files changed, 467 insertions(+), 431 deletions(-) rename src/{legacy/ui/public/saved_objects/__tests__/simple_saved_object.js => core/public/saved_objects/simple_saved_object.test.ts} (67%) create mode 100644 src/plugins/saved_objects/kibana.json rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public}/constants.ts (83%) create mode 100644 src/plugins/saved_objects/public/plugin.ts rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/apply_es_resp.ts (91%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/build_saved_object.ts (98%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/check_for_duplicate_title.ts (95%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/confirm_modal_promise.tsx (86%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/create_source.ts (85%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/display_duplicate_title_confirm_modal.ts (79%) rename src/{legacy/ui/public/saved_objects/__tests__/find_object_by_title.js => plugins/saved_objects/public/saved_object/helpers/find_object_by_title.test.ts} (72%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/find_object_by_title.ts (90%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/hydrate_index_pattern.ts (92%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/initialize_saved_object.ts (96%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/parse_search_source.ts (94%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/save_saved_object.ts (99%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/serialize_saved_object.ts (96%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/string_utils.test.ts (99%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/string_utils.ts (100%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/index.ts (92%) rename src/{legacy/ui/public/saved_objects/__tests__/saved_object.js => plugins/saved_objects/public/saved_object/saved_object.test.ts} (52%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/saved_object.ts (98%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/saved_object_loader.ts (98%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public}/types.ts (96%) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 9e57fc4c368762..19f62a7d489236 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1170,7 +1170,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | `import 'ui/query_bar'` | `import { QueryStringInput } from '../data/public'` | Directives are deprecated. | | `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../navigation/public'` | Directive is still available in `ui/kbn_top_nav`. | -| `ui/saved_objects/components/saved_object_finder` | `import { SavedObjectFinder } from '../kibana_react/public'` | | +| `ui/saved_objects/components/saved_object_finder` | `import { SavedObjectFinder } from '../saved_objects/public'` | | | `core_plugins/interpreter` | `data.expressions` | still in progress | | `ui/courier` | `data.search` | still in progress | | `ui/embeddable` | `embeddables` | still in progress | diff --git a/src/legacy/ui/public/saved_objects/__tests__/simple_saved_object.js b/src/core/public/saved_objects/simple_saved_object.test.ts similarity index 67% rename from src/legacy/ui/public/saved_objects/__tests__/simple_saved_object.js rename to src/core/public/saved_objects/simple_saved_object.test.ts index f2fc9bfe232e2a..99676f6b78d42d 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/simple_saved_object.js +++ b/src/core/public/saved_objects/simple_saved_object.test.ts @@ -17,36 +17,43 @@ * under the License. */ -import sinon from 'sinon'; -import expect from '@kbn/expect'; -import { SimpleSavedObject } from '../../../../../core/public'; +import { SavedObject } from '../../server'; +import { SimpleSavedObject } from './simple_saved_object'; +import { SavedObjectsClientContract } from './saved_objects_client'; describe('SimpleSavedObject', () => { + let client: SavedObjectsClientContract; + + beforeEach(() => { + client = { + update: jest.fn(), + create: jest.fn(), + delete: jest.fn(), + } as any; + }); + it('persists type and id', () => { const id = 'logstash-*'; const type = 'index-pattern'; - const client = sinon.stub(); - const savedObject = new SimpleSavedObject(client, { id, type }); + const savedObject = new SimpleSavedObject(client, { id, type } as SavedObject); - expect(savedObject.id).to.be(id); - expect(savedObject.type).to.be(type); + expect(savedObject.id).toEqual(id); + expect(savedObject.type).toEqual(type); }); it('persists attributes', () => { const attributes = { title: 'My title' }; - const client = sinon.stub(); - const savedObject = new SimpleSavedObject(client, { attributes }); + const savedObject = new SimpleSavedObject(client, { attributes } as SavedObject); - expect(savedObject.attributes).to.be(attributes); + expect(savedObject.attributes).toEqual(attributes); }); it('persists version', () => { - const version = 2; + const version = '2'; - const client = sinon.stub(); - const savedObject = new SimpleSavedObject(client, { version }); - expect(savedObject._version).to.be(version); + const savedObject = new SimpleSavedObject(client, { version } as SavedObject); + expect(savedObject._version).toEqual(version); }); }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index d5198dc557f04f..c1f679e9eb7ac5 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -24,7 +24,6 @@ * directly where they are needed. */ -export { SavedObjectSaveOpts } from 'ui/saved_objects/types'; export { npSetup, npStart } from 'ui/new_platform'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; export { KbnUrl } from 'ui/url/kbn_url'; @@ -33,7 +32,6 @@ export { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_to // @ts-ignore export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url/index'; export { IInjector } from 'ui/chrome'; -export { SavedObjectLoader } from 'ui/saved_objects'; export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { configureAppAngularModule, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index cc104c1a931d00..7239d8f2258a74 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -38,7 +38,6 @@ import { PrivateProvider, PromiseServiceCreator, RedirectWhenMissingProvider, - SavedObjectLoader, } from '../legacy_imports'; // @ts-ignore import { initDashboardApp } from './legacy_app'; @@ -47,6 +46,7 @@ import { NavigationPublicPluginStart as NavigationStart } from '../../../../../. import { DataPublicPluginStart } from '../../../../../../plugins/data/public'; import { SharePluginStart } from '../../../../../../plugins/share/public'; import { KibanaLegacyStart } from '../../../../../../plugins/kibana_legacy/public'; +import { SavedObjectLoader } from '../../../../../../plugins/saved_objects/public'; export interface RenderDeps { pluginInitializerContext: PluginInitializerContext; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index 465203be0d34c1..075516d52bab63 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -26,9 +26,10 @@ import angular from 'angular'; import { Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; import { History } from 'history'; +import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public'; import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen'; -import { migrateLegacyQuery, SavedObjectSaveOpts, subscribeWithScope } from '../legacy_imports'; +import { migrateLegacyQuery, subscribeWithScope } from '../legacy_imports'; import { esFilters, IndexPattern, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts index d80208ce27ffe4..db2b1f15247de5 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts @@ -18,7 +18,7 @@ */ import { TimefilterContract } from 'src/plugins/data/public'; -import { SavedObjectSaveOpts } from '../../legacy_imports'; +import { SavedObjectSaveOpts } from '../../../../../../../plugins/saved_objects/public'; import { updateSavedDashboard } from './update_saved_dashboard'; import { DashboardStateManager } from '../dashboard_state_manager'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.ts index 5babaf8061de9a..c5ac05b5a77ebc 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.ts @@ -16,8 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObject, SavedObjectKibanaServices } from 'ui/saved_objects/types'; -import { createSavedObjectClass } from 'ui/saved_objects/saved_object'; +import { + createSavedObjectClass, + SavedObject, + SavedObjectKibanaServices, +} from '../../../../../../plugins/saved_objects/public'; import { extractReferences, injectReferences } from './saved_dashboard_references'; import { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.ts index 4ece5d46358ba5..2ff76da9c5ca65 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.ts @@ -17,8 +17,10 @@ * under the License. */ -import { SavedObjectLoader } from 'ui/saved_objects'; -import { SavedObjectKibanaServices } from 'ui/saved_objects/types'; +import { + SavedObjectLoader, + SavedObjectKibanaServices, +} from '../../../../../../plugins/saved_objects/public'; import { createSavedDashboardClass } from './saved_dashboard'; export function createSavedDashboardLoader(services: SavedObjectKibanaServices) { diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts index 113d13287bd122..7bd0eef8c19afd 100644 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts @@ -16,8 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObjectKibanaServices } from 'ui/saved_objects/types'; -import { createSavedObjectClass } from 'ui/saved_objects/saved_object'; + +import { + createSavedObjectClass, + SavedObjectKibanaServices, +} from '../../../../../../plugins/saved_objects/public'; export function createSavedSearchClass(services: SavedObjectKibanaServices) { const SavedObjectClass = createSavedObjectClass(services); diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts index 0b346524610262..ebd341eba99fde 100644 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts @@ -16,8 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObjectLoader } from 'ui/saved_objects'; -import { SavedObjectKibanaServices } from 'ui/saved_objects/types'; + +import { + SavedObjectLoader, + SavedObjectKibanaServices, +} from '../../../../../../plugins/saved_objects/public'; import { createSavedSearchClass } from './_saved_search'; export function createSavedSearchesLoader(services: SavedObjectKibanaServices) { diff --git a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts index 0a6ac205026693..e0756b2e78e259 100644 --- a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts +++ b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts @@ -20,7 +20,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { npStart } from 'ui/new_platform'; -import { SavedObjectLoader } from 'ui/saved_objects'; +import { SavedObjectLoader } from '../../../../../plugins/saved_objects/public'; import { createSavedDashboardLoader } from '../dashboard'; import { createSavedSearchesLoader } from '../discover'; import { TypesService, createSavedVisLoader } from '../../../visualizations/public'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/render.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/render.test.js index af580547b11ed8..1b9dafb6daf237 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/render.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/render.test.js @@ -50,6 +50,7 @@ describe('CreateIndexPatternWizardRender', () => { config: {}, changeUrl: () => {}, indexPatternCreationType: {}, + openConfirm: jest.fn(), }); expect(render.mock.calls.length).toBe(1); diff --git a/src/legacy/core_plugins/timelion/public/directives/saved_object_save_as_checkbox.html b/src/legacy/core_plugins/timelion/public/directives/saved_object_save_as_checkbox.html index 3e4a1526113c3e..5adce4286010a6 100644 --- a/src/legacy/core_plugins/timelion/public/directives/saved_object_save_as_checkbox.html +++ b/src/legacy/core_plugins/timelion/public/directives/saved_object_save_as_checkbox.html @@ -5,7 +5,7 @@ i18n-id="timelion.savedObjects.howToSaveAsNewDescription" i18n-default-message="In previous versions of Kibana, changing the name of a {savedObjectName} would make a copy with the new name. Use the 'Save as a new {savedObjectName}' checkbox to do this now." i18n-values="{ savedObjectName: savedObject.getDisplayName() }" - i18n-description="'Save as a new {savedObjectName}' refers to common.ui.savedObjects.saveAsNewLabel and should be the same text." + i18n-description="'Save as a new {savedObjectName}' refers to timelion.savedObjects.saveAsNewLabel and should be the same text." >