diff --git a/.buildkite/pipelines/hourly.yml b/.buildkite/pipelines/hourly.yml index 8c1162954cfacb..78c57ff3bd1285 100644 --- a/.buildkite/pipelines/hourly.yml +++ b/.buildkite/pipelines/hourly.yml @@ -19,7 +19,7 @@ steps: label: 'Default CI Group' parallelism: 27 agents: - queue: n2-4-spot + queue: n2-4 depends_on: build timeout_in_minutes: 250 key: default-cigroup @@ -31,7 +31,7 @@ steps: - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Docker CI Group' agents: - queue: n2-4-spot + queue: n2-4 depends_on: build timeout_in_minutes: 120 key: default-cigroup-docker @@ -44,7 +44,7 @@ steps: label: 'OSS CI Group' parallelism: 11 agents: - queue: n2-4-spot + queue: ci-group-4d depends_on: build timeout_in_minutes: 120 key: oss-cigroup @@ -56,49 +56,29 @@ steps: - command: .buildkite/scripts/steps/functional/oss_accessibility.sh label: 'OSS Accessibility Tests' agents: - queue: n2-4-spot + queue: ci-group-4d depends_on: build timeout_in_minutes: 120 retry: automatic: - - exit_status: '1' + - exit_status: '*' limit: 1 - - exit_status: '-1' - limit: 3 - - exit_status: '130' - limit: 3 - - exit_status: '137' - limit: 3 - - exit_status: '143' - limit: 3 - - exit_status: '255' - limit: 3 - command: .buildkite/scripts/steps/functional/xpack_accessibility.sh label: 'Default Accessibility Tests' agents: - queue: n2-4-spot + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: automatic: - - exit_status: '1' + - exit_status: '*' limit: 1 - - exit_status: '-1' - limit: 3 - - exit_status: '130' - limit: 3 - - exit_status: '137' - limit: 3 - - exit_status: '143' - limit: 3 - - exit_status: '255' - limit: 3 - command: .buildkite/scripts/steps/functional/oss_firefox.sh label: 'OSS Firefox Tests' agents: - queue: n2-4-spot + queue: ci-group-4d depends_on: build timeout_in_minutes: 120 retry: @@ -109,28 +89,18 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_firefox.sh label: 'Default Firefox Tests' agents: - queue: n2-4-spot + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: automatic: - - exit_status: '1' + - exit_status: '*' limit: 1 - - exit_status: '-1' - limit: 3 - - exit_status: '130' - limit: 3 - - exit_status: '137' - limit: 3 - - exit_status: '143' - limit: 3 - - exit_status: '255' - limit: 3 - command: .buildkite/scripts/steps/functional/oss_misc.sh label: 'OSS Misc Functional Tests' agents: - queue: n2-4-spot + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -141,23 +111,13 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh label: 'Saved Object Field Metrics' agents: - queue: n2-4-spot + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: automatic: - - exit_status: '1' + - exit_status: '*' limit: 1 - - exit_status: '-1' - limit: 3 - - exit_status: '130' - limit: 3 - - exit_status: '137' - limit: 3 - - exit_status: '143' - limit: 3 - - exit_status: '255' - limit: 3 - command: .buildkite/scripts/steps/test/jest.sh label: 'Jest Tests' @@ -178,23 +138,9 @@ steps: - command: .buildkite/scripts/steps/test/api_integration.sh label: 'API Integration Tests' agents: - queue: n2-4-spot + queue: n2-2 timeout_in_minutes: 120 key: api-integration - retry: - automatic: - - exit_status: '1' - limit: 1 - - exit_status: '-1' - limit: 3 - - exit_status: '130' - limit: 3 - - exit_status: '137' - limit: 3 - - exit_status: '143' - limit: 3 - - exit_status: '255' - limit: 3 - command: .buildkite/scripts/steps/lint.sh label: 'Linting' diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 691daa042bba95..0a0aa994fb70bf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -425,8 +425,8 @@ x-pack/plugins/session_view @elastic/awp-platform # Security Asset Management /x-pack/plugins/osquery @elastic/security-asset-management -# Cloud Posture Security -/x-pack/plugins/cloud_security_posture/ @elastic/cloud-posture-security +# Cloud Security Posture +/x-pack/plugins/cloud_security_posture/ @elastic/cloud-security-posture-control-plane # Design (at the bottom for specificity of SASS files) **/*.scss @elastic/kibana-design diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclient.md index 9a04a1d5817658..1dfb1ab7a0b424 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclient.md @@ -9,5 +9,5 @@ Client used to query the elasticsearch cluster. Signature: ```typescript -export declare type ElasticsearchClient = Omit; +export declare type ElasticsearchClient = Omit; ``` diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index d73760b280d496..03948af6379104 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -196,6 +196,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { std_dev: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-extendedstats-aggregation.html`, sum: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-sum-aggregation.html`, top_hits: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-top-hits-aggregation.html`, + top_metrics: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-top-metrics.html`, }, runtimeFields: { overview: `${ELASTICSEARCH_DOCS}runtime.html`, diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel index 6732b08d8bc72a..4dc8d684941f33 100644 --- a/packages/kbn-test/BUILD.bazel +++ b/packages/kbn-test/BUILD.bazel @@ -53,6 +53,7 @@ RUNTIME_DEPS = [ "@npm//execa", "@npm//exit-hook", "@npm//form-data", + "@npm//get-port", "@npm//getopts", "@npm//globby", "@npm//he", @@ -90,6 +91,7 @@ TYPES_DEPS = [ "@npm//del", "@npm//exit-hook", "@npm//form-data", + "@npm//get-port", "@npm//getopts", "@npm//jest", "@npm//jest-cli", diff --git a/packages/kbn-test/src/es/es_client_for_testing.ts b/packages/kbn-test/src/es/es_client_for_testing.ts index 084cb8d77eac57..3eeccffcc2186a 100644 --- a/packages/kbn-test/src/es/es_client_for_testing.ts +++ b/packages/kbn-test/src/es/es_client_for_testing.ts @@ -27,6 +27,22 @@ export interface EsClientForTestingOptions extends Omit +) { + const ccsConfig = config.get('esTestCluster.ccs'); + if (!ccsConfig) { + throw new Error('FTR config is missing esTestCluster.ccs'); + } + + return createEsClientForTesting({ + esUrl: ccsConfig.remoteClusterUrl, + requestTimeout: config.get('timeouts.esRequestTimeout'), + ...overrides, + }); +} + export function createEsClientForFtrConfig( config: Config, overrides?: Omit diff --git a/packages/kbn-test/src/es/index.ts b/packages/kbn-test/src/es/index.ts index 641253acc3647e..bdc33889458279 100644 --- a/packages/kbn-test/src/es/index.ts +++ b/packages/kbn-test/src/es/index.ts @@ -9,5 +9,9 @@ export { createTestEsCluster } from './test_es_cluster'; export type { CreateTestEsClusterOptions, EsTestCluster, ICluster } from './test_es_cluster'; export { esTestConfig } from './es_test_config'; -export { createEsClientForTesting, createEsClientForFtrConfig } from './es_client_for_testing'; +export { + createEsClientForTesting, + createEsClientForFtrConfig, + createRemoteEsClientForFtrConfig, +} from './es_client_for_testing'; export type { EsClientForTestingOptions } from './es_client_for_testing'; diff --git a/packages/kbn-test/src/es/test_es_cluster.ts b/packages/kbn-test/src/es/test_es_cluster.ts index 6e4fc2fb14628f..27f29ce6995a7d 100644 --- a/packages/kbn-test/src/es/test_es_cluster.ts +++ b/packages/kbn-test/src/es/test_es_cluster.ts @@ -136,7 +136,15 @@ export interface CreateTestEsClusterOptions { * } */ port?: number; + /** + * Should this ES cluster use SSL? + */ ssl?: boolean; + /** + * Explicit transport port for a single node to run on, or a string port range to use eg. '9300-9400' + * defaults to the transport port from `packages/kbn-test/src/es/es_test_config.ts` + */ + transportPort?: number | string; } export function createTestEsCluster< @@ -155,13 +163,14 @@ export function createTestEsCluster< esJavaOpts, clusterName: customClusterName = 'es-test-cluster', ssl, + transportPort, } = options; const clusterName = `${CI_PARALLEL_PROCESS_PREFIX}${customClusterName}`; const defaultEsArgs = [ `cluster.name=${clusterName}`, - `transport.port=${esTestConfig.getTransportPort()}`, + `transport.port=${transportPort ?? esTestConfig.getTransportPort()}`, // For multi-node clusters, we make all nodes master-eligible by default. ...(nodes.length > 1 ? ['discovery.type=zen', `cluster.initial_master_nodes=${nodes.map((n) => n.name).join(',')}`] diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 42a77b85ddc6c3..cf1afbb810c713 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -192,12 +192,17 @@ export const schema = Joi.object() esTestCluster: Joi.object() .keys({ - license: Joi.string().default('basic'), + license: Joi.valid('basic', 'trial', 'gold').default('basic'), from: Joi.string().default('snapshot'), - serverArgs: Joi.array(), + serverArgs: Joi.array().items(Joi.string()), esJavaOpts: Joi.string(), dataArchive: Joi.string(), ssl: Joi.boolean().default(false), + ccs: Joi.object().keys({ + remoteClusterUrl: Joi.string().uri({ + scheme: /https?/, + }), + }), }) .default(), @@ -290,6 +295,7 @@ export const schema = Joi.object() security: Joi.object() .keys({ roles: Joi.object().default(), + remoteEsRoles: Joi.object(), defaultRoles: Joi.array() .items(Joi.string()) .when('$primary', { diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts index 68e7a4992fcfc8..ba314e8325a657 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts @@ -8,6 +8,7 @@ import { resolve } from 'path'; import type { ToolingLog } from '@kbn/dev-utils'; +import getPort from 'get-port'; import { KIBANA_ROOT } from './paths'; import type { Config } from '../../functional_test_runner/'; import { createTestEsCluster } from '../../es'; @@ -15,32 +16,102 @@ import { createTestEsCluster } from '../../es'; interface RunElasticsearchOptions { log: ToolingLog; esFrom?: string; + config: Config; +} + +interface CcsConfig { + remoteClusterUrl: string; } -export async function runElasticsearch({ + +type EsConfig = ReturnType; + +function getEsConfig({ config, - options, -}: { - config: Config; - options: RunElasticsearchOptions; -}) { - const { log, esFrom } = options; - const ssl = config.get('esTestCluster.ssl'); - const license = config.get('esTestCluster.license'); - const esArgs = config.get('esTestCluster.serverArgs'); - const esJavaOpts = config.get('esTestCluster.esJavaOpts'); + esFrom = config.get('esTestCluster.from'), +}: RunElasticsearchOptions) { + const ssl = !!config.get('esTestCluster.ssl'); + const license: 'basic' | 'trial' | 'gold' = config.get('esTestCluster.license'); + const esArgs: string[] = config.get('esTestCluster.serverArgs') ?? []; + const esJavaOpts: string | undefined = config.get('esTestCluster.esJavaOpts'); const isSecurityEnabled = esArgs.includes('xpack.security.enabled=true'); - const cluster = createTestEsCluster({ - port: config.get('servers.elasticsearch.port'), - password: isSecurityEnabled ? 'changeme' : config.get('servers.elasticsearch.password'), + const port: number | undefined = config.get('servers.elasticsearch.port'); + const ccsConfig: CcsConfig | undefined = config.get('esTestCluster.ccs'); + + const password: string | undefined = isSecurityEnabled + ? 'changeme' + : config.get('servers.elasticsearch.password'); + + const dataArchive: string | undefined = config.get('esTestCluster.dataArchive'); + + return { + ssl, license, - log, - basePath: resolve(KIBANA_ROOT, '.es'), - esFrom: esFrom || config.get('esTestCluster.from'), - dataArchive: config.get('esTestCluster.dataArchive'), esArgs, esJavaOpts, - ssl, + isSecurityEnabled, + esFrom, + port, + password, + dataArchive, + ccsConfig, + }; +} + +export async function runElasticsearch( + options: RunElasticsearchOptions +): Promise<() => Promise> { + const { log } = options; + const config = getEsConfig(options); + + if (!config.ccsConfig) { + const node = await startEsNode(log, 'ftr', config); + return async () => { + await node.cleanup(); + }; + } + + const remotePort = await getPort(); + const remoteNode = await startEsNode(log, 'ftr-remote', { + ...config, + port: parseInt(new URL(config.ccsConfig.remoteClusterUrl).port, 10), + transportPort: remotePort, + }); + + const localNode = await startEsNode(log, 'ftr-local', { + ...config, + esArgs: [...config.esArgs, `cluster.remote.ftr-remote.seeds=localhost:${remotePort}`], + }); + + return async () => { + await localNode.cleanup(); + await remoteNode.cleanup(); + }; +} + +async function startEsNode( + log: ToolingLog, + name: string, + config: EsConfig & { transportPort?: number } +) { + const cluster = createTestEsCluster({ + clusterName: `cluster-${name}`, + esArgs: config.esArgs, + esFrom: config.esFrom, + esJavaOpts: config.esJavaOpts, + license: config.license, + password: config.password, + port: config.port, + ssl: config.ssl, + log, + basePath: resolve(KIBANA_ROOT, '.es'), + nodes: [ + { + name, + dataArchive: config.dataArchive, + }, + ], + transportPort: config.transportPort, }); await cluster.start(); diff --git a/packages/kbn-test/src/functional_tests/tasks.ts b/packages/kbn-test/src/functional_tests/tasks.ts index 5906193ca145c7..b1213ceef2905e 100644 --- a/packages/kbn-test/src/functional_tests/tasks.ts +++ b/packages/kbn-test/src/functional_tests/tasks.ts @@ -108,10 +108,10 @@ export async function runTests(options: RunTestsParams) { await withProcRunner(log, async (procs) => { const config = await readConfigFile(log, options.esVersion, configPath); - let es; + let shutdownEs; try { if (process.env.TEST_ES_DISABLE_STARTUP !== 'true') { - es = await runElasticsearch({ config, options: { ...options, log } }); + shutdownEs = await runElasticsearch({ ...options, log, config }); } await runKibanaServer({ procs, config, options }); await runFtr({ configPath, options: { ...options, log } }); @@ -125,8 +125,8 @@ export async function runTests(options: RunTestsParams) { await procs.stop('kibana'); } finally { - if (es) { - await es.cleanup(); + if (shutdownEs) { + await shutdownEs(); } } } @@ -166,7 +166,7 @@ export async function startServers({ ...options }: StartServerOptions) { await withProcRunner(log, async (procs) => { const config = await readConfigFile(log, options.esVersion, options.config); - const es = await runElasticsearch({ config, options: opts }); + const shutdownEs = await runElasticsearch({ ...opts, config }); await runKibanaServer({ procs, config, @@ -190,7 +190,7 @@ export async function startServers({ ...options }: StartServerOptions) { log.success(makeSuccessMessage(options)); await procs.waitForAllToStop(); - await es.cleanup(); + await shutdownEs(); }); } diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index 26d40f70edb780..c9f0e67c558f19 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -36,6 +36,7 @@ export { createTestEsCluster, createEsClientForTesting, createEsClientForFtrConfig, + createRemoteEsClientForFtrConfig, } from './es'; export { diff --git a/packages/kbn-type-summarizer/BUILD.bazel b/packages/kbn-type-summarizer/BUILD.bazel index 13a89e0669b80f..ec0df11bc3762f 100644 --- a/packages/kbn-type-summarizer/BUILD.bazel +++ b/packages/kbn-type-summarizer/BUILD.bazel @@ -10,10 +10,7 @@ PKG_REQUIRE_NAME = "@kbn/type-summarizer" SOURCE_FILES = glob( [ "src/**/*.ts", - ], - exclude = [ - "**/*.test.*" - ], + ] ) SRCS = SOURCE_FILES @@ -49,6 +46,7 @@ TYPES_DEPS = [ "@npm//is-path-inside", "@npm//normalize-path", "@npm//source-map", + "@npm//strip-ansi", "@npm//tslib", ] diff --git a/packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts b/packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts index a0fdb3e4685b1f..7a0d9be4629cd7 100644 --- a/packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts +++ b/packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import Path from 'path'; import Fs from 'fs'; import { CliError } from './cli_error'; import { parseCliFlags } from './cli_flags'; +import * as Path from './path'; const TYPE_SUMMARIZER_PACKAGES = ['@kbn/type-summarizer', '@kbn/crypto']; @@ -25,6 +25,36 @@ interface BazelCliConfig { use: 'api-extractor' | 'type-summarizer'; } +function isKibanaRepo(dir: string) { + try { + const json = Fs.readFileSync(Path.join(dir, 'package.json'), 'utf8'); + const parsed = JSON.parse(json); + return parsed.name === 'kibana'; + } catch { + return false; + } +} + +function findRepoRoot() { + const start = Path.resolve(__dirname); + let dir = start; + while (true) { + if (isKibanaRepo(dir)) { + return dir; + } + + // this is not the kibana directory, try moving up a directory + const parent = Path.join(dir, '..'); + if (parent === dir) { + throw new Error( + `unable to find Kibana's package.json file when traversing up from [${start}]` + ); + } + + dir = parent; + } +} + export function parseBazelCliFlags(argv: string[]): BazelCliConfig { const { rawFlags, unknownFlags } = parseCliFlags(argv, { string: ['use'], @@ -39,19 +69,7 @@ export function parseBazelCliFlags(argv: string[]): BazelCliConfig { }); } - let REPO_ROOT; - try { - const name = 'utils'; - // eslint-disable-next-line @typescript-eslint/no-var-requires - const utils = require('@kbn/' + name); - REPO_ROOT = utils.REPO_ROOT as string; - } catch (error) { - if (error && error.code === 'MODULE_NOT_FOUND') { - throw new CliError('type-summarizer bazel cli only works after bootstrap'); - } - - throw error; - } + const repoRoot = findRepoRoot(); const [relativePackagePath, ...extraPositional] = rawFlags._; if (typeof relativePackagePath !== 'string') { @@ -70,26 +88,45 @@ export function parseBazelCliFlags(argv: string[]): BazelCliConfig { const packageName: string = JSON.parse( Fs.readFileSync(Path.join(packageDir, 'package.json'), 'utf8') ).name; - const repoRelativePackageDir = Path.relative(REPO_ROOT, packageDir); + const repoRelativePackageDir = Path.relative(repoRoot, packageDir); return { use, packageName, - tsconfigPath: Path.join(REPO_ROOT, repoRelativePackageDir, 'tsconfig.json'), - inputPath: Path.resolve(REPO_ROOT, 'node_modules', packageName, 'target_types/index.d.ts'), + tsconfigPath: Path.join(repoRoot, repoRelativePackageDir, 'tsconfig.json'), + inputPath: Path.join(repoRoot, 'node_modules', packageName, 'target_types/index.d.ts'), repoRelativePackageDir, - outputDir: Path.resolve(REPO_ROOT, 'data/type-summarizer-output', use), + outputDir: Path.join(repoRoot, 'data/type-summarizer-output', use), }; } -export function parseBazelCliJson(json: string): BazelCliConfig { - let config; +function parseJsonFromCli(json: string) { try { - config = JSON.parse(json); + return JSON.parse(json); } catch (error) { - throw new CliError('unable to parse first positional argument as JSON'); + // TODO: This is to handle a bug in Bazel which escapes `"` in .bat arguments incorrectly, replacing them with `\` + if ( + error.message === 'Unexpected token \\ in JSON at position 1' && + process.platform === 'win32' + ) { + const unescapedJson = json.replaceAll('\\', '"'); + try { + return JSON.parse(unescapedJson); + } catch (e) { + throw new CliError( + `unable to parse first positional argument as JSON: "${e.message}"\n unescaped value: ${unescapedJson}\n raw value: ${json}` + ); + } + } + + throw new CliError( + `unable to parse first positional argument as JSON: "${error.message}"\n value: ${json}` + ); } +} +export function parseBazelCliJson(json: string): BazelCliConfig { + const config = parseJsonFromCli(json); if (typeof config !== 'object' || config === null) { throw new CliError('config JSON must be an object'); } @@ -131,14 +168,12 @@ export function parseBazelCliJson(json: string): BazelCliConfig { throw new CliError(`buildFilePath [${buildFilePath}] must be a relative path`); } - const repoRelativePackageDir = Path.dirname(buildFilePath); - return { packageName, outputDir: Path.resolve(outputDir), tsconfigPath: Path.resolve(tsconfigPath), inputPath: Path.resolve(inputPath), - repoRelativePackageDir, + repoRelativePackageDir: Path.dirname(buildFilePath), use: TYPE_SUMMARIZER_PACKAGES.includes(packageName) ? 'type-summarizer' : 'api-extractor', }; } diff --git a/packages/kbn-type-summarizer/src/lib/is_node_module.ts b/packages/kbn-type-summarizer/src/lib/is_node_module.ts index 67efde569a1b4e..ba4d607ccb8640 100644 --- a/packages/kbn-type-summarizer/src/lib/is_node_module.ts +++ b/packages/kbn-type-summarizer/src/lib/is_node_module.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import Path from 'path'; - import isPathInside from 'is-path-inside'; +import * as Path from './path'; + export function isNodeModule(dtsDir: string, path: string) { return (isPathInside(path, dtsDir) ? Path.relative(dtsDir, path) : path) - .split(Path.sep) + .split('/') .includes('node_modules'); } diff --git a/packages/kbn-type-summarizer/src/lib/path.ts b/packages/kbn-type-summarizer/src/lib/path.ts new file mode 100644 index 00000000000000..79d56aa58fab64 --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/path.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; + +import normalizePath from 'normalize-path'; +const cwd = normalizePath(process.cwd()); + +export function cwdRelative(path: string) { + return relative(cwd, path); +} + +export function relative(from: string, to: string) { + return normalizePath(Path.relative(from, to)); +} + +export function join(...segments: string[]) { + return Path.join(...segments); +} + +export function dirname(path: string) { + return Path.dirname(path); +} + +export function resolve(path: string) { + return Path.isAbsolute(path) ? normalizePath(path) : join(cwd, path); +} + +export function isAbsolute(path: string) { + return Path.isAbsolute(path); +} diff --git a/packages/kbn-type-summarizer/src/lib/printer.ts b/packages/kbn-type-summarizer/src/lib/printer.ts index 3ce675f7279275..8ecc4356ea4a23 100644 --- a/packages/kbn-type-summarizer/src/lib/printer.ts +++ b/packages/kbn-type-summarizer/src/lib/printer.ts @@ -6,11 +6,10 @@ * Side Public License, v 1. */ -import Path from 'path'; - import * as ts from 'typescript'; import { SourceNode, CodeWithSourceMap } from 'source-map'; +import * as Path from './path'; import { findKind } from './ts_nodes'; import { SourceMapper } from './source_mapper'; import { CollectorResult } from './export_collector'; diff --git a/packages/kbn-type-summarizer/src/lib/source_mapper.ts b/packages/kbn-type-summarizer/src/lib/source_mapper.ts index f6075684e04a6d..1f03119e8e3fd2 100644 --- a/packages/kbn-type-summarizer/src/lib/source_mapper.ts +++ b/packages/kbn-type-summarizer/src/lib/source_mapper.ts @@ -6,16 +6,16 @@ * Side Public License, v 1. */ -import Path from 'path'; - import * as ts from 'typescript'; import { SourceNode, SourceMapConsumer, BasicSourceMapConsumer } from 'source-map'; -import normalizePath from 'normalize-path'; import { Logger } from './log'; import { tryReadFile } from './helpers/fs'; import { parseJson } from './helpers/json'; import { isNodeModule } from './is_node_module'; +import * as Path from './path'; + +type SourceMapConsumerEntry = [ts.SourceFile, BasicSourceMapConsumer | undefined]; export class SourceMapper { static async forSourceFiles( @@ -24,10 +24,8 @@ export class SourceMapper { repoRelativePackageDir: string, sourceFiles: readonly ts.SourceFile[] ) { - const consumers = new Map(); - - await Promise.all( - sourceFiles.map(async (sourceFile) => { + const entries = await Promise.all( + sourceFiles.map(async (sourceFile): Promise => { if (isNodeModule(dtsDir, sourceFile.fileName)) { return; } @@ -35,13 +33,12 @@ export class SourceMapper { const text = sourceFile.getText(); const match = text.match(/^\/\/#\s*sourceMappingURL=(.*)/im); if (!match) { - consumers.set(sourceFile, undefined); - return; + return [sourceFile, undefined]; } - const relSourceFile = Path.relative(process.cwd(), sourceFile.fileName); - const sourceMapPath = Path.resolve(Path.dirname(sourceFile.fileName), match[1]); - const relSourceMapPath = Path.relative(process.cwd(), sourceMapPath); + const relSourceFile = Path.cwdRelative(sourceFile.fileName); + const sourceMapPath = Path.join(Path.dirname(sourceFile.fileName), match[1]); + const relSourceMapPath = Path.cwdRelative(sourceMapPath); const sourceJson = await tryReadFile(sourceMapPath, 'utf8'); if (!sourceJson) { throw new Error( @@ -50,11 +47,16 @@ export class SourceMapper { } const json = parseJson(sourceJson, `source map at [${relSourceMapPath}]`); - consumers.set(sourceFile, await new SourceMapConsumer(json)); - log.debug('loaded sourcemap for', relSourceFile); + return [sourceFile, await new SourceMapConsumer(json)]; }) ); + const consumers = new Map(entries.filter((e): e is SourceMapConsumerEntry => !!e)); + log.debug( + 'loaded sourcemaps for', + Array.from(consumers.keys()).map((s) => Path.relative(process.cwd(), s.fileName)) + ); + return new SourceMapper(consumers, repoRelativePackageDir); } @@ -77,7 +79,7 @@ export class SourceMapper { * us the path to the source, relative to the `repoRelativePackageDir`. */ fixSourcePath(source: string) { - return normalizePath(Path.relative(this.sourceFixDir, Path.join('/', source))); + return Path.relative(this.sourceFixDir, Path.join('/', source)); } getSourceNode(generatedNode: ts.Node, code: string) { diff --git a/packages/kbn-type-summarizer/src/lib/tsconfig_file.ts b/packages/kbn-type-summarizer/src/lib/tsconfig_file.ts index 7d327b1f03e0a3..f3d491c93abcbe 100644 --- a/packages/kbn-type-summarizer/src/lib/tsconfig_file.ts +++ b/packages/kbn-type-summarizer/src/lib/tsconfig_file.ts @@ -7,8 +7,8 @@ */ import * as ts from 'typescript'; -import Path from 'path'; +import * as Path from './path'; import { CliError } from './cli_error'; export function readTsConfigFile(path: string) { diff --git a/packages/kbn-type-summarizer/tests/integration_helpers.ts b/packages/kbn-type-summarizer/src/tests/integration_helpers.ts similarity index 91% rename from packages/kbn-type-summarizer/tests/integration_helpers.ts rename to packages/kbn-type-summarizer/src/tests/integration_helpers.ts index 68e1f3cc3a3b0f..c64e58c4e33f93 100644 --- a/packages/kbn-type-summarizer/tests/integration_helpers.ts +++ b/packages/kbn-type-summarizer/src/tests/integration_helpers.ts @@ -13,13 +13,14 @@ import Fsp from 'fs/promises'; import * as ts from 'typescript'; import stripAnsi from 'strip-ansi'; +import normalizePath from 'normalize-path'; -import { loadTsConfigFile } from '../src/lib/tsconfig_file'; -import { createTsProject } from '../src/lib/ts_project'; -import { TestLog } from '../src/lib/log'; -import { summarizePackage } from '../src/summarize_package'; +import { loadTsConfigFile } from '../lib/tsconfig_file'; +import { createTsProject } from '../lib/ts_project'; +import { TestLog } from '../lib/log'; +import { summarizePackage } from '../summarize_package'; -const TMP_DIR = Path.resolve(__dirname, '__tmp__'); +const TMP_DIR = Path.resolve(__dirname, '../../__tmp__'); const DIAGNOSTIC_HOST = { getCanonicalFileName: (p: string) => p, @@ -153,11 +154,11 @@ class MockCli { // summarize the .d.ts files into the output dir await summarizePackage(log, { - dtsDir: this.dtsOutputDir, - inputPaths: [this.inputPath], - outputDir: this.outputDir, + dtsDir: normalizePath(this.dtsOutputDir), + inputPaths: [normalizePath(this.inputPath)], + outputDir: normalizePath(this.outputDir), repoRelativePackageDir: 'src', - tsconfigPath: this.tsconfigPath, + tsconfigPath: normalizePath(this.tsconfigPath), strictPrinting: false, }); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/class.test.ts b/packages/kbn-type-summarizer/src/tests/integration_tests/class.test.ts similarity index 95% rename from packages/kbn-type-summarizer/tests/integration_tests/class.test.ts rename to packages/kbn-type-summarizer/src/tests/integration_tests/class.test.ts index 84c1ee80c5f166..eaf87cda8521ba 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/class.test.ts +++ b/packages/kbn-type-summarizer/src/tests/integration_tests/class.test.ts @@ -69,7 +69,7 @@ it('prints basic class correctly', async () => { } `); expect(output.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/__tmp__/dist_dts/index.d.ts' ] debug Ignoring 1 global declarations for \\"Record\\" debug Ignoring 5 global declarations for \\"Promise\\" " diff --git a/packages/kbn-type-summarizer/tests/integration_tests/function.test.ts b/packages/kbn-type-summarizer/src/tests/integration_tests/function.test.ts similarity index 90% rename from packages/kbn-type-summarizer/tests/integration_tests/function.test.ts rename to packages/kbn-type-summarizer/src/tests/integration_tests/function.test.ts index 6afc04afe8faad..de0f1bb4c6d4c0 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/function.test.ts +++ b/packages/kbn-type-summarizer/src/tests/integration_tests/function.test.ts @@ -74,8 +74,10 @@ it('prints the function declaration, including comments', async () => { } `); expect(result.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/bar.d.ts - debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ + 'packages/kbn-type-summarizer/__tmp__/dist_dts/bar.d.ts', + 'packages/kbn-type-summarizer/__tmp__/dist_dts/index.d.ts' + ] " `); }); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/import_boundary.test.ts b/packages/kbn-type-summarizer/src/tests/integration_tests/import_boundary.test.ts similarity index 91% rename from packages/kbn-type-summarizer/tests/integration_tests/import_boundary.test.ts rename to packages/kbn-type-summarizer/src/tests/integration_tests/import_boundary.test.ts index f23b6c3656d508..f1e3279bb57b0d 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/import_boundary.test.ts +++ b/packages/kbn-type-summarizer/src/tests/integration_tests/import_boundary.test.ts @@ -52,7 +52,7 @@ it('output type links to named import from node modules', async () => { } `); expect(output.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/__tmp__/dist_dts/index.d.ts' ] " `); }); @@ -84,7 +84,7 @@ it('output type links to default import from node modules', async () => { } `); expect(output.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/__tmp__/dist_dts/index.d.ts' ] " `); }); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/interface.test.ts b/packages/kbn-type-summarizer/src/tests/integration_tests/interface.test.ts similarity index 93% rename from packages/kbn-type-summarizer/tests/integration_tests/interface.test.ts rename to packages/kbn-type-summarizer/src/tests/integration_tests/interface.test.ts index da53e91302eef0..cbccbfb1d77dc8 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/interface.test.ts +++ b/packages/kbn-type-summarizer/src/tests/integration_tests/interface.test.ts @@ -55,7 +55,7 @@ it('prints the whole interface, including comments', async () => { } `); expect(result.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/__tmp__/dist_dts/index.d.ts' ] debug Ignoring 5 global declarations for \\"Promise\\" " `); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/references.test.ts b/packages/kbn-type-summarizer/src/tests/integration_tests/references.test.ts similarity index 87% rename from packages/kbn-type-summarizer/tests/integration_tests/references.test.ts rename to packages/kbn-type-summarizer/src/tests/integration_tests/references.test.ts index 1733b43694000d..0a2cc9aaf5857f 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/references.test.ts +++ b/packages/kbn-type-summarizer/src/tests/integration_tests/references.test.ts @@ -59,9 +59,11 @@ it('collects references from source files which contribute to result', async () } `); expect(result.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/files/foo.d.ts - debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/files/index.d.ts - debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ + 'packages/kbn-type-summarizer/__tmp__/dist_dts/files/foo.d.ts', + 'packages/kbn-type-summarizer/__tmp__/dist_dts/files/index.d.ts', + 'packages/kbn-type-summarizer/__tmp__/dist_dts/index.d.ts' + ] debug Ignoring 5 global declarations for \\"Promise\\" debug Ignoring 4 global declarations for \\"Symbol\\" debug Ignoring 2 global declarations for \\"Component\\" diff --git a/packages/kbn-type-summarizer/tests/integration_tests/type_alias.test.ts b/packages/kbn-type-summarizer/src/tests/integration_tests/type_alias.test.ts similarity index 91% rename from packages/kbn-type-summarizer/tests/integration_tests/type_alias.test.ts rename to packages/kbn-type-summarizer/src/tests/integration_tests/type_alias.test.ts index 79c2ea69b94777..cbe99c54ca0425 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/type_alias.test.ts +++ b/packages/kbn-type-summarizer/src/tests/integration_tests/type_alias.test.ts @@ -36,7 +36,7 @@ it('prints basic type alias', async () => { } `); expect(output.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/__tmp__/dist_dts/index.d.ts' ] " `); }); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/variables.test.ts b/packages/kbn-type-summarizer/src/tests/integration_tests/variables.test.ts similarity index 94% rename from packages/kbn-type-summarizer/tests/integration_tests/variables.test.ts rename to packages/kbn-type-summarizer/src/tests/integration_tests/variables.test.ts index daa6abcc34c594..a2b47d64710252 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/variables.test.ts +++ b/packages/kbn-type-summarizer/src/tests/integration_tests/variables.test.ts @@ -62,7 +62,7 @@ it('prints basic variable exports with sourcemaps', async () => { } `); expect(output.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/__tmp__/dist_dts/index.d.ts' ] " `); }); diff --git a/packages/kbn-type-summarizer/tsconfig.json b/packages/kbn-type-summarizer/tsconfig.json index f3c3802071ac46..b3779bdd686ea0 100644 --- a/packages/kbn-type-summarizer/tsconfig.json +++ b/packages/kbn-type-summarizer/tsconfig.json @@ -11,7 +11,6 @@ ] }, "include": [ - "src/**/*", - "tests/**/*" + "src/**/*" ] } diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 972c682ab0d9f8..b185bcb0ea5d0c 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -9,7 +9,7 @@ require('../src/setup_node_env'); require('@kbn/test').runTestsCli([ require.resolve('../test/functional/config.js'), - require.resolve('../test/functional_ccs/config.js'), + require.resolve('../test/functional_ccs/config.ts'), require.resolve('../test/plugin_functional/config.ts'), require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), require.resolve('../test/new_visualize_flow/config.ts'), diff --git a/src/core/server/elasticsearch/client/types.ts b/src/core/server/elasticsearch/client/types.ts index 68fbc87193074f..17248e491962d5 100644 --- a/src/core/server/elasticsearch/client/types.ts +++ b/src/core/server/elasticsearch/client/types.ts @@ -15,7 +15,7 @@ import type { Client } from '@elastic/elasticsearch'; */ export type ElasticsearchClient = Omit< Client, - 'connectionPool' | 'serializer' | 'extend' | 'child' | 'close' | 'diagnostic' + 'connectionPool' | 'serializer' | 'extend' | 'close' | 'diagnostic' >; /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 5fe1942ed8453b..6d5b06346225b7 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -886,7 +886,7 @@ export { EcsEventOutcome } export { EcsEventType } // @public -export type ElasticsearchClient = Omit; +export type ElasticsearchClient = Omit; // @public export type ElasticsearchClientConfig = Pick & { diff --git a/src/fixtures/telemetry_collectors/imported_interface_from_export/index.ts b/src/fixtures/telemetry_collectors/imported_interface_from_export/index.ts index 222a89bf322984..991f8336e70202 100644 --- a/src/fixtures/telemetry_collectors/imported_interface_from_export/index.ts +++ b/src/fixtures/telemetry_collectors/imported_interface_from_export/index.ts @@ -11,7 +11,7 @@ import { createUsageCollectionSetupMock } from '../../../plugins/usage_collectio const { makeUsageCollector } = createUsageCollectionSetupMock(); -export const myCollector = makeUsageCollector({ +export const myCollector = makeUsageCollector({ type: 'importing_from_export_collector', isReady: () => true, fetch() { diff --git a/src/fixtures/telemetry_collectors/stats_collector.ts b/src/fixtures/telemetry_collectors/stats_collector.ts index c8f513a07253ba..6046973f42e849 100644 --- a/src/fixtures/telemetry_collectors/stats_collector.ts +++ b/src/fixtures/telemetry_collectors/stats_collector.ts @@ -19,7 +19,7 @@ interface Usage { * We should collect them when the schema is defined. */ -export const myCollectorWithSchema = makeStatsCollector({ +export const myCollectorWithSchema = makeStatsCollector({ type: 'my_stats_collector_with_schema', isReady: () => true, fetch() { diff --git a/src/plugins/console/public/application/components/settings_modal.tsx b/src/plugins/console/public/application/components/settings_modal.tsx index c4be329dabcb88..eafc2dea3f8734 100644 --- a/src/plugins/console/public/application/components/settings_modal.tsx +++ b/src/plugins/console/public/application/components/settings_modal.tsx @@ -70,6 +70,7 @@ export function DevToolsSettingsModal(props: Props) { const [fields, setFields] = useState(props.settings.autocomplete.fields); const [indices, setIndices] = useState(props.settings.autocomplete.indices); const [templates, setTemplates] = useState(props.settings.autocomplete.templates); + const [dataStreams, setDataStreams] = useState(props.settings.autocomplete.dataStreams); const [polling, setPolling] = useState(props.settings.polling); const [pollInterval, setPollInterval] = useState(props.settings.pollInterval); const [tripleQuotes, setTripleQuotes] = useState(props.settings.tripleQuotes); @@ -97,12 +98,20 @@ export function DevToolsSettingsModal(props: Props) { }), stateSetter: setTemplates, }, + { + id: 'dataStreams', + label: i18n.translate('console.settingsPage.dataStreamsLabelText', { + defaultMessage: 'Data streams', + }), + stateSetter: setDataStreams, + }, ]; const checkboxIdToSelectedMap = { fields, indices, templates, + dataStreams, }; const onAutocompleteChange = (optionId: AutocompleteOptions) => { @@ -120,6 +129,7 @@ export function DevToolsSettingsModal(props: Props) { fields, indices, templates, + dataStreams, }, polling, pollInterval, @@ -170,6 +180,7 @@ export function DevToolsSettingsModal(props: Props) { fields, indices, templates, + dataStreams, }); }} > diff --git a/src/plugins/console/public/lib/autocomplete/components/data_stream_autocomplete_component.js b/src/plugins/console/public/lib/autocomplete/components/data_stream_autocomplete_component.js new file mode 100644 index 00000000000000..015136b7670f50 --- /dev/null +++ b/src/plugins/console/public/lib/autocomplete/components/data_stream_autocomplete_component.js @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getDataStreams } from '../../mappings/mappings'; +import { ListComponent } from './list_component'; + +export class DataStreamAutocompleteComponent extends ListComponent { + constructor(name, parent, multiValued) { + super(name, getDataStreams, parent, multiValued); + } + + getContextKey() { + return 'data_stream'; + } +} diff --git a/src/plugins/console/public/lib/autocomplete/components/index.js b/src/plugins/console/public/lib/autocomplete/components/index.js index 32078ee2c1519a..4a8838a6fb821f 100644 --- a/src/plugins/console/public/lib/autocomplete/components/index.js +++ b/src/plugins/console/public/lib/autocomplete/components/index.js @@ -23,4 +23,5 @@ export { IdAutocompleteComponent } from './id_autocomplete_component'; export { UsernameAutocompleteComponent } from './username_autocomplete_component'; export { IndexTemplateAutocompleteComponent } from './index_template_autocomplete_component'; export { ComponentTemplateAutocompleteComponent } from './component_template_autocomplete_component'; +export { DataStreamAutocompleteComponent } from './data_stream_autocomplete_component'; export * from './legacy'; diff --git a/src/plugins/console/public/lib/kb/kb.js b/src/plugins/console/public/lib/kb/kb.js index 5f02365a48fdf9..e268f55be558e2 100644 --- a/src/plugins/console/public/lib/kb/kb.js +++ b/src/plugins/console/public/lib/kb/kb.js @@ -16,6 +16,7 @@ import { UsernameAutocompleteComponent, IndexTemplateAutocompleteComponent, ComponentTemplateAutocompleteComponent, + DataStreamAutocompleteComponent, } from '../autocomplete/components'; import $ from 'jquery'; @@ -94,6 +95,9 @@ const parametrizedComponentFactories = { component_template: function (name, parent) { return new ComponentTemplateAutocompleteComponent(name, parent); }, + data_stream: function (name, parent) { + return new DataStreamAutocompleteComponent(name, parent); + }, }; export function getUnmatchedEndpointComponents() { diff --git a/src/plugins/console/public/lib/mappings/mapping.test.js b/src/plugins/console/public/lib/mappings/mapping.test.js index 9191eb736be3c7..e2def74e892cc0 100644 --- a/src/plugins/console/public/lib/mappings/mapping.test.js +++ b/src/plugins/console/public/lib/mappings/mapping.test.js @@ -266,4 +266,13 @@ describe('Mappings', () => { expect(mappings.getIndexTemplates()).toEqual(expectedResult); expect(mappings.getComponentTemplates()).toEqual(expectedResult); }); + + test('Data streams', function () { + mappings.loadDataStreams({ + data_streams: [{ name: 'test_index1' }, { name: 'test_index2' }, { name: 'test_index3' }], + }); + + const expectedResult = ['test_index1', 'test_index2', 'test_index3']; + expect(mappings.getDataStreams()).toEqual(expectedResult); + }); }); diff --git a/src/plugins/console/public/lib/mappings/mappings.js b/src/plugins/console/public/lib/mappings/mappings.js index 75b8a263e8690c..96a5665e730a2b 100644 --- a/src/plugins/console/public/lib/mappings/mappings.js +++ b/src/plugins/console/public/lib/mappings/mappings.js @@ -17,6 +17,7 @@ let perAliasIndexes = []; let legacyTemplates = []; let indexTemplates = []; let componentTemplates = []; +let dataStreams = []; const mappingObj = {}; @@ -60,6 +61,10 @@ export function getComponentTemplates() { return [...componentTemplates]; } +export function getDataStreams() { + return [...dataStreams]; +} + export function getFields(indices, types) { // get fields for indices and types. Both can be a list, a string or null (meaning all). let ret = []; @@ -128,7 +133,9 @@ export function getTypes(indices) { export function getIndices(includeAliases) { const ret = []; $.each(perIndexTypes, function (index) { - ret.push(index); + if (!index.startsWith('.ds')) { + ret.push(index); + } }); if (typeof includeAliases === 'undefined' ? true : includeAliases) { $.each(perAliasIndexes, function (alias) { @@ -204,6 +211,10 @@ export function loadComponentTemplates(data) { componentTemplates = (data.component_templates ?? []).map(({ name }) => name); } +export function loadDataStreams(data) { + dataStreams = (data.data_streams ?? []).map(({ name }) => name); +} + export function loadMappings(mappings) { perIndexTypes = {}; @@ -265,6 +276,7 @@ function retrieveSettings(settingsKey, settingsToRetrieve) { legacyTemplates: '_template', indexTemplates: '_index_template', componentTemplates: '_component_template', + dataStreams: '_data_stream', }; // Fetch autocomplete info if setting is set to true, and if user has made changes. @@ -326,14 +338,16 @@ export function retrieveAutoCompleteInfo(settings, settingsToRetrieve) { 'componentTemplates', templatesSettingToRetrieve ); + const dataStreamsPromise = retrieveSettings('dataStreams', settingsToRetrieve); $.when( mappingPromise, aliasesPromise, legacyTemplatesPromise, indexTemplatesPromise, - componentTemplatesPromise - ).done((mappings, aliases, legacyTemplates, indexTemplates, componentTemplates) => { + componentTemplatesPromise, + dataStreamsPromise + ).done((mappings, aliases, legacyTemplates, indexTemplates, componentTemplates, dataStreams) => { let mappingsResponse; try { if (mappings && mappings.length) { @@ -365,6 +379,10 @@ export function retrieveAutoCompleteInfo(settings, settingsToRetrieve) { loadComponentTemplates(JSON.parse(componentTemplates[0])); } + if (dataStreams) { + loadDataStreams(JSON.parse(dataStreams[0])); + } + if (mappings && aliases) { // Trigger an update event with the mappings, aliases $(mappingObj).trigger('update', [mappingsResponse, aliases[0]]); diff --git a/src/plugins/console/public/services/settings.ts b/src/plugins/console/public/services/settings.ts index 058f6c20c18887..1a7eff3e7ca540 100644 --- a/src/plugins/console/public/services/settings.ts +++ b/src/plugins/console/public/services/settings.ts @@ -14,7 +14,7 @@ export const DEFAULT_SETTINGS = Object.freeze({ pollInterval: 60000, tripleQuotes: true, wrapMode: true, - autocomplete: Object.freeze({ fields: true, indices: true, templates: true }), + autocomplete: Object.freeze({ fields: true, indices: true, templates: true, dataStreams: true }), historyDisabled: false, }); @@ -25,6 +25,7 @@ export interface DevToolsSettings { fields: boolean; indices: boolean; templates: boolean; + dataStreams: boolean; }; polling: boolean; pollInterval: number; diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json index 9b91e3deb3a089..fb5cb446fb77e2 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json @@ -13,7 +13,7 @@ "DELETE" ], "patterns": [ - "_data_stream/{name}" + "_data_stream/{data_stream}" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/data-streams.html" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json index 45199a60f337d6..e383a1df4844a6 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json @@ -14,7 +14,8 @@ ], "patterns": [ "_data_stream", - "_data_stream/{name}" + "_data_stream/{name}", + "{data_stream}" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/data-streams.html" } diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 55049447aee576..862bed9d667a01 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -24,6 +24,7 @@ { "path": "../navigation/tsconfig.json" }, { "path": "../saved_objects_tagging_oss/tsconfig.json" }, { "path": "../saved_objects/tsconfig.json" }, + { "path": "../screenshot_mode/tsconfig.json" }, { "path": "../ui_actions/tsconfig.json" }, { "path": "../charts/tsconfig.json" }, { "path": "../discover/tsconfig.json" }, diff --git a/src/plugins/data/common/search/aggs/agg_types.ts b/src/plugins/data/common/search/aggs/agg_types.ts index 01ccd401c07acc..d7750c48016cd5 100644 --- a/src/plugins/data/common/search/aggs/agg_types.ts +++ b/src/plugins/data/common/search/aggs/agg_types.ts @@ -36,6 +36,7 @@ export const getAggTypes = () => ({ { name: METRIC_TYPES.PERCENTILES, fn: metrics.getPercentilesMetricAgg }, { name: METRIC_TYPES.PERCENTILE_RANKS, fn: metrics.getPercentileRanksMetricAgg }, { name: METRIC_TYPES.TOP_HITS, fn: metrics.getTopHitMetricAgg }, + { name: METRIC_TYPES.TOP_METRICS, fn: metrics.getTopMetricsMetricAgg }, { name: METRIC_TYPES.DERIVATIVE, fn: metrics.getDerivativeMetricAgg }, { name: METRIC_TYPES.CUMULATIVE_SUM, fn: metrics.getCumulativeSumMetricAgg }, { name: METRIC_TYPES.MOVING_FN, fn: metrics.getMovingAvgMetricAgg }, @@ -109,4 +110,5 @@ export const getAggTypesFunctions = () => [ metrics.aggStdDeviation, metrics.aggSum, metrics.aggTopHit, + metrics.aggTopMetrics, ]; diff --git a/src/plugins/data/common/search/aggs/aggs_service.test.ts b/src/plugins/data/common/search/aggs/aggs_service.test.ts index b7237c7b801342..6090e965489e74 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.test.ts @@ -95,6 +95,7 @@ describe('Aggs service', () => { "percentiles", "percentile_ranks", "top_hits", + "top_metrics", "derivative", "cumulative_sum", "moving_avg", @@ -147,6 +148,7 @@ describe('Aggs service', () => { "percentiles", "percentile_ranks", "top_hits", + "top_metrics", "derivative", "cumulative_sum", "moving_avg", diff --git a/src/plugins/data/common/search/aggs/metrics/index.ts b/src/plugins/data/common/search/aggs/metrics/index.ts index d37b74a1a28aef..4d80e363251001 100644 --- a/src/plugins/data/common/search/aggs/metrics/index.ts +++ b/src/plugins/data/common/search/aggs/metrics/index.ts @@ -56,3 +56,5 @@ export * from './sum_fn'; export * from './sum'; export * from './top_hit_fn'; export * from './top_hit'; +export * from './top_metrics'; +export * from './top_metrics_fn'; diff --git a/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts index 478b8309272e39..1fe703313218d8 100644 --- a/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts @@ -15,6 +15,7 @@ import { parentPipelineAggWriter } from './parent_pipeline_agg_writer'; const metricAggFilter = [ '!top_hits', + '!top_metrics', '!percentiles', '!percentile_ranks', '!median', diff --git a/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts index f8c903b8cfe420..243a119847a2cf 100644 --- a/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts @@ -13,6 +13,7 @@ import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type'; const metricAggFilter: string[] = [ '!top_hits', + '!top_metrics', '!percentiles', '!percentile_ranks', '!median', diff --git a/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts b/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts index 6ddb0fdd9410d4..5237c1ecffe584 100644 --- a/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts +++ b/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts @@ -22,6 +22,7 @@ export interface MetricAggParam extends AggParamType { filterFieldTypes?: FieldTypes; onlyAggregatable?: boolean; + scriptable?: boolean; } const metricType = 'metrics'; diff --git a/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts b/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts index a308153b3816b7..eed6d0a378fc2d 100644 --- a/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts +++ b/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts @@ -27,6 +27,7 @@ export enum METRIC_TYPES { SERIAL_DIFF = 'serial_diff', SUM = 'sum', TOP_HITS = 'top_hits', + TOP_METRICS = 'top_metrics', PERCENTILES = 'percentiles', PERCENTILE_RANKS = 'percentile_ranks', STD_DEV = 'std_dev', diff --git a/src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts b/src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts new file mode 100644 index 00000000000000..9bf5f581aa0a4a --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getTopMetricsMetricAgg } from './top_metrics'; +import { AggConfigs } from '../agg_configs'; +import { mockAggTypesRegistry } from '../test_helpers'; +import { IMetricAggConfig } from './metric_agg_type'; +import { KBN_FIELD_TYPES } from '../../../../common'; + +describe('Top metrics metric', () => { + let aggConfig: IMetricAggConfig; + + const init = ({ + fieldName = 'field', + fieldType = KBN_FIELD_TYPES.NUMBER, + sortFieldName = 'sortField', + sortFieldType = KBN_FIELD_TYPES.NUMBER, + sortOrder = 'desc', + size = 1, + }: any) => { + const typesRegistry = mockAggTypesRegistry(); + const field = { + name: fieldName, + displayName: fieldName, + type: fieldType, + }; + + const sortField = { + name: sortFieldName, + displayName: sortFieldName, + type: sortFieldType, + }; + + const params = { + size, + field: field.name, + sortField: sortField.name, + sortOrder: { + value: sortOrder, + }, + }; + + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: (name: string) => { + if (name === sortFieldName) return sortField; + if (name === fieldName) return field; + return null; + }, + filter: () => [field, sortField], + }, + } as any; + + const aggConfigs = new AggConfigs( + indexPattern, + [ + { + id: '1', + type: 'top_metrics', + schema: 'metric', + params, + }, + ], + { typesRegistry } + ); + + // Grab the aggConfig off the vis (we don't actually use the vis for anything else) + aggConfig = aggConfigs.aggs[0] as IMetricAggConfig; + }; + + it('should return a label prefixed with Last if sorting in descending order', () => { + init({ fieldName: 'bytes', sortFieldName: '@timestamp' }); + expect(getTopMetricsMetricAgg().makeLabel(aggConfig)).toEqual( + 'Last "bytes" value by "@timestamp"' + ); + }); + + it('should return a label prefixed with First if sorting in ascending order', () => { + init({ + fieldName: 'bytes', + sortFieldName: '@timestamp', + sortOrder: 'asc', + }); + expect(getTopMetricsMetricAgg().makeLabel(aggConfig)).toEqual( + 'First "bytes" value by "@timestamp"' + ); + }); + + it('should return a label with size if larger then 1', () => { + init({ + fieldName: 'bytes', + sortFieldName: '@timestamp', + sortOrder: 'asc', + size: 3, + }); + expect(getTopMetricsMetricAgg().makeLabel(aggConfig)).toEqual( + 'First 3 "bytes" values by "@timestamp"' + ); + }); + + it('should return a fieldName in getValueBucketPath', () => { + init({ + fieldName: 'bytes', + sortFieldName: '@timestamp', + sortOrder: 'asc', + size: 3, + }); + expect(getTopMetricsMetricAgg().getValueBucketPath(aggConfig)).toEqual('1[bytes]'); + }); + + it('produces the expected expression ast', () => { + init({ fieldName: 'machine.os', sortFieldName: '@timestamp' }); + expect(aggConfig.toExpressionAst()).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "field": Array [ + "machine.os", + ], + "id": Array [ + "1", + ], + "schema": Array [ + "metric", + ], + "size": Array [ + 1, + ], + "sortField": Array [ + "@timestamp", + ], + "sortOrder": Array [ + "desc", + ], + }, + "function": "aggTopMetrics", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + + describe('gets value from top metrics bucket', () => { + it('should return null if there is no hits', () => { + const bucket = { + '1': { + top: [], + }, + }; + + init({ fieldName: 'bytes' }); + expect(getTopMetricsMetricAgg().getValue(aggConfig, bucket)).toBe(null); + }); + + it('should return a single value if there is a single hit', () => { + const bucket = { + '1': { + top: [{ sort: [3], metrics: { bytes: 1024 } }], + }, + }; + + init({ fieldName: 'bytes' }); + expect(getTopMetricsMetricAgg().getValue(aggConfig, bucket)).toBe(1024); + }); + + it('should return an array of values if there is a multiple results', () => { + const bucket = { + '1': { + top: [ + { sort: [3], metrics: { bytes: 1024 } }, + { sort: [2], metrics: { bytes: 512 } }, + { sort: [1], metrics: { bytes: 256 } }, + ], + }, + }; + + init({ fieldName: 'bytes' }); + expect(getTopMetricsMetricAgg().getValue(aggConfig, bucket)).toEqual([1024, 512, 256]); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/metrics/top_metrics.ts b/src/plugins/data/common/search/aggs/metrics/top_metrics.ts new file mode 100644 index 00000000000000..2079925e0435b1 --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/top_metrics.ts @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import _ from 'lodash'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { i18n } from '@kbn/i18n'; +import { aggTopMetricsFnName } from './top_metrics_fn'; +import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; +import { METRIC_TYPES } from './metric_agg_types'; +import { KBN_FIELD_TYPES } from '../../../../common'; +import { BaseAggParams } from '../types'; + +export interface AggParamsTopMetrics extends BaseAggParams { + field: string; + sortField?: string; + sortOrder?: 'desc' | 'asc'; + size?: number; +} + +export const getTopMetricsMetricAgg = () => { + return new MetricAggType({ + name: METRIC_TYPES.TOP_METRICS, + expressionName: aggTopMetricsFnName, + title: i18n.translate('data.search.aggs.metrics.topMetricsTitle', { + defaultMessage: 'Top metrics', + }), + makeLabel(aggConfig) { + const isDescOrder = aggConfig.getParam('sortOrder').value === 'desc'; + const size = aggConfig.getParam('size'); + const field = aggConfig.getParam('field'); + const sortField = aggConfig.getParam('sortField'); + + if (isDescOrder) { + if (size > 1) { + return i18n.translate('data.search.aggs.metrics.topMetrics.descWithSizeLabel', { + defaultMessage: `Last {size} "{fieldName}" values by "{sortField}"`, + values: { + size, + fieldName: field?.displayName, + sortField: sortField?.displayName ?? '_score', + }, + }); + } else { + return i18n.translate('data.search.aggs.metrics.topMetrics.descNoSizeLabel', { + defaultMessage: `Last "{fieldName}" value by "{sortField}"`, + values: { + fieldName: field?.displayName, + sortField: sortField?.displayName ?? '_score', + }, + }); + } + } else { + if (size > 1) { + return i18n.translate('data.search.aggs.metrics.topMetrics.ascWithSizeLabel', { + defaultMessage: `First {size} "{fieldName}" values by "{sortField}"`, + values: { + size, + fieldName: field?.displayName, + sortField: sortField?.displayName ?? '_score', + }, + }); + } else { + return i18n.translate('data.search.aggs.metrics.topMetrics.ascNoSizeLabel', { + defaultMessage: `First "{fieldName}" value by "{sortField}"`, + values: { + fieldName: field?.displayName, + sortField: sortField?.displayName ?? '_score', + }, + }); + } + } + }, + params: [ + { + name: 'field', + type: 'field', + scriptable: false, + filterFieldTypes: [ + KBN_FIELD_TYPES.STRING, + KBN_FIELD_TYPES.IP, + KBN_FIELD_TYPES.BOOLEAN, + KBN_FIELD_TYPES.NUMBER, + KBN_FIELD_TYPES.DATE, + ], + write(agg, output) { + const field = agg.getParam('field'); + output.params.metrics = { field: field.name }; + }, + }, + { + name: 'size', + default: 1, + }, + { + name: 'sortField', + type: 'field', + scriptable: false, + filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE], + default(agg: IMetricAggConfig) { + return agg.getIndexPattern().timeFieldName; + }, + write: _.noop, // prevent default write, it is handled below + }, + { + name: 'sortOrder', + type: 'optioned', + default: 'desc', + options: [ + { + text: i18n.translate('data.search.aggs.metrics.topMetrics.descendingLabel', { + defaultMessage: 'Descending', + }), + value: 'desc', + }, + { + text: i18n.translate('data.search.aggs.metrics.topMetrics.ascendingLabel', { + defaultMessage: 'Ascending', + }), + value: 'asc', + }, + ], + write(agg, output) { + const sortField = agg.params.sortField; + const sortOrder = agg.params.sortOrder; + + if (sortField && sortOrder) { + output.params.sort = { + [sortField.name]: sortOrder.value, + }; + } else { + output.params.sort = '_score'; + } + }, + }, + ], + // override is needed to support top_metrics as an orderAgg of terms agg + getValueBucketPath(agg) { + const field = agg.getParam('field').name; + return `${agg.id}[${field}]`; + }, + getValue(agg, aggregate: Record) { + const metricFieldName = agg.getParam('field').name; + const results = aggregate[agg.id]?.top.map((result) => result.metrics[metricFieldName]) ?? []; + + if (results.length === 0) return null; + if (results.length === 1) return results[0]; + return results; + }, + }); +}; diff --git a/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.test.ts new file mode 100644 index 00000000000000..848fccda283faa --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggTopMetrics } from './top_metrics_fn'; + +describe('agg_expression_functions', () => { + describe('aggTopMetrics', () => { + const fn = functionWrapper(aggTopMetrics()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + "size": undefined, + "sortField": undefined, + "sortOrder": undefined, + }, + "schema": undefined, + "type": "top_metrics", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + id: '1', + enabled: false, + schema: 'whatever', + field: 'machine.os.keyword', + sortOrder: 'asc', + size: 6, + sortField: 'bytes', + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": false, + "id": "1", + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + "size": 6, + "sortField": "bytes", + "sortOrder": "asc", + }, + "schema": "whatever", + "type": "top_metrics", + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual('{ "foo": true }'); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.ts b/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.ts new file mode 100644 index 00000000000000..6fe9ba97fe4483 --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; + +export const aggTopMetricsFnName = 'aggTopMetrics'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggTopMetricsFnName, + Input, + AggArgs, + Output +>; + +export const aggTopMetrics = (): FunctionDefinition => ({ + name: aggTopMetricsFnName, + help: i18n.translate('data.search.aggs.function.metrics.topMetrics.help', { + defaultMessage: 'Generates a serialized aggregation configuration for Top metrics.', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.topMetrics.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.topMetrics.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + size: { + types: ['number'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.size.help', { + defaultMessage: 'Number of top values to retrieve', + }), + }, + sortOrder: { + types: ['string'], + options: ['desc', 'asc'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.sortOrder.help', { + defaultMessage: 'Order in which to return the results: asc or desc', + }), + }, + sortField: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.sortField.help', { + defaultMessage: 'Field to order results by', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.json.help', { + defaultMessage: 'Advanced JSON to include when the aggregation is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.TOP_METRICS, + params: { + ...rest, + }, + }, + }; + }, +}); diff --git a/src/plugins/data/common/search/aggs/param_types/field.ts b/src/plugins/data/common/search/aggs/param_types/field.ts index 940fdafd548753..b56787121f7248 100644 --- a/src/plugins/data/common/search/aggs/param_types/field.ts +++ b/src/plugins/data/common/search/aggs/param_types/field.ts @@ -43,6 +43,7 @@ export class FieldParamType extends BaseParamType { this.filterFieldTypes = config.filterFieldTypes || '*'; this.onlyAggregatable = config.onlyAggregatable !== false; + this.scriptable = config.scriptable !== false; this.filterField = config.filterField; if (!config.write) { diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index cf9a6123b14c8d..edc328bcb5099a 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -93,6 +93,8 @@ import { import { AggParamsSampler } from './buckets/sampler'; import { AggParamsDiversifiedSampler } from './buckets/diversified_sampler'; import { AggParamsSignificantText } from './buckets/significant_text'; +import { AggParamsTopMetrics } from './metrics/top_metrics'; +import { aggTopMetrics } from './metrics/top_metrics_fn'; export type { IAggConfig, AggConfigSerialized } from './agg_config'; export type { CreateAggConfigParams, IAggConfigs } from './agg_configs'; @@ -187,6 +189,7 @@ export interface AggParamsMapping { [METRIC_TYPES.PERCENTILES]: AggParamsPercentiles; [METRIC_TYPES.SERIAL_DIFF]: AggParamsSerialDiff; [METRIC_TYPES.TOP_HITS]: AggParamsTopHit; + [METRIC_TYPES.TOP_METRICS]: AggParamsTopMetrics; } /** @@ -229,4 +232,5 @@ export interface AggFunctionsMapping { aggStdDeviation: ReturnType; aggSum: ReturnType; aggTopHit: ReturnType; + aggTopMetrics: ReturnType; } diff --git a/src/plugins/data/public/search/aggs/aggs_service.test.ts b/src/plugins/data/public/search/aggs/aggs_service.test.ts index 101c2c909c7e1d..83328e196fa0a5 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -54,7 +54,7 @@ describe('AggsService - public', () => { service.setup(setupDeps); const start = service.start(startDeps); expect(start.types.getAll().buckets.length).toBe(16); - expect(start.types.getAll().metrics.length).toBe(23); + expect(start.types.getAll().metrics.length).toBe(24); }); test('registers custom agg types', () => { @@ -71,7 +71,7 @@ describe('AggsService - public', () => { const start = service.start(startDeps); expect(start.types.getAll().buckets.length).toBe(17); expect(start.types.getAll().buckets.some(({ name }) => name === 'foo')).toBe(true); - expect(start.types.getAll().metrics.length).toBe(24); + expect(start.types.getAll().metrics.length).toBe(25); expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true); }); }); diff --git a/src/plugins/data_views/common/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts index 2e31ed793c3dbb..04c1fd98a0f608 100644 --- a/src/plugins/data_views/common/data_views/data_views.ts +++ b/src/plugins/data_views/common/data_views/data_views.ts @@ -424,7 +424,7 @@ export class DataViewsService { ); if (!savedObject.version) { - throw new SavedObjectNotFound(DATA_VIEW_SAVED_OBJECT_TYPE, id, 'management/kibana/dataViews'); + throw new SavedObjectNotFound('data view', id, 'management/kibana/dataViews'); } return this.initFromSavedObject(savedObject); diff --git a/src/plugins/home/kibana.json b/src/plugins/home/kibana.json index d8c09ab5e80c6a..02b33e814e2a1c 100644 --- a/src/plugins/home/kibana.json +++ b/src/plugins/home/kibana.json @@ -8,6 +8,6 @@ "server": true, "ui": true, "requiredPlugins": ["dataViews", "share", "urlForwarding"], - "optionalPlugins": ["usageCollection", "telemetry", "customIntegrations"], + "optionalPlugins": ["usageCollection", "customIntegrations"], "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap b/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap index ab6ad1b6cc0c55..43d8f935221b36 100644 --- a/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap +++ b/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap @@ -374,202 +374,6 @@ exports[`home isNewKibanaInstance should set isNewKibanaInstance to false when t exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when there are no index patterns 1`] = ` `; diff --git a/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap b/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap index 17f7d2520e8621..861e0ee895887c 100644 --- a/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap +++ b/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render a Welcome screen with no telemetry disclaimer 1`] = ` +exports[`should render a Welcome screen 1`] = `
`; - -exports[`should render a Welcome screen with the telemetry disclaimer 1`] = ` - -
-
-
- - - - - -

- -

-
- -
-
-
- - - - - - - - - - - - - - - - - -
-
-
-`; - -exports[`should render a Welcome screen with the telemetry disclaimer when optIn is false 1`] = ` - -
-
-
- - - - - -

- -

-
- -
-
-
- - - - - - - - - - - - - - - - - -
-
-
-`; - -exports[`should render a Welcome screen with the telemetry disclaimer when optIn is true 1`] = ` - -
-
-
- - - - - -

- -

-
- -
-
-
- - - - - - - - - - - - - - - - - -
-
-
-`; - -exports[`should render a Welcome screen without the opt in/out link when user cannot change optIn status 1`] = ` - -
-
-
- - - - - -

- -

-
- -
-
-
- - - - - - - - - - - - - -
-
-
-`; diff --git a/src/plugins/home/public/application/components/home.test.tsx b/src/plugins/home/public/application/components/home.test.tsx index 9983afa3d4d611..f27a286488c2b1 100644 --- a/src/plugins/home/public/application/components/home.test.tsx +++ b/src/plugins/home/public/application/components/home.test.tsx @@ -12,7 +12,6 @@ import type { HomeProps } from './home'; import { Home } from './home'; import { FeatureCatalogueCategory } from '../../services'; -import { telemetryPluginMock } from '../../../../telemetry/public/mocks'; import { Welcome } from './welcome'; let mockHasIntegrationsPermission = true; @@ -57,7 +56,6 @@ describe('home', () => { setItem: jest.fn(), }, urlBasePath: 'goober', - telemetry: telemetryPluginMock.createStartContract(), addBasePath(url) { return `base_path/${url}`; }, diff --git a/src/plugins/home/public/application/components/home.tsx b/src/plugins/home/public/application/components/home.tsx index fdf04ea5806538..1fb0b3c790ab7e 100644 --- a/src/plugins/home/public/application/components/home.tsx +++ b/src/plugins/home/public/application/components/home.tsx @@ -10,7 +10,6 @@ import React, { Component } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { METRIC_TYPE } from '@kbn/analytics'; import { i18n } from '@kbn/i18n'; -import type { TelemetryPluginStart } from 'src/plugins/telemetry/public'; import { KibanaPageTemplate, OverviewPageFooter } from '../../../../kibana_react/public'; import { HOME_APP_BASE_PATH } from '../../../common/constants'; import type { FeatureCatalogueEntry, FeatureCatalogueSolution } from '../../services'; @@ -29,7 +28,6 @@ export interface HomeProps { solutions: FeatureCatalogueSolution[]; localStorage: Storage; urlBasePath: string; - telemetry: TelemetryPluginStart; hasUserDataView: () => Promise; } @@ -175,13 +173,7 @@ export class Home extends Component { } private renderWelcome() { - return ( - this.skipWelcome()} - urlBasePath={this.props.urlBasePath} - telemetry={this.props.telemetry} - /> - ); + return this.skipWelcome()} urlBasePath={this.props.urlBasePath} />; } public render() { diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index 62df479ecbfdf6..a634573aaf21ec 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -26,7 +26,6 @@ export function HomeApp({ directories, solutions }) { getBasePath, addBasePath, environmentService, - telemetry, dataViewsService, } = getServices(); const environment = environmentService.getEnvironment(); @@ -75,7 +74,6 @@ export function HomeApp({ directories, solutions }) { solutions={solutions} localStorage={localStorage} urlBasePath={getBasePath()} - telemetry={telemetry} hasUserDataView={() => dataViewsService.hasUserDataView()} /> diff --git a/src/plugins/home/public/application/components/welcome.test.mocks.ts b/src/plugins/home/public/application/components/welcome.test.mocks.ts new file mode 100644 index 00000000000000..fc9854bae31990 --- /dev/null +++ b/src/plugins/home/public/application/components/welcome.test.mocks.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { welcomeServiceMock } from '../../services/welcome/welcome_service.mocks'; + +jest.doMock('../kibana_services', () => ({ + getServices: () => ({ + addBasePath: (path: string) => `root${path}`, + trackUiMetric: () => {}, + welcomeService: welcomeServiceMock.create(), + }), +})); diff --git a/src/plugins/home/public/application/components/welcome.test.tsx b/src/plugins/home/public/application/components/welcome.test.tsx index b042a91e58c9d2..3400b4bfcdb75f 100644 --- a/src/plugins/home/public/application/components/welcome.test.tsx +++ b/src/plugins/home/public/application/components/welcome.test.tsx @@ -8,58 +8,11 @@ import React from 'react'; import { shallow } from 'enzyme'; +import './welcome.test.mocks'; import { Welcome } from './welcome'; -import { telemetryPluginMock } from '../../../../telemetry/public/mocks'; -jest.mock('../kibana_services', () => ({ - getServices: () => ({ - addBasePath: (path: string) => `root${path}`, - trackUiMetric: () => {}, - }), -})); - -test('should render a Welcome screen with the telemetry disclaimer', () => { - const telemetry = telemetryPluginMock.createStartContract(); - const component = shallow( {}} telemetry={telemetry} />); - - expect(component).toMatchSnapshot(); -}); - -test('should render a Welcome screen with the telemetry disclaimer when optIn is true', () => { - const telemetry = telemetryPluginMock.createStartContract(); - telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); - const component = shallow( {}} telemetry={telemetry} />); - - expect(component).toMatchSnapshot(); -}); - -test('should render a Welcome screen with the telemetry disclaimer when optIn is false', () => { - const telemetry = telemetryPluginMock.createStartContract(); - telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false); - const component = shallow( {}} telemetry={telemetry} />); - - expect(component).toMatchSnapshot(); -}); - -test('should render a Welcome screen with no telemetry disclaimer', () => { +test('should render a Welcome screen', () => { const component = shallow( {}} />); expect(component).toMatchSnapshot(); }); - -test('should render a Welcome screen without the opt in/out link when user cannot change optIn status', () => { - const telemetry = telemetryPluginMock.createStartContract(); - telemetry.telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(false); - const component = shallow( {}} telemetry={telemetry} />); - - expect(component).toMatchSnapshot(); -}); - -test('fires opt-in seen when mounted', () => { - const telemetry = telemetryPluginMock.createStartContract(); - const mockSetOptedInNoticeSeen = jest.fn(); - telemetry.telemetryNotifications.setOptedInNoticeSeen = mockSetOptedInNoticeSeen; - shallow( {}} telemetry={telemetry} />); - - expect(mockSetOptedInNoticeSeen).toHaveBeenCalled(); -}); diff --git a/src/plugins/home/public/application/components/welcome.tsx b/src/plugins/home/public/application/components/welcome.tsx index 1a6251ebdca118..9efa6d356d9716 100644 --- a/src/plugins/home/public/application/components/welcome.tsx +++ b/src/plugins/home/public/application/components/welcome.tsx @@ -12,27 +12,17 @@ * in Elasticsearch. */ -import React, { Fragment } from 'react'; -import { - EuiLink, - EuiTextColor, - EuiTitle, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiPortal, -} from '@elastic/eui'; +import React from 'react'; +import { EuiTitle, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPortal } from '@elastic/eui'; import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage } from '@kbn/i18n-react'; import { getServices } from '../kibana_services'; -import { TelemetryPluginStart } from '../../../../telemetry/public'; import { SampleDataCard } from './sample_data'; + interface Props { urlBasePath: string; onSkip: () => void; - telemetry?: TelemetryPluginStart; } /** @@ -47,7 +37,7 @@ export class Welcome extends React.Component { } }; - private redirecToAddData() { + private redirectToAddData() { this.services.application.navigateToApp('integrations', { path: '/browse' }); } @@ -58,68 +48,23 @@ export class Welcome extends React.Component { private onSampleDataConfirm = () => { this.services.trackUiMetric(METRIC_TYPE.CLICK, 'sampleDataConfirm'); - this.redirecToAddData(); + this.redirectToAddData(); }; componentDidMount() { - const { telemetry } = this.props; + const { welcomeService } = this.services; this.services.trackUiMetric(METRIC_TYPE.LOADED, 'welcomeScreenMount'); - if (telemetry?.telemetryService.userCanChangeSettings) { - telemetry.telemetryNotifications.setOptedInNoticeSeen(); - } document.addEventListener('keydown', this.hideOnEsc); + welcomeService.onRendered(); } componentWillUnmount() { document.removeEventListener('keydown', this.hideOnEsc); } - private renderTelemetryEnabledOrDisabledText = () => { - const { telemetry } = this.props; - if ( - !telemetry || - !telemetry.telemetryService.userCanChangeSettings || - !telemetry.telemetryService.getCanChangeOptInStatus() - ) { - return null; - } - - const isOptedIn = telemetry.telemetryService.getIsOptedIn(); - if (isOptedIn) { - return ( - - - - - - - ); - } else { - return ( - - - - - - - ); - } - }; - render() { - const { urlBasePath, telemetry } = this.props; + const { urlBasePath } = this.props; + const { welcomeService } = this.services; return (
@@ -146,28 +91,7 @@ export class Welcome extends React.Component { onDecline={this.onSampleDataDecline} /> - {!!telemetry && ( - - - - - - - {this.renderTelemetryEnabledOrDisabledText()} - - - - )} + {welcomeService.renderTelemetryNotice()}
diff --git a/src/plugins/home/public/application/kibana_services.ts b/src/plugins/home/public/application/kibana_services.ts index fdd325df96ac57..3ccfd9413a88ad 100644 --- a/src/plugins/home/public/application/kibana_services.ts +++ b/src/plugins/home/public/application/kibana_services.ts @@ -17,7 +17,6 @@ import { ApplicationStart, } from 'kibana/public'; import { UiCounterMetricType } from '@kbn/analytics'; -import { TelemetryPluginStart } from '../../../telemetry/public'; import { UrlForwardingStart } from '../../../url_forwarding/public'; import { DataViewsContract } from '../../../data_views/public'; import { TutorialService } from '../services/tutorials'; @@ -26,6 +25,7 @@ import { FeatureCatalogueRegistry } from '../services/feature_catalogue'; import { EnvironmentService } from '../services/environment'; import { ConfigSchema } from '../../config'; import { SharePluginSetup } from '../../../share/public'; +import type { WelcomeService } from '../services/welcome'; export interface HomeKibanaServices { dataViewsService: DataViewsContract; @@ -46,9 +46,9 @@ export interface HomeKibanaServices { docLinks: DocLinksStart; addBasePath: (url: string) => string; environmentService: EnvironmentService; - telemetry?: TelemetryPluginStart; tutorialService: TutorialService; addDataService: AddDataService; + welcomeService: WelcomeService; } let services: HomeKibanaServices | null = null; diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index 009382eee0009a..3450f4f9d2caf9 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -27,6 +27,8 @@ export type { TutorialVariables, TutorialDirectoryHeaderLinkComponent, TutorialModuleNoticeComponent, + WelcomeRenderTelemetryNotice, + WelcomeServiceSetup, } from './services'; export { INSTRUCTION_VARIANT, getDisplayText } from '../common/instruction_variant'; diff --git a/src/plugins/home/public/mocks.ts b/src/plugins/home/public/mocks.ts index 10c186ee3f4e30..42e489dea9d2a3 100644 --- a/src/plugins/home/public/mocks.ts +++ b/src/plugins/home/public/mocks.ts @@ -8,16 +8,17 @@ import { featureCatalogueRegistryMock } from './services/feature_catalogue/feature_catalogue_registry.mock'; import { environmentServiceMock } from './services/environment/environment.mock'; -import { configSchema } from '../config'; import { tutorialServiceMock } from './services/tutorials/tutorial_service.mock'; import { addDataServiceMock } from './services/add_data/add_data_service.mock'; +import { HomePublicPluginSetup } from './plugin'; +import { welcomeServiceMock } from './services/welcome/welcome_service.mocks'; -const createSetupContract = () => ({ +const createSetupContract = (): jest.Mocked => ({ featureCatalogue: featureCatalogueRegistryMock.createSetup(), environment: environmentServiceMock.createSetup(), tutorials: tutorialServiceMock.createSetup(), addData: addDataServiceMock.createSetup(), - config: configSchema.validate({}), + welcomeScreen: welcomeServiceMock.createSetup(), }); export const homePluginMock = { diff --git a/src/plugins/home/public/plugin.test.mocks.ts b/src/plugins/home/public/plugin.test.mocks.ts index c3e3c50a2fe0f3..22d314cbd6d068 100644 --- a/src/plugins/home/public/plugin.test.mocks.ts +++ b/src/plugins/home/public/plugin.test.mocks.ts @@ -10,14 +10,17 @@ import { featureCatalogueRegistryMock } from './services/feature_catalogue/featu import { environmentServiceMock } from './services/environment/environment.mock'; import { tutorialServiceMock } from './services/tutorials/tutorial_service.mock'; import { addDataServiceMock } from './services/add_data/add_data_service.mock'; +import { welcomeServiceMock } from './services/welcome/welcome_service.mocks'; export const registryMock = featureCatalogueRegistryMock.create(); export const environmentMock = environmentServiceMock.create(); export const tutorialMock = tutorialServiceMock.create(); export const addDataMock = addDataServiceMock.create(); +export const welcomeMock = welcomeServiceMock.create(); jest.doMock('./services', () => ({ FeatureCatalogueRegistry: jest.fn(() => registryMock), EnvironmentService: jest.fn(() => environmentMock), TutorialService: jest.fn(() => tutorialMock), AddDataService: jest.fn(() => addDataMock), + WelcomeService: jest.fn(() => welcomeMock), })); diff --git a/src/plugins/home/public/plugin.test.ts b/src/plugins/home/public/plugin.test.ts index 990f0dce54a05f..57a1f5ec112aaf 100644 --- a/src/plugins/home/public/plugin.test.ts +++ b/src/plugins/home/public/plugin.test.ts @@ -79,5 +79,18 @@ describe('HomePublicPlugin', () => { expect(setup).toHaveProperty('tutorials'); expect(setup.tutorials).toHaveProperty('setVariable'); }); + + test('wires up and returns welcome service', async () => { + const setup = await new HomePublicPlugin(mockInitializerContext).setup( + coreMock.createSetup() as any, + { + share: mockShare, + urlForwarding: urlForwardingPluginMock.createSetupContract(), + } + ); + expect(setup).toHaveProperty('welcomeScreen'); + expect(setup.welcomeScreen).toHaveProperty('registerOnRendered'); + expect(setup.welcomeScreen).toHaveProperty('registerTelemetryNoticeRenderer'); + }); }); }); diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 1ece73e71f393f..af43e56a1d75d3 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -25,11 +25,12 @@ import { TutorialServiceSetup, AddDataService, AddDataServiceSetup, + WelcomeService, + WelcomeServiceSetup, } from './services'; import { ConfigSchema } from '../config'; import { setServices } from './application/kibana_services'; import { DataViewsPublicPluginStart } from '../../data_views/public'; -import { TelemetryPluginStart } from '../../telemetry/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { UrlForwardingSetup, UrlForwardingStart } from '../../url_forwarding/public'; import { AppNavLinkStatus } from '../../../core/public'; @@ -38,7 +39,6 @@ import { SharePluginSetup } from '../../share/public'; export interface HomePluginStartDependencies { dataViews: DataViewsPublicPluginStart; - telemetry?: TelemetryPluginStart; urlForwarding: UrlForwardingStart; } @@ -61,6 +61,7 @@ export class HomePublicPlugin private readonly environmentService = new EnvironmentService(); private readonly tutorialService = new TutorialService(); private readonly addDataService = new AddDataService(); + private readonly welcomeService = new WelcomeService(); constructor(private readonly initializerContext: PluginInitializerContext) {} @@ -76,7 +77,7 @@ export class HomePublicPlugin const trackUiMetric = usageCollection ? usageCollection.reportUiCounter.bind(usageCollection, 'Kibana_home') : () => {}; - const [coreStart, { telemetry, dataViews, urlForwarding: urlForwardingStart }] = + const [coreStart, { dataViews, urlForwarding: urlForwardingStart }] = await core.getStartServices(); setServices({ share, @@ -89,7 +90,6 @@ export class HomePublicPlugin savedObjectsClient: coreStart.savedObjects.client, chrome: coreStart.chrome, application: coreStart.application, - telemetry, uiSettings: core.uiSettings, addBasePath: core.http.basePath.prepend, getBasePath: core.http.basePath.get, @@ -100,6 +100,7 @@ export class HomePublicPlugin tutorialService: this.tutorialService, addDataService: this.addDataService, featureCatalogue: this.featuresCatalogueRegistry, + welcomeService: this.welcomeService, }); coreStart.chrome.docTitle.change( i18n.translate('home.pageTitle', { defaultMessage: 'Home' }) @@ -132,6 +133,7 @@ export class HomePublicPlugin environment: { ...this.environmentService.setup() }, tutorials: { ...this.tutorialService.setup() }, addData: { ...this.addDataService.setup() }, + welcomeScreen: { ...this.welcomeService.setup() }, }; } @@ -159,12 +161,12 @@ export interface HomePublicPluginSetup { tutorials: TutorialServiceSetup; addData: AddDataServiceSetup; featureCatalogue: FeatureCatalogueSetup; + welcomeScreen: WelcomeServiceSetup; /** * The environment service is only available for a transition period and will * be replaced by display specific extension points. * @deprecated */ - environment: EnvironmentSetup; } diff --git a/src/plugins/home/public/services/index.ts b/src/plugins/home/public/services/index.ts index 2ee68a9eef0c29..41bc9ee258cebb 100644 --- a/src/plugins/home/public/services/index.ts +++ b/src/plugins/home/public/services/index.ts @@ -28,3 +28,6 @@ export type { export { AddDataService } from './add_data'; export type { AddDataServiceSetup, AddDataTab } from './add_data'; + +export { WelcomeService } from './welcome'; +export type { WelcomeServiceSetup, WelcomeRenderTelemetryNotice } from './welcome'; diff --git a/src/plugins/home/public/services/welcome/index.ts b/src/plugins/home/public/services/welcome/index.ts new file mode 100644 index 00000000000000..371c6044c5dc5c --- /dev/null +++ b/src/plugins/home/public/services/welcome/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type { WelcomeServiceSetup, WelcomeRenderTelemetryNotice } from './welcome_service'; +export { WelcomeService } from './welcome_service'; diff --git a/src/plugins/home/public/services/welcome/welcome_service.mocks.ts b/src/plugins/home/public/services/welcome/welcome_service.mocks.ts new file mode 100644 index 00000000000000..921cb990663276 --- /dev/null +++ b/src/plugins/home/public/services/welcome/welcome_service.mocks.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PublicMethodsOf } from '@kbn/utility-types'; +import { WelcomeService, WelcomeServiceSetup } from './welcome_service'; + +const createSetupMock = (): jest.Mocked => { + const welcomeService = new WelcomeService(); + const welcomeServiceSetup = welcomeService.setup(); + return { + registerTelemetryNoticeRenderer: jest + .fn() + .mockImplementation(welcomeServiceSetup.registerTelemetryNoticeRenderer), + registerOnRendered: jest.fn().mockImplementation(welcomeServiceSetup.registerOnRendered), + }; +}; + +const createMock = (): jest.Mocked> => { + const welcomeService = new WelcomeService(); + + return { + setup: jest.fn().mockImplementation(welcomeService.setup), + onRendered: jest.fn().mockImplementation(welcomeService.onRendered), + renderTelemetryNotice: jest.fn().mockImplementation(welcomeService.renderTelemetryNotice), + }; +}; + +export const welcomeServiceMock = { + createSetup: createSetupMock, + create: createMock, +}; diff --git a/src/plugins/home/public/services/welcome/welcome_service.test.ts b/src/plugins/home/public/services/welcome/welcome_service.test.ts new file mode 100644 index 00000000000000..df2f95718c78b5 --- /dev/null +++ b/src/plugins/home/public/services/welcome/welcome_service.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { WelcomeService, WelcomeServiceSetup } from './welcome_service'; + +describe('WelcomeService', () => { + let welcomeService: WelcomeService; + let welcomeServiceSetup: WelcomeServiceSetup; + + beforeEach(() => { + welcomeService = new WelcomeService(); + welcomeServiceSetup = welcomeService.setup(); + }); + describe('onRendered', () => { + test('it should register an onRendered listener', () => { + const onRendered = jest.fn(); + welcomeServiceSetup.registerOnRendered(onRendered); + + welcomeService.onRendered(); + expect(onRendered).toHaveBeenCalledTimes(1); + }); + + test('it should handle onRendered errors', () => { + const onRendered = jest.fn().mockImplementation(() => { + throw new Error('Something went terribly wrong'); + }); + welcomeServiceSetup.registerOnRendered(onRendered); + + expect(() => welcomeService.onRendered()).not.toThrow(); + expect(onRendered).toHaveBeenCalledTimes(1); + }); + + test('it should allow registering multiple onRendered listeners', () => { + const onRendered = jest.fn(); + const onRendered2 = jest.fn(); + welcomeServiceSetup.registerOnRendered(onRendered); + welcomeServiceSetup.registerOnRendered(onRendered2); + + welcomeService.onRendered(); + expect(onRendered).toHaveBeenCalledTimes(1); + expect(onRendered2).toHaveBeenCalledTimes(1); + }); + + test('if the same handler is registered twice, it is called twice', () => { + const onRendered = jest.fn(); + welcomeServiceSetup.registerOnRendered(onRendered); + welcomeServiceSetup.registerOnRendered(onRendered); + + welcomeService.onRendered(); + expect(onRendered).toHaveBeenCalledTimes(2); + }); + }); + describe('renderTelemetryNotice', () => { + test('it should register a renderer', () => { + const renderer = jest.fn().mockReturnValue('rendered text'); + welcomeServiceSetup.registerTelemetryNoticeRenderer(renderer); + + expect(welcomeService.renderTelemetryNotice()).toEqual('rendered text'); + }); + + test('it should fail to register a 2nd renderer and still use the first registered renderer', () => { + const renderer = jest.fn().mockReturnValue('rendered text'); + const renderer2 = jest.fn().mockReturnValue('other text'); + welcomeServiceSetup.registerTelemetryNoticeRenderer(renderer); + expect(() => welcomeServiceSetup.registerTelemetryNoticeRenderer(renderer2)).toThrowError( + 'Only one renderTelemetryNotice handler can be registered' + ); + + expect(welcomeService.renderTelemetryNotice()).toEqual('rendered text'); + }); + + test('it should handle errors in the renderer', () => { + const renderer = jest.fn().mockImplementation(() => { + throw new Error('Something went terribly wrong'); + }); + welcomeServiceSetup.registerTelemetryNoticeRenderer(renderer); + + expect(welcomeService.renderTelemetryNotice()).toEqual(null); + }); + }); +}); diff --git a/src/plugins/home/public/services/welcome/welcome_service.ts b/src/plugins/home/public/services/welcome/welcome_service.ts new file mode 100644 index 00000000000000..46cf139adb36a3 --- /dev/null +++ b/src/plugins/home/public/services/welcome/welcome_service.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type WelcomeRenderTelemetryNotice = () => null | JSX.Element; + +export interface WelcomeServiceSetup { + /** + * Register listeners to be called when the Welcome component is mounted. + * It can be called multiple times to register multiple listeners. + */ + registerOnRendered: (onRendered: () => void) => void; + /** + * Register a renderer of the telemetry notice to be shown below the Welcome page. + */ + registerTelemetryNoticeRenderer: (renderTelemetryNotice: WelcomeRenderTelemetryNotice) => void; +} + +export class WelcomeService { + private readonly onRenderedHandlers: Array<() => void> = []; + private renderTelemetryNoticeHandler?: WelcomeRenderTelemetryNotice; + + public setup = (): WelcomeServiceSetup => { + return { + registerOnRendered: (onRendered) => { + this.onRenderedHandlers.push(onRendered); + }, + registerTelemetryNoticeRenderer: (renderTelemetryNotice) => { + if (this.renderTelemetryNoticeHandler) { + throw new Error('Only one renderTelemetryNotice handler can be registered'); + } + this.renderTelemetryNoticeHandler = renderTelemetryNotice; + }, + }; + }; + + public onRendered = () => { + this.onRenderedHandlers.forEach((onRendered) => { + try { + onRendered(); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } + }); + }; + + public renderTelemetryNotice = () => { + if (this.renderTelemetryNoticeHandler) { + try { + return this.renderTelemetryNoticeHandler(); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } + } + return null; + }; +} diff --git a/src/plugins/home/tsconfig.json b/src/plugins/home/tsconfig.json index fa98b98ff8e1c3..17d0fc7bd91acf 100644 --- a/src/plugins/home/tsconfig.json +++ b/src/plugins/home/tsconfig.json @@ -15,7 +15,6 @@ { "path": "../kibana_react/tsconfig.json" }, { "path": "../share/tsconfig.json" }, { "path": "../url_forwarding/tsconfig.json" }, - { "path": "../usage_collection/tsconfig.json" }, - { "path": "../telemetry/tsconfig.json" } + { "path": "../usage_collection/tsconfig.json" } ] } diff --git a/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts b/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts index 29702c33568655..8e67dee3f8b6b4 100644 --- a/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts +++ b/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts @@ -24,6 +24,16 @@ describe('DependencyManager', () => { expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology); }); + it('should include final vertex if it has dependencies', () => { + const graph = { + A: [], + B: [], + C: ['A', 'B'], + }; + const sortedTopology = ['A', 'B', 'C']; + expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology); + }); + it('orderDependencies. Should return base topology if no depended vertices', () => { const graph = { N: [], @@ -34,22 +44,34 @@ describe('DependencyManager', () => { expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology); }); - it('orderDependencies. Should detect circular dependencies and throw error with path', () => { - const graph = { - N: ['R'], - R: ['A'], - A: ['B'], - B: ['C'], - C: ['D'], - D: ['E'], - E: ['F'], - F: ['L'], - L: ['G'], - G: ['N'], - }; - const circularPath = ['N', 'R', 'A', 'B', 'C', 'D', 'E', 'F', 'L', 'G', 'N'].join(' -> '); - const errorMessage = `Circular dependency detected while setting up services: ${circularPath}`; + describe('circular dependencies', () => { + it('should detect circular dependencies and throw error with path', () => { + const graph = { + N: ['R'], + R: ['A'], + A: ['B'], + B: ['C'], + C: ['D'], + D: ['E'], + E: ['F'], + F: ['L'], + L: ['G'], + G: ['N'], + }; + const circularPath = ['G', 'L', 'F', 'E', 'D', 'C', 'B', 'A', 'R', 'N'].join(' -> '); + const errorMessage = `Circular dependency detected while setting up services: ${circularPath}`; + + expect(() => DependencyManager.orderDependencies(graph)).toThrowError(errorMessage); + }); + + it('should detect circular dependency if circular reference is the first dependency for a vertex', () => { + const graph = { + A: ['B'], + B: ['A', 'C'], + C: [], + }; - expect(() => DependencyManager.orderDependencies(graph)).toThrowError(errorMessage); + expect(() => DependencyManager.orderDependencies(graph)).toThrow(); + }); }); }); diff --git a/src/plugins/presentation_util/public/services/create/dependency_manager.ts b/src/plugins/presentation_util/public/services/create/dependency_manager.ts index de30b180607fe4..3925f3e9d9c4fe 100644 --- a/src/plugins/presentation_util/public/services/create/dependency_manager.ts +++ b/src/plugins/presentation_util/public/services/create/dependency_manager.ts @@ -41,7 +41,14 @@ export class DependencyManager { return cycleInfo; } - return DependencyManager.sortVerticesFrom(srcVertex, graph, sortedVertices, {}, {}); + return DependencyManager.sortVerticesFrom( + srcVertex, + graph, + sortedVertices, + {}, + {}, + cycleInfo + ); }, DependencyManager.createCycleInfo()); } @@ -58,24 +65,30 @@ export class DependencyManager { graph: Graph, sortedVertices: Set, visited: BreadCrumbs = {}, - inpath: BreadCrumbs = {} + inpath: BreadCrumbs = {}, + cycle: CycleDetectionResult ): CycleDetectionResult { visited[srcVertex] = true; inpath[srcVertex] = true; - const cycleInfo = graph[srcVertex]?.reduce | undefined>( - (info, vertex) => { - if (inpath[vertex]) { - const path = (Object.keys(inpath) as T[]).filter( - (visitedVertex) => inpath[visitedVertex] - ); - return DependencyManager.createCycleInfo([...path, vertex], true); - } else if (!visited[vertex]) { - return DependencyManager.sortVerticesFrom(vertex, graph, sortedVertices, visited, inpath); - } - return info; - }, - undefined - ); + + const vertexEdges = + graph[srcVertex] === undefined || graph[srcVertex] === null ? [] : graph[srcVertex]; + + cycle = vertexEdges!.reduce>((info, vertex) => { + if (inpath[vertex]) { + return { ...info, hasCycle: true }; + } else if (!visited[vertex]) { + return DependencyManager.sortVerticesFrom( + vertex, + graph, + sortedVertices, + visited, + inpath, + info + ); + } + return info; + }, cycle); inpath[srcVertex] = false; @@ -83,7 +96,10 @@ export class DependencyManager { sortedVertices.add(srcVertex); } - return cycleInfo ?? DependencyManager.createCycleInfo([...sortedVertices]); + return { + ...cycle, + path: [...sortedVertices], + }; } private static createCycleInfo( diff --git a/src/plugins/telemetry/kibana.json b/src/plugins/telemetry/kibana.json index 09cc6accb68f4b..a6796e42f92282 100644 --- a/src/plugins/telemetry/kibana.json +++ b/src/plugins/telemetry/kibana.json @@ -8,6 +8,7 @@ "server": true, "ui": true, "requiredPlugins": ["telemetryCollectionManager", "usageCollection", "screenshotMode"], + "optionalPlugins": ["home", "security"], "extraPublicDirs": ["common/constants"], "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts index 3072ff67703d78..794183cb8a8f5d 100644 --- a/src/plugins/telemetry/public/plugin.ts +++ b/src/plugins/telemetry/public/plugin.ts @@ -31,6 +31,8 @@ import { } from '../common/telemetry_config'; import { getNotifyUserAboutOptInDefault } from '../common/telemetry_config/get_telemetry_notify_user_about_optin_default'; import { PRIVACY_STATEMENT_URL } from '../common/constants'; +import { HomePublicPluginSetup } from '../../home/public'; +import { renderWelcomeTelemetryNotice } from './render_welcome_telemetry_notice'; /** * Publicly exposed APIs from the Telemetry Service @@ -82,6 +84,7 @@ export interface TelemetryPluginStart { interface TelemetryPluginSetupDependencies { screenshotMode: ScreenshotModePluginSetup; + home?: HomePublicPluginSetup; } /** @@ -121,7 +124,7 @@ export class TelemetryPlugin implements Plugin { + if (this.telemetryService?.userCanChangeSettings) { + this.telemetryNotifications?.setOptedInNoticeSeen(); + } + }); + + home.welcomeScreen.registerTelemetryNoticeRenderer(() => + renderWelcomeTelemetryNotice(this.telemetryService!, http.basePath.prepend) + ); + } + return { telemetryService: this.getTelemetryServicePublicApis(), }; diff --git a/src/plugins/telemetry/public/render_welcome_telemetry_notice.test.ts b/src/plugins/telemetry/public/render_welcome_telemetry_notice.test.ts new file mode 100644 index 00000000000000..6da76db915656d --- /dev/null +++ b/src/plugins/telemetry/public/render_welcome_telemetry_notice.test.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { renderWelcomeTelemetryNotice } from './render_welcome_telemetry_notice'; +import { mockTelemetryService } from './mocks'; + +describe('renderWelcomeTelemetryNotice', () => { + test('it should show the opt-out message', () => { + const telemetryService = mockTelemetryService(); + const component = mountWithIntl(renderWelcomeTelemetryNotice(telemetryService, (url) => url)); + expect(component.exists('[id="telemetry.dataManagementDisableCollectionLink"]')).toBe(true); + }); + + test('it should show the opt-in message', () => { + const telemetryService = mockTelemetryService({ config: { optIn: false } }); + const component = mountWithIntl(renderWelcomeTelemetryNotice(telemetryService, (url) => url)); + expect(component.exists('[id="telemetry.dataManagementEnableCollectionLink"]')).toBe(true); + }); + + test('it should not show opt-in/out options if user cannot change the settings', () => { + const telemetryService = mockTelemetryService({ config: { allowChangingOptInStatus: false } }); + const component = mountWithIntl(renderWelcomeTelemetryNotice(telemetryService, (url) => url)); + expect(component.exists('[id="telemetry.dataManagementDisableCollectionLink"]')).toBe(false); + expect(component.exists('[id="telemetry.dataManagementEnableCollectionLink"]')).toBe(false); + }); +}); diff --git a/src/plugins/telemetry/public/render_welcome_telemetry_notice.tsx b/src/plugins/telemetry/public/render_welcome_telemetry_notice.tsx new file mode 100644 index 00000000000000..8ef26fb797d532 --- /dev/null +++ b/src/plugins/telemetry/public/render_welcome_telemetry_notice.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiLink, EuiSpacer, EuiTextColor } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { TelemetryService } from './services'; +import { PRIVACY_STATEMENT_URL } from '../common/constants'; + +export function renderWelcomeTelemetryNotice( + telemetryService: TelemetryService, + addBasePath: (url: string) => string +) { + return ( + <> + + + + + + {renderTelemetryEnabledOrDisabledText(telemetryService, addBasePath)} + + + + ); +} + +function renderTelemetryEnabledOrDisabledText( + telemetryService: TelemetryService, + addBasePath: (url: string) => string +) { + if (!telemetryService.userCanChangeSettings || !telemetryService.getCanChangeOptInStatus()) { + return null; + } + + const isOptedIn = telemetryService.getIsOptedIn(); + + if (isOptedIn) { + return ( + <> + + + + + + ); + } else { + return ( + <> + + + + + + ); + } +} diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index 73c61ea1c50386..681a871ba105b6 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -23,6 +23,7 @@ import type { Plugin, Logger, } from 'src/core/server'; +import type { SecurityPluginStart } from '../../../../x-pack/plugins/security/server'; import { SavedObjectsClient } from '../../../core/server'; import { registerRoutes } from './routes'; import { registerCollection } from './telemetry_collection'; @@ -42,6 +43,7 @@ interface TelemetryPluginsDepsSetup { interface TelemetryPluginsDepsStart { telemetryCollectionManager: TelemetryCollectionManagerPluginStart; + security?: SecurityPluginStart; } /** @@ -90,6 +92,8 @@ export class TelemetryPlugin implements Plugin(1); + private security?: SecurityPluginStart; + constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); this.isDev = initializerContext.env.mode.dev; @@ -119,6 +123,7 @@ export class TelemetryPlugin implements Plugin this.security, }); this.registerMappings((opts) => savedObjects.registerType(opts)); @@ -137,11 +142,17 @@ export class TelemetryPlugin implements Plugin; + getSecurity: SecurityGetter; } export function registerRoutes(options: RegisterRoutesParams) { - const { isDev, telemetryCollectionManager, router, savedObjectsInternalClient$ } = options; + const { isDev, telemetryCollectionManager, router, savedObjectsInternalClient$, getSecurity } = + options; registerTelemetryOptInRoutes(options); - registerTelemetryUsageStatsRoutes(router, telemetryCollectionManager, isDev); + registerTelemetryUsageStatsRoutes(router, telemetryCollectionManager, isDev, getSecurity); registerTelemetryOptInStatsRoutes(router, telemetryCollectionManager); registerTelemetryUserHasSeenNotice(router); registerTelemetryLastReported(router, savedObjectsInternalClient$); diff --git a/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts index 2a956656621944..6139eee3e10ca6 100644 --- a/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts +++ b/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts @@ -75,7 +75,6 @@ export function registerTelemetryOptInStatsRoutes( const statsGetterConfig: StatsGetterConfig = { unencrypted, - request: req, }; const optInStatus = await telemetryCollectionManager.getOptInStats( diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts index 736367446d3c05..bc7569585c127b 100644 --- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts +++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts @@ -8,7 +8,8 @@ import { registerTelemetryUsageStatsRoutes } from './telemetry_usage_stats'; import { coreMock, httpServerMock } from 'src/core/server/mocks'; -import type { RequestHandlerContext, IRouter } from 'kibana/server'; +import type { RequestHandlerContext, IRouter } from 'src/core/server'; +import { securityMock } from '../../../../../x-pack/plugins/security/server/mocks'; import { telemetryCollectionManagerPluginMock } from '../../../telemetry_collection_manager/server/mocks'; async function runRequest( @@ -35,13 +36,18 @@ describe('registerTelemetryUsageStatsRoutes', () => { }; const telemetryCollectionManager = telemetryCollectionManagerPluginMock.createSetupContract(); const mockCoreSetup = coreMock.createSetup(); - const mockRouter = mockCoreSetup.http.createRouter(); const mockStats = [{ clusterUuid: 'text', stats: 'enc_str' }]; telemetryCollectionManager.getStats.mockResolvedValue(mockStats); + const getSecurity = jest.fn(); + + let mockRouter: IRouter; + beforeEach(() => { + mockRouter = mockCoreSetup.http.createRouter(); + }); describe('clusters/_stats POST route', () => { it('registers _stats POST route and accepts body configs', () => { - registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity); expect(mockRouter.post).toBeCalledTimes(1); const [routeConfig, handler] = (mockRouter.post as jest.Mock).mock.calls[0]; expect(routeConfig.path).toMatchInlineSnapshot(`"/api/telemetry/v2/clusters/_stats"`); @@ -50,11 +56,10 @@ describe('registerTelemetryUsageStatsRoutes', () => { }); it('responds with encrypted stats with no cache refresh by default', async () => { - registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity); - const { mockRequest, mockResponse } = await runRequest(mockRouter); + const { mockResponse } = await runRequest(mockRouter); expect(telemetryCollectionManager.getStats).toBeCalledWith({ - request: mockRequest, unencrypted: undefined, refreshCache: undefined, }); @@ -63,39 +68,99 @@ describe('registerTelemetryUsageStatsRoutes', () => { }); it('when unencrypted is set getStats is called with unencrypted and refreshCache', async () => { - registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity); - const { mockRequest } = await runRequest(mockRouter, { unencrypted: true }); + await runRequest(mockRouter, { unencrypted: true }); expect(telemetryCollectionManager.getStats).toBeCalledWith({ - request: mockRequest, unencrypted: true, refreshCache: true, }); }); it('calls getStats with refreshCache when set in body', async () => { - registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); - const { mockRequest } = await runRequest(mockRouter, { refreshCache: true }); + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity); + await runRequest(mockRouter, { refreshCache: true }); expect(telemetryCollectionManager.getStats).toBeCalledWith({ - request: mockRequest, unencrypted: undefined, refreshCache: true, }); }); it('calls getStats with refreshCache:true even if set to false in body when unencrypted is set to true', async () => { - registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); - const { mockRequest } = await runRequest(mockRouter, { + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity); + await runRequest(mockRouter, { refreshCache: false, unencrypted: true, }); expect(telemetryCollectionManager.getStats).toBeCalledWith({ - request: mockRequest, unencrypted: true, refreshCache: true, }); }); + it('returns 403 when the user does not have enough permissions to request unencrypted telemetry', async () => { + const getSecurityMock = jest.fn().mockImplementation(() => { + const securityStartMock = securityMock.createStart(); + securityStartMock.authz.checkPrivilegesWithRequest.mockReturnValue({ + globally: () => ({ hasAllRequested: false }), + }); + return securityStartMock; + }); + registerTelemetryUsageStatsRoutes( + mockRouter, + telemetryCollectionManager, + true, + getSecurityMock + ); + const { mockResponse } = await runRequest(mockRouter, { + refreshCache: false, + unencrypted: true, + }); + expect(mockResponse.forbidden).toBeCalled(); + }); + + it('returns 200 when the user has enough permissions to request unencrypted telemetry', async () => { + const getSecurityMock = jest.fn().mockImplementation(() => { + const securityStartMock = securityMock.createStart(); + securityStartMock.authz.checkPrivilegesWithRequest.mockReturnValue({ + globally: () => ({ hasAllRequested: true }), + }); + return securityStartMock; + }); + registerTelemetryUsageStatsRoutes( + mockRouter, + telemetryCollectionManager, + true, + getSecurityMock + ); + const { mockResponse } = await runRequest(mockRouter, { + refreshCache: false, + unencrypted: true, + }); + expect(mockResponse.ok).toBeCalled(); + }); + + it('returns 200 when the user does not have enough permissions to request unencrypted telemetry but it requests encrypted', async () => { + const getSecurityMock = jest.fn().mockImplementation(() => { + const securityStartMock = securityMock.createStart(); + securityStartMock.authz.checkPrivilegesWithRequest.mockReturnValue({ + globally: () => ({ hasAllRequested: false }), + }); + return securityStartMock; + }); + registerTelemetryUsageStatsRoutes( + mockRouter, + telemetryCollectionManager, + true, + getSecurityMock + ); + const { mockResponse } = await runRequest(mockRouter, { + refreshCache: false, + unencrypted: false, + }); + expect(mockResponse.ok).toBeCalled(); + }); + it.todo('always returns an empty array on errors on encrypted payload'); it.todo('returns the actual request error object when in development mode'); it.todo('returns forbidden on unencrypted and ES returns 403 in getStats'); diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts index 2f72ae818f1121..4647f5afe0760b 100644 --- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts +++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts @@ -12,11 +12,15 @@ import { TelemetryCollectionManagerPluginSetup, StatsGetterConfig, } from 'src/plugins/telemetry_collection_manager/server'; +import type { SecurityPluginStart } from '../../../../../x-pack/plugins/security/server'; + +export type SecurityGetter = () => SecurityPluginStart | undefined; export function registerTelemetryUsageStatsRoutes( router: IRouter, telemetryCollectionManager: TelemetryCollectionManagerPluginSetup, - isDev: boolean + isDev: boolean, + getSecurity: SecurityGetter ) { router.post( { @@ -31,9 +35,22 @@ export function registerTelemetryUsageStatsRoutes( async (context, req, res) => { const { unencrypted, refreshCache } = req.body; + const security = getSecurity(); + if (security && unencrypted) { + // Normally we would use `options: { tags: ['access:decryptedTelemetry'] }` in the route definition to check authorization for an + // API action, however, we want to check this conditionally based on the `unencrypted` parameter. In this case we need to use the + // security API directly to check privileges for this action. Note that the 'decryptedTelemetry' API privilege string is only + // granted to users that have "Global All" or "Global Read" privileges in Kibana. + const { checkPrivilegesWithRequest, actions } = security.authz; + const privileges = { kibana: actions.api.get('decryptedTelemetry') }; + const { hasAllRequested } = await checkPrivilegesWithRequest(req).globally(privileges); + if (!hasAllRequested) { + return res.forbidden(); + } + } + try { const statsConfig: StatsGetterConfig = { - request: req, unencrypted, refreshCache: unencrypted || refreshCache, }; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts index 83f33a894b9032..4340eaafd2d8ff 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts @@ -7,10 +7,9 @@ */ import { omit } from 'lodash'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; -import { StatsCollectionContext } from 'src/plugins/telemetry_collection_manager/server'; -import { ElasticsearchClient } from 'src/core/server'; +import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import type { StatsCollectionContext } from 'src/plugins/telemetry_collection_manager/server'; export interface KibanaUsageStats { kibana: { @@ -71,9 +70,8 @@ export function handleKibanaStats( export async function getKibana( usageCollection: UsageCollectionSetup, asInternalUser: ElasticsearchClient, - soClient: SavedObjectsClientContract, - kibanaRequest: KibanaRequest | undefined // intentionally `| undefined` to enforce providing the parameter + soClient: SavedObjectsClientContract ): Promise { - const usage = await usageCollection.bulkFetch(asInternalUser, soClient, kibanaRequest); + const usage = await usageCollection.bulkFetch(asInternalUser, soClient); return usageCollection.toObject(usage); } diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts index 2392ac570ecbc1..fa45438e00fbe3 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts @@ -14,7 +14,7 @@ import { usageCollectionPluginMock, createCollectorFetchContextMock, } from '../../../usage_collection/server/mocks'; -import { elasticsearchServiceMock, httpServerMock } from '../../../../../src/core/server/mocks'; +import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { StatsCollectionConfig } from '../../../telemetry_collection_manager/server'; function mockUsageCollection(kibanaUsage = {}) { @@ -74,7 +74,6 @@ function mockStatsCollectionConfig( ...createCollectorFetchContextMock(), esClient: mockGetLocalStats(clusterInfo, clusterStats), usageCollection: mockUsageCollection(kibana), - kibanaRequest: httpServerMock.createKibanaRequest(), refreshCache: false, }; } diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts index ae2a849ccfa19a..73de59ae8156aa 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -65,7 +65,7 @@ export const getLocalStats: StatsGetter = async ( config, context ) => { - const { usageCollection, esClient, soClient, kibanaRequest } = config; + const { usageCollection, esClient, soClient } = config; return await Promise.all( clustersDetails.map(async (clustersDetail) => { @@ -73,7 +73,7 @@ export const getLocalStats: StatsGetter = async ( getClusterInfo(esClient), // cluster info getClusterStats(esClient), // cluster stats (not to be confused with cluster _state_) getNodesUsage(esClient), // nodes_usage info - getKibana(usageCollection, esClient, soClient, kibanaRequest), + getKibana(usageCollection, esClient, soClient), getDataTelemetry(esClient), ]); return handleLocalStats( diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json index d50ccd563fe5ac..052d484447e428 100644 --- a/src/plugins/telemetry/tsconfig.json +++ b/src/plugins/telemetry/tsconfig.json @@ -17,10 +17,12 @@ ], "references": [ { "path": "../../core/tsconfig.json" }, + { "path": "../../plugins/home/tsconfig.json" }, { "path": "../../plugins/kibana_react/tsconfig.json" }, { "path": "../../plugins/kibana_utils/tsconfig.json" }, { "path": "../../plugins/screenshot_mode/tsconfig.json" }, { "path": "../../plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "../../plugins/usage_collection/tsconfig.json" } + { "path": "../../plugins/usage_collection/tsconfig.json" }, + { "path": "../../../x-pack/plugins/security/tsconfig.json" } ] } diff --git a/src/plugins/telemetry_collection_manager/server/plugin.test.ts b/src/plugins/telemetry_collection_manager/server/plugin.test.ts index ca932e92d98bdb..990e237b6b2724 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.test.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { coreMock, httpServerMock } from '../../../core/server/mocks'; +import { coreMock } from '../../../core/server/mocks'; import { usageCollectionPluginMock } from '../../usage_collection/server/mocks'; import { TelemetryCollectionManagerPlugin } from './plugin'; import type { BasicStatsPayload, CollectionStrategyConfig, StatsGetterConfig } from './types'; @@ -217,19 +217,17 @@ describe('Telemetry Collection Manager', () => { }); }); describe('unencrypted: true', () => { - const mockRequest = httpServerMock.createKibanaRequest(); const config: StatsGetterConfig = { unencrypted: true, - request: mockRequest, }; describe('getStats', () => { - test('getStats returns empty because clusterDetails returns empty, and the soClient is not an instance of the TelemetrySavedObjectsClient', async () => { + test('getStats returns empty because clusterDetails returns empty, and the soClient is an instance of the TelemetrySavedObjectsClient', async () => { collectionStrategy.clusterDetailsGetter.mockResolvedValue([]); await expect(setupApi.getStats(config)).resolves.toStrictEqual([]); expect( collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + ).toBeInstanceOf(TelemetrySavedObjectsClient); }); test('returns encrypted payload (assumes opted-in when no explicitly opted-out)', async () => { collectionStrategy.clusterDetailsGetter.mockResolvedValue([ @@ -249,7 +247,7 @@ describe('Telemetry Collection Manager', () => { expect( collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + ).toBeInstanceOf(TelemetrySavedObjectsClient); }); it('calls getStats with config { refreshCache: true } even if set to false', async () => { @@ -267,7 +265,6 @@ describe('Telemetry Collection Manager', () => { expect(getStatsCollectionConfig).toReturnWith( expect.objectContaining({ refreshCache: true, - kibanaRequest: mockRequest, }) ); @@ -281,7 +278,7 @@ describe('Telemetry Collection Manager', () => { await expect(setupApi.getOptInStats(true, config)).resolves.toStrictEqual([]); expect( collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + ).toBeInstanceOf(TelemetrySavedObjectsClient); }); test('returns results for opt-in true', async () => { @@ -296,7 +293,7 @@ describe('Telemetry Collection Manager', () => { ]); expect( collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + ).toBeInstanceOf(TelemetrySavedObjectsClient); }); test('returns results for opt-in false', async () => { @@ -311,7 +308,7 @@ describe('Telemetry Collection Manager', () => { ]); expect( collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + ).toBeInstanceOf(TelemetrySavedObjectsClient); }); }); }); diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index fad51ca1dbfde8..cffe736f8eeaf5 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -126,11 +126,10 @@ export class TelemetryCollectionManagerPlugin const esClient = this.getElasticsearchClient(config); const soClient = this.getSavedObjectsClient(config); // Provide the kibanaRequest so opted-in plugins can scope their custom clients only if the request is not encrypted - const kibanaRequest = config.unencrypted ? config.request : void 0; const refreshCache = config.unencrypted ? true : !!config.refreshCache; if (esClient && soClient) { - return { usageCollection, esClient, soClient, kibanaRequest, refreshCache }; + return { usageCollection, esClient, soClient, refreshCache }; } } @@ -142,9 +141,7 @@ export class TelemetryCollectionManagerPlugin * @private */ private getElasticsearchClient(config: StatsGetterConfig): ElasticsearchClient | undefined { - return config.unencrypted - ? this.elasticsearchClient?.asScoped(config.request).asCurrentUser - : this.elasticsearchClient?.asInternalUser; + return this.elasticsearchClient?.asInternalUser; } /** @@ -155,11 +152,7 @@ export class TelemetryCollectionManagerPlugin * @private */ private getSavedObjectsClient(config: StatsGetterConfig): SavedObjectsClientContract | undefined { - if (config.unencrypted) { - // Intentionally using the scoped client here to make use of all the security wrappers. - // It also returns spaces-scoped telemetry. - return this.savedObjectsService?.getScopedClient(config.request); - } else if (this.savedObjectsService) { + if (this.savedObjectsService) { // Wrapping the internalRepository with the `TelemetrySavedObjectsClient` // to ensure some best practices when collecting "all the telemetry" // (i.e.: `.find` requests should query all spaces) diff --git a/src/plugins/telemetry_collection_manager/server/types.ts b/src/plugins/telemetry_collection_manager/server/types.ts index 7ea32844a858cb..9658c0d68d05db 100644 --- a/src/plugins/telemetry_collection_manager/server/types.ts +++ b/src/plugins/telemetry_collection_manager/server/types.ts @@ -6,14 +6,9 @@ * Side Public License, v 1. */ -import { - ElasticsearchClient, - Logger, - KibanaRequest, - SavedObjectsClientContract, -} from 'src/core/server'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { TelemetryCollectionManagerPlugin } from './plugin'; +import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from 'src/core/server'; +import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import type { TelemetryCollectionManagerPlugin } from './plugin'; export interface TelemetryCollectionManagerPluginSetup { setCollectionStrategy: ( @@ -36,7 +31,6 @@ export interface TelemetryOptInStats { export interface BaseStatsGetterConfig { unencrypted: boolean; refreshCache?: boolean; - request?: KibanaRequest; } export interface EncryptedStatsGetterConfig extends BaseStatsGetterConfig { @@ -45,7 +39,6 @@ export interface EncryptedStatsGetterConfig extends BaseStatsGetterConfig { export interface UnencryptedStatsGetterConfig extends BaseStatsGetterConfig { unencrypted: true; - request: KibanaRequest; } export interface ClusterDetails { @@ -56,7 +49,6 @@ export interface StatsCollectionConfig { usageCollection: UsageCollectionSetup; esClient: ElasticsearchClient; soClient: SavedObjectsClientContract; - kibanaRequest: KibanaRequest | undefined; // intentionally `| undefined` to enforce providing the parameter refreshCache: boolean; } diff --git a/src/plugins/usage_collection/README.mdx b/src/plugins/usage_collection/README.mdx index a58f197818bf4e..03d8f7badb8c2a 100644 --- a/src/plugins/usage_collection/README.mdx +++ b/src/plugins/usage_collection/README.mdx @@ -297,8 +297,7 @@ Some background: - `isReady` (added in v7.2.0 and v6.8.4) is a way for a usage collector to announce that some async process must finish first before it can return data in the `fetch` method (e.g. a client needs to ne initialized, or the task manager needs to run a task first). If any collector reports that it is not ready when we call its `fetch` method, we reset a flag to try again and, after a set amount of time, collect data from those collectors that are ready and skip any that are not. This means that if a collector returns `true` for `isReady` and it actually isn't ready to return data, there won't be telemetry data from that collector in that telemetry report (usually once per day). You should consider what it means if your collector doesn't return data in the first few documents when Kibana starts or, if we should wait for any other reason (e.g. the task manager needs to run your task first). If you need to tell telemetry collection to wait, you should implement this function with custom logic. If your `fetch` method can run without the need of any previous dependencies, then you can return true for `isReady` as shown in the example below. -- The `fetch` method needs to support multiple contexts in which it is called. For example, when a user requests the example of what we collect in the **Kibana>Advanced Settings>Usage data** section, the clients provided in the context of the function (`CollectorFetchContext`) are scoped to that user's privileges. The reason is to avoid exposing via telemetry any data that user should not have access to (i.e.: if the user does not have access to certain indices, they shouldn't be allowed to see the number of documents that exists in it). In this case, the `fetch` method receives the clients `esClient` and `soClient` scoped to the user who performed the HTTP API request. Alternatively, when requesting the usage data to be reported to the Remote Telemetry Service, the clients are scoped to the internal Kibana user (`kibana_system`). Please, mind it might have lower-level access than the default super-admin `elastic` test user. -In some scenarios, your collector might need to maintain its own client. An example of that is the `monitoring` plugin, that maintains a connection to the Remote Monitoring Cluster to push its monitoring data. If that's the case, your plugin can opt-in to receive the additional `kibanaRequest` parameter by adding `extendFetchContext.kibanaRequest: true` to the collector's config: it will be appended to the context of the `fetch` method only if the request needs to be scoped to a user other than Kibana Internal, so beware that your collector will need to work for both scenarios (especially for the scenario when `kibanaRequest` is missing). +- The clients provided to the `fetch` method are scoped to the internal Kibana user (`kibana_system`). Note: there will be many cases where you won't need to use the `esClient` or `soClient` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS. diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 74373d44a359b6..1ff04cf3650c0b 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -7,20 +7,14 @@ */ import type { Logger } from 'src/core/server'; -import type { - CollectorFetchMethod, - CollectorOptions, - CollectorOptionsFetchExtendedContext, - ICollector, -} from './types'; +import type { CollectorFetchMethod, CollectorOptions, ICollector } from './types'; export class Collector implements ICollector { - public readonly extendFetchContext: CollectorOptionsFetchExtendedContext; - public readonly type: CollectorOptions['type']; - public readonly fetch: CollectorFetchMethod; - public readonly isReady: CollectorOptions['isReady']; + public readonly type: CollectorOptions['type']; + public readonly fetch: CollectorFetchMethod; + public readonly isReady: CollectorOptions['isReady']; /** * @private Constructor of a Collector. It should be called via the CollectorSet factory methods: `makeStatsCollector` and `makeUsageCollector` * @param log {@link Logger} @@ -28,15 +22,7 @@ export class Collector */ constructor( public readonly log: Logger, - { - type, - fetch, - isReady, - extendFetchContext = {}, - ...options - }: // Any does not affect here, but needs to be set so it doesn't affect anything else down the line - // eslint-disable-next-line @typescript-eslint/no-explicit-any - CollectorOptions + { type, fetch, isReady, ...options }: CollectorOptions ) { if (type === undefined) { throw new Error('Collector must be instantiated with a options.type string property'); @@ -50,6 +36,5 @@ export class Collector this.type = type; this.fetch = fetch; this.isReady = typeof isReady === 'function' ? isReady : () => true; - this.extendFetchContext = extendFetchContext; } } diff --git a/src/plugins/usage_collection/server/collector/collector_set.test.ts b/src/plugins/usage_collection/server/collector/collector_set.test.ts index 5e0698b286f79b..87e841f3de4c54 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.test.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.test.ts @@ -15,7 +15,6 @@ import { elasticsearchServiceMock, loggingSystemMock, savedObjectsClientMock, - httpServerMock, executionContextServiceMock, } from '../../../../core/server/mocks'; import type { ExecutionContextSetup, Logger } from 'src/core/server'; @@ -39,7 +38,6 @@ describe('CollectorSet', () => { }); const mockEsClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const mockSoClient = savedObjectsClientMock.create(); - const req = void 0; // No need to instantiate any KibanaRequest in these tests it('should throw an error if non-Collector type of object is registered', () => { const collectors = new CollectorSet(collectorSetConfig); @@ -88,7 +86,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); + const result = await collectors.bulkFetch(mockEsClient, mockSoClient); expect(logger.debug).toHaveBeenCalledTimes(2); expect(logger.debug).toHaveBeenCalledWith('Getting ready collectors'); expect(logger.debug).toHaveBeenCalledWith('Fetching data from MY_TEST_COLLECTOR collector'); @@ -121,7 +119,7 @@ describe('CollectorSet', () => { let result; try { - result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); + result = await collectors.bulkFetch(mockEsClient, mockSoClient); } catch (err) { // Do nothing } @@ -150,7 +148,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); + const result = await collectors.bulkFetch(mockEsClient, mockSoClient); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', @@ -178,7 +176,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); + const result = await collectors.bulkFetch(mockEsClient, mockSoClient); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', @@ -269,50 +267,6 @@ describe('CollectorSet', () => { collectorSet = new CollectorSet(collectorSetConfig); }); - test('TS should hide kibanaRequest when not opted-in', () => { - collectorSet.makeStatsCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - }); - }); - - test('TS should hide kibanaRequest when not opted-in (explicit false)', () => { - collectorSet.makeStatsCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: false, - }, - }); - }); - - test('TS should allow using kibanaRequest when opted-in (explicit true)', () => { - collectorSet.makeStatsCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: true, - }, - }); - }); - test('fetch can use the logger (TS allows it)', () => { const collector = collectorSet.makeStatsCollector({ type: 'MY_TEST_COLLECTOR', @@ -339,188 +293,6 @@ describe('CollectorSet', () => { collectorSet = new CollectorSet(collectorSetConfig); }); - describe('TS validations', () => { - describe('when types are inferred', () => { - test('TS should hide kibanaRequest when not opted-in', () => { - collectorSet.makeUsageCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - }); - }); - - test('TS should hide kibanaRequest when not opted-in (explicit false)', () => { - collectorSet.makeUsageCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: false, - }, - }); - }); - - test('TS should allow using kibanaRequest when opted-in (explicit true)', () => { - collectorSet.makeUsageCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: true, - }, - }); - }); - }); - - describe('when types are explicit', () => { - test('TS should hide `kibanaRequest` from ctx when undefined or false', () => { - collectorSet.makeUsageCollector<{ test: number }>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - }); - collectorSet.makeUsageCollector<{ test: number }, false>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: false, - }, - }); - collectorSet.makeUsageCollector<{ test: number }, false>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - }); - }); - test('TS should not allow `true` when types declare false', () => { - // false is the default when at least 1 type is specified - collectorSet.makeUsageCollector<{ test: number }>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - // @ts-expect-error - kibanaRequest: true, - }, - }); - collectorSet.makeUsageCollector<{ test: number }, false>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - // @ts-expect-error - kibanaRequest: true, - }, - }); - }); - - test('TS should allow `true` when types explicitly declare `true` and do not allow `false` or undefined', () => { - // false is the default when at least 1 type is specified - collectorSet.makeUsageCollector<{ test: number }, true>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: true, - }, - }); - collectorSet.makeUsageCollector<{ test: number }, true>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - // @ts-expect-error - kibanaRequest: false, - }, - }); - collectorSet.makeUsageCollector<{ test: number }, true>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - // @ts-expect-error - kibanaRequest: undefined, - }, - }); - collectorSet.makeUsageCollector<{ test: number }, true>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - // @ts-expect-error - extendFetchContext: {}, - }); - collectorSet.makeUsageCollector<{ test: number }, true>( - // @ts-expect-error - { - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - } - ); - }); - }); - }); - test('fetch can use the logger (TS allows it)', () => { const collector = collectorSet.makeUsageCollector({ type: 'MY_TEST_COLLECTOR', @@ -777,31 +549,5 @@ describe('CollectorSet', () => { expect.any(Function) ); }); - - it('adds extra context to collectors with extendFetchContext config', async () => { - const mockReadyFetch = jest.fn().mockResolvedValue({}); - collectorSet.registerCollector( - collectorSet.makeUsageCollector({ - type: 'ready_col', - isReady: () => true, - schema: {}, - fetch: mockReadyFetch, - extendFetchContext: { kibanaRequest: true }, - }) - ); - - const mockEsClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - const mockSoClient = savedObjectsClientMock.create(); - const request = httpServerMock.createKibanaRequest(); - const results = await collectorSet.bulkFetch(mockEsClient, mockSoClient, request); - - expect(mockReadyFetch).toBeCalledTimes(1); - expect(mockReadyFetch).toBeCalledWith({ - esClient: mockEsClient, - soClient: mockSoClient, - kibanaRequest: request, - }); - expect(results).toHaveLength(2); - }); }); }); diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts index 49332b0a1826fc..3a7c0a66ac60d1 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.ts @@ -11,7 +11,6 @@ import type { Logger, ElasticsearchClient, SavedObjectsClientContract, - KibanaRequest, KibanaExecutionContext, ExecutionContextSetup, } from 'src/core/server'; @@ -64,12 +63,8 @@ export class CollectorSet { * Instantiates a stats collector with the definition provided in the options * @param options Definition of the collector {@link CollectorOptions} */ - public makeStatsCollector = < - TFetchReturn, - WithKibanaRequest extends boolean, - ExtraOptions extends object = {} - >( - options: CollectorOptions + public makeStatsCollector = ( + options: CollectorOptions ) => { return new Collector(this.logger, options); }; @@ -78,15 +73,8 @@ export class CollectorSet { * Instantiates an usage collector with the definition provided in the options * @param options Definition of the collector {@link CollectorOptions} */ - public makeUsageCollector = < - TFetchReturn, - // TODO: Right now, users will need to explicitly claim `true` for TS to allow `kibanaRequest` usage. - // If we improve `telemetry-check-tools` so plugins do not need to specify TFetchReturn, - // we'll be able to remove the type defaults and TS will successfully infer the config value as provided in JS. - WithKibanaRequest extends boolean = false, - ExtraOptions extends object = {} - >( - options: UsageCollectorOptions + public makeUsageCollector = ( + options: UsageCollectorOptions ) => { return new UsageCollector(this.logger, options); }; @@ -191,7 +179,6 @@ export class CollectorSet { public bulkFetch = async ( esClient: ElasticsearchClient, soClient: SavedObjectsClientContract, - kibanaRequest: KibanaRequest | undefined, // intentionally `| undefined` to enforce providing the parameter collectors: Map = this.collectors ) => { this.logger.debug(`Getting ready collectors`); @@ -209,11 +196,7 @@ export class CollectorSet { readyCollectors.map(async (collector) => { this.logger.debug(`Fetching data from ${collector.type} collector`); try { - const context = { - esClient, - soClient, - ...(collector.extendFetchContext.kibanaRequest && { kibanaRequest }), - }; + const context = { esClient, soClient }; const executionContext: KibanaExecutionContext = { type: 'usage_collection', name: 'collector.fetch', @@ -254,16 +237,10 @@ export class CollectorSet { public bulkFetchUsage = async ( esClient: ElasticsearchClient, - savedObjectsClient: SavedObjectsClientContract, - kibanaRequest: KibanaRequest | undefined // intentionally `| undefined` to enforce providing the parameter + savedObjectsClient: SavedObjectsClientContract ) => { const usageCollectors = this.getFilteredCollectorSet((c) => c instanceof UsageCollector); - return await this.bulkFetch( - esClient, - savedObjectsClient, - kibanaRequest, - usageCollectors.collectors - ); + return await this.bulkFetch(esClient, savedObjectsClient, usageCollectors.collectors); }; /** diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts index ca240a520ee24a..e284844b34c344 100644 --- a/src/plugins/usage_collection/server/collector/index.ts +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -17,7 +17,6 @@ export type { CollectorOptions, CollectorFetchContext, CollectorFetchMethod, - CollectorOptionsFetchExtendedContext, ICollector as Collector, } from './types'; export type { UsageCollectorOptions } from './usage_collector'; diff --git a/src/plugins/usage_collection/server/collector/types.ts b/src/plugins/usage_collection/server/collector/types.ts index bf1e9f4644b1b7..8d427d211a191b 100644 --- a/src/plugins/usage_collection/server/collector/types.ts +++ b/src/plugins/usage_collection/server/collector/types.ts @@ -6,12 +6,7 @@ * Side Public License, v 1. */ -import type { - ElasticsearchClient, - KibanaRequest, - SavedObjectsClientContract, - Logger, -} from 'src/core/server'; +import type { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'src/core/server'; /** Types matching number values **/ export type AllowedSchemaNumberTypes = @@ -73,7 +68,7 @@ export type MakeSchemaFrom = { * * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster. */ -export type CollectorFetchContext = { +export interface CollectorFetchContext { /** * Request-scoped Elasticsearch client * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster (more info: {@link CollectorFetchContext}) @@ -84,58 +79,22 @@ export type CollectorFetchContext = ( +export type CollectorFetchMethod = ( this: ICollector & ExtraOptions, // Specify the context of `this` for this.log and others to become available - context: CollectorFetchContext + context: CollectorFetchContext ) => Promise | TReturn; -export interface ICollectorOptionsFetchExtendedContext { - /** - * Set to `true` if your `fetch` method requires the `KibanaRequest` object to be added in its context {@link CollectorFetchContextWithRequest}. - * @remark You should fully acknowledge that by using the `KibanaRequest` in your collector, you need to ensure it should specially work without it because it won't be provided when building the telemetry payload actually sent to the remote telemetry service. - */ - kibanaRequest?: WithKibanaRequest; -} - -/** - * The options to extend the context provided to the `fetch` method. - * @remark Only to be used in very rare scenarios when this is really needed. - */ -export type CollectorOptionsFetchExtendedContext = - ICollectorOptionsFetchExtendedContext & - (WithKibanaRequest extends true // If enforced to true via Types, the config must be expected - ? Required, 'kibanaRequest'>> - : {}); - /** * Options to instantiate a collector */ -export type CollectorOptions< - TFetchReturn = unknown, - WithKibanaRequest extends boolean = boolean, - ExtraOptions extends object = {} -> = { +export type CollectorOptions = { /** * Unique string identifier for the collector */ @@ -152,17 +111,8 @@ export type CollectorOptions< * The method that will collect and return the data in the final format. * @param collectorFetchContext {@link CollectorFetchContext} */ - fetch: CollectorFetchMethod; -} & ExtraOptions & - (WithKibanaRequest extends true // If enforced to true via Types, the config must be enforced - ? { - /** {@link CollectorOptionsFetchExtendedContext} **/ - extendFetchContext: CollectorOptionsFetchExtendedContext; - } - : { - /** {@link CollectorOptionsFetchExtendedContext} **/ - extendFetchContext?: CollectorOptionsFetchExtendedContext; - }); + fetch: CollectorFetchMethod; +} & ExtraOptions; /** * Common interface for Usage and Stats Collectors @@ -170,13 +120,8 @@ export type CollectorOptions< export interface ICollector { /** Logger **/ readonly log: Logger; - /** - * The options to extend the context provided to the `fetch` method: {@link CollectorOptionsFetchExtendedContext}. - * @remark Only to be used in very rare scenarios when this is really needed. - */ - readonly extendFetchContext: CollectorOptionsFetchExtendedContext; /** The registered type (aka name) of the collector **/ - readonly type: CollectorOptions['type']; + readonly type: CollectorOptions['type']; /** * The actual logic that reports the Usage collection. * It will be called on every collection request. @@ -188,9 +133,9 @@ export interface ICollector { * [type]: await fetch(context) * } */ - readonly fetch: CollectorFetchMethod; + readonly fetch: CollectorFetchMethod; /** * Should return `true` when it's safe to call the `fetch` method. */ - readonly isReady: CollectorOptions['isReady']; + readonly isReady: CollectorOptions['isReady']; } diff --git a/src/plugins/usage_collection/server/collector/usage_collector.ts b/src/plugins/usage_collection/server/collector/usage_collector.ts index 15f7cd9c627fcb..2ed8c2a50dbafd 100644 --- a/src/plugins/usage_collection/server/collector/usage_collector.ts +++ b/src/plugins/usage_collection/server/collector/usage_collector.ts @@ -15,10 +15,9 @@ import { Collector } from './collector'; */ export type UsageCollectorOptions< TFetchReturn = unknown, - WithKibanaRequest extends boolean = false, ExtraOptions extends object = {} -> = CollectorOptions & - Required, 'schema'>>; +> = CollectorOptions & + Required, 'schema'>>; /** * @private Only used in fixtures as a type @@ -27,12 +26,7 @@ export class UsageCollector exte TFetchReturn, ExtraOptions > { - constructor( - log: Logger, - // Needed because it doesn't affect on anything here but being explicit creates a lot of pain down the line - // eslint-disable-next-line @typescript-eslint/no-explicit-any - collectorOptions: UsageCollectorOptions - ) { + constructor(log: Logger, collectorOptions: UsageCollectorOptions) { super(log, collectorOptions); } } diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts index 74fa77be9843cb..907a61a752052a 100644 --- a/src/plugins/usage_collection/server/index.ts +++ b/src/plugins/usage_collection/server/index.ts @@ -17,7 +17,6 @@ export type { UsageCollectorOptions, CollectorFetchContext, CollectorFetchMethod, - CollectorOptionsFetchExtendedContext, } from './collector'; export type { diff --git a/src/plugins/usage_collection/server/mocks.ts b/src/plugins/usage_collection/server/mocks.ts index 6f7d4f19cbaf12..ac7ad69ed4bce7 100644 --- a/src/plugins/usage_collection/server/mocks.ts +++ b/src/plugins/usage_collection/server/mocks.ts @@ -9,7 +9,6 @@ import { elasticsearchServiceMock, executionContextServiceMock, - httpServerMock, loggingSystemMock, savedObjectsClientMock, } from '../../../../src/core/server/mocks'; @@ -45,25 +44,14 @@ export const createUsageCollectionSetupMock = () => { return usageCollectionSetupMock; }; -export function createCollectorFetchContextMock(): jest.Mocked> { - const collectorFetchClientsMock: jest.Mocked> = { +export function createCollectorFetchContextMock(): jest.Mocked { + const collectorFetchClientsMock: jest.Mocked = { esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, soClient: savedObjectsClientMock.create(), }; return collectorFetchClientsMock; } -export function createCollectorFetchContextWithKibanaMock(): jest.Mocked< - CollectorFetchContext -> { - const collectorFetchClientsMock: jest.Mocked> = { - esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, - soClient: savedObjectsClientMock.create(), - kibanaRequest: httpServerMock.createKibanaRequest(), - }; - return collectorFetchClientsMock; -} - export const usageCollectionPluginMock = { createSetupContract: createUsageCollectionSetupMock, }; diff --git a/src/plugins/usage_collection/server/plugin.ts b/src/plugins/usage_collection/server/plugin.ts index f415dd768dc226..7cde8bad706dd1 100644 --- a/src/plugins/usage_collection/server/plugin.ts +++ b/src/plugins/usage_collection/server/plugin.ts @@ -15,7 +15,6 @@ import type { Plugin, ElasticsearchClient, SavedObjectsClientContract, - KibanaRequest, } from 'src/core/server'; import type { ConfigType } from './config'; import { CollectorSet } from './collector'; @@ -39,12 +38,8 @@ export interface UsageCollectionSetup { * Creates a usage collector to collect plugin telemetry data. * registerCollector must be called to connect the created collector with the service. */ - makeUsageCollector: < - TFetchReturn, - WithKibanaRequest extends boolean = false, - ExtraOptions extends object = {} - >( - options: UsageCollectorOptions + makeUsageCollector: ( + options: UsageCollectorOptions ) => Collector; /** * Register a usage collector or a stats collector. @@ -66,7 +61,6 @@ export interface UsageCollectionSetup { bulkFetch: ( esClient: ElasticsearchClient, soClient: SavedObjectsClientContract, - kibanaRequest: KibanaRequest | undefined, // intentionally `| undefined` to enforce providing the parameter collectors?: Map> ) => Promise>; /** @@ -88,12 +82,8 @@ export interface UsageCollectionSetup { * registerCollector must be called to connect the created collector with the service. * @internal: telemetry and monitoring use */ - makeStatsCollector: < - TFetchReturn, - WithKibanaRequest extends boolean, - ExtraOptions extends object = {} - >( - options: CollectorOptions + makeStatsCollector: ( + options: CollectorOptions ) => Collector; } diff --git a/src/plugins/usage_collection/server/routes/stats/stats.ts b/src/plugins/usage_collection/server/routes/stats/stats.ts index 8e5382d1631721..72cbd2e5899ff5 100644 --- a/src/plugins/usage_collection/server/routes/stats/stats.ts +++ b/src/plugins/usage_collection/server/routes/stats/stats.ts @@ -15,7 +15,6 @@ import { first } from 'rxjs/operators'; import { ElasticsearchClient, IRouter, - KibanaRequest, MetricsServiceSetup, SavedObjectsClientContract, ServiceStatus, @@ -55,10 +54,9 @@ export function registerStatsRoute({ }) { const getUsage = async ( esClient: ElasticsearchClient, - savedObjectsClient: SavedObjectsClientContract, - kibanaRequest: KibanaRequest + savedObjectsClient: SavedObjectsClientContract ): Promise => { - const usage = await collectorSet.bulkFetchUsage(esClient, savedObjectsClient, kibanaRequest); + const usage = await collectorSet.bulkFetchUsage(esClient, savedObjectsClient); return collectorSet.toObject(usage); }; @@ -97,7 +95,7 @@ export function registerStatsRoute({ const [usage, clusterUuid] = await Promise.all([ shouldGetUsage - ? getUsage(asCurrentUser, savedObjectsClient, req) + ? getUsage(asCurrentUser, savedObjectsClient) : Promise.resolve({}), getClusterUuid(asCurrentUser), ]); diff --git a/src/plugins/vis_default_editor/public/components/agg_params_map.ts b/src/plugins/vis_default_editor/public/components/agg_params_map.ts index a61df61f2316c6..283e1d7511b750 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_map.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_map.ts @@ -67,6 +67,11 @@ const metrics = { sortField: controls.TopSortFieldParamEditor, sortOrder: controls.OrderParamEditor, }, + [METRIC_TYPES.TOP_METRICS]: { + field: controls.FieldParamEditor, + sortField: controls.TopSortFieldParamEditor, + sortOrder: controls.OrderParamEditor, + }, [METRIC_TYPES.PERCENTILES]: { percents: controls.PercentilesEditor, }, diff --git a/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx b/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx index 1f844b50424746..2888d399bc0148 100644 --- a/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx @@ -13,7 +13,14 @@ import { i18n } from '@kbn/i18n'; import { useAvailableOptions, useFallbackMetric, useValidation } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; -const aggFilter = ['!top_hits', '!percentiles', '!percentile_ranks', '!median', '!std_dev']; +const aggFilter = [ + '!top_hits', + '!top_metrics', + '!percentiles', + '!percentile_ranks', + '!median', + '!std_dev', +]; const EMPTY_VALUE = 'EMPTY_VALUE'; const DEFAULT_OPTIONS = [{ text: '', value: EMPTY_VALUE, hidden: true }]; diff --git a/src/plugins/vis_types/xy/public/config/get_config.ts b/src/plugins/vis_types/xy/public/config/get_config.ts index d7cf22625e10ed..7aad30c5b743e6 100644 --- a/src/plugins/vis_types/xy/public/config/get_config.ts +++ b/src/plugins/vis_types/xy/public/config/get_config.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ScaleContinuousType } from '@elastic/charts'; +import { Fit, ScaleContinuousType } from '@elastic/charts'; import { Datatable } from '../../../../expressions/public'; import { BUCKET_TYPES } from '../../../../data/public'; @@ -92,7 +92,7 @@ export function getConfig( return { // NOTE: downscale ratio to match current vislib implementation markSizeRatio: radiusRatio * 0.6, - fittingFunction, + fittingFunction: fittingFunction ?? Fit.Linear, fillOpacity, detailedTooltip, orderBucketsBySum, diff --git a/src/plugins/vis_types/xy/public/editor/components/options/point_series/elastic_charts_options.tsx b/src/plugins/vis_types/xy/public/editor/components/options/point_series/elastic_charts_options.tsx index 105cd667990416..1c93fe92b79af7 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/point_series/elastic_charts_options.tsx +++ b/src/plugins/vis_types/xy/public/editor/components/options/point_series/elastic_charts_options.tsx @@ -78,7 +78,7 @@ export function ElasticChartsOptions(props: ValidationVisOptionsProps })} options={fittingFunctions} paramName="fittingFunction" - value={stateParams.fittingFunction} + value={stateParams.fittingFunction ?? fittingFunctions[2].value} setValue={(paramName, value) => { if (trackUiMetric) { trackUiMetric(METRIC_TYPE.CLICK, 'fitting_function_selected'); diff --git a/src/plugins/visualizations/tsconfig.json b/src/plugins/visualizations/tsconfig.json index 57d21d8719ede3..2bc25cfb3c3463 100644 --- a/src/plugins/visualizations/tsconfig.json +++ b/src/plugins/visualizations/tsconfig.json @@ -30,6 +30,7 @@ { "path": "../home/tsconfig.json" }, { "path": "../share/tsconfig.json" }, { "path": "../presentation_util/tsconfig.json" }, + { "path": "../screenshot_mode/tsconfig.json" }, { "path": "../../../x-pack/plugins/spaces/tsconfig.json" } ] } diff --git a/test/common/services/es_archiver.ts b/test/common/services/es_archiver.ts index 2ea4b6ce3a4342..3212e809286860 100644 --- a/test/common/services/es_archiver.ts +++ b/test/common/services/es_archiver.ts @@ -8,11 +8,12 @@ import { EsArchiver } from '@kbn/es-archiver'; import { FtrProviderContext } from '../ftr_provider_context'; -import * as KibanaServer from './kibana_server'; +import * as KibanaServer from '../../common/services/kibana_server'; export function EsArchiverProvider({ getService }: FtrProviderContext): EsArchiver { const config = getService('config'); const client = getService('es'); + const log = getService('log'); const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); diff --git a/test/common/services/security/system_indices_user.ts b/test/common/services/security/system_indices_user.ts index 2546fbeafffa71..091621207a6710 100644 --- a/test/common/services/security/system_indices_user.ts +++ b/test/common/services/security/system_indices_user.ts @@ -6,25 +6,18 @@ * Side Public License, v 1. */ -import { systemIndicesSuperuser, createEsClientForFtrConfig } from '@kbn/test'; +import { Client } from '@elastic/elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; +import { + systemIndicesSuperuser, + createEsClientForFtrConfig, + createRemoteEsClientForFtrConfig, +} from '@kbn/test'; import { FtrProviderContext } from '../../ftr_provider_context'; const SYSTEM_INDICES_SUPERUSER_ROLE = 'system_indices_superuser'; -export async function createSystemIndicesUser(ctx: FtrProviderContext) { - const log = ctx.getService('log'); - const config = ctx.getService('config'); - - const enabled = !config - .get('esTestCluster.serverArgs') - .some((arg: string) => arg === 'xpack.security.enabled=false'); - - if (!enabled) { - return; - } - - const es = createEsClientForFtrConfig(config); - +async function ensureSystemIndicesUser(es: Client, log: ToolingLog) { // There are cases where the test config file doesn't have security disabled // but tests are still executed on ES without security. Checking this case // by trying to fetch the users list. @@ -67,3 +60,24 @@ export async function createSystemIndicesUser(ctx: FtrProviderContext) { await es.close(); } + +export async function createSystemIndicesUser(ctx: FtrProviderContext) { + const log = ctx.getService('log'); + const config = ctx.getService('config'); + + const enabled = !config + .get('esTestCluster.serverArgs') + .some((arg: string) => arg === 'xpack.security.enabled=false'); + + if (!enabled) { + return; + } + + const localEs = createEsClientForFtrConfig(config); + await ensureSystemIndicesUser(localEs, log); + + if (config.get('esTestCluster.ccs')) { + const remoteEs = createRemoteEsClientForFtrConfig(config); + await ensureSystemIndicesUser(remoteEs, log); + } +} diff --git a/test/common/services/security/test_user.ts b/test/common/services/security/test_user.ts index 7c4751220fa1f1..bc5dbf68698bc0 100644 --- a/test/common/services/security/test_user.ts +++ b/test/common/services/security/test_user.ts @@ -90,6 +90,28 @@ export async function createTestUserService(ctx: FtrProviderContext, role: Role, await role.create(name, definition); } + // when configured to setup remote roles, load the remote es service and set them up directly via es + const remoteEsRoles: undefined | Record = config.get('security.remoteEsRoles'); + if (remoteEsRoles) { + let remoteEs; + try { + remoteEs = ctx.getService('remoteEs' as 'es'); + } catch (error) { + throw new Error( + 'unable to load `remoteEs` cluster, which should provide an ES client configured to talk to the remote cluster. Include that service from another FTR config or fix the error it is throwing on creation: ' + + error.message + ); + } + + for (const [name, body] of Object.entries(remoteEsRoles)) { + log.info(`creating ${name} role on remote cluster`); + await remoteEs.security.putRole({ + name, + ...body, + }); + } + } + // delete the test_user if present (will it error if the user doesn't exist?) try { await user.delete(TEST_USER_NAME); diff --git a/test/functional/apps/management/_create_index_pattern_wizard.js b/test/functional/apps/management/_create_index_pattern_wizard.ts similarity index 93% rename from test/functional/apps/management/_create_index_pattern_wizard.js rename to test/functional/apps/management/_create_index_pattern_wizard.ts index b2f24e530cb120..cf732e178aa74c 100644 --- a/test/functional/apps/management/_create_index_pattern_wizard.js +++ b/test/functional/apps/management/_create_index_pattern_wizard.ts @@ -6,7 +6,9 @@ * Side Public License, v 1. */ -export default function ({ getService, getPageObjects }) { +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const es = getService('es'); @@ -38,7 +40,7 @@ export default function ({ getService, getPageObjects }) { body: { actions: [{ add: { index: 'blogs', alias: 'alias1' } }] }, }); - await PageObjects.settings.createIndexPattern('alias1', false); + await PageObjects.settings.createIndexPattern('alias1', null); }); it('can delete an index pattern', async () => { diff --git a/test/functional/apps/management/_exclude_index_pattern.js b/test/functional/apps/management/_exclude_index_pattern.ts similarity index 89% rename from test/functional/apps/management/_exclude_index_pattern.js rename to test/functional/apps/management/_exclude_index_pattern.ts index b71222c1ec44d6..8c20acdc21f926 100644 --- a/test/functional/apps/management/_exclude_index_pattern.js +++ b/test/functional/apps/management/_exclude_index_pattern.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['settings']); const es = getService('es'); diff --git a/test/functional/apps/management/_handle_alias.js b/test/functional/apps/management/_handle_alias.ts similarity index 95% rename from test/functional/apps/management/_handle_alias.js rename to test/functional/apps/management/_handle_alias.ts index 891e59d84a04bc..04496bf9ed7583 100644 --- a/test/functional/apps/management/_handle_alias.js +++ b/test/functional/apps/management/_handle_alias.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const es = getService('es'); const retry = getService('retry'); diff --git a/test/functional/apps/management/_handle_version_conflict.js b/test/functional/apps/management/_handle_version_conflict.ts similarity index 96% rename from test/functional/apps/management/_handle_version_conflict.js rename to test/functional/apps/management/_handle_version_conflict.ts index a04c5d34b2d351..2f65f966c55967 100644 --- a/test/functional/apps/management/_handle_version_conflict.js +++ b/test/functional/apps/management/_handle_version_conflict.ts @@ -16,8 +16,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); @@ -93,7 +94,6 @@ export default function ({ getService, getPageObjects }) { expect(response.body.result).to.be('updated'); await PageObjects.settings.controlChangeSave(); await retry.try(async function () { - //await PageObjects.common.sleep(2000); const message = await PageObjects.common.closeToast(); expect(message).to.contain('Unable'); }); diff --git a/test/functional/apps/management/_index_pattern_create_delete.js b/test/functional/apps/management/_index_pattern_create_delete.ts similarity index 91% rename from test/functional/apps/management/_index_pattern_create_delete.js rename to test/functional/apps/management/_index_pattern_create_delete.ts index 4c9f5a5210ac68..6b2036499a1edd 100644 --- a/test/functional/apps/management/_index_pattern_create_delete.js +++ b/test/functional/apps/management/_index_pattern_create_delete.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); @@ -35,8 +36,7 @@ export default function ({ getService, getPageObjects }) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/124663 - describe.skip('validation', function () { + describe('validation', function () { it('can display errors', async function () { await PageObjects.settings.clickAddNewIndexPatternButton(); await PageObjects.settings.setIndexPatternField('log-fake*'); @@ -46,7 +46,7 @@ export default function ({ getService, getPageObjects }) { it('can resolve errors and submit', async function () { await PageObjects.settings.setIndexPatternField('log*'); - await (await PageObjects.settings.getSaveIndexPatternButton()).click(); + await (await PageObjects.settings.getSaveDataViewButtonActive()).click(); await PageObjects.settings.removeIndexPattern(); }); }); @@ -72,10 +72,12 @@ export default function ({ getService, getPageObjects }) { }); describe('index pattern creation', function indexPatternCreation() { - let indexPatternId; + let indexPatternId: string; before(function () { - return PageObjects.settings.createIndexPattern().then((id) => (indexPatternId = id)); + return PageObjects.settings + .createIndexPattern('logstash-*') + .then((id) => (indexPatternId = id)); }); it('should have index pattern in page header', async function () { diff --git a/test/functional/apps/management/_index_pattern_filter.js b/test/functional/apps/management/_index_pattern_filter.ts similarity index 90% rename from test/functional/apps/management/_index_pattern_filter.js rename to test/functional/apps/management/_index_pattern_filter.ts index 3e9d316b59c618..afa64c474d39d4 100644 --- a/test/functional/apps/management/_index_pattern_filter.js +++ b/test/functional/apps/management/_index_pattern_filter.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); const PageObjects = getPageObjects(['settings']); @@ -23,7 +24,7 @@ export default function ({ getService, getPageObjects }) { }); beforeEach(async function () { - await PageObjects.settings.createIndexPattern(); + await PageObjects.settings.createIndexPattern('logstash-*'); }); afterEach(async function () { diff --git a/test/functional/apps/management/_index_pattern_popularity.js b/test/functional/apps/management/_index_pattern_popularity.ts similarity index 92% rename from test/functional/apps/management/_index_pattern_popularity.js rename to test/functional/apps/management/_index_pattern_popularity.ts index 1a71e4c5fbc68b..bff6cdce0f7a69 100644 --- a/test/functional/apps/management/_index_pattern_popularity.js +++ b/test/functional/apps/management/_index_pattern_popularity.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const log = getService('log'); @@ -23,7 +24,7 @@ export default function ({ getService, getPageObjects }) { }); beforeEach(async () => { - await PageObjects.settings.createIndexPattern(); + await PageObjects.settings.createIndexPattern('logstash-*'); // increase Popularity of geo.coordinates log.debug('Starting openControlsByName (' + fieldName + ')'); await PageObjects.settings.openControlsByName(fieldName); diff --git a/test/functional/apps/management/_index_pattern_results_sort.js b/test/functional/apps/management/_index_pattern_results_sort.ts similarity index 90% rename from test/functional/apps/management/_index_pattern_results_sort.js rename to test/functional/apps/management/_index_pattern_results_sort.ts index cedf5ee355b36a..305a72889e95ab 100644 --- a/test/functional/apps/management/_index_pattern_results_sort.js +++ b/test/functional/apps/management/_index_pattern_results_sort.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); const PageObjects = getPageObjects(['settings', 'common']); @@ -18,7 +19,7 @@ export default function ({ getService, getPageObjects }) { // delete .kibana index and then wait for Kibana to re-create it await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); - await PageObjects.settings.createIndexPattern(); + await PageObjects.settings.createIndexPattern('logstash-*'); }); after(async function () { @@ -30,7 +31,7 @@ export default function ({ getService, getPageObjects }) { heading: 'Name', first: '@message', last: 'xss.raw', - selector: async function () { + async selector() { const tableRow = await PageObjects.settings.getTableRow(0, 0); return await tableRow.getVisibleText(); }, @@ -39,7 +40,7 @@ export default function ({ getService, getPageObjects }) { heading: 'Type', first: '', last: 'text', - selector: async function () { + async selector() { const tableRow = await PageObjects.settings.getTableRow(0, 1); return await tableRow.getVisibleText(); }, @@ -49,7 +50,6 @@ export default function ({ getService, getPageObjects }) { columns.forEach(function (col) { describe('sort by heading - ' + col.heading, function indexPatternCreation() { it('should sort ascending', async function () { - console.log('col.heading', col.heading); if (col.heading !== 'Name') { await PageObjects.settings.sortBy(col.heading); } diff --git a/test/functional/apps/management/_kibana_settings.js b/test/functional/apps/management/_kibana_settings.ts similarity index 96% rename from test/functional/apps/management/_kibana_settings.js rename to test/functional/apps/management/_kibana_settings.ts index cfe4e88cda21de..d459643849fbc0 100644 --- a/test/functional/apps/management/_kibana_settings.js +++ b/test/functional/apps/management/_kibana_settings.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); const PageObjects = getPageObjects(['settings', 'common', 'dashboard', 'timePicker', 'header']); diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.ts similarity index 80% rename from test/functional/apps/management/_mgmt_import_saved_objects.js rename to test/functional/apps/management/_mgmt_import_saved_objects.ts index 95b0bbb7ed03b3..04a1bb59383223 100644 --- a/test/functional/apps/management/_mgmt_import_saved_objects.js +++ b/test/functional/apps/management/_mgmt_import_saved_objects.ts @@ -8,13 +8,14 @@ import expect from '@kbn/expect'; import path from 'path'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']); - //in 6.4.0 bug the Saved Search conflict would be resolved and get imported but the visualization - //that referenced the saved search was not imported.( https://github.com/elastic/kibana/issues/22238) + // in 6.4.0 bug the Saved Search conflict would be resolved and get imported but the visualization + // that referenced the saved search was not imported.( https://github.com/elastic/kibana/issues/22238) describe('mgmt saved objects', function describeIndexTests() { before(async () => { @@ -41,7 +42,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.savedObjects.waitTableIsLoaded(); await PageObjects.savedObjects.searchForObject('mysaved'); - //instead of asserting on count- am asserting on the titles- which is more accurate than count. + // instead of asserting on count- am asserting on the titles- which is more accurate than count. const objects = await PageObjects.savedObjects.getRowTitles(); expect(objects.includes('mysavedsearch')).to.be(true); expect(objects.includes('mysavedviz')).to.be(true); diff --git a/test/functional/apps/management/_runtime_fields.js b/test/functional/apps/management/_runtime_fields.ts similarity index 91% rename from test/functional/apps/management/_runtime_fields.js rename to test/functional/apps/management/_runtime_fields.ts index 3a70df81b55d96..8ec9fb92c58eae 100644 --- a/test/functional/apps/management/_runtime_fields.js +++ b/test/functional/apps/management/_runtime_fields.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const log = getService('log'); const browser = getService('browser'); @@ -36,7 +37,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getFieldsTabCount(), 10); await log.debug('add runtime field'); await PageObjects.settings.addRuntimeField( fieldName, @@ -51,7 +52,9 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.clickSaveField(); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getFieldsTabCount())).to.be(startingCount + 1); + expect(parseInt(await PageObjects.settings.getFieldsTabCount(), 10)).to.be( + startingCount + 1 + ); }); }); diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.ts similarity index 96% rename from test/functional/apps/management/_scripted_fields.js rename to test/functional/apps/management/_scripted_fields.ts index 72f45e1fedb4db..c8c605ec7ed19e 100644 --- a/test/functional/apps/management/_scripted_fields.js +++ b/test/functional/apps/management/_scripted_fields.ts @@ -23,8 +23,9 @@ // it will automatically insert a a closing square brace ], etc. import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const log = getService('log'); const browser = getService('browser'); @@ -77,7 +78,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); await PageObjects.settings.clickScriptedFieldsTab(); await log.debug('add scripted field'); const script = `1`; @@ -90,7 +91,7 @@ export default function ({ getService, getPageObjects }) { script ); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be( + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( startingCount + 1 ); }); @@ -111,7 +112,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); await PageObjects.settings.clickScriptedFieldsTab(); await log.debug('add scripted field'); const script = `if (doc['machine.ram'].size() == 0) return -1; @@ -126,7 +127,7 @@ export default function ({ getService, getPageObjects }) { script ); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be( + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( startingCount + 1 ); }); @@ -150,7 +151,7 @@ export default function ({ getService, getPageObjects }) { }); }); - //add a test to sort numeric scripted field + // add a test to sort numeric scripted field it('should sort scripted field value in Discover', async function () { await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName}`); // after the first click on the scripted field, it becomes secondary sort after time. @@ -201,7 +202,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); await PageObjects.settings.clickScriptedFieldsTab(); await log.debug('add scripted field'); await PageObjects.settings.addScriptedField( @@ -213,7 +214,7 @@ export default function ({ getService, getPageObjects }) { "if (doc['response.raw'].value == '200') { return 'good'} else { return 'bad'}" ); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be( + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( startingCount + 1 ); }); @@ -237,7 +238,7 @@ export default function ({ getService, getPageObjects }) { }); }); - //add a test to sort string scripted field + // add a test to sort string scripted field it('should sort scripted field value in Discover', async function () { await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); // after the first click on the scripted field, it becomes secondary sort after time. @@ -287,7 +288,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); await PageObjects.settings.clickScriptedFieldsTab(); await log.debug('add scripted field'); await PageObjects.settings.addScriptedField( @@ -299,7 +300,7 @@ export default function ({ getService, getPageObjects }) { "doc['response.raw'].value == '200'" ); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be( + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( startingCount + 1 ); }); @@ -335,8 +336,8 @@ export default function ({ getService, getPageObjects }) { await filterBar.removeAllFilters(); }); - //add a test to sort boolean - //existing bug: https://github.com/elastic/kibana/issues/75519 hence the issue is skipped. + // add a test to sort boolean + // existing bug: https://github.com/elastic/kibana/issues/75519 hence the issue is skipped. it.skip('should sort scripted field value in Discover', async function () { await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); // after the first click on the scripted field, it becomes secondary sort after time. @@ -374,7 +375,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); await PageObjects.settings.clickScriptedFieldsTab(); await log.debug('add scripted field'); await PageObjects.settings.addScriptedField( @@ -386,7 +387,7 @@ export default function ({ getService, getPageObjects }) { "doc['utc_time'].value.toEpochMilli() + (1000) * 60 * 60" ); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be( + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( startingCount + 1 ); }); @@ -410,8 +411,8 @@ export default function ({ getService, getPageObjects }) { }); }); - //add a test to sort date scripted field - //https://github.com/elastic/kibana/issues/75711 + // add a test to sort date scripted field + // https://github.com/elastic/kibana/issues/75711 it.skip('should sort scripted field value in Discover', async function () { await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); // after the first click on the scripted field, it becomes secondary sort after time. diff --git a/test/functional/apps/management/_scripted_fields_filter.js b/test/functional/apps/management/_scripted_fields_filter.ts similarity index 92% rename from test/functional/apps/management/_scripted_fields_filter.js rename to test/functional/apps/management/_scripted_fields_filter.ts index abae9a300994dc..4f6d1a41d05237 100644 --- a/test/functional/apps/management/_scripted_fields_filter.js +++ b/test/functional/apps/management/_scripted_fields_filter.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); const log = getService('log'); @@ -16,7 +17,8 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['settings']); - describe('filter scripted fields', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/126027 + describe.skip('filter scripted fields', function describeIndexTests() { before(async function () { // delete .kibana index and then wait for Kibana to re-create it await browser.setWindowSize(1200, 800); diff --git a/test/functional/apps/management/_scripted_fields_preview.js b/test/functional/apps/management/_scripted_fields_preview.ts similarity index 90% rename from test/functional/apps/management/_scripted_fields_preview.js rename to test/functional/apps/management/_scripted_fields_preview.ts index b6c941fe21d0ac..380b4659c0f38f 100644 --- a/test/functional/apps/management/_scripted_fields_preview.js +++ b/test/functional/apps/management/_scripted_fields_preview.ts @@ -7,13 +7,14 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const PageObjects = getPageObjects(['settings']); const SCRIPTED_FIELD_NAME = 'myScriptedField'; - const scriptResultToJson = (scriptResult) => { + const scriptResultToJson = (scriptResult: string) => { try { return JSON.parse(scriptResult); } catch (e) { @@ -26,7 +27,7 @@ export default function ({ getService, getPageObjects }) { await browser.setWindowSize(1200, 800); await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); - await PageObjects.settings.createIndexPattern(); + await PageObjects.settings.createIndexPattern('logstash-*'); await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); @@ -67,7 +68,7 @@ export default function ({ getService, getPageObjects }) { it('should display additional fields', async function () { const scriptResults = await PageObjects.settings.executeScriptedField( `doc['bytes'].value * 2`, - ['bytes'] + 'bytes' ); const [{ _id, bytes }] = scriptResultToJson(scriptResults); expect(_id).to.be.a('string'); diff --git a/test/functional/apps/management/_test_huge_fields.js b/test/functional/apps/management/_test_huge_fields.ts similarity index 90% rename from test/functional/apps/management/_test_huge_fields.js rename to test/functional/apps/management/_test_huge_fields.ts index 7b756839409286..abc338cb8abc82 100644 --- a/test/functional/apps/management/_test_huge_fields.js +++ b/test/functional/apps/management/_test_huge_fields.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings']); @@ -19,7 +20,7 @@ export default function ({ getService, getPageObjects }) { const EXPECTED_FIELD_COUNT = '10006'; before(async function () { - await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader'], false); + await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader']); await esArchiver.emptyKibanaIndex(); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/large_fields'); await PageObjects.settings.navigateTo(); diff --git a/test/functional/fixtures/kbn_archiver/date_nested_ccs.json b/test/functional/fixtures/kbn_archiver/date_nested_ccs.json index 933b21d920c00b..9a411ba96705a9 100644 --- a/test/functional/fixtures/kbn_archiver/date_nested_ccs.json +++ b/test/functional/fixtures/kbn_archiver/date_nested_ccs.json @@ -2,10 +2,10 @@ "attributes": { "fields": "[]", "timeFieldName": "nested.timestamp", - "title": "remote:date-nested" + "title": "ftr-remote:date-nested" }, "coreMigrationVersion": "8.2.0", - "id": "remote:date-nested", + "id": "ftr-remote:date-nested", "migrationVersion": { "index-pattern": "8.0.0" }, diff --git a/test/functional/fixtures/kbn_archiver/discover_ccs.json b/test/functional/fixtures/kbn_archiver/discover_ccs.json index d53aa1bc759afb..4c1143ed4e798f 100644 --- a/test/functional/fixtures/kbn_archiver/discover_ccs.json +++ b/test/functional/fixtures/kbn_archiver/discover_ccs.json @@ -3,10 +3,10 @@ "fieldAttrs": "{\"referer\":{\"customLabel\":\"Referer custom\"}}", "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"esTypes\":[\"double\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nestedField.child\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"nested\":{\"path\":\"nestedField\"}}},{\"name\":\"phpmemory\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"xss\"}}}]", "timeFieldName": "@timestamp", - "title": "remote:logstash-*" + "title": "ftr-remote:logstash-*" }, "coreMigrationVersion": "8.0.0", - "id": "remote:logstash-*", + "id": "ftr-remote:logstash-*", "migrationVersion": { "index-pattern": "7.11.0" }, @@ -41,7 +41,7 @@ }, "references": [ { - "id": "remote:logstash-*", + "id": "ftr-remote:logstash-*", "name": "kibanaSavedObjectMeta.searchSourceJSON.index", "type": "index-pattern" } diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 70cdbea7fa8970..98fdff82e13c55 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -164,6 +164,19 @@ export class SettingsPageObject extends FtrService { return await this.testSubjects.find('saveIndexPatternButton'); } + async getSaveDataViewButtonActive() { + await this.retry.try(async () => { + expect( + ( + await this.find.allByCssSelector( + '[data-test-subj="saveIndexPatternButton"]:not(.euiButton-isDisabled)' + ) + ).length + ).to.be(1); + }); + return await this.testSubjects.find('saveIndexPatternButton'); + } + async getCreateButton() { return await this.find.displayedByCssSelector('[type="submit"]'); } @@ -550,7 +563,7 @@ export class SettingsPageObject extends FtrService { name: string, language: string, type: string, - format: Record, + format: Record | null, popularity: string, script: string ) { @@ -790,7 +803,7 @@ export class SettingsPageObject extends FtrService { await this.flyout.ensureClosed('scriptedFieldsHelpFlyout'); } - async executeScriptedField(script: string, additionalField: string) { + async executeScriptedField(script: string, additionalField?: string) { this.log.debug('execute Scripted Fields help'); await this.closeScriptedFieldHelp(); // ensure script help is closed so script input is not blocked await this.setScriptedFieldScript(script); @@ -801,7 +814,7 @@ export class SettingsPageObject extends FtrService { await this.testSubjects.click('runScriptButton'); await this.testSubjects.waitForDeleted('.euiLoadingSpinner'); } - let scriptResults; + let scriptResults: string = ''; await this.retry.try(async () => { scriptResults = await this.testSubjects.getVisibleText('scriptedFieldPreview'); }); diff --git a/test/functional_ccs/apps/discover/_data_view_ccs.ts b/test/functional_ccs/apps/discover/data_view_ccs.ts similarity index 80% rename from test/functional_ccs/apps/discover/_data_view_ccs.ts rename to test/functional_ccs/apps/discover/data_view_ccs.ts index 91d9cb2faf6816..44258b9cbadd63 100644 --- a/test/functional_ccs/apps/discover/_data_view_ccs.ts +++ b/test/functional_ccs/apps/discover/data_view_ccs.ts @@ -6,13 +6,13 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from './ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const testSubjects = getService('testSubjects'); const kibanaServer = getService('kibanaServer'); - const esArchiver = getService('esArchiver'); + const remoteEsArchiver = getService('remoteEsArchiver'); const security = getService('security'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); @@ -27,10 +27,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('saveIndexPatternButton'); }; - describe('discover integration with data view editor', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/126658 + describe.skip('discover integration with data view editor', function describeIndexTests() { before(async function () { - await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await security.testUser.setRoles([ + 'kibana_admin', + 'test_logstash_reader', + 'ccs_remote_search', + ]); + await remoteEsArchiver.loadIfNeeded( + 'test/functional/fixtures/es_archiver/logstash_functional' + ); await kibanaServer.savedObjects.clean({ types: ['saved-search', 'index-pattern'] }); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); @@ -44,7 +51,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('use ccs to create a new data view', async function () { - const dataViewToCreate = 'remote:logstash'; + const dataViewToCreate = 'ftr-remote:logstash'; await createDataView(dataViewToCreate); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.waitForWithTimeout( diff --git a/test/functional_ccs/apps/discover/index.ts b/test/functional_ccs/apps/discover/index.ts index 629423b1b75aa4..2e9d428f44c601 100644 --- a/test/functional_ccs/apps/discover/index.ts +++ b/test/functional_ccs/apps/discover/index.ts @@ -6,38 +6,24 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from './ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); - const esClient = getService('es'); describe('discover app css', function () { this.tags('ciGroup6'); - before(async function () { - await esClient.cluster.putSettings({ - persistent: { - cluster: { - remote: { - remote: { - skip_unavailable: 'true', - seeds: ['localhost:9300'], - }, - }, - }, - }, - }); - return browser.setWindowSize(1300, 800); + before(async () => { + await browser.setWindowSize(1300, 800); }); - after(function unloadMakelogs() { - // Make sure to clean up the cluster setting from the before above. - return esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); - }); + loadTestFile(require.resolve('./data_view_ccs')); + loadTestFile(require.resolve('./saved_queries_ccs')); - loadTestFile(require.resolve('./_data_view_ccs')); - loadTestFile(require.resolve('./_saved_queries_ccs')); + after(async () => { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + }); }); } diff --git a/test/functional_ccs/apps/discover/_saved_queries_ccs.ts b/test/functional_ccs/apps/discover/saved_queries_ccs.ts similarity index 93% rename from test/functional_ccs/apps/discover/_saved_queries_ccs.ts rename to test/functional_ccs/apps/discover/saved_queries_ccs.ts index 325f279ff28ab7..08b6d61368f5d4 100644 --- a/test/functional_ccs/apps/discover/_saved_queries_ccs.ts +++ b/test/functional_ccs/apps/discover/saved_queries_ccs.ts @@ -8,12 +8,12 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from './ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const log = getService('log'); - const esArchiver = getService('esArchiver'); + const remoteEsArchiver = getService('remoteEsArchiver'); const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); const browser = getService('browser'); @@ -47,8 +47,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.importExport.load( 'test/functional/fixtures/kbn_archiver/date_nested_ccs.json' ); - await esArchiver.load('test/functional/fixtures/es_archiver/date_nested'); - await esArchiver.load('test/functional/fixtures/es_archiver/logstash_functional'); + await remoteEsArchiver.load('test/functional/fixtures/es_archiver/date_nested'); + await remoteEsArchiver.load('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.uiSettings.replace(defaultSettings); log.debug('discover'); @@ -61,8 +61,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.importExport.unload( 'test/functional/fixtures/kbn_archiver/date_nested_ccs' ); - await esArchiver.unload('test/functional/fixtures/es_archiver/date_nested'); - await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await remoteEsArchiver.unload('test/functional/fixtures/es_archiver/date_nested'); + await remoteEsArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); await PageObjects.common.unsetTime(); }); @@ -87,12 +87,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false); expect(await queryBar.getQueryString()).to.eql(''); - await PageObjects.discover.selectIndexPattern('remote:date-nested'); + await PageObjects.discover.selectIndexPattern('ftr-remote:date-nested'); expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false); expect(await queryBar.getQueryString()).to.eql(''); - await PageObjects.discover.selectIndexPattern('remote:logstash-*'); + await PageObjects.discover.selectIndexPattern('ftr-remote:logstash-*'); expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false); expect(await queryBar.getQueryString()).to.eql(''); diff --git a/test/functional_ccs/config.js b/test/functional_ccs/config.js deleted file mode 100644 index 4cd88757983720..00000000000000 --- a/test/functional_ccs/config.js +++ /dev/null @@ -1,25 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { services } from '../functional/services'; - -export default async function ({ readConfigFile }) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); - - return { - ...functionalConfig.getAll(), - - testFiles: [require.resolve('./apps/discover')], - - services, - - junit: { - reportName: 'Kibana CCS Tests', - }, - }; -} diff --git a/test/functional_ccs/config.ts b/test/functional_ccs/config.ts new file mode 100644 index 00000000000000..e99a5310453d93 --- /dev/null +++ b/test/functional_ccs/config.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; +import { services } from './services'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + + return { + ...functionalConfig.getAll(), + + testFiles: [require.resolve('./apps/discover')], + + services, + + junit: { + reportName: 'Kibana CCS Tests', + }, + + security: { + ...functionalConfig.get('security'), + remoteEsRoles: { + ccs_remote_search: { + indices: [ + { + names: ['*'], + privileges: ['read', 'view_index_metadata', 'read_cross_cluster'], + }, + ], + }, + }, + defaultRoles: [...(functionalConfig.get('security.defaultRoles') ?? []), 'ccs_remote_search'], + }, + + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + ccs: { + remoteClusterUrl: + process.env.REMOTE_CLUSTER_URL ?? + `http://elastic:changeme@localhost:${ + functionalConfig.get('servers.elasticsearch.port') + 1 + }`, + }, + }, + }; +} diff --git a/test/functional_ccs/apps/discover/ftr_provider_context.d.ts b/test/functional_ccs/ftr_provider_context.ts similarity index 80% rename from test/functional_ccs/apps/discover/ftr_provider_context.d.ts rename to test/functional_ccs/ftr_provider_context.ts index ea232d23463e65..8fa82b46ac4063 100644 --- a/test/functional_ccs/apps/discover/ftr_provider_context.d.ts +++ b/test/functional_ccs/ftr_provider_context.ts @@ -7,7 +7,7 @@ */ import { GenericFtrProviderContext } from '@kbn/test'; -import { services } from '../../../functional/services'; -import { pageObjects } from '../../../functional/page_objects'; +import { services } from './services'; +import { pageObjects } from '../functional/page_objects'; export type FtrProviderContext = GenericFtrProviderContext; diff --git a/test/functional_ccs/services/index.ts b/test/functional_ccs/services/index.ts new file mode 100644 index 00000000000000..dcdffa077fe083 --- /dev/null +++ b/test/functional_ccs/services/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { services as functionalServices } from '../../functional/services'; +import { RemoteEsProvider } from './remote_es'; +import { RemoteEsArchiverProvider } from './remote_es_archiver'; + +export const services = { + ...functionalServices, + remoteEs: RemoteEsProvider, + remoteEsArchiver: RemoteEsArchiverProvider, +}; diff --git a/test/functional_ccs/services/remote_es.ts b/test/functional_ccs/services/remote_es.ts new file mode 100644 index 00000000000000..05a10d9e068f03 --- /dev/null +++ b/test/functional_ccs/services/remote_es.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Client } from '@elastic/elasticsearch'; + +import { systemIndicesSuperuser, createRemoteEsClientForFtrConfig } from '@kbn/test'; +import { FtrProviderContext } from '../ftr_provider_context'; + +/** + * Kibana-specific @elastic/elasticsearch client instance. + */ +export function RemoteEsProvider({ getService }: FtrProviderContext): Client { + const config = getService('config'); + + return createRemoteEsClientForFtrConfig(config, { + // Use system indices user so tests can write to system indices + authOverride: systemIndicesSuperuser, + }); +} diff --git a/test/functional_ccs/services/remote_es_archiver.ts b/test/functional_ccs/services/remote_es_archiver.ts new file mode 100644 index 00000000000000..569792d050a4d5 --- /dev/null +++ b/test/functional_ccs/services/remote_es_archiver.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EsArchiver } from '@kbn/es-archiver'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export function RemoteEsArchiverProvider({ getService }: FtrProviderContext): EsArchiver { + const remoteEs = getService('remoteEs'); + const log = getService('log'); + const kibanaServer = getService('kibanaServer'); + + return new EsArchiver({ + client: remoteEs, + log, + kbnClient: kibanaServer, + }); +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs_topmetrics.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs_topmetrics.ts new file mode 100644 index 00000000000000..4f43709ba4a7e0 --- /dev/null +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs_topmetrics.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { ExpectExpression, expectExpressionProvider } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +export default function ({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; + + describe('esaggs_topmetrics', () => { + before(() => { + expectExpression = expectExpressionProvider({ getService, updateBaselines }); + }); + + const timeRange = { + from: '2015-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + + describe('aggTopMetrics', () => { + it('can execute aggTopMetrics', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggTerms id="1" enabled=true schema="bucket" field="extension.raw"} + aggs={aggTopMetrics id="2" enabled=true schema="metric" field="bytes" sortField="@timestamp" sortOrder="desc" size=3 } + `; + const result = await expectExpression('aggTopMetrics', expression).getResponse(); + + expect(result.rows.map((r: { 'col-0-1': string }) => r['col-0-1'])).to.eql([ + 'jpg', + 'css', + 'png', + 'gif', + 'php', + ]); + + result.rows.forEach((r: { 'col-1-2': number[] }) => { + expect(r['col-1-2'].length).to.be(3); + expect( + r['col-1-2'].forEach((metric) => { + expect(typeof metric).to.be('number'); + }) + ); + }); + }); + + it('can execute aggTopMetrics with different sortOrder and size', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggTerms id="1" enabled=true schema="bucket" field="extension.raw"} + aggs={aggTopMetrics id="2" enabled=true schema="metric" field="bytes" sortField="@timestamp" sortOrder="asc" size=1 } + `; + const result = await expectExpression('aggTopMetrics', expression).getResponse(); + + expect(result.rows.map((r: { 'col-0-1': string }) => r['col-0-1'])).to.eql([ + 'jpg', + 'css', + 'png', + 'gif', + 'php', + ]); + + result.rows.forEach((r: { 'col-1-2': number[] }) => { + expect(typeof r['col-1-2']).to.be('number'); + }); + }); + + it('can use aggTopMetrics as an orderAgg of aggTerms', async () => { + const expressionSortBytesAsc = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggTerms id="1" enabled=true schema="bucket" field="extension.raw" size=1 orderAgg={aggTopMetrics id="order" enabled=true schema="metric" field="bytes" sortField="@timestamp" sortOrder="asc" size=1}} + aggs={aggCount id="2" enabled=true schema="metric"} + `; + + const resultSortBytesAsc = await expectExpression( + 'sortBytesAsc', + expressionSortBytesAsc + ).getResponse(); + + const expressionSortBytesDesc = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggTerms id="1" enabled=true schema="bucket" field="extension.raw" size=1 orderAgg={aggTopMetrics id="order" enabled=true schema="metric" field="bytes" sortField="@timestamp" sortOrder="desc" size=1}} + aggs={aggCount id="2" enabled=true schema="metric"} + `; + + const resultSortBytesDesc = await expectExpression( + 'sortBytesDesc', + expressionSortBytesDesc + ).getResponse(); + + expect(resultSortBytesAsc.rows.length).to.be(1); + expect(resultSortBytesAsc.rows[0]['col-0-1']).to.be('jpg'); + + expect(resultSortBytesDesc.rows.length).to.be(1); + expect(resultSortBytesDesc.rows[0]['col-0-1']).to.be('php'); + }); + }); + }); +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.ts b/test/interpreter_functional/test_suites/run_pipeline/index.ts index 97387fc0a965fc..e24563a5918eb8 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/index.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts @@ -25,6 +25,7 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'Australia/North', defaultIndex: 'logstash-*', + 'bfetch:disableCompression': true, // makes it easier to debug while developing tests }); await browser.setWindowSize(1300, 900); await PageObjects.common.navigateToApp('settings'); @@ -47,5 +48,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid loadTestFile(require.resolve('./esaggs_sampler')); loadTestFile(require.resolve('./esaggs_significanttext')); loadTestFile(require.resolve('./esaggs_rareterms')); + loadTestFile(require.resolve('./esaggs_topmetrics')); }); } diff --git a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts index 15f5a37edb910e..d6101c9c6cec4f 100644 --- a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts +++ b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts @@ -9,7 +9,6 @@ import { Client } from '@elastic/elasticsearch'; import { loggingSystemMock } from 'src/core/server/mocks'; import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { createWrappedScopedClusterClientFactory } from './wrap_scoped_cluster_client'; -import { ElasticsearchClientWithChild } from '../types'; const esQuery = { body: { query: { bool: { filter: { range: { '@timestamp': { gte: 0 } } } } } }, @@ -41,9 +40,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asInternalUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asInternalUser.child.mockReturnValue(childClient as unknown as Client); const asInternalUserWrappedSearchFn = childClient.search; const wrappedSearchClient = createWrappedScopedClusterClientFactory({ @@ -62,9 +59,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asCurrentUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asCurrentUser.child.mockReturnValue(childClient as unknown as Client); const asCurrentUserWrappedSearchFn = childClient.search; const wrappedSearchClient = createWrappedScopedClusterClientFactory({ @@ -83,9 +78,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asInternalUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asInternalUser.child.mockReturnValue(childClient as unknown as Client); const asInternalUserWrappedSearchFn = childClient.search; const wrappedSearchClient = createWrappedScopedClusterClientFactory({ @@ -106,9 +99,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asInternalUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asInternalUser.child.mockReturnValue(childClient as unknown as Client); const asInternalUserWrappedSearchFn = childClient.search; asInternalUserWrappedSearchFn.mockRejectedValueOnce(new Error('something went wrong!')); @@ -127,9 +118,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asInternalUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asInternalUser.child.mockReturnValue(childClient as unknown as Client); const asInternalUserWrappedSearchFn = childClient.search; // @ts-ignore incomplete return type asInternalUserWrappedSearchFn.mockResolvedValue({}); @@ -156,9 +145,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asInternalUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asInternalUser.child.mockReturnValue(childClient as unknown as Client); const asInternalUserWrappedSearchFn = childClient.search; // @ts-ignore incomplete return type asInternalUserWrappedSearchFn.mockResolvedValue({ took: 333 }); diff --git a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts index dfe32a48ce4384..2b71f95cd9f1c6 100644 --- a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts +++ b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts @@ -21,7 +21,7 @@ import type { AggregationsAggregate, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IScopedClusterClient, ElasticsearchClient, Logger } from 'src/core/server'; -import { ElasticsearchClientWithChild, RuleExecutionMetrics } from '../types'; +import { RuleExecutionMetrics } from '../types'; import { Alert as Rule } from '../types'; type RuleInfo = Pick & { spaceId: string }; @@ -87,8 +87,7 @@ function wrapScopedClusterClient(opts: WrapScopedClusterClientOpts): IScopedClus function wrapEsClient(opts: WrapEsClientOpts): ElasticsearchClient { const { esClient, ...rest } = opts; - // Core hides access to .child via TS - const wrappedClient = (esClient as ElasticsearchClientWithChild).child({}); + const wrappedClient = esClient.child({}); // Mutating the functions we want to wrap wrappedClient.search = getWrappedSearchFn({ esClient: wrappedClient, ...rest }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index c05bdc3cf7bd94..ccf9659a8e67d1 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -142,7 +142,7 @@ export class TaskRunner< this.executionId = uuid.v4(); } - async getDecryptedAttributes( + private async getDecryptedAttributes( ruleId: string, spaceId: string ): Promise<{ apiKey: string | null; enabled: boolean }> { @@ -267,7 +267,7 @@ export class TaskRunner< } } - async executeAlert( + private async executeAlert( alertId: string, alert: CreatedAlert, executionHandler: ExecutionHandler @@ -283,7 +283,7 @@ export class TaskRunner< return executionHandler({ actionGroup, actionSubgroup, context, state, alertId }); } - async executeAlerts( + private async executeAlerts( fakeRequest: KibanaRequest, rule: SanitizedAlert, params: Params, @@ -548,7 +548,7 @@ export class TaskRunner< }; } - async validateAndExecuteRule( + private async validateAndExecuteRule( fakeRequest: KibanaRequest, apiKey: RawRule['apiKey'], rule: SanitizedAlert, @@ -574,7 +574,9 @@ export class TaskRunner< return this.executeAlerts(fakeRequest, rule, validatedParams, executionHandler, spaceId, event); } - async loadRuleAttributesAndRun(event: Event): Promise> { + private async loadRuleAttributesAndRun( + event: Event + ): Promise> { const { params: { alertId: ruleId, spaceId }, } = this.taskInstance; diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 6b06f7efe30660..5499ba0c76caf6 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -5,12 +5,10 @@ * 2.0. */ -import { Client } from '@elastic/elasticsearch'; import type { IRouter, RequestHandlerContext, SavedObjectReference, - ElasticsearchClient, IUiSettingsClient, } from 'src/core/server'; import type { PublicMethodsOf } from '@kbn/utility-types'; @@ -48,10 +46,6 @@ import { IAbortableClusterClient } from './lib/create_abortable_es_client_factor export type WithoutQueryAndParams = Pick>; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; -export interface ElasticsearchClientWithChild extends ElasticsearchClient { - child: Client['child']; -} - /** * @public */ diff --git a/x-pack/plugins/cloud/public/fullstory.ts b/x-pack/plugins/cloud/public/fullstory.ts index 4f76abf540cae3..602b1c4cc63d37 100644 --- a/x-pack/plugins/cloud/public/fullstory.ts +++ b/x-pack/plugins/cloud/public/fullstory.ts @@ -15,9 +15,11 @@ export interface FullStoryDeps { } export type FullstoryUserVars = Record; +export type FullstoryVars = Record; export interface FullStoryApi { identify(userId: string, userVars?: FullstoryUserVars): void; + setVars(pageName: string, vars?: FullstoryVars): void; setUserVars(userVars?: FullstoryUserVars): void; event(eventName: string, eventProperties: Record): void; } diff --git a/x-pack/plugins/cloud/public/plugin.test.mocks.ts b/x-pack/plugins/cloud/public/plugin.test.mocks.ts index b79fb1bc651307..1c185d0194912a 100644 --- a/x-pack/plugins/cloud/public/plugin.test.mocks.ts +++ b/x-pack/plugins/cloud/public/plugin.test.mocks.ts @@ -11,6 +11,7 @@ import type { FullStoryDeps, FullStoryApi, FullStoryService } from './fullstory' export const fullStoryApiMock: jest.Mocked = { event: jest.fn(), setUserVars: jest.fn(), + setVars: jest.fn(), identify: jest.fn(), }; export const initializeFullStoryMock = jest.fn(() => ({ diff --git a/x-pack/plugins/cloud/public/plugin.test.ts b/x-pack/plugins/cloud/public/plugin.test.ts index 1eef581610f004..edbf724e25390e 100644 --- a/x-pack/plugins/cloud/public/plugin.test.ts +++ b/x-pack/plugins/cloud/public/plugin.test.ts @@ -12,6 +12,7 @@ import { securityMock } from '../../security/public/mocks'; import { fullStoryApiMock, initializeFullStoryMock } from './plugin.test.mocks'; import { CloudPlugin, CloudConfigType, loadFullStoryUserId } from './plugin'; import { Observable, Subject } from 'rxjs'; +import { KibanaExecutionContext } from 'kibana/public'; describe('Cloud Plugin', () => { describe('#setup', () => { @@ -24,12 +25,12 @@ describe('Cloud Plugin', () => { config = {}, securityEnabled = true, currentUserProps = {}, - currentAppId$ = undefined, + currentContext$ = undefined, }: { config?: Partial; securityEnabled?: boolean; currentUserProps?: Record; - currentAppId$?: Observable; + currentContext$?: Observable; }) => { const initContext = coreMock.createPluginInitializerContext({ id: 'cloudId', @@ -51,8 +52,8 @@ describe('Cloud Plugin', () => { const coreSetup = coreMock.createSetup(); const coreStart = coreMock.createStart(); - if (currentAppId$) { - coreStart.application.currentAppId$ = currentAppId$; + if (currentContext$) { + coreStart.executionContext.context$ = currentContext$; } coreSetup.getStartServices.mockResolvedValue([coreStart, {}, undefined]); @@ -94,44 +95,98 @@ describe('Cloud Plugin', () => { }); expect(fullStoryApiMock.identify).toHaveBeenCalledWith( - '03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4', + '5ef112cfdae3dea57097bc276e275b2816e73ef2a398dc0ffaf5b6b4e3af2041', { version_str: 'version', version_major_int: -1, version_minor_int: -1, version_patch_int: -1, + org_id_str: 'cloudId', } ); }); - it('calls FS.setUserVars everytime an app changes', async () => { - const currentAppId$ = new Subject(); + it('user hash includes org id', async () => { + await setupPlugin({ + config: { full_story: { enabled: true, org_id: 'foo' }, id: 'esOrg1' }, + currentUserProps: { + username: '1234', + }, + }); + + const hashId1 = fullStoryApiMock.identify.mock.calls[0][0]; + + await setupPlugin({ + config: { full_story: { enabled: true, org_id: 'foo' }, id: 'esOrg2' }, + currentUserProps: { + username: '1234', + }, + }); + + const hashId2 = fullStoryApiMock.identify.mock.calls[1][0]; + + expect(hashId1).not.toEqual(hashId2); + }); + + it('calls FS.setVars everytime an app changes', async () => { + const currentContext$ = new Subject(); const { plugin } = await setupPlugin({ config: { full_story: { enabled: true, org_id: 'foo' } }, currentUserProps: { username: '1234', }, - currentAppId$, + currentContext$, }); - expect(fullStoryApiMock.setUserVars).not.toHaveBeenCalled(); - currentAppId$.next('App1'); - expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({ + // takes the app name + expect(fullStoryApiMock.setVars).not.toHaveBeenCalled(); + currentContext$.next({ + name: 'App1', + description: '123', + }); + + expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', { + pageName: 'App1', app_id_str: 'App1', }); - currentAppId$.next(); - expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({ - app_id_str: 'unknown', + + // context clear + currentContext$.next({}); + expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', { + pageName: 'App1', + app_id_str: 'App1', }); - currentAppId$.next('App2'); - expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({ + // different app + currentContext$.next({ + name: 'App2', + page: 'page2', + id: '123', + }); + expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', { + pageName: 'App2:page2', app_id_str: 'App2', + page_str: 'page2', + ent_id_str: '123', + }); + + // Back to first app + currentContext$.next({ + name: 'App1', + page: 'page3', + id: '123', + }); + + expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', { + pageName: 'App1:page3', + app_id_str: 'App1', + page_str: 'page3', + ent_id_str: '123', }); - expect(currentAppId$.observers.length).toBe(1); + expect(currentContext$.observers.length).toBe(1); plugin.stop(); - expect(currentAppId$.observers.length).toBe(0); + expect(currentContext$.observers.length).toBe(0); }); it('does not call FS.identify when security is not available', async () => { diff --git a/x-pack/plugins/cloud/public/plugin.tsx b/x-pack/plugins/cloud/public/plugin.tsx index 991a7c1f8b565c..89f24971de25c1 100644 --- a/x-pack/plugins/cloud/public/plugin.tsx +++ b/x-pack/plugins/cloud/public/plugin.tsx @@ -13,11 +13,12 @@ import { PluginInitializerContext, HttpStart, IBasePath, - ApplicationStart, + ExecutionContextStart, } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import useObservable from 'react-use/lib/useObservable'; import { BehaviorSubject, Subscription } from 'rxjs'; +import { compact, isUndefined, omitBy } from 'lodash'; import type { AuthenticatedUser, SecurityPluginSetup, @@ -83,8 +84,9 @@ export interface CloudSetup { } interface SetupFullstoryDeps extends CloudSetupDependencies { - application?: Promise; + executionContextPromise?: Promise; basePath: IBasePath; + esOrgId?: string; } interface SetupChatDeps extends Pick { @@ -103,11 +105,16 @@ export class CloudPlugin implements Plugin { } public setup(core: CoreSetup, { home, security }: CloudSetupDependencies) { - const application = core.getStartServices().then(([coreStart]) => { - return coreStart.application; + const executionContextPromise = core.getStartServices().then(([coreStart]) => { + return coreStart.executionContext; }); - this.setupFullstory({ basePath: core.http.basePath, security, application }).catch((e) => + this.setupFullstory({ + basePath: core.http.basePath, + security, + executionContextPromise, + esOrgId: this.config.id, + }).catch((e) => // eslint-disable-next-line no-console console.debug(`Error setting up FullStory: ${e.toString()}`) ); @@ -223,9 +230,14 @@ export class CloudPlugin implements Plugin { return user?.roles.includes('superuser') ?? true; } - private async setupFullstory({ basePath, security, application }: SetupFullstoryDeps) { - const { enabled, org_id: orgId } = this.config.full_story; - if (!enabled || !orgId) { + private async setupFullstory({ + basePath, + security, + executionContextPromise, + esOrgId, + }: SetupFullstoryDeps) { + const { enabled, org_id: fsOrgId } = this.config.full_story; + if (!enabled || !fsOrgId) { return; // do not load any fullstory code in the browser if not enabled } @@ -243,7 +255,7 @@ export class CloudPlugin implements Plugin { const { fullStory, sha256 } = initializeFullStory({ basePath, - orgId, + orgId: fsOrgId, packageInfo: this.initializerContext.env.packageInfo, }); @@ -252,16 +264,29 @@ export class CloudPlugin implements Plugin { // This needs to be called syncronously to be sure that we populate the user ID soon enough to make sessions merging // across domains work if (userId) { - // Do the hashing here to keep it at clear as possible in our source code that we do not send literal user IDs - const hashedId = sha256(userId.toString()); - application - ?.then(async () => { - const appStart = await application; - this.appSubscription = appStart.currentAppId$.subscribe((appId) => { - // Update the current application every time it changes - fullStory.setUserVars({ - app_id_str: appId ?? 'unknown', - }); + // Join the cloud org id and the user to create a truly unique user id. + // The hashing here is to keep it at clear as possible in our source code that we do not send literal user IDs + const hashedId = sha256(esOrgId ? `${esOrgId}:${userId}` : `${userId}`); + + executionContextPromise + ?.then(async (executionContext) => { + this.appSubscription = executionContext.context$.subscribe((context) => { + const { name, page, id } = context; + // Update the current context every time it changes + fullStory.setVars( + 'page', + omitBy( + { + // Read about the special pageName property + // https://help.fullstory.com/hc/en-us/articles/1500004101581-FS-setVars-API-Sending-custom-page-data-to-FullStory + pageName: `${compact([name, page]).join(':')}`, + app_id_str: name ?? 'unknown', + page_str: page, + ent_id_str: id, + }, + isUndefined + ) + ); }); }) .catch((e) => { @@ -282,6 +307,7 @@ export class CloudPlugin implements Plugin { version_major_int: parsedVer[0] ?? -1, version_minor_int: parsedVer[1] ?? -1, version_patch_int: parsedVer[2] ?? -1, + org_id_str: esOrgId, }); } } catch (e) { diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts index b728948cf2a056..8c9d04dc207f3e 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts @@ -76,6 +76,18 @@ describe('benchmarks API', () => { }); }); + it('expect to find benchmark_name', async () => { + const validatedQuery = benchmarksInputSchema.validate({ + benchmark_name: 'my_cis_benchmark', + }); + + expect(validatedQuery).toMatchObject({ + page: 1, + per_page: DEFAULT_BENCHMARKS_PER_PAGE, + benchmark_name: 'my_cis_benchmark', + }); + }); + it('should throw when page field is not a positive integer', async () => { expect(() => { benchmarksInputSchema.validate({ page: -2 }); @@ -125,6 +137,24 @@ describe('benchmarks API', () => { }); }); + it('should format request by benchmark_name', async () => { + const mockAgentPolicyService = createPackagePolicyServiceMock(); + + await getPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { + page: 1, + per_page: 100, + benchmark_name: 'my_cis_benchmark', + }); + + expect(mockAgentPolicyService.list.mock.calls[0][1]).toMatchObject( + expect.objectContaining({ + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:myPackage AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: *my_cis_benchmark*`, + page: 1, + perPage: 100, + }) + ); + }); + describe('test getAgentPolicies', () => { it('should return one agent policy id when there is duplication', async () => { const agentPolicyService = createMockAgentPolicyService(); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts index 80c526c248c0ff..c52aeead6cd4da 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts @@ -43,8 +43,13 @@ export interface Benchmark { export const DEFAULT_BENCHMARKS_PER_PAGE = 20; export const PACKAGE_POLICY_SAVED_OBJECT_TYPE = 'ingest-package-policies'; -const getPackageNameQuery = (packageName: string): string => { - return `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageName}`; +const getPackageNameQuery = (packageName: string, benchmarkFilter?: string): string => { + const integrationNameQuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageName}`; + const kquery = benchmarkFilter + ? `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageName} AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: *${benchmarkFilter}*` + : integrationNameQuery; + + return kquery; }; export const getPackagePolicies = async ( @@ -57,7 +62,7 @@ export const getPackagePolicies = async ( throw new Error('packagePolicyService is undefined'); } - const packageNameQuery = getPackageNameQuery(packageName); + const packageNameQuery = getPackageNameQuery(packageName, queryParams.benchmark_name); const { items: packagePolicies } = (await packagePolicyService?.list(soClient, { kuery: packageNameQuery, @@ -193,4 +198,8 @@ export const benchmarksInputSchema = rt.object({ * The number of objects to include in each page */ per_page: rt.number({ defaultValue: DEFAULT_BENCHMARKS_PER_PAGE, min: 0 }), + /** + * Benchmark filter + */ + benchmark_name: rt.maybe(rt.string()), }); diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index 7a6203c994f4d9..456a76d914f7d2 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -89,3 +89,4 @@ export const READ_ONLY_MODE_HEADER = 'x-ent-search-read-only-mode'; export const ENTERPRISE_SEARCH_KIBANA_COOKIE = '_enterprise_search'; export const LOGS_SOURCE_ID = 'ent-search-logs'; +export const ENTERPRISE_SEARCH_AUDIT_LOGS_SOURCE_ID = 'ent-search-audit-logs'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_custom_settings_flyout/crawl_custom_settings_flyout_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_custom_settings_flyout/crawl_custom_settings_flyout_logic.test.ts index ddfc23b5aa6285..bb20e0e639aa2a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_custom_settings_flyout/crawl_custom_settings_flyout_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_custom_settings_flyout/crawl_custom_settings_flyout_logic.test.ts @@ -297,7 +297,25 @@ describe('CrawlCustomSettingsFlyoutLogic', () => { }); describe('startCustomCrawl', () => { - it('starts a custom crawl with the user set values', async () => { + it('can start a custom crawl for selected domains', async () => { + mount({ + includeSitemapsInRobotsTxt: true, + maxCrawlDepth: 5, + selectedDomainUrls: ['https://www.elastic.co', 'https://swiftype.com'], + }); + jest.spyOn(CrawlerLogic.actions, 'startCrawl'); + + CrawlCustomSettingsFlyoutLogic.actions.startCustomCrawl(); + await nextTick(); + + expect(CrawlerLogic.actions.startCrawl).toHaveBeenCalledWith({ + domain_allowlist: ['https://www.elastic.co', 'https://swiftype.com'], + max_crawl_depth: 5, + sitemap_discovery_disabled: false, + }); + }); + + it('can start a custom crawl selected domains, sitemaps, and seed urls', async () => { mount({ includeSitemapsInRobotsTxt: true, maxCrawlDepth: 5, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_custom_settings_flyout/crawl_custom_settings_flyout_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_custom_settings_flyout/crawl_custom_settings_flyout_logic.ts index f22dcc7487af3d..3b04e1b28c17eb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_custom_settings_flyout/crawl_custom_settings_flyout_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_custom_settings_flyout/crawl_custom_settings_flyout_logic.ts @@ -11,7 +11,7 @@ import { flashAPIErrors } from '../../../../../shared/flash_messages'; import { HttpLogic } from '../../../../../shared/http'; import { EngineLogic } from '../../../engine'; -import { CrawlerLogic } from '../../crawler_logic'; +import { CrawlerLogic, CrawlRequestOverrides } from '../../crawler_logic'; import { DomainConfig, DomainConfigFromServer } from '../../types'; import { domainConfigServerToClient } from '../../utils'; import { extractDomainAndEntryPointFromUrl } from '../add_domain/utils'; @@ -213,13 +213,23 @@ export const CrawlCustomSettingsFlyoutLogic = kea< actions.fetchDomainConfigData(); }, startCustomCrawl: () => { - CrawlerLogic.actions.startCrawl({ - domain_allowlist: values.selectedDomainUrls, - max_crawl_depth: values.maxCrawlDepth, - seed_urls: [...values.selectedEntryPointUrls, ...values.customEntryPointUrls], - sitemap_urls: [...values.selectedSitemapUrls, ...values.customSitemapUrls], + const overrides: CrawlRequestOverrides = { sitemap_discovery_disabled: !values.includeSitemapsInRobotsTxt, - }); + max_crawl_depth: values.maxCrawlDepth, + domain_allowlist: values.selectedDomainUrls, + }; + + const seedUrls = [...values.selectedEntryPointUrls, ...values.customEntryPointUrls]; + if (seedUrls.length > 0) { + overrides.seed_urls = seedUrls; + } + + const sitemapUrls = [...values.selectedSitemapUrls, ...values.customSitemapUrls]; + if (sitemapUrls.length > 0) { + overrides.sitemap_urls = sitemapUrls; + } + + CrawlerLogic.actions.startCrawl(overrides); }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts index 68b1cb6ec9b267..2d1b8a9e7aa27e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts @@ -33,7 +33,7 @@ const ACTIVE_STATUSES = [ CrawlerStatus.Canceling, ]; -interface CrawlRequestOverrides { +export interface CrawlRequestOverrides { domain_allowlist?: string[]; max_crawl_depth?: number; seed_urls?: string[]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.scss new file mode 100644 index 00000000000000..11a008a3cc51fc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.scss @@ -0,0 +1,9 @@ +.auditLogsModal { + width: 75vw; +} + +@media (max-width: 1200px) { + .auditLogsModal { + width: 100vw; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.test.tsx new file mode 100644 index 00000000000000..f6687e431e9836 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogicMounter, setMockValues, setMockActions } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiText, EuiModal } from '@elastic/eui'; + +import { EntSearchLogStream } from '../../../../../shared/log_stream'; + +import { AuditLogsModal } from './audit_logs_modal'; + +import { AuditLogsModalLogic } from './audit_logs_modal_logic'; + +describe('AuditLogsModal', () => { + const { mount } = new LogicMounter(AuditLogsModalLogic); + beforeEach(() => { + jest.clearAllMocks(); + mount({ isModalVisible: true }); + }); + + it('renders nothing by default', () => { + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders the modal when modal visible', () => { + const testEngineName = 'test-engine-123'; + const mockClose = jest.fn(); + setMockValues({ + isModalVisible: true, + engineName: testEngineName, + }); + setMockActions({ + hideModal: mockClose, + }); + + const wrapper = shallow(); + expect(wrapper.find(EntSearchLogStream).prop('query')).toBe( + `event.kind: event and event.action: audit and enterprisesearch.data_repository.name: ${testEngineName}` + ); + expect(wrapper.find(EuiText).children().text()).toBe('Showing events from last 24 hours'); + expect(wrapper.find(EuiModal).prop('onClose')).toBe(mockClose); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.tsx new file mode 100644 index 00000000000000..3807234fd5c118 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues, useActions } from 'kea'; + +import { + EuiButton, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { ENTERPRISE_SEARCH_AUDIT_LOGS_SOURCE_ID } from '../../../../../../../common/constants'; +import { EntSearchLogStream } from '../../../../../shared/log_stream'; + +import { AuditLogsModalLogic } from './audit_logs_modal_logic'; + +import './audit_logs_modal.scss'; + +export const AuditLogsModal: React.FC = () => { + const auditLogsModalLogic = AuditLogsModalLogic(); + const { isModalVisible, engineName } = useValues(auditLogsModalLogic); + const { hideModal } = useActions(auditLogsModalLogic); + + const filters = [ + 'event.kind: event', + 'event.action: audit', + `enterprisesearch.data_repository.name: ${engineName}`, + ].join(' and '); + + return !isModalVisible ? null : ( + + + +

{engineName}

+
+
+ + + {i18n.translate('xpack.enterpriseSearch.appSearch.engines.auditLogsModal.eventTip', { + defaultMessage: 'Showing events from last 24 hours', + })} + + + + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engines.auditLogsModal.closeButton', { + defaultMessage: 'Close', + })} + + +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.test.ts new file mode 100644 index 00000000000000..f869dd145087d3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogicMounter } from '../../../../../__mocks__/kea_logic'; + +import { AuditLogsModalLogic } from './audit_logs_modal_logic'; + +describe('AuditLogsModalLogic', () => { + const { mount } = new LogicMounter(AuditLogsModalLogic); + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has excepted default values', () => { + expect(AuditLogsModalLogic.values).toEqual({ + isModalVisible: false, + engineName: '', + }); + }); + + describe('actions', () => { + describe('hideModal', () => { + it('hides the modal', () => { + mount({ + isModalVisible: true, + engineName: 'test_engine', + }); + + AuditLogsModalLogic.actions.hideModal(); + expect(AuditLogsModalLogic.values).toEqual({ + isModalVisible: false, + engineName: '', + }); + }); + }); + + describe('showModal', () => { + it('show the modal with correct engine name', () => { + AuditLogsModalLogic.actions.showModal('test-engine-123'); + expect(AuditLogsModalLogic.values).toEqual({ + isModalVisible: true, + engineName: 'test-engine-123', + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.ts new file mode 100644 index 00000000000000..afa70b4f3dee0e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea } from 'kea'; + +export const AuditLogsModalLogic = kea({ + path: ['enterprise_search', 'app_search', 'engines_overview', 'audit_logs_modal'], + actions: () => ({ + hideModal: true, + showModal: (engineName: string) => ({ engineName }), + }), + reducers: () => ({ + isModalVisible: [ + false, + { + showModal: () => true, + hideModal: () => false, + }, + ], + engineName: [ + '', + { + showModal: (_, { engineName }) => engineName, + hideModal: () => '', + }, + ], + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engine_link_helpers.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engine_link_helpers.tsx index a3350d1ef9939c..229e0def4700e9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engine_link_helpers.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engine_link_helpers.tsx @@ -7,11 +7,14 @@ import React from 'react'; +import { EuiLink } from '@elastic/eui'; + import { KibanaLogic } from '../../../../../shared/kibana'; import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; import { TelemetryLogic } from '../../../../../shared/telemetry'; import { ENGINE_PATH } from '../../../../routes'; import { generateEncodedPath } from '../../../../utils/encode_path_params'; +import { FormattedDateTime } from '../../../../utils/formatted_date_time'; const sendEngineTableLinkClickTelemetry = () => { TelemetryLogic.actions.sendAppSearchTelemetry({ @@ -34,3 +37,9 @@ export const renderEngineLink = (engineName: string) => ( {engineName} ); + +export const renderLastChangeLink = (dateString: string, onClick = () => {}) => ( + + {!dateString ? '-' : } + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engines_table.tsx index 563e272a4a7303..5e6ece1003e7fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engines_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engines_table.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { useValues } from 'kea'; +import { useActions, useValues } from 'kea'; import { EuiBasicTable, EuiBasicTableColumn, EuiTableFieldDataColumnType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -16,10 +16,13 @@ import { AppLogic } from '../../../../app_logic'; import { UNIVERSAL_LANGUAGE } from '../../../../constants'; import { EngineDetails } from '../../../engine/types'; -import { renderEngineLink } from './engine_link_helpers'; +import { AuditLogsModalLogic } from '../audit_logs_modal/audit_logs_modal_logic'; + +import { renderEngineLink, renderLastChangeLink } from './engine_link_helpers'; import { ACTIONS_COLUMN, CREATED_AT_COLUMN, + LAST_UPDATED_COLUMN, DOCUMENT_COUNT_COLUMN, FIELD_COUNT_COLUMN, NAME_COLUMN, @@ -46,12 +49,22 @@ export const EnginesTable: React.FC = ({ myRole: { canManageEngines }, } = useValues(AppLogic); + const { showModal: showAuditLogModal } = useActions(AuditLogsModalLogic); + const columns: Array> = [ { ...NAME_COLUMN, render: (name: string) => renderEngineLink(name), }, CREATED_AT_COLUMN, + { + ...LAST_UPDATED_COLUMN, + render: (dateString: string, engineDetails) => { + return renderLastChangeLink(dateString, () => { + showAuditLogModal(engineDetails.name); + }); + }, + }, LANGUAGE_COLUMN, DOCUMENT_COUNT_COLUMN, FIELD_COUNT_COLUMN, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table.tsx index f99dc7e15eaec5..24eb8cc8a6b812 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table.tsx @@ -14,6 +14,9 @@ import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import { AppLogic } from '../../../../app_logic'; import { EngineDetails } from '../../../engine/types'; +import { AuditLogsModalLogic } from '../audit_logs_modal/audit_logs_modal_logic'; + +import { renderLastChangeLink } from './engine_link_helpers'; import { MetaEnginesTableExpandedRow } from './meta_engines_table_expanded_row'; import { MetaEnginesTableLogic } from './meta_engines_table_logic'; import { MetaEnginesTableNameColumnContent } from './meta_engines_table_name_column_content'; @@ -21,6 +24,7 @@ import { ACTIONS_COLUMN, BLANK_COLUMN, CREATED_AT_COLUMN, + LAST_UPDATED_COLUMN, DOCUMENT_COUNT_COLUMN, FIELD_COUNT_COLUMN, NAME_COLUMN, @@ -49,6 +53,8 @@ export const MetaEnginesTable: React.FC = ({ myRole: { canManageMetaEngines }, } = useValues(AppLogic); + const { showModal: showAuditLogModal } = useActions(AuditLogsModalLogic); + const conflictingEnginesSets: ConflictingEnginesSets = useMemo( () => items.reduce((accumulator, metaEngine) => { @@ -89,6 +95,14 @@ export const MetaEnginesTable: React.FC = ({ ), }, CREATED_AT_COLUMN, + { + ...LAST_UPDATED_COLUMN, + render: (dateString: string, engineDetails) => { + return renderLastChangeLink(dateString, () => { + showAuditLogModal(engineDetails.name); + }); + }, + }, BLANK_COLUMN, DOCUMENT_COUNT_COLUMN, FIELD_COUNT_COLUMN, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/shared_columns.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/shared_columns.tsx index 325760b641efdc..b0ca36a7778389 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/shared_columns.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/shared_columns.tsx @@ -50,6 +50,17 @@ export const CREATED_AT_COLUMN: EuiTableFieldDataColumnType = { render: (dateString: string) => , }; +export const LAST_UPDATED_COLUMN: EuiTableFieldDataColumnType = { + field: 'updated_at', + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.enginesOverview.table.column.lastUpdated', + { + defaultMessage: 'Last updated', + } + ), + dataType: 'string', +}; + export const DOCUMENT_COUNT_COLUMN: EuiTableFieldDataColumnType = { field: 'document_count', name: i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx index f8df9f5abfaa54..27cdff5d69812e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx @@ -21,6 +21,7 @@ import { DataPanel } from '../data_panel'; import { AppSearchPageTemplate } from '../layout'; import { EmptyState, EmptyMetaEnginesState } from './components'; +import { AuditLogsModal } from './components/audit_logs_modal/audit_logs_modal'; import { EnginesTable } from './components/tables/engines_table'; import { MetaEnginesTable } from './components/tables/meta_engines_table'; import { @@ -144,6 +145,7 @@ export const EnginesOverview: React.FC = () => { data-test-subj="metaEnginesLicenseCTA" /> )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts index 60d0dcc0c5911e..0d5e4e9824f170 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts @@ -23,6 +23,7 @@ describe('getRoleAbilities', () => { // Has access canViewAccountCredentials: true, canManageEngines: true, + canManageMetaEngines: true, // Does not have access canViewMetaEngines: false, canViewEngineAnalytics: false, @@ -35,7 +36,6 @@ describe('getRoleAbilities', () => { canViewMetaEngineSourceEngines: false, canViewSettings: false, canViewRoleMappings: false, - canManageMetaEngines: false, canManageLogSettings: false, canManageSettings: false, canManageEngineCrawler: false, @@ -81,10 +81,10 @@ describe('getRoleAbilities', () => { expect(myRole.canManageMetaEngines).toEqual(true); }); - it('returns false when the user can manage any engines but the account does not have a platinum license', () => { + it('returns true when the user can manage any engines but the account does not have a platinum license', () => { const myRole = getRoleAbilities({ ...mockRole, ...canManageEngines }, false); - expect(myRole.canManageMetaEngines).toEqual(false); + expect(myRole.canManageMetaEngines).toEqual(true); }); it('returns false when has a platinum license but the user cannot manage any engines', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts index ef3e22d851f387..15cba16ab04345 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts @@ -49,7 +49,7 @@ export const getRoleAbilities = (role: Account['role'], hasPlatinumLicense = fal canViewSettings: myRole.can('view', 'account_settings'), canViewRoleMappings: myRole.can('view', 'role_mappings'), canManageEngines: myRole.can('manage', 'account_engines'), - canManageMetaEngines: hasPlatinumLicense && myRole.can('manage', 'account_engines'), + canManageMetaEngines: myRole.can('manage', 'account_engines'), canManageLogSettings: myRole.can('manage', 'account_log_settings'), canManageSettings: myRole.can('manage', 'account_settings'), canManageEngineCrawler: myRole.can('manage', 'engine_crawler'), diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx index 4598ca337f4e2c..76c6c3cfa9d592 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx @@ -27,6 +27,7 @@ import { staticSourceData } from '../../source_data'; import { AddSource } from './add_source'; import { AddSourceSteps } from './add_source_logic'; import { ConfigCompleted } from './config_completed'; +import { ConfigurationChoice } from './configuration_choice'; import { ConfigurationIntro } from './configuration_intro'; import { ConfigureOauth } from './configure_oauth'; import { ConnectInstance } from './connect_instance'; @@ -71,6 +72,22 @@ describe('AddSourceList', () => { expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep); }); + it('renders default state correctly when there are multiple connector options', () => { + const wrapper = shallow( + + ); + wrapper.find(ConfigurationIntro).prop('advanceStep')(); + + expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.ChoiceStep); + }); + describe('layout', () => { it('renders the default workplace search layout when on an organization view', () => { setMockValues({ ...mockValues, isOrganization: true }); @@ -153,4 +170,19 @@ describe('AddSourceList', () => { expect(wrapper.find(Reauthenticate)).toHaveLength(1); }); + + it('renders Config Choice step', () => { + setMockValues({ + ...mockValues, + addSourceCurrentStep: AddSourceSteps.ChoiceStep, + }); + const wrapper = shallow(); + const advance = wrapper.find(ConfigurationChoice).prop('goToInternalStep'); + expect(advance).toBeDefined(); + if (advance) { + advance(); + } + + expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx index 1e9be74224c5ed..f03c77290f22dd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx @@ -21,9 +21,12 @@ import { import { NAV } from '../../../../constants'; import { SOURCES_PATH, getSourcesPath, getAddPath } from '../../../../routes'; +import { hasMultipleConnectorOptions } from '../../../../utils'; + import { AddSourceHeader } from './add_source_header'; import { AddSourceLogic, AddSourceProps, AddSourceSteps } from './add_source_logic'; import { ConfigCompleted } from './config_completed'; +import { ConfigurationChoice } from './configuration_choice'; import { ConfigurationIntro } from './configuration_intro'; import { ConfigureOauth } from './configure_oauth'; import { ConnectInstance } from './connect_instance'; @@ -51,6 +54,7 @@ export const AddSource: React.FC = (props) => { const goToSaveConfig = () => setAddSourceStep(AddSourceSteps.SaveConfigStep); const setConfigCompletedStep = () => setAddSourceStep(AddSourceSteps.ConfigCompletedStep); const goToConfigCompleted = () => saveSourceConfig(false, setConfigCompletedStep); + const goToChoice = () => setAddSourceStep(AddSourceSteps.ChoiceStep); const FORM_SOURCE_ADDED_SUCCESS_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.contentSource.formSourceAddedSuccessMessage', { @@ -75,7 +79,11 @@ export const AddSource: React.FC = (props) => { return ( {addSourceCurrentStep === AddSourceSteps.ConfigIntroStep && ( - + )} {addSourceCurrentStep === AddSourceSteps.SaveConfigStep && ( = (props) => { {addSourceCurrentStep === AddSourceSteps.ReauthenticateStep && ( )} + {addSourceCurrentStep === AddSourceSteps.ChoiceStep && ( + + )} ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts index 80f8a2fc18218d..a633beac3a1c23 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts @@ -664,11 +664,13 @@ describe('AddSourceLogic', () => { }); it('handles error', async () => { + const setButtonNotLoadingSpy = jest.spyOn(AddSourceLogic.actions, 'setButtonNotLoading'); http.post.mockReturnValue(Promise.reject('this is an error')); AddSourceLogic.actions.createContentSource(serviceType, successCallback, errorCallback); await nextTick(); + expect(setButtonNotLoadingSpy).toHaveBeenCalled(); expect(errorCallback).toHaveBeenCalled(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts index db0c5b97372636..92fab713a3fa09 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts @@ -41,6 +41,7 @@ export enum AddSourceSteps { ConnectInstanceStep = 'Connect Instance', ConfigureOauthStep = 'Configure Oauth', ReauthenticateStep = 'Reauthenticate', + ChoiceStep = 'Choice', } export interface OauthParams { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.test.tsx index bfb916847d865e..392ce175d271db 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.test.tsx @@ -13,10 +13,6 @@ import { shallow } from 'enzyme'; import { EuiText, EuiButton } from '@elastic/eui'; -import { - PersonalDashboardLayout, - WorkplaceSearchPageTemplate, -} from '../../../../components/layout'; import { staticSourceData } from '../../source_data'; import { ConfigurationChoice } from './configuration_choice'; @@ -35,22 +31,6 @@ describe('ConfigurationChoice', () => { jest.clearAllMocks(); }); - describe('layout', () => { - it('renders the default workplace search layout when on an organization view', () => { - setMockValues({ ...mockValues, isOrganization: true }); - const wrapper = shallow(); - - expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate); - }); - - it('renders the personal dashboard layout when not in an organization', () => { - setMockValues({ ...mockValues, isOrganization: false }); - const wrapper = shallow(); - - expect(wrapper.type()).toEqual(PersonalDashboardLayout); - }); - }); - it('renders internal connector if available', () => { const wrapper = shallow(); @@ -64,6 +44,16 @@ describe('ConfigurationChoice', () => { button.simulate('click'); expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/box/internal/'); }); + it('should call prop function when provided on internal connector click', () => { + const advanceSpy = jest.fn(); + const wrapper = shallow( + + ); + const button = wrapper.find(EuiButton); + button.simulate('click'); + expect(navigateToUrl).not.toHaveBeenCalled(); + expect(advanceSpy).toHaveBeenCalled(); + }); it('renders external connector if available', () => { const wrapper = shallow( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.tsx index 46a8998c9dd10a..f5d6d51651dd4d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.tsx @@ -9,16 +9,11 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { KibanaLogic } from '../../../../../shared/kibana'; import { AppLogic } from '../../../../app_logic'; -import { - WorkplaceSearchPageTemplate, - PersonalDashboardLayout, -} from '../../../../components/layout'; -import { NAV } from '../../../../constants'; import { getAddPath, getSourcesPath } from '../../../../routes'; import { SourceDataItem } from '../../../../types'; @@ -26,6 +21,7 @@ import { AddSourceHeader } from './add_source_header'; interface ConfigurationIntroProps { sourceData: SourceDataItem; + goToInternalStep?: () => void; } export const ConfigurationChoice: React.FC = ({ @@ -36,15 +32,18 @@ export const ConfigurationChoice: React.FC = ({ internalConnectorAvailable, customConnectorAvailable, }, + goToInternalStep, }) => { const { isOrganization } = useValues(AppLogic); - const goToInternal = () => - KibanaLogic.values.navigateToUrl( - `${getSourcesPath( - `${getSourcesPath(getAddPath(serviceType), isOrganization)}/internal`, - isOrganization - )}/` - ); + const goToInternal = goToInternalStep + ? goToInternalStep + : () => + KibanaLogic.values.navigateToUrl( + `${getSourcesPath( + `${getSourcesPath(getAddPath(serviceType), isOrganization)}/internal`, + isOrganization + )}/` + ); const goToExternal = () => KibanaLogic.values.navigateToUrl( `${getSourcesPath( @@ -59,12 +58,10 @@ export const ConfigurationChoice: React.FC = ({ isOrganization )}/` ); - const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; return ( - + <> - = ({ )} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx index 0ee80019ea720e..0ae176dbef019f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx @@ -36,9 +36,8 @@ describe('ConnectInstance', () => { const getSourceConnectData = jest.fn((_, redirectOauth) => { redirectOauth(); }); - const createContentSource = jest.fn((_, redirectFormCreated, handleFormSubmitError) => { + const createContentSource = jest.fn((_, redirectFormCreated) => { redirectFormCreated(); - handleFormSubmitError(); }); const credentialsSourceData = staticSourceData[13]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx index a9e24c7b944aba..352addd8176d84 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, useEffect, FormEvent } from 'react'; +import React, { useEffect, FormEvent } from 'react'; import { useActions, useValues } from 'kea'; @@ -51,8 +51,6 @@ export const ConnectInstance: React.FC = ({ onFormCreated, header, }) => { - const [formLoading, setFormLoading] = useState(false); - const { hasPlatinumLicense } = useValues(LicensingLogic); const { @@ -64,7 +62,7 @@ export const ConnectInstance: React.FC = ({ setSourceIndexPermissionsValue, } = useActions(AddSourceLogic); - const { loginValue, passwordValue, indexPermissionsValue, subdomainValue } = + const { buttonLoading, loginValue, passwordValue, indexPermissionsValue, subdomainValue } = useValues(AddSourceLogic); const { isOrganization } = useValues(AppLogic); @@ -77,12 +75,9 @@ export const ConnectInstance: React.FC = ({ const redirectOauth = (oauthUrl: string) => window.location.replace(oauthUrl); const redirectFormCreated = () => onFormCreated(name); const onOauthFormSubmit = () => getSourceConnectData(serviceType, redirectOauth); - const handleFormSubmitError = () => setFormLoading(false); - const onCredentialsFormSubmit = () => - createContentSource(serviceType, redirectFormCreated, handleFormSubmitError); + const onCredentialsFormSubmit = () => createContentSource(serviceType, redirectFormCreated); const handleFormSubmit = (e: FormEvent) => { - setFormLoading(true); e.preventDefault(); const onSubmit = hasOauthRedirect ? onOauthFormSubmit : onCredentialsFormSubmit; onSubmit(); @@ -145,7 +140,7 @@ export const ConnectInstance: React.FC = ({ {permissionsExcluded && !hasPlatinumLicense && } - + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.contentSource.connect.button', { defaultMessage: 'Connect {name}', values: { name }, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx index c2cd58a90f209d..e735119f687cc1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx @@ -90,9 +90,10 @@ export const SourcesRouter: React.FC = () => { : externalConnectorAvailable ? 'external' : 'custom'; + const showChoice = defaultOption !== 'internal' && hasMultipleConnectorOptions(sourceData); return ( - {hasMultipleConnectorOptions(sourceData) ? ( + {showChoice ? ( ) : ( diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index ef9a0cea9da60f..f393ca59a44118 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -27,6 +27,7 @@ import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN, LOGS_SOURCE_ID, + ENTERPRISE_SEARCH_AUDIT_LOGS_SOURCE_ID, } from '../common/constants'; import { registerTelemetryUsageCollector as registerASTelemetryUsageCollector } from './collectors/app_search/telemetry'; @@ -185,6 +186,14 @@ export class EnterpriseSearchPlugin implements Plugin { indexName: '.ent-search-*', }, }); + + infra.defineInternalSourceConfiguration(ENTERPRISE_SEARCH_AUDIT_LOGS_SOURCE_ID, { + name: 'Enterprise Search Audit Logs', + logIndices: { + type: 'index_name', + indexName: 'logs-enterprise_search*', + }, + }); } public start() {} diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.test.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.test.ts new file mode 100644 index 00000000000000..51aee45c83cf3d --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.test.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { + ISavedObjectsImporter, + SavedObjectsImportFailure, + SavedObjectsImportSuccess, + SavedObjectsImportResponse, +} from 'src/core/server'; + +import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks'; + +import type { ArchiveAsset } from './install'; + +jest.mock('timers/promises', () => ({ + async setTimeout() {}, +})); + +import { installKibanaSavedObjects } from './install'; + +const mockLogger = loggingSystemMock.createLogger(); + +const mockImporter: jest.Mocked = { + import: jest.fn(), + resolveImportErrors: jest.fn(), +}; + +const createImportError = (so: ArchiveAsset, type: string) => + ({ id: so.id, error: { type } } as SavedObjectsImportFailure); +const createImportSuccess = (so: ArchiveAsset) => + ({ id: so.id, type: so.type, meta: {} } as SavedObjectsImportSuccess); +const createAsset = (asset: Partial) => + ({ id: 1234, type: 'dashboard', attributes: {}, ...asset } as ArchiveAsset); + +const createImportResponse = ( + errors: SavedObjectsImportFailure[] = [], + successResults: SavedObjectsImportSuccess[] = [] +) => + ({ + success: !!successResults.length, + errors, + successResults, + warnings: [], + successCount: successResults.length, + } as SavedObjectsImportResponse); + +describe('installKibanaSavedObjects', () => { + beforeEach(() => { + mockImporter.import.mockReset(); + mockImporter.resolveImportErrors.mockReset(); + }); + + it('should retry on conflict error', async () => { + const asset = createAsset({ attributes: { hello: 'world' } }); + const conflictResponse = createImportResponse([createImportError(asset, 'conflict')]); + const successResponse = createImportResponse([], [createImportSuccess(asset)]); + + mockImporter.import + .mockResolvedValueOnce(conflictResponse) + .mockResolvedValueOnce(successResponse); + + await installKibanaSavedObjects({ + savedObjectsImporter: mockImporter, + logger: mockLogger, + kibanaAssets: [asset], + }); + + expect(mockImporter.import).toHaveBeenCalledTimes(2); + }); + + it('should give up after 50 retries on conflict errors', async () => { + const asset = createAsset({ attributes: { hello: 'world' } }); + const conflictResponse = createImportResponse([createImportError(asset, 'conflict')]); + + mockImporter.import.mockImplementation(() => Promise.resolve(conflictResponse)); + + await expect( + installKibanaSavedObjects({ + savedObjectsImporter: mockImporter, + logger: mockLogger, + kibanaAssets: [asset], + }) + ).rejects.toEqual(expect.any(Error)); + expect(mockImporter.import).toHaveBeenCalledTimes(51); + }); + it('should not retry errors that arent conflict errors', async () => { + const asset = createAsset({ attributes: { hello: 'world' } }); + const errorResponse = createImportResponse([createImportError(asset, 'something_bad')]); + const successResponse = createImportResponse([], [createImportSuccess(asset)]); + + mockImporter.import.mockResolvedValueOnce(errorResponse).mockResolvedValueOnce(successResponse); + + expect( + installKibanaSavedObjects({ + savedObjectsImporter: mockImporter, + logger: mockLogger, + kibanaAssets: [asset], + }) + ).rejects.toEqual(expect.any(Error)); + }); + + it('should resolve reference errors', async () => { + const asset = createAsset({ attributes: { hello: 'world' } }); + const referenceErrorResponse = createImportResponse([ + createImportError(asset, 'missing_references'), + ]); + const successResponse = createImportResponse([], [createImportSuccess(asset)]); + + mockImporter.import.mockResolvedValueOnce(referenceErrorResponse); + mockImporter.resolveImportErrors.mockResolvedValueOnce(successResponse); + + await installKibanaSavedObjects({ + savedObjectsImporter: mockImporter, + logger: mockLogger, + kibanaAssets: [asset], + }); + + expect(mockImporter.import).toHaveBeenCalledTimes(1); + expect(mockImporter.resolveImportErrors).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts index 5ab15a1f52e755..d654fab427f198 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { setTimeout } from 'timers/promises'; + import type { SavedObject, SavedObjectsBulkCreateObject, @@ -13,7 +15,6 @@ import type { Logger, } from 'src/core/server'; import type { SavedObjectsImportSuccess, SavedObjectsImportFailure } from 'src/core/server/types'; - import { createListStream } from '@kbn/utils'; import { partition } from 'lodash'; @@ -166,7 +167,40 @@ export async function getKibanaAssets( return result; } -async function installKibanaSavedObjects({ +const isImportConflictError = (e: SavedObjectsImportFailure) => e?.error?.type === 'conflict'; +/** + * retry saved object import if only conflict errors are encountered + */ +async function retryImportOnConflictError( + importCall: () => ReturnType, + { + logger, + maxAttempts = 50, + _attempt = 0, + }: { logger?: Logger; _attempt?: number; maxAttempts?: number } = {} +): ReturnType { + const result = await importCall(); + + const errors = result.errors ?? []; + if (_attempt < maxAttempts && errors.length && errors.every(isImportConflictError)) { + const retryCount = _attempt + 1; + const retryDelayMs = 1000 + Math.floor(Math.random() * 3000); // 1s + 0-3s of jitter + + logger?.debug( + `Retrying import operation after [${ + retryDelayMs * 1000 + }s] due to conflict errors: ${JSON.stringify(errors)}` + ); + + await setTimeout(retryDelayMs); + return retryImportOnConflictError(importCall, { logger, _attempt: retryCount }); + } + + return result; +} + +// only exported for testing +export async function installKibanaSavedObjects({ savedObjectsImporter, kibanaAssets, logger, @@ -185,18 +219,19 @@ async function installKibanaSavedObjects({ return []; } else { const { successResults: importSuccessResults = [], errors: importErrors = [] } = - await savedObjectsImporter.import({ - overwrite: true, - readStream: createListStream(toBeSavedObjects), - createNewCopies: false, - }); + await retryImportOnConflictError(() => + savedObjectsImporter.import({ + overwrite: true, + readStream: createListStream(toBeSavedObjects), + createNewCopies: false, + }) + ); allSuccessResults = importSuccessResults; const [referenceErrors, otherErrors] = partition( importErrors, (e) => e?.error?.type === 'missing_references' ); - if (otherErrors?.length) { throw new Error( `Encountered ${ diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx index 482a5b931ed78a..f44aef76ab83d4 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx @@ -11,6 +11,7 @@ import type { Action, UiActionsStart } from 'src/plugins/ui_actions/public'; import type { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { EuiLoadingChart } from '@elastic/eui'; import { + EmbeddableFactory, EmbeddableInput, EmbeddableOutput, EmbeddablePanel, @@ -69,41 +70,48 @@ interface PluginsStartDependencies { } export function getEmbeddableComponent(core: CoreStart, plugins: PluginsStartDependencies) { + const { embeddable: embeddableStart, uiActions, inspector } = plugins; + const factory = embeddableStart.getEmbeddableFactory('lens')!; + const theme = core.theme; return (props: EmbeddableComponentProps) => { - const { embeddable: embeddableStart, uiActions, inspector } = plugins; - const factory = embeddableStart.getEmbeddableFactory('lens')!; const input = { ...props }; - const [embeddable, loading, error] = useEmbeddableFactory({ factory, input }); const hasActions = - Boolean(props.withDefaultActions) || (props.extraActions && props.extraActions?.length > 0); + Boolean(input.withDefaultActions) || (input.extraActions && input.extraActions?.length > 0); - const theme = core.theme; - - if (loading) { - return ; - } - - if (embeddable && hasActions) { + if (hasActions) { return ( } + factory={factory} uiActions={uiActions} inspector={inspector} actionPredicate={() => hasActions} input={input} theme={theme} - extraActions={props.extraActions} - withDefaultActions={props.withDefaultActions} + extraActions={input.extraActions} + withDefaultActions={input.withDefaultActions} /> ); } - - return ; + return ; }; } +function EmbeddableRootWrapper({ + factory, + input, +}: { + factory: EmbeddableFactory; + input: EmbeddableComponentProps; +}) { + const [embeddable, loading, error] = useEmbeddableFactory({ factory, input }); + if (loading) { + return ; + } + return ; +} + interface EmbeddablePanelWrapperProps { - embeddable: IEmbeddable; + factory: EmbeddableFactory; uiActions: PluginsStartDependencies['uiActions']; inspector: PluginsStartDependencies['inspector']; actionPredicate: (id: string) => boolean; @@ -114,7 +122,7 @@ interface EmbeddablePanelWrapperProps { } const EmbeddablePanelWrapper: FC = ({ - embeddable, + factory, uiActions, actionPredicate, inspector, @@ -123,10 +131,17 @@ const EmbeddablePanelWrapper: FC = ({ extraActions, withDefaultActions, }) => { + const [embeddable, loading] = useEmbeddableFactory({ factory, input }); useEffect(() => { - embeddable.updateInput(input); + if (embeddable) { + embeddable.updateInput(input); + } }, [embeddable, input]); + if (loading || !embeddable) { + return ; + } + return ( ) { return async ( dispatch: ThunkDispatch, getState: () => MapStoreState @@ -369,7 +375,10 @@ export function addNewFeatureToIndex(geometry: Geometry | Position[]) { } try { - await (layer as IVectorLayer).addFeature(geometry); + dispatch(updateEditShape(DRAW_SHAPE.WAIT)); + await asyncForEach(geometries, async (geometry) => { + await (layer as IVectorLayer).addFeature(geometry); + }); await dispatch(syncDataForLayerDueToDrawing(layer)); } catch (e) { getToasts().addError(e, { @@ -378,6 +387,7 @@ export function addNewFeatureToIndex(geometry: Geometry | Position[]) { }), }); } + dispatch(updateEditShape(DRAW_SHAPE.SIMPLE_SELECT)); }; } @@ -386,6 +396,12 @@ export function deleteFeatureFromIndex(featureId: string) { dispatch: ThunkDispatch, getState: () => MapStoreState ) => { + // There is a race condition where users can click on a previously deleted feature before layer has re-rendered after feature delete. + // Check ensures delete requests for previously deleted features are aborted. + if (getDeletedFeatureIds(getState()).includes(featureId)) { + return; + } + const editState = getEditState(getState()); const layerId = editState ? editState.layerId : undefined; if (!layerId) { @@ -395,8 +411,11 @@ export function deleteFeatureFromIndex(featureId: string) { if (!layer || !isVectorLayer(layer)) { return; } + try { + dispatch(updateEditShape(DRAW_SHAPE.WAIT)); await (layer as IVectorLayer).deleteFeature(featureId); + dispatch(pushDeletedFeatureId(featureId)); await dispatch(syncDataForLayerDueToDrawing(layer)); } catch (e) { getToasts().addError(e, { @@ -405,5 +424,6 @@ export function deleteFeatureFromIndex(featureId: string) { }), }); } + dispatch(updateEditShape(DRAW_SHAPE.DELETE)); }; } diff --git a/x-pack/plugins/maps/public/actions/ui_actions.ts b/x-pack/plugins/maps/public/actions/ui_actions.ts index 70e24283ef48f7..1ffcf416f6f8fa 100644 --- a/x-pack/plugins/maps/public/actions/ui_actions.ts +++ b/x-pack/plugins/maps/public/actions/ui_actions.ts @@ -24,6 +24,8 @@ export const SET_OPEN_TOC_DETAILS = 'SET_OPEN_TOC_DETAILS'; export const SHOW_TOC_DETAILS = 'SHOW_TOC_DETAILS'; export const HIDE_TOC_DETAILS = 'HIDE_TOC_DETAILS'; export const SET_DRAW_MODE = 'SET_DRAW_MODE'; +export const PUSH_DELETED_FEATURE_ID = 'PUSH_DELETED_FEATURE_ID'; +export const CLEAR_DELETED_FEATURE_IDS = 'CLEAR_DELETED_FEATURE_IDS'; export function exitFullScreen() { return { @@ -123,3 +125,16 @@ export function closeTimeslider() { dispatch(setQuery({ clearTimeslice: true })); }; } + +export function pushDeletedFeatureId(featureId: string) { + return { + type: PUSH_DELETED_FEATURE_ID, + featureId, + }; +} + +export function clearDeletedFeatureIds() { + return { + type: CLEAR_DELETED_FEATURE_IDS, + }; +} diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx index a404db91a942ef..8cbfcd3a41e80e 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx @@ -20,15 +20,6 @@ import { DRAW_SHAPE } from '../../../../common/constants'; import { DrawCircle, DRAW_CIRCLE_RADIUS_LABEL_STYLE } from './draw_circle'; import { DrawTooltip } from './draw_tooltip'; -const mbModeEquivalencies = new Map([ - ['simple_select', DRAW_SHAPE.SIMPLE_SELECT], - ['draw_rectangle', DRAW_SHAPE.BOUNDS], - ['draw_circle', DRAW_SHAPE.DISTANCE], - ['draw_polygon', DRAW_SHAPE.POLYGON], - ['draw_line_string', DRAW_SHAPE.LINE], - ['draw_point', DRAW_SHAPE.POINT], -]); - const DRAW_RECTANGLE = 'draw_rectangle'; const DRAW_CIRCLE = 'draw_circle'; const mbDrawModes = MapboxDraw.modes; @@ -41,7 +32,6 @@ export interface Props { onClick?: (event: MapMouseEvent, drawControl?: MapboxDraw) => void; mbMap: MbMap; enable: boolean; - updateEditShape: (shapeToDraw: DRAW_SHAPE) => void; } export class DrawControl extends Component { @@ -91,12 +81,6 @@ export class DrawControl extends Component { } }, 0); - _onModeChange = ({ mode }: { mode: string }) => { - if (mbModeEquivalencies.has(mode)) { - this.props.updateEditShape(mbModeEquivalencies.get(mode)!); - } - }; - _removeDrawControl() { // Do not remove draw control after mbMap.remove is called, causes execeptions and mbMap.remove cleans up all map resources. const isMapRemoved = !this.props.mbMap.loaded(); @@ -105,7 +89,6 @@ export class DrawControl extends Component { } this.props.mbMap.getCanvas().style.cursor = ''; - this.props.mbMap.off('draw.modechange', this._onModeChange); this.props.mbMap.off('draw.create', this._onDraw); if (this.props.onClick) { this.props.mbMap.off('click', this._onClick); @@ -118,7 +101,6 @@ export class DrawControl extends Component { if (!this._mbDrawControlAdded) { this.props.mbMap.addControl(this._mbDrawControl); this._mbDrawControlAdded = true; - this.props.mbMap.on('draw.modechange', this._onModeChange); this.props.mbMap.on('draw.create', this._onDraw); if (this.props.onClick) { @@ -144,6 +126,9 @@ export class DrawControl extends Component { this._mbDrawControl.changeMode(DRAW_POINT); } else if (this.props.drawShape === DRAW_SHAPE.DELETE) { this._mbDrawControl.changeMode(SIMPLE_SELECT); + } else if (this.props.drawShape === DRAW_SHAPE.WAIT) { + this.props.mbMap.getCanvas().style.cursor = 'wait'; + this._mbDrawControl.changeMode(SIMPLE_SELECT); } else { this._mbDrawControl.changeMode(SIMPLE_SELECT); } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx index 6c7fe9f0ad213a..b6ffacc491030e 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import * as jsts from 'jsts'; import { MapMouseEvent } from '@kbn/mapbox-gl'; import { getToasts } from '../../../../kibana_services'; -import { DrawControl } from '../'; +import { DrawControl } from '../draw_control'; import { DRAW_MODE, DRAW_SHAPE } from '../../../../../common/constants'; import { ILayer } from '../../../../classes/layers/layer'; import { EXCLUDE_CENTROID_FEATURES } from '../../../../classes/util/mb_filter_expressions'; @@ -29,9 +29,8 @@ export interface ReduxStateProps { } export interface ReduxDispatchProps { - addNewFeatureToIndex: (geometry: Geometry | Position[]) => void; + addNewFeatureToIndex: (geometries: Array) => void; deleteFeatureFromIndex: (featureId: string) => void; - disableDrawState: () => void; } export interface OwnProps { @@ -43,6 +42,7 @@ type Props = ReduxStateProps & ReduxDispatchProps & OwnProps; export class DrawFeatureControl extends Component { _onDraw = async (e: { features: Feature[] }, mbDrawControl: MapboxDraw) => { try { + const geometries: Array = []; e.features.forEach((feature: Feature) => { const { geometry } = geoJSONReader.read(feature); if (!geometry.isSimple() || !geometry.isValid()) { @@ -58,9 +58,13 @@ export class DrawFeatureControl extends Component { this.props.drawMode === DRAW_MODE.DRAW_POINTS ? feature.geometry.coordinates : feature.geometry; - this.props.addNewFeatureToIndex(featureGeom); + geometries.push(featureGeom); } }); + + if (geometries.length) { + this.props.addNewFeatureToIndex(geometries); + } } catch (error) { getToasts().addWarning( i18n.translate('xpack.maps.drawFeatureControl.unableToCreateFeature', { @@ -71,7 +75,6 @@ export class DrawFeatureControl extends Component { }) ); } finally { - this.props.disableDrawState(); try { mbDrawControl.deleteAll(); } catch (_e) { @@ -86,6 +89,7 @@ export class DrawFeatureControl extends Component { if (!this.props.editLayer || this.props.drawShape !== DRAW_SHAPE.DELETE) { return; } + const mbEditLayerIds = this.props.editLayer .getMbLayerIds() .filter((mbLayerId) => !!this.props.mbMap.getLayer(mbLayerId)); diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts index e1d703173fc2da..d2c369b4bd50a2 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts @@ -15,7 +15,7 @@ import { ReduxStateProps, OwnProps, } from './draw_feature_control'; -import { addNewFeatureToIndex, deleteFeatureFromIndex, updateEditShape } from '../../../../actions'; +import { addNewFeatureToIndex, deleteFeatureFromIndex } from '../../../../actions'; import { MapStoreState } from '../../../../reducers/store'; import { getEditState, getLayerById } from '../../../../selectors/map_selectors'; import { getDrawMode } from '../../../../selectors/ui_selectors'; @@ -34,15 +34,12 @@ function mapDispatchToProps( dispatch: ThunkDispatch ): ReduxDispatchProps { return { - addNewFeatureToIndex(geometry: Geometry | Position[]) { - dispatch(addNewFeatureToIndex(geometry)); + addNewFeatureToIndex(geometries: Array) { + dispatch(addNewFeatureToIndex(geometries)); }, deleteFeatureFromIndex(featureId: string) { dispatch(deleteFeatureFromIndex(featureId)); }, - disableDrawState() { - dispatch(updateEditShape(null)); - }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx index 2f652506857d2a..98d88d43fc65f9 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx @@ -20,7 +20,7 @@ import { roundCoordinates, } from '../../../../../common/elasticsearch_util'; import { getToasts } from '../../../../kibana_services'; -import { DrawControl } from '../'; +import { DrawControl } from '../draw_control'; import { DrawCircleProperties } from '../draw_circle'; export interface Props { diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts deleted file mode 100644 index b0f1941caec08c..00000000000000 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts +++ /dev/null @@ -1,25 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ThunkDispatch } from 'redux-thunk'; -import { AnyAction } from 'redux'; -import { connect } from 'react-redux'; -import { updateEditShape } from '../../../actions'; -import { MapStoreState } from '../../../reducers/store'; -import { DrawControl } from './draw_control'; -import { DRAW_SHAPE } from '../../../../common/constants'; - -function mapDispatchToProps(dispatch: ThunkDispatch) { - return { - updateEditShape(shapeToDraw: DRAW_SHAPE) { - dispatch(updateEditShape(shapeToDraw)); - }, - }; -} - -const connected = connect(null, mapDispatchToProps)(DrawControl); -export { connected as DrawControl }; diff --git a/x-pack/plugins/maps/public/reducers/ui.ts b/x-pack/plugins/maps/public/reducers/ui.ts index f3f948bb96508a..f0f22c5a8c4a93 100644 --- a/x-pack/plugins/maps/public/reducers/ui.ts +++ b/x-pack/plugins/maps/public/reducers/ui.ts @@ -19,6 +19,8 @@ import { SHOW_TOC_DETAILS, HIDE_TOC_DETAILS, SET_DRAW_MODE, + PUSH_DELETED_FEATURE_ID, + CLEAR_DELETED_FEATURE_IDS, } from '../actions'; import { DRAW_MODE } from '../../common/constants'; @@ -37,6 +39,7 @@ export type MapUiState = { isLayerTOCOpen: boolean; isTimesliderOpen: boolean; openTOCDetails: string[]; + deletedFeatureIds: string[]; }; export const DEFAULT_IS_LAYER_TOC_OPEN = true; @@ -51,6 +54,7 @@ export const DEFAULT_MAP_UI_STATE = { // storing TOC detail visibility outside of map.layerList because its UI state and not map rendering state. // This also makes for easy read/write access for embeddables. openTOCDetails: [], + deletedFeatureIds: [], }; // Reducer @@ -82,6 +86,16 @@ export function ui(state: MapUiState = DEFAULT_MAP_UI_STATE, action: any) { return layerId !== action.layerId; }), }; + case PUSH_DELETED_FEATURE_ID: + return { + ...state, + deletedFeatureIds: [...state.deletedFeatureIds, action.featureId], + }; + case CLEAR_DELETED_FEATURE_IDS: + return { + ...state, + deletedFeatureIds: [], + }; default: return state; } diff --git a/x-pack/plugins/maps/public/selectors/ui_selectors.ts b/x-pack/plugins/maps/public/selectors/ui_selectors.ts index 942a5190691a15..6bdf5a35679a73 100644 --- a/x-pack/plugins/maps/public/selectors/ui_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/ui_selectors.ts @@ -17,3 +17,4 @@ export const getIsTimesliderOpen = ({ ui }: MapStoreState): boolean => ui.isTime export const getOpenTOCDetails = ({ ui }: MapStoreState): string[] => ui.openTOCDetails; export const getIsFullScreen = ({ ui }: MapStoreState): boolean => ui.isFullScreen; export const getIsReadOnly = ({ ui }: MapStoreState): boolean => ui.isReadOnly; +export const getDeletedFeatureIds = ({ ui }: MapStoreState): string[] => ui.deletedFeatureIds; diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 3df5016f560c07..4a621bc5f608b5 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -124,6 +124,17 @@ export class DataRecognizer { private _resultsService: ReturnType; private _calculateModelMemoryLimit: ReturnType; + /** + * A temporary cache of configs loaded from disk and from save object service. + * The configs from disk will not change while kibana is running. + * The configs from saved objects could potentially change while an instance of + * DataRecognizer exists, if a fleet package containing modules is installed. + * However the chance of this happening is very low and so the benefit of using + * this cache outweighs the risk of the cache being out of date during the short + * existence of a DataRecognizer instance. + */ + private _configCache: Config[] | null = null; + /** * List of the module jobs that require model memory estimation */ @@ -181,6 +192,10 @@ export class DataRecognizer { } private async _loadConfigs(): Promise { + if (this._configCache !== null) { + return this._configCache; + } + const configs: Config[] = []; const dirs = await this._listDirs(this._modulesDir); await Promise.all( @@ -211,7 +226,9 @@ export class DataRecognizer { isSavedObject: true, })); - return [...configs, ...savedObjectConfigs]; + this._configCache = [...configs, ...savedObjectConfigs]; + + return this._configCache; } private async _loadSavedObjectModules() { diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 2ab10bda361909..1fa7217e7d252d 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -609,12 +609,12 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout /** * @apiGroup DataFrameAnalytics * - * @api {post} /api/ml/data_frame/analytics/job_exists Check whether jobs exists in current or any space - * @apiName JobExists - * @apiDescription Checks if each of the jobs in the specified list of IDs exist. + * @api {post} /api/ml/data_frame/analytics/jobs_exist Check whether jobs exist in current or any space + * @apiName JobsExist + * @apiDescription Checks if each of the jobs in the specified list of IDs exists. * If allSpaces is true, the check will look across all spaces. * - * @apiSchema (params) analyticsIdSchema + * @apiSchema (params) jobsExistSchema */ router.post( { @@ -707,7 +707,7 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout /** * @apiGroup DataFrameAnalytics * - * @api {get} api/data_frame/analytics/fields/:indexPattern Get fields for a pattern of indices used for analytics + * @api {get} /api/ml/data_frame/analytics/new_job_caps/:indexPattern Get fields for a pattern of indices used for analytics * @apiName AnalyticsNewJobCaps * @apiDescription Retrieve the index fields for analytics */ diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/enterprise_search_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/enterprise_search_panel.js index 1459ccb2ecac63..b8ff05213c1c03 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/enterprise_search_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/enterprise_search_panel.js @@ -31,6 +31,12 @@ import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link' export function EnterpriseSearchPanel(props) { const { setupMode } = props; const setupModeData = get(setupMode.data, 'enterprise_search'); + const nodesCount = props.stats.totalInstances || 0; + + // Do not show if we are not in setup mode + if (!nodesCount && !setupMode.enabled) { + return null; + } return ( diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts index 7096647854c157..76cc9adeb43ecd 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts @@ -90,7 +90,6 @@ export function getSettingsCollector( ) { return usageCollection.makeStatsCollector< EmailSettingData | undefined, - false, KibanaSettingsCollectorExtraOptions >({ type: 'kibana_settings', diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts index cbbfe64f5e3e22..0c952949c56b49 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts @@ -18,7 +18,7 @@ export function getMonitoringUsageCollector( config: MonitoringConfig, getClient: () => IClusterClient ) { - return usageCollection.makeUsageCollector({ + return usageCollection.makeUsageCollector({ type: 'monitoring', isReady: () => true, schema: { @@ -95,13 +95,8 @@ export function getMonitoringUsageCollector( }, }, }, - extendFetchContext: { - kibanaRequest: true, - }, - fetch: async ({ kibanaRequest }) => { - const callCluster = kibanaRequest - ? getClient().asScoped(kibanaRequest).asCurrentUser - : getClient().asInternalUser; + fetch: async () => { + const callCluster = getClient().asInternalUser; const usageClusters: MonitoringClusterStackProductUsage[] = []; const availableCcs = config.ui.ccs.enabled; const clusters = await fetchClusters(callCluster); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts b/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts index bce6f57d6f950a..344b04fb4780d4 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts @@ -34,13 +34,9 @@ export function registerMonitoringTelemetryCollection( getClient: () => IClusterClient, maxBucketSize: number ) { - const monitoringStatsCollector = usageCollection.makeStatsCollector< - MonitoringTelemetryUsage, - true - >({ + const monitoringStatsCollector = usageCollection.makeStatsCollector({ type: 'monitoringTelemetry', isReady: () => true, - extendFetchContext: { kibanaRequest: true }, schema: { stats: { type: 'array', @@ -137,13 +133,13 @@ export function registerMonitoringTelemetryCollection( }, }, }, - fetch: async ({ kibanaRequest, esClient }) => { + fetch: async () => { const timestamp = Date.now(); // Collect the telemetry from the monitoring indices for this moment. // NOTE: Usually, the monitoring indices index stats for each product every 10s (by default). // However, some data may be delayed up-to 24h because monitoring only collects extended Kibana stats in that interval // to avoid overloading of the system when retrieving data from the collectors (that delay is dealt with in the Kibana Stats getter inside the `getAllStats` method). // By 8.x, we expect to stop collecting the Kibana extended stats and keep only the monitoring-related metrics. - const callCluster = kibanaRequest ? esClient : getClient().asInternalUser; + const callCluster = getClient().asInternalUser; const clusterDetails = await getClusterUuids(callCluster, timestamp, maxBucketSize); const [licenses, stats] = await Promise.all([ getLicenses(clusterDetails, callCluster, maxBucketSize), diff --git a/x-pack/plugins/reporting/server/config/config.ts b/x-pack/plugins/reporting/server/config/config.ts index 00c57053653f75..269a66503a741f 100644 --- a/x-pack/plugins/reporting/server/config/config.ts +++ b/x-pack/plugins/reporting/server/config/config.ts @@ -7,8 +7,7 @@ import { get } from 'lodash'; import { first } from 'rxjs/operators'; -import { CoreSetup, PluginInitializerContext } from 'src/core/server'; -import { LevelLogger } from '../lib'; +import type { CoreSetup, Logger, PluginInitializerContext } from 'kibana/server'; import { createConfig$ } from './create_config'; import { ReportingConfigType } from './schema'; @@ -63,13 +62,13 @@ export interface ReportingConfig extends Config { * @internal * @param {PluginInitializerContext} initContext * @param {CoreSetup} core - * @param {LevelLogger} logger + * @param {Logger} logger * @returns {Promise} */ export const buildConfig = async ( initContext: PluginInitializerContext, core: CoreSetup, - logger: LevelLogger + logger: Logger ): Promise => { const config$ = initContext.config.create(); const { http } = core; diff --git a/x-pack/plugins/reporting/server/config/create_config.test.ts b/x-pack/plugins/reporting/server/config/create_config.test.ts index fd8180bd46a05d..f839d72e1a45df 100644 --- a/x-pack/plugins/reporting/server/config/create_config.test.ts +++ b/x-pack/plugins/reporting/server/config/create_config.test.ts @@ -6,11 +6,10 @@ */ import * as Rx from 'rxjs'; -import { CoreSetup, HttpServerInfo, PluginInitializerContext } from 'src/core/server'; -import { coreMock } from 'src/core/server/mocks'; -import { LevelLogger } from '../lib/level_logger'; -import { createMockConfigSchema, createMockLevelLogger } from '../test_helpers'; -import { ReportingConfigType } from './'; +import type { CoreSetup, HttpServerInfo, Logger, PluginInitializerContext } from 'kibana/server'; +import { coreMock, loggingSystemMock } from 'src/core/server/mocks'; +import { createMockConfigSchema } from '../test_helpers'; +import type { ReportingConfigType } from './'; import { createConfig$ } from './create_config'; const createMockConfig = ( @@ -20,14 +19,14 @@ const createMockConfig = ( describe('Reporting server createConfig$', () => { let mockCoreSetup: CoreSetup; let mockInitContext: PluginInitializerContext; - let mockLogger: jest.Mocked; + let mockLogger: jest.Mocked; beforeEach(() => { mockCoreSetup = coreMock.createSetup(); mockInitContext = coreMock.createPluginInitializerContext( createMockConfigSchema({ kibanaServer: {} }) ); - mockLogger = createMockLevelLogger(); + mockLogger = loggingSystemMock.createLogger(); }); afterEach(() => { diff --git a/x-pack/plugins/reporting/server/config/create_config.ts b/x-pack/plugins/reporting/server/config/create_config.ts index 2ac225ec4576a8..ff8d00c30d4f83 100644 --- a/x-pack/plugins/reporting/server/config/create_config.ts +++ b/x-pack/plugins/reporting/server/config/create_config.ts @@ -7,11 +7,10 @@ import crypto from 'crypto'; import ipaddr from 'ipaddr.js'; +import type { CoreSetup, Logger } from 'kibana/server'; import { sum } from 'lodash'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { CoreSetup } from 'src/core/server'; -import { LevelLogger } from '../lib'; import { ReportingConfigType } from './schema'; /* @@ -22,9 +21,9 @@ import { ReportingConfigType } from './schema'; export function createConfig$( core: CoreSetup, config$: Observable, - parentLogger: LevelLogger + parentLogger: Logger ) { - const logger = parentLogger.clone(['config']); + const logger = parentLogger.get('config'); return config$.pipe( map((config) => { // encryption key diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index 745542c358a69b..a4e4f43f90e1ee 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -11,6 +11,7 @@ import { filter, first, map, switchMap, take } from 'rxjs/operators'; import type { BasePath, IClusterClient, + Logger, PackageInfo, PluginInitializerContext, SavedObjectsClientContract, @@ -32,7 +33,7 @@ import { REPORTING_REDIRECT_LOCATOR_STORE_KEY } from '../common/constants'; import { durationToNumber } from '../common/schema_utils'; import type { ReportingConfig, ReportingSetup } from './'; import { ReportingConfigType } from './config'; -import { checkLicense, getExportTypesRegistry, LevelLogger } from './lib'; +import { checkLicense, getExportTypesRegistry } from './lib'; import { reportingEventLoggerFactory } from './lib/event_logger/logger'; import type { IReport, ReportingStore } from './lib/store'; import { ExecuteReportTask, MonitorReportsTask, ReportTaskParams } from './lib/tasks'; @@ -45,7 +46,7 @@ export interface ReportingInternalSetup { security?: SecurityPluginSetup; spaces?: SpacesPluginSetup; taskManager: TaskManagerSetupContract; - logger: LevelLogger; + logger: Logger; status: StatusServiceSetup; } @@ -57,7 +58,7 @@ export interface ReportingInternalStart { data: DataPluginStart; fieldFormats: FieldFormatsStart; licensing: LicensingPluginStart; - logger: LevelLogger; + logger: Logger; screenshotting: ScreenshottingStart; security?: SecurityPluginStart; taskManager: TaskManagerStartContract; @@ -81,7 +82,7 @@ export class ReportingCore { public getContract: () => ReportingSetup; - constructor(private logger: LevelLogger, context: PluginInitializerContext) { + constructor(private logger: Logger, context: PluginInitializerContext) { this.packageInfo = context.env.packageInfo; const syncConfig = context.config.get(); this.deprecatedAllowedRoles = syncConfig.roles.enabled ? syncConfig.roles.allow : false; diff --git a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts index b5258d91485f70..56a1c39e75aa46 100644 --- a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts @@ -5,11 +5,11 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { cryptoFactory } from '../../lib'; -import { createMockLevelLogger } from '../../test_helpers'; import { decryptJobHeaders } from './'; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); const encryptHeaders = async (encryptionKey: string, headers: Record) => { const crypto = cryptoFactory(encryptionKey); diff --git a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts index f126d1edbfce3a..3dfcfe362abd49 100644 --- a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts @@ -6,12 +6,13 @@ */ import { i18n } from '@kbn/i18n'; -import { cryptoFactory, LevelLogger } from '../../lib'; +import type { Logger } from 'kibana/server'; +import { cryptoFactory } from '../../lib'; export const decryptJobHeaders = async ( encryptionKey: string | undefined, headers: string, - logger: LevelLogger + logger: Logger ): Promise> => { try { if (typeof headers !== 'string') { diff --git a/x-pack/plugins/reporting/server/export_types/common/generate_png.ts b/x-pack/plugins/reporting/server/export_types/common/generate_png.ts index caa0b7fb91b3f2..272d1c287178ad 100644 --- a/x-pack/plugins/reporting/server/export_types/common/generate_png.ts +++ b/x-pack/plugins/reporting/server/export_types/common/generate_png.ts @@ -6,14 +6,14 @@ */ import apm from 'elastic-apm-node'; +import type { Logger } from 'kibana/server'; import * as Rx from 'rxjs'; import { finalize, map, tap } from 'rxjs/operators'; +import type { ReportingCore } from '../../'; import { LayoutTypes } from '../../../../screenshotting/common'; import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; import type { PngMetrics } from '../../../common/types'; -import { ReportingCore } from '../../'; -import { ScreenshotOptions } from '../../types'; -import { LevelLogger } from '../../lib'; +import type { ScreenshotOptions } from '../../types'; interface PngResult { buffer: Buffer; @@ -23,7 +23,7 @@ interface PngResult { export function generatePngObservable( reporting: ReportingCore, - logger: LevelLogger, + logger: Logger, options: ScreenshotOptions ): Rx.Observable { const apmTrans = apm.startTransaction('generate-png', REPORTING_TRANSACTION_TYPE); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts index f5675b50cfddd4..850d0ae507e126 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts @@ -5,17 +5,14 @@ * 2.0. */ -import { ReportingCore } from '../..'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { ReportingCore } from '../../'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { getCustomLogo } from './get_custom_logo'; let mockReportingPlugin: ReportingCore; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); beforeEach(async () => { mockReportingPlugin = await createMockReportingCore(createMockConfigSchema()); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts index fcabd34a642c8c..10873155039885 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts @@ -5,16 +5,15 @@ * 2.0. */ -import type { Headers } from 'src/core/server'; +import type { Headers, Logger } from 'kibana/server'; import { ReportingCore } from '../../'; import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../common/constants'; -import { LevelLogger } from '../../lib'; export const getCustomLogo = async ( reporting: ReportingCore, headers: Headers, spaceId: string | undefined, - logger: LevelLogger + logger: Logger ) => { const fakeRequest = reporting.getFakeRequest({ headers }, spaceId, logger); const uiSettingsClient = await reporting.getUiSettingsClient(fakeRequest, logger); diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts index ee6d6daab88e06..5a8c4f1fd760c0 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts @@ -16,18 +16,15 @@ jest.mock('./generate_csv/generate_csv', () => ({ }, })); -import { Writable } from 'stream'; import nodeCrypto from '@elastic/node-crypto'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { Writable } from 'stream'; import { ReportingCore } from '../../'; import { CancellationToken } from '../../../common/cancellation_token'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { runTaskFnFactory } from './execute_job'; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); const encryptionKey = 'tetkey'; const headers = { sid: 'cooltestheaders' }; let encryptedHeaders: string; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts index 97f0aa65e3d68e..8b5f0e5395827b 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { CSV_JOB_TYPE } from '../../../common/constants'; import { getFieldFormats } from '../../services'; import { RunTaskFn, RunTaskFnFactory } from '../../types'; import { decryptJobHeaders } from '../common'; @@ -19,7 +18,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = ( const config = reporting.getConfig(); return async function runTask(jobId, job, cancellationToken, stream) { - const logger = parentLogger.clone([CSV_JOB_TYPE, 'execute-job', jobId]); + const logger = parentLogger.get(`execute-job:${jobId}`); const encryptionKey = config.get('encryptionKey'); const headers = await decryptJobHeaders(encryptionKey, job.headers, logger); diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts index c525cb7c0def2d..4755d153666e40 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts @@ -5,21 +5,22 @@ * 2.0. */ -import { Writable } from 'stream'; -import * as Rx from 'rxjs'; import { errors as esErrors } from '@elastic/elasticsearch'; +import type { IScopedClusterClient, IUiSettingsClient, SearchResponse } from 'kibana/server'; import { identity, range } from 'lodash'; -import { IScopedClusterClient, IUiSettingsClient, SearchResponse } from 'src/core/server'; +import * as Rx from 'rxjs'; import { elasticsearchServiceMock, + loggingSystemMock, savedObjectsClientMock, uiSettingsServiceMock, } from 'src/core/server/mocks'; import { ISearchStartSearchSource } from 'src/plugins/data/common'; -import { FieldFormatsRegistry } from 'src/plugins/field_formats/common'; import { searchSourceInstanceMock } from 'src/plugins/data/common/search/search_source/mocks'; import { IScopedSearchClient } from 'src/plugins/data/server'; import { dataPluginMock } from 'src/plugins/data/server/mocks'; +import { FieldFormatsRegistry } from 'src/plugins/field_formats/common'; +import { Writable } from 'stream'; import { ReportingConfig } from '../../../'; import { CancellationToken } from '../../../../common/cancellation_token'; import { @@ -28,11 +29,7 @@ import { UI_SETTINGS_DATEFORMAT_TZ, } from '../../../../common/constants'; import { UnknownError } from '../../../../common/errors'; -import { - createMockConfig, - createMockConfigSchema, - createMockLevelLogger, -} from '../../../test_helpers'; +import { createMockConfig, createMockConfigSchema } from '../../../test_helpers'; import { JobParamsCSV } from '../types'; import { CsvGenerator } from './generate_csv'; @@ -125,7 +122,7 @@ beforeEach(async () => { }); }); -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); it('formats an empty search result to CSV content', async () => { const generateCsv = new CsvGenerator( diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts index 201484af9d7d0a..c913706f585624 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { errors as esErrors } from '@elastic/elasticsearch'; -import type { IScopedClusterClient, IUiSettingsClient } from 'src/core/server'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { IScopedClusterClient, IUiSettingsClient, Logger } from 'kibana/server'; import type { IScopedSearchClient } from 'src/plugins/data/server'; import type { Datatable } from 'src/plugins/expressions/server'; import type { Writable } from 'stream'; @@ -32,16 +32,15 @@ import type { CancellationToken } from '../../../../common/cancellation_token'; import { CONTENT_TYPE_CSV } from '../../../../common/constants'; import { AuthenticationExpiredError, - UnknownError, ReportingError, + UnknownError, } from '../../../../common/errors'; import { byteSizeValueToNumber } from '../../../../common/schema_utils'; -import type { LevelLogger } from '../../../lib'; import type { TaskRunResult } from '../../../lib/tasks'; import type { JobParamsCSV } from '../types'; import { CsvExportSettings, getExportSettings } from './get_export_settings'; -import { MaxSizeStringBuilder } from './max_size_string_builder'; import { i18nTexts } from './i18n_texts'; +import { MaxSizeStringBuilder } from './max_size_string_builder'; interface Clients { es: IScopedClusterClient; @@ -65,7 +64,7 @@ export class CsvGenerator { private clients: Clients, private dependencies: Dependencies, private cancellationToken: CancellationToken, - private logger: LevelLogger, + private logger: Logger, private stream: Writable ) {} @@ -316,7 +315,7 @@ export class CsvGenerator { } if (!results) { - this.logger.warning(`Search results are undefined!`); + this.logger.warn(`Search results are undefined!`); break; } @@ -396,7 +395,7 @@ export class CsvGenerator { this.logger.debug(`Finished generating. Row count: ${this.csvRowCount}.`); if (!this.maxSizeReached && this.csvRowCount !== totalRecords) { - this.logger.warning( + this.logger.warn( `ES scroll returned fewer total hits than expected! ` + `Search result total hits: ${totalRecords}. Row count: ${this.csvRowCount}.` ); diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts index 2ae3e5e712d313..ef0f0062bf19b6 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts @@ -12,18 +12,18 @@ import { UI_SETTINGS_SEARCH_INCLUDE_FROZEN, } from '../../../../common/constants'; import { IUiSettingsClient } from 'kibana/server'; -import { savedObjectsClientMock, uiSettingsServiceMock } from 'src/core/server/mocks'; import { - createMockConfig, - createMockConfigSchema, - createMockLevelLogger, -} from '../../../test_helpers'; + loggingSystemMock, + savedObjectsClientMock, + uiSettingsServiceMock, +} from 'src/core/server/mocks'; +import { createMockConfig, createMockConfigSchema } from '../../../test_helpers'; import { getExportSettings } from './get_export_settings'; describe('getExportSettings', () => { let uiSettingsClient: IUiSettingsClient; const config = createMockConfig(createMockConfigSchema({})); - const logger = createMockLevelLogger(); + const logger = loggingSystemMock.createLogger(); beforeEach(() => { uiSettingsClient = uiSettingsServiceMock diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts index 5b69e33624c5c2..6a07e3184eb48f 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts @@ -6,7 +6,7 @@ */ import { ByteSizeValue } from '@kbn/config-schema'; -import { IUiSettingsClient } from 'kibana/server'; +import type { IUiSettingsClient, Logger } from 'kibana/server'; import { createEscapeValue } from '../../../../../../../src/plugins/data/common'; import { ReportingConfig } from '../../../'; import { @@ -16,7 +16,6 @@ import { UI_SETTINGS_DATEFORMAT_TZ, UI_SETTINGS_SEARCH_INCLUDE_FROZEN, } from '../../../../common/constants'; -import { LevelLogger } from '../../../lib'; export interface CsvExportSettings { timezone: string; @@ -37,7 +36,7 @@ export const getExportSettings = async ( client: IUiSettingsClient, config: ReportingConfig, timezone: string | undefined, - logger: LevelLogger + logger: Logger ): Promise => { let setTimezone: string; if (timezone) { diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts index 53e1f6ba3c95bf..50ae2ab10f6e75 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts @@ -8,7 +8,6 @@ import { KibanaRequest } from 'src/core/server'; import { Writable } from 'stream'; import { CancellationToken } from '../../../common/cancellation_token'; -import { CSV_SEARCHSOURCE_IMMEDIATE_TYPE } from '../../../common/constants'; import { TaskRunResult } from '../../lib/tasks'; import { getFieldFormats } from '../../services'; import { ReportingRequestHandlerContext, RunTaskFnFactory } from '../../types'; @@ -32,7 +31,7 @@ export const runTaskFnFactory: RunTaskFnFactory = function e parentLogger ) { const config = reporting.getConfig(); - const logger = parentLogger.clone([CSV_SEARCHSOURCE_IMMEDIATE_TYPE, 'execute-job']); + const logger = parentLogger.get('execute-job'); return async function runTask(_jobId, immediateJobParams, context, stream, req) { const job = { @@ -82,7 +81,7 @@ export const runTaskFnFactory: RunTaskFnFactory = function e const { warnings } = result; if (warnings) { warnings.forEach((warning) => { - logger.warning(warning); + logger.warn(warning); }); } diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts index 9069ec63a88250..bc37978372ba63 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { Writable } from 'stream'; import * as Rx from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { Writable } from 'stream'; import { ReportingCore } from '../../../'; import { CancellationToken } from '../../../../common/cancellation_token'; -import { cryptoFactory, LevelLogger } from '../../../lib'; +import { cryptoFactory } from '../../../lib'; import { createMockConfig, createMockConfigSchema, @@ -29,14 +30,7 @@ const cancellationToken = { on: jest.fn(), } as unknown as CancellationToken; -const mockLoggerFactory = { - get: jest.fn().mockImplementation(() => ({ - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - })), -}; -const getMockLogger = () => new LevelLogger(mockLoggerFactory); +const getMockLogger = () => loggingSystemMock.createLogger(); const mockEncryptionKey = 'abcabcsecuresecret'; const encryptHeaders = async (headers: Record) => { diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts index 67d013740bedd3..52023e53b80b56 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; -import { PNG_JOB_TYPE, REPORTING_TRANSACTION_TYPE } from '../../../../common/constants'; +import { REPORTING_TRANSACTION_TYPE } from '../../../../common/constants'; import { TaskRunResult } from '../../../lib/tasks'; import { RunTaskFn, RunTaskFnFactory } from '../../../types'; import { decryptJobHeaders, getFullUrls, generatePngObservable } from '../../common'; @@ -24,7 +24,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup'); let apmGeneratePng: { end: () => void } | null | undefined; - const jobLogger = parentLogger.clone([PNG_JOB_TYPE, 'execute', jobId]); + const jobLogger = parentLogger.get(`execute:${jobId}`); const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)), mergeMap((headers) => { diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts index 1b1ad6878d78fb..1403873e8da4b8 100644 --- a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts @@ -6,11 +6,12 @@ */ import * as Rx from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { Writable } from 'stream'; import { ReportingCore } from '../../'; import { CancellationToken } from '../../../common/cancellation_token'; import { LocatorParams } from '../../../common/types'; -import { cryptoFactory, LevelLogger } from '../../lib'; +import { cryptoFactory } from '../../lib'; import { createMockConfig, createMockConfigSchema, @@ -30,14 +31,7 @@ const cancellationToken = { on: jest.fn(), } as unknown as CancellationToken; -const mockLoggerFactory = { - get: jest.fn().mockImplementation(() => ({ - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - })), -}; -const getMockLogger = () => new LevelLogger(mockLoggerFactory); +const getMockLogger = () => loggingSystemMock.createLogger(); const mockEncryptionKey = 'abcabcsecuresecret'; const encryptHeaders = async (headers: Record) => { diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts index 51044aa324a1aa..5df7a497adf6c7 100644 --- a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; -import { PNG_JOB_TYPE_V2, REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; +import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; import { TaskRunResult } from '../../lib/tasks'; import { RunTaskFn, RunTaskFnFactory } from '../../types'; import { decryptJobHeaders, generatePngObservable } from '../common'; @@ -25,7 +25,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup'); let apmGeneratePng: { end: () => void } | null | undefined; - const jobLogger = parentLogger.clone([PNG_JOB_TYPE_V2, 'execute', jobId]); + const jobLogger = parentLogger.get(`execute:${jobId}`); const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)), mergeMap((headers) => { diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts index a8d2027f2ba120..7faa13486b5a18 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts @@ -6,10 +6,11 @@ */ import * as Rx from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { Writable } from 'stream'; import { ReportingCore } from '../../../'; import { CancellationToken } from '../../../../common/cancellation_token'; -import { cryptoFactory, LevelLogger } from '../../../lib'; +import { cryptoFactory } from '../../../lib'; import { createMockConfigSchema, createMockReportingCore } from '../../../test_helpers'; import { generatePdfObservable } from '../lib/generate_pdf'; import { TaskPayloadPDF } from '../types'; @@ -25,14 +26,7 @@ const cancellationToken = { on: jest.fn(), } as unknown as CancellationToken; -const mockLoggerFactory = { - get: jest.fn().mockImplementation(() => ({ - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - })), -}; -const getMockLogger = () => new LevelLogger(mockLoggerFactory); +const getMockLogger = () => loggingSystemMock.createLogger(); const mockEncryptionKey = 'testencryptionkey'; const encryptHeaders = async (headers: Record) => { diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts index ab3793935e1d86..9b4db48ed66970 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; -import { PDF_JOB_TYPE, REPORTING_TRANSACTION_TYPE } from '../../../../common/constants'; +import { REPORTING_TRANSACTION_TYPE } from '../../../../common/constants'; import { TaskRunResult } from '../../../lib/tasks'; import { RunTaskFn, RunTaskFnFactory } from '../../../types'; import { decryptJobHeaders, getFullUrls, getCustomLogo } from '../../common'; @@ -21,7 +21,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = const encryptionKey = config.get('encryptionKey'); return async function runTask(jobId, job, cancellationToken, stream) { - const jobLogger = parentLogger.clone([PDF_JOB_TYPE, 'execute-job', jobId]); + const jobLogger = parentLogger.get(`execute-job:${jobId}`); const apmTrans = apm.startTransaction('execute-job-pdf', REPORTING_TRANSACTION_TYPE); const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup'); let apmGeneratePdf: { end: () => void } | null | undefined; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index a401f59b8f4bf0..ff0ef2cf39af4b 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -5,13 +5,13 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import { groupBy } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap, tap } from 'rxjs/operators'; +import { ReportingCore } from '../../../'; import { ScreenshotResult } from '../../../../../screenshotting/server'; import type { PdfMetrics } from '../../../../common/types'; -import { ReportingCore } from '../../../'; -import { LevelLogger } from '../../../lib'; import { ScreenshotOptions } from '../../../types'; import { PdfMaker } from '../../common/pdf'; import { getTracker } from './tracker'; @@ -34,7 +34,7 @@ interface PdfResult { export function generatePdfObservable( reporting: ReportingCore, - logger: LevelLogger, + logger: Logger, title: string, options: ScreenshotOptions, logo?: string diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts index 3cf7f82058563a..efad71a64a81d6 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts @@ -8,11 +8,12 @@ jest.mock('./lib/generate_pdf'); import * as Rx from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { Writable } from 'stream'; import { ReportingCore } from '../../'; import { CancellationToken } from '../../../common/cancellation_token'; import { LocatorParams } from '../../../common/types'; -import { cryptoFactory, LevelLogger } from '../../lib'; +import { cryptoFactory } from '../../lib'; import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { runTaskFnFactory } from './execute_job'; import { generatePdfObservable } from './lib/generate_pdf'; @@ -26,14 +27,7 @@ const cancellationToken = { on: jest.fn(), } as unknown as CancellationToken; -const mockLoggerFactory = { - get: jest.fn().mockImplementation(() => ({ - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - })), -}; -const getMockLogger = () => new LevelLogger(mockLoggerFactory); +const getMockLogger = () => loggingSystemMock.createLogger(); const mockEncryptionKey = 'testencryptionkey'; const encryptHeaders = async (headers: Record) => { diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts index 85684bca66b869..7f887707829cb1 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; -import { PDF_JOB_TYPE_V2, REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; +import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; import { TaskRunResult } from '../../lib/tasks'; import { RunTaskFn, RunTaskFnFactory } from '../../types'; import { decryptJobHeaders, getCustomLogo } from '../common'; @@ -21,7 +21,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = const encryptionKey = config.get('encryptionKey'); return async function runTask(jobId, job, cancellationToken, stream) { - const jobLogger = parentLogger.clone([PDF_JOB_TYPE_V2, 'execute-job', jobId]); + const jobLogger = parentLogger.get(`execute-job:${jobId}`); const apmTrans = apm.startTransaction('execute-job-pdf-v2', REPORTING_TRANSACTION_TYPE); const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup'); let apmGeneratePdf: { end: () => void } | null | undefined; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts index ac922c07574b3c..8bec3cac28f430 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts @@ -5,14 +5,14 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import { groupBy } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap, tap } from 'rxjs/operators'; -import { ReportingCore } from '../../../'; -import { ScreenshotResult } from '../../../../../screenshotting/server'; -import { LocatorParams, PdfMetrics, UrlOrUrlLocatorTuple } from '../../../../common/types'; -import { LevelLogger } from '../../../lib'; -import { ScreenshotOptions } from '../../../types'; +import type { ReportingCore } from '../../../'; +import type { ScreenshotResult } from '../../../../../screenshotting/server'; +import type { LocatorParams, PdfMetrics, UrlOrUrlLocatorTuple } from '../../../../common/types'; +import type { ScreenshotOptions } from '../../../types'; import { PdfMaker } from '../../common/pdf'; import { getFullRedirectAppUrl } from '../../common/v2/get_full_redirect_app_url'; import type { TaskPayloadPDFV2 } from '../types'; @@ -36,7 +36,7 @@ interface PdfResult { export function generatePdfObservable( reporting: ReportingCore, - logger: LevelLogger, + logger: Logger, job: TaskPayloadPDFV2, title: string, locatorParams: LocatorParams[], diff --git a/x-pack/plugins/reporting/server/lib/check_params_version.ts b/x-pack/plugins/reporting/server/lib/check_params_version.ts index 7298384b875715..79237ba56677a5 100644 --- a/x-pack/plugins/reporting/server/lib/check_params_version.ts +++ b/x-pack/plugins/reporting/server/lib/check_params_version.ts @@ -5,16 +5,16 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import { UNVERSIONED_VERSION } from '../../common/constants'; import type { BaseParams } from '../../common/types'; -import type { LevelLogger } from './'; -export function checkParamsVersion(jobParams: BaseParams, logger: LevelLogger) { +export function checkParamsVersion(jobParams: BaseParams, logger: Logger) { if (jobParams.version) { logger.debug(`Using reporting job params v${jobParams.version}`); return jobParams.version; } - logger.warning(`No version provided in report job params. Assuming ${UNVERSIONED_VERSION}`); + logger.warn(`No version provided in report job params. Assuming ${UNVERSIONED_VERSION}`); return UNVERSIONED_VERSION; } diff --git a/x-pack/plugins/reporting/server/lib/content_stream.test.ts b/x-pack/plugins/reporting/server/lib/content_stream.test.ts index 0c45ef2d5f5ce8..069ac22258ad10 100644 --- a/x-pack/plugins/reporting/server/lib/content_stream.test.ts +++ b/x-pack/plugins/reporting/server/lib/content_stream.test.ts @@ -5,20 +5,20 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import { set } from 'lodash'; -import { elasticsearchServiceMock } from 'src/core/server/mocks'; -import { createMockLevelLogger } from '../test_helpers'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; import { ContentStream } from './content_stream'; describe('ContentStream', () => { let client: ReturnType; - let logger: ReturnType; + let logger: Logger; let stream: ContentStream; let base64Stream: ContentStream; beforeEach(() => { client = elasticsearchServiceMock.createClusterClient().asInternalUser; - logger = createMockLevelLogger(); + logger = loggingSystemMock.createLogger(); stream = new ContentStream( client, logger, diff --git a/x-pack/plugins/reporting/server/lib/content_stream.ts b/x-pack/plugins/reporting/server/lib/content_stream.ts index c0b2d458b4d594..b09e446ff576c3 100644 --- a/x-pack/plugins/reporting/server/lib/content_stream.ts +++ b/x-pack/plugins/reporting/server/lib/content_stream.ts @@ -5,14 +5,13 @@ * 2.0. */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { Duplex } from 'stream'; +import { ByteSizeValue } from '@kbn/config-schema'; +import type { ElasticsearchClient, Logger } from 'kibana/server'; import { defaults, get } from 'lodash'; import Puid from 'puid'; -import { ByteSizeValue } from '@kbn/config-schema'; -import type { ElasticsearchClient } from 'src/core/server'; -import { ReportingCore } from '..'; -import { ReportSource } from '../../common/types'; -import { LevelLogger } from './level_logger'; +import { Duplex } from 'stream'; +import type { ReportingCore } from '../'; +import type { ReportSource } from '../../common/types'; /** * @note The Elasticsearch `http.max_content_length` is including the whole POST body. @@ -87,7 +86,7 @@ export class ContentStream extends Duplex { constructor( private client: ElasticsearchClient, - private logger: LevelLogger, + private logger: Logger, private document: ContentStreamDocument, { encoding = 'base64' }: ContentStreamParameters = {} ) { @@ -348,7 +347,7 @@ export async function getContentStream( return new ContentStream( client, - logger.clone(['content_stream', document.id]), + logger.get('content_stream').get(document.id), document, parameters ); diff --git a/x-pack/plugins/reporting/server/lib/event_logger/adapter.test.ts b/x-pack/plugins/reporting/server/lib/event_logger/adapter.test.ts index aef569a49e357d..90c546b198a08b 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/adapter.test.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/adapter.test.ts @@ -6,11 +6,11 @@ */ import { LogMeta } from 'kibana/server'; -import { createMockLevelLogger } from '../../test_helpers'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { EcsLogAdapter } from './adapter'; describe('EcsLogAdapter', () => { - const logger = createMockLevelLogger(); + const logger = loggingSystemMock.createLogger(); beforeAll(() => { jest .spyOn(global.Date, 'now') @@ -28,7 +28,7 @@ describe('EcsLogAdapter', () => { const event = { kibana: { reporting: { wins: 5000 } } } as object & LogMeta; // an object that extends LogMeta eventLogger.logEvent('hello world', event); - expect(logger.debug).toBeCalledWith('hello world', ['events'], { + expect(logger.debug).toBeCalledWith('hello world', { event: { duration: undefined, end: undefined, @@ -50,7 +50,7 @@ describe('EcsLogAdapter', () => { const event = { kibana: { reporting: { wins: 9000 } } } as object & LogMeta; // an object that extends LogMeta eventLogger.logEvent('hello duration', event); - expect(logger.debug).toBeCalledWith('hello duration', ['events'], { + expect(logger.debug).toBeCalledWith('hello duration', { event: { duration: 120000000000, end: '2021-04-12T16:02:00.000Z', diff --git a/x-pack/plugins/reporting/server/lib/event_logger/adapter.ts b/x-pack/plugins/reporting/server/lib/event_logger/adapter.ts index c9487a79d9e70e..71116d8f334b50 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/adapter.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/adapter.ts @@ -6,23 +6,26 @@ */ import deepMerge from 'deepmerge'; -import { LogMeta } from 'src/core/server'; -import { LevelLogger } from '../level_logger'; -import { IReportingEventLogger } from './logger'; +import type { Logger, LogMeta } from 'kibana/server'; +import type { IReportingEventLogger } from './logger'; /** @internal */ export class EcsLogAdapter implements IReportingEventLogger { start?: Date; end?: Date; + private logger: Logger; + /** * This class provides a logging system to Reporting code, using a shape similar to the EventLog service. * The logging action causes ECS data with Reporting metrics sent to DEBUG logs. * - * @param {LevelLogger} logger - Reporting's wrapper of the core logger + * @param {Logger} logger - Reporting's wrapper of the core logger * @param {Partial} properties - initial ECS data with template for Reporting metrics */ - constructor(private logger: LevelLogger, private properties: Partial) {} + constructor(logger: Logger, private properties: Partial) { + this.logger = logger.get('events'); + } logEvent(message: string, properties: LogMeta) { if (this.start && !this.end) { @@ -44,7 +47,7 @@ export class EcsLogAdapter implements IReportingEventLogger { }); // sends an ECS object with Reporting metrics to the DEBUG logs - this.logger.debug(message, ['events'], deepMerge(newProperties, properties)); + this.logger.debug(message, deepMerge(newProperties, properties)); } startTiming() { diff --git a/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts b/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts index fa45a8d04176c8..c58777747c3fd3 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts @@ -5,8 +5,8 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { ConcreteTaskInstance } from '../../../../task_manager/server'; -import { createMockLevelLogger } from '../../test_helpers'; import { BasePayload } from '../../types'; import { Report } from '../store'; import { ReportingEventLogger, reportingEventLoggerFactory } from './logger'; @@ -21,7 +21,7 @@ describe('Event Logger', () => { let factory: ReportingEventLogger; beforeEach(() => { - factory = reportingEventLoggerFactory(createMockLevelLogger()); + factory = reportingEventLoggerFactory(loggingSystemMock.createLogger()); }); it(`should construct with an internal seed object`, () => { diff --git a/x-pack/plugins/reporting/server/lib/event_logger/logger.ts b/x-pack/plugins/reporting/server/lib/event_logger/logger.ts index 6a7feea0c335d0..965a55e24229a2 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/logger.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/logger.ts @@ -6,8 +6,7 @@ */ import deepMerge from 'deepmerge'; -import { LogMeta } from 'src/core/server'; -import { LevelLogger } from '../'; +import type { Logger, LogMeta } from 'kibana/server'; import { PLUGIN_ID } from '../../../common/constants'; import type { TaskRunMetrics } from '../../../common/types'; import { IReport } from '../store'; @@ -46,7 +45,7 @@ export interface BaseEvent { } /** @internal */ -export function reportingEventLoggerFactory(logger: LevelLogger) { +export function reportingEventLoggerFactory(logger: Logger) { const genericLogger = new EcsLogAdapter(logger, { event: { provider: PLUGIN_ID } }); return class ReportingEventLogger { diff --git a/x-pack/plugins/reporting/server/lib/index.ts b/x-pack/plugins/reporting/server/lib/index.ts index 682f547380ba0d..36d310fcd131b1 100644 --- a/x-pack/plugins/reporting/server/lib/index.ts +++ b/x-pack/plugins/reporting/server/lib/index.ts @@ -10,7 +10,6 @@ export { checkParamsVersion } from './check_params_version'; export { ContentStream, getContentStream } from './content_stream'; export { cryptoFactory } from './crypto'; export { ExportTypesRegistry, getExportTypesRegistry } from './export_types_registry'; -export { LevelLogger } from './level_logger'; export { PassThroughStream } from './passthrough_stream'; export { statuses } from './statuses'; export { ReportingStore, IlmPolicyManager } from './store'; diff --git a/x-pack/plugins/reporting/server/lib/level_logger.ts b/x-pack/plugins/reporting/server/lib/level_logger.ts deleted file mode 100644 index 91cf6757dbee22..00000000000000 --- a/x-pack/plugins/reporting/server/lib/level_logger.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { LoggerFactory, LogMeta } from 'src/core/server'; - -const trimStr = (toTrim: string) => { - return typeof toTrim === 'string' ? toTrim.trim() : toTrim; -}; - -export interface GenericLevelLogger { - debug: (msg: string, tags: string[], meta: T) => void; - info: (msg: string) => void; - warning: (msg: string) => void; - error: (msg: Error) => void; -} - -export class LevelLogger implements GenericLevelLogger { - private _logger: LoggerFactory; - private _tags: string[]; - public warning: (msg: string, tags?: string[]) => void; - - constructor(logger: LoggerFactory, tags?: string[]) { - this._logger = logger; - this._tags = tags || []; - - /* - * This shortcut provides maintenance convenience: Reporting code has been - * using both .warn and .warning - */ - this.warning = this.warn.bind(this); - } - - private getLogger(tags: string[]) { - return this._logger.get(...this._tags, ...tags); - } - - public error(err: string | Error, tags: string[] = []) { - this.getLogger(tags).error(err); - } - - public warn(msg: string, tags: string[] = []) { - this.getLogger(tags).warn(msg); - } - - // only "debug" logging supports the LogMeta for now... - public debug(msg: string, tags: string[] = [], meta?: T) { - this.getLogger(tags).debug(msg, meta); - } - - public trace(msg: string, tags: string[] = []) { - this.getLogger(tags).trace(msg); - } - - public info(msg: string, tags: string[] = []) { - this.getLogger(tags).info(trimStr(msg)); - } - - public clone(tags: string[]) { - return new LevelLogger(this._logger, [...this._tags, ...tags]); - } -} diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts index 3e8942be1ffa0b..7ceafef261dd49 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -5,17 +5,13 @@ * 2.0. */ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; import { ReportingCore } from '../../'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { Report, ReportDocument, ReportingStore, SavedReport } from './'; describe('ReportingStore', () => { - const mockLogger = createMockLevelLogger(); + const mockLogger = loggingSystemMock.createLogger(); let mockCore: ReportingCore; let mockEsClient: ReturnType; diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 41fdd9580c996c..7e920e718d51ee 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -6,13 +6,14 @@ */ import { IndexResponse, UpdateResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { ElasticsearchClient } from 'src/core/server'; -import { LevelLogger, statuses } from '../'; -import { ReportingCore } from '../../'; +import type { ElasticsearchClient, Logger } from 'kibana/server'; +import { statuses } from '../'; +import type { ReportingCore } from '../../'; import { ILM_POLICY_NAME, REPORTING_SYSTEM_INDEX } from '../../../common/constants'; -import { JobStatus, ReportOutput, ReportSource } from '../../../common/types'; -import { ReportTaskParams } from '../tasks'; -import { IReport, Report, ReportDocument, SavedReport } from './'; +import type { JobStatus, ReportOutput, ReportSource } from '../../../common/types'; +import type { ReportTaskParams } from '../tasks'; +import type { IReport, Report, ReportDocument } from './'; +import { SavedReport } from './'; import { IlmPolicyManager } from './ilm_policy_manager'; import { indexTimestamp } from './index_timestamp'; import { mapping } from './mapping'; @@ -83,12 +84,12 @@ export class ReportingStore { private client?: ElasticsearchClient; private ilmPolicyManager?: IlmPolicyManager; - constructor(private reportingCore: ReportingCore, private logger: LevelLogger) { + constructor(private reportingCore: ReportingCore, private logger: Logger) { const config = reportingCore.getConfig(); this.indexPrefix = REPORTING_SYSTEM_INDEX; this.indexInterval = config.get('queue', 'indexInterval'); - this.logger = logger.clone(['store']); + this.logger = logger.get('store'); } private async getClient() { diff --git a/x-pack/plugins/reporting/server/lib/tasks/error_logger.test.ts b/x-pack/plugins/reporting/server/lib/tasks/error_logger.test.ts index 607c9c32538be4..302088e6a6eb13 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/error_logger.test.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/error_logger.test.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { createMockLevelLogger } from '../../test_helpers'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { errorLogger } from './error_logger'; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); describe('Execute Report Error Logger', () => { const errorLogSpy = jest.spyOn(logger, 'error'); diff --git a/x-pack/plugins/reporting/server/lib/tasks/error_logger.ts b/x-pack/plugins/reporting/server/lib/tasks/error_logger.ts index b4d4028230666a..a67e3caeb2c78c 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/error_logger.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/error_logger.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LevelLogger } from '..'; +import type { Logger } from 'kibana/server'; const MAX_PARTIAL_ERROR_LENGTH = 1000; // 1000 of beginning, 1000 of end const ERROR_PARTIAL_SEPARATOR = '...'; @@ -15,7 +15,7 @@ const MAX_ERROR_LENGTH = MAX_PARTIAL_ERROR_LENGTH * 2 + ERROR_PARTIAL_SEPARATOR. * An error message string could be very long, as it sometimes includes huge * amount of base64 */ -export const errorLogger = (logger: LevelLogger, message: string, err?: Error) => { +export const errorLogger = (logger: Logger, message: string, err?: Error) => { if (err) { const errString = `${message}: ${err}`; const errLength = errString.length; diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.test.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.test.ts index df662d963d0edf..b47df99b7a0fde 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.test.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.test.ts @@ -5,18 +5,15 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { ReportingCore } from '../..'; import { RunContext } from '../../../../task_manager/server'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { ReportingConfigType } from '../../config'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { ExecuteReportTask } from './'; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); describe('Execute Report Task', () => { let mockReporting: ReportingCore; diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts index 449f3b8da7671a..4d4959eef00c44 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts @@ -6,20 +6,21 @@ */ import { UpdateResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { Logger } from 'kibana/server'; import moment from 'moment'; import * as Rx from 'rxjs'; import { timeout } from 'rxjs/operators'; import { finished, Writable } from 'stream'; import { promisify } from 'util'; -import { getContentStream, LevelLogger } from '../'; -import { ReportingCore } from '../../'; -import { +import { getContentStream } from '../'; +import type { ReportingCore } from '../../'; +import type { RunContext, TaskManagerStartContract, TaskRunCreatorFunction, } from '../../../../task_manager/server'; import { CancellationToken } from '../../../common/cancellation_token'; -import { ReportingError, UnknownError, QueueTimeoutError } from '../../../common/errors'; +import { QueueTimeoutError, ReportingError, UnknownError } from '../../../common/errors'; import { durationToNumber, numberToDuration } from '../../../common/schema_utils'; import type { ReportOutput } from '../../../common/types'; import type { ReportingConfigType } from '../../config'; @@ -60,7 +61,7 @@ function reportFromTask(task: ReportTaskParams) { export class ExecuteReportTask implements ReportingTask { public TYPE = REPORTING_EXECUTE_TYPE; - private logger: LevelLogger; + private logger: Logger; private taskManagerStart?: TaskManagerStartContract; private taskExecutors?: Map; private kibanaId?: string; @@ -70,9 +71,9 @@ export class ExecuteReportTask implements ReportingTask { constructor( private reporting: ReportingCore, private config: ReportingConfigType, - logger: LevelLogger + logger: Logger ) { - this.logger = logger.clone(['runTask']); + this.logger = logger.get('runTask'); } /* @@ -86,7 +87,7 @@ export class ExecuteReportTask implements ReportingTask { const exportTypesRegistry = reporting.getExportTypesRegistry(); const executors = new Map(); for (const exportType of exportTypesRegistry.getAll()) { - const exportTypeLogger = this.logger.clone([exportType.id]); + const exportTypeLogger = this.logger.get(exportType.jobType); const jobExecutor = exportType.runTaskFnFactory(reporting, exportTypeLogger); // The task will run the function with the job type as a param. // This allows us to retrieve the specific export type runFn when called to run an export @@ -476,7 +477,7 @@ export class ExecuteReportTask implements ReportingTask { return await this.getTaskManagerStart().schedule(taskInstance); } - private async rescheduleTask(task: ReportTaskParams, logger: LevelLogger) { + private async rescheduleTask(task: ReportTaskParams, logger: Logger) { logger.info(`Rescheduling task:${task.id} to retry after error.`); const oldTaskInstance: ReportingExecuteTaskInstance = { diff --git a/x-pack/plugins/reporting/server/lib/tasks/monitor_report.test.ts b/x-pack/plugins/reporting/server/lib/tasks/monitor_report.test.ts index d737c7032855b9..b7e75de2475358 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/monitor_report.test.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/monitor_report.test.ts @@ -5,18 +5,15 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { ReportingCore } from '../..'; import { RunContext } from '../../../../task_manager/server'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { ReportingConfigType } from '../../config'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { MonitorReportsTask } from './'; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); describe('Execute Report Task', () => { let mockReporting: ReportingCore; diff --git a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts index 4af28e3d1a6981..56e12d7d5512bd 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts @@ -5,8 +5,9 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import moment from 'moment'; -import { LevelLogger, ReportingStore } from '../'; +import { ReportingStore } from '../'; import { ReportingCore } from '../../'; import { TaskManagerStartContract, TaskRunCreatorFunction } from '../../../../task_manager/server'; import { numberToDuration } from '../../../common/schema_utils'; @@ -38,7 +39,7 @@ import { ReportingTask, ReportingTaskStatus, REPORTING_MONITOR_TYPE, ReportTaskP export class MonitorReportsTask implements ReportingTask { public TYPE = REPORTING_MONITOR_TYPE; - private logger: LevelLogger; + private logger: Logger; private taskManagerStart?: TaskManagerStartContract; private store?: ReportingStore; private timeout: moment.Duration; @@ -46,9 +47,9 @@ export class MonitorReportsTask implements ReportingTask { constructor( private reporting: ReportingCore, private config: ReportingConfigType, - parentLogger: LevelLogger + parentLogger: Logger ) { - this.logger = parentLogger.clone([REPORTING_MONITOR_TYPE]); + this.logger = parentLogger.get(REPORTING_MONITOR_TYPE); this.timeout = numberToDuration(config.queue.timeout); } @@ -91,31 +92,42 @@ export class MonitorReportsTask implements ReportingTask { return; } - const { - _id: jobId, - _source: { process_expiration: processExpiration, status }, - } = recoveredJob; + const report = new SavedReport({ ...recoveredJob, ...recoveredJob._source }); + const { _id: jobId, process_expiration: processExpiration, status } = report; + const eventLog = this.reporting.getEventLogger(report); if (![statuses.JOB_STATUS_PENDING, statuses.JOB_STATUS_PROCESSING].includes(status)) { - throw new Error(`Invalid job status in the monitoring search result: ${status}`); // only pending or processing jobs possibility need rescheduling + const invalidStatusError = new Error( + `Invalid job status in the monitoring search result: ${status}` + ); // only pending or processing jobs possibility need rescheduling + this.logger.error(invalidStatusError); + eventLog.logError(invalidStatusError); + + // fatal: can not reschedule the job + throw invalidStatusError; } if (status === statuses.JOB_STATUS_PENDING) { - this.logger.info( + const migratingJobError = new Error( `${jobId} was scheduled in a previous version and left in [${status}] status. Rescheduling...` ); + this.logger.error(migratingJobError); + eventLog.logError(migratingJobError); } if (status === statuses.JOB_STATUS_PROCESSING) { const expirationTime = moment(processExpiration); const overdueValue = moment().valueOf() - expirationTime.valueOf(); - this.logger.info( + const overdueExpirationError = new Error( `${jobId} status is [${status}] and the expiration time was [${overdueValue}ms] ago. Rescheduling...` ); + this.logger.error(overdueExpirationError); + eventLog.logError(overdueExpirationError); } + eventLog.logRetry(); + // clear process expiration and set status to pending - const report = new SavedReport({ ...recoveredJob, ...recoveredJob._source }); await reportingStore.prepareReportForRetry(report); // if there is a version conflict response, this just throws and logs an error // clear process expiration and reschedule @@ -145,7 +157,7 @@ export class MonitorReportsTask implements ReportingTask { } // reschedule the task with TM - private async rescheduleTask(task: ReportTaskParams, logger: LevelLogger) { + private async rescheduleTask(task: ReportTaskParams, logger: Logger) { if (!this.taskManagerStart) { throw new Error('Reporting task runner has not been initialized!'); } @@ -153,8 +165,6 @@ export class MonitorReportsTask implements ReportingTask { const newTask = await this.reporting.scheduleTask(task); - this.reporting.getEventLogger({ _id: task.id, ...task }, newTask).logRetry(); - return newTask; } diff --git a/x-pack/plugins/reporting/server/plugin.test.ts b/x-pack/plugins/reporting/server/plugin.test.ts index e179d847d95260..98f02668323b1a 100644 --- a/x-pack/plugins/reporting/server/plugin.test.ts +++ b/x-pack/plugins/reporting/server/plugin.test.ts @@ -5,14 +5,12 @@ * 2.0. */ -import type { CoreSetup, CoreStart } from 'kibana/server'; -import { coreMock } from 'src/core/server/mocks'; +import type { CoreSetup, CoreStart, Logger } from 'kibana/server'; +import { coreMock, loggingSystemMock } from 'src/core/server/mocks'; import type { ReportingCore, ReportingInternalStart } from './core'; -import { LevelLogger } from './lib'; import { ReportingPlugin } from './plugin'; import { createMockConfigSchema, - createMockLevelLogger, createMockPluginSetup, createMockPluginStart, } from './test_helpers'; @@ -27,7 +25,7 @@ describe('Reporting Plugin', () => { let coreStart: CoreStart; let pluginSetup: ReportingSetupDeps; let pluginStart: ReportingInternalStart; - let logger: jest.Mocked; + let logger: jest.Mocked; let plugin: ReportingPlugin; beforeEach(async () => { @@ -38,9 +36,9 @@ describe('Reporting Plugin', () => { pluginSetup = createMockPluginSetup({}) as unknown as ReportingSetupDeps; pluginStart = await createMockPluginStart(coreStart, configSchema); - logger = createMockLevelLogger(); + logger = loggingSystemMock.createLogger(); plugin = new ReportingPlugin(initContext); - (plugin as unknown as { logger: LevelLogger }).logger = logger; + (plugin as unknown as { logger: Logger }).logger = logger; }); it('has a sync setup process', () => { diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index a0d4bfed7c7e0a..37d6494f5e079b 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -5,12 +5,12 @@ * 2.0. */ -import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; +import type { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import { PLUGIN_ID } from '../common/constants'; import { ReportingCore } from './'; import { buildConfig, registerUiSettings, ReportingConfigType } from './config'; import { registerDeprecations } from './deprecations'; -import { LevelLogger, ReportingStore } from './lib'; +import { ReportingStore } from './lib'; import { registerRoutes } from './routes'; import { setFieldFormats } from './services'; import type { @@ -28,11 +28,11 @@ import { registerReportingUsageCollector } from './usage'; export class ReportingPlugin implements Plugin { - private logger: LevelLogger; + private logger: Logger; private reportingCore?: ReportingCore; constructor(private initContext: PluginInitializerContext) { - this.logger = new LevelLogger(initContext.logger.get()); + this.logger = initContext.logger.get(); } public setup(core: CoreSetup, plugins: ReportingSetupDeps) { diff --git a/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts b/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts index 4c368337cd4822..89d55ff04ab8fd 100644 --- a/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts +++ b/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts @@ -5,16 +5,16 @@ * 2.0. */ import { errors } from '@elastic/elasticsearch'; -import { SecurityHasPrivilegesIndexPrivilegesCheck } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { RequestHandler } from 'src/core/server'; +import type { SecurityHasPrivilegesIndexPrivilegesCheck } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { Logger, RequestHandler } from 'kibana/server'; import { API_GET_ILM_POLICY_STATUS, API_MIGRATE_ILM_POLICY_URL, ILM_POLICY_NAME, } from '../../../common/constants'; -import { IlmPolicyStatusResponse } from '../../../common/types'; -import { ReportingCore } from '../../core'; -import { IlmPolicyManager, LevelLogger as Logger } from '../../lib'; +import type { IlmPolicyStatusResponse } from '../../../common/types'; +import type { ReportingCore } from '../../core'; +import { IlmPolicyManager } from '../../lib'; import { deprecations } from '../../lib/deprecations'; export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Logger) => { diff --git a/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts b/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts index 67d7d0c4a0c080..9c76aade058f0e 100644 --- a/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts +++ b/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { licensingMock } from '../../../../../licensing/server/mocks'; @@ -12,7 +13,6 @@ import { securityMock } from '../../../../../security/server/mocks'; import { API_GET_ILM_POLICY_STATUS } from '../../../../common/constants'; import { createMockConfigSchema, - createMockLevelLogger, createMockPluginSetup, createMockPluginStart, createMockReportingCore, @@ -54,7 +54,7 @@ describe(`GET ${API_GET_ILM_POLICY_STATUS}`, () => { it('correctly handles authz when security is unavailable', async () => { const core = await createReportingCore({}); - registerDeprecationsRoutes(core, createMockLevelLogger()); + registerDeprecationsRoutes(core, loggingSystemMock.createLogger()); await server.start(); await supertest(httpSetup.server.listener) @@ -68,7 +68,7 @@ describe(`GET ${API_GET_ILM_POLICY_STATUS}`, () => { security.license.isEnabled.mockReturnValue(false); const core = await createReportingCore({ security }); - registerDeprecationsRoutes(core, createMockLevelLogger()); + registerDeprecationsRoutes(core, loggingSystemMock.createLogger()); await server.start(); await supertest(httpSetup.server.listener) diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts index f68df294b41183..fb95ad9e318804 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts @@ -6,11 +6,11 @@ */ import { i18n } from '@kbn/i18n'; -import { ReportingCore } from '../..'; +import type { Logger } from 'kibana/server'; +import type { ReportingCore } from '../..'; import { API_DIAGNOSE_URL } from '../../../common/constants'; -import { LevelLogger as Logger } from '../../lib'; import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; -import { DiagnosticResponse } from './'; +import type { DiagnosticResponse } from './'; const logsToHelpMap = { 'error while loading shared libraries': i18n.translate( diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/index.ts b/x-pack/plugins/reporting/server/routes/diagnostic/index.ts index 92404b76e07418..b5e2a8585afb32 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/index.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/index.ts @@ -5,10 +5,10 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; +import type { ReportingCore } from '../../core'; import { registerDiagnoseBrowser } from './browser'; import { registerDiagnoseScreenshot } from './screenshot'; -import { LevelLogger as Logger } from '../../lib'; -import { ReportingCore } from '../../core'; export const registerDiagnosticRoutes = (reporting: ReportingCore, logger: Logger) => { registerDiagnoseBrowser(reporting, logger); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts index 911807e63a9d56..dc8fdb7e6d0c87 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts @@ -6,13 +6,13 @@ */ import * as Rx from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { ReportingCore } from '../../../'; import type { ScreenshottingStart } from '../../../../../screenshotting/server'; import { createMockConfigSchema, - createMockLevelLogger, createMockPluginSetup, createMockReportingCore, } from '../../../test_helpers'; @@ -27,7 +27,7 @@ const fontNotFoundMessage = 'Could not find the default font'; describe('POST /diagnose/browser', () => { jest.setTimeout(6000); const reportingSymbol = Symbol('reporting'); - const mockLogger = createMockLevelLogger(); + const mockLogger = loggingSystemMock.createLogger(); let server: SetupServerReturn['server']; let httpSetup: SetupServerReturn['httpSetup']; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts index ad90679e67adb8..3bc3f5bbb5e287 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts @@ -5,13 +5,13 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { ReportingCore } from '../../../'; import { generatePngObservable } from '../../../export_types/common'; import { createMockConfigSchema, - createMockLevelLogger, createMockPluginSetup, createMockReportingCore, } from '../../../test_helpers'; @@ -38,7 +38,7 @@ describe('POST /diagnose/screenshot', () => { }; const config = createMockConfigSchema({ queue: { timeout: 120000 } }); - const mockLogger = createMockLevelLogger(); + const mockLogger = loggingSystemMock.createLogger(); beforeEach(async () => { ({ server, httpSetup } = await setupServer(reportingSymbol)); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts index 90b4c9d9a30c6f..6819970fe753a9 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts @@ -6,12 +6,12 @@ */ import { i18n } from '@kbn/i18n'; +import type { Logger } from 'kibana/server'; import { ReportingCore } from '../..'; import { APP_WRAPPER_CLASS } from '../../../../../../src/core/server'; import { API_DIAGNOSE_URL } from '../../../common/constants'; import { generatePngObservable } from '../../export_types/common'; import { getAbsoluteUrlFactory } from '../../export_types/common/get_absolute_url'; -import { LevelLogger as Logger } from '../../lib'; import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; import { DiagnosticResponse } from './'; diff --git a/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts b/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts index b6ada00ba55ab7..19687b9d3ec9b8 100644 --- a/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts @@ -6,13 +6,13 @@ */ import { schema } from '@kbn/config-schema'; -import { KibanaRequest } from 'src/core/server'; -import { ReportingCore } from '../../'; +import type { KibanaRequest, Logger } from 'kibana/server'; +import type { ReportingCore } from '../../'; import { CSV_SEARCHSOURCE_IMMEDIATE_TYPE } from '../../../common/constants'; import { runTaskFnFactory } from '../../export_types/csv_searchsource_immediate/execute_job'; -import { JobParamsDownloadCSV } from '../../export_types/csv_searchsource_immediate/types'; -import { LevelLogger as Logger, PassThroughStream } from '../../lib'; -import { BaseParams } from '../../types'; +import type { JobParamsDownloadCSV } from '../../export_types/csv_searchsource_immediate/types'; +import { PassThroughStream } from '../../lib'; +import type { BaseParams } from '../../types'; import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; import { RequestHandler } from '../lib/request_handler'; @@ -64,7 +64,7 @@ export function registerGenerateCsvFromSavedObjectImmediate( authorizedUserPreRouting( reporting, async (user, context, req: CsvFromSavedObjectRequest, res) => { - const logger = parentLogger.clone([CSV_SEARCHSOURCE_IMMEDIATE_TYPE]); + const logger = parentLogger.get(CSV_SEARCHSOURCE_IMMEDIATE_TYPE); const runTaskFn = runTaskFnFactory(reporting, logger); const requestHandler = new RequestHandler(reporting, user, context, req, res, logger); const stream = new PassThroughStream(); diff --git a/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts b/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts index cfcb7d6d2b05c6..c5e7bb2197d722 100644 --- a/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts +++ b/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts @@ -7,16 +7,16 @@ import { schema } from '@kbn/config-schema'; import rison from 'rison-node'; -import { ReportingCore } from '../..'; +import type { Logger } from 'kibana/server'; +import type { ReportingCore } from '../..'; import { API_BASE_URL } from '../../../common/constants'; -import { LevelLogger } from '../../lib'; -import { BaseParams } from '../../types'; +import type { BaseParams } from '../../types'; import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; import { RequestHandler } from '../lib/request_handler'; const BASE_GENERATE = `${API_BASE_URL}/generate`; -export function registerJobGenerationRoutes(reporting: ReportingCore, logger: LevelLogger) { +export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Logger) { const setupDeps = reporting.getPluginSetupDeps(); const { router } = setupDeps; diff --git a/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts b/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts index f6db9e92086eb2..f0db06485cf448 100644 --- a/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts +++ b/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts @@ -7,6 +7,7 @@ import rison from 'rison-node'; import { BehaviorSubject } from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { ReportingCore } from '../../../'; @@ -16,7 +17,6 @@ import { ExportTypesRegistry } from '../../../lib/export_types_registry'; import { Report } from '../../../lib/store'; import { createMockConfigSchema, - createMockLevelLogger, createMockPluginSetup, createMockPluginStart, createMockReportingCore, @@ -38,7 +38,7 @@ describe('POST /api/reporting/generate', () => { queue: { indexInterval: 'year', timeout: 10000, pollEnabled: true }, }); - const mockLogger = createMockLevelLogger(); + const mockLogger = loggingSystemMock.createLogger(); beforeEach(async () => { ({ server, httpSetup } = await setupServer(reportingSymbol)); diff --git a/x-pack/plugins/reporting/server/routes/index.ts b/x-pack/plugins/reporting/server/routes/index.ts index 49f602062b0c16..0cc0d1bdc67967 100644 --- a/x-pack/plugins/reporting/server/routes/index.ts +++ b/x-pack/plugins/reporting/server/routes/index.ts @@ -5,8 +5,8 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import { ReportingCore } from '..'; -import { LevelLogger } from '../lib'; import { registerDeprecationsRoutes } from './deprecations/deprecations'; import { registerDiagnosticRoutes } from './diagnostic'; import { @@ -15,7 +15,7 @@ import { } from './generate'; import { registerJobInfoRoutes } from './management'; -export function registerRoutes(reporting: ReportingCore, logger: LevelLogger) { +export function registerRoutes(reporting: ReportingCore, logger: Logger) { registerDeprecationsRoutes(reporting, logger); registerDiagnosticRoutes(reporting, logger); registerGenerateCsvFromSavedObjectImmediate(reporting, logger); diff --git a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts index 7f4d85ff141560..27126baad021d6 100644 --- a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts +++ b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts @@ -138,7 +138,7 @@ export function jobsQueryFactory(reportingCore: ReportingCore): JobsQueryFactory async get(user, id) { const { logger } = reportingCore.getPluginSetupDeps(); if (!id) { - logger.warning(`No ID provided for GET`); + logger.warn(`No ID provided for GET`); return; } @@ -163,7 +163,7 @@ export function jobsQueryFactory(reportingCore: ReportingCore): JobsQueryFactory const result = response?.hits?.hits?.[0]; if (!result?._source) { - logger.warning(`No hits resulted in search`); + logger.warn(`No hits resulted in search`); return; } diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts index d1c1dddb3c3021..c97ec3285839df 100644 --- a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts @@ -6,16 +6,12 @@ */ import { KibanaRequest, KibanaResponseFactory } from 'kibana/server'; -import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { coreMock, httpServerMock, loggingSystemMock } from 'src/core/server/mocks'; import { ReportingCore } from '../..'; import { JobParamsPDFDeprecated, TaskPayloadPDF } from '../../export_types/printable_pdf/types'; import { Report, ReportingStore } from '../../lib/store'; import { ReportApiJSON } from '../../lib/store/report'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { ReportingRequestHandlerContext, ReportingSetup } from '../../types'; import { RequestHandler } from './request_handler'; @@ -43,7 +39,7 @@ const getMockResponseFactory = () => unauthorized: (obj: unknown) => obj, } as unknown as KibanaResponseFactory); -const mockLogger = createMockLevelLogger(); +const mockLogger = loggingSystemMock.createLogger(); describe('Handle request to generate', () => { let reportingCore: ReportingCore; diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.ts b/x-pack/plugins/reporting/server/routes/lib/request_handler.ts index b0a2032c18f19e..b8a3a4c69802ca 100644 --- a/x-pack/plugins/reporting/server/routes/lib/request_handler.ts +++ b/x-pack/plugins/reporting/server/routes/lib/request_handler.ts @@ -7,12 +7,12 @@ import Boom from '@hapi/boom'; import { i18n } from '@kbn/i18n'; -import { KibanaRequest, KibanaResponseFactory } from 'kibana/server'; -import { ReportingCore } from '../..'; +import type { KibanaRequest, KibanaResponseFactory, Logger } from 'kibana/server'; +import type { ReportingCore } from '../..'; import { API_BASE_URL } from '../../../common/constants'; -import { checkParamsVersion, cryptoFactory, LevelLogger } from '../../lib'; +import { checkParamsVersion, cryptoFactory } from '../../lib'; import { Report } from '../../lib/store'; -import { BaseParams, ReportingRequestHandlerContext, ReportingUser } from '../../types'; +import type { BaseParams, ReportingRequestHandlerContext, ReportingUser } from '../../types'; export const handleUnavailable = (res: KibanaResponseFactory) => { return res.custom({ statusCode: 503, body: 'Not Available' }); @@ -30,7 +30,7 @@ export class RequestHandler { private context: ReportingRequestHandlerContext, private req: KibanaRequest, private res: KibanaResponseFactory, - private logger: LevelLogger + private logger: Logger ) {} private async encryptHeaders() { @@ -53,7 +53,7 @@ export class RequestHandler { } const [createJob, store] = await Promise.all([ - exportType.createJobFnFactory(reporting, logger.clone([exportType.id])), + exportType.createJobFnFactory(reporting, logger.get(exportType.id)), reporting.getStore(), ]); diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_levellogger.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_levellogger.ts deleted file mode 100644 index a6e6be47bdfcdd..00000000000000 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_levellogger.ts +++ /dev/null @@ -1,29 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -jest.mock('../lib/level_logger'); - -import { loggingSystemMock } from 'src/core/server/mocks'; -import { LevelLogger } from '../lib/level_logger'; - -export function createMockLevelLogger() { - // eslint-disable-next-line no-console - const consoleLogger = (tag: string) => (message: unknown) => console.log(tag, message); - - const logger = new LevelLogger(loggingSystemMock.create()) as jest.Mocked; - - // logger.debug.mockImplementation(consoleLogger('debug')); // uncomment this to see debug logs in jest tests - logger.info.mockImplementation(consoleLogger('info')); - logger.warn.mockImplementation(consoleLogger('warn')); - logger.warning = jest.fn().mockImplementation(consoleLogger('warn')); - logger.error.mockImplementation(consoleLogger('error')); - logger.trace.mockImplementation(consoleLogger('trace')); - - logger.clone.mockImplementation(() => logger); - - return logger; -} diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index 49d92a0fe4448e..e00ebd99f0420a 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -10,7 +10,12 @@ jest.mock('../usage'); import _ from 'lodash'; import { BehaviorSubject } from 'rxjs'; -import { coreMock, elasticsearchServiceMock, statusServiceMock } from 'src/core/server/mocks'; +import { + coreMock, + elasticsearchServiceMock, + loggingSystemMock, + statusServiceMock, +} from 'src/core/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { dataPluginMock } from 'src/plugins/data/server/mocks'; import { FieldFormatsRegistry } from 'src/plugins/field_formats/common'; @@ -27,7 +32,6 @@ import { buildConfig, ReportingConfigType } from '../config'; import { ReportingInternalSetup, ReportingInternalStart } from '../core'; import { ReportingStore } from '../lib'; import { setFieldFormats } from '../services'; -import { createMockLevelLogger } from './create_mock_levellogger'; export const createMockPluginSetup = ( setupMock: Partial> @@ -38,13 +42,13 @@ export const createMockPluginSetup = ( router: { get: jest.fn(), post: jest.fn(), put: jest.fn(), delete: jest.fn() }, security: securityMock.createSetup(), taskManager: taskManagerMock.createSetup(), - logger: createMockLevelLogger(), + logger: loggingSystemMock.createLogger(), status: statusServiceMock.createSetupContract(), ...setupMock, }; }; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); const createMockReportingStore = async (config: ReportingConfigType) => { const mockConfigSchema = createMockConfigSchema(config); diff --git a/x-pack/plugins/reporting/server/test_helpers/index.ts b/x-pack/plugins/reporting/server/test_helpers/index.ts index df0a182075341a..0e1dffe142c748 100644 --- a/x-pack/plugins/reporting/server/test_helpers/index.ts +++ b/x-pack/plugins/reporting/server/test_helpers/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -export { createMockLevelLogger } from './create_mock_levellogger'; export { createMockConfig, createMockConfigSchema, diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index fa69509d16be8a..b3c9261bfd9244 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { IRouter, RequestHandlerContext } from 'src/core/server'; +import type { IRouter, Logger, RequestHandlerContext } from 'kibana/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import type { DataPluginStart } from 'src/plugins/data/server/plugin'; import { FieldFormatsStart } from 'src/plugins/field_formats/server'; @@ -29,7 +29,6 @@ import type { CancellationToken } from '../common/cancellation_token'; import type { BaseParams, BasePayload, TaskRunResult, UrlOrUrlLocatorTuple } from '../common/types'; import type { ReportingConfigType } from './config'; import type { ReportingCore } from './core'; -import type { LevelLogger } from './lib'; import type { ReportTaskParams } from './lib/tasks'; /** @@ -71,12 +70,12 @@ export type RunTaskFn = ( export type CreateJobFnFactory = ( reporting: ReportingCore, - logger: LevelLogger + logger: Logger ) => CreateJobFnType; export type RunTaskFnFactory = ( reporting: ReportingCore, - logger: LevelLogger + logger: Logger ) => RunTaskFnType; export interface ExportTypeDefinition< diff --git a/x-pack/plugins/security/server/session_management/session_index.test.ts b/x-pack/plugins/security/server/session_management/session_index.test.ts index 94e92a2689fc38..b19f580f2d8dd1 100644 --- a/x-pack/plugins/security/server/session_management/session_index.test.ts +++ b/x-pack/plugins/security/server/session_management/session_index.test.ts @@ -215,6 +215,7 @@ describe('Session index', () => { expect(mockElasticsearchClient.search).toHaveBeenCalledTimes(1); expect(mockElasticsearchClient.bulk).not.toHaveBeenCalled(); expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.indices.refresh).not.toHaveBeenCalled(); // since the search failed, we don't refresh the index }); it('throws if bulk delete call to Elasticsearch fails', async () => { @@ -227,7 +228,20 @@ describe('Session index', () => { expect(mockElasticsearchClient.openPointInTime).toHaveBeenCalledTimes(1); expect(mockElasticsearchClient.search).toHaveBeenCalledTimes(1); expect(mockElasticsearchClient.bulk).toHaveBeenCalledTimes(1); - expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1); // since we attempted to delete sessions, we still refresh the index + }); + + it('does not throw if index refresh call to Elasticsearch fails', async () => { + const failureReason = new errors.ResponseError( + securityMock.createApiResponse(securityMock.createApiResponse({ body: { type: 'Uh oh.' } })) + ); + mockElasticsearchClient.indices.refresh.mockRejectedValue(failureReason); + + await sessionIndex.cleanUp(); + expect(mockElasticsearchClient.openPointInTime).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.search).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.bulk).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1); // since we attempted to delete sessions, we still refresh the index }); it('when neither `lifespan` nor `idleTimeout` is configured', async () => { @@ -388,6 +402,7 @@ describe('Session index', () => { } ); expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.indices.refresh).toHaveBeenCalledTimes(1); }); it('when only `idleTimeout` is configured', async () => { @@ -474,6 +489,7 @@ describe('Session index', () => { } ); expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.indices.refresh).toHaveBeenCalledTimes(1); }); it('when both `lifespan` and `idleTimeout` are configured', async () => { @@ -570,6 +586,7 @@ describe('Session index', () => { } ); expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.indices.refresh).toHaveBeenCalledTimes(1); }); it('when both `lifespan` and `idleTimeout` are configured and multiple providers are enabled', async () => { @@ -714,6 +731,7 @@ describe('Session index', () => { } ); expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.indices.refresh).toHaveBeenCalledTimes(1); }); it('should clean up sessions in batches of 10,000', async () => { @@ -729,6 +747,7 @@ describe('Session index', () => { expect(mockElasticsearchClient.search).toHaveBeenCalledTimes(2); expect(mockElasticsearchClient.bulk).toHaveBeenCalledTimes(2); expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.indices.refresh).toHaveBeenCalledTimes(1); }); it('should limit number of batches to 10', async () => { @@ -742,6 +761,7 @@ describe('Session index', () => { expect(mockElasticsearchClient.search).toHaveBeenCalledTimes(10); expect(mockElasticsearchClient.bulk).toHaveBeenCalledTimes(10); expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.indices.refresh).toHaveBeenCalledTimes(1); }); it('should log audit event', async () => { diff --git a/x-pack/plugins/security/server/session_management/session_index.ts b/x-pack/plugins/security/server/session_management/session_index.ts index 8a69b9b7f0043c..2a677f6ecf5616 100644 --- a/x-pack/plugins/security/server/session_management/session_index.ts +++ b/x-pack/plugins/security/server/session_management/session_index.ts @@ -444,20 +444,21 @@ export class SessionIndex { * Trigger a removal of any outdated session values. */ async cleanUp() { - this.options.logger.debug(`Running cleanup routine.`); + const { auditLogger, elasticsearchClient, logger } = this.options; + logger.debug(`Running cleanup routine.`); + let error: Error | undefined; + let indexNeedsRefresh = false; try { for await (const sessionValues of this.getSessionValuesInBatches()) { const operations: Array>> = []; sessionValues.forEach(({ _id, _source }) => { const { usernameHash, provider } = _source!; - this.options.auditLogger.log( - sessionCleanupEvent({ sessionId: _id, usernameHash, provider }) - ); + auditLogger.log(sessionCleanupEvent({ sessionId: _id, usernameHash, provider })); operations.push({ delete: { _id } }); }); if (operations.length > 0) { - const bulkResponse = await this.options.elasticsearchClient.bulk( + const bulkResponse = await elasticsearchClient.bulk( { index: this.indexName, operations, @@ -471,24 +472,40 @@ export class SessionIndex { 0 ); if (errorCount < bulkResponse.items.length) { - this.options.logger.warn( + logger.warn( `Failed to clean up ${errorCount} of ${bulkResponse.items.length} invalid or expired sessions. The remaining sessions were cleaned up successfully.` ); + indexNeedsRefresh = true; } else { - this.options.logger.error( + logger.error( `Failed to clean up ${bulkResponse.items.length} invalid or expired sessions.` ); } } else { - this.options.logger.debug( - `Cleaned up ${bulkResponse.items.length} invalid or expired sessions.` - ); + logger.debug(`Cleaned up ${bulkResponse.items.length} invalid or expired sessions.`); + indexNeedsRefresh = true; } } } } catch (err) { - this.options.logger.error(`Failed to clean up sessions: ${err.message}`); - throw err; + logger.error(`Failed to clean up sessions: ${err.message}`); + error = err; + } + + if (indexNeedsRefresh) { + // Only refresh the index if we have actually deleted one or more sessions. The index will auto-refresh eventually anyway, this just + // ensures that searches after the cleanup process are accurate, and this only impacts integration tests. + try { + await elasticsearchClient.indices.refresh({ index: this.indexName }); + logger.debug(`Refreshed session index.`); + } catch (err) { + logger.error(`Failed to refresh session index: ${err.message}`); + } + } + + if (error) { + // If we couldn't fetch or delete sessions, throw an error so the task will be retried. + throw error; } } diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts index 64377c03af6c4d..cf74ca338220bb 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts @@ -72,24 +72,7 @@ describe('Detections > Callouts', () => { }); }); - context('On Rules Management page', () => { - beforeEach(() => { - loadPageAsReadOnlyUser(DETECTIONS_RULE_MANAGEMENT_URL); - }); - - it('We show one primary callout', () => { - waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary'); - }); - - context('When a user clicks Dismiss on the callout', () => { - it('We hide it and persist the dismissal', () => { - waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary'); - dismissCallOut(MISSING_PRIVILEGES_CALLOUT); - reloadPage(); - getCallOut(MISSING_PRIVILEGES_CALLOUT).should('not.exist'); - }); - }); - }); + // FYI: Rules Management check moved to ../detection_rules/all_rules_read_only.spec.ts context('On Rule Details page', () => { beforeEach(() => { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/all_rules_read_only.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/all_rules_read_only.spec.ts new file mode 100644 index 00000000000000..845c5c2bca9d63 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/all_rules_read_only.spec.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ROLES } from '../../../common/test'; +import { getNewRule } from '../../objects/rule'; +import { + COLLAPSED_ACTION_BTN, + RULE_CHECKBOX, + RULE_NAME, +} from '../../screens/alerts_detection_rules'; +import { PAGE_TITLE } from '../../screens/common/page'; +import { waitForRulesTableToBeLoaded } from '../../tasks/alerts_detection_rules'; +import { createCustomRule } from '../../tasks/api_calls/rules'; +import { cleanKibana } from '../../tasks/common'; +import { dismissCallOut, getCallOut, waitForCallOutToBeShown } from '../../tasks/common/callouts'; +import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; +import { SECURITY_DETECTIONS_RULES_URL } from '../../urls/navigation'; + +const MISSING_PRIVILEGES_CALLOUT = 'missing-user-privileges'; + +describe('All rules - read only', () => { + before(() => { + cleanKibana(); + createCustomRule(getNewRule(), '1'); + loginAndWaitForPageWithoutDateRange(SECURITY_DETECTIONS_RULES_URL, ROLES.reader); + waitForRulesTableToBeLoaded(); + cy.get(RULE_NAME).should('have.text', getNewRule().name); + }); + + it('Does not display select boxes for rules', () => { + cy.get(RULE_CHECKBOX).should('not.exist'); + }); + + it('Does not display action options', () => { + // These are the 3 dots at the end of the row that opens up + // options to take action on the rule + cy.get(COLLAPSED_ACTION_BTN).should('not.exist'); + }); + + it('Displays missing privileges primary callout', () => { + waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary'); + }); + + context('When a user clicks Dismiss on the callouts', () => { + it('We hide them and persist the dismissal', () => { + waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary'); + + dismissCallOut(MISSING_PRIVILEGES_CALLOUT); + cy.reload(); + cy.get(PAGE_TITLE).should('be.visible'); + cy.get(RULE_NAME).should('have.text', getNewRule().name); + + getCallOut(MISSING_PRIVILEGES_CALLOUT).should('not.exist'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/all_exception_lists_read_only.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/all_exception_lists_read_only.spec.ts new file mode 100644 index 00000000000000..e332019f2754ed --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/all_exception_lists_read_only.spec.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ROLES } from '../../../common/test'; +import { getExceptionList } from '../../objects/exception'; +import { EXCEPTIONS_TABLE_SHOWING_LISTS } from '../../screens/exceptions'; +import { createExceptionList } from '../../tasks/api_calls/exceptions'; +import { cleanKibana } from '../../tasks/common'; +import { dismissCallOut, getCallOut, waitForCallOutToBeShown } from '../../tasks/common/callouts'; +import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; +import { EXCEPTIONS_URL } from '../../urls/navigation'; + +const MISSING_PRIVILEGES_CALLOUT = 'missing-user-privileges'; + +describe('All exception lists - read only', () => { + before(() => { + cleanKibana(); + + // Create exception list not used by any rules + createExceptionList(getExceptionList(), getExceptionList().list_id); + + loginAndWaitForPageWithoutDateRange(EXCEPTIONS_URL, ROLES.reader); + + cy.reload(); + + // Using cy.contains because we do not care about the exact text, + // just checking number of lists shown + cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '1'); + }); + + it('Displays missing privileges primary callout', () => { + waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary'); + }); + + context('When a user clicks Dismiss on the callouts', () => { + it('We hide them and persist the dismissal', () => { + waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary'); + + dismissCallOut(MISSING_PRIVILEGES_CALLOUT); + cy.reload(); + cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '1'); + + getCallOut(MISSING_PRIVILEGES_CALLOUT).should('not.exist'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts index d68def1284468c..60202a4f6a52a5 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts @@ -29,10 +29,21 @@ import { EXCEPTION_ITEM_CONTAINER, ADD_EXCEPTIONS_BTN, EXCEPTION_FIELD_LIST, + EDIT_EXCEPTIONS_BTN, + EXCEPTION_EDIT_FLYOUT_SAVE_BTN, + EXCEPTION_FLYOUT_VERSION_CONFLICT, + EXCEPTION_FLYOUT_LIST_DELETED_ERROR, } from '../../screens/exceptions'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; import { cleanKibana, reload } from '../../tasks/common'; +import { + createExceptionList, + createExceptionListItem, + updateExceptionListItem, + deleteExceptionList, +} from '../../tasks/api_calls/exceptions'; +import { getExceptionList } from '../../objects/exception'; // NOTE: You might look at these tests and feel they're overkill, // but the exceptions flyout has a lot of logic making it difficult @@ -42,18 +53,28 @@ import { cleanKibana, reload } from '../../tasks/common'; describe('Exceptions flyout', () => { before(() => { cleanKibana(); - loginAndWaitForPageWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - createCustomRule({ ...getNewRule(), index: ['exceptions-*'] }); - reload(); - goToRuleDetails(); - - cy.get(RULE_STATUS).should('have.text', '—'); - // this is a made-up index that has just the necessary // mappings to conduct tests, avoiding loading large // amounts of data like in auditbeat_exceptions esArchiverLoad('exceptions'); - + loginAndWaitForPageWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + createExceptionList(getExceptionList(), getExceptionList().list_id).then((response) => + createCustomRule({ + ...getNewRule(), + index: ['exceptions-*'], + exceptionLists: [ + { + id: response.body.id, + list_id: getExceptionList().list_id, + type: getExceptionList().type, + namespace_type: getExceptionList().namespace_type, + }, + ], + }) + ); + reload(); + goToRuleDetails(); + cy.get(RULE_STATUS).should('have.text', '—'); goToExceptionsTab(); }); @@ -62,7 +83,12 @@ describe('Exceptions flyout', () => { }); it('Does not overwrite values and-ed together', () => { - cy.get(ADD_EXCEPTIONS_BTN).click({ force: true }); + cy.root() + .pipe(($el) => { + $el.find(ADD_EXCEPTIONS_BTN).trigger('click'); + return $el.find(ADD_AND_BTN); + }) + .should('be.visible'); // add multiple entries with invalid field values addExceptionEntryFieldValue('agent.name', 0); @@ -80,8 +106,12 @@ describe('Exceptions flyout', () => { }); it('Does not overwrite values or-ed together', () => { - cy.get(ADD_EXCEPTIONS_BTN).click({ force: true }); - + cy.root() + .pipe(($el) => { + $el.find(ADD_EXCEPTIONS_BTN).trigger('click'); + return $el.find(ADD_AND_BTN); + }) + .should('be.visible'); // exception item 1 addExceptionEntryFieldValueOfItemX('agent.name', 0, 0); cy.get(ADD_AND_BTN).click(); @@ -197,11 +227,89 @@ describe('Exceptions flyout', () => { }); it('Contains custom index fields', () => { - cy.get(ADD_EXCEPTIONS_BTN).click({ force: true }); - + cy.root() + .pipe(($el) => { + $el.find(ADD_EXCEPTIONS_BTN).trigger('click'); + return $el.find(ADD_AND_BTN); + }) + .should('be.visible'); cy.get(FIELD_INPUT).eq(0).click({ force: true }); cy.get(EXCEPTION_FIELD_LIST).contains('unique_value.test'); closeExceptionBuilderFlyout(); }); + + describe('flyout errors', () => { + before(() => { + // create exception item via api + createExceptionListItem(getExceptionList().list_id, { + list_id: getExceptionList().list_id, + item_id: 'simple_list_item', + tags: [], + type: 'simple', + description: 'Test exception item', + name: 'Sample Exception List Item', + namespace_type: 'single', + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match_any', + value: ['some host', 'another host'], + }, + ], + }); + + reload(); + cy.get(RULE_STATUS).should('have.text', '—'); + goToExceptionsTab(); + }); + + context('When updating an item with version conflict', () => { + it('Displays version conflict error', () => { + cy.get(EDIT_EXCEPTIONS_BTN).should('be.visible'); + cy.get(EDIT_EXCEPTIONS_BTN).click({ force: true }); + + // update exception item via api + updateExceptionListItem('simple_list_item', { + name: 'Updated item name', + item_id: 'simple_list_item', + tags: [], + type: 'simple', + description: 'Test exception item', + namespace_type: 'single', + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match_any', + value: ['some host', 'another host'], + }, + ], + }); + + // try to save and see version conflict error + cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).click({ force: true }); + + cy.get(EXCEPTION_FLYOUT_VERSION_CONFLICT).should('be.visible'); + + closeExceptionBuilderFlyout(); + }); + }); + + context('When updating an item for a list that has since been deleted', () => { + it('Displays missing exception list error', () => { + cy.get(EDIT_EXCEPTIONS_BTN).should('be.visible'); + cy.get(EDIT_EXCEPTIONS_BTN).click({ force: true }); + + // delete exception list via api + deleteExceptionList(getExceptionList().list_id, getExceptionList().namespace_type); + + // try to save and see error + cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).click({ force: true }); + + cy.get(EXCEPTION_FLYOUT_LIST_DELETED_ERROR).should('be.visible'); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts index c28c55e0eb3f7f..47e71345ff0c49 100644 --- a/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts @@ -8,7 +8,7 @@ import { FIELDS_BROWSER_CHECKBOX, FIELDS_BROWSER_CONTAINER, - FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, + FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES, } from '../../screens/fields_browser'; import { HOST_GEO_CITY_NAME_HEADER, @@ -17,7 +17,11 @@ import { SERVER_SIDE_EVENT_COUNT, } from '../../screens/hosts/events'; -import { closeFieldsBrowser, filterFieldsBrowser } from '../../tasks/fields_browser'; +import { + closeFieldsBrowser, + filterFieldsBrowser, + toggleCategory, +} from '../../tasks/fields_browser'; import { loginAndWaitForPage } from '../../tasks/login'; import { openEvents } from '../../tasks/hosts/main'; import { @@ -60,11 +64,13 @@ describe('Events Viewer', () => { cy.get(FIELDS_BROWSER_CONTAINER).should('not.exist'); }); - it('displays the `default ECS` category (by default)', () => { - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE).should('have.text', 'default ECS'); + it('displays all categories (by default)', () => { + cy.get(FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES).should('be.empty'); }); it('displays a checked checkbox for all of the default events viewer columns that are also in the default ECS category', () => { + const category = 'default ECS'; + toggleCategory(category); defaultHeadersInDefaultEcsCategory.forEach((header) => cy.get(FIELDS_BROWSER_CHECKBOX(header.id)).should('be.checked') ); diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/host_risk_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/hosts/host_risk_tab.spec.ts index 38a639e19c6b87..3af77036649aa1 100644 --- a/x-pack/plugins/security_solution/cypress/integration/hosts/host_risk_tab.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/hosts/host_risk_tab.spec.ts @@ -7,14 +7,20 @@ import { cleanKibana } from '../../tasks/common'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; -import { navigateToHostRiskDetailTab } from '../../tasks/host_risk'; +import { + navigateToHostRiskDetailTab, + openRiskTableFilterAndSelectTheCriticalOption, + removeCritialFilter, + selectFiveItemsPerPageOption, +} from '../../tasks/host_risk'; import { HOST_BY_RISK_TABLE_CELL, - HOST_BY_RISK_TABLE_FILTER, - HOST_BY_RISK_TABLE_FILTER_CRITICAL, + HOST_BY_RISK_TABLE_HOSTNAME_CELL, + HOST_BY_RISK_TABLE_NEXT_PAGE_BUTTON, } from '../../screens/hosts/host_risk'; import { loginAndWaitForPage } from '../../tasks/login'; import { HOSTS_URL } from '../../urls/navigation'; +import { clearSearchBar, kqlSearch } from '../../tasks/security_header'; describe('risk tab', () => { before(() => { @@ -29,15 +35,30 @@ describe('risk tab', () => { }); it('renders the table', () => { + kqlSearch('host.name: "siem-kibana" {enter}'); cy.get(HOST_BY_RISK_TABLE_CELL).eq(3).should('have.text', 'siem-kibana'); cy.get(HOST_BY_RISK_TABLE_CELL).eq(4).should('have.text', '21.00'); cy.get(HOST_BY_RISK_TABLE_CELL).eq(5).should('have.text', 'Low'); + clearSearchBar(); }); it('filters the table', () => { - cy.get(HOST_BY_RISK_TABLE_FILTER).click(); - cy.get(HOST_BY_RISK_TABLE_FILTER_CRITICAL).click(); + openRiskTableFilterAndSelectTheCriticalOption(); cy.get(HOST_BY_RISK_TABLE_CELL).eq(3).should('not.have.text', 'siem-kibana'); + + removeCritialFilter(); + }); + + it('should be able to change items count per page', () => { + selectFiveItemsPerPageOption(); + + cy.get(HOST_BY_RISK_TABLE_HOSTNAME_CELL).should('have.length', 5); + }); + + it('should not allow page change when page is empty', () => { + kqlSearch('host.name: "nonexistent_host" {enter}'); + cy.get(HOST_BY_RISK_TABLE_NEXT_PAGE_BUTTON).should(`not.exist`); + clearSearchBar(); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/risky_hosts_panel.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview/risky_hosts_panel.spec.ts index 1c55a38b324953..652b3c1118b30e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview/risky_hosts_panel.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview/risky_hosts_panel.spec.ts @@ -69,7 +69,7 @@ describe('Risky Hosts Link Panel', () => { `${OVERVIEW_RISKY_HOSTS_LINKS} ${OVERVIEW_RISKY_HOSTS_LINKS_WARNING_INNER_PANEL}` ).should('not.exist'); cy.get(`${OVERVIEW_RISKY_HOSTS_VIEW_DASHBOARD_BUTTON}`).should('be.disabled'); - cy.get(`${OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 1 host'); + cy.get(`${OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 6 hosts'); changeSpace(testSpaceName); cy.visit(`/s/${testSpaceName}${OVERVIEW_URL}`); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts index 07ea4078ce7c4b..89a9fc4c0c6ba1 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts @@ -8,14 +8,13 @@ import { FIELDS_BROWSER_CATEGORIES_COUNT, FIELDS_BROWSER_FIELDS_COUNT, - FIELDS_BROWSER_HOST_CATEGORIES_COUNT, FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER, FIELDS_BROWSER_HEADER_HOST_GEO_CONTINENT_NAME_HEADER, FIELDS_BROWSER_MESSAGE_HEADER, - FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, - FIELDS_BROWSER_SELECTED_CATEGORY_COUNT, - FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT, FIELDS_BROWSER_FILTER_INPUT, + FIELDS_BROWSER_CATEGORIES_FILTER_CONTAINER, + FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES, + FIELDS_BROWSER_CATEGORY_BADGE, } from '../../screens/fields_browser'; import { TIMELINE_FIELDS_BUTTON } from '../../screens/timeline'; import { cleanKibana } from '../../tasks/common'; @@ -26,13 +25,14 @@ import { clearFieldsBrowser, closeFieldsBrowser, filterFieldsBrowser, + toggleCategoryFilter, removesMessageField, resetFields, + toggleCategory, } from '../../tasks/fields_browser'; import { loginAndWaitForPage } from '../../tasks/login'; import { openTimelineUsingToggle } from '../../tasks/security_main'; import { openTimelineFieldsBrowser, populateTimeline } from '../../tasks/timeline'; -import { ecsFieldMap } from '../../../../rule_registry/common/assets/field_maps/ecs_field_map'; import { HOSTS_URL } from '../../urls/navigation'; @@ -61,21 +61,8 @@ describe('Fields Browser', () => { clearFieldsBrowser(); }); - it('displays the `default ECS` category (by default)', () => { - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE).should('have.text', 'default ECS'); - }); - - it('the `defaultECS` (selected) category count matches the default timeline header count', () => { - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT).should( - 'have.text', - `${defaultHeaders.length}` - ); - }); - - it('displays a checked checkbox for all of the default timeline columns', () => { - defaultHeaders.forEach((header) => - cy.get(`[data-test-subj="field-${header.id}-checkbox"]`).should('be.checked') - ); + it('displays all categories (by default)', () => { + cy.get(FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES).should('be.empty'); }); it('displays the expected count of categories that match the filter input', () => { @@ -83,54 +70,50 @@ describe('Fields Browser', () => { filterFieldsBrowser(filterInput); - cy.get(FIELDS_BROWSER_CATEGORIES_COUNT).should('have.text', '2 categories'); + cy.get(FIELDS_BROWSER_CATEGORIES_COUNT).should('have.text', '2'); }); it('displays a search results label with the expected count of fields matching the filter input', () => { const filterInput = 'host.mac'; - filterFieldsBrowser(filterInput); - cy.get(FIELDS_BROWSER_HOST_CATEGORIES_COUNT) - .invoke('text') - .then((hostCategoriesCount) => { - cy.get(FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT) - .invoke('text') - .then((systemCategoriesCount) => { - cy.get(FIELDS_BROWSER_FIELDS_COUNT).should( - 'have.text', - `${+hostCategoriesCount + +systemCategoriesCount} fields` - ); - }); - }); - }); - - it('displays a count of only the fields in the selected category that match the filter input', () => { - const filterInput = 'host.geo.c'; + cy.get(FIELDS_BROWSER_FIELDS_COUNT).should('contain.text', '2'); + }); - filterFieldsBrowser(filterInput); + it('the `default ECS` category matches the default timeline header fields', () => { + const category = 'default ECS'; + toggleCategory(category); + cy.get(FIELDS_BROWSER_FIELDS_COUNT).should('contain.text', `${defaultHeaders.length}`); + + defaultHeaders.forEach((header) => { + cy.get(`[data-test-subj="field-${header.id}-checkbox"]`).should('be.checked'); + }); + toggleCategory(category); + }); + + it('creates the category badge when it is selected', () => { + const category = 'host'; + + cy.get(FIELDS_BROWSER_CATEGORY_BADGE(category)).should('not.exist'); + toggleCategory(category); + cy.get(FIELDS_BROWSER_CATEGORY_BADGE(category)).should('exist'); + toggleCategory(category); + }); + + it('search a category should match the category in the category filter', () => { + const category = 'host'; - const fieldsThatMatchFilterInput = Object.keys(ecsFieldMap).filter((fieldName) => { - const dotDelimitedFieldParts = fieldName.split('.'); - const fieldPartMatch = dotDelimitedFieldParts.filter((fieldPart) => { - const camelCasedStringsMatching = fieldPart - .split('_') - .some((part) => part.startsWith(filterInput)); - if (fieldPart.startsWith(filterInput)) { - return true; - } else if (camelCasedStringsMatching) { - return true; - } else { - return false; - } - }); - return fieldName.startsWith(filterInput) || fieldPartMatch.length > 0; - }).length; - - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT).should( - 'have.text', - fieldsThatMatchFilterInput - ); + filterFieldsBrowser(category); + toggleCategoryFilter(); + cy.get(FIELDS_BROWSER_CATEGORIES_FILTER_CONTAINER).should('contain.text', category); + }); + + it('search a category should filter out non matching categories in the category filter', () => { + const category = 'host'; + const categoryCheck = 'event'; + filterFieldsBrowser(category); + toggleCategoryFilter(); + cy.get(FIELDS_BROWSER_CATEGORIES_FILTER_CONTAINER).should('not.contain.text', categoryCheck); }); }); @@ -157,18 +140,15 @@ describe('Fields Browser', () => { cy.get(FIELDS_BROWSER_MESSAGE_HEADER).should('not.exist'); }); - it('selects a search results label with the expected count of categories matching the filter input', () => { - const category = 'host'; - filterFieldsBrowser(category); - - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE).should('have.text', category); - }); - it('adds a field to the timeline when the user clicks the checkbox', () => { const filterInput = 'host.geo.c'; - filterFieldsBrowser(filterInput); + closeFieldsBrowser(); cy.get(FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER).should('not.exist'); + + openTimelineFieldsBrowser(); + + filterFieldsBrowser(filterInput); addsHostGeoCityNameToTimeline(); closeFieldsBrowser(); diff --git a/x-pack/plugins/security_solution/cypress/objects/exception.ts b/x-pack/plugins/security_solution/cypress/objects/exception.ts index fbb000f43fdd23..637adc9fc01348 100644 --- a/x-pack/plugins/security_solution/cypress/objects/exception.ts +++ b/x-pack/plugins/security_solution/cypress/objects/exception.ts @@ -22,6 +22,17 @@ export interface ExceptionList { type: 'detection' | 'endpoint'; } +export interface ExceptionListItem { + description: string; + list_id: string; + item_id: string; + name: string; + namespace_type: 'single' | 'agnostic'; + tags: string[]; + type: 'simple'; + entries: Array<{ field: string; operator: string; type: string; value: string[] }>; +} + export const getExceptionList = (): ExceptionList => ({ description: 'Test exception list description', list_id: 'test_exception_list', diff --git a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts index e1b9e0639dfaad..c94c2be8b976f0 100644 --- a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts @@ -5,6 +5,8 @@ * 2.0. */ +export const EDIT_EXCEPTIONS_BTN = '[data-test-subj="exceptionsViewerEditBtn"]'; + export const ADD_EXCEPTIONS_BTN = '[data-test-subj="exceptionsHeaderAddExceptionBtn"]'; export const CLOSE_ALERTS_CHECKBOX = @@ -61,3 +63,10 @@ export const EXCEPTION_FIELD_LIST = '[data-test-subj="comboBoxOptionsList fieldAutocompleteComboBox-optionsList"]'; export const EXCEPTION_FLYOUT_TITLE = '[data-test-subj="exception-flyout-title"]'; + +export const EXCEPTION_EDIT_FLYOUT_SAVE_BTN = '[data-test-subj="edit-exception-confirm-button"]'; + +export const EXCEPTION_FLYOUT_VERSION_CONFLICT = + '[data-test-subj="exceptionsFlyoutVersionConflict"]'; + +export const EXCEPTION_FLYOUT_LIST_DELETED_ERROR = '[data-test-subj="errorCalloutContainer"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts b/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts index 4a5f813c301db0..66a7ba50c8070e 100644 --- a/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts +++ b/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts @@ -7,20 +7,16 @@ export const CLOSE_BTN = '[data-test-subj="close"]'; -export const FIELDS_BROWSER_CATEGORIES_COUNT = '[data-test-subj="categories-count"]'; +export const FIELDS_BROWSER_CONTAINER = '[data-test-subj="fields-browser-container"]'; export const FIELDS_BROWSER_CHECKBOX = (id: string) => { - return `[data-test-subj="category-table-container"] [data-test-subj="field-${id}-checkbox"]`; + return `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-${id}-checkbox"]`; }; -export const FIELDS_BROWSER_CONTAINER = '[data-test-subj="fields-browser-container"]'; - export const FIELDS_BROWSER_FIELDS_COUNT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="fields-count"]`; export const FIELDS_BROWSER_FILTER_INPUT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-search"]`; -export const FIELDS_BROWSER_HOST_CATEGORIES_COUNT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="host-category-count"]`; - export const FIELDS_BROWSER_HOST_GEO_CITY_NAME_CHECKBOX = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-host.geo.city_name-checkbox"]`; export const FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER = @@ -38,8 +34,22 @@ export const FIELDS_BROWSER_MESSAGE_HEADER = export const FIELDS_BROWSER_RESET_FIELDS = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="reset-fields"]`; -export const FIELDS_BROWSER_SELECTED_CATEGORY_COUNT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="selected-category-count-badge"]`; +export const FIELDS_BROWSER_CATEGORIES_FILTER_BUTTON = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="categories-filter-button"]`; +export const FIELDS_BROWSER_SELECTED_CATEGORY_COUNT = `${FIELDS_BROWSER_CATEGORIES_FILTER_BUTTON} span.euiNotificationBadge`; +export const FIELDS_BROWSER_CATEGORIES_COUNT = `${FIELDS_BROWSER_CATEGORIES_FILTER_BUTTON} span.euiNotificationBadge`; -export const FIELDS_BROWSER_SELECTED_CATEGORY_TITLE = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="selected-category-title"]`; +export const FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="category-badges"]`; +export const FIELDS_BROWSER_CATEGORY_BADGE = (id: string) => { + return `${FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES} [data-test-subj="category-badge-${id}"]`; +}; + +export const FIELDS_BROWSER_CATEGORIES_FILTER_CONTAINER = + '[data-test-subj="categories-selector-container"]'; +export const FIELDS_BROWSER_CATEGORIES_FILTER_SEARCH = + '[data-test-subj="categories-selector-search"]'; +export const FIELDS_BROWSER_CATEGORY_FILTER_OPTION = (id: string) => { + const idAttr = id.replace(/\s/g, ''); + return `${FIELDS_BROWSER_CATEGORIES_FILTER_CONTAINER} [data-test-subj="categories-selector-option-${idAttr}"]`; +}; export const FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="system-category-count"]`; diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts index 58331518255df7..3209200cf25a18 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts @@ -24,3 +24,14 @@ export const HOST_BY_RISK_TABLE_FILTER = '[data-test-subj="host-risk-filter-butt export const HOST_BY_RISK_TABLE_FILTER_CRITICAL = '[data-test-subj="host-risk-filter-item-Critical"]'; + +export const HOST_BY_RISK_TABLE_PERPAGE_BUTTON = + '[data-test-subj="loadingMoreSizeRowPopover"] button'; + +export const HOST_BY_RISK_TABLE_PERPAGE_OPTIONS = + '[data-test-subj="loadingMorePickSizeRow"] button'; + +export const HOST_BY_RISK_TABLE_NEXT_PAGE_BUTTON = + '[data-test-subj="numberedPagination"] [data-test-subj="pagination-button-next"]'; + +export const HOST_BY_RISK_TABLE_HOSTNAME_CELL = '[data-test-subj="render-content-host.name"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index a057f27df42807..a9134e5b124ebd 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -33,6 +33,8 @@ export const DETAILS_TITLE = '.euiDescriptionList__title'; export const EXCEPTIONS_TAB = '[data-test-subj="exceptionsTab"]'; +export const EXCEPTIONS_TAB_SEARCH = '[data-test-subj="exceptionsHeaderSearch"]'; + export const FALSE_POSITIVES_DETAILS = 'False positive examples'; export const INDEX_PATTERNS_DETAILS = 'Index patterns'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/exceptions.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/exceptions.ts index 7363bd5991b1c2..ab6c649c7c61f6 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/exceptions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ExceptionList } from '../../objects/exception'; +import { ExceptionList, ExceptionListItem } from '../../objects/exception'; export const createExceptionList = ( exceptionList: ExceptionList, @@ -23,3 +23,58 @@ export const createExceptionList = ( headers: { 'kbn-xsrf': 'cypress-creds' }, failOnStatusCode: false, }); + +export const createExceptionListItem = ( + exceptionListId: string, + exceptionListItem?: ExceptionListItem +) => + cy.request({ + method: 'POST', + url: '/api/exception_lists/items', + body: { + list_id: exceptionListItem?.list_id ?? exceptionListId, + item_id: exceptionListItem?.item_id ?? 'simple_list_item', + tags: exceptionListItem?.tags ?? ['user added string for a tag', 'malware'], + type: exceptionListItem?.type ?? 'simple', + description: exceptionListItem?.description ?? 'This is a sample endpoint type exception', + name: exceptionListItem?.name ?? 'Sample Exception List Item', + entries: exceptionListItem?.entries ?? [ + { + field: 'actingProcess.file.signer', + operator: 'excluded', + type: 'exists', + }, + { + field: 'host.name', + operator: 'included', + type: 'match_any', + value: ['some host', 'another host'], + }, + ], + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + failOnStatusCode: false, + }); + +export const updateExceptionListItem = ( + exceptionListItemId: string, + exceptionListItemUpdate?: Partial +) => + cy.request({ + method: 'PUT', + url: '/api/exception_lists/items', + body: { + item_id: exceptionListItemId, + ...exceptionListItemUpdate, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + failOnStatusCode: false, + }); + +export const deleteExceptionList = (listId: string, namespaceType: string) => + cy.request({ + method: 'DELETE', + url: `/api/exception_lists?list_id=${listId}&namespace_type=${namespaceType}`, + headers: { 'kbn-xsrf': 'cypress-creds' }, + failOnStatusCode: false, + }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts index 941a19669f2efb..04b59305b591a2 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts @@ -13,6 +13,9 @@ import { FIELDS_BROWSER_RESET_FIELDS, FIELDS_BROWSER_CHECKBOX, CLOSE_BTN, + FIELDS_BROWSER_CATEGORIES_FILTER_BUTTON, + FIELDS_BROWSER_CATEGORY_FILTER_OPTION, + FIELDS_BROWSER_CATEGORIES_FILTER_SEARCH, } from '../screens/fields_browser'; export const addsFields = (fields: string[]) => { @@ -34,10 +37,9 @@ export const addsHostGeoContinentNameToTimeline = () => { }; export const clearFieldsBrowser = () => { - cy.clock(); - cy.get(FIELDS_BROWSER_FILTER_INPUT).type('{selectall}{backspace}'); - cy.wait(0); - cy.tick(1000); + cy.get(FIELDS_BROWSER_FILTER_INPUT) + .type('{selectall}{backspace}') + .waitUntil((subject) => !subject.hasClass('euiFieldSearch-isLoading')); }; export const closeFieldsBrowser = () => { @@ -46,12 +48,21 @@ export const closeFieldsBrowser = () => { }; export const filterFieldsBrowser = (fieldName: string) => { - cy.clock(); - cy.get(FIELDS_BROWSER_FILTER_INPUT).type(fieldName, { delay: 50 }); - cy.wait(0); - cy.tick(1000); - // the text filter is debounced by 250 ms, wait 1s for changes to be applied - cy.get(FIELDS_BROWSER_FILTER_INPUT).should('not.have.class', 'euiFieldSearch-isLoading'); + cy.get(FIELDS_BROWSER_FILTER_INPUT) + .clear() + .type(fieldName) + .waitUntil((subject) => !subject.hasClass('euiFieldSearch-isLoading')); +}; + +export const toggleCategoryFilter = () => { + cy.get(FIELDS_BROWSER_CATEGORIES_FILTER_BUTTON).click({ force: true }); +}; + +export const toggleCategory = (category: string) => { + toggleCategoryFilter(); + cy.get(FIELDS_BROWSER_CATEGORIES_FILTER_SEARCH).clear().type(category); + cy.get(FIELDS_BROWSER_CATEGORY_FILTER_OPTION(category)).click({ force: true }); + toggleCategoryFilter(); }; export const removesMessageField = () => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts b/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts index 7a357e8a5c7fb2..afa04bb6de0ca9 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts @@ -5,7 +5,15 @@ * 2.0. */ -import { LOADING_TABLE, RISK_DETAILS_NAV, RISK_FLYOUT_TRIGGER } from '../screens/hosts/host_risk'; +import { + HOST_BY_RISK_TABLE_FILTER, + HOST_BY_RISK_TABLE_FILTER_CRITICAL, + HOST_BY_RISK_TABLE_PERPAGE_BUTTON, + HOST_BY_RISK_TABLE_PERPAGE_OPTIONS, + LOADING_TABLE, + RISK_DETAILS_NAV, + RISK_FLYOUT_TRIGGER, +} from '../screens/hosts/host_risk'; export const navigateToHostRiskDetailTab = () => cy.get(RISK_DETAILS_NAV).click(); @@ -15,3 +23,15 @@ export const waitForTableToLoad = () => { cy.get(LOADING_TABLE).should('exist'); cy.get(LOADING_TABLE).should('not.exist'); }; + +export const openRiskTableFilterAndSelectTheCriticalOption = () => { + cy.get(HOST_BY_RISK_TABLE_FILTER).click(); + cy.get(HOST_BY_RISK_TABLE_FILTER_CRITICAL).click(); +}; +export const removeCritialFilter = () => { + cy.get(HOST_BY_RISK_TABLE_FILTER_CRITICAL).click(); +}; +export const selectFiveItemsPerPageOption = () => { + cy.get(HOST_BY_RISK_TABLE_PERPAGE_BUTTON).click(); + cy.get(HOST_BY_RISK_TABLE_PERPAGE_OPTIONS).first().click(); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index e69c217c4a764e..d42ebcf9da68ed 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -91,7 +91,12 @@ export const goToAlertsTab = () => { }; export const goToExceptionsTab = () => { - cy.get(EXCEPTIONS_TAB).click(); + cy.root() + .pipe(($el) => { + $el.find(EXCEPTIONS_TAB).trigger('click'); + return $el.find(ADD_EXCEPTIONS_BTN); + }) + .should('be.visible'); }; export const removeException = () => { diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index 2ecae444879082..cdc9cc9b6f32dc 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -34,7 +34,7 @@ jest.mock('../../../timelines/containers', () => ({ jest.mock('../../components/url_state/normalize_time_range.ts'); const mockUseCreateFieldButton = jest.fn().mockReturnValue(<>); -jest.mock('../../../timelines/components/create_field_button', () => ({ +jest.mock('../../../timelines/components/fields_browser/create_field_button', () => ({ useCreateFieldButton: (...params: unknown[]) => mockUseCreateFieldButton(...params), })); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 5e3fc4e81f9dc3..68c4af5ee2fe8f 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -30,9 +30,9 @@ import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../lib/cell_actions/constants'; import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana'; import { GraphOverlay } from '../../../timelines/components/graph_overlay'; import { + useFieldBrowserOptions, CreateFieldEditorActions, - useCreateFieldButton, -} from '../../../timelines/components/create_field_button'; +} from '../../../timelines/components/fields_browser'; const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = []; @@ -177,7 +177,11 @@ const StatefulEventsViewerComponent: React.FC = ({ }, [id, timelineQuery, globalQuery]); const bulkActions = useMemo(() => ({ onAlertStatusActionSuccess }), [onAlertStatusActionSuccess]); - const createFieldComponent = useCreateFieldButton(scopeId, id, editorActionsRef); + const fieldBrowserOptions = useFieldBrowserOptions({ + sourcererScope: scopeId, + timelineId: id, + editorActionsRef, + }); const casesPermissions = useGetUserCasesPermissions(); const CasesContext = casesUi.getCasesContext(); @@ -201,6 +205,7 @@ const StatefulEventsViewerComponent: React.FC = ({ docValueFields, end, entityType, + fieldBrowserOptions, filters: globalFilters, filterStatus: currentFilter, globalFullScreen, @@ -228,7 +233,6 @@ const StatefulEventsViewerComponent: React.FC = ({ trailingControlColumns, type: 'embedded', unit, - createFieldComponent, })} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx index 6d01908732ec1b..0ed810b4ad2639 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx @@ -410,27 +410,35 @@ export const EditExceptionFlyout = memo(function EditExceptionFlyout({ )} - {updateError != null && ( - - - - )} - {hasVersionConflict && ( - - -

{i18n.VERSION_CONFLICT_ERROR_DESCRIPTION}

-
-
- )} - {updateError == null && ( - + + + {hasVersionConflict && ( + <> + +

{i18n.VERSION_CONFLICT_ERROR_DESCRIPTION}

+
+ + + )} + {updateError != null && ( + <> + + + + )} + {updateError === null && ( {i18n.CANCEL} @@ -446,8 +454,8 @@ export const EditExceptionFlyout = memo(function EditExceptionFlyout({ {i18n.EDIT_EXCEPTION_SAVE_BUTTON}
- - )} + )} + ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx index 64c3584bc668cd..0c09dce9c07cb3 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx @@ -327,6 +327,33 @@ describe('Paginated Table Component', () => { ); expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); }); + + test('Should hide pagination if totalCount is zero', () => { + const wrapper = mount( + + {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={DEFAULT_MAX_TABLE_QUERY_SIZE} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={0} + updateActivePage={updateActivePage} + updateLimitPagination={(limit) => updateLimitPagination({ limit })} + /> +
+ ); + + expect(wrapper.find('[data-test-subj="numberedPagination"]').exists()).toBeFalsy(); + }); }); describe('Events', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx index 6100c03d38bfa1..310ab039057c2f 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx @@ -303,12 +303,14 @@ const PaginatedTableComponent: FC = ({ - + {totalCount > 0 && ( + + )} {(isInspect || myLoading) && ( diff --git a/x-pack/plugins/security_solution/public/common/components/truncatable_text/truncatable_text.tsx b/x-pack/plugins/security_solution/public/common/components/truncatable_text/truncatable_text.tsx index cc1c53d1071002..27369dadb8a3bf 100644 --- a/x-pack/plugins/security_solution/public/common/components/truncatable_text/truncatable_text.tsx +++ b/x-pack/plugins/security_solution/public/common/components/truncatable_text/truncatable_text.tsx @@ -16,7 +16,7 @@ import { EuiToolTip } from '@elastic/eui'; * Note: Requires a parent container with a defined width or max-width. */ -const EllipsisText = styled.span` +export const EllipsisText = styled.span` &, & * { display: inline-block; diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_timelines_plugin.tsx b/x-pack/plugins/security_solution/public/common/mock/mock_timelines_plugin.tsx index 9a757763043492..e2b92b0eeae410 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_timelines_plugin.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/mock_timelines_plugin.tsx @@ -15,18 +15,4 @@ export const mockTimelines = { onBlur: jest.fn(), onKeyDown: jest.fn(), }), - getAddToCasePopover: jest - .fn() - .mockReturnValue(
{'Add to case'}
), - getAddToCaseAction: jest.fn(), - getAddToExistingCaseButton: jest.fn().mockReturnValue( -
- {'Add to existing case'} -
- ), - getAddToNewCaseButton: jest.fn().mockReturnValue( -
- {'Add to new case'} -
- ), }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 1499e803fdf37e..0f6d2d260ae0d7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -99,7 +99,6 @@ export const AlertsTableComponent: React.FC = ({ const { browserFields, indexPattern: indexPatterns, - loading: indexPatternsLoading, selectedPatterns, } = useSourcererDataView(SourcererScopeName.detections); const kibana = useKibana(); @@ -360,7 +359,7 @@ export const AlertsTableComponent: React.FC = ({ const casesPermissions = useGetUserCasesPermissions(); const CasesContext = kibana.services.cases.getCasesContext(); - if (loading || indexPatternsLoading || isEmpty(selectedPatterns)) { + if (loading || isEmpty(selectedPatterns)) { return null; } diff --git a/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx index 18952feee528bd..3cf344c691ccdd 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React from 'react'; import { Route, Switch } from 'react-router-dom'; import { ALERTS_PATH, SecurityPageName } from '../../../../common/constants'; @@ -13,9 +13,8 @@ import { NotFoundPage } from '../../../app/404'; import * as i18n from './translations'; import { TrackApplicationView } from '../../../../../../../src/plugins/usage_collection/public'; import { DetectionEnginePage } from '../../pages/detection_engine/detection_engine'; -import { useKibana } from '../../../common/lib/kibana'; import { SpyRoute } from '../../../common/utils/route/spy_routes'; -import { useAlertsPrivileges } from '../../containers/detection_engine/alerts/use_alerts_privileges'; +import { useReadonlyHeader } from '../../../use_readonly_header'; const AlertsRoute = () => ( @@ -25,24 +24,7 @@ const AlertsRoute = () => ( ); const AlertsContainerComponent: React.FC = () => { - const { chrome } = useKibana().services; - const { hasIndexRead, hasIndexWrite } = useAlertsPrivileges(); - - useEffect(() => { - // if the user is read only then display the glasses badge in the global navigation header - if (!hasIndexWrite && hasIndexRead) { - chrome.setBadge({ - text: i18n.READ_ONLY_BADGE_TEXT, - tooltip: i18n.READ_ONLY_BADGE_TOOLTIP, - iconType: 'glasses', - }); - } - - // remove the icon after the component unmounts - return () => { - chrome.setBadge(); - }; - }, [chrome, hasIndexRead, hasIndexWrite]); + useReadonlyHeader(i18n.READ_ONLY_BADGE_TOOLTIP); return ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/alerts/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/alerts/translations.ts index 734e93925e5365..de0b6a5f37d93c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alerts/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/alerts/translations.ts @@ -7,13 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const READ_ONLY_BADGE_TEXT = i18n.translate( - 'xpack.securitySolution.alerts.badge.readOnly.text', - { - defaultMessage: 'Read only', - } -); - export const READ_ONLY_BADGE_TOOLTIP = i18n.translate( 'xpack.securitySolution.alerts.badge.readOnly.tooltip', { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index e4f51b05ad6d9b..eccb2e081cd9de 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -140,7 +140,7 @@ const DetectionEnginePageComponent: React.FC = ({ const { formatUrl } = useFormatUrl(SecurityPageName.rules); const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false); const [showOnlyThreatIndicatorAlerts, setShowOnlyThreatIndicatorAlerts] = useState(false); - const loading = userInfoLoading || listsConfigLoading || isLoadingIndexPattern; + const loading = userInfoLoading || listsConfigLoading; const { application: { navigateToUrl }, timelines: timelinesUi, @@ -341,24 +341,32 @@ const DetectionEnginePageComponent: React.FC = ({ - + {isLoadingIndexPattern ? ( + + ) : ( + + )} - + {isLoadingIndexPattern ? ( + + ) : ( + + )} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index c40b6b95717241..65684a7c7d9de9 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -39,6 +39,7 @@ import { useUserData } from '../../../../../components/user_info'; import { userHasPermissions } from '../../helpers'; import { useListsConfig } from '../../../../../containers/detection_engine/lists/use_lists_config'; import { ExceptionsTableItem } from './types'; +import { MissingPrivilegesCallOut } from '../../../../../components/callouts/missing_privileges_callout'; export type Func = () => Promise; @@ -349,6 +350,7 @@ export const ExceptionListsTable = React.memo(() => { return ( <> + ( onChange={tableOnChangeCallback} pagination={paginationMemo} ref={tableRef} - selection={euiBasicTableSelectionProps} + selection={hasPermissions ? euiBasicTableSelectionProps : undefined} sorting={{ sort: { // EuiBasicTable has incorrect `sort.field` types which accept only `keyof Item` and reject fields in dot notation diff --git a/x-pack/plugins/security_solution/public/exceptions/routes.tsx b/x-pack/plugins/security_solution/public/exceptions/routes.tsx index a5b95ffa64d4d7..262db114ae84e4 100644 --- a/x-pack/plugins/security_solution/public/exceptions/routes.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/routes.tsx @@ -7,11 +7,13 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; +import * as i18n from './translations'; import { TrackApplicationView } from '../../../../../src/plugins/usage_collection/public'; import { EXCEPTIONS_PATH, SecurityPageName } from '../../common/constants'; import { ExceptionListsTable } from '../detections/pages/detection_engine/rules/all/exceptions/exceptions_table'; import { SpyRoute } from '../common/utils/route/spy_routes'; import { NotFoundPage } from '../app/404'; +import { useReadonlyHeader } from '../use_readonly_header'; const ExceptionsRoutes = () => { return ( @@ -22,7 +24,9 @@ const ExceptionsRoutes = () => { ); }; -const renderExceptionsRoutes = () => { +const ExceptionsContainerComponent: React.FC = () => { + useReadonlyHeader(i18n.READ_ONLY_BADGE_TOOLTIP); + return ( @@ -31,6 +35,10 @@ const renderExceptionsRoutes = () => { ); }; +const Exceptions = React.memo(ExceptionsContainerComponent); + +const renderExceptionsRoutes = () => ; + export const routes = [ { path: EXCEPTIONS_PATH, diff --git a/x-pack/plugins/timelines/public/components/actions/index.ts b/x-pack/plugins/security_solution/public/exceptions/translations.ts similarity index 52% rename from x-pack/plugins/timelines/public/components/actions/index.ts rename to x-pack/plugins/security_solution/public/exceptions/translations.ts index 9464a33082a495..780ed23a64ffe5 100644 --- a/x-pack/plugins/timelines/public/components/actions/index.ts +++ b/x-pack/plugins/security_solution/public/exceptions/translations.ts @@ -5,4 +5,11 @@ * 2.0. */ -export * from './timeline'; +import { i18n } from '@kbn/i18n'; + +export const READ_ONLY_BADGE_TOOLTIP = i18n.translate( + 'xpack.securitySolution.exceptions.badge.readOnly.tooltip', + { + defaultMessage: 'Unable to create, edit or delete exceptions', + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/host_risk_score/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/host_risk_score/index.tsx index 7f2c41f1414cfc..516895d49b8667 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/host_risk_score/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/host_risk_score/index.tsx @@ -180,7 +180,7 @@ export const useHostRiskScore = ({ factoryQueryType: HostsQueries.hostsRiskScore, filterQuery: createFilter(filterQuery), pagination: - cursorStart && querySize + cursorStart !== undefined && querySize !== undefined ? { cursorStart, querySize, diff --git a/x-pack/plugins/security_solution/public/rules/routes.tsx b/x-pack/plugins/security_solution/public/rules/routes.tsx index fcb434ae760ed1..4172e75a3cd96f 100644 --- a/x-pack/plugins/security_solution/public/rules/routes.tsx +++ b/x-pack/plugins/security_solution/public/rules/routes.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; +import * as i18n from './translations'; import { TrackApplicationView } from '../../../../../src/plugins/usage_collection/public'; import { RULES_PATH, SecurityPageName } from '../../common/constants'; import { NotFoundPage } from '../app/404'; @@ -14,6 +15,7 @@ import { RulesPage } from '../detections/pages/detection_engine/rules'; import { CreateRulePage } from '../detections/pages/detection_engine/rules/create'; import { RuleDetailsPage } from '../detections/pages/detection_engine/rules/details'; import { EditRulePage } from '../detections/pages/detection_engine/rules/edit'; +import { useReadonlyHeader } from '../use_readonly_header'; const RulesSubRoutes = [ { @@ -38,18 +40,26 @@ const RulesSubRoutes = [ }, ]; -const renderRulesRoutes = () => ( - - - {RulesSubRoutes.map((route, index) => ( - - - - ))} - - - -); +const RulesContainerComponent: React.FC = () => { + useReadonlyHeader(i18n.READ_ONLY_BADGE_TOOLTIP); + + return ( + + + {RulesSubRoutes.map((route, index) => ( + + + + ))} + + + + ); +}; + +const Rules = React.memo(RulesContainerComponent); + +const renderRulesRoutes = () => ; export const routes = [ { diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/index.tsx b/x-pack/plugins/security_solution/public/rules/translations.ts similarity index 53% rename from x-pack/plugins/timelines/public/components/actions/timeline/cases/index.tsx rename to x-pack/plugins/security_solution/public/rules/translations.ts index bb3bd63e316ed5..2d2c5de70dba9b 100644 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/index.tsx +++ b/x-pack/plugins/security_solution/public/rules/translations.ts @@ -5,7 +5,11 @@ * 2.0. */ -export * from './add_to_case_action'; -export * from './toaster_content'; -export * from './add_to_existing_case_button'; -export * from './add_to_new_case_button'; +import { i18n } from '@kbn/i18n'; + +export const READ_ONLY_BADGE_TOOLTIP = i18n.translate( + 'xpack.securitySolution.rules.badge.readOnly.tooltip', + { + defaultMessage: 'Unable to create, edit or delete rules', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.test.tsx similarity index 92% rename from x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.test.tsx rename to x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.test.tsx index 0afb2bf6413517..1bddd96c057277 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.test.tsx @@ -11,15 +11,15 @@ import { CreateFieldButton, CreateFieldEditorActions } from './index'; import { indexPatternFieldEditorPluginMock, Start, -} from '../../../../../../../src/plugins/data_view_field_editor/public/mocks'; +} from '../../../../../../../../src/plugins/data_view_field_editor/public/mocks'; -import { TestProviders } from '../../../common/mock'; -import { useKibana } from '../../../common/lib/kibana'; -import type { DataView } from '../../../../../../../src/plugins/data/common'; -import { TimelineId } from '../../../../common/types'; +import { TestProviders } from '../../../../common/mock'; +import { useKibana } from '../../../../common/lib/kibana'; +import type { DataView } from '../../../../../../../../src/plugins/data/common'; +import { TimelineId } from '../../../../../common/types'; let mockIndexPatternFieldEditor: Start; -jest.mock('../../../common/lib/kibana'); +jest.mock('../../../../common/lib/kibana'); const useKibanaMock = useKibana as jest.Mocked; const runAllPromises = () => new Promise(setImmediate); diff --git a/x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.tsx similarity index 80% rename from x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.tsx rename to x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.tsx index 8979a78d7aa465..645e1f0b29aed5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.tsx @@ -10,23 +10,26 @@ import { EuiButton } from '@elastic/eui'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; -import type { DataViewField, DataView } from '../../../../../../../src/plugins/data_views/common'; -import { useKibana } from '../../../common/lib/kibana'; +import type { + DataViewField, + DataView, +} from '../../../../../../../../src/plugins/data_views/common'; +import { useKibana } from '../../../../common/lib/kibana'; import * as i18n from './translations'; -import { CreateFieldComponentType, TimelineId } from '../../../../../timelines/common'; -import { upsertColumn } from '../../../../../timelines/public'; -import { useDataView } from '../../../common/containers/source/use_data_view'; -import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -import { sourcererSelectors } from '../../../common/store'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; -import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; -import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; +import { FieldBrowserOptions, TimelineId } from '../../../../../../timelines/common'; +import { upsertColumn } from '../../../../../../timelines/public'; +import { useDataView } from '../../../../common/containers/source/use_data_view'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { sourcererSelectors } from '../../../../common/store'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { DEFAULT_COLUMN_MIN_WIDTH } from '../../timeline/body/constants'; +import { defaultColumnHeaderType } from '../../timeline/body/column_headers/default_headers'; export type CreateFieldEditorActions = { closeEditor: () => void } | null; -type CreateFieldEditorActionsRef = MutableRefObject; +export type CreateFieldEditorActionsRef = MutableRefObject; -interface CreateFieldButtonProps { +export interface CreateFieldButtonProps { selectedDataViewId: string; onClick: () => void; timelineId: TimelineId; @@ -142,7 +145,7 @@ export const useCreateFieldButton = ( return; } // It receives onClick props from field browser in order to close the modal. - const CreateFieldButtonComponent: CreateFieldComponentType = ({ onClick }) => ( + const CreateFieldButtonComponent: FieldBrowserOptions['createFieldButton'] = ({ onClick }) => ( void; -}) => { - const keyboardHandlerRef = useRef(null); - const [closePopOverTrigger, setClosePopOverTrigger] = useState(false); - const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState(false); - const { timelines } = useKibana().services; - - const handleClosePopOverTrigger = useCallback(() => { - setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger); - - setHoverActionsOwnFocus((prevHoverActionsOwnFocus) => { - if (prevHoverActionsOwnFocus) { - // on the next tick, re-focus the keyboard handler if the hover actions owned focus - setTimeout(() => { - keyboardHandlerRef.current?.focus(); - }, 0); - } - return false; // always give up ownership - }); - - setTimeout(() => { - setHoverActionsOwnFocus(false); - }, 0); // invoked on the next tick, because we want to restore focus first - }, []); - - const openPopover = useCallback(() => { - setHoverActionsOwnFocus(true); - }, [setHoverActionsOwnFocus]); - - const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({ - closePopover: handleClosePopOverTrigger, - draggableId: getDraggableFieldId({ - contextId: `field-browser-field-items-field-draggable-${timelineId}-${categoryId}-${fieldName}`, - fieldId: fieldName, - }), - fieldName, - keyboardHandlerRef, - openPopover, - }); - - const onFocus = useCallback(() => { - keyboardHandlerRef.current?.focus(); - }, []); - - const onCloseRequested = useCallback(() => { - setHoverActionsOwnFocus((prevHoverActionOwnFocus) => - prevHoverActionOwnFocus ? false : prevHoverActionOwnFocus - ); - - setTimeout(() => { - onFocus(); // return focus to this draggable on the next tick, because we owned focus - }, 0); - }, [onFocus]); - - return ( -
- - {(provided) => ( -
- -
- )} -
-
- ); -}; - -export const DraggableFieldsBrowserField = React.memo(DraggableFieldsBrowserFieldComponent); -DraggableFieldsBrowserField.displayName = 'DraggableFieldsBrowserFieldComponent'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx deleted file mode 100644 index 5acc0ef9aa46b3..00000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx +++ /dev/null @@ -1,81 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount } from 'enzyme'; -import React from 'react'; -import { waitFor } from '@testing-library/react'; -import { mockBrowserFields } from '../../../common/containers/source/mock'; -import { TestProviders } from '../../../common/mock'; -import '../../../common/mock/match_media'; -import { getColumnsWithTimestamp } from '../../../common/components/event_details/helpers'; - -import { FieldName } from './field_name'; - -jest.mock('../../../common/lib/kibana'); - -const categoryId = 'base'; -const timestampFieldId = '@timestamp'; - -const defaultProps = { - categoryId, - categoryColumns: getColumnsWithTimestamp({ - browserFields: mockBrowserFields, - category: categoryId, - }), - closePopOverTrigger: false, - fieldId: timestampFieldId, - handleClosePopOverTrigger: jest.fn(), - hoverActionsOwnFocus: false, - onCloseRequested: jest.fn(), - onUpdateColumns: jest.fn(), - setClosePopOverTrigger: jest.fn(), -}; - -describe('FieldName', () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - - test('it renders the field name', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="field-name-${timestampFieldId}"]`).first().text() - ).toEqual(timestampFieldId); - }); - - test('it renders a copy to clipboard action menu item a user hovers over the name', async () => { - const wrapper = mount( - - - - ); - await waitFor(() => { - wrapper.find('[data-test-subj="withHoverActionsButton"]').simulate('mouseenter'); - wrapper.update(); - jest.runAllTimers(); - wrapper.update(); - expect(wrapper.find('[data-test-subj="hover-actions-copy-button"]').exists()).toBe(true); - }); - }); - - test('it highlights the text specified by the `highlight` prop', () => { - const highlight = 'stamp'; - - const wrapper = mount( - - - - ); - - expect(wrapper.find('mark').first().text()).toEqual(highlight); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx deleted file mode 100644 index 6e9672d08b3666..00000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx +++ /dev/null @@ -1,165 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiHighlight, EuiText } from '@elastic/eui'; -import React, { useCallback, useState, useMemo, useRef, useContext } from 'react'; -import styled from 'styled-components'; - -import { OnUpdateColumns } from '../timeline/events'; -import { WithHoverActions } from '../../../common/components/with_hover_actions'; -import { ColumnHeaderOptions } from '../../../../common/types'; -import { HoverActions } from '../../../common/components/hover_actions'; -import { TimelineContext } from '../../../../../timelines/public'; - -/** - * The name of a (draggable) field - */ -export const FieldNameContainer = styled.span` - border-radius: 4px; - display: flex; - padding: 0 4px 0 8px; - position: relative; - - &::before { - background-image: linear-gradient( - 135deg, - ${({ theme }) => theme.eui.euiColorMediumShade} 25%, - transparent 25% - ), - linear-gradient(-135deg, ${({ theme }) => theme.eui.euiColorMediumShade} 25%, transparent 25%), - linear-gradient(135deg, transparent 75%, ${({ theme }) => theme.eui.euiColorMediumShade} 75%), - linear-gradient(-135deg, transparent 75%, ${({ theme }) => theme.eui.euiColorMediumShade} 75%); - background-position: 0 0, 1px 0, 1px -1px, 0px 1px; - background-size: 2px 2px; - bottom: 2px; - content: ''; - display: block; - left: 2px; - position: absolute; - top: 2px; - width: 4px; - } - - &:hover, - &:focus { - transition: background-color 0.7s ease; - background-color: #000; - color: #fff; - - &::before { - background-image: linear-gradient(135deg, #fff 25%, transparent 25%), - linear-gradient( - -135deg, - ${({ theme }) => theme.eui.euiColorLightestShade} 25%, - transparent 25% - ), - linear-gradient( - 135deg, - transparent 75%, - ${({ theme }) => theme.eui.euiColorLightestShade} 75% - ), - linear-gradient( - -135deg, - transparent 75%, - ${({ theme }) => theme.eui.euiColorLightestShade} 75% - ); - } - } -`; - -FieldNameContainer.displayName = 'FieldNameContainer'; - -/** Renders a field name in it's non-dragging state */ -export const FieldName = React.memo<{ - categoryId: string; - categoryColumns: ColumnHeaderOptions[]; - closePopOverTrigger: boolean; - fieldId: string; - highlight?: string; - handleClosePopOverTrigger: () => void; - hoverActionsOwnFocus: boolean; - onCloseRequested: () => void; - onUpdateColumns: OnUpdateColumns; -}>( - ({ - closePopOverTrigger, - fieldId, - highlight = '', - handleClosePopOverTrigger, - hoverActionsOwnFocus, - onCloseRequested, - }) => { - const containerRef = useRef(null); - const [showTopN, setShowTopN] = useState(false); - const { timelineId: timelineIdFind } = useContext(TimelineContext); - - const toggleTopN = useCallback(() => { - setShowTopN((prevShowTopN) => { - const newShowTopN = !prevShowTopN; - if (newShowTopN === false) { - handleClosePopOverTrigger(); - } - return newShowTopN; - }); - }, [handleClosePopOverTrigger]); - - const closeTopN = useCallback(() => { - setShowTopN(false); - }, []); - - const hoverContent = useMemo( - () => ( - - ), - [ - closeTopN, - fieldId, - handleClosePopOverTrigger, - hoverActionsOwnFocus, - showTopN, - timelineIdFind, - toggleTopN, - ] - ); - - const render = useCallback( - () => ( - - - - {fieldId} - - - - ), - [fieldId, highlight] - ); - - return ( -
- -
- ); - } -); - -FieldName.displayName = 'FieldName'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.tsx new file mode 100644 index 00000000000000..b060575fdc5cb5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { + EuiToolTip, + EuiFlexGroup, + EuiFlexItem, + EuiScreenReaderOnly, + EuiHealth, + EuiBadge, + EuiIcon, + EuiText, + EuiHighlight, +} from '@elastic/eui'; +import type { FieldTableColumns } from '../../../../../../timelines/common/types'; +import * as i18n from './translations'; +import { + getExampleText, + getIconFromType, +} from '../../../../common/components/event_details/helpers'; +import { getEmptyValue } from '../../../../common/components/empty_value'; +import { EllipsisText } from '../../../../common/components/truncatable_text'; + +const TypeIcon = styled(EuiIcon)` + margin: 0 4px; + position: relative; + top: -1px; +`; +TypeIcon.displayName = 'TypeIcon'; + +export const Description = styled.span` + user-select: text; + width: 400px; +`; +Description.displayName = 'Description'; + +export const FieldName = React.memo<{ + fieldId: string; + highlight?: string; +}>(({ fieldId, highlight = '' }) => ( + + + {fieldId} + + +)); +FieldName.displayName = 'FieldName'; + +export const getFieldTableColumns = (highlight: string): FieldTableColumns => [ + { + field: 'name', + name: i18n.NAME, + render: (name: string, { type }) => { + return ( + + + + + + + + + + + + ); + }, + sortable: true, + width: '200px', + }, + { + field: 'description', + name: i18n.DESCRIPTION, + render: (description, { name, example }) => ( + + <> + +

{i18n.DESCRIPTION_FOR_FIELD(name)}

+
+ + + {`${description ?? getEmptyValue()} ${getExampleText(example)}`} + + + +
+ ), + sortable: true, + width: '400px', + }, + { + field: 'isRuntime', + name: i18n.RUNTIME, + render: (isRuntime: boolean) => + isRuntime ? : null, + sortable: true, + width: '80px', + }, + { + field: 'category', + name: i18n.CATEGORY, + render: (category: string, { name }) => ( + {category} + ), + sortable: true, + width: '100px', + }, +]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/translations.ts new file mode 100644 index 00000000000000..c16307250c2c81 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/translations.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const NAME = i18n.translate('xpack.securitySolution.fieldBrowser.fieldName', { + defaultMessage: 'Name', +}); + +export const DESCRIPTION = i18n.translate('xpack.securitySolution.fieldBrowser.descriptionLabel', { + defaultMessage: 'Description', +}); + +export const DESCRIPTION_FOR_FIELD = (field: string) => + i18n.translate('xpack.securitySolution.fieldBrowser.descriptionForScreenReaderOnly', { + values: { + field, + }, + defaultMessage: 'Description for field {field}:', + }); + +export const CATEGORY = i18n.translate('xpack.securitySolution.fieldBrowser.categoryLabel', { + defaultMessage: 'Category', +}); + +export const RUNTIME = i18n.translate('xpack.securitySolution.fieldBrowser.runtimeLabel', { + defaultMessage: 'Runtime', +}); + +export const RUNTIME_FIELD = i18n.translate('xpack.securitySolution.fieldBrowser.runtimeTitle', { + defaultMessage: 'Runtime Field', +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx new file mode 100644 index 00000000000000..46f2caa147a408 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TimelineId } from '../../../../common/types'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; +import { useCreateFieldButton, CreateFieldEditorActionsRef } from './create_field_button'; +import { getFieldTableColumns } from './field_table_columns'; + +export type { CreateFieldEditorActions } from './create_field_button'; + +export interface UseFieldBrowserOptions { + sourcererScope: SourcererScopeName; + timelineId: TimelineId; + editorActionsRef?: CreateFieldEditorActionsRef; +} + +export const useFieldBrowserOptions = ({ + sourcererScope, + timelineId, + editorActionsRef, +}: UseFieldBrowserOptions) => { + const createFieldButton = useCreateFieldButton(sourcererScope, timelineId, editorActionsRef); + return { + createFieldButton, + getFieldTableColumns, + }; +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx index 9636aadbc08e3c..0e26edc6ae1c1a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx @@ -86,7 +86,7 @@ const HeaderActionsComponent: React.FC = ({ sort, tabType, timelineId, - createFieldComponent, + fieldBrowserOptions, }) => { const { timelines: timelinesUi } = useKibana().services; const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); @@ -184,7 +184,7 @@ const HeaderActionsComponent: React.FC = ({ browserFields, columnHeaders, timelineId, - createFieldComponent, + options: fieldBrowserOptions, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx index aec28732f38afa..7e3de3514f5a78 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx @@ -28,7 +28,7 @@ import { HeaderActions } from '../actions/header_actions'; jest.mock('../../../../../common/lib/kibana'); const mockUseCreateFieldButton = jest.fn().mockReturnValue(<>); -jest.mock('../../../create_field_button', () => ({ +jest.mock('../../../fields_browser/create_field_button', () => ({ useCreateFieldButton: (...params: unknown[]) => mockUseCreateFieldButton(...params), })); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index ca1cdef903de84..e58dd520181c1a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -34,7 +34,7 @@ import { Sort } from '../sort'; import { ColumnHeader } from './column_header'; import { SourcererScopeName } from '../../../../../common/store/sourcerer/model'; -import { CreateFieldEditorActions, useCreateFieldButton } from '../../../create_field_button'; +import { useFieldBrowserOptions, CreateFieldEditorActions } from '../../../fields_browser'; export interface ColumnHeadersComponentProps { actionsColumnWidth: number; @@ -190,11 +190,11 @@ export const ColumnHeadersComponent = ({ [trailingControlColumns] ); - const createFieldComponent = useCreateFieldButton( - SourcererScopeName.timeline, - timelineId as TimelineId, - fieldEditorActionsRef - ); + const fieldBrowserOptions = useFieldBrowserOptions({ + sourcererScope: SourcererScopeName.timeline, + timelineId: timelineId as TimelineId, + editorActionsRef: fieldEditorActionsRef, + }); const LeadingHeaderActions = useMemo(() => { return leadingHeaderCells.map( @@ -221,7 +221,7 @@ export const ColumnHeadersComponent = ({ sort={sort} tabType={tabType} timelineId={timelineId} - createFieldComponent={createFieldComponent} + fieldBrowserOptions={fieldBrowserOptions} /> )} @@ -234,7 +234,7 @@ export const ColumnHeadersComponent = ({ actionsColumnWidth, browserFields, columnHeaders, - createFieldComponent, + fieldBrowserOptions, isEventViewer, isSelectAllChecked, onSelectAll, @@ -270,7 +270,7 @@ export const ColumnHeadersComponent = ({ sort={sort} tabType={tabType} timelineId={timelineId} - createFieldComponent={createFieldComponent} + fieldBrowserOptions={fieldBrowserOptions} /> )} @@ -283,7 +283,7 @@ export const ColumnHeadersComponent = ({ actionsColumnWidth, browserFields, columnHeaders, - createFieldComponent, + fieldBrowserOptions, isEventViewer, isSelectAllChecked, onSelectAll, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx index 890175ac8daf9a..ac4a09c01cc062 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -52,17 +52,6 @@ jest.mock('../../../../../common/lib/kibana', () => ({ useGetUserCasesPermissions: jest.fn(), })); -jest.mock( - '../../../../../../../timelines/public/components/actions/timeline/cases/add_to_case_action', - () => { - return { - AddToCasePopover: () => { - return
{'Add to case'}
; - }, - }; - } -); - describe('EventColumnView', () => { useIsExperimentalFeatureEnabledMock.mockReturnValue(false); (useShallowEqualSelector as jest.Mock).mockReturnValue(TimelineType.default); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index f616b4afc2af5f..bcb4c01fa409f3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -61,10 +61,6 @@ jest.mock('../../../../common/lib/kibana', () => { onBlur: jest.fn(), onKeyDown: jest.fn(), }), - getAddToCasePopover: jest - .fn() - .mockReturnValue(
{'Add to case'}
), - getAddToCaseAction: jest.fn(), }, }, }), @@ -114,7 +110,7 @@ jest.mock('../../../../common/lib/helpers/scheduler', () => ({ maxDelay: () => 3000, })); -jest.mock('../../create_field_button', () => ({ +jest.mock('../../fields_browser/create_field_button', () => ({ useCreateFieldButton: () => <>, })); diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/index.tsx b/x-pack/plugins/security_solution/public/translations.ts similarity index 60% rename from x-pack/plugins/timelines/public/components/actions/timeline/index.tsx rename to x-pack/plugins/security_solution/public/translations.ts index c5a69fd5e57451..18c6ceea1f8377 100644 --- a/x-pack/plugins/timelines/public/components/actions/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/translations.ts @@ -5,4 +5,8 @@ * 2.0. */ -export * from './cases'; +import { i18n } from '@kbn/i18n'; + +export const READ_ONLY_BADGE_TEXT = i18n.translate('xpack.securitySolution.badge.readOnly.text', { + defaultMessage: 'Read only', +}); diff --git a/x-pack/plugins/security_solution/public/use_readonly_header.ts b/x-pack/plugins/security_solution/public/use_readonly_header.ts new file mode 100644 index 00000000000000..d48855b3971055 --- /dev/null +++ b/x-pack/plugins/security_solution/public/use_readonly_header.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect } from 'react'; + +import * as i18n from './translations'; +import { useKibana } from './common/lib/kibana'; +import { useAlertsPrivileges } from './detections/containers/detection_engine/alerts/use_alerts_privileges'; + +/** + * This component places a read-only icon badge in the header + * if user only has read *Kibana* privileges, not individual data index + * privileges + */ +export function useReadonlyHeader(tooltip: string) { + const { hasKibanaREAD, hasKibanaCRUD } = useAlertsPrivileges(); + const chrome = useKibana().services.chrome; + + useEffect(() => { + if (hasKibanaREAD && !hasKibanaCRUD) { + chrome.setBadge({ + text: i18n.READ_ONLY_BADGE_TEXT, + tooltip, + iconType: 'glasses', + }); + } + + // remove the icon after the component unmounts + return () => { + chrome.setBadge(); + }; + }, [chrome, hasKibanaREAD, hasKibanaCRUD, tooltip]); +} diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts index 4d9b67af6c311c..e18d104b0d73ac 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts @@ -44,6 +44,7 @@ export const createMockTelemetryReceiver = ( fetchTrustedApplications: jest.fn(), fetchEndpointList: jest.fn(), fetchDetectionRules: jest.fn().mockReturnValue({ body: null }), + fetchEndpointMetadata: jest.fn(), } as unknown as jest.Mocked; }; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts index 6e24cea41b7187..91054577656b13 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts @@ -80,6 +80,13 @@ export interface ITelemetryReceiver { TransportResult>, unknown> >; + fetchEndpointMetadata( + executeFrom: string, + executeTo: string + ): Promise< + TransportResult>, unknown> + >; + fetchDiagnosticAlerts( executeFrom: string, executeTo: string @@ -270,6 +277,53 @@ export class TelemetryReceiver implements ITelemetryReceiver { return this.esClient.search(query, { meta: true }); } + public async fetchEndpointMetadata(executeFrom: string, executeTo: string) { + if (this.esClient === undefined || this.esClient === null) { + throw Error('elasticsearch client is unavailable: cannot retrieve elastic endpoint metrics'); + } + + const query: SearchRequest = { + expand_wildcards: ['open' as const, 'hidden' as const], + index: `.ds-metrics-endpoint.metadata-*`, + ignore_unavailable: false, + size: 0, // no query results required - only aggregation quantity + body: { + query: { + range: { + '@timestamp': { + gte: executeFrom, + lt: executeTo, + }, + }, + }, + aggs: { + endpoint_metadata: { + terms: { + field: 'agent.id', + size: this.max_records, + }, + aggs: { + latest_metadata: { + top_hits: { + size: 1, + sort: [ + { + '@timestamp': { + order: 'desc' as const, + }, + }, + ], + }, + }, + }, + }, + }, + }, + }; + + return this.esClient.search(query, { meta: true }); + } + public async fetchDiagnosticAlerts(executeFrom: string, executeTo: string) { if (this.esClient === undefined || this.esClient === null) { throw Error('elasticsearch client is unavailable: cannot retrieve diagnostic alerts'); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts index e9cc36bbff907f..c2c318debccdaa 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts @@ -11,6 +11,8 @@ import type { EndpointMetricsAggregation, EndpointPolicyResponseAggregation, EndpointPolicyResponseDocument, + EndpointMetadataAggregation, + EndpointMetadataDocument, ESClusterInfo, ESLicense, } from '../types'; @@ -188,7 +190,36 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { ) : new Map(); - /** STAGE 4 - Create the telemetry log records + /** STAGE 4 - Fetch Endpoint Agent Metadata + * + * Reads Endpoint Agent metadata out of the `.ds-metrics-endpoint.metadata` data stream + * and buckets them by Endpoint Agent id and sorts by the top hit. The EP agent will + * report its metadata once per day OR every time a policy change has occured. If + * a metadata document(s) exists for an EP agent we map to fleet agent and policy + */ + if (endpointData.endpointMetadata === undefined) { + logger.debug(`no endpoint metadata to report`); + } + + const { body: endpointMetadataResponse } = endpointData.endpointMetadata as unknown as { + body: EndpointMetadataAggregation; + }; + + if (endpointMetadataResponse.aggregations === undefined) { + logger.debug(`no endpoint metadata to report`); + } + + const endpointMetadata = + endpointMetadataResponse.aggregations.endpoint_metadata.buckets.reduce( + (cache, endpointAgentId) => { + const doc = endpointAgentId.latest_metadata.hits.hits[0]; + cache.set(endpointAgentId.key, doc); + return cache; + }, + new Map() + ); + + /** STAGE 5 - Create the telemetry log records * * Iterates through the endpoint metrics documents at STAGE 1 and joins them together * to form the telemetry log that is sent back to Elastic Security developers to @@ -199,6 +230,7 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { const telemetryPayloads = endpointMetrics.map((endpoint) => { let policyConfig = null; let failedPolicy = null; + let endpointMetadataById = null; const fleetAgentId = endpoint.endpoint_metrics.elastic.agent.id; const endpointAgentId = endpoint.endpoint_agent; @@ -212,6 +244,10 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { } } + if (endpointMetadata) { + endpointMetadataById = endpointMetadata.get(endpointAgentId); + } + const { cpu, memory, @@ -242,6 +278,10 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { }, endpoint_meta: { os: endpoint.endpoint_metrics.host.os, + capabilities: + endpointMetadataById !== null && endpointMetadataById !== undefined + ? endpointMetadataById._source.Endpoint.capabilities + : [], }, policy_config: endpointPolicyDetail !== null ? endpointPolicyDetail : {}, policy_response: @@ -265,7 +305,7 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { }); /** - * STAGE 5 - Send the documents + * STAGE 6 - Send the documents * * Send the documents in a batches of maxTelemetryBatch */ @@ -287,11 +327,13 @@ async function fetchEndpointData( executeFrom: string, executeTo: string ) { - const [fleetAgentsResponse, epMetricsResponse, policyResponse] = await Promise.allSettled([ - receiver.fetchFleetAgents(), - receiver.fetchEndpointMetrics(executeFrom, executeTo), - receiver.fetchEndpointPolicyResponses(executeFrom, executeTo), - ]); + const [fleetAgentsResponse, epMetricsResponse, policyResponse, endpointMetadata] = + await Promise.allSettled([ + receiver.fetchFleetAgents(), + receiver.fetchEndpointMetrics(executeFrom, executeTo), + receiver.fetchEndpointPolicyResponses(executeFrom, executeTo), + receiver.fetchEndpointMetadata(executeFrom, executeTo), + ]); return { fleetAgentsResponse: @@ -300,5 +342,6 @@ async function fetchEndpointData( : EmptyFleetAgentResponse, endpointMetrics: epMetricsResponse.status === 'fulfilled' ? epMetricsResponse.value : undefined, epPolicyResponse: policyResponse.status === 'fulfilled' ? policyResponse.value : undefined, + endpointMetadata: endpointMetadata.status === 'fulfilled' ? endpointMetadata.value : undefined, }; } diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts index 35b531ae6941c3..c1c65a428f62d9 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts @@ -243,6 +243,44 @@ interface EndpointMetricOS { full: string; } +// EP Metadata + +export interface EndpointMetadataAggregation { + hits: { + total: { value: number }; + }; + aggregations: { + endpoint_metadata: { + buckets: Array<{ key: string; doc_count: number; latest_metadata: EndpointMetadataHits }>; + }; + }; +} + +interface EndpointMetadataHits { + hits: { + total: { value: number }; + hits: EndpointMetadataDocument[]; + }; +} + +export interface EndpointMetadataDocument { + _source: { + '@timestamp': string; + agent: { + id: string; + version: string; + }; + Endpoint: { + capabilities: string[]; + }; + elastic: { + agent: { + id: string; + }; + }; + }; +} + // List HTTP Types export const GetTrustedAppsRequestSchema = { diff --git a/x-pack/plugins/task_manager/server/usage/task_manager_usage_collector.test.ts b/x-pack/plugins/task_manager/server/usage/task_manager_usage_collector.test.ts index 9b66792efcd9e3..b7a52a7a41bcf6 100644 --- a/x-pack/plugins/task_manager/server/usage/task_manager_usage_collector.test.ts +++ b/x-pack/plugins/task_manager/server/usage/task_manager_usage_collector.test.ts @@ -9,7 +9,7 @@ import { merge } from 'lodash'; import { loggingSystemMock } from 'src/core/server/mocks'; import { Collector, - createCollectorFetchContextWithKibanaMock, + createCollectorFetchContextMock, createUsageCollectionSetupMock, } from 'src/plugins/usage_collection/server/mocks'; import { HealthStatus } from '../monitoring'; @@ -26,7 +26,7 @@ describe('registerTaskManagerUsageCollector', () => { it('should report telemetry on the ephemeral queue', async () => { const monitoringStats$ = new Subject(); const usageCollectionMock = createUsageCollectionSetupMock(); - const fetchContext = createCollectorFetchContextWithKibanaMock(); + const fetchContext = createCollectorFetchContextMock(); usageCollectionMock.makeUsageCollector.mockImplementation((config) => { collector = new Collector(logger, config); return createUsageCollectionSetupMock().makeUsageCollector(config); @@ -53,7 +53,7 @@ describe('registerTaskManagerUsageCollector', () => { it('should report telemetry on the excluded task types', async () => { const monitoringStats$ = new Subject(); const usageCollectionMock = createUsageCollectionSetupMock(); - const fetchContext = createCollectorFetchContextWithKibanaMock(); + const fetchContext = createCollectorFetchContextMock(); usageCollectionMock.makeUsageCollector.mockImplementation((config) => { collector = new Collector(logger, config); return createUsageCollectionSetupMock().makeUsageCollector(config); diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts index 7febebc2a51795..e1bea8d1aa0e18 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts @@ -112,7 +112,6 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { esClient, usageCollection, soClient, - kibanaRequest: undefined, refreshCache: false, }, context @@ -135,7 +134,6 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { esClient, usageCollection, soClient, - kibanaRequest: undefined, refreshCache: false, }, context @@ -163,7 +161,6 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { esClient, usageCollection, soClient, - kibanaRequest: undefined, refreshCache: false, }, context diff --git a/x-pack/plugins/timelines/common/index.ts b/x-pack/plugins/timelines/common/index.ts index 0002dd6eb14327..96728a07432fdb 100644 --- a/x-pack/plugins/timelines/common/index.ts +++ b/x-pack/plugins/timelines/common/index.ts @@ -20,7 +20,6 @@ export type { ActionProps, AlertWorkflowStatus, CellValueElementProps, - CreateFieldComponentType, ColumnId, ColumnRenderer, ColumnHeaderType, @@ -28,6 +27,7 @@ export type { ControlColumnProps, DataProvidersAnd, DataProvider, + FieldBrowserOptions, GenericActionRowCellRenderProps, HeaderActionProps, HeaderCellRender, diff --git a/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts index 0b83cf28f9bb7a..544ca033b060cc 100644 --- a/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts @@ -11,6 +11,7 @@ import type { IEsSearchRequest, IEsSearchResponse, FieldSpec, + RuntimeField, } from '../../../../../../src/plugins/data/common'; import type { DocValueFields, Maybe } from '../common'; @@ -71,6 +72,7 @@ export interface BrowserField { type: string; subType?: IFieldSubType; readFromDocValues: boolean; + runtimeField?: RuntimeField; } export type BrowserFields = Readonly>>; diff --git a/x-pack/plugins/timelines/common/types/fields_browser/index.ts b/x-pack/plugins/timelines/common/types/fields_browser/index.ts new file mode 100644 index 00000000000000..7aac02be877d21 --- /dev/null +++ b/x-pack/plugins/timelines/common/types/fields_browser/index.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiBasicTableColumn } from '@elastic/eui'; +import { BrowserFields } from '../../search_strategy'; +import { ColumnHeaderOptions } from '../timeline/columns'; + +/** + * An item rendered in the table + */ +export interface BrowserFieldItem { + name: string; + type?: string; + description?: string; + example?: string; + category: string; + selected: boolean; + isRuntime: boolean; +} + +export type OnFieldSelected = (fieldId: string) => void; + +export type CreateFieldComponent = React.FC<{ + onClick: () => void; +}>; +export type FieldTableColumns = Array>; +export type GetFieldTableColumns = (highlight: string) => FieldTableColumns; +export interface FieldBrowserOptions { + createFieldButton?: CreateFieldComponent; + getFieldTableColumns?: GetFieldTableColumns; +} + +export interface FieldBrowserProps { + /** The timeline associated with this field browser */ + timelineId: string; + /** The timeline's current column headers */ + columnHeaders: ColumnHeaderOptions[]; + /** A map of categoryId -> metadata about the fields in that category */ + browserFields: BrowserFields; + /** When true, this Fields Browser is being used as an "events viewer" */ + isEventViewer?: boolean; + /** The options to customize the field browser, supporting columns rendering and button to create fields */ + options?: FieldBrowserOptions; + /** The width of the field browser */ + width?: number; +} diff --git a/x-pack/plugins/timelines/common/types/index.ts b/x-pack/plugins/timelines/common/types/index.ts index 9464a33082a495..f8e2b030638606 100644 --- a/x-pack/plugins/timelines/common/types/index.ts +++ b/x-pack/plugins/timelines/common/types/index.ts @@ -5,4 +5,5 @@ * 2.0. */ +export * from './fields_browser'; export * from './timeline'; diff --git a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts index 0662c63f35eddb..6a9c6bf8e74a00 100644 --- a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts @@ -7,11 +7,12 @@ import { ComponentType, JSXElementConstructor } from 'react'; import { EuiDataGridControlColumn, EuiDataGridCellValueElementProps } from '@elastic/eui'; -import { CreateFieldComponentType, OnRowSelected, SortColumnTimeline, TimelineTabs } from '..'; +import { OnRowSelected, SortColumnTimeline, TimelineTabs } from '..'; import { BrowserFields } from '../../../search_strategy/index_fields'; import { ColumnHeaderOptions } from '../columns'; import { TimelineNonEcsData } from '../../../search_strategy'; import { Ecs } from '../../../ecs'; +import { FieldBrowserOptions } from '../../fields_browser'; export interface ActionProps { action?: RowCellRender; @@ -67,7 +68,7 @@ export interface HeaderActionProps { width: number; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; - createFieldComponent?: CreateFieldComponentType; + fieldBrowserOptions?: FieldBrowserOptions; isEventViewer?: boolean; isSelectAllChecked: boolean; onSelectAll: ({ isSelected }: { isSelected: boolean }) => void; diff --git a/x-pack/plugins/timelines/common/types/timeline/index.ts b/x-pack/plugins/timelines/common/types/timeline/index.ts index 4ebc84a41f4b3f..a6c8ed1b74bfff 100644 --- a/x-pack/plugins/timelines/common/types/timeline/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/index.ts @@ -465,10 +465,6 @@ export enum TimelineTabs { eql = 'eql', } -export type CreateFieldComponentType = React.FC<{ - onClick: () => void; -}>; - // eslint-disable-next-line @typescript-eslint/no-explicit-any type EmptyObject = Partial>; diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.test.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.test.tsx deleted file mode 100644 index 80ae1cfa444656..00000000000000 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.test.tsx +++ /dev/null @@ -1,195 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount } from 'enzyme'; -import { - TestProviders, - mockGetAllCasesSelectorModal, - mockGetCreateCaseFlyout, -} from '../../../../mock'; -import { AddToCaseAction } from './add_to_case_action'; -import { SECURITY_SOLUTION_OWNER } from '../../../../../../cases/common'; -import { AddToCaseActionButton } from './add_to_case_action_button'; -import { ALERT_RULE_UUID } from '@kbn/rule-data-utils'; - -jest.mock('react-router-dom', () => ({ - useLocation: () => ({ - search: '', - }), -})); -jest.mock('./helpers'); - -describe('AddToCaseAction', () => { - const props = { - event: { - _id: 'test-id', - data: [], - ecs: { - _id: 'test-id', - _index: 'test-index', - signal: { rule: { id: ['rule-id'], name: ['rule-name'], false_positives: [] } }, - }, - }, - casePermissions: { - crud: true, - read: true, - }, - appId: 'securitySolutionUI', - owner: 'securitySolution', - onClose: () => null, - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('it renders', () => { - const wrapper = mount( - - - - - ); - - expect(wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).exists()).toBeTruthy(); - }); - - it('it opens the context menu', () => { - const wrapper = mount( - - - - - ); - - wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click'); - expect(wrapper.find(`[data-test-subj="add-new-case-item"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="add-existing-case-menu-item"]`).exists()).toBeTruthy(); - }); - - it('it opens the create case flyout', () => { - const wrapper = mount( - - - - - ); - - wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click'); - wrapper.find(`[data-test-subj="add-new-case-item"]`).first().simulate('click'); - expect(mockGetCreateCaseFlyout).toHaveBeenCalled(); - }); - - it('it opens the all cases modal', () => { - const wrapper = mount( - - - - - ); - - wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click'); - wrapper.find(`[data-test-subj="add-existing-case-menu-item"]`).first().simulate('click'); - - expect(mockGetAllCasesSelectorModal).toHaveBeenCalled(); - }); - - it('it set rule information as null when missing', () => { - const wrapper = mount( - - - - - ); - - wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click'); - wrapper.find(`[data-test-subj="add-existing-case-menu-item"]`).first().simulate('click'); - expect(mockGetAllCasesSelectorModal.mock.calls[0][0].alertData).toEqual({ - alertId: 'test-id', - index: 'test-index', - rule: { - id: 'rule-id', - name: null, - }, - owner: SECURITY_SOLUTION_OWNER, - }); - }); - - it('disabled when event type is not supported', () => { - const wrapper = mount( - - - - - ); - - expect( - wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('isDisabled') - ).toBeTruthy(); - }); - - it('hides the icon when user does not have crud permissions', () => { - const newProps = { - ...props, - casePermissions: { - crud: false, - read: true, - }, - }; - const wrapper = mount( - - - - - ); - - expect(wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).exists()).toBeFalsy(); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx deleted file mode 100644 index 193d63e2e849c7..00000000000000 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx +++ /dev/null @@ -1,147 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useMemo, useCallback } from 'react'; -import { useDispatch } from 'react-redux'; -import { - GetAllCasesSelectorModalProps, - GetCreateCaseFlyoutProps, -} from '../../../../../../cases/public'; -import { - CaseStatuses, - StatusAll, - CasesFeatures, - CommentType, -} from '../../../../../../cases/common'; -import { TimelineItem } from '../../../../../common/search_strategy'; -import { useAddToCase, normalizedEventFields } from '../../../../hooks/use_add_to_case'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { TimelinesStartServices } from '../../../../types'; -import { setOpenAddToExistingCase, setOpenAddToNewCase } from '../../../../store/t_grid/actions'; - -export interface AddToCaseActionProps { - event?: TimelineItem; - useInsertTimeline?: Function; - casePermissions: { - crud: boolean; - read: boolean; - } | null; - appId: string; - owner: string; - onClose?: Function; - casesFeatures?: CasesFeatures; -} - -const AddToCaseActionComponent: React.FC = ({ - event, - useInsertTimeline, - casePermissions, - appId, - owner, - onClose, - casesFeatures, -}) => { - const eventId = event?.ecs._id ?? ''; - const eventIndex = event?.ecs._index ?? ''; - const dispatch = useDispatch(); - const { cases } = useKibana().services; - const { - onCaseClicked, - onCaseSuccess, - onCaseCreated, - isAllCaseModalOpen, - isCreateCaseFlyoutOpen, - } = useAddToCase({ event, casePermissions, appId, owner, onClose }); - - const allCasesSelectorModalProps: GetAllCasesSelectorModalProps = useMemo(() => { - const { ruleId, ruleName } = normalizedEventFields(event); - return { - alertData: { - alertId: eventId, - index: eventIndex ?? '', - rule: { - id: ruleId, - name: ruleName, - }, - owner, - }, - hooks: { - useInsertTimeline, - }, - hiddenStatuses: [CaseStatuses.closed, StatusAll], - onRowClick: onCaseClicked, - updateCase: onCaseSuccess, - userCanCrud: casePermissions?.crud ?? false, - owner: [owner], - onClose: () => dispatch(setOpenAddToExistingCase({ id: eventId, isOpen: false })), - }; - }, [ - casePermissions?.crud, - onCaseSuccess, - onCaseClicked, - eventId, - eventIndex, - dispatch, - owner, - useInsertTimeline, - event, - ]); - - const closeCaseFlyoutOpen = useCallback(() => { - dispatch(setOpenAddToNewCase({ id: eventId, isOpen: false })); - }, [dispatch, eventId]); - - const createCaseFlyoutProps: GetCreateCaseFlyoutProps = useMemo(() => { - const { ruleId, ruleName } = normalizedEventFields(event); - const attachments = [ - { - alertId: eventId, - index: eventIndex ?? '', - rule: { - id: ruleId, - name: ruleName, - }, - owner, - type: CommentType.alert as const, - }, - ]; - return { - afterCaseCreated: onCaseCreated, - onClose: closeCaseFlyoutOpen, - onSuccess: onCaseSuccess, - useInsertTimeline, - owner: [owner], - userCanCrud: casePermissions?.crud ?? false, - features: casesFeatures, - attachments, - }; - }, [ - event, - eventId, - eventIndex, - owner, - onCaseCreated, - closeCaseFlyoutOpen, - onCaseSuccess, - useInsertTimeline, - casePermissions?.crud, - casesFeatures, - ]); - - return ( - <> - {isCreateCaseFlyoutOpen && cases.getCreateCaseFlyout(createCaseFlyoutProps)} - {isAllCaseModalOpen && cases.getAllCasesSelectorModal(allCasesSelectorModalProps)} - - ); -}; -AddToCaseActionComponent.displayName = 'AddToCaseAction'; - -export const AddToCaseAction = memo(AddToCaseActionComponent); - -// eslint-disable-next-line import/no-default-export -export default AddToCaseAction; diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action_button.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action_button.tsx deleted file mode 100644 index 9757bd94b17468..00000000000000 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action_button.tsx +++ /dev/null @@ -1,109 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useMemo } from 'react'; -import { - EuiPopover, - EuiButtonIcon, - EuiContextMenuPanel, - EuiText, - EuiContextMenuItem, - EuiToolTip, -} from '@elastic/eui'; -import { AddToCaseActionProps } from './add_to_case_action'; -import { useAddToCase } from '../../../../hooks/use_add_to_case'; -import { ActionIconItem } from '../../action_icon_item'; -import * as i18n from './translations'; - -const AddToCaseActionButtonComponent: React.FC = ({ - event, - useInsertTimeline, - casePermissions, - appId, - owner, - onClose, -}) => { - const { - addNewCaseClick, - addExistingCaseClick, - isDisabled, - userCanCrud, - isEventSupported, - openPopover, - closePopover, - isPopoverOpen, - } = useAddToCase({ event, useInsertTimeline, casePermissions, appId, owner, onClose }); - const tooltipContext = userCanCrud - ? isEventSupported - ? i18n.ACTION_ADD_TO_CASE_TOOLTIP - : i18n.UNSUPPORTED_EVENTS_MSG - : i18n.PERMISSIONS_MSG; - const items = useMemo( - () => [ - - {i18n.ACTION_ADD_NEW_CASE} - , - - {i18n.ACTION_ADD_EXISTING_CASE} - , - ], - [addExistingCaseClick, addNewCaseClick, isDisabled] - ); - - const button = useMemo( - () => ( - - - - ), - [isDisabled, openPopover, tooltipContext] - ); - - return ( - <> - {userCanCrud && ( - - - - - - )} - - ); -}; - -export const AddToCaseActionButton = memo(AddToCaseActionButtonComponent); - -// eslint-disable-next-line import/no-default-export -export default AddToCaseActionButton; diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_existing_case_button.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_existing_case_button.tsx deleted file mode 100644 index 4d19a890968820..00000000000000 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_existing_case_button.tsx +++ /dev/null @@ -1,76 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo } from 'react'; -import { EuiContextMenuItem } from '@elastic/eui'; - -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { TimelinesStartServices } from '../../../../types'; -import { useAddToCase } from '../../../../hooks/use_add_to_case'; -import { AddToCaseActionProps } from './add_to_case_action'; -import * as i18n from './translations'; - -interface AddToCaseActionButtonProps extends AddToCaseActionProps { - ariaLabel?: string; -} - -const AddToCaseActionButtonComponent: React.FC = ({ - ariaLabel = i18n.ACTION_ADD_TO_CASE_ARIA_LABEL, - event, - useInsertTimeline, - casePermissions, - appId, - owner, - onClose, -}) => { - const { onCaseSuccess, onCaseClicked, isDisabled, userCanCrud, caseAttachments } = useAddToCase({ - event, - useInsertTimeline, - casePermissions, - appId, - owner, - onClose, - }); - const { cases } = useKibana().services; - const addToCaseModal = cases.hooks.getUseCasesAddToExistingCaseModal({ - attachments: caseAttachments, - updateCase: onCaseSuccess, - onRowClick: onCaseClicked, - }); - - // TODO To be further refactored and moved to cases plugins - // https://github.com/elastic/kibana/issues/123183 - const handleClick = () => { - // close the popover - if (onClose) { - onClose(); - } - addToCaseModal.open(); - }; - - return ( - <> - {userCanCrud && ( - - {i18n.ACTION_ADD_EXISTING_CASE} - - )} - - ); -}; - -export const AddToExistingCaseButton = memo(AddToCaseActionButtonComponent); - -// eslint-disable-next-line import/no-default-export -export default AddToExistingCaseButton; diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_new_case_button.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_new_case_button.tsx deleted file mode 100644 index eb83cb21aea6ea..00000000000000 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_new_case_button.tsx +++ /dev/null @@ -1,77 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo } from 'react'; -import { EuiContextMenuItem } from '@elastic/eui'; - -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { TimelinesStartServices } from '../../../../types'; -import { useAddToCase } from '../../../../hooks/use_add_to_case'; -import { AddToCaseActionProps } from './add_to_case_action'; -import * as i18n from './translations'; - -export interface AddToNewCaseButtonProps extends AddToCaseActionProps { - ariaLabel?: string; -} - -const AddToNewCaseButtonComponent: React.FC = ({ - ariaLabel = i18n.ACTION_ADD_TO_CASE_ARIA_LABEL, - event, - useInsertTimeline, - casePermissions, - appId, - owner, - onClose, -}) => { - const { isDisabled, userCanCrud, caseAttachments, onCaseSuccess, onCaseCreated } = useAddToCase({ - event, - useInsertTimeline, - casePermissions, - appId, - owner, - onClose, - }); - const { cases } = useKibana().services; - const createCaseFlyout = cases.hooks.getUseCasesAddToNewCaseFlyout({ - attachments: caseAttachments, - afterCaseCreated: onCaseCreated, - onSuccess: onCaseSuccess, - }); - - // TODO To be further refactored and moved to cases plugins - // https://github.com/elastic/kibana/issues/123183 - const handleClick = () => { - // close the popover - if (onClose) { - onClose(); - } - createCaseFlyout.open(); - }; - - return ( - <> - {userCanCrud && ( - - {i18n.ACTION_ADD_NEW_CASE} - - )} - - ); -}; -AddToNewCaseButtonComponent.displayName = 'AddToNewCaseButton'; - -export const AddToNewCaseButton = memo(AddToNewCaseButtonComponent); - -// eslint-disable-next-line import/no-default-export -export default AddToNewCaseButton; diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/helpers.test.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/helpers.test.tsx deleted file mode 100644 index efb11393a42448..00000000000000 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/helpers.test.tsx +++ /dev/null @@ -1,45 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import 'jest-styled-components'; -import type { MockedKeys } from '@kbn/utility-types/jest'; -import { CoreStart } from 'kibana/public'; -import { coreMock } from 'src/core/public/mocks'; -import type { IToasts } from '../../../../../../../../src/core/public'; - -import { createUpdateSuccessToaster } from './helpers'; -import { Case } from '../../../../../../cases/common'; - -let mockCoreStart: MockedKeys; -let toasts: IToasts; -let toastsSpy: jest.SpyInstance; - -const theCase = { - id: 'case-id', - title: 'My case', - settings: { - syncAlerts: true, - }, -} as Case; - -describe('helpers', () => { - beforeEach(() => { - mockCoreStart = coreMock.createStart(); - }); - - describe('createUpdateSuccessToaster', () => { - it('creates the correct toast when the sync alerts is on', () => { - const onViewCaseClick = jest.fn(); - - toasts = mockCoreStart.notifications.toasts; - toastsSpy = jest.spyOn(mockCoreStart.notifications.toasts, 'addSuccess'); - createUpdateSuccessToaster(toasts, theCase, onViewCaseClick); - - expect(toastsSpy).toHaveBeenCalled(); - }); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/helpers.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/helpers.tsx deleted file mode 100644 index 71e2f41a5288a8..00000000000000 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/helpers.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import styled from 'styled-components'; -import { ToasterContent } from './toaster_content'; -import * as i18n from './translations'; -import type { Case } from '../../../../../../cases/common'; -import type { ToastsStart, Toast } from '../../../../../../../../src/core/public'; -import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; - -const LINE_CLAMP = 3; - -const Title = styled.span` - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: ${LINE_CLAMP}; - -webkit-box-orient: vertical; - overflow: hidden; -`; - -export const createUpdateSuccessToaster = ( - toasts: ToastsStart, - theCase: Case, - onViewCaseClick: (id: string) => void -): Toast => { - return toasts.addSuccess({ - color: 'success', - iconType: 'check', - title: toMountPoint({i18n.CASE_CREATED_SUCCESS_TOAST(theCase.title)}), - text: toMountPoint( - - ), - }); -}; diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/toaster_content.test.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/toaster_content.test.tsx deleted file mode 100644 index fd20366e7891f5..00000000000000 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/toaster_content.test.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount } from 'enzyme'; - -import { ToasterContent } from './toaster_content'; - -describe('ToasterContent', () => { - const onViewCaseClick = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('renders with syncAlerts=true', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="toaster-content-case-view-link"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="toaster-content-sync-text"]').exists()).toBeTruthy(); - }); - - it('renders with syncAlerts=false', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="toaster-content-case-view-link"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="toaster-content-sync-text"]').exists()).toBeFalsy(); - }); - - it('calls onViewCaseClick', () => { - const wrapper = mount( - - ); - - wrapper.find('[data-test-subj="toaster-content-case-view-link"]').first().simulate('click'); - expect(onViewCaseClick).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/toaster_content.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/toaster_content.tsx deleted file mode 100644 index 147dd3f5e8399b..00000000000000 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/toaster_content.tsx +++ /dev/null @@ -1,47 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useCallback } from 'react'; -import { EuiButtonEmpty, EuiText } from '@elastic/eui'; -import styled from 'styled-components'; - -import * as i18n from './translations'; - -const EuiTextStyled = styled(EuiText)` - ${({ theme }) => ` - margin-bottom: ${theme.eui?.paddingSizes?.s ?? 8}px; - `} -`; - -interface Props { - caseId: string; - syncAlerts: boolean; - onViewCaseClick: (id: string) => void; -} - -const ToasterContentComponent: React.FC = ({ caseId, syncAlerts, onViewCaseClick }) => { - const onClick = useCallback(() => onViewCaseClick(caseId), [caseId, onViewCaseClick]); - return ( - <> - {syncAlerts && ( - - {i18n.CASE_CREATED_SUCCESS_TOAST_TEXT} - - )} - - {i18n.VIEW_CASE} - - - ); -}; - -export const ToasterContent = memo(ToasterContentComponent); diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/translations.ts b/x-pack/plugins/timelines/public/components/actions/timeline/cases/translations.ts deleted file mode 100644 index df0dfb9048ace2..00000000000000 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/translations.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const ACTION_ADD_CASE = i18n.translate('xpack.timelines.cases.timeline.actions.addCase', { - defaultMessage: 'Add to case', -}); - -export const ACTION_ADD_NEW_CASE = i18n.translate( - 'xpack.timelines.cases.timeline.actions.addNewCase', - { - defaultMessage: 'Add to new case', - } -); - -export const ACTION_ADD_EXISTING_CASE = i18n.translate( - 'xpack.timelines.cases.timeline.actions.addExistingCase', - { - defaultMessage: 'Add to existing case', - } -); - -export const ACTION_ADD_TO_CASE_ARIA_LABEL = i18n.translate( - 'xpack.timelines.cases.timeline.actions.addToCaseAriaLabel', - { - defaultMessage: 'Attach alert to case', - } -); - -export const ACTION_ADD_TO_CASE_TOOLTIP = i18n.translate( - 'xpack.timelines.cases.timeline.actions.addToCaseTooltip', - { - defaultMessage: 'Add to case', - } -); - -export const CASE_CREATED_SUCCESS_TOAST = (title: string) => - i18n.translate('xpack.timelines.cases.timeline.actions.caseCreatedSuccessToast', { - values: { title }, - defaultMessage: 'An alert has been added to "{title}"', - }); - -export const CASE_CREATED_SUCCESS_TOAST_TEXT = i18n.translate( - 'xpack.timelines.cases.timeline.actions.caseCreatedSuccessToastText', - { - defaultMessage: 'Alerts in this case have their status synched with the case status', - } -); - -export const VIEW_CASE = i18n.translate( - 'xpack.timelines.cases.timeline.actions.caseCreatedSuccessToastViewCaseLink', - { - defaultMessage: 'View Case', - } -); - -export const PERMISSIONS_MSG = i18n.translate( - 'xpack.timelines.cases.timeline.actions.permissionsMessage', - { - defaultMessage: - 'You are currently missing the required permissions to attach alerts to cases. Please contact your administrator for further assistance.', - } -); - -export const UNSUPPORTED_EVENTS_MSG = i18n.translate( - 'xpack.timelines.cases.timeline.actions.unsupportedEventsMessage', - { - defaultMessage: 'This event cannot be attached to a case', - } -); diff --git a/x-pack/plugins/timelines/public/components/fields_browser/index.tsx b/x-pack/plugins/timelines/public/components/fields_browser/index.tsx index 31b8e9f62803ec..12133cbee303ea 100644 --- a/x-pack/plugins/timelines/public/components/fields_browser/index.tsx +++ b/x-pack/plugins/timelines/public/components/fields_browser/index.tsx @@ -9,9 +9,14 @@ import React from 'react'; import type { Store } from 'redux'; import { Provider } from 'react-redux'; import { I18nProvider } from '@kbn/i18n-react'; -import type { FieldBrowserProps } from '../t_grid/toolbar/fields_browser/types'; import { StatefulFieldsBrowser } from '../t_grid/toolbar/fields_browser'; -export type { FieldBrowserProps } from '../t_grid/toolbar/fields_browser/types'; +import { FieldBrowserProps } from '../../../common/types/fields_browser'; +export type { + CreateFieldComponent, + FieldBrowserOptions, + FieldBrowserProps, + GetFieldTableColumns, +} from '../../../common/types/fields_browser'; const EMPTY_BROWSER_FIELDS = {}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index 2ae0146f80f7e5..4ba36a3ec6419b 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -47,7 +47,6 @@ import { TimelineTabs, SetEventsLoading, SetEventsDeleted, - CreateFieldComponentType, } from '../../../../common/types/timeline'; import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; @@ -63,10 +62,11 @@ import { import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; import type { OnRowSelected, OnSelectAll } from '../types'; +import type { FieldBrowserOptions } from '../../../../common/types'; import type { Refetch } from '../../../store/t_grid/inputs'; import { getPageRowIndex } from '../../../../common/utils/pagination'; import { StatefulEventContext } from '../../../components/stateful_event_context'; -import { StatefulFieldsBrowser } from '../../../components/t_grid/toolbar/fields_browser'; +import { StatefulFieldsBrowser } from '../toolbar/fields_browser'; import { tGridActions, TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { RowAction } from './row_action'; @@ -88,10 +88,10 @@ interface OwnProps { appId?: string; browserFields: BrowserFields; bulkActions?: BulkActionsProp; - createFieldComponent?: CreateFieldComponentType; data: TimelineItem[]; defaultCellActions?: TGridCellAction[]; disabledCellActions: string[]; + fieldBrowserOptions?: FieldBrowserOptions; filters?: Filter[]; filterQuery?: string; filterStatus?: AlertStatus; @@ -149,8 +149,8 @@ const EuiDataGridContainer = styled.div<{ hideLastPage: boolean }>` const transformControlColumns = ({ columnHeaders, controlColumns, - createFieldComponent, data, + fieldBrowserOptions, isEventViewer = false, loadingEventIds, onRowSelected, @@ -171,9 +171,9 @@ const transformControlColumns = ({ }: { columnHeaders: ColumnHeaderOptions[]; controlColumns: ControlColumnProps[]; - createFieldComponent?: CreateFieldComponentType; data: TimelineItem[]; disabledCellActions: string[]; + fieldBrowserOptions?: FieldBrowserOptions; isEventViewer?: boolean; loadingEventIds: string[]; onRowSelected: OnRowSelected; @@ -209,6 +209,7 @@ const transformControlColumns = ({ )} @@ -303,10 +303,10 @@ export const BodyComponent = React.memo( bulkActions = true, clearSelected, columnHeaders, - createFieldComponent, data, defaultCellActions, disabledCellActions, + fieldBrowserOptions, filterQuery, filters, filterStatus, @@ -502,7 +502,7 @@ export const BodyComponent = React.memo( @@ -529,6 +529,7 @@ export const BodyComponent = React.memo( id, totalSelectAllAlerts, totalItems, + fieldBrowserOptions, filterStatus, filterQuery, indexNames, @@ -539,7 +540,6 @@ export const BodyComponent = React.memo( additionalControls, browserFields, columnHeaders, - createFieldComponent, ] ); @@ -629,9 +629,9 @@ export const BodyComponent = React.memo( transformControlColumns({ columnHeaders, controlColumns, - createFieldComponent, data, disabledCellActions, + fieldBrowserOptions, isEventViewer, loadingEventIds, onRowSelected, @@ -656,9 +656,9 @@ export const BodyComponent = React.memo( leadingControlColumns, trailingControlColumns, columnHeaders, - createFieldComponent, data, disabledCellActions, + fieldBrowserOptions, isEventViewer, id, loadingEventIds, diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index b97e4047d10e7e..69c04b31fa44be 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -21,7 +21,6 @@ import type { CoreStart } from '../../../../../../../src/core/public'; import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; import { BulkActionsProp, - CreateFieldComponentType, TGridCellAction, TimelineId, TimelineTabs, @@ -43,6 +42,7 @@ import { defaultHeaders } from '../body/column_headers/default_headers'; import { buildCombinedQuery, getCombinedFilterQuery, resolverIsShowing } from '../helpers'; import { tGridActions, tGridSelectors } from '../../../store/t_grid'; import { useTimelineEvents, InspectResponse, Refetch } from '../../../container'; +import { FieldBrowserOptions } from '../../fields_browser'; import { StatefulBody } from '../body'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexGroup, UpdatedFlexItem } from '../styles'; import { Sort } from '../body/sort'; @@ -98,7 +98,6 @@ export interface TGridIntegratedProps { browserFields: BrowserFields; bulkActions?: BulkActionsProp; columns: ColumnHeaderOptions[]; - createFieldComponent?: CreateFieldComponentType; data?: DataPublicPluginStart; dataProviders: DataProvider[]; dataViewId?: string | null; @@ -108,6 +107,7 @@ export interface TGridIntegratedProps { docValueFields: DocValueFields[]; end: string; entityType: EntityType; + fieldBrowserOptions?: FieldBrowserOptions; filters: Filter[]; filterStatus?: AlertStatus; globalFullScreen: boolean; @@ -153,12 +153,12 @@ const TGridIntegratedComponent: React.FC = ({ docValueFields, end, entityType, + fieldBrowserOptions, filters, filterStatus, globalFullScreen, graphEventId, graphOverlay = null, - createFieldComponent, hasAlertsCrud, id, indexNames, @@ -363,10 +363,10 @@ const TGridIntegratedComponent: React.FC = ({ appId={appId} browserFields={browserFields} bulkActions={bulkActions} - createFieldComponent={createFieldComponent} data={nonDeletedEvents} defaultCellActions={defaultCellActions} disabledCellActions={disabledCellActions} + fieldBrowserOptions={fieldBrowserOptions} filterQuery={filterQuery} filters={filters} filterStatus={filterStatus} diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx index f0488d26f5f16e..be71d159eafca3 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx @@ -8,7 +8,7 @@ import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useEffect, useMemo, useState, useRef } from 'react'; import styled from 'styled-components'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { Filter, Query } from '@kbn/es-query'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; @@ -39,7 +39,6 @@ import { LastUpdatedAt } from '../..'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem, UpdatedFlexGroup } from '../styles'; import { InspectButton, InspectButtonContainer } from '../../inspect'; import { useFetchIndex } from '../../../container/source'; -import { AddToCaseAction } from '../../actions/timeline/cases/add_to_case_action'; import { TGridLoading, TGridEmpty, TimelineContext } from '../shared'; const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` @@ -76,16 +75,7 @@ const ScrollableFlexItem = styled(EuiFlexItem)` overflow: auto; `; -const casesFeatures = { alerts: { sync: false } }; - export interface TGridStandaloneProps { - appId: string; - casesOwner: string; - casePermissions: { - crud: boolean; - read: boolean; - } | null; - afterCaseSelection?: Function; columns: ColumnHeaderOptions[]; dataViewId?: string | null; defaultCellActions?: TGridCellAction[]; @@ -127,10 +117,6 @@ export interface TGridStandaloneProps { } const TGridStandaloneComponent: React.FC = ({ - afterCaseSelection, - appId, - casesOwner, - casePermissions, columns, dataViewId = null, defaultCellActions, @@ -272,26 +258,6 @@ const TGridStandaloneComponent: React.FC = ({ ); const hasAlerts = totalCountMinusDeleted > 0; - const activeCaseFlowId = useSelector((state: State) => tGridSelectors.activeCaseFlowId(state)); - const selectedEvent = useMemo(() => { - const matchedEvent = events.find((event) => event.ecs._id === activeCaseFlowId); - if (matchedEvent) { - return matchedEvent; - } else { - return undefined; - } - }, [events, activeCaseFlowId]); - - const addToCaseActionProps = useMemo(() => { - return { - event: selectedEvent, - casePermissions: casePermissions ?? null, - appId, - owner: casesOwner, - onClose: afterCaseSelection, - }; - }, [appId, casePermissions, afterCaseSelection, selectedEvent, casesOwner]); - const nonDeletedEvents = useMemo( () => events.filter((e) => !deletedEventIds.includes(e._id)), [deletedEventIds, events] @@ -425,7 +391,6 @@ const TGridStandaloneComponent: React.FC = ({ ) : null} - ); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.test.tsx new file mode 100644 index 00000000000000..e945f91c47afda --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { TestProviders } from '../../../../mock'; + +import { CategoriesBadges } from './categories_badges'; + +const mockSetSelectedCategoryIds = jest.fn(); +const defaultProps = { + setSelectedCategoryIds: mockSetSelectedCategoryIds, + selectedCategoryIds: [], +}; + +describe('CategoriesBadges', () => { + beforeEach(() => { + mockSetSelectedCategoryIds.mockClear(); + }); + + it('should render empty badges', () => { + const result = render( + + + + ); + + const badges = result.getByTestId('category-badges'); + expect(badges).toBeInTheDocument(); + expect(badges.childNodes.length).toBe(0); + }); + + it('should render the selector button with selected categories', () => { + const result = render( + + + + ); + + const badges = result.getByTestId('category-badges'); + expect(badges.childNodes.length).toBe(2); + expect(result.getByTestId('category-badge-base')).toBeInTheDocument(); + expect(result.getByTestId('category-badge-event')).toBeInTheDocument(); + }); + + it('should call the set selected callback when badge unselect button clicked', () => { + const result = render( + + + + ); + + result.getByTestId('category-badge-unselect-base').click(); + expect(mockSetSelectedCategoryIds).toHaveBeenCalledWith(['event']); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.tsx new file mode 100644 index 00000000000000..14b928d18de452 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import styled from 'styled-components'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +interface CategoriesBadgesProps { + setSelectedCategoryIds: (categoryIds: string[]) => void; + selectedCategoryIds: string[]; +} + +const CategoriesBadgesGroup = styled(EuiFlexGroup)` + margin-top: ${({ theme }) => theme.eui.euiSizeXS}; + min-height: 24px; +`; +CategoriesBadgesGroup.displayName = 'CategoriesBadgesGroup'; + +const CategoriesBadgesComponent: React.FC = ({ + setSelectedCategoryIds, + selectedCategoryIds, +}) => { + const onUnselectCategory = useCallback( + (categoryId: string) => { + setSelectedCategoryIds( + selectedCategoryIds.filter((selectedCategoryId) => selectedCategoryId !== categoryId) + ); + }, + [setSelectedCategoryIds, selectedCategoryIds] + ); + + return ( + + {selectedCategoryIds.map((categoryId) => ( + + onUnselectCategory(categoryId)} + iconOnClickAriaLabel="unselect category" + data-test-subj={`category-badge-${categoryId}`} + closeButtonProps={{ 'data-test-subj': `category-badge-unselect-${categoryId}` }} + > + {categoryId} + + + ))} + + ); +}; + +export const CategoriesBadges = React.memo(CategoriesBadgesComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.test.tsx deleted file mode 100644 index e2f1d78cf5bc2b..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.test.tsx +++ /dev/null @@ -1,51 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount } from 'enzyme'; -import React from 'react'; - -import { mockBrowserFields } from '../../../../mock'; - -import { CATEGORY_PANE_WIDTH } from './helpers'; -import { CategoriesPane } from './categories_pane'; -import * as i18n from './translations'; - -const timelineId = 'test'; - -describe('CategoriesPane', () => { - test('it renders the expected title', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="categories-pane-title"]').first().text()).toEqual( - i18n.CATEGORIES - ); - }); - - test('it renders a "No fields match" message when filteredBrowserFields is empty', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="categories-container"] tbody').first().text()).toEqual( - i18n.NO_FIELDS_MATCH - ); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx deleted file mode 100644 index ffb93aee11b556..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx +++ /dev/null @@ -1,118 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiInMemoryTable, EuiTitle } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React, { useCallback, useRef } from 'react'; -import styled from 'styled-components'; -import { - DATA_COLINDEX_ATTRIBUTE, - DATA_ROWINDEX_ATTRIBUTE, - onKeyDownFocusHandler, -} from '../../../../../common/utils/accessibility'; -import type { BrowserFields } from '../../../../../common/search_strategy'; -import { getCategoryColumns } from './category_columns'; -import { CATEGORIES_PANE_CLASS_NAME, TABLE_HEIGHT } from './helpers'; - -import * as i18n from './translations'; - -const CategoryNames = styled.div<{ height: number; width: number }>` - ${({ width }) => `width: ${width}px`}; - ${({ height }) => `height: ${height}px`}; - overflow-y: hidden; - padding: 5px; - thead { - display: none; - } -`; - -CategoryNames.displayName = 'CategoryNames'; - -const Title = styled(EuiTitle)` - padding-left: 5px; -`; - -const H3 = styled.h3` - text-align: left; -`; - -Title.displayName = 'Title'; - -interface Props { - /** - * A map of categoryId -> metadata about the fields in that category, - * filtered such that the name of every field in the category includes - * the filter input (as a substring). - */ - filteredBrowserFields: BrowserFields; - /** - * Invoked when the user clicks on the name of a category in the left-hand - * side of the field browser - */ - onCategorySelected: (categoryId: string) => void; - /** The category selected on the left-hand side of the field browser */ - selectedCategoryId: string; - timelineId: string; - /** The width of the categories pane */ - width: number; -} - -export const CategoriesPane = React.memo( - ({ filteredBrowserFields, onCategorySelected, selectedCategoryId, timelineId, width }) => { - const containerElement = useRef(null); - const onKeyDown = useCallback( - (e: React.KeyboardEvent) => { - onKeyDownFocusHandler({ - colindexAttribute: DATA_COLINDEX_ATTRIBUTE, - containerElement: containerElement?.current, - event: e, - maxAriaColindex: 1, - maxAriaRowindex: Object.keys(filteredBrowserFields).length, - onColumnFocused: noop, - rowindexAttribute: DATA_ROWINDEX_ATTRIBUTE, - }); - }, - [containerElement, filteredBrowserFields] - ); - - return ( - <> - - <H3 data-test-subj="categories-pane-title">{i18n.CATEGORIES}</H3> - - - - ({ categoryId, ariaRowindex: i + 1 }))} - message={i18n.NO_FIELDS_MATCH} - pagination={false} - sorting={false} - tableCaption={i18n.CATEGORIES} - /> - - - ); - } -); - -CategoriesPane.displayName = 'CategoriesPane'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.test.tsx new file mode 100644 index 00000000000000..eff37376a296e4 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.test.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { mockBrowserFields, TestProviders } from '../../../../mock'; + +import { CategoriesSelector } from './categories_selector'; + +const mockSetSelectedCategoryIds = jest.fn(); +const defaultProps = { + filteredBrowserFields: mockBrowserFields, + setSelectedCategoryIds: mockSetSelectedCategoryIds, + selectedCategoryIds: [], +}; + +describe('CategoriesSelector', () => { + beforeEach(() => { + mockSetSelectedCategoryIds.mockClear(); + }); + + it('should render the default selector button', () => { + const categoriesCount = Object.keys(mockBrowserFields).length; + const result = render( + + + + ); + + expect(result.getByTestId('categories-filter-button')).toBeInTheDocument(); + expect(result.getByText('Categories')).toBeInTheDocument(); + expect(result.getByText(categoriesCount)).toBeInTheDocument(); + }); + + it('should render the selector button with selected categories', () => { + const result = render( + + + + ); + + expect(result.getByTestId('categories-filter-button')).toBeInTheDocument(); + expect(result.getByText('Categories')).toBeInTheDocument(); + expect(result.getByText('2')).toBeInTheDocument(); + }); + + it('should open the category selector', () => { + const result = render( + + + + ); + + result.getByTestId('categories-filter-button').click(); + + expect(result.getByTestId('categories-selector-search')).toBeInTheDocument(); + expect(result.getByTestId(`categories-selector-option-base`)).toBeInTheDocument(); + }); + + it('should open the category selector with selected categories', () => { + const result = render( + + + + ); + + result.getByTestId('categories-filter-button').click(); + + expect(result.getByTestId('categories-selector-search')).toBeInTheDocument(); + expect(result.getByTestId(`categories-selector-option-base`)).toBeInTheDocument(); + expect(result.getByTestId(`categories-selector-option-name-base`)).toHaveStyleRule( + 'font-weight', + 'bold' + ); + }); + + it('should call setSelectedCategoryIds when category selected', () => { + const result = render( + + + + ); + + result.getByTestId('categories-filter-button').click(); + result.getByTestId(`categories-selector-option-base`).click(); + expect(mockSetSelectedCategoryIds).toHaveBeenCalledWith(['base']); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.tsx new file mode 100644 index 00000000000000..6aebd32543ea3e --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.tsx @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import { omit } from 'lodash'; +import { + EuiFilterButton, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiHighlight, + EuiPopover, + EuiSelectable, + FilterChecked, +} from '@elastic/eui'; +import { BrowserFields } from '../../../../../common'; +import * as i18n from './translations'; +import { CountBadge, getFieldCount, CategoryName, CategorySelectableContainer } from './helpers'; +import { isEscape } from '../../../../../common/utils/accessibility'; + +interface CategoriesSelectorProps { + /** + * A map of categoryId -> metadata about the fields in that category, + * filtered such that the name of every field in the category includes + * the filter input (as a substring). + */ + filteredBrowserFields: BrowserFields; + /** + * Invoked when the user clicks on the name of a category in the left-hand + * side of the field browser + */ + setSelectedCategoryIds: (categoryIds: string[]) => void; + /** The category selected on the left-hand side of the field browser */ + selectedCategoryIds: string[]; +} + +interface CategoryOption { + label: string; + count: number; + checked?: FilterChecked; +} + +const renderOption = (option: CategoryOption, searchValue: string) => { + const { label, count, checked } = option; + // Some category names have spaces, but test selectors don't like spaces, + // Tests are not able to find subjects with spaces, so we need to clean them. + const idAttr = label.replace(/\s/g, ''); + return ( + + + + {label} + + + + {count} + + + ); +}; + +const CategoriesSelectorComponent: React.FC = ({ + filteredBrowserFields, + setSelectedCategoryIds, + selectedCategoryIds, +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const togglePopover = useCallback(() => { + setIsPopoverOpen((open) => !open); + }, []); + const closePopover = useCallback(() => { + setIsPopoverOpen(false); + }, []); + + const totalCategories = useMemo( + () => Object.keys(filteredBrowserFields).length, + [filteredBrowserFields] + ); + + const categoryOptions: CategoryOption[] = useMemo(() => { + const unselectedCategoryIds = Object.keys( + omit(filteredBrowserFields, selectedCategoryIds) + ).sort(); + return [ + ...selectedCategoryIds.map((categoryId) => ({ + label: categoryId, + count: getFieldCount(filteredBrowserFields[categoryId]), + checked: 'on', + })), + ...unselectedCategoryIds.map((categoryId) => ({ + label: categoryId, + count: getFieldCount(filteredBrowserFields[categoryId]), + })), + ]; + }, [selectedCategoryIds, filteredBrowserFields]); + + const onCategoriesChange = useCallback( + (options: CategoryOption[]) => { + setSelectedCategoryIds( + options.filter(({ checked }) => checked === 'on').map(({ label }) => label) + ); + }, + [setSelectedCategoryIds] + ); + + const onKeyDown = useCallback((keyboardEvent: React.KeyboardEvent) => { + if (isEscape(keyboardEvent)) { + // Prevent escape to close the field browser modal after closing the category selector + keyboardEvent.stopPropagation(); + } + }, []); + + return ( + + 0} + iconType="arrowDown" + isSelected={isPopoverOpen} + numActiveFilters={selectedCategoryIds.length} + numFilters={totalCategories} + onClick={togglePopover} + > + {i18n.CATEGORIES} + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + > + + + {(list, search) => ( + <> + {search} + {list} + + )} + + + + + ); +}; + +export const CategoriesSelector = React.memo(CategoriesSelectorComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.test.tsx deleted file mode 100644 index 98f02a9484eaba..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.test.tsx +++ /dev/null @@ -1,100 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { useMountAppended } from '../../../utils/use_mount_appended'; - -import { Category } from './category'; -import { getFieldItems } from './field_items'; -import { FIELDS_PANE_WIDTH } from './helpers'; -import { mockBrowserFields, TestProviders } from '../../../../mock'; - -import * as i18n from './translations'; - -describe('Category', () => { - const timelineId = 'test'; - const selectedCategoryId = 'client'; - const mount = useMountAppended(); - - test('it renders the category id as the value of the title', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="selected-category-title"]').first().text()).toEqual( - selectedCategoryId - ); - }); - - test('it renders the Field column header', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('.euiTableCellContent__text').at(1).text()).toEqual(i18n.FIELD); - }); - - test('it renders the Description column header', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('.euiTableCellContent__text').at(2).text()).toEqual(i18n.DESCRIPTION); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx deleted file mode 100644 index 3130c46aa06843..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx +++ /dev/null @@ -1,114 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiInMemoryTable } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React, { useCallback, useMemo, useRef } from 'react'; -import styled from 'styled-components'; -import { - arrayIndexToAriaIndex, - DATA_COLINDEX_ATTRIBUTE, - DATA_ROWINDEX_ATTRIBUTE, - onKeyDownFocusHandler, -} from '../../../../../common/utils/accessibility'; -import type { BrowserFields } from '../../../../../common/search_strategy'; -import type { OnUpdateColumns } from '../../../../../common/types'; - -import { CategoryTitle } from './category_title'; -import { getFieldColumns } from './field_items'; -import type { FieldItem } from './field_items'; -import { CATEGORY_TABLE_CLASS_NAME, TABLE_HEIGHT } from './helpers'; - -import * as i18n from './translations'; - -const TableContainer = styled.div<{ height: number; width: number }>` - ${({ height }) => `height: ${height}px`}; - ${({ width }) => `width: ${width}px`}; - overflow: hidden; -`; - -TableContainer.displayName = 'TableContainer'; - -/** - * This callback, invoked via `EuiInMemoryTable`'s `rowProps, assigns - * attributes to every ``. - */ -const getAriaRowindex = (fieldItem: FieldItem) => - fieldItem.ariaRowindex != null ? { 'data-rowindex': fieldItem.ariaRowindex } : {}; - -interface Props { - categoryId: string; - fieldItems: FieldItem[]; - filteredBrowserFields: BrowserFields; - onCategorySelected: (categoryId: string) => void; - onUpdateColumns: OnUpdateColumns; - timelineId: string; - width: number; -} - -export const Category = React.memo( - ({ categoryId, filteredBrowserFields, fieldItems, onUpdateColumns, timelineId, width }) => { - const containerElement = useRef(null); - const onKeyDown = useCallback( - (keyboardEvent: React.KeyboardEvent) => { - onKeyDownFocusHandler({ - colindexAttribute: DATA_COLINDEX_ATTRIBUTE, - containerElement: containerElement?.current, - event: keyboardEvent, - maxAriaColindex: 3, - maxAriaRowindex: fieldItems.length, - onColumnFocused: noop, - rowindexAttribute: DATA_ROWINDEX_ATTRIBUTE, - }); - }, - [fieldItems.length] - ); - - const fieldItemsWithRowindex = useMemo( - () => - fieldItems.map((fieldItem, i) => ({ - ...fieldItem, - ariaRowindex: arrayIndexToAriaIndex(i), - })), - [fieldItems] - ); - - const columns = useMemo(() => getFieldColumns(), []); - - return ( - <> - - - - - - - ); - } -); - -Category.displayName = 'Category'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.test.tsx deleted file mode 100644 index a94ffee597c791..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.test.tsx +++ /dev/null @@ -1,153 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount } from 'enzyme'; -import React from 'react'; - -import { mockBrowserFields, TestProviders } from '../../../../mock'; - -import { CATEGORY_PANE_WIDTH, getFieldCount, VIEW_ALL_BUTTON_CLASS_NAME } from './helpers'; -import { CategoriesPane } from './categories_pane'; -import { ViewAllButton } from './category_columns'; - -const timelineId = 'test'; - -describe('getCategoryColumns', () => { - Object.keys(mockBrowserFields).forEach((categoryId) => { - test(`it renders the ${categoryId} category name (from filteredBrowserFields)`, () => { - const wrapper = mount( - - ); - - const fieldCount = Object.keys(mockBrowserFields[categoryId].fields ?? {}).length; - - expect( - wrapper.find(`.field-browser-category-pane-${categoryId}-${timelineId}`).first().text() - ).toEqual(`${categoryId}${fieldCount}`); - }); - }); - - Object.keys(mockBrowserFields).forEach((categoryId) => { - test(`it renders the correct field count for the ${categoryId} category (from filteredBrowserFields)`, () => { - const wrapper = mount( - - ); - - expect( - wrapper.find(`[data-test-subj="${categoryId}-category-count"]`).first().text() - ).toEqual(`${getFieldCount(mockBrowserFields[categoryId])}`); - }); - }); - - test('it renders the selected category with bold text', () => { - const selectedCategoryId = 'auditd'; - - const wrapper = mount( - - ); - - expect( - wrapper - .find(`.field-browser-category-pane-${selectedCategoryId}-${timelineId}`) - .find('[data-test-subj="categoryName"]') - .at(1) - ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); - }); - - test('it does NOT render an un-selected category with bold text', () => { - const selectedCategoryId = 'auditd'; - const notTheSelectedCategoryId = 'base'; - - const wrapper = mount( - - ); - - expect( - wrapper - .find(`.field-browser-category-pane-${notTheSelectedCategoryId}-${timelineId}`) - .find('[data-test-subj="categoryName"]') - .at(1) - ).toHaveStyleRule('font-weight', 'normal', { modifier: '.euiText' }); - }); - - test('it invokes onCategorySelected when a user clicks a category', () => { - const selectedCategoryId = 'auditd'; - const notTheSelectedCategoryId = 'base'; - - const onCategorySelected = jest.fn(); - - const wrapper = mount( - - ); - - wrapper - .find(`.field-browser-category-pane-${notTheSelectedCategoryId}-${timelineId}`) - .first() - .simulate('click'); - - expect(onCategorySelected).toHaveBeenCalledWith(notTheSelectedCategoryId); - }); -}); - -describe('ViewAllButton', () => { - it(`should update fields with the timestamp and category fields`, () => { - const onUpdateColumns = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper.find(`.${VIEW_ALL_BUTTON_CLASS_NAME}`).first().simulate('click'); - - expect(onUpdateColumns).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ id: '@timestamp' }), - expect.objectContaining({ id: 'agent.ephemeral_id' }), - expect.objectContaining({ id: 'agent.hostname' }), - expect.objectContaining({ id: 'agent.id' }), - expect.objectContaining({ id: 'agent.name' }), - ]) - ); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx deleted file mode 100644 index 0fdf71ff5ffe1a..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx +++ /dev/null @@ -1,157 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiText, - EuiToolTip, -} from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import styled from 'styled-components'; - -import { useDeepEqualSelector } from '../../../../hooks/use_selector'; -import { - LoadingSpinner, - getCategoryPaneCategoryClassName, - getFieldCount, - VIEW_ALL_BUTTON_CLASS_NAME, - CountBadge, -} from './helpers'; -import * as i18n from './translations'; -import { tGridSelectors } from '../../../../store/t_grid'; -import { getColumnsWithTimestamp } from '../../../utils/helpers'; -import type { BrowserFields } from '../../../../../common/search_strategy'; -import type { OnUpdateColumns } from '../../../../../common/types'; - -const CategoryName = styled.span<{ bold: boolean }>` - .euiText { - font-weight: ${({ bold }) => (bold ? 'bold' : 'normal')}; - } -`; - -CategoryName.displayName = 'CategoryName'; - -const LinkContainer = styled.div` - width: 100%; - .euiLink { - width: 100%; - } -`; - -LinkContainer.displayName = 'LinkContainer'; - -const ViewAll = styled(EuiButtonIcon)` - margin-left: 2px; -`; - -ViewAll.displayName = 'ViewAll'; - -export interface CategoryItem { - categoryId: string; -} - -interface ViewAllButtonProps { - categoryId: string; - browserFields: BrowserFields; - onUpdateColumns: OnUpdateColumns; - timelineId: string; -} - -export const ViewAllButton = React.memo( - ({ categoryId, browserFields, onUpdateColumns, timelineId }) => { - const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); - const { isLoading } = useDeepEqualSelector((state) => - getManageTimeline(state, timelineId ?? '') - ); - - const handleClick = useCallback(() => { - onUpdateColumns( - getColumnsWithTimestamp({ - browserFields, - category: categoryId, - }) - ); - }, [browserFields, categoryId, onUpdateColumns]); - - return ( - - {!isLoading ? ( - - ) : ( - - )} - - ); - } -); - -ViewAllButton.displayName = 'ViewAllButton'; - -/** - * Returns the column definition for the (single) column that displays all the - * category names in the field browser */ -export const getCategoryColumns = ({ - filteredBrowserFields, - onCategorySelected, - selectedCategoryId, - timelineId, -}: { - filteredBrowserFields: BrowserFields; - onCategorySelected: (categoryId: string) => void; - selectedCategoryId: string; - timelineId: string; -}) => [ - { - field: 'categoryId', - name: '', - sortable: true, - truncateText: false, - render: ( - categoryId: string, - { ariaRowindex }: { categoryId: string; ariaRowindex: number } - ) => ( - - onCategorySelected(categoryId)} - > - - - - {categoryId} - - - - - - {getFieldCount(filteredBrowserFields[categoryId])} - - - - - - ), - }, -]; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.test.tsx deleted file mode 100644 index 746668491abb84..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.test.tsx +++ /dev/null @@ -1,72 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount } from 'enzyme'; -import React from 'react'; - -import { mockBrowserFields, TestProviders } from '../../../../mock'; - -import { CategoryTitle } from './category_title'; -import { getFieldCount } from './helpers'; - -describe('CategoryTitle', () => { - const timelineId = 'test'; - - test('it renders the category id as the value of the title', () => { - const categoryId = 'client'; - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="selected-category-title"]').first().text()).toEqual( - categoryId - ); - }); - - test('when `categoryId` specifies a valid category in `filteredBrowserFields`, a count of the field is displayed in the badge', () => { - const validCategoryId = 'client'; - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="selected-category-count-badge"]`).first().text()).toEqual( - `${getFieldCount(mockBrowserFields[validCategoryId])}` - ); - }); - - test('when `categoryId` specifies an INVALID category in `filteredBrowserFields`, a count of zero is displayed in the badge', () => { - const invalidCategoryId = 'this.is.not.happening'; - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="selected-category-count-badge"]`).first().text()).toEqual( - '0' - ); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx deleted file mode 100644 index 0858f30a352463..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx +++ /dev/null @@ -1,67 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem, EuiScreenReaderOnly, EuiTitle } from '@elastic/eui'; -import React from 'react'; - -import { CountBadge, getFieldBrowserCategoryTitleClassName, getFieldCount } from './helpers'; -import type { BrowserFields } from '../../../../../common/search_strategy'; -import type { OnUpdateColumns } from '../../../../../common/types'; - -import { ViewAllButton } from './category_columns'; -import * as i18n from './translations'; - -interface Props { - /** The title of the category */ - categoryId: string; - /** - * A map of categoryId -> metadata about the fields in that category, - * filtered such that the name of every field in the category includes - * the filter input (as a substring). - */ - filteredBrowserFields: BrowserFields; - onUpdateColumns: OnUpdateColumns; - - /** The timeline associated with this field browser */ - timelineId: string; -} - -export const CategoryTitle = React.memo( - ({ filteredBrowserFields, categoryId, onUpdateColumns, timelineId }) => ( - - - -

{i18n.CATEGORY}

-
- -

{categoryId}

-
-
- - - - {getFieldCount(filteredBrowserFields[categoryId])} - - - - - - -
- ) -); - -CategoryTitle.displayName = 'CategoryTitle'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx index d435d7a280840b..ed665155ddcf52 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx @@ -34,18 +34,20 @@ const testProps = { searchInput: '', appliedFilterInput: '', isSearching: false, - onCategorySelected: jest.fn(), + setSelectedCategoryIds: jest.fn(), onHide, onSearchInputChange: jest.fn(), restoreFocusTo: React.createRef(), - selectedCategoryId: '', + selectedCategoryIds: [], timelineId, }; const { storage } = createSecuritySolutionStorageMock(); + describe('FieldsBrowser', () => { beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); + test('it renders the Close button', () => { const wrapper = mount( @@ -80,20 +82,7 @@ describe('FieldsBrowser', () => { test('it invokes updateColumns action when the user clicks the Reset Fields button', () => { const wrapper = mount( - ()} - selectedCategoryId={''} - timelineId={timelineId} - /> + ); @@ -129,24 +118,24 @@ describe('FieldsBrowser', () => { expect(wrapper.find('[data-test-subj="field-search"]').exists()).toBe(true); }); - test('it renders the categories pane', () => { + test('it renders the categories selector', () => { const wrapper = mount( ); - expect(wrapper.find('[data-test-subj="left-categories-pane"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="categories-selector"]').exists()).toBe(true); }); - test('it renders the fields pane', () => { + test('it renders the fields table', () => { const wrapper = mount( ); - expect(wrapper.find('[data-test-subj="fields-pane"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="field-table"]').exists()).toBe(true); }); test('focuses the search input when the component mounts', () => { @@ -183,19 +172,24 @@ describe('FieldsBrowser', () => { expect(onSearchInputChange).toBeCalledWith(inputText); }); - test('does not render the CreateField button when createFieldComponent is provided without a dataViewId', () => { + test('does not render the CreateFieldButton when it is provided but does not have a dataViewId', () => { const MyTestComponent = () =>
{'test'}
; const wrapper = mount( - + ); expect(wrapper.find(MyTestComponent).exists()).toBeFalsy(); }); - test('it renders the CreateField button when createFieldComponent is provided with a dataViewId', () => { + test('it renders the CreateFieldButton when it is provided and have a dataViewId', () => { const state: State = { ...mockGlobalState, timelineById: { @@ -212,7 +206,12 @@ describe('FieldsBrowser', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx index e55f54e946ad13..5a01c820aa9619 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx @@ -17,51 +17,27 @@ import { EuiButtonEmpty, EuiSpacer, } from '@elastic/eui'; -import React, { useEffect, useCallback, useRef, useMemo } from 'react'; -import styled from 'styled-components'; +import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import type { BrowserFields } from '../../../../../common/search_strategy'; -import type { ColumnHeaderOptions, CreateFieldComponentType } from '../../../../../common/types'; -import { - isEscape, - isTab, - stopPropagationAndPreventDefault, -} from '../../../../../common/utils/accessibility'; -import { CategoriesPane } from './categories_pane'; -import { FieldsPane } from './fields_pane'; +import type { FieldBrowserProps, ColumnHeaderOptions } from '../../../../../common/types'; import { Search } from './search'; -import { - CATEGORY_PANE_WIDTH, - CLOSE_BUTTON_CLASS_NAME, - FIELDS_PANE_WIDTH, - FIELD_BROWSER_WIDTH, - focusSearchInput, - onFieldsBrowserTabPressed, - PANES_FLEX_GROUP_WIDTH, - RESET_FIELDS_CLASS_NAME, - scrollCategoriesPane, -} from './helpers'; -import type { FieldBrowserProps } from './types'; +import { CLOSE_BUTTON_CLASS_NAME, FIELD_BROWSER_WIDTH, RESET_FIELDS_CLASS_NAME } from './helpers'; import { tGridActions, tGridSelectors } from '../../../../store/t_grid'; import * as i18n from './translations'; import { useDeepEqualSelector } from '../../../../hooks/use_selector'; +import { CategoriesSelector } from './categories_selector'; +import { FieldTable } from './field_table'; +import { CategoriesBadges } from './categories_badges'; -const PanesFlexGroup = styled(EuiFlexGroup)` - width: ${PANES_FLEX_GROUP_WIDTH}px; -`; -PanesFlexGroup.displayName = 'PanesFlexGroup'; - -type Props = Pick & { +type Props = Pick & { /** * The current timeline column headers */ columnHeaders: ColumnHeaderOptions[]; - - createFieldComponent?: CreateFieldComponentType; - /** * A map of categoryId -> metadata about the fields in that category, * filtered such that the name of every field in the category includes @@ -80,12 +56,12 @@ type Props = Pick & /** * The category selected on the left-hand side of the field browser */ - selectedCategoryId: string; + selectedCategoryIds: string[]; /** * Invoked when the user clicks on the name of a category in the left-hand * side of the field browser */ - onCategorySelected: (categoryId: string) => void; + setSelectedCategoryIds: (categoryIds: string[]) => void; /** * Hides the field browser when invoked */ @@ -110,23 +86,23 @@ type Props = Pick & const FieldsBrowserComponent: React.FC = ({ columnHeaders, filteredBrowserFields, - createFieldComponent: CreateField, isSearching, - onCategorySelected, + setSelectedCategoryIds, onSearchInputChange, onHide, + options, restoreFocusTo, searchInput, appliedFilterInput, - selectedCategoryId, + selectedCategoryIds, timelineId, width = FIELD_BROWSER_WIDTH, }) => { const dispatch = useDispatch(); - const containerElement = useRef(null); const onUpdateColumns = useCallback( - (columns) => dispatch(tGridActions.updateColumns({ id: timelineId, columns })), + (columns: ColumnHeaderOptions[]) => + dispatch(tGridActions.updateColumns({ id: timelineId, columns })), [dispatch, timelineId] ); @@ -156,45 +132,14 @@ const FieldsBrowserComponent: React.FC = ({ [onSearchInputChange] ); - const scrollViewsAndFocusInput = useCallback(() => { - scrollCategoriesPane({ - containerElement: containerElement.current, - selectedCategoryId, - timelineId, - }); - - // always re-focus the input to enable additional filtering - focusSearchInput({ - containerElement: containerElement.current, - timelineId, - }); - }, [selectedCategoryId, timelineId]); - - useEffect(() => { - scrollViewsAndFocusInput(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedCategoryId, timelineId]); - - const onKeyDown = useCallback( - (keyboardEvent: React.KeyboardEvent) => { - if (isEscape(keyboardEvent)) { - stopPropagationAndPreventDefault(keyboardEvent); - closeAndRestoreFocus(); - } else if (isTab(keyboardEvent)) { - onFieldsBrowserTabPressed({ - containerElement: containerElement.current, - keyboardEvent, - selectedCategoryId, - timelineId, - }); - } - }, - [closeAndRestoreFocus, containerElement, selectedCategoryId, timelineId] - ); + const [CreateFieldButton, getFieldTableColumns] = [ + options?.createFieldButton, + options?.getFieldTableColumns, + ]; return ( -
+

{i18n.FIELDS_BROWSER}

@@ -202,11 +147,10 @@ const FieldsBrowserComponent: React.FC = ({
- + = ({ /> - {CreateField && dataViewId != null && dataViewId.length > 0 && ( - + + + + {CreateFieldButton && dataViewId != null && dataViewId.length > 0 && ( + )} + + - - - - - - - - + diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx index a4c830c3d8808a..45b122354528b4 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx @@ -5,21 +5,17 @@ * 2.0. */ -import { omit } from 'lodash/fp'; import React from 'react'; -import { waitFor } from '@testing-library/react'; -import { mockBrowserFields, TestProviders } from '../../../../mock'; +import { omit } from 'lodash/fp'; +import { render } from '@testing-library/react'; +import { EuiInMemoryTable } from '@elastic/eui'; +import { mockBrowserFields } from '../../../../mock'; import { defaultColumnHeaderType } from '../../body/column_headers/default_headers'; import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../body/constants'; -import { Category } from './category'; import { getFieldColumns, getFieldItems } from './field_items'; -import { FIELDS_PANE_WIDTH } from './helpers'; -import { useMountAppended } from '../../../utils/use_mount_appended'; import { ColumnHeaderOptions } from '../../../../../common/types'; -const selectedCategoryId = 'base'; -const selectedCategoryFields = mockBrowserFields[selectedCategoryId].fields; const timestampFieldId = '@timestamp'; const columnHeaders: ColumnHeaderOptions[] = [ { @@ -28,7 +24,7 @@ const columnHeaders: ColumnHeaderOptions[] = [ description: 'Date/time when the event originated.\nFor log events this is the date/time when the event was generated, and not when it was read.\nRequired field for all events.', example: '2016-05-23T08:05:34.853Z', - id: '@timestamp', + id: timestampFieldId, type: 'date', aggregatable: true, initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, @@ -36,295 +32,199 @@ const columnHeaders: ColumnHeaderOptions[] = [ ]; describe('field_items', () => { - const timelineId = 'test'; - const mount = useMountAppended(); - describe('getFieldItems', () => { - Object.keys(selectedCategoryFields!).forEach((fieldId) => { - test(`it renders the name of the ${fieldId} field`, () => { - const wrapper = mount( - - - - ); + const timestampField = mockBrowserFields.base.fields![timestampFieldId]; - expect(wrapper.find(`[data-test-subj="field-name-${fieldId}"]`).first().text()).toEqual( - fieldId - ); + it('should return browser field item format', () => { + const fieldItems = getFieldItems({ + selectedCategoryIds: ['base'], + browserFields: { base: { fields: { [timestampFieldId]: timestampField } } }, + columnHeaders: [], }); - }); - Object.keys(selectedCategoryFields!).forEach((fieldId) => { - test(`it renders a checkbox for the ${fieldId} field`, () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="field-${fieldId}-checkbox"]`).first().exists()).toBe( - true - ); + expect(fieldItems[0]).toEqual({ + name: timestampFieldId, + description: timestampField.description, + category: 'base', + selected: false, + type: timestampField.type, + example: timestampField.example, + isRuntime: false, }); }); - test('it renders a checkbox in the checked state when the field is selected to be displayed as a column in the timeline', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="field-${timestampFieldId}-checkbox"]`).first().props() - .checked - ).toBe(true); - }); - - test('it does NOT render a checkbox in the checked state when the field is NOT selected to be displayed as a column in the timeline', () => { - const wrapper = mount( - - header.id !== timestampFieldId), - highlight: '', - timelineId, - toggleColumn: jest.fn(), - })} - width={FIELDS_PANE_WIDTH} - onCategorySelected={jest.fn()} - onUpdateColumns={jest.fn()} - timelineId={timelineId} - /> - - ); - - expect( - wrapper.find(`[data-test-subj="field-${timestampFieldId}-checkbox"]`).first().props() - .checked - ).toBe(false); - }); - - test('it invokes `toggleColumn` when the user interacts with the checkbox', () => { - const toggleColumn = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper - .find('input[type="checkbox"]') - .first() - .simulate('change', { - target: { checked: true }, - }); - wrapper.update(); + it('should return selected item', () => { + const fieldItems = getFieldItems({ + selectedCategoryIds: ['base'], + browserFields: { base: { fields: { [timestampFieldId]: timestampField } } }, + columnHeaders, + }); - expect(toggleColumn).toBeCalledWith({ - columnHeaderType: 'not-filtered', - id: '@timestamp', - initialWidth: 180, + expect(fieldItems[0]).toMatchObject({ + selected: true, }); }); - test('it returns the expected signal column settings', async () => { - const mockSelectedCategoryId = 'signal'; - const mockBrowserFieldsWithSignal = { - ...mockBrowserFields, - signal: { - fields: { - 'signal.rule.name': { - aggregatable: true, - category: 'signal', - description: 'rule name', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'signal.rule.name', - searchable: true, - type: 'string', + it('should return isRuntime field', () => { + const fieldItems = getFieldItems({ + selectedCategoryIds: ['base'], + browserFields: { + base: { + fields: { + [timestampFieldId]: { + ...timestampField, + runtimeField: { type: 'keyword', script: { source: 'scripts are fun' } }, + }, }, }, }, - }; - const toggleColumn = jest.fn(); - const wrapper = mount( - - - - ); - wrapper - .find(`[data-test-subj="field-signal.rule.name-checkbox"]`) - .last() - .simulate('change', { - target: { checked: true }, - }); + columnHeaders, + }); - await waitFor(() => { - expect(toggleColumn).toBeCalledWith({ - columnHeaderType: 'not-filtered', - id: 'signal.rule.name', - initialWidth: 180, - }); + expect(fieldItems[0]).toMatchObject({ + isRuntime: true, }); }); - test('it renders the expected icon for a field', () => { - const wrapper = mount( - - - + it('should return all field items of all categories if no category selected', () => { + const fieldCount = Object.values(mockBrowserFields).reduce( + (total, { fields }) => total + Object.keys(fields ?? {}).length, + 0 ); - expect( - wrapper.find(`[data-test-subj="field-${timestampFieldId}-icon"]`).first().props().type - ).toEqual('clock'); + const fieldItems = getFieldItems({ + selectedCategoryIds: [], + browserFields: mockBrowserFields, + columnHeaders: [], + }); + + expect(fieldItems.length).toBe(fieldCount); }); - test('it renders the expected field description', () => { - const wrapper = mount( - - - + it('should return filtered field items of selected categories', () => { + const selectedCategoryIds = ['base', 'event']; + const fieldCount = selectedCategoryIds.reduce( + (total, selectedCategoryId) => + total + Object.keys(mockBrowserFields[selectedCategoryId].fields ?? {}).length, + 0 ); - expect( - wrapper.find(`[data-test-subj="field-${timestampFieldId}-description"]`).first().text() - ).toEqual( - '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' - ); + const fieldItems = getFieldItems({ + selectedCategoryIds, + browserFields: mockBrowserFields, + columnHeaders: [], + }); + + expect(fieldItems.length).toBe(fieldCount); }); }); describe('getFieldColumns', () => { - test('it returns the expected column definitions', () => { - expect(getFieldColumns().map((column) => omit('render', column))).toEqual([ + const onToggleColumn = jest.fn(); + + beforeEach(() => { + onToggleColumn.mockClear(); + }); + + it('should return default field columns', () => { + expect(getFieldColumns({ onToggleColumn }).map((column) => omit('render', column))).toEqual([ { - field: 'checkbox', + field: 'selected', name: '', sortable: false, width: '25px', }, - { field: 'field', name: 'Field', sortable: false, width: '225px' }, + { + field: 'name', + name: 'Name', + sortable: true, + width: '225px', + }, { field: 'description', name: 'Description', + sortable: true, + width: '400px', + }, + { + field: 'category', + name: 'Category', + sortable: true, + width: '100px', + }, + ]); + }); + + it('should return custom field columns', () => { + const customColumns = [ + { + field: 'name', + name: 'customColumn1', sortable: false, - truncateText: true, + width: '225px', + }, + { + field: 'description', + name: 'customColumn2', + sortable: true, width: '400px', }, + ]; + + expect( + getFieldColumns({ + onToggleColumn, + getFieldTableColumns: () => customColumns, + }).map((column) => omit('render', column)) + ).toEqual([ + { + field: 'selected', + name: '', + sortable: false, + width: '25px', + }, + ...customColumns, ]); }); + + it('should render default columns', () => { + const timestampField = mockBrowserFields.base.fields![timestampFieldId]; + const fieldItems = getFieldItems({ + selectedCategoryIds: ['base'], + browserFields: { base: { fields: { [timestampFieldId]: timestampField } } }, + columnHeaders: [], + }); + + const columns = getFieldColumns({ onToggleColumn }); + const { getByTestId, getAllByText } = render( + + ); + + expect(getAllByText('Name').at(0)).toBeInTheDocument(); + expect(getAllByText('Description').at(0)).toBeInTheDocument(); + expect(getAllByText('Category').at(0)).toBeInTheDocument(); + + expect(getByTestId(`field-${timestampFieldId}-checkbox`)).toBeInTheDocument(); + expect(getByTestId(`field-${timestampFieldId}-name`)).toBeInTheDocument(); + expect(getByTestId(`field-${timestampFieldId}-description`)).toBeInTheDocument(); + expect(getByTestId(`field-${timestampFieldId}-category`)).toBeInTheDocument(); + }); + + it('should call call toggle callback on checkbox click', () => { + const timestampField = mockBrowserFields.base.fields![timestampFieldId]; + const fieldItems = getFieldItems({ + selectedCategoryIds: ['base'], + browserFields: { base: { fields: { [timestampFieldId]: timestampField } } }, + columnHeaders: [], + }); + + const columns = getFieldColumns({ onToggleColumn }); + const { getByTestId } = render( + + ); + + getByTestId(`field-${timestampFieldId}-checkbox`).click(); + expect(onToggleColumn).toHaveBeenCalledWith(timestampFieldId); + }); }); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx index a979e209bf64aa..1e066eb2174a53 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx @@ -13,14 +13,22 @@ import { EuiFlexGroup, EuiFlexItem, EuiScreenReaderOnly, + EuiBadge, + EuiBasicTableColumn, + EuiTableActionsColumnType, } from '@elastic/eui'; import { uniqBy } from 'lodash/fp'; import styled from 'styled-components'; import { getEmptyValue } from '../../../empty_value'; import { getExampleText, getIconFromType } from '../../../utils/helpers'; -import type { BrowserField } from '../../../../../common/search_strategy'; -import type { ColumnHeaderOptions } from '../../../../../common/types'; +import type { BrowserFields } from '../../../../../common/search_strategy'; +import type { + ColumnHeaderOptions, + BrowserFieldItem, + FieldTableColumns, + GetFieldTableColumns, +} from '../../../../../common/types'; import { defaultColumnHeaderType } from '../../body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../../body/constants'; import { TruncatableText } from '../../../truncatable_text'; @@ -33,125 +41,155 @@ const TypeIcon = styled(EuiIcon)` position: relative; top: -1px; `; - TypeIcon.displayName = 'TypeIcon'; export const Description = styled.span` user-select: text; width: 400px; `; - Description.displayName = 'Description'; /** - * An item rendered in the table - */ -export interface FieldItem { - ariaRowindex?: number; - checkbox: React.ReactNode; - description: React.ReactNode; - field: React.ReactNode; - fieldId: string; -} - -/** - * Returns the fields items, values, and descriptions shown when a user expands an event + * Returns the field items of all categories selected */ export const getFieldItems = ({ - category, + browserFields, + selectedCategoryIds, columnHeaders, - highlight = '', - timelineId, - toggleColumn, }: { - category: Partial; + browserFields: BrowserFields; + selectedCategoryIds: string[]; columnHeaders: ColumnHeaderOptions[]; - highlight?: string; - timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; -}): FieldItem[] => - uniqBy('name', [ - ...Object.values(category != null && category.fields != null ? category.fields : {}), - ]).map((field) => ({ - checkbox: ( - - c.id === field.name) !== -1} - data-test-subj={`field-${field.name}-checkbox`} - data-colindex={1} - id={field.name ?? ''} - onChange={() => - toggleColumn({ - columnHeaderType: defaultColumnHeaderType, - id: field.name ?? '', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - ...getAlertColumnHeader(timelineId, field.name ?? ''), - }) - } - /> - - ), - field: ( - - - - - - +}): BrowserFieldItem[] => { + const categoryIds = + selectedCategoryIds.length > 0 ? selectedCategoryIds : Object.keys(browserFields); + const selectedFieldIds = new Set(columnHeaders.map(({ id }) => id)); - - - - + return uniqBy( + 'name', + categoryIds.reduce((fieldItems, categoryId) => { + const categoryBrowserFields = Object.values(browserFields[categoryId]?.fields ?? {}); + if (categoryBrowserFields.length > 0) { + fieldItems.push( + ...categoryBrowserFields.map(({ name = '', ...field }) => ({ + name, + type: field.type, + description: field.description ?? '', + example: field.example?.toString(), + category: categoryId, + selected: selectedFieldIds.has(name), + isRuntime: !!field.runtimeField, + })) + ); + } + return fieldItems; + }, []) + ); +}; + +/** + * Returns the column header for a field + */ +export const getColumnHeader = (timelineId: string, fieldName: string): ColumnHeaderOptions => ({ + columnHeaderType: defaultColumnHeaderType, + id: fieldName, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + ...getAlertColumnHeader(timelineId, fieldName), +}); + +const getDefaultFieldTableColumns = (highlight: string): FieldTableColumns => [ + { + field: 'name', + name: i18n.NAME, + render: (name: string, { type }) => { + return ( + + + + + + + + + + + + ); + }, + sortable: true, + width: '225px', + }, + { + field: 'description', + name: i18n.DESCRIPTION, + render: (description: string, { name, example }) => ( + + <> + +

{i18n.DESCRIPTION_FOR_FIELD(name)}

+
+ + + {`${description ?? getEmptyValue()} ${getExampleText(example)}`} + + + +
), - description: ( -
- - <> - -

{i18n.DESCRIPTION_FOR_FIELD(field.name ?? '')}

-
- - - {`${field.description ?? getEmptyValue()} ${getExampleText(field.example)}`} - - - -
-
+ sortable: true, + width: '400px', + }, + { + field: 'category', + name: i18n.CATEGORY, + render: (category: string, { name }) => ( + {category} ), - fieldId: field.name ?? '', - })); + sortable: true, + width: '100px', + }, +]; /** * Returns a table column template provided to the `EuiInMemoryTable`'s * `columns` prop */ -export const getFieldColumns = () => [ +export const getFieldColumns = ({ + onToggleColumn, + highlight = '', + getFieldTableColumns, +}: { + onToggleColumn: (id: string) => void; + highlight?: string; + getFieldTableColumns?: GetFieldTableColumns; +}): FieldTableColumns => [ { - field: 'checkbox', + field: 'selected', name: '', - render: (checkbox: React.ReactNode, _: FieldItem) => checkbox, + render: (selected: boolean, { name }) => ( + + onToggleColumn(name)} + /> + + ), sortable: false, width: '25px', }, - { - field: 'field', - name: i18n.FIELD, - render: (field: React.ReactNode, _: FieldItem) => field, - sortable: false, - width: '225px', - }, - { - field: 'description', - name: i18n.DESCRIPTION, - render: (description: React.ReactNode, _: FieldItem) => description, - sortable: false, - truncateText: true, - width: '400px', - }, + ...(getFieldTableColumns + ? getFieldTableColumns(highlight) + : getDefaultFieldTableColumns(highlight)), ]; + +/** Returns whether the table column has actions attached to it */ +export const isActionsColumn = (column: EuiBasicTableColumn): boolean => { + return !!(column as EuiTableActionsColumnType).actions?.length; +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx index 05f093eaf1805d..6bda5873edc257 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx @@ -43,7 +43,7 @@ describe('FieldName', () => { ); expect( - wrapper.find(`[data-test-subj="field-name-${timestampFieldId}"]`).first().text() + wrapper.find(`[data-test-subj="field-${timestampFieldId}-name"]`).first().text() ).toEqual(timestampFieldId); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx index 5781211058d3c3..0ef0ce64c637b8 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx @@ -15,7 +15,7 @@ export const FieldName = React.memo<{ }>(({ fieldId, highlight = '' }) => { return ( - + {fieldId} diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.test.tsx new file mode 100644 index 00000000000000..14f2151d240747 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.test.tsx @@ -0,0 +1,225 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { mockBrowserFields, TestProviders } from '../../../../mock'; +import { tGridActions } from '../../../../store/t_grid'; +import { defaultColumnHeaderType } from '../../body/column_headers/default_headers'; +import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../body/constants'; + +import { ColumnHeaderOptions } from '../../../../../common'; +import { FieldTable } from './field_table'; + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +const timestampFieldId = '@timestamp'; + +const columnHeaders: ColumnHeaderOptions[] = [ + { + category: 'base', + columnHeaderType: defaultColumnHeaderType, + description: + 'Date/time when the event originated.\nFor log events this is the date/time when the event was generated, and not when it was read.\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + id: timestampFieldId, + type: 'date', + aggregatable: true, + initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, + }, +]; + +describe('FieldTable', () => { + const timelineId = 'test'; + const timestampField = mockBrowserFields.base.fields![timestampFieldId]; + const defaultPageSize = 10; + const totalFields = Object.values(mockBrowserFields).reduce( + (total, { fields }) => total + Object.keys(fields ?? {}).length, + 0 + ); + + beforeEach(() => { + mockDispatch.mockClear(); + }); + + it('should render empty field table', () => { + const result = render( + + + + ); + + expect(result.getByText('No items found')).toBeInTheDocument(); + expect(result.getByTestId('fields-count').textContent).toContain('0'); + }); + + it('should render field table with fields of all categories', () => { + const result = render( + + + + ); + + expect(result.container.getElementsByClassName('euiTableRow').length).toBe(defaultPageSize); + expect(result.getByTestId('fields-count').textContent).toContain(totalFields); + }); + + it('should render field table with fields of categories selected', () => { + const selectedCategoryIds = ['client', 'event']; + + const fieldCount = selectedCategoryIds.reduce( + (total, selectedCategoryId) => + total + Object.keys(mockBrowserFields[selectedCategoryId].fields ?? {}).length, + 0 + ); + + const result = render( + + + + ); + + expect(result.container.getElementsByClassName('euiTableRow').length).toBe(fieldCount); + expect(result.getByTestId('fields-count').textContent).toContain(fieldCount); + }); + + it('should render field table with custom columns', () => { + const fieldTableColumns = [ + { + field: 'name', + name: 'Custom column', + render: () =>
, + }, + ]; + + const result = render( + + fieldTableColumns} + selectedCategoryIds={[]} + columnHeaders={[]} + filteredBrowserFields={mockBrowserFields} + searchInput="" + timelineId={timelineId} + /> + + ); + + expect(result.getByTestId('fields-count').textContent).toContain(totalFields); + expect(result.getAllByText('Custom column').length).toBeGreaterThan(0); + expect(result.getAllByTestId('customColumn').length).toEqual(defaultPageSize); + }); + + it('should render field table with unchecked field', () => { + const result = render( + + + + ); + + const checkbox = result.getByTestId(`field-${timestampFieldId}-checkbox`); + expect(checkbox).not.toHaveAttribute('checked'); + }); + + it('should render field table with checked field', () => { + const result = render( + + + + ); + + const checkbox = result.getByTestId(`field-${timestampFieldId}-checkbox`); + expect(checkbox).toHaveAttribute('checked'); + }); + + it('should dispatch remove column action on field unchecked', () => { + const result = render( + + + + ); + + result.getByTestId(`field-${timestampFieldId}-checkbox`).click(); + + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch).toHaveBeenCalledWith( + tGridActions.removeColumn({ id: timelineId, columnId: timestampFieldId }) + ); + }); + + it('should dispatch upsert column action on field checked', () => { + const result = render( + + + + ); + + result.getByTestId(`field-${timestampFieldId}-checkbox`).click(); + + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch).toHaveBeenCalledWith( + tGridActions.upsertColumn({ + id: timelineId, + column: { + columnHeaderType: defaultColumnHeaderType, + id: timestampFieldId, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + index: 1, + }) + ); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.tsx new file mode 100644 index 00000000000000..332422ed664f6c --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; +import styled from 'styled-components'; +import { EuiInMemoryTable, EuiText } from '@elastic/eui'; +import { useDispatch } from 'react-redux'; +import { BrowserFields, ColumnHeaderOptions } from '../../../../../common'; +import * as i18n from './translations'; +import { getColumnHeader, getFieldColumns, getFieldItems, isActionsColumn } from './field_items'; +import { CATEGORY_TABLE_CLASS_NAME, TABLE_HEIGHT } from './helpers'; +import { tGridActions } from '../../../../store/t_grid'; +import type { GetFieldTableColumns } from '../../../../../common/types/fields_browser'; + +interface FieldTableProps { + timelineId: string; + columnHeaders: ColumnHeaderOptions[]; + /** + * A map of categoryId -> metadata about the fields in that category, + * filtered such that the name of every field in the category includes + * the filter input (as a substring). + */ + filteredBrowserFields: BrowserFields; + /** + * Optional function to customize field table columns + */ + getFieldTableColumns?: GetFieldTableColumns; + /** + * The category selected on the left-hand side of the field browser + */ + selectedCategoryIds: string[]; + /** The text displayed in the search input */ + /** Invoked when a user chooses to view a new set of columns in the timeline */ + searchInput: string; +} + +const TableContainer = styled.div<{ height: number }>` + margin-top: ${({ theme }) => theme.eui.euiSizeXS}; + border-top: ${({ theme }) => theme.eui.euiBorderThin}; + ${({ height }) => `height: ${height}px`}; + overflow: hidden; +`; +TableContainer.displayName = 'TableContainer'; + +const Count = styled.span` + font-weight: bold; +`; +Count.displayName = 'Count'; + +const FieldTableComponent: React.FC = ({ + columnHeaders, + filteredBrowserFields, + getFieldTableColumns, + searchInput, + selectedCategoryIds, + timelineId, +}) => { + const dispatch = useDispatch(); + + const fieldItems = useMemo( + () => + getFieldItems({ + browserFields: filteredBrowserFields, + selectedCategoryIds, + columnHeaders, + }), + [columnHeaders, filteredBrowserFields, selectedCategoryIds] + ); + + const onToggleColumn = useCallback( + (fieldId: string) => { + if (columnHeaders.some(({ id }) => id === fieldId)) { + dispatch( + tGridActions.removeColumn({ + columnId: fieldId, + id: timelineId, + }) + ); + } else { + dispatch( + tGridActions.upsertColumn({ + column: getColumnHeader(timelineId, fieldId), + id: timelineId, + index: 1, + }) + ); + } + }, + [columnHeaders, dispatch, timelineId] + ); + + const columns = useMemo( + () => getFieldColumns({ highlight: searchInput, onToggleColumn, getFieldTableColumns }), + [onToggleColumn, searchInput, getFieldTableColumns] + ); + const hasActions = useMemo(() => columns.some((column) => isActionsColumn(column)), [columns]); + + return ( + <> + + {i18n.FIELDS_SHOWING} + {fieldItems.length} + {i18n.FIELDS_COUNT(fieldItems.length)} + + + + + + + ); +}; + +export const FieldTable = React.memo(FieldTableComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx deleted file mode 100644 index aec21b48471362..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx +++ /dev/null @@ -1,112 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { useMountAppended } from '../../../utils/use_mount_appended'; -import { mockBrowserFields, TestProviders } from '../../../../mock'; - -import { FIELDS_PANE_WIDTH } from './helpers'; -import { FieldsPane } from './fields_pane'; - -const timelineId = 'test'; - -describe('FieldsPane', () => { - const mount = useMountAppended(); - - test('it renders the selected category', () => { - const selectedCategory = 'auditd'; - - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="selected-category-title"]`).first().text()).toEqual( - selectedCategory - ); - }); - - test('it renders a unknown category that does not exist in filteredBrowserFields', () => { - const selectedCategory = 'unknown'; - - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="selected-category-title"]`).first().text()).toEqual( - selectedCategory - ); - }); - - test('it renders the expected message when `filteredBrowserFields` is empty and `searchInput` is empty', () => { - const searchInput = ''; - - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="no-fields-match"]`).first().text()).toEqual( - 'No fields match ' - ); - }); - - test('it renders the expected message when `filteredBrowserFields` is empty and `searchInput` is an unknown field name', () => { - const searchInput = 'thisFieldDoesNotExist'; - - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="no-fields-match"]`).first().text()).toEqual( - `No fields match ${searchInput}` - ); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx deleted file mode 100644 index d1d0254d0c917d..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx +++ /dev/null @@ -1,145 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import styled from 'styled-components'; -import { useDispatch } from 'react-redux'; - -import { Category } from './category'; -import { getFieldItems } from './field_items'; -import { FIELDS_PANE_WIDTH, TABLE_HEIGHT } from './helpers'; - -import * as i18n from './translations'; -import type { BrowserFields } from '../../../../../common/search_strategy'; -import type { ColumnHeaderOptions, OnUpdateColumns } from '../../../../../common/types'; -import { tGridActions } from '../../../../store/t_grid'; - -const NoFieldsPanel = styled.div` - background-color: ${(props) => props.theme.eui.euiColorLightestShade}; - width: ${FIELDS_PANE_WIDTH}px; - height: ${TABLE_HEIGHT}px; -`; - -NoFieldsPanel.displayName = 'NoFieldsPanel'; - -const NoFieldsFlexGroup = styled(EuiFlexGroup)` - height: 100%; -`; - -NoFieldsFlexGroup.displayName = 'NoFieldsFlexGroup'; - -interface Props { - timelineId: string; - columnHeaders: ColumnHeaderOptions[]; - /** - * A map of categoryId -> metadata about the fields in that category, - * filtered such that the name of every field in the category includes - * the filter input (as a substring). - */ - filteredBrowserFields: BrowserFields; - /** - * Invoked when the user clicks on the name of a category in the left-hand - * side of the field browser - */ - onCategorySelected: (categoryId: string) => void; - /** The text displayed in the search input */ - /** Invoked when a user chooses to view a new set of columns in the timeline */ - onUpdateColumns: OnUpdateColumns; - searchInput: string; - /** - * The category selected on the left-hand side of the field browser - */ - selectedCategoryId: string; - /** The width field browser */ - width: number; -} -export const FieldsPane = React.memo( - ({ - columnHeaders, - filteredBrowserFields, - onCategorySelected, - onUpdateColumns, - searchInput, - selectedCategoryId, - timelineId, - width, - }) => { - const dispatch = useDispatch(); - - const toggleColumn = useCallback( - (column: ColumnHeaderOptions) => { - if (columnHeaders.some((c) => c.id === column.id)) { - dispatch( - tGridActions.removeColumn({ - columnId: column.id, - id: timelineId, - }) - ); - } else { - dispatch( - tGridActions.upsertColumn({ - column, - id: timelineId, - index: 1, - }) - ); - } - }, - [columnHeaders, dispatch, timelineId] - ); - - const filteredBrowserFieldsExists = useMemo( - () => Object.keys(filteredBrowserFields).length > 0, - [filteredBrowserFields] - ); - - const fieldItems = useMemo(() => { - return getFieldItems({ - category: filteredBrowserFields[selectedCategoryId], - columnHeaders, - highlight: searchInput, - timelineId, - toggleColumn, - }); - }, [ - columnHeaders, - filteredBrowserFields, - searchInput, - selectedCategoryId, - timelineId, - toggleColumn, - ]); - - if (filteredBrowserFieldsExists) { - return ( - - ); - } - - return ( - - - -

{i18n.NO_FIELDS_MATCH_INPUT(searchInput)}

-
-
-
- ); - } -); - -FieldsPane.displayName = 'FieldsPane'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx index 239d7c726e286b..ad90956013e41d 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx @@ -10,45 +10,12 @@ import { mockBrowserFields } from '../../../../mock'; import { categoryHasFields, createVirtualCategory, - getCategoryPaneCategoryClassName, - getFieldBrowserCategoryTitleClassName, - getFieldBrowserSearchInputClassName, getFieldCount, filterBrowserFieldsByFieldName, } from './helpers'; import { BrowserFields } from '../../../../../common/search_strategy'; -const timelineId = 'test'; - describe('helpers', () => { - describe('getCategoryPaneCategoryClassName', () => { - test('it returns the expected class name', () => { - const categoryId = 'auditd'; - - expect(getCategoryPaneCategoryClassName({ categoryId, timelineId })).toEqual( - 'field-browser-category-pane-auditd-test' - ); - }); - }); - - describe('getFieldBrowserCategoryTitleClassName', () => { - test('it returns the expected class name', () => { - const categoryId = 'auditd'; - - expect(getFieldBrowserCategoryTitleClassName({ categoryId, timelineId })).toEqual( - 'field-browser-category-title-auditd-test' - ); - }); - }); - - describe('getFieldBrowserSearchInputClassName', () => { - test('it returns the expected class name', () => { - expect(getFieldBrowserSearchInputClassName(timelineId)).toEqual( - 'field-browser-search-input-test' - ); - }); - }); - describe('categoryHasFields', () => { test('it returns false if the category fields property is undefined', () => { expect(categoryHasFields({})).toBe(false); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx index 5406940aab3e9c..21829bda265e1f 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx @@ -9,11 +9,6 @@ import { EuiBadge, EuiLoadingSpinner } from '@elastic/eui'; import { filter, get, pickBy } from 'lodash/fp'; import styled from 'styled-components'; -import { - elementOrChildrenHasFocus, - skipFocusInContainerTo, - stopPropagationAndPreventDefault, -} from '../../../../../common/utils/accessibility'; import { TimelineId } from '../../../../../public/types'; import type { BrowserField, BrowserFields } from '../../../../../common/search_strategy'; import { defaultHeaders } from '../../../../store/t_grid/defaults'; @@ -27,44 +22,8 @@ export const LoadingSpinner = styled(EuiLoadingSpinner)` LoadingSpinner.displayName = 'LoadingSpinner'; -export const CATEGORY_PANE_WIDTH = 200; -export const DESCRIPTION_COLUMN_WIDTH = 300; -export const FIELD_COLUMN_WIDTH = 200; export const FIELD_BROWSER_WIDTH = 925; -export const FIELDS_PANE_WIDTH = 670; -export const HEADER_HEIGHT = 40; -export const PANES_FLEX_GROUP_WIDTH = CATEGORY_PANE_WIDTH + FIELDS_PANE_WIDTH + 10; -export const PANES_FLEX_GROUP_HEIGHT = 260; export const TABLE_HEIGHT = 260; -export const TYPE_COLUMN_WIDTH = 50; - -/** - * Returns the CSS class name for the title of a category shown in the left - * side field browser - */ -export const getCategoryPaneCategoryClassName = ({ - categoryId, - timelineId, -}: { - categoryId: string; - timelineId: string; -}): string => `field-browser-category-pane-${categoryId}-${timelineId}`; - -/** - * Returns the CSS class name for the title of a category shown in the right - * side of field browser - */ -export const getFieldBrowserCategoryTitleClassName = ({ - categoryId, - timelineId, -}: { - categoryId: string; - timelineId: string; -}): string => `field-browser-category-title-${categoryId}-${timelineId}`; - -/** Returns the class name for a field browser search input */ -export const getFieldBrowserSearchInputClassName = (timelineId: string): string => - `field-browser-search-input-${timelineId}`; /** Returns true if the specified category has at least one field */ export const categoryHasFields = (category: Partial): boolean => @@ -160,272 +119,22 @@ export const getAlertColumnHeader = (timelineId: string, fieldId: string) => ? defaultHeaders.find((c) => c.id === fieldId) ?? {} : {}; -export const CATEGORIES_PANE_CLASS_NAME = 'categories-pane'; export const CATEGORY_TABLE_CLASS_NAME = 'category-table'; export const CLOSE_BUTTON_CLASS_NAME = 'close-button'; export const RESET_FIELDS_CLASS_NAME = 'reset-fields'; -export const VIEW_ALL_BUTTON_CLASS_NAME = 'view-all'; - -export const categoriesPaneHasFocus = (containerElement: HTMLElement | null): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector(`.${CATEGORIES_PANE_CLASS_NAME}`) - ); - -export const categoryTableHasFocus = (containerElement: HTMLElement | null): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector(`.${CATEGORY_TABLE_CLASS_NAME}`) - ); - -export const closeButtonHasFocus = (containerElement: HTMLElement | null): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector(`.${CLOSE_BUTTON_CLASS_NAME}`) - ); - -export const searchInputHasFocus = ({ - containerElement, - timelineId, -}: { - containerElement: HTMLElement | null; - timelineId: string; -}): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector( - `.${getFieldBrowserSearchInputClassName(timelineId)}` - ) - ); - -export const viewAllHasFocus = (containerElement: HTMLElement | null): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector(`.${VIEW_ALL_BUTTON_CLASS_NAME}`) - ); - -export const resetButtonHasFocus = (containerElement: HTMLElement | null): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector(`.${RESET_FIELDS_CLASS_NAME}`) - ); - -export const scrollCategoriesPane = ({ - containerElement, - selectedCategoryId, - timelineId, -}: { - containerElement: HTMLElement | null; - selectedCategoryId: string; - timelineId: string; -}) => { - if (selectedCategoryId !== '') { - const selectedCategories = - containerElement?.getElementsByClassName( - getCategoryPaneCategoryClassName({ - categoryId: selectedCategoryId, - timelineId, - }) - ) ?? []; - - if (selectedCategories.length > 0) { - selectedCategories[0].scrollIntoView(); - } - } -}; - -export const focusCategoriesPane = ({ - containerElement, - selectedCategoryId, - timelineId, -}: { - containerElement: HTMLElement | null; - selectedCategoryId: string; - timelineId: string; -}) => { - if (selectedCategoryId !== '') { - const selectedCategories = - containerElement?.getElementsByClassName( - getCategoryPaneCategoryClassName({ - categoryId: selectedCategoryId, - timelineId, - }) - ) ?? []; - - if (selectedCategories.length > 0) { - (selectedCategories[0] as HTMLButtonElement).focus(); - } - } -}; - -export const focusCategoryTable = (containerElement: HTMLElement | null) => { - const firstEntry = containerElement?.querySelector( - `.${CATEGORY_TABLE_CLASS_NAME} [data-colindex="1"]` - ); - - if (firstEntry != null) { - firstEntry.focus(); - } else { - skipFocusInContainerTo({ - containerElement, - className: CATEGORY_TABLE_CLASS_NAME, - }); - } -}; - -export const focusCloseButton = (containerElement: HTMLElement | null) => - skipFocusInContainerTo({ - containerElement, - className: CLOSE_BUTTON_CLASS_NAME, - }); - -export const focusResetFieldsButton = (containerElement: HTMLElement | null) => - skipFocusInContainerTo({ containerElement, className: RESET_FIELDS_CLASS_NAME }); - -export const focusSearchInput = ({ - containerElement, - timelineId, -}: { - containerElement: HTMLElement | null; - timelineId: string; -}) => - skipFocusInContainerTo({ - containerElement, - className: getFieldBrowserSearchInputClassName(timelineId), - }); - -export const focusViewAllButton = (containerElement: HTMLElement | null) => - skipFocusInContainerTo({ containerElement, className: VIEW_ALL_BUTTON_CLASS_NAME }); - -export const onCategoriesPaneFocusChanging = ({ - containerElement, - shiftKey, - timelineId, -}: { - containerElement: HTMLElement | null; - shiftKey: boolean; - timelineId: string; -}) => - shiftKey - ? focusSearchInput({ - containerElement, - timelineId, - }) - : focusViewAllButton(containerElement); - -export const onCategoryTableFocusChanging = ({ - containerElement, - shiftKey, -}: { - containerElement: HTMLElement | null; - shiftKey: boolean; -}) => (shiftKey ? focusViewAllButton(containerElement) : focusResetFieldsButton(containerElement)); - -export const onCloseButtonFocusChanging = ({ - containerElement, - shiftKey, - timelineId, -}: { - containerElement: HTMLElement | null; - shiftKey: boolean; - timelineId: string; -}) => - shiftKey - ? focusResetFieldsButton(containerElement) - : focusSearchInput({ containerElement, timelineId }); - -export const onSearchInputFocusChanging = ({ - containerElement, - selectedCategoryId, - shiftKey, - timelineId, -}: { - containerElement: HTMLElement | null; - selectedCategoryId: string; - shiftKey: boolean; - timelineId: string; -}) => - shiftKey - ? focusCloseButton(containerElement) - : focusCategoriesPane({ containerElement, selectedCategoryId, timelineId }); - -export const onViewAllFocusChanging = ({ - containerElement, - selectedCategoryId, - shiftKey, - timelineId, -}: { - containerElement: HTMLElement | null; - selectedCategoryId: string; - shiftKey: boolean; - timelineId: string; -}) => - shiftKey - ? focusCategoriesPane({ containerElement, selectedCategoryId, timelineId }) - : focusCategoryTable(containerElement); - -export const onResetButtonFocusChanging = ({ - containerElement, - shiftKey, -}: { - containerElement: HTMLElement | null; - shiftKey: boolean; -}) => (shiftKey ? focusCategoryTable(containerElement) : focusCloseButton(containerElement)); - -export const onFieldsBrowserTabPressed = ({ - containerElement, - keyboardEvent, - selectedCategoryId, - timelineId, -}: { - containerElement: HTMLElement | null; - keyboardEvent: React.KeyboardEvent; - selectedCategoryId: string; - timelineId: string; -}) => { - const { shiftKey } = keyboardEvent; - - if (searchInputHasFocus({ containerElement, timelineId })) { - stopPropagationAndPreventDefault(keyboardEvent); - onSearchInputFocusChanging({ - containerElement, - selectedCategoryId, - shiftKey, - timelineId, - }); - } else if (categoriesPaneHasFocus(containerElement)) { - stopPropagationAndPreventDefault(keyboardEvent); - onCategoriesPaneFocusChanging({ - containerElement, - shiftKey, - timelineId, - }); - } else if (viewAllHasFocus(containerElement)) { - stopPropagationAndPreventDefault(keyboardEvent); - onViewAllFocusChanging({ - containerElement, - selectedCategoryId, - shiftKey, - timelineId, - }); - } else if (categoryTableHasFocus(containerElement)) { - stopPropagationAndPreventDefault(keyboardEvent); - onCategoryTableFocusChanging({ - containerElement, - shiftKey, - }); - } else if (resetButtonHasFocus(containerElement)) { - stopPropagationAndPreventDefault(keyboardEvent); - onResetButtonFocusChanging({ - containerElement, - shiftKey, - }); - } else if (closeButtonHasFocus(containerElement)) { - stopPropagationAndPreventDefault(keyboardEvent); - onCloseButtonFocusChanging({ - containerElement, - shiftKey, - timelineId, - }); - } -}; export const CountBadge = styled(EuiBadge)` margin-left: 5px; ` as unknown as typeof EuiBadge; CountBadge.displayName = 'CountBadge'; + +export const CategoryName = styled.span<{ bold: boolean }>` + font-weight: ${({ bold }) => (bold ? 'bold' : 'normal')}; +`; +CategoryName.displayName = 'CategoryName'; + +export const CategorySelectableContainer = styled.div` + width: 300px; +`; +CategorySelectableContainer.displayName = 'CategorySelectableContainer'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx index b8bc2a12ffd6e8..7db742fd11302c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import { mount } from 'enzyme'; import React from 'react'; -import { waitFor } from '@testing-library/react'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; import { mockBrowserFields, TestProviders } from '../../../../mock'; @@ -18,12 +17,8 @@ import { StatefulFieldsBrowserComponent } from '.'; describe('StatefulFieldsBrowser', () => { const timelineId = 'test'; - beforeEach(() => { - window.HTMLElement.prototype.scrollIntoView = jest.fn(); - }); - - test('it renders the Fields button, which displays the fields browser on click', () => { - const wrapper = mount( + it('should render the Fields button, which displays the fields browser on click', () => { + const result = render( { ); - expect(wrapper.find('[data-test-subj="show-field-browser"]').exists()).toBe(true); + expect(result.getByTestId('show-field-browser')).toBeInTheDocument(); }); describe('toggleShow', () => { - test('it does NOT render the fields browser until the Fields button is clicked', () => { - const wrapper = mount( + it('should NOT render the fields browser until the Fields button is clicked', () => { + const result = render( { ); - expect(wrapper.find('[data-test-subj="fields-browser-container"]').exists()).toBe(false); + expect(result.queryByTestId('fields-browser-container')).toBeNull(); }); - test('it renders the fields browser when the Fields button is clicked', () => { - const wrapper = mount( + it('should render the fields browser when the Fields button is clicked', async () => { + const result = render( { /> ); - - wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); - - expect(wrapper.find('[data-test-subj="fields-browser-container"]').exists()).toBe(true); + result.getByTestId('show-field-browser').click(); + await waitFor(() => { + expect(result.getByTestId('fields-browser-container')).toBeInTheDocument(); + }); }); }); - describe('updateSelectedCategoryId', () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - test('it updates the selectedCategoryId state, which makes the category bold, when the user clicks a category name in the left hand side of the field browser', async () => { - const wrapper = mount( + describe('updateSelectedCategoryIds', () => { + it('should add a selected category, which creates the category badge', async () => { + const result = render( { ); - wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); + result.getByTestId('show-field-browser').click(); + await waitFor(() => { + expect(result.getByTestId('fields-browser-container')).toBeInTheDocument(); + }); + + await act(async () => { + result.getByTestId('categories-filter-button').click(); + }); + await act(async () => { + result.getByTestId('categories-selector-option-base').click(); + }); + + expect(result.getByTestId('category-badge-base')).toBeInTheDocument(); + }); + + it('should remove a selected category, which deletes the category badge', async () => { + const result = render( + + + + ); - wrapper.find(`.field-browser-category-pane-auditd-${timelineId}`).first().simulate('click'); + result.getByTestId('show-field-browser').click(); await waitFor(() => { - wrapper.update(); - expect( - wrapper - .find(`.field-browser-category-pane-auditd-${timelineId}`) - .find('[data-test-subj="categoryName"]') - .at(1) - ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); + expect(result.getByTestId('fields-browser-container')).toBeInTheDocument(); }); + + await act(async () => { + result.getByTestId('categories-filter-button').click(); + }); + await act(async () => { + result.getByTestId('categories-selector-option-base').click(); + }); + expect(result.getByTestId('category-badge-base')).toBeInTheDocument(); + + await act(async () => { + result.getByTestId('category-badge-unselect-base').click(); + }); + expect(result.queryByTestId('category-badge-base')).toBeNull(); }); - test('it updates the selectedCategoryId state according to most fields returned', async () => { - const wrapper = mount( + it('should update the available categories according to the search input', async () => { + const result = render( { ); + result.getByTestId('show-field-browser').click(); await waitFor(() => { - wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); - jest.runOnlyPendingTimers(); - wrapper.update(); - - expect( - wrapper - .find(`.field-browser-category-pane-cloud-${timelineId}`) - .find('[data-test-subj="categoryName"]') - .at(1) - ).toHaveStyleRule('font-weight', 'normal', { modifier: '.euiText' }); + expect(result.getByTestId('fields-browser-container')).toBeInTheDocument(); }); + result.getByTestId('categories-filter-button').click(); + expect(result.getByTestId('categories-selector-option-base')).toBeInTheDocument(); + + fireEvent.change(result.getByTestId('field-search'), { target: { value: 'client' } }); await waitFor(() => { - wrapper - .find('[data-test-subj="field-search"]') - .last() - .simulate('change', { target: { value: 'cloud' } }); - - jest.runOnlyPendingTimers(); - wrapper.update(); - expect( - wrapper - .find(`.field-browser-category-pane-cloud-${timelineId}`) - .find('[data-test-subj="categoryName"]') - .at(1) - ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); + expect(result.queryByTestId('categories-selector-option-base')).toBeNull(); }); + expect(result.queryByTestId('categories-selector-option-client')).toBeInTheDocument(); }); }); - test('it renders the Fields Browser button as a settings gear when the isEventViewer prop is true', () => { + it('should render the Fields Browser button as a settings gear when the isEventViewer prop is true', () => { const isEventViewer = true; - const wrapper = mount( + const result = render( { ); - expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(true); + expect(result.getByTestId('show-field-browser')).toBeInTheDocument(); }); - test('it renders the Fields Browser button as a settings gear when the isEventViewer prop is false', () => { - const isEventViewer = true; + it('should render the Fields Browser button as a settings gear when the isEventViewer prop is false', () => { + const isEventViewer = false; - const wrapper = mount( + const result = render( { ); - expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(true); + expect(result.getByTestId('show-field-browser')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx index 13549e2d5be109..c5647c973b9d8c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx @@ -6,15 +6,15 @@ */ import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { debounce } from 'lodash'; import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; import styled from 'styled-components'; import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; -import { DEFAULT_CATEGORY_NAME } from '../../body/column_headers/default_headers'; +import type { FieldBrowserProps } from '../../../../../common/types/fields_browser'; import { FieldsBrowser } from './field_browser'; import { filterBrowserFieldsByFieldName, mergeBrowserFieldsWithDefaultCategory } from './helpers'; import * as i18n from './translations'; -import type { FieldBrowserProps } from './types'; const FIELDS_BUTTON_CLASS_NAME = 'fields-button'; @@ -34,26 +34,48 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ timelineId, columnHeaders, browserFields, - createFieldComponent, + options, width, }) => { const customizeColumnsButtonRef = useRef(null); - /** tracks the latest timeout id from `setTimeout`*/ - const inputTimeoutId = useRef(0); - /** all field names shown in the field browser must contain this string (when specified) */ const [filterInput, setFilterInput] = useState(''); - + /** debounced filterInput, the one that is applied to the filteredBrowserFields */ const [appliedFilterInput, setAppliedFilterInput] = useState(''); /** all fields in this collection have field names that match the filterInput */ const [filteredBrowserFields, setFilteredBrowserFields] = useState(null); /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */ const [isSearching, setIsSearching] = useState(false); /** this category will be displayed in the right-hand pane of the field browser */ - const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME); + const [selectedCategoryIds, setSelectedCategoryIds] = useState([]); /** show the field browser */ const [show, setShow] = useState(false); + // debounced function to apply the input filter + // will delay the call to setAppliedFilterInput by INPUT_TIMEOUT ms + // the parameter used will be the last one passed + const debouncedApplyFilterInput = useMemo( + () => + debounce((input: string) => { + setAppliedFilterInput(input); + }, INPUT_TIMEOUT), + [] + ); + useEffect(() => { + return () => { + debouncedApplyFilterInput.cancel(); + }; + }, [debouncedApplyFilterInput]); + + useEffect(() => { + const newFilteredBrowserFields = filterBrowserFieldsByFieldName({ + browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), + substring: appliedFilterInput, + }); + setFilteredBrowserFields(newFilteredBrowserFields); + setIsSearching(false); + }, [appliedFilterInput, browserFields]); + /** Shows / hides the field browser */ const onShow = useCallback(() => { setShow(true); @@ -65,65 +87,19 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ setAppliedFilterInput(''); setFilteredBrowserFields(null); setIsSearching(false); - setSelectedCategoryId(DEFAULT_CATEGORY_NAME); + setSelectedCategoryIds([]); setShow(false); }, []); - const newFilteredBrowserFields = useMemo(() => { - return filterBrowserFieldsByFieldName({ - browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), - substring: appliedFilterInput, - }); - }, [appliedFilterInput, browserFields]); - - const newSelectedCategoryId = useMemo(() => { - if (appliedFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0) { - return DEFAULT_CATEGORY_NAME; - } else { - return Object.keys(newFilteredBrowserFields) - .sort() - .reduce((selected, category) => { - const filteredBrowserFieldsByCategory = - (newFilteredBrowserFields[category] && newFilteredBrowserFields[category].fields) || []; - const filteredBrowserFieldsBySelected = - (newFilteredBrowserFields[selected] && newFilteredBrowserFields[selected].fields) || []; - return newFilteredBrowserFields[category].fields != null && - newFilteredBrowserFields[selected].fields != null && - Object.keys(filteredBrowserFieldsByCategory).length > - Object.keys(filteredBrowserFieldsBySelected).length - ? category - : selected; - }, Object.keys(newFilteredBrowserFields)[0]); - } - }, [appliedFilterInput, newFilteredBrowserFields]); - /** Invoked when the user types in the filter input */ - const updateFilter = useCallback((newFilterInput: string) => { - setFilterInput(newFilterInput); - setIsSearching(true); - }, []); - - useEffect(() => { - if (inputTimeoutId.current !== 0) { - clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers - } - // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: - inputTimeoutId.current = window.setTimeout(() => { - setIsSearching(false); - setAppliedFilterInput(filterInput); - }, INPUT_TIMEOUT); - return () => { - clearTimeout(inputTimeoutId.current); - }; - }, [filterInput]); - - useEffect(() => { - setFilteredBrowserFields(newFilteredBrowserFields); - }, [newFilteredBrowserFields]); - - useEffect(() => { - setSelectedCategoryId(newSelectedCategoryId); - }, [newSelectedCategoryId]); + const updateFilter = useCallback( + (newFilterInput: string) => { + setIsSearching(true); + setFilterInput(newFilterInput); + debouncedApplyFilterInput(newFilterInput); + }, + [debouncedApplyFilterInput] + ); // only merge in the default category if the field browser is visible const browserFieldsWithDefaultCategory = useMemo(() => { @@ -150,19 +126,19 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ {show && ( diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.test.tsx index f5668b1bdc08d4..fb6363e2444592 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.test.tsx @@ -7,7 +7,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { mockBrowserFields, TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../../mock'; import { Search } from './search'; const timelineId = 'test'; @@ -17,7 +17,6 @@ describe('Search', () => { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { expect(onSearchInputChange).toBeCalled(); }); - - test('it returns the expected categories count when filteredBrowserFields is empty', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="categories-count"]').first().text()).toEqual( - '0 categories' - ); - }); - - test('it returns the expected categories count when filteredBrowserFields is NOT empty', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="categories-count"]').first().text()).toEqual( - '12 categories' - ); - }); - - test('it returns the expected fields count when filteredBrowserFields is empty', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="fields-count"]').first().text()).toEqual('0 fields'); - }); - - test('it returns the expected fields count when filteredBrowserFields is NOT empty', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="fields-count"]').first().text()).toEqual('34 fields'); - }); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx index 935952fbf37e00..037dcdc9033d2f 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx @@ -6,75 +6,28 @@ */ import React from 'react'; -import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import styled from 'styled-components'; -import type { BrowserFields } from '../../../../../common/search_strategy'; - -import { getFieldBrowserSearchInputClassName, getFieldCount } from './helpers'; - +import { EuiFieldSearch } from '@elastic/eui'; import * as i18n from './translations'; - -const CountsFlexGroup = styled(EuiFlexGroup)` - margin-top: ${({ theme }) => theme.eui.euiSizeXS}; - margin-left: ${({ theme }) => theme.eui.euiSizeXS}; -`; - -CountsFlexGroup.displayName = 'CountsFlexGroup'; - interface Props { - filteredBrowserFields: BrowserFields; isSearching: boolean; onSearchInputChange: (event: React.ChangeEvent) => void; searchInput: string; timelineId: string; } -const CountRow = React.memo>(({ filteredBrowserFields }) => ( - - - - {i18n.CATEGORIES_COUNT(Object.keys(filteredBrowserFields).length)} - - - - - - {i18n.FIELDS_COUNT( - Object.keys(filteredBrowserFields).reduce( - (fieldsCount, category) => getFieldCount(filteredBrowserFields[category]) + fieldsCount, - 0 - ) - )} - - - -)); - -CountRow.displayName = 'CountRow'; - const inputRef = (node: HTMLInputElement | null) => node?.focus(); export const Search = React.memo( - ({ isSearching, filteredBrowserFields, onSearchInputChange, searchInput, timelineId }) => ( - <> - - - + ({ isSearching, onSearchInputChange, searchInput, timelineId }) => ( + ) ); - Search.displayName = 'Search'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts index ac0160fad6cdee..eab412971c580c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts @@ -21,21 +21,6 @@ export const CATEGORIES_COUNT = (totalCount: number) => defaultMessage: '{totalCount} {totalCount, plural, =1 {category} other {categories}}', }); -export const CATEGORY_LINK = ({ category, totalCount }: { category: string; totalCount: number }) => - i18n.translate('xpack.timelines.fieldBrowser.categoryLinkAriaLabel', { - values: { category, totalCount }, - defaultMessage: - '{category} {totalCount} {totalCount, plural, =1 {field} other {fields}}. Click this button to select the {category} category.', - }); - -export const CATEGORY_FIELDS_TABLE_CAPTION = (categoryId: string) => - i18n.translate('xpack.timelines.fieldBrowser.categoryFieldsTableCaption', { - defaultMessage: 'category {categoryId} fields', - values: { - categoryId, - }, - }); - export const CLOSE = i18n.translate('xpack.timelines.fieldBrowser.closeButton', { defaultMessage: 'Close', }); @@ -56,6 +41,10 @@ export const DESCRIPTION_FOR_FIELD = (field: string) => defaultMessage: 'Description for field {field}:', }); +export const NAME = i18n.translate('xpack.timelines.fieldBrowser.fieldName', { + defaultMessage: 'Name', +}); + export const FIELD = i18n.translate('xpack.timelines.fieldBrowser.fieldLabel', { defaultMessage: 'Field', }); @@ -64,10 +53,14 @@ export const FIELDS = i18n.translate('xpack.timelines.fieldBrowser.fieldsTitle', defaultMessage: 'Fields', }); +export const FIELDS_SHOWING = i18n.translate('xpack.timelines.fieldBrowser.fieldsCountShowing', { + defaultMessage: 'Showing', +}); + export const FIELDS_COUNT = (totalCount: number) => i18n.translate('xpack.timelines.fieldBrowser.fieldsCountTitle', { values: { totalCount }, - defaultMessage: '{totalCount} {totalCount, plural, =1 {field} other {fields}}', + defaultMessage: '{totalCount, plural, =1 {field} other {fields}}', }); export const FILTER_PLACEHOLDER = i18n.translate('xpack.timelines.fieldBrowser.filterPlaceholder', { @@ -90,14 +83,6 @@ export const RESET_FIELDS = i18n.translate('xpack.timelines.fieldBrowser.resetFi defaultMessage: 'Reset Fields', }); -export const VIEW_ALL_CATEGORY_FIELDS = (categoryId: string) => - i18n.translate('xpack.timelines.fieldBrowser.viewCategoryTooltip', { - defaultMessage: 'View all {categoryId} fields', - values: { - categoryId, - }, - }); - export const VIEW_COLUMN = (field: string) => i18n.translate('xpack.timelines.fieldBrowser.viewColumnCheckboxAriaLabel', { values: { field }, diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts deleted file mode 100644 index bcf7287950624f..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts +++ /dev/null @@ -1,27 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CreateFieldComponentType } from '../../../../../common/types'; -import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; -import type { ColumnHeaderOptions } from '../../../../../common/types/timeline/columns'; - -export type OnFieldSelected = (fieldId: string) => void; - -export interface FieldBrowserProps { - /** The timeline associated with this field browser */ - timelineId: string; - /** The timeline's current column headers */ - columnHeaders: ColumnHeaderOptions[]; - /** A map of categoryId -> metadata about the fields in that category */ - browserFields: BrowserFields; - - createFieldComponent?: CreateFieldComponentType; - /** When true, this Fields Browser is being used as an "events viewer" */ - isEventViewer?: boolean; - /** The width of the field browser */ - width?: number; -} diff --git a/x-pack/plugins/timelines/public/hooks/use_add_to_case.test.ts b/x-pack/plugins/timelines/public/hooks/use_add_to_case.test.ts deleted file mode 100644 index 5b654f40deea66..00000000000000 --- a/x-pack/plugins/timelines/public/hooks/use_add_to_case.test.ts +++ /dev/null @@ -1,71 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { normalizedEventFields } from './use_add_to_case'; -import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; -import { merge } from 'lodash'; - -const defaultArgs = { - _id: 'test-id', - data: [ - { field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] }, - { field: ALERT_RULE_UUID, value: ['data-rule-id'] }, - { field: ALERT_RULE_NAME, value: ['data-rule-name'] }, - ], - ecs: { - _id: 'test-id', - _index: 'test-index', - signal: { rule: { id: ['rule-id'], name: ['rule-name'], false_positives: [] } }, - }, -}; -describe('normalizedEventFields', () => { - it('uses rule data when provided', () => { - const result = normalizedEventFields(defaultArgs); - expect(result).toEqual({ - ruleId: 'data-rule-id', - ruleName: 'data-rule-name', - }); - }); - const makeObj = (s: string, v: string[]) => { - const keys = s.split('.'); - return keys - .reverse() - .reduce((prev, current, i) => (i === 0 ? { [current]: v } : { [current]: { ...prev } }), {}); - }; - it('uses rule/ecs combo Xavier thinks is a thing but Steph has yet to see', () => { - const args = { - ...defaultArgs, - data: [], - ecs: { - _id: 'string', - ...merge( - makeObj(ALERT_RULE_UUID, ['xavier-rule-id']), - makeObj(ALERT_RULE_NAME, ['xavier-rule-name']) - ), - }, - }; - const result = normalizedEventFields(args); - expect(result).toEqual({ - ruleId: 'xavier-rule-id', - ruleName: 'xavier-rule-name', - }); - }); - it('falls back to use ecs data', () => { - const result = normalizedEventFields({ ...defaultArgs, data: [] }); - expect(result).toEqual({ - ruleId: 'rule-id', - ruleName: 'rule-name', - }); - }); - it('returns null when all the data is bad', () => { - const result = normalizedEventFields({ ...defaultArgs, data: [], ecs: { _id: 'bad' } }); - expect(result).toEqual({ - ruleId: null, - ruleName: null, - }); - }); -}); diff --git a/x-pack/plugins/timelines/public/hooks/use_add_to_case.ts b/x-pack/plugins/timelines/public/hooks/use_add_to_case.ts deleted file mode 100644 index 63a7d07831c4b7..00000000000000 --- a/x-pack/plugins/timelines/public/hooks/use_add_to_case.ts +++ /dev/null @@ -1,200 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { get, isEmpty } from 'lodash/fp'; -import { useState, useCallback, useMemo, SyntheticEvent } from 'react'; -import { useDispatch } from 'react-redux'; -import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; -import { useKibana } from '../../../../../src/plugins/kibana_react/public'; -import { Case, CommentType } from '../../../cases/common'; -import { TimelinesStartServices } from '../types'; -import { TimelineItem } from '../../common/search_strategy'; -import { tGridActions } from '../store/t_grid'; -import { useDeepEqualSelector } from './use_selector'; -import { createUpdateSuccessToaster } from '../components/actions/timeline/cases/helpers'; -import { AddToCaseActionProps } from '../components/actions'; -import { CaseAttachments, CasesDeepLinkId, generateCaseViewPath } from '../../../cases/public'; - -interface UseAddToCase { - addNewCaseClick: () => void; - addExistingCaseClick: () => void; - onCaseClicked: (theCase?: Case) => void; - onCaseSuccess: (theCase: Case) => Promise; - onCaseCreated: () => Promise; - isAllCaseModalOpen: boolean; - isDisabled: boolean; - userCanCrud: boolean; - isEventSupported: boolean; - openPopover: (event: SyntheticEvent) => void; - closePopover: () => void; - isPopoverOpen: boolean; - isCreateCaseFlyoutOpen: boolean; - caseAttachments?: CaseAttachments; -} - -export const useAddToCase = ({ - event, - casePermissions, - appId, - onClose, - owner, -}: AddToCaseActionProps): UseAddToCase => { - const eventId = event?.ecs._id ?? ''; - const dispatch = useDispatch(); - // TODO: use correct value in standalone or integrated. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const timelineById = useDeepEqualSelector((state: any) => { - if (state.timeline) { - return state.timeline.timelineById[eventId]; - } else { - return state.timelineById[eventId]; - } - }); - const isAllCaseModalOpen = useMemo(() => { - if (timelineById) { - return timelineById.isAddToExistingCaseOpen; - } else { - return false; - } - }, [timelineById]); - const isCreateCaseFlyoutOpen = useMemo(() => { - if (timelineById) { - return timelineById.isCreateNewCaseOpen; - } else { - return false; - } - }, [timelineById]); - const { - application: { navigateToApp }, - notifications: { toasts }, - } = useKibana().services; - - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const openPopover = useCallback(() => setIsPopoverOpen(true), []); - const closePopover = useCallback(() => setIsPopoverOpen(false), []); - - const isEventSupported = useMemo(() => { - if (event !== undefined) { - if (event.data.some(({ field }) => field === 'kibana.alert.rule.uuid')) { - return true; - } - return !isEmpty(event.ecs.signal?.rule?.id ?? event.ecs.kibana?.alert?.rule?.uuid); - } else { - return false; - } - }, [event]); - - const userCanCrud = casePermissions?.crud ?? false; - const isDisabled = !userCanCrud || !isEventSupported; - - const onViewCaseClick = useCallback( - (id) => { - navigateToApp(appId, { - deepLinkId: CasesDeepLinkId.cases, - path: generateCaseViewPath({ detailName: id }), - }); - }, - [navigateToApp, appId] - ); - - const onCaseCreated = useCallback(async () => { - dispatch(tGridActions.setOpenAddToNewCase({ id: eventId, isOpen: false })); - }, [eventId, dispatch]); - - const onCaseSuccess = useCallback( - async (theCase: Case) => { - dispatch(tGridActions.setOpenAddToExistingCase({ id: eventId, isOpen: false })); - createUpdateSuccessToaster(toasts, theCase, onViewCaseClick); - }, - [onViewCaseClick, toasts, dispatch, eventId] - ); - const caseAttachments: CaseAttachments = useMemo(() => { - const eventIndex = event?.ecs._index ?? ''; - const { ruleId, ruleName } = normalizedEventFields(event); - const attachments = [ - { - alertId: eventId, - index: eventIndex ?? '', - rule: { - id: ruleId, - name: ruleName, - }, - owner, - type: CommentType.alert as const, - }, - ]; - return attachments; - }, [event, eventId, owner]); - - const onCaseClicked = useCallback( - (theCase?: Case) => { - /** - * No cases listed on the table. - * The user pressed the add new case table's button. - * We gonna open the create case modal. - */ - if (theCase == null) { - dispatch(tGridActions.setOpenAddToNewCase({ id: eventId, isOpen: true })); - } - dispatch(tGridActions.setOpenAddToExistingCase({ id: eventId, isOpen: false })); - }, - [dispatch, eventId] - ); - const addNewCaseClick = useCallback(() => { - closePopover(); - dispatch(tGridActions.setOpenAddToNewCase({ id: eventId, isOpen: true })); - if (onClose) { - onClose(); - } - }, [onClose, closePopover, dispatch, eventId]); - - const addExistingCaseClick = useCallback(() => { - closePopover(); - dispatch(tGridActions.setOpenAddToExistingCase({ id: eventId, isOpen: true })); - if (onClose) { - onClose(); - } - }, [onClose, closePopover, dispatch, eventId]); - return { - caseAttachments, - addNewCaseClick, - addExistingCaseClick, - onCaseClicked, - onCaseSuccess, - onCaseCreated, - isAllCaseModalOpen, - isDisabled, - userCanCrud, - isEventSupported, - openPopover, - closePopover, - isPopoverOpen, - isCreateCaseFlyoutOpen, - }; -}; - -export function normalizedEventFields(event?: TimelineItem) { - const ruleUuidData = event && event.data.find(({ field }) => field === ALERT_RULE_UUID); - const ruleNameData = event && event.data.find(({ field }) => field === ALERT_RULE_NAME); - const ruleUuidValueData = ruleUuidData && ruleUuidData.value && ruleUuidData.value[0]; - const ruleNameValueData = ruleNameData && ruleNameData.value && ruleNameData.value[0]; - - const ruleUuid = - ruleUuidValueData ?? - get(`ecs.${ALERT_RULE_UUID}[0]`, event) ?? - get(`ecs.signal.rule.id[0]`, event) ?? - null; - const ruleName = - ruleNameValueData ?? - get(`ecs.${ALERT_RULE_NAME}[0]`, event) ?? - get(`ecs.signal.rule.name[0]`, event) ?? - null; - - return { - ruleId: ruleUuid, - ruleName, - }; -} diff --git a/x-pack/plugins/timelines/public/methods/index.tsx b/x-pack/plugins/timelines/public/methods/index.tsx index c8b5e28c2d21ef..7d9ec4bcaab275 100644 --- a/x-pack/plugins/timelines/public/methods/index.tsx +++ b/x-pack/plugins/timelines/public/methods/index.tsx @@ -7,14 +7,11 @@ import React, { lazy, Suspense } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; -import { I18nProvider } from '@kbn/i18n-react'; import type { Store } from 'redux'; -import { Provider } from 'react-redux'; import type { Storage } from '../../../../../src/plugins/kibana_utils/public'; import type { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import type { TGridProps } from '../types'; import type { LastUpdatedAtProps, LoadingPanelProps, FieldBrowserProps } from '../components'; -import type { AddToCaseActionProps } from '../components/actions/timeline/cases/add_to_case_action'; import { initialTGridState } from '../store/t_grid/reducer'; import { createStore } from '../store/t_grid'; import { TGridLoading } from '../components/t_grid/shared'; @@ -84,76 +81,3 @@ export const getFieldsBrowserLazy = (props: FieldBrowserProps, { store }: { stor ); }; - -const AddToCaseLazy = lazy(() => import('../components/actions/timeline/cases/add_to_case_action')); -export const getAddToCaseLazy = ( - props: AddToCaseActionProps, - { store, storage, setStore }: { store: Store; storage: Storage; setStore: (store: Store) => void } -) => { - return ( - }> - - - - - - - ); -}; - -const AddToCasePopover = lazy( - () => import('../components/actions/timeline/cases/add_to_case_action_button') -); -export const getAddToCasePopoverLazy = ( - props: AddToCaseActionProps, - { store, storage, setStore }: { store: Store; storage: Storage; setStore: (store: Store) => void } -) => { - initializeStore({ store, storage, setStore }); - return ( - }> - - - - - - - ); -}; - -const AddToExistingButton = lazy( - () => import('../components/actions/timeline/cases/add_to_existing_case_button') -); -export const getAddToExistingCaseButtonLazy = ( - props: AddToCaseActionProps, - { store, storage, setStore }: { store: Store; storage: Storage; setStore: (store: Store) => void } -) => { - initializeStore({ store, storage, setStore }); - return ( - }> - - - - - - - ); -}; - -const AddToNewCaseButton = lazy( - () => import('../components/actions/timeline/cases/add_to_new_case_button') -); -export const getAddToNewCaseButtonLazy = ( - props: AddToCaseActionProps, - { store, storage, setStore }: { store: Store; storage: Storage; setStore: (store: Store) => void } -) => { - initializeStore({ store, storage, setStore }); - return ( - }> - - - - - - - ); -}; diff --git a/x-pack/plugins/timelines/public/mock/global_state.ts b/x-pack/plugins/timelines/public/mock/global_state.ts index fea8aa57b88dd3..955a612f89c1d4 100644 --- a/x-pack/plugins/timelines/public/mock/global_state.ts +++ b/x-pack/plugins/timelines/public/mock/global_state.ts @@ -34,8 +34,6 @@ export const mockGlobalState: TimelineState = { 'packetbeat-*', 'winlogbeat-*', ], - isAddToExistingCaseOpen: false, - isCreateNewCaseOpen: false, isLoading: false, isSelectAllChecked: false, itemsPerPage: 5, diff --git a/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts b/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts index 363a67a30b978e..55ec6862309aa0 100644 --- a/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts +++ b/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts @@ -1566,8 +1566,6 @@ export const mockTgridModel: TGridModel = { selectAll: false, id: 'ef579e40-jibber-jabber', indexNames: [], - isAddToExistingCaseOpen: false, - isCreateNewCaseOpen: false, isLoading: false, isSelectAllChecked: false, kqlQuery: { diff --git a/x-pack/plugins/timelines/public/mock/plugin_mock.tsx b/x-pack/plugins/timelines/public/mock/plugin_mock.tsx index 066485319f9116..be2adfe84a1018 100644 --- a/x-pack/plugins/timelines/public/mock/plugin_mock.tsx +++ b/x-pack/plugins/timelines/public/mock/plugin_mock.tsx @@ -24,6 +24,4 @@ export const createTGridMocks = () => ({ getUseAddToTimeline: () => useAddToTimeline, getUseAddToTimelineSensor: () => useAddToTimelineSensor, getUseDraggableKeyboardWrapper: () => useDraggableKeyboardWrapper, - getAddToExistingCaseButton: () =>
, - getAddToNewCaseButton: () =>
, }); diff --git a/x-pack/plugins/timelines/public/plugin.ts b/x-pack/plugins/timelines/public/plugin.ts index 0ecb063445a46f..8b4bdae43dfe34 100644 --- a/x-pack/plugins/timelines/public/plugin.ts +++ b/x-pack/plugins/timelines/public/plugin.ts @@ -16,10 +16,6 @@ import { getLoadingPanelLazy, getTGridLazy, getFieldsBrowserLazy, - getAddToCaseLazy, - getAddToExistingCaseButtonLazy, - getAddToNewCaseButtonLazy, - getAddToCasePopoverLazy, } from './methods'; import type { TimelinesUIStart, TGridProps, TimelinesStartPlugins } from './types'; import { tGridReducer } from './store/t_grid/reducer'; @@ -88,38 +84,6 @@ export class TimelinesPlugin implements Plugin { setTGridEmbeddedStore: (store: Store) => { this.setStore(store); }, - getAddToCaseAction: (props) => { - return getAddToCaseLazy(props, { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - store: this._store!, - storage: this._storage, - setStore: this.setStore.bind(this), - }); - }, - getAddToCasePopover: (props) => { - return getAddToCasePopoverLazy(props, { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - store: this._store!, - storage: this._storage, - setStore: this.setStore.bind(this), - }); - }, - getAddToExistingCaseButton: (props) => { - return getAddToExistingCaseButtonLazy(props, { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - store: this._store!, - storage: this._storage, - setStore: this.setStore.bind(this), - }); - }, - getAddToNewCaseButton: (props) => { - return getAddToNewCaseButtonLazy(props, { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - store: this._store!, - storage: this._storage, - setStore: this.setStore.bind(this), - }); - }, }; } diff --git a/x-pack/plugins/timelines/public/store/t_grid/actions.ts b/x-pack/plugins/timelines/public/store/t_grid/actions.ts index feab12b616c783..00e207180b1347 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/actions.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/actions.ts @@ -117,11 +117,3 @@ export const setTimelineUpdatedAt = export const addProviderToTimeline = actionCreator<{ id: string; dataProvider: DataProvider }>( 'ADD_PROVIDER_TO_TIMELINE' ); - -export const setOpenAddToExistingCase = actionCreator<{ id: string; isOpen: boolean }>( - 'SET_OPEN_ADD_TO_EXISTING_CASE' -); - -export const setOpenAddToNewCase = actionCreator<{ id: string; isOpen: boolean }>( - 'SET_OPEN_ADD_TO_NEW_CASE' -); diff --git a/x-pack/plugins/timelines/public/store/t_grid/model.ts b/x-pack/plugins/timelines/public/store/t_grid/model.ts index 0640cfb845d9c9..82a4c3e68fd021 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/model.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/model.ts @@ -66,8 +66,6 @@ export interface TGridModel extends TGridModelSettings { /** Uniquely identifies the timeline */ id: string; indexNames: string[]; - isAddToExistingCaseOpen: boolean; - isCreateNewCaseOpen: boolean; isLoading: boolean; /** If selectAll checkbox in header is checked **/ isSelectAllChecked: boolean; diff --git a/x-pack/plugins/timelines/public/store/t_grid/reducer.ts b/x-pack/plugins/timelines/public/store/t_grid/reducer.ts index d3af1dc4e9b302..ae3f9dc5c20b30 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/reducer.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/reducer.ts @@ -18,8 +18,6 @@ import { setEventsDeleted, setEventsLoading, setTGridSelectAll, - setOpenAddToExistingCase, - setOpenAddToNewCase, setSelected, setTimelineUpdatedAt, toggleDetailPanel, @@ -239,26 +237,6 @@ export const tGridReducer = reducerWithInitialState(initialTGridState) ...state, timelineById: addProviderToTimelineHelper(id, dataProvider, state.timelineById), })) - .case(setOpenAddToExistingCase, (state, { id, isOpen }) => ({ - ...state, - timelineById: { - ...state.timelineById, - [id]: { - ...state.timelineById[id], - isAddToExistingCaseOpen: isOpen, - }, - }, - })) - .case(setOpenAddToNewCase, (state, { id, isOpen }) => ({ - ...state, - timelineById: { - ...state.timelineById, - [id]: { - ...state.timelineById[id], - isCreateNewCaseOpen: isOpen, - }, - }, - })) .case(setTimelineUpdatedAt, (state, { id, updated }) => ({ ...state, timelineById: { diff --git a/x-pack/plugins/timelines/public/store/t_grid/selectors.ts b/x-pack/plugins/timelines/public/store/t_grid/selectors.ts index 9077cac2b5dd01..2db463e47cc883 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/selectors.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/selectors.ts @@ -6,26 +6,11 @@ */ import { getOr } from 'lodash/fp'; import { createSelector } from 'reselect'; -import { TGridModel, State } from '.'; +import { TGridModel } from '.'; import { tGridDefaults, getTGridManageDefaults } from './defaults'; -interface TGridById { - [id: string]: TGridModel; -} - const getDefaultTgrid = (id: string) => ({ ...tGridDefaults, ...getTGridManageDefaults(id) }); -const standaloneTGridById = (state: State): TGridById => state.timelineById; - -export const activeCaseFlowId = createSelector(standaloneTGridById, (tGrid) => { - return ( - tGrid && - Object.entries(tGrid) - .map(([id, data]) => (data.isAddToExistingCaseOpen || data.isCreateNewCaseOpen ? id : null)) - .find((id) => id) - ); -}); - export const selectTGridById = (state: unknown, timelineId: string): TGridModel => { return getOr( getOr(getDefaultTgrid(timelineId), ['timelineById', timelineId], state), diff --git a/x-pack/plugins/timelines/public/types.ts b/x-pack/plugins/timelines/public/types.ts index ddecac02be705d..0c9e6aa3e325f8 100644 --- a/x-pack/plugins/timelines/public/types.ts +++ b/x-pack/plugins/timelines/public/types.ts @@ -23,7 +23,6 @@ import type { TGridIntegratedProps } from './components/t_grid/integrated'; import type { TGridStandaloneProps } from './components/t_grid/standalone'; import type { UseAddToTimelineProps, UseAddToTimeline } from './hooks/use_add_to_timeline'; import { HoverActionsConfig } from './components/hover_actions/index'; -import type { AddToCaseActionProps } from './components/actions/timeline/cases/add_to_case_action'; import { TimelineTabs } from '../common/types'; export * from './store/t_grid'; export interface TimelinesUIStart { @@ -42,10 +41,6 @@ export interface TimelinesUIStart { props: UseDraggableKeyboardWrapperProps ) => UseDraggableKeyboardWrapper; setTGridEmbeddedStore: (store: Store) => void; - getAddToCaseAction: (props: AddToCaseActionProps) => ReactElement; - getAddToCasePopover: (props: AddToCaseActionProps) => ReactElement; - getAddToExistingCaseButton: (props: AddToCaseActionProps) => ReactElement; - getAddToNewCaseButton: (props: AddToCaseActionProps) => ReactElement; } export interface TimelinesStartPlugins { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 70d65550a40563..a80c43bd832d87 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3394,12 +3394,6 @@ "home.addData.uploadFileButtonLabel": "ファイルをアップロード", "home.breadcrumbs.homeTitle": "ホーム", "home.breadcrumbs.integrationsAppTitle": "統合", - "home.dataManagementDisableCollection": " 収集を停止するには、", - "home.dataManagementDisableCollectionLink": "ここで使用状況データを無効にします。", - "home.dataManagementDisclaimerPrivacy": "使用状況データがどのように製品とサービスの管理と改善につながるのかに関する詳細については ", - "home.dataManagementDisclaimerPrivacyLink": "プライバシーポリシーをご覧ください。", - "home.dataManagementEnableCollection": " 収集を開始するには、", - "home.dataManagementEnableCollectionLink": "ここで使用状況データを有効にします。", "home.exploreButtonLabel": "独りで閲覧", "home.exploreYourDataDescription": "すべてのステップを終えたら、データ閲覧準備の完了です。", "home.header.title": "ようこそホーム", @@ -22657,7 +22651,6 @@ "xpack.securitySolution.alertDetails.summary.readLess": "表示を減らす", "xpack.securitySolution.alertDetails.summary.readMore": "続きを読む", "xpack.securitySolution.alertDetails.threatIntel": "Threat Intel", - "xpack.securitySolution.alerts.badge.readOnly.text": "読み取り専用", "xpack.securitySolution.alerts.badge.readOnly.tooltip": "アラートを更新できません", "xpack.securitySolution.alerts.riskScoreMapping.defaultDescriptionLabel": "このルールで生成されたすべてのアラートのリスクスコアを選択します。", "xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle": "デフォルトリスクスコア", @@ -27010,16 +27003,6 @@ "xpack.timelines.alerts.summaryView.options.summaryView.description": "各アラートのイベントフローのレンダリングを表示", "xpack.timelines.beatFields.errorSearchDescription": "Beatフィールドの取得でエラーが発生しました", "xpack.timelines.beatFields.failSearchDescription": "Beat フィールドで検索を実行できませんでした", - "xpack.timelines.cases.timeline.actions.addCase": "ケースに追加", - "xpack.timelines.cases.timeline.actions.addExistingCase": "既存のケースに追加", - "xpack.timelines.cases.timeline.actions.addNewCase": "新しいケースに追加", - "xpack.timelines.cases.timeline.actions.addToCaseAriaLabel": "アラートをケースに関連付ける", - "xpack.timelines.cases.timeline.actions.addToCaseTooltip": "ケースに追加", - "xpack.timelines.cases.timeline.actions.caseCreatedSuccessToast": "アラートが「{title}」に追加されました", - "xpack.timelines.cases.timeline.actions.caseCreatedSuccessToastText": "このケースのアラートはステータスがケースステータスと同期されました", - "xpack.timelines.cases.timeline.actions.caseCreatedSuccessToastViewCaseLink": "ケースの表示", - "xpack.timelines.cases.timeline.actions.permissionsMessage": "現在、アラートをケースに関連付けるための必要な権限がありません。サポートについては、管理者にお問い合わせください。", - "xpack.timelines.cases.timeline.actions.unsupportedEventsMessage": "このイベントはケースに関連付けられません", "xpack.timelines.clipboard.copied": "コピー完了", "xpack.timelines.clipboard.copy": "コピー", "xpack.timelines.clipboard.copy.successToastTitle": "フィールド{field}をクリップボードにコピーしました", @@ -27035,9 +27018,7 @@ "xpack.timelines.exitFullScreenButton": "全画面を終了", "xpack.timelines.fieldBrowser.categoriesCountTitle": "{totalCount} {totalCount, plural, other {カテゴリ}}", "xpack.timelines.fieldBrowser.categoriesTitle": "カテゴリー", - "xpack.timelines.fieldBrowser.categoryFieldsTableCaption": "カテゴリ {categoryId} フィールド", "xpack.timelines.fieldBrowser.categoryLabel": "カテゴリー", - "xpack.timelines.fieldBrowser.categoryLinkAriaLabel": "{category} {totalCount} {totalCount, plural, other {フィールド}}このボタンをクリックすると、{category} カテゴリを選択します。", "xpack.timelines.fieldBrowser.closeButton": "閉じる", "xpack.timelines.fieldBrowser.descriptionForScreenReaderOnly": "フィールド {field} の説明:", "xpack.timelines.fieldBrowser.descriptionLabel": "説明", @@ -27049,7 +27030,6 @@ "xpack.timelines.fieldBrowser.noFieldsMatchInputLabel": "{searchInput} に一致するフィールドがありません", "xpack.timelines.fieldBrowser.noFieldsMatchLabel": "一致するフィールドがありません", "xpack.timelines.fieldBrowser.resetFieldsLink": "フィールドをリセット", - "xpack.timelines.fieldBrowser.viewCategoryTooltip": "すべての {categoryId} フィールドを表示します", "xpack.timelines.fieldBrowser.viewColumnCheckboxAriaLabel": "{field} 列を表示", "xpack.timelines.footer.autoRefreshActiveDescription": "自動更新アクション", "xpack.timelines.footer.autoRefreshActiveTooltip": "自動更新が有効な間、タイムラインはクエリに一致する最新の {numberOfItems} 件のイベントを表示します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 90eb0d3c35f653..15a194346a9e11 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3402,12 +3402,6 @@ "home.addData.uploadFileButtonLabel": "上传文件", "home.breadcrumbs.homeTitle": "主页", "home.breadcrumbs.integrationsAppTitle": "集成", - "home.dataManagementDisableCollection": " 要停止收集,", - "home.dataManagementDisableCollectionLink": "请在此禁用使用情况数据。", - "home.dataManagementDisclaimerPrivacy": "要了解使用情况数据如何帮助我们管理和改善产品和服务,请参阅我们的 ", - "home.dataManagementDisclaimerPrivacyLink": "隐私声明。", - "home.dataManagementEnableCollection": " 要启动收集,", - "home.dataManagementEnableCollectionLink": "请在此处启用使用情况数据。", "home.exploreButtonLabel": "自己浏览", "home.exploreYourDataDescription": "完成所有步骤后,您便可以随时浏览自己的数据。", "home.header.title": "欢迎归来", @@ -22686,7 +22680,6 @@ "xpack.securitySolution.alertDetails.summary.readLess": "阅读更少内容", "xpack.securitySolution.alertDetails.summary.readMore": "阅读更多内容", "xpack.securitySolution.alertDetails.threatIntel": "威胁情报", - "xpack.securitySolution.alerts.badge.readOnly.text": "只读", "xpack.securitySolution.alerts.badge.readOnly.tooltip": "无法更新告警", "xpack.securitySolution.alerts.riskScoreMapping.defaultDescriptionLabel": "选择此规则生成的所有告警的风险分数。", "xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle": "默认风险分数", @@ -27042,16 +27035,6 @@ "xpack.timelines.alerts.summaryView.options.summaryView.description": "查看每个告警的事件渲染", "xpack.timelines.beatFields.errorSearchDescription": "获取 Beat 字段时发生错误", "xpack.timelines.beatFields.failSearchDescription": "无法对 Beat 字段执行搜索", - "xpack.timelines.cases.timeline.actions.addCase": "添加到案例", - "xpack.timelines.cases.timeline.actions.addExistingCase": "添加到现有案例", - "xpack.timelines.cases.timeline.actions.addNewCase": "添加到新案例", - "xpack.timelines.cases.timeline.actions.addToCaseAriaLabel": "将告警附加到案例", - "xpack.timelines.cases.timeline.actions.addToCaseTooltip": "添加到案例", - "xpack.timelines.cases.timeline.actions.caseCreatedSuccessToast": "告警已添加到“{title}”", - "xpack.timelines.cases.timeline.actions.caseCreatedSuccessToastText": "此案例中的告警的状态已经与案例状态同步", - "xpack.timelines.cases.timeline.actions.caseCreatedSuccessToastViewCaseLink": "查看案例", - "xpack.timelines.cases.timeline.actions.permissionsMessage": "您当前缺少所需的权限,无法向案例附加告警。有关进一步帮助,请联系您的管理员。", - "xpack.timelines.cases.timeline.actions.unsupportedEventsMessage": "此事件无法附加到案例", "xpack.timelines.clipboard.copied": "已复制", "xpack.timelines.clipboard.copy": "复制", "xpack.timelines.clipboard.copy.successToastTitle": "已将字段 {field} 复制到剪贴板", @@ -27067,9 +27050,7 @@ "xpack.timelines.exitFullScreenButton": "退出全屏", "xpack.timelines.fieldBrowser.categoriesCountTitle": "{totalCount} {totalCount, plural, other {个类别}}", "xpack.timelines.fieldBrowser.categoriesTitle": "类别", - "xpack.timelines.fieldBrowser.categoryFieldsTableCaption": "类别 {categoryId} 字段", "xpack.timelines.fieldBrowser.categoryLabel": "类别", - "xpack.timelines.fieldBrowser.categoryLinkAriaLabel": "{category} {totalCount} 个{totalCount, plural, other {字段}}。单击此按钮可选择 {category} 类别。", "xpack.timelines.fieldBrowser.closeButton": "关闭", "xpack.timelines.fieldBrowser.descriptionForScreenReaderOnly": "{field} 字段的描述:", "xpack.timelines.fieldBrowser.descriptionLabel": "描述", @@ -27081,7 +27062,6 @@ "xpack.timelines.fieldBrowser.noFieldsMatchInputLabel": "没有字段匹配“{searchInput}”", "xpack.timelines.fieldBrowser.noFieldsMatchLabel": "没有字段匹配", "xpack.timelines.fieldBrowser.resetFieldsLink": "重置字段", - "xpack.timelines.fieldBrowser.viewCategoryTooltip": "查看所有 {categoryId} 字段", "xpack.timelines.fieldBrowser.viewColumnCheckboxAriaLabel": "查看 {field} 列", "xpack.timelines.footer.autoRefreshActiveDescription": "自动刷新已启用", "xpack.timelines.footer.autoRefreshActiveTooltip": "自动刷新已启用时,时间线将显示匹配查询的最近 {numberOfItems} 个事件。", diff --git a/x-pack/test/alerting_api_integration/basic/tests/alerts/basic_noop_alert_type.ts b/x-pack/test/alerting_api_integration/basic/tests/alerts/basic_noop_alert_type.ts index 805f4440909ec2..a176d4e73ccf36 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/alerts/basic_noop_alert_type.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/alerts/basic_noop_alert_type.ts @@ -5,19 +5,19 @@ * 2.0. */ -import { getTestAlertData } from '../../../common/lib'; +import { getTestRuleData } from '../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export -export default function basicAlertTest({ getService }: FtrProviderContext) { +export default function basicRuleTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - describe('basic alert', () => { - it('should return 200 when creating a basic license alert', async () => { + describe('basic rule', () => { + it('should return 200 when creating a basic license rule', async () => { await supertest .post(`/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); }); }); diff --git a/x-pack/test/alerting_api_integration/basic/tests/alerts/gold_noop_alert_type.ts b/x-pack/test/alerting_api_integration/basic/tests/alerts/gold_noop_alert_type.ts index 9e66282d42454d..5726ad8d5d86cd 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/alerts/gold_noop_alert_type.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/alerts/gold_noop_alert_type.ts @@ -5,19 +5,19 @@ * 2.0. */ -import { getTestAlertData } from '../../../common/lib'; +import { getTestRuleData } from '../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function emailTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - describe('create gold noop alert', () => { - it('should return 403 when creating an gold alert', async () => { + describe('create gold noop rule', () => { + it('should return 403 when creating an gold rule', async () => { await supertest .post(`/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ rule_type_id: 'test.gold.noop' })) + .send(getTestRuleData({ rule_type_id: 'test.gold.noop' })) .expect(403, { statusCode: 403, error: 'Forbidden', diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index 7937a9a2db92c6..0f345c81f08b4c 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -5,9 +5,15 @@ * 2.0. */ -import { Plugin, CoreSetup, Logger, PluginInitializerContext } from 'kibana/server'; +import { Plugin, CoreSetup, CoreStart, Logger, PluginInitializerContext } from 'kibana/server'; +import { Subject } from 'rxjs'; +import { first } from 'rxjs/operators'; import { PluginSetupContract as ActionsPluginSetup } from '../../../../../../../plugins/actions/server/plugin'; import { PluginSetupContract as AlertingPluginSetup } from '../../../../../../../plugins/alerting/server/plugin'; +import { + TaskManagerSetupContract, + TaskManagerStartContract, +} from '../../../../../../../plugins/task_manager/server/plugin'; import { EncryptedSavedObjectsPluginStart } from '../../../../../../../plugins/encrypted_saved_objects/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; import { defineAlertTypes } from './alert_types'; @@ -21,6 +27,7 @@ export interface FixtureSetupDeps { features: FeaturesPluginSetup; actions: ActionsPluginSetup; alerting: AlertingPluginSetup; + taskManager: TaskManagerSetupContract; } export interface FixtureStartDeps { @@ -28,11 +35,17 @@ export interface FixtureStartDeps { security?: SecurityPluginStart; spaces?: SpacesPluginStart; actions: ActionsPluginStart; + taskManager: TaskManagerStartContract; } export class FixturePlugin implements Plugin { private readonly logger: Logger; + taskManagerStart$: Subject = new Subject(); + taskManagerStart: Promise = this.taskManagerStart$ + .pipe(first()) + .toPromise(); + constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get('fixtures', 'plugins', 'alerts'); } @@ -127,9 +140,12 @@ export class FixturePlugin implements Plugin, { logger }: { logger: Logger }) { +export function defineRoutes( + core: CoreSetup, + taskManagerStart: Promise, + { logger }: { logger: Logger } +) { const router = core.http.createRouter(); router.put( { @@ -324,4 +329,39 @@ export function defineRoutes(core: CoreSetup, { logger }: { lo } } ); + + router.post( + { + path: `/api/alerting_actions_telemetry/run_now`, + validate: { + body: schema.object({ + taskId: schema.string({ + validate: (telemetryTaskId: string) => { + if ( + ['Alerting-alerting_telemetry', 'Actions-actions_telemetry'].includes( + telemetryTaskId + ) + ) { + return; + } + return 'invalid telemetry task id'; + }, + }), + }), + }, + }, + async function ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + const { taskId } = req.body; + try { + const taskManager = await taskManagerStart; + return res.ok({ body: await taskManager.runNow(taskId) }); + } catch (err) { + return res.ok({ body: { id: taskId, error: `${err}` } }); + } + } + ); } diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index ea16351b495438..9da73e1ca6f43e 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -9,7 +9,7 @@ import { Space, User } from '../types'; import { ObjectRemover } from './object_remover'; import { getUrlPrefix } from './space_test_utils'; import { ES_TEST_INDEX_NAME } from './es_test_index_tool'; -import { getTestAlertData } from './get_test_alert_data'; +import { getTestRuleData } from './get_test_rule_data'; export interface AlertUtilsOpts { user?: User; @@ -293,7 +293,7 @@ export class AlertUtils { request = request.auth(this.user.username, this.user.password); } const response = await request.send({ - ...getTestAlertData(), + ...getTestRuleData(), ...overwrites, }); if (response.statusCode === 200) { diff --git a/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts b/x-pack/test/alerting_api_integration/common/lib/get_test_rule_data.ts similarity index 91% rename from x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts rename to x-pack/test/alerting_api_integration/common/lib/get_test_rule_data.ts index 22dc93b110a074..ace220a5e81deb 100644 --- a/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts +++ b/x-pack/test/alerting_api_integration/common/lib/get_test_rule_data.ts @@ -5,7 +5,7 @@ * 2.0. */ -export function getTestAlertData(overwrites = {}) { +export function getTestRuleData(overwrites = {}) { return { enabled: true, name: 'abc', diff --git a/x-pack/test/alerting_api_integration/common/lib/index.ts b/x-pack/test/alerting_api_integration/common/lib/index.ts index 305c42b5c1d64d..df7895ed03f6ac 100644 --- a/x-pack/test/alerting_api_integration/common/lib/index.ts +++ b/x-pack/test/alerting_api_integration/common/lib/index.ts @@ -8,7 +8,7 @@ export { ObjectRemover } from './object_remover'; export { getUrlPrefix } from './space_test_utils'; export { ES_TEST_INDEX_NAME, ESTestIndexTool } from './es_test_index_tool'; -export { getTestAlertData } from './get_test_alert_data'; +export { getTestRuleData } from './get_test_rule_data'; export { AlertUtils, getConsumerUnauthorizedErrorMessage, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts index 5692e5dd8f8b2c..8ae50b91584878 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -134,7 +134,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ actions: [ { group: 'default', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index c3e3c4fc930054..37455149a2a429 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -14,7 +14,7 @@ import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, AlertUtils, getConsumerUnauthorizedErrorMessage, @@ -494,7 +494,7 @@ instanceStateValue: true .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.always-firing', params: { index: ES_TEST_INDEX_NAME, @@ -603,7 +603,7 @@ instanceStateValue: true .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.authorization', params: { callClusterAuthorizationIndex: authorizationIndex, @@ -711,7 +711,7 @@ instanceStateValue: true .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.always-firing', params: { index: ES_TEST_INDEX_NAME, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts index eaa73facb37345..3044142e3c54c4 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { UserAtSpaceScenarios } from '../../scenarios'; import { checkAAD, - getTestAlertData, + getTestRuleData, getConsumerUnauthorizedErrorMessage, getUrlPrefix, ObjectRemover, @@ -57,7 +57,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ actions: [ { id: createdAction.id, @@ -155,7 +155,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', }) @@ -194,7 +194,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', }) @@ -244,7 +244,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.noop', consumer: 'alerts', }) @@ -290,7 +290,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.noop', consumer: 'some consumer patrick invented', }) @@ -325,7 +325,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) - .send(getTestAlertData({ enabled: false })); + .send(getTestRuleData({ enabled: false })); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -361,7 +361,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ name: ' leading and trailing whitespace ', }) ); @@ -400,7 +400,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unregistered-alert-type', }) ); @@ -458,7 +458,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.validation', }) ); @@ -500,7 +500,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) - .send(getTestAlertData(getTestAlertData({ schedule: { interval: '10x' } }))); + .send(getTestRuleData({ schedule: { interval: '10x' } })); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -527,7 +527,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) - .send(getTestAlertData(getTestAlertData({ schedule: { interval: '0s' } }))); + .send(getTestRuleData({ schedule: { interval: '0s' } })); switch (scenario.id) { case 'no_kibana_privileges at space1': diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts index d43fb2e7d835fa..93e39a011ba3aa 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { UserAtSpaceScenarios } from '../../scenarios'; import { getUrlPrefix, - getTestAlertData, + getTestRuleData, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, ObjectRemover, @@ -42,7 +42,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); const response = await supertestWithoutAuth @@ -91,7 +91,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', }) @@ -144,7 +144,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', }) @@ -211,7 +211,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.noop', consumer: 'alerts', }) @@ -270,7 +270,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -303,7 +303,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); await retry.try(async () => { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts index 66f01000ede5e7..8a4266eb8dc8a5 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, @@ -58,7 +58,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: true, actions: [ { @@ -121,7 +121,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', enabled: true, @@ -170,7 +170,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', enabled: true, @@ -230,7 +230,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.noop', consumer: 'alerts', enabled: true, @@ -285,7 +285,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: true })) + .send(getTestRuleData({ enabled: true })) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -351,7 +351,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertest .post(`${getUrlPrefix('other')}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: true })) + .send(getTestRuleData({ enabled: true })) .expect(200); objectRemover.add('other', createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts index 1589a63cb7108a..205bfe3fda2abb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, @@ -60,7 +60,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, actions: [ { @@ -146,7 +146,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', enabled: false, @@ -195,7 +195,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', enabled: false, @@ -249,7 +249,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.noop', consumer: 'alerts', enabled: false, @@ -304,7 +304,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -375,7 +375,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex const { body: createdAlert } = await supertest .post(`${getUrlPrefix('other')}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add('other', createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts index 940203a9b1f8c5..04ff3d929dc159 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover, getEventLog } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { validateEvent } from '../../../spaces_only/tests/alerting/event_log'; @@ -27,7 +27,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(spaceId)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.noop', schedule: { interval: '1s' }, throttle: null, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/excluded.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/excluded.ts index add30b178c7e6e..eae80da85dc599 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/excluded.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/excluded.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { UserAtSpaceScenarios } from '../../scenarios'; import { - getTestAlertData, + getTestRuleData, getUrlPrefix, ObjectRemover, getEventLog, @@ -57,7 +57,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(scenario.user.username, scenario.user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.always-firing', schedule: { interval: '1s' }, throttle: '1s', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts index 2bae1c541bc485..dba73cba184ddb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { AlertExecutionStatusErrorReasons } from '../../../../../plugins/alerting/common'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -28,7 +28,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(spaceId)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.noop', schedule: { interval: '1s' }, }) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts index 3274e25e48301a..f6ba70e7c21972 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts @@ -10,7 +10,7 @@ import { SuperTest, Test } from 'supertest'; import { chunk, omit } from 'lodash'; import uuid from 'uuid'; import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; const findTestUtils = ( @@ -29,7 +29,7 @@ const findTestUtils = ( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -94,7 +94,7 @@ const findTestUtils = ( it('should filter out types that the user is not authorized to `get` retaining pagination', async () => { async function createNoOpAlert(overrides = {}) { - const alert = getTestAlertData(overrides); + const alert = getTestRuleData(overrides); const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') @@ -212,7 +212,7 @@ const findTestUtils = ( .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, actions: [ { @@ -297,7 +297,7 @@ const findTestUtils = ( .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, tags: [myTag], rule_type_id: 'test.restricted-noop', @@ -312,7 +312,7 @@ const findTestUtils = ( .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ tags: [myTag], rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', @@ -374,7 +374,7 @@ const findTestUtils = ( .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, tags: [myTag], rule_type_id: 'test.restricted-noop', @@ -389,7 +389,7 @@ const findTestUtils = ( .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ tags: [myTag], rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', @@ -451,7 +451,7 @@ const findTestUtils = ( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts index 05b053f468a694..6d072b2e26f455 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts @@ -10,7 +10,7 @@ import { SuperTest, Test } from 'supertest'; import { UserAtSpaceScenarios } from '../../scenarios'; import { getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, @@ -32,7 +32,7 @@ const getTestUtils = ( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -96,7 +96,7 @@ const getTestUtils = ( .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', }) @@ -143,7 +143,7 @@ const getTestUtils = ( .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', }) @@ -201,7 +201,7 @@ const getTestUtils = ( .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alerts', }) @@ -258,7 +258,7 @@ const getTestUtils = ( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts index e00d8e53e438eb..3bdfe49464fcfe 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { getUrlPrefix, ObjectRemover, - getTestAlertData, + getTestRuleData, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, } from '../../../common/lib'; @@ -33,7 +33,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -69,7 +69,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', }) @@ -123,7 +123,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_summary.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_summary.ts index 3becd487116f72..eb4e592a91d8af 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_summary.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_summary.ts @@ -10,7 +10,7 @@ import { omit } from 'lodash'; import { getUrlPrefix, ObjectRemover, - getTestAlertData, + getTestRuleData, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, } from '../../../common/lib'; @@ -34,7 +34,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo const { body: createdRule } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); @@ -98,7 +98,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', }) @@ -154,7 +154,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo const { body: createdRule } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts index 22bf2cdc4204bd..d51cf8cc96af90 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts @@ -10,7 +10,7 @@ import { UserAtSpaceScenarios } from '../../scenarios'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, AlertUtils, ESTestIndexTool, @@ -105,7 +105,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ schedule: { interval: '5m', }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mustache_templates.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mustache_templates.ts index 8344d4a281ba1f..7e3a7599a73e06 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mustache_templates.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mustache_templates.ts @@ -19,7 +19,7 @@ import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { getSlackServer } from '../../../common/fixtures/plugins/actions_simulators/server/plugin'; import { getHttpProxyServer } from '../../../common/lib/get_proxy_server'; @@ -81,7 +81,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces[0].id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ name: 'testing context variable kibanaBaseUrl', rule_type_id: 'test.patternFiring', params: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts index 993b66353756f6..bb570e5754e99f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, @@ -49,7 +49,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, actions: [ { @@ -117,7 +117,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', @@ -173,7 +173,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', @@ -240,7 +240,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.restricted-noop', consumer: 'alerts', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts index 4948737e0778a3..3948f910423a9d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, @@ -49,7 +49,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, actions: [ { @@ -117,7 +117,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', @@ -173,7 +173,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', @@ -240,7 +240,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.restricted-noop', consumer: 'alerts', @@ -306,7 +306,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts index 526f809033646c..f9c1bce2b03188 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, @@ -49,7 +49,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, actions: [ { @@ -122,7 +122,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', @@ -183,7 +183,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', @@ -255,7 +255,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.restricted-noop', consumer: 'alerts', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts index 9c045db8883911..17ee25e822a6da 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, @@ -49,7 +49,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, actions: [ { @@ -122,7 +122,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', @@ -183,7 +183,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', @@ -255,7 +255,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.restricted-noop', consumer: 'alerts', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index e628f0b3d950e2..b2a1ae223f62c9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -11,7 +11,7 @@ import { UserAtSpaceScenarios } from '../../scenarios'; import { checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, ensureDatetimeIsWithinRange, getConsumerUnauthorizedErrorMessage, @@ -55,7 +55,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -156,7 +156,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', }) @@ -240,7 +240,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', }) @@ -335,7 +335,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alerts', }) @@ -429,7 +429,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -522,7 +522,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -574,7 +574,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -618,7 +618,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -691,7 +691,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.validation', params: { param1: 'test', @@ -753,7 +753,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ schedule: { interval: '10x' }, enabled: undefined, consumer: undefined, @@ -785,7 +785,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ schedule: { interval: '30m' }, }) ) @@ -931,7 +931,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ schedule: { interval: '1m' }, }) ) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts index a434109a18933a..1c25ec550c41ef 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, @@ -50,7 +50,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ actions: [ { id: createdAction.id, @@ -116,7 +116,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', }) @@ -170,7 +170,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', }) @@ -235,7 +235,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alerts', }) @@ -299,7 +299,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -363,7 +363,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertest .post(`${getUrlPrefix('other')}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add('other', createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts index 211fe9ec268632..2b26410afeaede 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts @@ -60,6 +60,7 @@ export default function alertingApiIntegrationTests({ loadTestFile }: FtrProvide describe('alerting api integration security and spaces enabled', function () { this.tags('ciGroup17'); + loadTestFile(require.resolve('./telemetry')); loadTestFile(require.resolve('./actions')); loadTestFile(require.resolve('./alerting')); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/actions_telemetry.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/actions_telemetry.ts new file mode 100644 index 00000000000000..350f0019641b8b --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/actions_telemetry.ts @@ -0,0 +1,241 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { Spaces, Superuser } from '../../scenarios'; +import { + getUrlPrefix, + getEventLog, + getTestRuleData, + ObjectRemover, + TaskManagerDoc, +} from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createActionsTelemetryTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + const retry = getService('retry'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('actions telemetry', () => { + const alwaysFiringRuleId: { [key: string]: string } = {}; + const objectRemover = new ObjectRemover(supertest); + + before(async () => { + // reset the state in the telemetry task + await es.update({ + id: `task:Actions-actions_telemetry`, + index: '.kibana_task_manager', + body: { + doc: { + task: { + state: '{}', + }, + }, + }, + }); + }); + after(() => objectRemover.removeAll()); + + async function createConnector(opts: { name: string; space: string; connectorTypeId: string }) { + const { name, space, connectorTypeId } = opts; + const { body: createdConnector } = await supertestWithoutAuth + .post(`${getUrlPrefix(space)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .auth(Superuser.username, Superuser.password) + .send({ + name, + connector_type_id: connectorTypeId, + config: {}, + secrets: {}, + }) + .expect(200); + objectRemover.add(space, createdConnector.id, 'action', 'actions'); + return createdConnector.id; + } + + async function createRule(opts: { space: string; ruleOverwrites: any }) { + const { ruleOverwrites, space } = opts; + const ruleResponse = await supertestWithoutAuth + .post(`${getUrlPrefix(space)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .auth(Superuser.username, Superuser.password) + .send(getTestRuleData(ruleOverwrites)); + expect(ruleResponse.status).to.eql(200); + objectRemover.add(space, ruleResponse.body.id, 'rule', 'alerting'); + return ruleResponse.body.id; + } + + async function setup() { + // Create rules and connectors in multiple spaces + for (const space of Spaces) { + const noopConnectorId = await createConnector({ + name: 'noop connector', + space: space.id, + connectorTypeId: 'test.noop', + }); + const failingConnectorId = await createConnector({ + name: 'connector that throws', + space: space.id, + connectorTypeId: 'test.throw', + }); + + await createConnector({ + name: 'unused connector', + space: space.id, + connectorTypeId: 'test.excluded', + }); + await createRule({ + space: space.id, + ruleOverwrites: { + rule_type_id: 'test.noop', + schedule: { interval: '1s' }, + throttle: null, + notify_when: 'onActiveAlert', + params: {}, + actions: [ + { + id: noopConnectorId, + group: 'default', + params: {}, + }, + ], + }, + }); + await createRule({ + space: space.id, + ruleOverwrites: { + rule_type_id: 'test.noop', + schedule: { interval: '1s' }, + throttle: null, + params: {}, + notify_when: 'onActiveAlert', + actions: [ + { + id: failingConnectorId, + group: 'default', + params: {}, + }, + ], + }, + }); + + alwaysFiringRuleId[space.id] = await createRule({ + space: space.id, + ruleOverwrites: { + rule_type_id: 'test.cumulative-firing', + schedule: { interval: '3s' }, + throttle: null, + notify_when: 'onActiveAlert', + params: {}, + actions: [ + { + id: noopConnectorId, + group: 'default', + params: {}, + }, + { + id: failingConnectorId, + group: 'default', + params: {}, + }, + { + id: 'my-slack1', + group: 'other', + params: {}, + }, + ], + }, + }); + } + } + + it('should retrieve telemetry data in the expected format', async () => { + await setup(); + + // let it run for a bit + await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: Spaces[0].id, + type: 'alert', + id: alwaysFiringRuleId[Spaces[0].id], + provider: 'alerting', + actions: new Map([['execute', { gte: 5 }]]), + }); + }); + + // request telemetry task to run + await supertest + .post('/api/alerting_actions_telemetry/run_now') + .set('kbn-xsrf', 'xxx') + .send({ taskId: 'Actions-actions_telemetry' }) + .expect(200); + + // get telemetry task doc + const telemetryTask = await es.get({ + id: `task:Actions-actions_telemetry`, + index: '.kibana_task_manager', + }); + const taskState = telemetryTask?._source?.task?.state; + expect(taskState).not.to.be(undefined); + const telemetry = JSON.parse(taskState!); + + // total number of connectors + expect(telemetry.count_total).to.equal(17); + + // total number of active connectors (used by a rule) + expect(telemetry.count_active_total).to.equal(7); + + // total number of connectors broken down by connector type + expect(telemetry.count_by_type['test.throw']).to.equal(3); + expect(telemetry.count_by_type['test.excluded']).to.equal(3); + expect(telemetry.count_by_type['test.noop']).to.equal(3); + expect(telemetry.count_by_type.__slack).to.equal(1); + expect(telemetry.count_by_type['system-abc-action-type']).to.equal(1); + expect(telemetry.count_by_type.__index).to.equal(1); + expect(telemetry.count_by_type['test.index-record']).to.equal(1); + expect(telemetry.count_by_type.__webhook).to.equal(4); + + // total number of active connectors broken down by connector type + expect(telemetry.count_active_by_type['test.throw']).to.equal(3); + expect(telemetry.count_active_by_type['test.noop']).to.equal(3); + expect(telemetry.count_active_by_type.__slack).to.equal(1); + + // total number of rules using the alert history connector + expect(telemetry.count_active_alert_history_connectors).to.equal(0); + + // total number of email connectors used by rules broken down by service type + // testing for existence of this field but we don't have any rules using email + // connectors in this test + expect(telemetry.count_active_email_connectors_by_service_type).to.be.empty(); + + // number of spaces with connectors + expect(telemetry.count_actions_namespaces).to.equal(3); + + // number of action executions - just checking for non-zero as we can't set an exact number + expect(telemetry.count_actions_executions_per_day > 0).to.be(true); + + // number of action executions broken down by connector type + expect(telemetry.count_actions_executions_by_type_per_day['test.noop'] > 0).to.be(true); + + // average execution time - just checking for non-zero as we can't set an exact number + expect(telemetry.avg_execution_time_per_day > 0).to.be(true); + + // average execution time broken down by rule type + expect(telemetry.avg_execution_time_by_type_per_day['test.noop'] > 0).to.be(true); + + // number of failed executions + expect(telemetry.count_actions_executions_failed_per_day > 0).to.be(true); + expect(telemetry.count_actions_executions_failed_by_type_per_day['test.throw'] > 0).to.be( + true + ); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/alerting_telemetry.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/alerting_telemetry.ts new file mode 100644 index 00000000000000..9b8a96bc056cef --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/alerting_telemetry.ts @@ -0,0 +1,304 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { Spaces, Superuser } from '../../scenarios'; +import { + getUrlPrefix, + getEventLog, + getTestRuleData, + ObjectRemover, + TaskManagerDoc, +} from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createAlertingTelemetryTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + const retry = getService('retry'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('alerting telemetry', () => { + const alwaysFiringRuleId: { [key: string]: string } = {}; + const objectRemover = new ObjectRemover(supertest); + + before(async () => { + // reset the state in the telemetry task + await es.update({ + id: `task:Alerting-alerting_telemetry`, + index: '.kibana_task_manager', + body: { + doc: { + task: { + state: '{}', + }, + }, + }, + }); + }); + after(() => objectRemover.removeAll()); + + async function createConnector(opts: { name: string; space: string; connectorTypeId: string }) { + const { name, space, connectorTypeId } = opts; + const { body: createdConnector } = await supertestWithoutAuth + .post(`${getUrlPrefix(space)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .auth(Superuser.username, Superuser.password) + .send({ + name, + connector_type_id: connectorTypeId, + config: {}, + secrets: {}, + }) + .expect(200); + objectRemover.add(space, createdConnector.id, 'action', 'actions'); + return createdConnector.id; + } + + async function createRule(opts: { space: string; ruleOverwrites: any }) { + const { ruleOverwrites, space } = opts; + const ruleResponse = await supertestWithoutAuth + .post(`${getUrlPrefix(space)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .auth(Superuser.username, Superuser.password) + .send(getTestRuleData(ruleOverwrites)); + expect(ruleResponse.status).to.eql(200); + objectRemover.add(space, ruleResponse.body.id, 'rule', 'alerting'); + return ruleResponse.body.id; + } + + async function setup() { + // Create rules and connectors in multiple spaces + for (const space of Spaces) { + const noopConnectorId = await createConnector({ + name: 'noop connector', + space: space.id, + connectorTypeId: 'test.noop', + }); + await createConnector({ + name: 'connector that errors', + space: space.id, + connectorTypeId: 'test.throw', + }); + await createRule({ + space: space.id, + ruleOverwrites: { + rule_type_id: 'test.noop', + schedule: { interval: '30s' }, + throttle: '1s', + params: {}, + actions: [ + { + id: noopConnectorId, + group: 'default', + params: {}, + }, + ], + }, + }); + await createRule({ + space: space.id, + ruleOverwrites: { + rule_type_id: 'test.onlyContextVariables', + schedule: { interval: '10s' }, + throttle: '10m', + params: {}, + actions: [ + { + id: noopConnectorId, + group: 'default', + params: {}, + }, + ], + }, + }); + await createRule({ + space: space.id, + ruleOverwrites: { + rule_type_id: 'test.throw', + schedule: { interval: '1m' }, + throttle: '30s', + params: {}, + actions: [ + { + id: noopConnectorId, + group: 'default', + params: {}, + }, + ], + }, + }); + + alwaysFiringRuleId[space.id] = await createRule({ + space: space.id, + ruleOverwrites: { + rule_type_id: 'example.always-firing', + schedule: { interval: '3s' }, + throttle: null, + params: {}, + actions: [ + { + id: noopConnectorId, + group: 'small', + params: {}, + }, + { + id: noopConnectorId, + group: 'medium', + params: {}, + }, + { + id: noopConnectorId, + group: 'large', + params: {}, + }, + ], + }, + }); + + await createRule({ + space: space.id, + ruleOverwrites: { + rule_type_id: 'test.noop', + schedule: { interval: '5m' }, + throttle: null, + enabled: false, + params: {}, + actions: [ + { + id: noopConnectorId, + group: 'default', + params: {}, + }, + ], + }, + }); + } + } + + it('should retrieve telemetry data in the expected format', async () => { + await setup(); + + // let it run for a bit + await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: Spaces[0].id, + type: 'alert', + id: alwaysFiringRuleId[Spaces[0].id], + provider: 'alerting', + actions: new Map([['execute', { gte: 5 }]]), + }); + }); + + // request telemetry task to run + await supertest + .post('/api/alerting_actions_telemetry/run_now') + .set('kbn-xsrf', 'xxx') + .send({ taskId: 'Alerting-alerting_telemetry' }) + .expect(200); + + // get telemetry task doc + const telemetryTask = await es.get({ + id: `task:Alerting-alerting_telemetry`, + index: '.kibana_task_manager', + }); + const taskState = telemetryTask?._source?.task?.state; + expect(taskState).not.to.be(undefined); + const telemetry = JSON.parse(taskState!); + + // total number of rules + expect(telemetry.count_total).to.equal(15); + + // total number of enabled rules + expect(telemetry.count_active_total).to.equal(12); + + // total number of disabled rules + expect(telemetry.count_disabled_total).to.equal(3); + + // total number of rules broken down by rule type + expect(telemetry.count_by_type.test__onlyContextVariables).to.equal(3); + expect(telemetry.count_by_type['example__always-firing']).to.equal(3); + expect(telemetry.count_by_type.test__throw).to.equal(3); + expect(telemetry.count_by_type.test__noop).to.equal(6); + + // total number of enabled rules broken down by rule type + expect(telemetry.count_active_by_type.test__onlyContextVariables).to.equal(3); + expect(telemetry.count_active_by_type['example__always-firing']).to.equal(3); + expect(telemetry.count_active_by_type.test__throw).to.equal(3); + expect(telemetry.count_active_by_type.test__noop).to.equal(3); + + // throttle time stats + expect(telemetry.throttle_time.min).to.equal('0s'); + expect(telemetry.throttle_time.avg).to.equal('157.75s'); + expect(telemetry.throttle_time.max).to.equal('600s'); + expect(telemetry.throttle_time_number_s.min).to.equal(0); + expect(telemetry.throttle_time_number_s.avg).to.equal(157.75); + expect(telemetry.throttle_time_number_s.max).to.equal(600); + + // schedule interval stats + expect(telemetry.schedule_time.min).to.equal('3s'); + expect(telemetry.schedule_time.avg).to.equal('80.6s'); + expect(telemetry.schedule_time.max).to.equal('300s'); + expect(telemetry.schedule_time_number_s.min).to.equal(3); + expect(telemetry.schedule_time_number_s.avg).to.equal(80.6); + expect(telemetry.schedule_time_number_s.max).to.equal(300); + + // attached connectors stats + expect(telemetry.connectors_per_alert.min).to.equal(1); + expect(telemetry.connectors_per_alert.avg).to.equal(1.4); + expect(telemetry.connectors_per_alert.max).to.equal(3); + + // number of spaces with rules + expect(telemetry.count_rules_namespaces).to.equal(3); + + // number of rule executions - just checking for non-zero as we can't set an exact number + // each rule should have had a chance to execute once + expect(telemetry.count_rules_executions_per_day >= 15).to.be(true); + + // number of rule executions broken down by rule type + expect(telemetry.count_by_type.test__onlyContextVariables >= 3).to.be(true); + expect(telemetry.count_by_type['example__always-firing'] >= 3).to.be(true); + expect(telemetry.count_by_type.test__throw >= 3).to.be(true); + expect(telemetry.count_by_type.test__noop >= 3).to.be(true); + + // average execution time - just checking for non-zero as we can't set an exact number + expect(telemetry.avg_execution_time_per_day > 0).to.be(true); + + // average execution time broken down by rule type + expect(telemetry.avg_execution_time_by_type_per_day.test__onlyContextVariables > 0).to.be( + true + ); + expect(telemetry.avg_execution_time_by_type_per_day['example__always-firing'] > 0).to.be( + true + ); + expect(telemetry.avg_execution_time_by_type_per_day.test__throw > 0).to.be(true); + expect(telemetry.avg_execution_time_by_type_per_day.test__noop > 0).to.be(true); + + // number of failed executions - we have one rule that always fails + expect(telemetry.count_rules_executions_failured_per_day >= 1).to.be(true); + expect(telemetry.count_rules_executions_failured_by_reason_per_day.execute >= 1).to.be(true); + expect( + telemetry.count_rules_executions_failured_by_reason_by_type_per_day.execute.test__throw >= 1 + ).to.be(true); + + // number of execution timeouts - testing for existence of this field but + // this test doesn't have any rules that timeout + expect(telemetry.count_rules_executions_timeouts_per_day).to.equal(0); + expect(telemetry.count_rules_executions_timeouts_by_type_per_day).to.be.empty(); + + // number of failed/unrecognized tasks - testing for existence of this field but + // this test doesn't have any unrecognized rule types + expect(telemetry.count_failed_and_unrecognized_rule_tasks_per_day).to.equal(0); + expect(telemetry.count_failed_and_unrecognized_rule_tasks_by_status_per_day).to.be.empty(); + expect( + telemetry.count_failed_and_unrecognized_rule_tasks_by_status_by_type_per_day + ).to.be.empty(); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/index.ts new file mode 100644 index 00000000000000..9e73fafc9f7bd5 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { setupSpacesAndUsers, tearDown } from '..'; + +// eslint-disable-next-line import/no-default-export +export default function actionsTests({ loadTestFile, getService }: FtrProviderContext) { + describe('Alerting and Actions Telemetry', () => { + before(async () => { + await setupSpacesAndUsers(getService); + }); + + after(async () => { + await tearDown(getService); + }); + + // run telemetry tests before anything else + loadTestFile(require.resolve('./actions_telemetry')); + loadTestFile(require.resolve('./alerting_telemetry')); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/preconfigured_alert_history_connector.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/preconfigured_alert_history_connector.ts index dea873073f61f8..274e147898d9bc 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/preconfigured_alert_history_connector.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/preconfigured_alert_history_connector.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { getTestAlertData, ObjectRemover } from '../../../../common/lib'; +import { getTestRuleData, ObjectRemover } from '../../../../common/lib'; import { AlertHistoryDefaultIndexName } from '../../../../../../plugins/actions/common'; const ALERT_HISTORY_OVERRIDE_INDEX = 'kibana-alert-history-not-the-default'; @@ -27,7 +27,7 @@ export default function preconfiguredAlertHistoryConnectorTests({ const alertId = 'instance'; function getTestData(params = {}) { - return getTestAlertData({ + return getTestRuleData({ rule_type_id: ruleTypeId, schedule: { interval: '1s' }, params: { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts index cf7ebffef85a28..961a15dd5223de 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -239,7 +239,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData(testAlertOverrides)) + .send(getTestRuleData(testAlertOverrides)) .expect(200); await waitForStatus(createdAlert.id, new Set([status])); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts index ea818a6e64b0dc..58ddd5516d8a54 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts @@ -16,7 +16,7 @@ import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, AlertUtils, ensureDatetimeIsWithinRange, @@ -198,7 +198,7 @@ instanceStateValue: true .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', schedule: { interval: '1s' }, throttle: null, @@ -259,7 +259,7 @@ instanceStateValue: true .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', schedule: { interval: '1s' }, enabled: false, @@ -354,7 +354,7 @@ instanceStateValue: true .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ schedule: { interval: '1m' }, rule_type_id: 'test.always-firing', params: { @@ -427,7 +427,7 @@ instanceStateValue: true .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.authorization', params: { callClusterAuthorizationIndex: authorizationIndex, @@ -473,7 +473,7 @@ instanceStateValue: true .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.always-firing', params: { index: ES_TEST_INDEX_NAME, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index b002e0668dc527..1d5eb16ff3f899 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -11,7 +11,7 @@ import { Spaces } from '../../scenarios'; import { checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, TaskManagerDoc, @@ -53,7 +53,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ actions: [ { id: createdAction.id, @@ -132,7 +132,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ actions: [ { id: createdAction.id, @@ -244,7 +244,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ params: { ignoredButPersisted: lotsOfSpaces, }, @@ -288,7 +288,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ params: { risk_score: 40, severity: 'medium', @@ -325,7 +325,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { const response = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${customId}`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()); + .send(getTestRuleData()); expect(response.status).to.eql(200); objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting'); @@ -344,7 +344,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { const response = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${customId}`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()); + .send(getTestRuleData()); expect(response.status).to.eql(200); objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting'); @@ -363,7 +363,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { const response = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${customId}`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()); + .send(getTestRuleData()); expect(response.status).to.eql(400); expect(response.body).to.eql({ @@ -379,13 +379,13 @@ export default function createAlertTests({ getService }: FtrProviderContext) { const createdAlertResponse = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${customId}`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlertResponse.body.id, 'rule', 'alerting'); await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${customId}`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(409); }); @@ -393,7 +393,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { const response = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ consumer: 'some consumer patrick invented' })); + .send(getTestRuleData({ consumer: 'some consumer patrick invented' })); expect(response.status).to.eql(403); expect(response.body).to.eql({ @@ -411,7 +411,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { const response = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })); + .send(getTestRuleData({ enabled: false })); expect(response.status).to.eql(200); objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting'); @@ -435,7 +435,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { rule_type_id: alertTypeId, notify_when: notifyWhen, ...testAlert - } = getTestAlertData({ + } = getTestRuleData({ actions: [ { id: createdAction.id, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts index 0a2df70b6316ac..073d76dc859a57 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -31,7 +31,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); await supertest @@ -51,7 +51,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); await supertest @@ -69,7 +69,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); await supertest diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts index 51cb54aa5f9e54..2a1d27a4d3b399 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts @@ -12,7 +12,7 @@ import { AlertUtils as RuleUtils, checkAAD, getUrlPrefix, - getTestAlertData as getTestRuleData, + getTestRuleData, ObjectRemover, getEventLog, } from '../../../common/lib'; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts index 611c2498dd9d33..c0c56ed354a843 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, TaskManagerDoc, } from '../../../common/lib'; @@ -40,7 +40,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -72,7 +72,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.other.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.other.id, createdAlert.id, 'rule', 'alerting'); @@ -88,7 +88,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ephemeral.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ephemeral.ts index a3b8c75f79e621..ac095fd4c44190 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ephemeral.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ephemeral.ts @@ -11,7 +11,7 @@ import { Spaces } from '../../scenarios'; import { getUrlPrefix, ObjectRemover, - getTestAlertData, + getTestRuleData, getEventLog, ESTestIndexTool, ES_TEST_INDEX_NAME, @@ -69,7 +69,7 @@ export default function createNotifyWhenTests({ getService }: FtrProviderContext const pattern = { instance: [true, true, true, false, true, true], }; - const alertData = getTestAlertData({ + const alertData = getTestRuleData({ rule_type_id: 'test.patternFiring', params: { pattern }, schedule: { interval: '1m' }, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index c36f9a0da75bcd..2cc2044653fd9c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -10,7 +10,7 @@ import uuid from 'uuid'; import { Spaces } from '../../scenarios'; import { getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getEventLog, ESTestIndexTool, @@ -62,7 +62,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', schedule: { interval: '1s' }, throttle: null, @@ -300,7 +300,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.multipleSearches', schedule: { interval: '1s' }, throttle: null, @@ -405,7 +405,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', schedule: { interval: '1s' }, throttle: null, @@ -599,7 +599,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.throw', schedule: { interval: '1s' }, throttle: null, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts index 6fa3eb1a43b620..6d7f95df10d9b2 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover, getEventLog } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { IValidatedEvent } from '../../../../../plugins/event_log/server'; @@ -31,7 +31,7 @@ export default function eventLogAlertTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', schedule: { interval: '1s' }, throttle: null, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts index 0f7ed80cfd38df..d5bcd0c7a9ae2b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts @@ -10,7 +10,7 @@ import { Spaces } from '../../scenarios'; import { checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, ensureDatetimesAreOrdered, } from '../../../common/lib'; @@ -30,7 +30,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon const response = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()); + .send(getTestRuleData()); const dateEnd = Date.now(); expect(response.status).to.eql(200); objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting'); @@ -61,7 +61,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.noop', schedule: { interval: '1s' }, }) @@ -94,7 +94,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', schedule: { interval: '1s' }, params: { @@ -130,7 +130,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.throw', schedule: { interval: '1s' }, }) @@ -166,7 +166,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.throw', schedule: { interval: '1s' }, }) @@ -188,7 +188,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.validation', schedule: { interval: '1s' }, params: { param1: 'valid now, but will change to a number soon!' }, @@ -227,7 +227,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.throw', schedule: { interval: '1s' }, }) diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts index 7a4a91bd575bb1..5ab632c6a66b86 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { SuperTest, Test } from 'supertest'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; async function createAlert( @@ -19,7 +19,7 @@ async function createAlert( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData(overwrites)) + .send(getTestRuleData(overwrites)) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); return createdAlert; @@ -36,7 +36,7 @@ const findTestUtils = ( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -82,7 +82,7 @@ const findTestUtils = ( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -266,7 +266,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts index 9a4be8951f8f00..81f67c8d49e332 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { SuperTest, Test } from 'supertest'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; const getTestUtils = ( @@ -22,7 +22,7 @@ const getTestUtils = ( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -64,7 +64,7 @@ const getTestUtils = ( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -113,7 +113,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts index 318dfdfe065dfe..61d38b522bb59d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, ObjectRemover, getTestAlertData } from '../../../common/lib'; +import { getUrlPrefix, ObjectRemover, getTestRuleData } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -24,7 +24,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_summary.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_summary.ts index 8cd9a0bbb12900..d13da4694bbe27 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_summary.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_summary.ts @@ -12,7 +12,7 @@ import { Spaces } from '../../scenarios'; import { getUrlPrefix, ObjectRemover, - getTestAlertData, + getTestRuleData, AlertUtils, getEventLog, } from '../../../common/lib'; @@ -44,7 +44,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); @@ -83,7 +83,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); @@ -122,7 +122,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); @@ -142,7 +142,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); @@ -165,7 +165,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); @@ -189,7 +189,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ rule_type_id: 'test.throw' })) + .send(getTestRuleData({ rule_type_id: 'test.throw' })) .expect(200); objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); @@ -216,7 +216,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', params: { pattern }, schedule: { interval: '1s' }, @@ -271,7 +271,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', params: { pattern }, schedule: { interval: '1s' }, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/monitoring.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/monitoring.ts index 9b91d395d16c6f..c08a28b3c3ca30 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/monitoring.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/monitoring.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -23,7 +23,7 @@ export default function monitoringAlertTests({ getService }: FtrProviderContext) const createResponse = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ schedule: { interval: '3s' } })); + .send(getTestRuleData({ schedule: { interval: '3s' } })); expect(createResponse.status).to.eql(200); objectRemover.add(Spaces.space1.id, createResponse.body.id, 'rule', 'alerting'); @@ -44,7 +44,7 @@ export default function monitoringAlertTests({ getService }: FtrProviderContext) const createResponse = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ schedule: { interval: '3s' } })); + .send(getTestRuleData({ schedule: { interval: '3s' } })); expect(createResponse.status).to.eql(200); objectRemover.add(Spaces.space1.id, createResponse.body.id, 'rule', 'alerting'); @@ -69,7 +69,7 @@ export default function monitoringAlertTests({ getService }: FtrProviderContext) .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternSuccessOrFailure', schedule: { interval: '3s' }, params: { @@ -102,7 +102,7 @@ export default function monitoringAlertTests({ getService }: FtrProviderContext) .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ schedule: { interval: '3s' }, }) ); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mustache_templates.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mustache_templates.ts index 8d300733bafc37..ff596db062b75d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mustache_templates.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mustache_templates.ts @@ -19,7 +19,7 @@ import axios from 'axios'; import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { getWebhookServer, @@ -88,7 +88,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ name: 'testing variable escapes for webhook', rule_type_id: 'test.patternFiring', params: { @@ -139,7 +139,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ name: 'testing variable escapes for slack', rule_type_id: 'test.patternFiring', params: { @@ -189,7 +189,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ name: 'testing context variable expansion', rule_type_id: 'test.patternFiring', params: { @@ -239,7 +239,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ name: 'testing context variable kibanaBaseUrl', rule_type_id: 'test.patternFiring', params: { @@ -290,7 +290,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ name: 'testing variable escapes for webhook', rule_type_id: 'test.patternFiring', params: { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts index c21a13edbf2cb3..27475049ac9a6f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, } from '../../../common/lib'; @@ -30,7 +30,7 @@ export default function createMuteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -56,7 +56,7 @@ export default function createMuteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts index afe29280748a5a..d32b74fd394475 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, } from '../../../common/lib'; @@ -30,7 +30,7 @@ export default function createMuteInstanceTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -56,7 +56,7 @@ export default function createMuteInstanceTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/notify_when.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/notify_when.ts index 7f1b82614a1000..5049f7c863a06a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/notify_when.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/notify_when.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, ObjectRemover, getTestAlertData, getEventLog } from '../../../common/lib'; +import { getUrlPrefix, ObjectRemover, getTestRuleData, getEventLog } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { IValidatedEvent } from '../../../../../plugins/event_log/server'; @@ -55,7 +55,7 @@ export default function createNotifyWhenTests({ getService }: FtrProviderContext .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', params: { pattern }, schedule: { interval: '1s' }, @@ -131,7 +131,7 @@ export default function createNotifyWhenTests({ getService }: FtrProviderContext .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', params: { pattern }, schedule: { interval: '1s' }, @@ -222,7 +222,7 @@ export default function createNotifyWhenTests({ getService }: FtrProviderContext .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', params: { pattern }, schedule: { interval: '1s' }, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/scheduled_task_id.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/scheduled_task_id.ts index 9f087b73921325..a83cd4241d1446 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/scheduled_task_id.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/scheduled_task_id.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { getUrlPrefix, TaskManagerDoc, ObjectRemover, getTestAlertData } from '../../../common/lib'; +import { getUrlPrefix, TaskManagerDoc, ObjectRemover, getTestRuleData } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; const MIGRATED_RULE_ID = '74f3e6d7-b7bb-477d-ac28-92ee22728e6e'; @@ -47,7 +47,7 @@ export default function createScheduledTaskIdTests({ getService }: FtrProviderCo await supertest .post(`${getUrlPrefix(``)}/api/alerting/rule/${MIGRATED_TASK_ID}`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(409); }); @@ -94,7 +94,7 @@ export default function createScheduledTaskIdTests({ getService }: FtrProviderCo const response = await supertestWithoutAuth .post(`${getUrlPrefix(``)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()); + .send(getTestRuleData()); expect(response.status).to.eql(200); objectRemover.add('default', response.body.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts index 2fffa9189e0ad7..47f61250157a37 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, } from '../../../common/lib'; @@ -30,7 +30,7 @@ export default function createUnmuteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -57,7 +57,7 @@ export default function createUnmuteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts index e0c42136628d3d..086f40d9febae1 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, } from '../../../common/lib'; @@ -30,7 +30,7 @@ export default function createUnmuteInstanceTests({ getService }: FtrProviderCon const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -57,7 +57,7 @@ export default function createUnmuteInstanceTests({ getService }: FtrProviderCon const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts index d97ca18c52d4a8..c5a9c93d45e81a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { checkAAD, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { checkAAD, getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -23,7 +23,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -94,7 +94,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -124,7 +124,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts index 78ceadec44a9ae..9fe5c7e112c79d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, } from '../../../common/lib'; @@ -34,7 +34,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -59,7 +59,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.other.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.other.id, createdAlert.id, 'rule', 'alerting'); @@ -75,7 +75,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts index 21ff8f2cc64c16..9c9bcb318e7ec2 100644 --- a/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts @@ -22,5 +22,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./delete_spaces')); loadTestFile(require.resolve('./evaluate')); loadTestFile(require.resolve('./explain')); + loadTestFile(require.resolve('./jobs_exist_spaces')); + loadTestFile(require.resolve('./new_job_caps')); }); } diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/jobs_exist_spaces.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/jobs_exist_spaces.ts new file mode 100644 index 00000000000000..4934af379ae660 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/jobs_exist_spaces.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const spacesService = getService('spaces'); + const supertest = getService('supertestWithoutAuth'); + + const jobIdSpace1 = 'ihp_od_space1'; + const jobIdSpace2 = 'ihp_od_space2'; + const idSpace1 = 'space1'; + const idSpace2 = 'space2'; + + const initialModelMemoryLimit = '17mb'; + + async function runRequest( + space: string, + expectedStatusCode: number, + analyticsIds?: string[], + allSpaces?: boolean + ) { + const { body } = await supertest + .post(`/s/${space}/api/ml/data_frame/analytics/jobs_exist`) + .auth( + USER.ML_VIEWER_ALL_SPACES, + ml.securityCommon.getPasswordForUser(USER.ML_VIEWER_ALL_SPACES) + ) + .set(COMMON_REQUEST_HEADERS) + .send(allSpaces ? { analyticsIds, allSpaces } : { analyticsIds }) + .expect(expectedStatusCode); + + return body; + } + + describe('POST data_frame/analytics/jobs_exist with spaces', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ihp_outlier'); + await spacesService.create({ id: idSpace1, name: 'space_one', disabledFeatures: [] }); + await spacesService.create({ id: idSpace2, name: 'space_two', disabledFeatures: [] }); + + const jobConfigSpace1 = ml.commonConfig.getDFAIhpOutlierDetectionJobConfig(jobIdSpace1); + await ml.api.createDataFrameAnalyticsJob( + { ...jobConfigSpace1, model_memory_limit: initialModelMemoryLimit }, + idSpace1 + ); + + const jobConfigSpace2 = ml.commonConfig.getDFAIhpOutlierDetectionJobConfig(jobIdSpace2); + await ml.api.createDataFrameAnalyticsJob( + { ...jobConfigSpace2, model_memory_limit: initialModelMemoryLimit }, + idSpace2 + ); + + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await spacesService.delete(idSpace1); + await spacesService.delete(idSpace2); + await ml.api.cleanMlIndices(); + await ml.testResources.cleanMLSavedObjects(); + }); + + it('should find single job from same space', async () => { + const body = await runRequest(idSpace1, 200, [jobIdSpace1]); + expect(body).to.eql({ [jobIdSpace1]: { exists: true } }); + }); + + it('should not find single job from different space', async () => { + const body = await runRequest(idSpace2, 200, [jobIdSpace1]); + expect(body).to.eql({ [jobIdSpace1]: { exists: false } }); + }); + + it('should only find job from same space when called with a list of jobs', async () => { + const body = await runRequest(idSpace1, 200, [jobIdSpace1, jobIdSpace2]); + expect(body).to.eql({ + [jobIdSpace1]: { exists: true }, + [jobIdSpace2]: { exists: false }, + }); + }); + + it('should find single job from different space when run across all spaces', async () => { + const body = await runRequest(idSpace1, 200, [jobIdSpace2], true); + expect(body).to.eql({ [jobIdSpace2]: { exists: true } }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/new_job_caps.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/new_job_caps.ts new file mode 100644 index 00000000000000..72ac632a8b8dd2 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/new_job_caps.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const supertest = getService('supertestWithoutAuth'); + const testIndexPattern = 'ft_bank_marketing'; + + async function runRequest(indexPattern: string, expectedStatusCode: number, rollup?: boolean) { + let url = `/api/ml/data_frame/analytics/new_job_caps/${indexPattern}`; + if (rollup !== undefined) { + url += `?rollup=${rollup}`; + } + const { body } = await supertest + .get(url) + .auth( + USER.ML_VIEWER_ALL_SPACES, + ml.securityCommon.getPasswordForUser(USER.ML_VIEWER_ALL_SPACES) + ) + .set(COMMON_REQUEST_HEADERS) + .expect(expectedStatusCode); + + return body; + } + + describe('GET data_frame/analytics/new_job_caps', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/bm_classification'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should return job capabilities of fields for an index that exists', async () => { + const body = await runRequest(testIndexPattern, 200); + await ml.testExecution.logTestStep( + `response should contain object for ${testIndexPattern} index pattern` + ); + expect(body).to.have.keys(testIndexPattern); + const testIndexPatternCaps = body[testIndexPattern]; + + // The data frame analytics UI does not use the aggs prop, so just perform basic checks this prop + await ml.testExecution.logTestStep( + `should contain aggs and fields props for ${testIndexPattern} index pattern` + ); + expect(testIndexPatternCaps).to.have.keys('aggs', 'fields'); + const aggs = testIndexPatternCaps.aggs; + expect(aggs).to.have.length(35); + + // The data frames analytics UI uses this endpoint to extract the names and types of fields, + // so check this info is present for some example fields + const fields = testIndexPatternCaps.fields; + expect(fields).to.have.length(24); + + await ml.testExecution.logTestStep( + `fields should contain expected name and type attributes for ${testIndexPattern} index pattern` + ); + const balanceTextField = fields.find((obj: any) => obj.id === 'balance'); + expect(balanceTextField).to.have.keys('name', 'type'); + expect(balanceTextField.name).to.eql('balance'); + expect(balanceTextField.type).to.eql('text'); + + const balanceKeywordField = fields.find((obj: any) => obj.id === 'balance.keyword'); + expect(balanceKeywordField).to.have.keys('name', 'type'); + expect(balanceKeywordField.name).to.eql('balance.keyword'); + expect(balanceKeywordField.type).to.eql('keyword'); + }); + + it('should fail to return job capabilities of fields for an index that does not exist', async () => { + await runRequest(`${testIndexPattern}_invalid`, 404); + }); + + it('should return empty job capabilities of fields for a non-rollup index with rollup parameter set to true', async () => { + const body = await runRequest(testIndexPattern, 200, true); + await ml.testExecution.logTestStep( + `response should contain object for ${testIndexPattern} index pattern` + ); + expect(body).to.have.keys(testIndexPattern); + const testIndexPatternCaps = body[testIndexPattern]; + + await ml.testExecution.logTestStep( + `should contain empty aggs and fields props for ${testIndexPattern} index pattern` + ); + expect(testIndexPatternCaps).to.have.keys('aggs', 'fields'); + const aggs = testIndexPatternCaps.aggs; + expect(aggs).to.have.length(0); + const fields = testIndexPatternCaps.fields; + expect(fields).to.have.length(0); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry.ts b/x-pack/test/api_integration/apis/telemetry/telemetry.ts index 088678a74813b6..4b0137ab5f8427 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry.ts +++ b/x-pack/test/api_integration/apis/telemetry/telemetry.ts @@ -10,6 +10,7 @@ import moment from 'moment'; import type SuperTest from 'supertest'; import deepmerge from 'deepmerge'; import type { FtrProviderContext } from '../../ftr_provider_context'; +import type { SecurityService } from '../../../../../test/common/services/security/security'; import multiClusterFixture from './fixtures/multicluster.json'; import basicClusterFixture from './fixtures/basiccluster.json'; @@ -90,10 +91,31 @@ function updateMonitoringDates( ]); } +async function createUserWithRole( + security: SecurityService, + userName: string, + roleName: string, + role: unknown +) { + await security.role.create(roleName, role); + + await security.user.create(userName, { + password: password(userName), + roles: [roleName], + full_name: `User ${userName}`, + }); +} + +function password(userName: string) { + return `${userName}-password`; +} + export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); // We need this because `.auth` in the already authed one does not work as expected const esArchiver = getService('esArchiver'); const esSupertest = getService('esSupertest'); + const security = getService('security'); describe('/api/telemetry/v2/clusters/_stats', () => { const timestamp = new Date().toISOString(); @@ -236,5 +258,114 @@ export default function ({ getService }: FtrProviderContext) { expect(new Date(fetchedAt).getTime()).to.be.greaterThan(now); }); }); + + describe('Only global read+ users can fetch unencrypted telemetry', () => { + describe('superadmin user', () => { + it('should return unencrypted telemetry for the admin user', async () => { + await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: true }) + .expect(200); + }); + + it('should return encrypted telemetry for the admin user', async () => { + await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: false }) + .expect(200); + }); + }); + + describe('global-read user', () => { + const globalReadOnlyUser = 'telemetry-global-read-only-user'; + const globalReadOnlyRole = 'telemetry-global-read-only-role'; + + before('create user', async () => { + await createUserWithRole(security, globalReadOnlyUser, globalReadOnlyRole, { + kibana: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + ], + }); + }); + + after(async () => { + await security.user.delete(globalReadOnlyUser); + await security.role.delete(globalReadOnlyRole); + }); + + it('should return encrypted telemetry for the global-read user', async () => { + await supertestWithoutAuth + .post('/api/telemetry/v2/clusters/_stats') + .auth(globalReadOnlyUser, password(globalReadOnlyUser)) + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: false }) + .expect(200); + }); + + it('should return unencrypted telemetry for the global-read user', async () => { + await supertestWithoutAuth + .post('/api/telemetry/v2/clusters/_stats') + .auth(globalReadOnlyUser, password(globalReadOnlyUser)) + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: true }) + .expect(200); + }); + }); + + describe('non global-read user', () => { + const noGlobalUser = 'telemetry-no-global-user'; + const noGlobalRole = 'telemetry-no-global-role'; + + before('create user', async () => { + await createUserWithRole(security, noGlobalUser, noGlobalRole, { + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + // It has access to many features specified individually but not a global one + discover: ['all'], + dashboard: ['all'], + canvas: ['all'], + maps: ['all'], + ml: ['all'], + visualize: ['all'], + dev_tools: ['all'], + }, + }, + ], + }); + }); + + after(async () => { + await security.user.delete(noGlobalUser); + await security.role.delete(noGlobalRole); + }); + + it('should return encrypted telemetry for the read-only user', async () => { + await supertestWithoutAuth + .post('/api/telemetry/v2/clusters/_stats') + .auth(noGlobalUser, password(noGlobalUser)) + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: false }) + .expect(200); + }); + + it('should return 403 when the read-only user requests unencrypted telemetry', async () => { + await supertestWithoutAuth + .post('/api/telemetry/v2/clusters/_stats') + .auth(noGlobalUser, password(noGlobalUser)) + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: true }) + .expect(403); + }); + }); + }); }); } diff --git a/x-pack/test/functional/apps/lens/runtime_fields.ts b/x-pack/test/functional/apps/lens/runtime_fields.ts index 2ea0a05aece265..dec66fc20caef1 100644 --- a/x-pack/test/functional/apps/lens/runtime_fields.ts +++ b/x-pack/test/functional/apps/lens/runtime_fields.ts @@ -25,6 +25,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await fieldEditor.enableValue(); await fieldEditor.typeScript("emit('abc')"); await fieldEditor.save(); + await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.lens.searchField('runtime'); await PageObjects.lens.waitForField('runtimefield'); await PageObjects.lens.dragFieldToWorkspace('runtimefield'); diff --git a/x-pack/test/functional/apps/monitoring/cluster/overview.js b/x-pack/test/functional/apps/monitoring/cluster/overview.js index 25e52535a39b28..d4c646d48b92df 100644 --- a/x-pack/test/functional/apps/monitoring/cluster/overview.js +++ b/x-pack/test/functional/apps/monitoring/cluster/overview.js @@ -112,13 +112,9 @@ export default function ({ getService, getPageObjects }) { expect(await overview.getKbnConnections()).to.be('174'); expect(await overview.getKbnMemoryUsage()).to.be('15.33%\n219.6 MB / 1.4 GB'); }); - - it('does not show logstash panel', async () => { - expect(await overview.doesLsPanelExist()).to.be(false); - }); }); - describe('for Yellow cluster with Basic license and no Kibana and Logstash', () => { + describe('for Yellow cluster with Basic license and no other stack components', () => { const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); before(async () => { @@ -156,12 +152,8 @@ export default function ({ getService, getPageObjects }) { expect(await overview.getEsReplicaShards()).to.be('0'); }); - it('shows kibana panel', async () => { - expect(await overview.doesKbnPanelExist()).to.be(false); - }); - - it('does not show logstash panel', async () => { - expect(await overview.doesLsPanelExist()).to.be(false); + it('shows only elasticsearch panel', async () => { + expect(await overview.getPresentPanels()).to.eql(['Elasticsearch']); }); }); diff --git a/x-pack/test/functional/services/monitoring/cluster_overview.js b/x-pack/test/functional/services/monitoring/cluster_overview.js index e6c58ba4eac030..ff146c94de732f 100644 --- a/x-pack/test/functional/services/monitoring/cluster_overview.js +++ b/x-pack/test/functional/services/monitoring/cluster_overview.js @@ -10,11 +10,14 @@ import expect from '@kbn/expect'; export function MonitoringClusterOverviewProvider({ getService }) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const find = getService('find'); const SUBJ_CLUSTER_ALERTS = `clusterAlertsContainer`; const SUBJ_CLUSTER_NAME = `overviewTabsclusterName`; - const SUBJ_ES_PANEL = `clusterItemContainerElasticsearch`; + const SUBJ_CLUSTER_ITEM_CONTAINER_PREFIX = `clusterItemContainer`; + + const SUBJ_ES_PANEL = `${SUBJ_CLUSTER_ITEM_CONTAINER_PREFIX}Elasticsearch`; const SUBJ_ES_STATUS = `${SUBJ_ES_PANEL} > statusIcon`; const SUBJ_ES_VERSION = `${SUBJ_ES_PANEL} > esVersion`; const SUBJ_ES_UPTIME = `${SUBJ_ES_PANEL} > esUptime`; @@ -29,7 +32,7 @@ export function MonitoringClusterOverviewProvider({ getService }) { const SUBJ_ES_REPLICA_SHARDS = `${SUBJ_ES_PANEL} > esReplicaShards`; const SUBJ_ES_ML_JOBS = `${SUBJ_ES_PANEL} > esMlJobs`; - const SUBJ_KBN_PANEL = `clusterItemContainerKibana`; + const SUBJ_KBN_PANEL = `${SUBJ_CLUSTER_ITEM_CONTAINER_PREFIX}Kibana`; const SUBJ_KBN_STATUS = `${SUBJ_KBN_PANEL} > statusIcon`; const SUBJ_KBN_REQUESTS = `${SUBJ_KBN_PANEL} > kbnRequests`; const SUBJ_KBN_MAX_RESPONSE_TIME = `${SUBJ_KBN_PANEL} > kbnMaxResponseTime`; @@ -38,7 +41,7 @@ export function MonitoringClusterOverviewProvider({ getService }) { const SUBJ_KBN_OVERVIEW = `${SUBJ_KBN_PANEL} > kbnOverview`; const SUBJ_KBN_INSTANCES = `${SUBJ_KBN_PANEL} > kbnInstances`; - const SUBJ_LS_PANEL = `clusterItemContainerLogstash`; + const SUBJ_LS_PANEL = `${SUBJ_CLUSTER_ITEM_CONTAINER_PREFIX}Logstash`; const SUBJ_LS_EVENTS_RECEIVED = `${SUBJ_LS_PANEL} > lsEventsReceived`; const SUBJ_LS_EVENTS_EMITTED = `${SUBJ_LS_PANEL} > lsEventsEmitted`; const SUBJ_LS_NODES = `${SUBJ_LS_PANEL} > lsNodes`; @@ -47,14 +50,14 @@ export function MonitoringClusterOverviewProvider({ getService }) { const SUBJ_LS_PIPELINES = `${SUBJ_LS_PANEL} > lsPipelines`; const SUBJ_LS_OVERVIEW = `${SUBJ_LS_PANEL} > lsOverview`; - const SUBJ_BEATS_PANEL = `clusterItemContainerBeats`; + const SUBJ_BEATS_PANEL = `${SUBJ_CLUSTER_ITEM_CONTAINER_PREFIX}Beats`; const SUBJ_BEATS_OVERVIEW = `${SUBJ_BEATS_PANEL} > beatsOverview`; const SUBJ_BEATS_TOTAL_EVENTS = `${SUBJ_BEATS_PANEL} > beatsTotalEvents`; const SUBJ_BEATS_BYTES_SENT = `${SUBJ_BEATS_PANEL} > beatsBytesSent`; const SUBJ_BEATS_LISTING = `${SUBJ_BEATS_PANEL} > beatsListing`; const SUBJ_BEATS_TYPES_COUNTS = `${SUBJ_BEATS_PANEL} > beatTypeCount`; - const SUBJ_ENT_SEARCH_PANEL = `clusterItemContainerEnterprise Search`; + const SUBJ_ENT_SEARCH_PANEL = `${SUBJ_CLUSTER_ITEM_CONTAINER_PREFIX}Enterprise Search`; const SUBJ_ENT_SEARCH_TOTAL_NODES = `${SUBJ_ENT_SEARCH_PANEL} > entSearchTotalNodes`; const SUBJ_ENT_SEARCH_OVERVIEW = `${SUBJ_ENT_SEARCH_PANEL} > entSearchOverview`; const SUBJ_ENT_SEARCH_ENGINES = `${SUBJ_ENT_SEARCH_PANEL} > appSearchEngines`; @@ -89,6 +92,16 @@ export function MonitoringClusterOverviewProvider({ getService }) { return testSubjects.click('alerts-modal-button'); } + async getPresentPanels() { + const panelElements = await find.allByCssSelector( + `[data-test-subj^="${SUBJ_CLUSTER_ITEM_CONTAINER_PREFIX}"]` + ); + const panelTestSubjects = await Promise.all( + panelElements.map((e) => e.getAttribute('data-test-subj')) + ); + return panelTestSubjects.map((e) => e.replace(SUBJ_CLUSTER_ITEM_CONTAINER_PREFIX, '')); + } + getEsStatus() { return testSubjects.getVisibleText(SUBJ_ES_STATUS); } @@ -140,9 +153,6 @@ export function MonitoringClusterOverviewProvider({ getService }) { return testSubjects.getVisibleText(SUBJ_ES_ML_JOBS); } - doesKbnPanelExist() { - return testSubjects.exists(SUBJ_KBN_PANEL); - } getKbnStatus() { return testSubjects.getVisibleText(SUBJ_KBN_STATUS); } @@ -168,9 +178,6 @@ export function MonitoringClusterOverviewProvider({ getService }) { return testSubjects.click(SUBJ_KBN_INSTANCES); } - doesLsPanelExist() { - return testSubjects.exists(SUBJ_LS_PANEL); - } getLsEventsReceived() { return testSubjects.getVisibleText(SUBJ_LS_EVENTS_RECEIVED); } diff --git a/x-pack/test/functional/services/transform/edit_flyout.ts b/x-pack/test/functional/services/transform/edit_flyout.ts index fb1d77f7abc6c1..0198a1d3d8c366 100644 --- a/x-pack/test/functional/services/transform/edit_flyout.ts +++ b/x-pack/test/functional/services/transform/edit_flyout.ts @@ -45,28 +45,32 @@ export function TransformEditFlyoutProvider({ getService }: FtrProviderContext) await testSubjects.existOrFail(`transformEditRetentionPolicySwitch`, { timeout: 1000, }); - const isEnabled = await testSubjects.isEnabled(`transformEditRetentionPolicySwitch`); - expect(isEnabled).to.eql( - expectedValue, - `Expected 'transformEditRetentionPolicySwitch' input to be '${ - expectedValue ? 'enabled' : 'disabled' - }' (got '${isEnabled ? 'enabled' : 'disabled'}')` - ); + await retry.tryForTime(5000, async () => { + const isEnabled = await testSubjects.isEnabled(`transformEditRetentionPolicySwitch`); + expect(isEnabled).to.eql( + expectedValue, + `Expected 'transformEditRetentionPolicySwitch' input to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }); }, async assertTransformEditFlyoutRetentionPolicyFieldSelectEnabled(expectedValue: boolean) { await testSubjects.existOrFail(`transformEditFlyoutRetentionPolicyFieldSelect`, { timeout: 1000, }); - const isEnabled = await testSubjects.isEnabled( - `transformEditFlyoutRetentionPolicyFieldSelect` - ); - expect(isEnabled).to.eql( - expectedValue, - `Expected 'transformEditFlyoutRetentionPolicyFieldSelect' input to be '${ - expectedValue ? 'enabled' : 'disabled' - }' (got '${isEnabled ? 'enabled' : 'disabled'}')` - ); + await retry.tryForTime(5000, async () => { + const isEnabled = await testSubjects.isEnabled( + `transformEditFlyoutRetentionPolicyFieldSelect` + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected 'transformEditFlyoutRetentionPolicyFieldSelect' input to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }); }, async assertTransformEditFlyoutRetentionPolicyFieldSelectValue(expectedValue: string) { diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx index c034ba1612a2c5..2542e82c5e28d6 100644 --- a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx @@ -65,13 +65,7 @@ const AppRoot = React.memo( {(timelinesPluginSetup && timelinesPluginSetup.getTGrid && timelinesPluginSetup.getTGrid<'standalone'>({ - appId: 'securitySolution', - casesOwner: 'securitySolutionUI', type: 'standalone', - casePermissions: { - read: true, - crud: true, - }, columns: [], indexNames: [], deletedEventIds: [], diff --git a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json index cde819a836b0a3..3e468d7a84ca26 100644 --- a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json @@ -13,7 +13,7 @@ "rule_risk": 42 } ] - }, + }, "host":{ "name":"siem-kibana" }, @@ -23,6 +23,131 @@ } } +{ + "type":"doc", + "value":{ + "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb71f", + "index":"ml_host_risk_score_latest_default", + "source":{ + "@timestamp":"2021-03-10T14:51:05.766Z", + "risk_stats": { + "risk_score": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + }, + "host":{ + "name":"fake-1" + }, + "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", + "risk":"Moderate" + } + } +} + +{ + "type":"doc", + "value":{ + "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb72f", + "index":"ml_host_risk_score_latest_default", + "source":{ + "@timestamp":"2021-03-10T14:51:05.766Z", + "risk_stats": { + "risk_score": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + }, + "host":{ + "name":"fake-2" + }, + "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", + "risk":"Moderate" + } + } +} + +{ + "type":"doc", + "value":{ + "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb73f", + "index":"ml_host_risk_score_latest_default", + "source":{ + "@timestamp":"2021-03-10T14:51:05.766Z", + "risk_stats": { + "risk_score": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + }, + "host":{ + "name":"fake-3" + }, + "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", + "risk":"Moderate" + } + } +} + +{ + "type":"doc", + "value":{ + "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f", + "index":"ml_host_risk_score_latest_default", + "source":{ + "@timestamp":"2021-03-10T14:51:05.766Z", + "risk_stats": { + "risk_score": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + }, + "host":{ + "name":"fake-4" + }, + "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", + "risk":"Moderate" + } + } +} + +{ + "type":"doc", + "value":{ + "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb75f", + "index":"ml_host_risk_score_latest_default", + "source":{ + "@timestamp":"2021-03-10T14:51:05.766Z", + "risk_stats": { + "risk_score": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + }, + "host":{ + "name":"fake-5" + }, + "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", + "risk":"Moderate" + } + } +} + { "type":"doc", "value":{ @@ -38,7 +163,7 @@ "rule_risk": 42 } ] - }, + }, "host":{ "name":"siem-kibana" },