diff --git a/package.json b/package.json index 9d30aee0a7515..8372b59033445 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "@elastic/apm-generator": "link:bazel-bin/packages/elastic-apm-generator", "@elastic/apm-rum": "^5.9.1", "@elastic/apm-rum-react": "^1.3.1", - "@elastic/charts": "37.0.0", + "@elastic/charts": "38.0.1", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^7.16.0-canary.4", "@elastic/ems-client": "7.16.0", @@ -750,7 +750,7 @@ "jsondiffpatch": "0.4.1", "license-checker": "^16.0.0", "listr": "^0.14.1", - "lmdb-store": "^1.6.8", + "lmdb-store": "^1.6.11", "marge": "^1.0.1", "micromatch": "3.1.10", "minimist": "^1.2.5", @@ -772,6 +772,7 @@ "ora": "^4.0.4", "parse-link-header": "^1.0.1", "pbf": "3.2.1", + "pdf-to-img": "^1.1.1", "pirates": "^4.0.1", "pixelmatch": "^5.1.0", "postcss": "^7.0.32", diff --git a/packages/elastic-apm-generator/BUILD.bazel b/packages/elastic-apm-generator/BUILD.bazel index 6b46b2b9181e5..396c27b3a4c89 100644 --- a/packages/elastic-apm-generator/BUILD.bazel +++ b/packages/elastic-apm-generator/BUILD.bazel @@ -25,6 +25,7 @@ NPM_MODULE_EXTRA_FILES = [ ] RUNTIME_DEPS = [ + "//packages/elastic-datemath", "@npm//@elastic/elasticsearch", "@npm//lodash", "@npm//moment", @@ -36,6 +37,7 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ + "//packages/elastic-datemath", "@npm//@elastic/elasticsearch", "@npm//moment", "@npm//p-limit", diff --git a/packages/elastic-apm-generator/README.md b/packages/elastic-apm-generator/README.md index e43187a8155d3..b442c0ec23ee0 100644 --- a/packages/elastic-apm-generator/README.md +++ b/packages/elastic-apm-generator/README.md @@ -11,7 +11,7 @@ This section assumes that you've installed Kibana's dependencies by running `yar This library can currently be used in two ways: - Imported as a Node.js module, for instance to be used in Kibana's functional test suite. -- With a command line interface, to index data based on some example scenarios. +- With a command line interface, to index data based on a specified scenario. ### Using the Node.js module @@ -32,7 +32,7 @@ const instance = service('synth-go', 'production', 'go') .instance('instance-a'); const from = new Date('2021-01-01T12:00:00.000Z').getTime(); -const to = new Date('2021-01-01T12:00:00.000Z').getTime() - 1; +const to = new Date('2021-01-01T12:00:00.000Z').getTime(); const traceEvents = timerange(from, to) .interval('1m') @@ -82,12 +82,26 @@ const esEvents = toElasticsearchOutput([ ### CLI -Via the CLI, you can upload examples. The supported examples are listed in `src/lib/es.ts`. A `--target` option that specifies the Elasticsearch URL should be defined when running the `example` command. Here's an example: +Via the CLI, you can upload scenarios, either using a fixed time range or continuously generating data. Some examples are available in in `src/scripts/examples`. Here's an example for live data: -`$ node packages/elastic-apm-generator/src/scripts/es.js example simple-trace --target=http://admin:changeme@localhost:9200` +`$ node packages/elastic-apm-generator/src/scripts/run packages/elastic-apm-generator/src/examples/01_simple_trace.ts --target=http://admin:changeme@localhost:9200 --live` + +For a fixed time window: +`$ node packages/elastic-apm-generator/src/scripts/run packages/elastic-apm-generator/src/examples/01_simple_trace.ts --target=http://admin:changeme@localhost:9200 --from=now-24h --to=now` + +The script will try to automatically find bootstrapped APM indices. __If these indices do not exist, the script will exit with an error. It will not bootstrap the indices itself.__ The following options are supported: -- `to`: the end of the time range, in ISO format. By default, the current time will be used. -- `from`: the start of the time range, in ISO format. By default, `to` minus 15 minutes will be used. -- `apm-server-version`: the version used in the index names bootstrapped by APM Server, e.g. `7.16.0`. __If these indices do not exist, the script will exit with an error. It will not bootstrap the indices itself.__ +| Option | Description | Default | +| -------------- | ------------------------------------------------------- | ------------ | +| `--from` | The start of the time window. | `now - 15m` | +| `--to` | The end of the time window. | `now` | +| `--live` | Continously ingest data | `false` | +| `--bucketSize` | Size of bucket for which to generate data. | `15m` | +| `--clean` | Clean APM indices before indexing new data. | `false` | +| `--interval` | The interval at which to index data. | `10s` | +| `--logLevel` | Log level. | `info` | +| `--lookback` | The lookback window for which data should be generated. | `15m` | +| `--target` | Elasticsearch target, including username/password. | **Required** | +| `--workers` | Amount of simultaneously connected ES clients. | `1` | diff --git a/packages/elastic-apm-generator/src/.eslintrc.js b/packages/elastic-apm-generator/src/.eslintrc.js new file mode 100644 index 0000000000000..2e3eef95f4bf3 --- /dev/null +++ b/packages/elastic-apm-generator/src/.eslintrc.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +module.exports = { + rules: { + 'import/no-default-export': 'off', + }, +}; diff --git a/packages/elastic-apm-generator/src/lib/interval.ts b/packages/elastic-apm-generator/src/lib/interval.ts index f13d54fd7415e..bafd1a06c5348 100644 --- a/packages/elastic-apm-generator/src/lib/interval.ts +++ b/packages/elastic-apm-generator/src/lib/interval.ts @@ -21,7 +21,7 @@ export class Interval { throw new Error('Failed to parse interval'); } const timestamps: number[] = []; - while (now <= this.to) { + while (now < this.to) { timestamps.push(...new Array(rate).fill(now)); now = moment(now) .add(Number(args[1]), args[2] as any) diff --git a/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts b/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts index d90ce8e01f83d..31f3e8c8ed270 100644 --- a/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts +++ b/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts @@ -10,7 +10,25 @@ import { set } from 'lodash'; import { getObserverDefaults } from '../..'; import { Fields } from '../entity'; -export function toElasticsearchOutput(events: Fields[], versionOverride?: string) { +export interface ElasticsearchOutput { + _index: string; + _source: unknown; +} + +export interface ElasticsearchOutputWriteTargets { + transaction: string; + span: string; + error: string; + metric: string; +} + +export function toElasticsearchOutput({ + events, + writeTargets, +}: { + events: Fields[]; + writeTargets: ElasticsearchOutputWriteTargets; +}): ElasticsearchOutput[] { return events.map((event) => { const values = { ...event, @@ -29,7 +47,7 @@ export function toElasticsearchOutput(events: Fields[], versionOverride?: string set(document, key, val); } return { - _index: `apm-${versionOverride || values['observer.version']}-${values['processor.event']}`, + _index: writeTargets[event['processor.event'] as keyof ElasticsearchOutputWriteTargets], _source: document, }; }); diff --git a/packages/elastic-apm-generator/src/scripts/es.ts b/packages/elastic-apm-generator/src/scripts/es.ts deleted file mode 100644 index d023ef7172892..0000000000000 --- a/packages/elastic-apm-generator/src/scripts/es.ts +++ /dev/null @@ -1,113 +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 { inspect } from 'util'; -import { Client } from '@elastic/elasticsearch'; -import { chunk } from 'lodash'; -import pLimit from 'p-limit'; -import yargs from 'yargs/yargs'; -import { toElasticsearchOutput } from '..'; -import { simpleTrace } from './examples/01_simple_trace'; - -yargs(process.argv.slice(2)) - .command( - 'example', - 'run an example scenario', - (y) => { - return y - .positional('scenario', { - describe: 'scenario to run', - choices: ['simple-trace'], - demandOption: true, - }) - .option('target', { - describe: 'elasticsearch target, including username/password', - }) - .option('from', { describe: 'start of timerange' }) - .option('to', { describe: 'end of timerange' }) - .option('workers', { - default: 1, - describe: 'number of concurrently connected ES clients', - }) - .option('apm-server-version', { - describe: 'APM Server version override', - }) - .demandOption('target'); - }, - (argv) => { - let events: any[] = []; - const toDateString = (argv.to as string | undefined) || new Date().toISOString(); - const fromDateString = - (argv.from as string | undefined) || - new Date(new Date(toDateString).getTime() - 15 * 60 * 1000).toISOString(); - - const to = new Date(toDateString).getTime(); - const from = new Date(fromDateString).getTime(); - - switch (argv._[1]) { - case 'simple-trace': - events = simpleTrace(from, to); - break; - } - - const docs = toElasticsearchOutput(events, argv['apm-server-version'] as string); - - const client = new Client({ - node: argv.target as string, - }); - - const fn = pLimit(argv.workers); - - const batches = chunk(docs, 1000); - - // eslint-disable-next-line no-console - console.log( - 'Uploading', - docs.length, - 'docs in', - batches.length, - 'batches', - 'from', - fromDateString, - 'to', - toDateString - ); - - Promise.all( - batches.map((batch) => - fn(() => { - return client.bulk({ - require_alias: true, - body: batch.flatMap((doc) => { - return [{ index: { _index: doc._index } }, doc._source]; - }), - }); - }) - ) - ) - .then((results) => { - const errors = results - .flatMap((result) => result.body.items) - .filter((item) => !!item.index?.error) - .map((item) => item.index?.error); - - if (errors.length) { - // eslint-disable-next-line no-console - console.error(inspect(errors.slice(0, 10), { depth: null })); - throw new Error('Failed to upload some items'); - } - process.exit(); - }) - .catch((err) => { - // eslint-disable-next-line no-console - console.error(err); - process.exit(1); - }); - } - ) - .parse(); diff --git a/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts b/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts index f6aad154532c2..6b857391b4f96 100644 --- a/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts +++ b/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts @@ -9,12 +9,12 @@ import { service, timerange, getTransactionMetrics, getSpanDestinationMetrics } from '../..'; import { getBreakdownMetrics } from '../../lib/utils/get_breakdown_metrics'; -export function simpleTrace(from: number, to: number) { +export default function ({ from, to }: { from: number; to: number }) { const instance = service('opbeans-go', 'production', 'go').instance('instance'); const range = timerange(from, to); - const transactionName = '240rpm/60% 1000ms'; + const transactionName = '240rpm/75% 1000ms'; const successfulTraceEvents = range .interval('1s') diff --git a/packages/elastic-apm-generator/src/scripts/es.js b/packages/elastic-apm-generator/src/scripts/run.js similarity index 96% rename from packages/elastic-apm-generator/src/scripts/es.js rename to packages/elastic-apm-generator/src/scripts/run.js index 9f99a5d19b8f8..426b247b6b623 100644 --- a/packages/elastic-apm-generator/src/scripts/es.js +++ b/packages/elastic-apm-generator/src/scripts/run.js @@ -12,4 +12,4 @@ require('@babel/register')({ presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], }); -require('./es.ts'); +require('./run.ts'); diff --git a/packages/elastic-apm-generator/src/scripts/run.ts b/packages/elastic-apm-generator/src/scripts/run.ts new file mode 100644 index 0000000000000..ad453ac96ff10 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/run.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 datemath from '@elastic/datemath'; +import yargs from 'yargs/yargs'; +import { cleanWriteTargets } from './utils/clean_write_targets'; +import { + bucketSizeOption, + cleanOption, + fileOption, + intervalOption, + targetOption, + workerOption, + logLevelOption, +} from './utils/common_options'; +import { intervalToMs } from './utils/interval_to_ms'; +import { getCommonResources } from './utils/get_common_resources'; +import { startHistoricalDataUpload } from './utils/start_historical_data_upload'; +import { startLiveDataUpload } from './utils/start_live_data_upload'; + +yargs(process.argv.slice(2)) + .command( + '*', + 'Generate data and index into Elasticsearch', + (y) => { + return y + .positional('file', fileOption) + .option('bucketSize', bucketSizeOption) + .option('workers', workerOption) + .option('interval', intervalOption) + .option('clean', cleanOption) + .option('target', targetOption) + .option('logLevel', logLevelOption) + .option('from', { + description: 'The start of the time window', + }) + .option('to', { + description: 'The end of the time window', + }) + .option('live', { + description: 'Generate and index data continuously', + boolean: true, + }) + .conflicts('to', 'live'); + }, + async (argv) => { + const { + scenario, + intervalInMs, + bucketSizeInMs, + target, + workers, + clean, + logger, + writeTargets, + client, + } = await getCommonResources(argv); + + if (clean) { + await cleanWriteTargets({ writeTargets, client, logger }); + } + + const to = datemath.parse(String(argv.to ?? 'now'))!.valueOf(); + const from = argv.from + ? datemath.parse(String(argv.from))!.valueOf() + : to - intervalToMs('15m'); + + const live = argv.live; + + logger.info( + `Starting data generation\n: ${JSON.stringify( + { + intervalInMs, + bucketSizeInMs, + workers, + target, + writeTargets, + from: new Date(from).toISOString(), + to: new Date(to).toISOString(), + live, + }, + null, + 2 + )}` + ); + + startHistoricalDataUpload({ + from, + to, + scenario, + intervalInMs, + bucketSizeInMs, + client, + workers, + writeTargets, + logger, + }); + + if (live) { + startLiveDataUpload({ + bucketSizeInMs, + client, + intervalInMs, + logger, + scenario, + start: to, + workers, + writeTargets, + }); + } + } + ) + .parse(); diff --git a/packages/elastic-apm-generator/src/scripts/utils/clean_write_targets.ts b/packages/elastic-apm-generator/src/scripts/utils/clean_write_targets.ts new file mode 100644 index 0000000000000..efa24f164d51e --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/clean_write_targets.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. + */ + +import { Client } from '@elastic/elasticsearch'; +import { ElasticsearchOutputWriteTargets } from '../../lib/output/to_elasticsearch_output'; +import { Logger } from './logger'; + +export async function cleanWriteTargets({ + writeTargets, + client, + logger, +}: { + writeTargets: ElasticsearchOutputWriteTargets; + client: Client; + logger: Logger; +}) { + const targets = Object.values(writeTargets); + + logger.info(`Cleaning indices: ${targets.join(', ')}`); + + const response = await client.deleteByQuery({ + index: targets, + allow_no_indices: true, + conflicts: 'proceed', + body: { + query: { + match_all: {}, + }, + }, + wait_for_completion: false, + }); + + const task = response.body.task; + + if (task) { + await new Promise((resolve, reject) => { + const pollForTaskCompletion = async () => { + const taskResponse = await client.tasks.get({ + task_id: String(task), + }); + + logger.debug( + `Polled for task:\n${JSON.stringify(taskResponse.body, ['completed', 'error'], 2)}` + ); + + if (taskResponse.body.completed) { + resolve(); + } else if (taskResponse.body.error) { + reject(taskResponse.body.error); + } else { + setTimeout(pollForTaskCompletion, 2500); + } + }; + + pollForTaskCompletion(); + }); + } +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/common_options.ts b/packages/elastic-apm-generator/src/scripts/utils/common_options.ts new file mode 100644 index 0000000000000..eba547114d533 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/common_options.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 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. + */ + +const fileOption = { + describe: 'File that contains the trace scenario', + demandOption: true, +}; + +const intervalOption = { + describe: 'The interval at which to index data', + default: '10s', +}; + +const targetOption = { + describe: 'Elasticsearch target, including username/password', + demandOption: true, +}; + +const bucketSizeOption = { + describe: 'Size of bucket for which to generate data', + default: '15m', +}; + +const workerOption = { + describe: 'Amount of simultaneously connected ES clients', + default: 1, +}; + +const cleanOption = { + describe: 'Clean APM indices before indexing new data', + default: false, + boolean: true as const, +}; + +const logLevelOption = { + describe: 'Log level', + default: 'info', +}; + +export { + fileOption, + intervalOption, + targetOption, + bucketSizeOption, + workerOption, + cleanOption, + logLevelOption, +}; diff --git a/packages/elastic-apm-generator/src/scripts/utils/get_common_resources.ts b/packages/elastic-apm-generator/src/scripts/utils/get_common_resources.ts new file mode 100644 index 0000000000000..1288c1390e92c --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/get_common_resources.ts @@ -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 { Client } from '@elastic/elasticsearch'; +import { getScenario } from './get_scenario'; +import { getWriteTargets } from './get_write_targets'; +import { intervalToMs } from './interval_to_ms'; +import { createLogger, LogLevel } from './logger'; + +export async function getCommonResources({ + file, + interval, + bucketSize, + workers, + target, + clean, + logLevel, +}: { + file: unknown; + interval: unknown; + bucketSize: unknown; + workers: unknown; + target: unknown; + clean: boolean; + logLevel: unknown; +}) { + let parsedLogLevel = LogLevel.info; + switch (logLevel) { + case 'info': + parsedLogLevel = LogLevel.info; + break; + + case 'debug': + parsedLogLevel = LogLevel.debug; + break; + + case 'quiet': + parsedLogLevel = LogLevel.quiet; + break; + } + + const logger = createLogger(parsedLogLevel); + + const intervalInMs = intervalToMs(interval); + if (!intervalInMs) { + throw new Error('Invalid interval'); + } + + const bucketSizeInMs = intervalToMs(bucketSize); + + if (!bucketSizeInMs) { + throw new Error('Invalid bucket size'); + } + + const client = new Client({ + node: String(target), + }); + + const [scenario, writeTargets] = await Promise.all([ + getScenario({ file, logger }), + getWriteTargets({ client }), + ]); + + return { + scenario, + writeTargets, + logger, + client, + intervalInMs, + bucketSizeInMs, + workers: Number(workers), + target: String(target), + clean, + }; +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/get_scenario.ts b/packages/elastic-apm-generator/src/scripts/utils/get_scenario.ts new file mode 100644 index 0000000000000..887969e8459cc --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/get_scenario.ts @@ -0,0 +1,25 @@ +/* + * 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 { Fields } from '../../lib/entity'; +import { Logger } from './logger'; + +export type Scenario = (options: { from: number; to: number }) => Fields[]; + +export function getScenario({ file, logger }: { file: unknown; logger: Logger }) { + const location = Path.join(process.cwd(), String(file)); + + logger.debug(`Loading scenario from ${location}`); + + return import(location).then((m) => { + if (m && m.default) { + return m.default; + } + throw new Error(`Could not find scenario at ${location}`); + }) as Promise; +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/get_write_targets.ts b/packages/elastic-apm-generator/src/scripts/utils/get_write_targets.ts new file mode 100644 index 0000000000000..3640e4efaf796 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/get_write_targets.ts @@ -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 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 { ElasticsearchOutputWriteTargets } from '../../lib/output/to_elasticsearch_output'; + +export async function getWriteTargets({ + client, +}: { + client: Client; +}): Promise { + const [indicesResponse, datastreamsResponse] = await Promise.all([ + client.indices.getAlias({ + index: 'apm-*', + }), + client.indices.getDataStream({ + name: '*apm', + }), + ]); + + function getDataStreamName(filter: string) { + return datastreamsResponse.body.data_streams.find((stream) => stream.name.includes(filter)) + ?.name; + } + + function getAlias(filter: string) { + return Object.keys(indicesResponse.body) + .map((key) => { + return { + key, + writeIndexAlias: Object.entries(indicesResponse.body[key].aliases).find( + ([_, alias]) => alias.is_write_index + )?.[0], + }; + }) + .find(({ key }) => key.includes(filter))?.writeIndexAlias!; + } + + const targets = { + transaction: getDataStreamName('traces-apm') || getAlias('-transaction'), + span: getDataStreamName('traces-apm') || getAlias('-span'), + metric: getDataStreamName('metrics-apm') || getAlias('-metric'), + error: getDataStreamName('logs-apm') || getAlias('-error'), + }; + + if (!targets.transaction || !targets.span || !targets.metric || !targets.error) { + throw new Error('Write targets could not be determined'); + } + + return targets; +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/interval_to_ms.ts b/packages/elastic-apm-generator/src/scripts/utils/interval_to_ms.ts new file mode 100644 index 0000000000000..4cba832be3161 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/interval_to_ms.ts @@ -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 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 function intervalToMs(interval: unknown) { + const [, valueAsString, unit] = String(interval).split(/(.*)(s|m|h|d|w)/); + + const value = Number(valueAsString); + + switch (unit) { + case 's': + return value * 1000; + case 'm': + return value * 1000 * 60; + + case 'h': + return value * 1000 * 60 * 60; + + case 'd': + return value * 1000 * 60 * 60 * 24; + + case 'w': + return value * 1000 * 60 * 60 * 24 * 7; + } + + throw new Error('Could not parse interval'); +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/logger.ts b/packages/elastic-apm-generator/src/scripts/utils/logger.ts new file mode 100644 index 0000000000000..c9017cb08e663 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/logger.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. + */ + +export enum LogLevel { + debug = 0, + info = 1, + quiet = 2, +} + +export function createLogger(logLevel: LogLevel) { + return { + debug: (...args: any[]) => { + if (logLevel <= LogLevel.debug) { + // eslint-disable-next-line no-console + console.debug(...args); + } + }, + info: (...args: any[]) => { + if (logLevel <= LogLevel.info) { + // eslint-disable-next-line no-console + console.log(...args); + } + }, + }; +} + +export type Logger = ReturnType; diff --git a/packages/elastic-apm-generator/src/scripts/utils/start_historical_data_upload.ts b/packages/elastic-apm-generator/src/scripts/utils/start_historical_data_upload.ts new file mode 100644 index 0000000000000..db14090dd1d8f --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/start_historical_data_upload.ts @@ -0,0 +1,64 @@ +/* + * 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 { ElasticsearchOutputWriteTargets } from '../../lib/output/to_elasticsearch_output'; +import { Scenario } from './get_scenario'; +import { Logger } from './logger'; +import { uploadEvents } from './upload_events'; + +export async function startHistoricalDataUpload({ + from, + to, + scenario, + intervalInMs, + bucketSizeInMs, + client, + workers, + writeTargets, + logger, +}: { + from: number; + to: number; + scenario: Scenario; + intervalInMs: number; + bucketSizeInMs: number; + client: Client; + workers: number; + writeTargets: ElasticsearchOutputWriteTargets; + logger: Logger; +}) { + let requestedUntil: number = from; + function uploadNextBatch() { + const bucketFrom = requestedUntil; + const bucketTo = Math.min(to, bucketFrom + bucketSizeInMs); + + const events = scenario({ from: bucketFrom, to: bucketTo }); + + logger.info( + `Uploading: ${new Date(bucketFrom).toISOString()} to ${new Date(bucketTo).toISOString()}` + ); + + uploadEvents({ + events, + client, + workers, + writeTargets, + logger, + }).then(() => { + if (bucketTo >= to) { + return; + } + uploadNextBatch(); + }); + + requestedUntil = bucketTo; + } + + return uploadNextBatch(); +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/start_live_data_upload.ts b/packages/elastic-apm-generator/src/scripts/utils/start_live_data_upload.ts new file mode 100644 index 0000000000000..bf330732f343e --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/start_live_data_upload.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { partition } from 'lodash'; +import { Fields } from '../../lib/entity'; +import { ElasticsearchOutputWriteTargets } from '../../lib/output/to_elasticsearch_output'; +import { Scenario } from './get_scenario'; +import { Logger } from './logger'; +import { uploadEvents } from './upload_events'; + +export function startLiveDataUpload({ + start, + bucketSizeInMs, + intervalInMs, + workers, + writeTargets, + scenario, + client, + logger, +}: { + start: number; + bucketSizeInMs: number; + intervalInMs: number; + workers: number; + writeTargets: ElasticsearchOutputWriteTargets; + scenario: Scenario; + client: Client; + logger: Logger; +}) { + let queuedEvents: Fields[] = []; + let requestedUntil: number = start; + + function uploadNextBatch() { + const end = new Date().getTime(); + if (end > requestedUntil) { + const bucketFrom = requestedUntil; + const bucketTo = requestedUntil + bucketSizeInMs; + const nextEvents = scenario({ from: bucketFrom, to: bucketTo }); + logger.debug( + `Requesting ${new Date(bucketFrom).toISOString()} to ${new Date( + bucketTo + ).toISOString()}, events: ${nextEvents.length}` + ); + queuedEvents.push(...nextEvents); + requestedUntil = bucketTo; + } + + const [eventsToUpload, eventsToRemainInQueue] = partition( + queuedEvents, + (event) => event['@timestamp']! <= end + ); + + logger.info(`Uploading until ${new Date(end).toISOString()}, events: ${eventsToUpload.length}`); + + queuedEvents = eventsToRemainInQueue; + + uploadEvents({ + events: eventsToUpload, + client, + workers, + writeTargets, + logger, + }); + } + + setInterval(uploadNextBatch, intervalInMs); + + uploadNextBatch(); +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/upload_events.ts b/packages/elastic-apm-generator/src/scripts/utils/upload_events.ts new file mode 100644 index 0000000000000..89cf4d4602177 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/upload_events.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { chunk } from 'lodash'; +import pLimit from 'p-limit'; +import { inspect } from 'util'; +import { Fields } from '../../lib/entity'; +import { + ElasticsearchOutputWriteTargets, + toElasticsearchOutput, +} from '../../lib/output/to_elasticsearch_output'; +import { Logger } from './logger'; + +export function uploadEvents({ + events, + client, + workers, + writeTargets, + logger, +}: { + events: Fields[]; + client: Client; + workers: number; + writeTargets: ElasticsearchOutputWriteTargets; + logger: Logger; +}) { + const esDocuments = toElasticsearchOutput({ events, writeTargets }); + const fn = pLimit(workers); + + const batches = chunk(esDocuments, 5000); + + logger.debug(`Uploading ${esDocuments.length} in ${batches.length} batches`); + + const time = new Date().getTime(); + + return Promise.all( + batches.map((batch) => + fn(() => { + return client.bulk({ + require_alias: true, + body: batch.flatMap((doc) => { + return [{ index: { _index: doc._index } }, doc._source]; + }), + }); + }) + ) + ) + .then((results) => { + const errors = results + .flatMap((result) => result.body.items) + .filter((item) => !!item.index?.error) + .map((item) => item.index?.error); + + if (errors.length) { + // eslint-disable-next-line no-console + console.error(inspect(errors.slice(0, 10), { depth: null })); + throw new Error('Failed to upload some items'); + } + + logger.debug(`Uploaded ${events.length} in ${new Date().getTime() - time}ms`); + }) + .catch((err) => { + // eslint-disable-next-line no-console + console.error(err); + process.exit(1); + }); +} diff --git a/packages/elastic-apm-generator/src/test/scenarios/01_simple_trace.test.ts b/packages/elastic-apm-generator/src/test/scenarios/01_simple_trace.test.ts index 733093ce0a71c..866a9745befc3 100644 --- a/packages/elastic-apm-generator/src/test/scenarios/01_simple_trace.test.ts +++ b/packages/elastic-apm-generator/src/test/scenarios/01_simple_trace.test.ts @@ -18,7 +18,7 @@ describe('simple trace', () => { const range = timerange( new Date('2021-01-01T00:00:00.000Z').getTime(), - new Date('2021-01-01T00:15:00.000Z').getTime() - 1 + new Date('2021-01-01T00:15:00.000Z').getTime() ); events = range diff --git a/packages/elastic-apm-generator/src/test/scenarios/02_transaction_metrics.test.ts b/packages/elastic-apm-generator/src/test/scenarios/02_transaction_metrics.test.ts index 0b9f192d3d27d..58b28f71b9afc 100644 --- a/packages/elastic-apm-generator/src/test/scenarios/02_transaction_metrics.test.ts +++ b/packages/elastic-apm-generator/src/test/scenarios/02_transaction_metrics.test.ts @@ -19,7 +19,7 @@ describe('transaction metrics', () => { const range = timerange( new Date('2021-01-01T00:00:00.000Z').getTime(), - new Date('2021-01-01T00:15:00.000Z').getTime() - 1 + new Date('2021-01-01T00:15:00.000Z').getTime() ); events = getTransactionMetrics( diff --git a/packages/elastic-apm-generator/src/test/scenarios/03_span_destination_metrics.test.ts b/packages/elastic-apm-generator/src/test/scenarios/03_span_destination_metrics.test.ts index 158ccc5b5e714..0bf59f044bf03 100644 --- a/packages/elastic-apm-generator/src/test/scenarios/03_span_destination_metrics.test.ts +++ b/packages/elastic-apm-generator/src/test/scenarios/03_span_destination_metrics.test.ts @@ -19,7 +19,7 @@ describe('span destination metrics', () => { const range = timerange( new Date('2021-01-01T00:00:00.000Z').getTime(), - new Date('2021-01-01T00:15:00.000Z').getTime() - 1 + new Date('2021-01-01T00:15:00.000Z').getTime() ); events = getSpanDestinationMetrics( diff --git a/packages/elastic-apm-generator/src/test/scenarios/04_breakdown_metrics.test.ts b/packages/elastic-apm-generator/src/test/scenarios/04_breakdown_metrics.test.ts index aeb944f35faf6..469f56b99c5f2 100644 --- a/packages/elastic-apm-generator/src/test/scenarios/04_breakdown_metrics.test.ts +++ b/packages/elastic-apm-generator/src/test/scenarios/04_breakdown_metrics.test.ts @@ -26,7 +26,7 @@ describe('breakdown metrics', () => { const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const range = timerange(start, start + INTERVALS * 30 * 1000 - 1); + const range = timerange(start, start + INTERVALS * 30 * 1000); events = getBreakdownMetrics([ ...range diff --git a/packages/elastic-apm-generator/src/test/to_elasticsearch_output.test.ts b/packages/elastic-apm-generator/src/test/to_elasticsearch_output.test.ts index c1a5d47654fc9..d15ea89083112 100644 --- a/packages/elastic-apm-generator/src/test/to_elasticsearch_output.test.ts +++ b/packages/elastic-apm-generator/src/test/to_elasticsearch_output.test.ts @@ -9,6 +9,13 @@ import { Fields } from '../lib/entity'; import { toElasticsearchOutput } from '../lib/output/to_elasticsearch_output'; +const writeTargets = { + transaction: 'apm-8.0.0-transaction', + span: 'apm-8.0.0-span', + metric: 'apm-8.0.0-metric', + error: 'apm-8.0.0-error', +}; + describe('output to elasticsearch', () => { let event: Fields; @@ -21,13 +28,13 @@ describe('output to elasticsearch', () => { }); it('properly formats @timestamp', () => { - const doc = toElasticsearchOutput([event])[0] as any; + const doc = toElasticsearchOutput({ events: [event], writeTargets })[0] as any; expect(doc._source['@timestamp']).toEqual('2020-12-31T23:00:00.000Z'); }); it('formats a nested object', () => { - const doc = toElasticsearchOutput([event])[0] as any; + const doc = toElasticsearchOutput({ events: [event], writeTargets })[0] as any; expect(doc._source.processor).toEqual({ event: 'transaction', diff --git a/packages/kbn-dev-utils/src/vscode_config/managed_config_keys.ts b/packages/kbn-dev-utils/src/vscode_config/managed_config_keys.ts index f5bee0ce67fe4..32cc91ad74c50 100644 --- a/packages/kbn-dev-utils/src/vscode_config/managed_config_keys.ts +++ b/packages/kbn-dev-utils/src/vscode_config/managed_config_keys.ts @@ -20,21 +20,23 @@ export const MANAGED_CONFIG_KEYS: ManagedConfigKey[] = [ { key: 'files.watcherExclude', value: { - ['**/.eslintcache']: true, + ['**/.chromium']: true, ['**/.es']: true, + ['**/.eslintcache']: true, ['**/.yarn-local-mirror']: true, - ['**/.chromium']: true, - ['**/packages/kbn-pm/dist/index.js']: true, + ['**/*.log']: true, + ['**/api_docs']: true, ['**/bazel-*']: true, ['**/node_modules']: true, + ['**/packages/kbn-pm/dist/index.js']: true, ['**/target']: true, - ['**/*.log']: true, }, }, { key: 'search.exclude', value: { ['**/packages/kbn-pm/dist/index.js']: true, + ['**/api_docs']: true, }, }, { diff --git a/packages/kbn-monaco/src/xjson/grammar.test.ts b/packages/kbn-monaco/src/xjson/grammar.test.ts new file mode 100644 index 0000000000000..29d338cd71b0c --- /dev/null +++ b/packages/kbn-monaco/src/xjson/grammar.test.ts @@ -0,0 +1,189 @@ +/* + * 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 { createParser } from './grammar'; + +describe('createParser', () => { + let parser: ReturnType; + + beforeEach(() => { + parser = createParser(); + }); + + test('should create a xjson grammar parser', () => { + expect(createParser()).toBeInstanceOf(Function); + }); + + test('should return no annotations in case of valid json', () => { + expect( + parser(` + {"menu": { + "id": "file", + "value": "File", + "quotes": "'\\"", + "popup": { + "actions": [ + "new", + "open", + "close" + ], + "menuitem": [ + {"value": "New"}, + {"value": "Open"}, + {"value": "Close"} + ] + } + }} + `) + ).toMatchInlineSnapshot(` + Object { + "annotations": Array [], + } + `); + }); + + test('should support triple quotes', () => { + expect( + parser(` + {"menu": { + "id": """ + file + """, + "value": "File" + }} + `) + ).toMatchInlineSnapshot(` + Object { + "annotations": Array [], + } + `); + }); + + test('triple quotes should be correctly closed', () => { + expect( + parser(` + {"menu": { + "id": """" + file + "", + "value": "File" + }} + `) + ).toMatchInlineSnapshot(` + Object { + "annotations": Array [ + Object { + "at": 36, + "text": "Expected ',' instead of '\\"'", + "type": "error", + }, + ], + } + `); + }); + + test('an escaped quote can be appended to the end of triple quotes', () => { + expect( + parser(` + {"menu": { + "id": """ + file + \\"""", + "value": "File" + }} + `) + ).toMatchInlineSnapshot(` + Object { + "annotations": Array [], + } + `); + }); + + test('text values should be wrapper into quotes', () => { + expect( + parser(` + {"menu": { + "id": id, + "value": "File" + }} + `) + ).toMatchInlineSnapshot(` + Object { + "annotations": Array [ + Object { + "at": 36, + "text": "Unexpected 'i'", + "type": "error", + }, + ], + } + `); + }); + + test('check for close quotes', () => { + expect( + parser(` + {"menu": { + "id": "id, + "value": "File" + }} + `) + ).toMatchInlineSnapshot(` + Object { + "annotations": Array [ + Object { + "at": 52, + "text": "Expected ',' instead of 'v'", + "type": "error", + }, + ], + } + `); + }); + test('no duplicate keys', () => { + expect( + parser(` + {"menu": { + "id": "id", + "id": "File" + }} + `) + ).toMatchInlineSnapshot(` + Object { + "annotations": Array [ + Object { + "at": 53, + "text": "Duplicate key \\"id\\"", + "type": "warning", + }, + ], + } + `); + }); + + test('all curly quotes should be closed', () => { + expect( + parser(` + {"menu": { + "id": "id", + "name": "File" + } + `) + ).toMatchInlineSnapshot(` + Object { + "annotations": Array [ + Object { + "at": 82, + "text": "Expected ',' instead of ''", + "type": "error", + }, + ], + } + `); + }); +}); diff --git a/packages/kbn-monaco/src/xjson/grammar.ts b/packages/kbn-monaco/src/xjson/grammar.ts index 32c958e66d594..5d26e92f005ba 100644 --- a/packages/kbn-monaco/src/xjson/grammar.ts +++ b/packages/kbn-monaco/src/xjson/grammar.ts @@ -57,10 +57,6 @@ export const createParser = () => { text: m, }); }, - reset = function (newAt: number) { - ch = text.charAt(newAt); - at = newAt + 1; - }, next = function (c?: string) { return ( c && c !== ch && error("Expected '" + c + "' instead of '" + ch + "'"), @@ -69,15 +65,6 @@ export const createParser = () => { ch ); }, - nextUpTo = function (upTo: any, errorMessage: string) { - let currentAt = at, - i = text.indexOf(upTo, currentAt); - if (i < 0) { - error(errorMessage || "Expected '" + upTo + "'"); - } - reset(i + upTo.length); - return text.substring(currentAt, i); - }, peek = function (c: string) { return text.substr(at, c.length) === c; // nocommit - double check }, @@ -96,37 +83,50 @@ export const createParser = () => { (string += ch), next(); return (number = +string), isNaN(number) ? (error('Bad number'), void 0) : number; }, + stringLiteral = function () { + let quotes = '"""'; + let end = text.indexOf('\\"' + quotes, at + quotes.length); + + if (end >= 0) { + quotes = '\\"' + quotes; + } else { + end = text.indexOf(quotes, at + quotes.length); + } + + if (end >= 0) { + for (let l = end - at + quotes.length; l > 0; l--) { + next(); + } + } + + return next(); + }, string = function () { let hex: any, i: any, uffff: any, string = ''; + if ('"' === ch) { - if (peek('""')) { - // literal - next('"'); - next('"'); - return nextUpTo('"""', 'failed to find closing \'"""\''); - } else { - for (; next(); ) { - if ('"' === ch) return next(), string; - if ('\\' === ch) - if ((next(), 'u' === ch)) { - for ( - uffff = 0, i = 0; - 4 > i && ((hex = parseInt(next(), 16)), isFinite(hex)); - i += 1 - ) - uffff = 16 * uffff + hex; - string += String.fromCharCode(uffff); - } else { - if ('string' != typeof escapee[ch]) break; - string += escapee[ch]; - } - else string += ch; - } + for (; next(); ) { + if ('"' === ch) return next(), string; + if ('\\' === ch) + if ((next(), 'u' === ch)) { + for ( + uffff = 0, i = 0; + 4 > i && ((hex = parseInt(next(), 16)), isFinite(hex)); + i += 1 + ) + uffff = 16 * uffff + hex; + string += String.fromCharCode(uffff); + } else { + if ('string' != typeof escapee[ch]) break; + string += escapee[ch]; + } + else string += ch; } } + error('Bad string'); }, white = function () { @@ -165,9 +165,9 @@ export const createParser = () => { ((key = string()), white(), next(':'), - Object.hasOwnProperty.call(object, key) && + Object.hasOwnProperty.call(object, key!) && warning('Duplicate key "' + key + '"', latchKeyStart), - (object[key] = value()), + (object[key!] = value()), white(), '}' === ch) ) @@ -179,6 +179,9 @@ export const createParser = () => { }; return ( (value = function () { + if (peek('"""')) { + return stringLiteral(); + } switch ((white(), ch)) { case '{': return object(); diff --git a/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts b/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts index 2c8186ac7fa4f..f2ab22f8c97df 100644 --- a/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts +++ b/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts @@ -103,6 +103,7 @@ export const lexerRules: monaco.languages.IMonarchLanguage = { string_literal: [ [/"""/, { token: 'punctuation.end_triple_quote', next: '@pop' }], + [/\\""""/, { token: 'punctuation.end_triple_quote', next: '@pop' }], [/./, { token: 'multi_string' }], ], }, diff --git a/packages/kbn-test/src/functional_tests/lib/babel_register_for_test_plugins.js b/packages/kbn-test/src/functional_tests/lib/babel_register_for_test_plugins.js index 2ded0e509c253..09ed81b62a09d 100644 --- a/packages/kbn-test/src/functional_tests/lib/babel_register_for_test_plugins.js +++ b/packages/kbn-test/src/functional_tests/lib/babel_register_for_test_plugins.js @@ -6,23 +6,30 @@ * Side Public License, v 1. */ +const Fs = require('fs'); const Path = require('path'); -const { REPO_ROOT } = require('@kbn/dev-utils'); +const { REPO_ROOT: REPO_ROOT_FOLLOWING_SYMLINKS } = require('@kbn/dev-utils'); +const BASE_REPO_ROOT = Path.resolve( + Fs.realpathSync(Path.resolve(REPO_ROOT_FOLLOWING_SYMLINKS, 'package.json')), + '..' +); + +const transpileKbnPaths = [ + 'test', + 'x-pack/test', + 'examples', + 'x-pack/examples', + // TODO: should should probably remove this link back to the source + 'x-pack/plugins/task_manager/server/config.ts', + 'src/core/utils/default_app_categories.ts', +].map((path) => Path.resolve(BASE_REPO_ROOT, path)); // modifies all future calls to require() to automatically // compile the required source with babel require('@babel/register')({ ignore: [/[\/\\](node_modules|target|dist)[\/\\]/], - only: [ - Path.resolve(REPO_ROOT, 'test'), - Path.resolve(REPO_ROOT, 'x-pack/test'), - Path.resolve(REPO_ROOT, 'examples'), - Path.resolve(REPO_ROOT, 'x-pack/examples'), - // TODO: should should probably remove this link back to the source - Path.resolve(REPO_ROOT, 'x-pack/plugins/task_manager/server/config.ts'), - Path.resolve(REPO_ROOT, 'src/core/utils/default_app_categories.ts'), - ], + only: transpileKbnPaths, babelrc: false, presets: [require.resolve('@kbn/babel-preset/node_preset')], extensions: ['.js', '.ts', '.tsx'], diff --git a/src/plugins/data/common/query/persistable_state.test.ts b/src/plugins/data/common/query/persistable_state.test.ts index 807cc72a071be..93f14a0fc2e08 100644 --- a/src/plugins/data/common/query/persistable_state.test.ts +++ b/src/plugins/data/common/query/persistable_state.test.ts @@ -8,6 +8,7 @@ import { extract, inject } from './persistable_state'; import { Filter } from '@kbn/es-query'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../common'; describe('filter manager persistable state tests', () => { const filters: Filter[] = [ @@ -15,13 +16,15 @@ describe('filter manager persistable state tests', () => { ]; describe('reference injection', () => { test('correctly inserts reference to filter', () => { - const updatedFilters = inject(filters, [{ type: 'index_pattern', name: 'test', id: '123' }]); + const updatedFilters = inject(filters, [ + { type: DATA_VIEW_SAVED_OBJECT_TYPE, name: 'test', id: '123' }, + ]); expect(updatedFilters[0]).toHaveProperty('meta.index', '123'); }); test('drops index setting if reference is missing', () => { const updatedFilters = inject(filters, [ - { type: 'index_pattern', name: 'test123', id: '123' }, + { type: DATA_VIEW_SAVED_OBJECT_TYPE, name: 'test123', id: '123' }, ]); expect(updatedFilters[0]).toHaveProperty('meta.index', undefined); }); diff --git a/src/plugins/data/common/query/persistable_state.ts b/src/plugins/data/common/query/persistable_state.ts index 934d481685db4..177aae391c4fb 100644 --- a/src/plugins/data/common/query/persistable_state.ts +++ b/src/plugins/data/common/query/persistable_state.ts @@ -8,7 +8,9 @@ import uuid from 'uuid'; import { Filter } from '@kbn/es-query'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../common'; import { SavedObjectReference } from '../../../../core/types'; +import { MigrateFunctionsObject } from '../../../kibana_utils/common'; export const extract = (filters: Filter[]) => { const references: SavedObjectReference[] = []; @@ -16,7 +18,7 @@ export const extract = (filters: Filter[]) => { if (filter.meta?.index) { const id = uuid(); references.push({ - type: 'index_pattern', + type: DATA_VIEW_SAVED_OBJECT_TYPE, name: id, id: filter.meta.index, }); @@ -54,6 +56,10 @@ export const telemetry = (filters: Filter[], collector: unknown) => { return {}; }; -export const getAllMigrations = () => { +export const migrateToLatest = (filters: Filter[], version: string) => { + return filters; +}; + +export const getAllMigrations = (): MigrateFunctionsObject => { return {}; }; diff --git a/src/plugins/data/common/query/types.ts b/src/plugins/data/common/query/types.ts index c1861beb1ed90..fea59ea558a35 100644 --- a/src/plugins/data/common/query/types.ts +++ b/src/plugins/data/common/query/types.ts @@ -6,6 +6,25 @@ * Side Public License, v 1. */ -export * from './timefilter/types'; +import type { Query, Filter } from '@kbn/es-query'; +import type { RefreshInterval, TimeRange } from './timefilter/types'; -export { Query } from '@kbn/es-query'; +export type { RefreshInterval, TimeRange, TimeRangeBounds } from './timefilter/types'; +export type { Query } from '@kbn/es-query'; + +export type SavedQueryTimeFilter = TimeRange & { + refreshInterval: RefreshInterval; +}; + +export interface SavedQuery { + id: string; + attributes: SavedQueryAttributes; +} + +export interface SavedQueryAttributes { + title: string; + description: string; + query: Query; + filters?: Filter[]; + timefilter?: SavedQueryTimeFilter; +} diff --git a/src/plugins/data/common/search/aggs/param_types/json.test.ts b/src/plugins/data/common/search/aggs/param_types/json.test.ts index 1b3af5b92c26b..8e71cf4657e1f 100644 --- a/src/plugins/data/common/search/aggs/param_types/json.test.ts +++ b/src/plugins/data/common/search/aggs/param_types/json.test.ts @@ -67,10 +67,34 @@ describe('JSON', function () { aggParam.write(aggConfig, output); expect(aggConfig.params).toHaveProperty(paramName); - expect(output.params).toEqual({ - existing: 'true', - new_param: 'should exist in output', - }); + expect(output.params).toMatchInlineSnapshot(` + Object { + "existing": "true", + "new_param": "should exist in output", + } + `); + }); + + it('should append param when valid JSON with triple quotes', () => { + const aggParam = initAggParam(); + const jsonData = `{ + "a": """ + multiline string - line 1 + """ + }`; + + aggConfig.params[paramName] = jsonData; + + aggParam.write(aggConfig, output); + expect(aggConfig.params).toHaveProperty(paramName); + + expect(output.params).toMatchInlineSnapshot(` + Object { + "a": " + multiline string - line 1 + ", + } + `); }); it('should not overwrite existing params', () => { diff --git a/src/plugins/data/common/search/aggs/param_types/json.ts b/src/plugins/data/common/search/aggs/param_types/json.ts index 1678b6586ce80..f499286140af1 100644 --- a/src/plugins/data/common/search/aggs/param_types/json.ts +++ b/src/plugins/data/common/search/aggs/param_types/json.ts @@ -11,6 +11,17 @@ import _ from 'lodash'; import { IAggConfig } from '../agg_config'; import { BaseParamType } from './base'; +function collapseLiteralStrings(xjson: string) { + const tripleQuotes = '"""'; + const splitData = xjson.split(tripleQuotes); + + for (let idx = 1; idx < splitData.length - 1; idx += 2) { + splitData[idx] = JSON.stringify(splitData[idx]); + } + + return splitData.join(''); +} + export class JsonParamType extends BaseParamType { constructor(config: Record) { super(config); @@ -26,9 +37,8 @@ export class JsonParamType extends BaseParamType { return; } - // handle invalid Json input try { - paramJson = JSON.parse(param); + paramJson = JSON.parse(collapseLiteralStrings(param)); } catch (err) { return; } diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 4a55cc2a0d511..25f649f69a052 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -130,7 +130,7 @@ export class DataPublicPlugin core: CoreStart, { uiActions, fieldFormats, dataViews }: DataStartDependencies ): DataPublicPluginStart { - const { uiSettings, notifications, savedObjects, overlays } = core; + const { uiSettings, notifications, overlays } = core; setNotifications(notifications); setOverlays(overlays); setUiSettings(uiSettings); @@ -138,7 +138,7 @@ export class DataPublicPlugin const query = this.queryService.start({ storage: this.storage, - savedObjectsClient: savedObjects.client, + http: core.http, uiSettings, }); diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts index 5104a934fdec8..314f13e3524db 100644 --- a/src/plugins/data/public/query/query_service.ts +++ b/src/plugins/data/public/query/query_service.ts @@ -7,7 +7,7 @@ */ import { share } from 'rxjs/operators'; -import { IUiSettingsClient, SavedObjectsClientContract } from 'src/core/public'; +import { HttpStart, IUiSettingsClient } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { buildEsQuery } from '@kbn/es-query'; import { FilterManager } from './filter_manager'; @@ -15,7 +15,7 @@ import { createAddToQueryLog } from './lib'; import { TimefilterService, TimefilterSetup } from './timefilter'; import { createSavedQueryService } from './saved_query/saved_query_service'; import { createQueryStateObservable } from './state_sync/create_global_query_observable'; -import { QueryStringManager, QueryStringContract } from './query_string'; +import { QueryStringContract, QueryStringManager } from './query_string'; import { getEsQueryConfig, TimeRange } from '../../common'; import { getUiSettings } from '../services'; import { NowProviderInternalContract } from '../now_provider'; @@ -33,9 +33,9 @@ interface QueryServiceSetupDependencies { } interface QueryServiceStartDependencies { - savedObjectsClient: SavedObjectsClientContract; storage: IStorageWrapper; uiSettings: IUiSettingsClient; + http: HttpStart; } export class QueryService { @@ -70,7 +70,7 @@ export class QueryService { }; } - public start({ savedObjectsClient, storage, uiSettings }: QueryServiceStartDependencies) { + public start({ storage, uiSettings, http }: QueryServiceStartDependencies) { return { addToQueryLog: createAddToQueryLog({ storage, @@ -78,7 +78,7 @@ export class QueryService { }), filterManager: this.filterManager, queryString: this.queryStringManager, - savedQueries: createSavedQueryService(savedObjectsClient), + savedQueries: createSavedQueryService(http), state$: this.state$, timefilter: this.timefilter, getEsQuery: (indexPattern: IndexPattern, timeRange?: TimeRange) => { diff --git a/src/plugins/data/public/query/saved_query/saved_query_service.test.ts b/src/plugins/data/public/query/saved_query/saved_query_service.test.ts index 673a86df98881..047051c302083 100644 --- a/src/plugins/data/public/query/saved_query/saved_query_service.test.ts +++ b/src/plugins/data/public/query/saved_query/saved_query_service.test.ts @@ -7,8 +7,20 @@ */ import { createSavedQueryService } from './saved_query_service'; -import { FilterStateStore } from '../../../common'; -import { SavedQueryAttributes } from './types'; +import { httpServiceMock } from '../../../../../core/public/mocks'; +import { SavedQueryAttributes } from '../../../common'; + +const http = httpServiceMock.createStartContract(); + +const { + deleteSavedQuery, + getSavedQuery, + findSavedQueries, + createQuery, + updateQuery, + getAllSavedQueries, + getSavedQueryCount, +} = createSavedQueryService(http); const savedQueryAttributes: SavedQueryAttributes = { title: 'foo', @@ -17,416 +29,90 @@ const savedQueryAttributes: SavedQueryAttributes = { language: 'kuery', query: 'response:200', }, -}; -const savedQueryAttributesBar: SavedQueryAttributes = { - title: 'bar', - description: 'baz', - query: { - language: 'kuery', - query: 'response:200', - }, -}; - -const savedQueryAttributesWithFilters: SavedQueryAttributes = { - ...savedQueryAttributes, - filters: [ - { - query: { match_all: {} }, - $state: { store: FilterStateStore.APP_STATE }, - meta: { - disabled: false, - negate: false, - alias: null, - }, - }, - ], - timefilter: { - to: 'now', - from: 'now-15m', - refreshInterval: { - pause: false, - value: 0, - }, - }, + filters: [], }; -const mockSavedObjectsClient = { - create: jest.fn(), - error: jest.fn(), - find: jest.fn(), - resolve: jest.fn(), - delete: jest.fn(), -}; - -const { - deleteSavedQuery, - getSavedQuery, - findSavedQueries, - saveQuery, - getAllSavedQueries, - getSavedQueryCount, -} = createSavedQueryService( - // @ts-ignore - mockSavedObjectsClient -); - describe('saved query service', () => { afterEach(() => { - mockSavedObjectsClient.create.mockReset(); - mockSavedObjectsClient.find.mockReset(); - mockSavedObjectsClient.resolve.mockReset(); - mockSavedObjectsClient.delete.mockReset(); + http.post.mockReset(); + http.get.mockReset(); + http.delete.mockReset(); }); - describe('saveQuery', function () { - it('should create a saved object for the given attributes', async () => { - mockSavedObjectsClient.create.mockReturnValue({ - id: 'foo', - attributes: savedQueryAttributes, + describe('createQuery', function () { + it('should post the stringified given attributes', async () => { + await createQuery(savedQueryAttributes); + expect(http.post).toBeCalled(); + expect(http.post).toHaveBeenCalledWith('/api/saved_query/_create', { + body: '{"title":"foo","description":"bar","query":{"language":"kuery","query":"response:200"},"filters":[]}', }); - - const response = await saveQuery(savedQueryAttributes); - expect(mockSavedObjectsClient.create).toHaveBeenCalledWith('query', savedQueryAttributes, { - id: 'foo', - }); - expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributes }); }); + }); - it('should allow overwriting an existing saved query', async () => { - mockSavedObjectsClient.create.mockReturnValue({ - id: 'foo', - attributes: savedQueryAttributes, - }); - - const response = await saveQuery(savedQueryAttributes, { overwrite: true }); - expect(mockSavedObjectsClient.create).toHaveBeenCalledWith('query', savedQueryAttributes, { - id: 'foo', - overwrite: true, + describe('updateQuery', function () { + it('should put the ID & stringified given attributes', async () => { + await updateQuery('foo', savedQueryAttributes); + expect(http.put).toBeCalled(); + expect(http.put).toHaveBeenCalledWith('/api/saved_query/foo', { + body: '{"title":"foo","description":"bar","query":{"language":"kuery","query":"response:200"},"filters":[]}', }); - expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributes }); }); + }); - it('should optionally accept filters and timefilters in object format', async () => { - const serializedSavedQueryAttributesWithFilters = { - ...savedQueryAttributesWithFilters, - filters: savedQueryAttributesWithFilters.filters, - timefilter: savedQueryAttributesWithFilters.timefilter, - }; - - mockSavedObjectsClient.create.mockReturnValue({ - id: 'foo', - attributes: serializedSavedQueryAttributesWithFilters, + describe('getAllSavedQueries', function () { + it('should post and extract the saved queries from the response', async () => { + http.post.mockResolvedValue({ + total: 0, + savedQueries: [{ attributes: savedQueryAttributes }], }); - - const response = await saveQuery(savedQueryAttributesWithFilters); - - expect(mockSavedObjectsClient.create).toHaveBeenCalledWith( - 'query', - serializedSavedQueryAttributesWithFilters, - { id: 'foo' } - ); - expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributesWithFilters }); - }); - - it('should throw an error when saved objects client returns error', async () => { - mockSavedObjectsClient.create.mockReturnValue({ - error: { - error: '123', - message: 'An Error', - }, + const result = await getAllSavedQueries(); + expect(http.post).toBeCalled(); + expect(http.post).toHaveBeenCalledWith('/api/saved_query/_find', { + body: '{"perPage":10000}', }); - - let error = null; - try { - await saveQuery(savedQueryAttributes); - } catch (e) { - error = e; - } - expect(error).not.toBe(null); - }); - it('should throw an error if the saved query does not have a title', async () => { - let error = null; - try { - await saveQuery({ ...savedQueryAttributes, title: '' }); - } catch (e) { - error = e; - } - expect(error).not.toBe(null); + expect(result).toEqual([{ attributes: savedQueryAttributes }]); }); }); - describe('findSavedQueries', function () { - it('should find and return saved queries without search text or pagination parameters', async () => { - mockSavedObjectsClient.find.mockReturnValue({ - savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }], - total: 5, - }); - - const response = await findSavedQueries(); - expect(response.queries).toEqual([{ id: 'foo', attributes: savedQueryAttributes }]); - }); - it('should return the total count along with the requested queries', async () => { - mockSavedObjectsClient.find.mockReturnValue({ - savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }], - total: 5, - }); - - const response = await findSavedQueries(); - expect(response.total).toEqual(5); - }); - - it('should find and return saved queries with search text matching the title field', async () => { - mockSavedObjectsClient.find.mockReturnValue({ - savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }], - total: 5, - }); - const response = await findSavedQueries('foo'); - expect(mockSavedObjectsClient.find).toHaveBeenCalledWith({ - page: 1, - perPage: 50, - search: 'foo', - searchFields: ['title^5', 'description'], - sortField: '_score', - type: 'query', - }); - expect(response.queries).toEqual([{ id: 'foo', attributes: savedQueryAttributes }]); - }); - it('should find and return parsed filters and timefilters items', async () => { - const serializedSavedQueryAttributesWithFilters = { - ...savedQueryAttributesWithFilters, - filters: savedQueryAttributesWithFilters.filters, - timefilter: savedQueryAttributesWithFilters.timefilter, - }; - mockSavedObjectsClient.find.mockReturnValue({ - savedObjects: [{ id: 'foo', attributes: serializedSavedQueryAttributesWithFilters }], - total: 5, - }); - const response = await findSavedQueries('bar'); - expect(response.queries).toEqual([ - { id: 'foo', attributes: savedQueryAttributesWithFilters }, - ]); - }); - it('should return an array of saved queries', async () => { - mockSavedObjectsClient.find.mockReturnValue({ - savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }], - total: 5, + describe('findSavedQueries', function () { + it('should post and return the total & saved queries', async () => { + http.post.mockResolvedValue({ + total: 0, + savedQueries: [{ attributes: savedQueryAttributes }], }); - const response = await findSavedQueries(); - expect(response.queries).toEqual( - expect.objectContaining([ - { - attributes: { - description: 'bar', - query: { language: 'kuery', query: 'response:200' }, - title: 'foo', - }, - id: 'foo', - }, - ]) - ); - }); - it('should accept perPage and page properties', async () => { - mockSavedObjectsClient.find.mockReturnValue({ - savedObjects: [ - { id: 'foo', attributes: savedQueryAttributes }, - { id: 'bar', attributes: savedQueryAttributesBar }, - ], - total: 5, + const result = await findSavedQueries(); + expect(http.post).toBeCalled(); + expect(http.post).toHaveBeenCalledWith('/api/saved_query/_find', { + body: '{"page":1,"perPage":50,"search":""}', }); - const response = await findSavedQueries(undefined, 2, 1); - expect(mockSavedObjectsClient.find).toHaveBeenCalledWith({ - page: 1, - perPage: 2, - search: '', - searchFields: ['title^5', 'description'], - sortField: '_score', - type: 'query', + expect(result).toEqual({ + queries: [{ attributes: savedQueryAttributes }], + total: 0, }); - expect(response.queries).toEqual( - expect.objectContaining([ - { - attributes: { - description: 'bar', - query: { language: 'kuery', query: 'response:200' }, - title: 'foo', - }, - id: 'foo', - }, - { - attributes: { - description: 'baz', - query: { language: 'kuery', query: 'response:200' }, - title: 'bar', - }, - id: 'bar', - }, - ]) - ); }); }); describe('getSavedQuery', function () { - it('should retrieve a saved query by id', async () => { - mockSavedObjectsClient.resolve.mockReturnValue({ - saved_object: { - id: 'foo', - attributes: savedQueryAttributes, - }, - outcome: 'exactMatch', - }); - - const response = await getSavedQuery('foo'); - expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributes }); - }); - it('should only return saved queries', async () => { - mockSavedObjectsClient.resolve.mockReturnValue({ - saved_object: { - id: 'foo', - attributes: savedQueryAttributes, - }, - outcome: 'exactMatch', - }); - - await getSavedQuery('foo'); - expect(mockSavedObjectsClient.resolve).toHaveBeenCalledWith('query', 'foo'); - }); - - it('should parse a json query', async () => { - mockSavedObjectsClient.resolve.mockReturnValue({ - saved_object: { - id: 'food', - attributes: { - title: 'food', - description: 'bar', - query: { - language: 'kuery', - query: '{"x": "y"}', - }, - }, - }, - outcome: 'exactMatch', - }); - - const response = await getSavedQuery('food'); - expect(response.attributes.query.query).toEqual({ x: 'y' }); - }); - - it('should handle null string', async () => { - mockSavedObjectsClient.resolve.mockReturnValue({ - saved_object: { - id: 'food', - attributes: { - title: 'food', - description: 'bar', - query: { - language: 'kuery', - query: 'null', - }, - }, - }, - outcome: 'exactMatch', - }); - - const response = await getSavedQuery('food'); - expect(response.attributes.query.query).toEqual('null'); - }); - - it('should handle null quoted string', async () => { - mockSavedObjectsClient.resolve.mockReturnValue({ - saved_object: { - id: 'food', - attributes: { - title: 'food', - description: 'bar', - query: { - language: 'kuery', - query: '"null"', - }, - }, - }, - outcome: 'exactMatch', - }); - - const response = await getSavedQuery('food'); - expect(response.attributes.query.query).toEqual('"null"'); - }); - - it('should not lose quotes', async () => { - mockSavedObjectsClient.resolve.mockReturnValue({ - saved_object: { - id: 'food', - attributes: { - title: 'food', - description: 'bar', - query: { - language: 'kuery', - query: '"Bob"', - }, - }, - }, - outcome: 'exactMatch', - }); - - const response = await getSavedQuery('food'); - expect(response.attributes.query.query).toEqual('"Bob"'); - }); - - it('should throw if conflict', async () => { - mockSavedObjectsClient.resolve.mockReturnValue({ - saved_object: { - id: 'foo', - attributes: savedQueryAttributes, - }, - outcome: 'conflict', - }); - - const result = getSavedQuery('food'); - expect(result).rejects.toMatchInlineSnapshot( - `[Error: Multiple saved queries found with ID: food (legacy URL alias conflict)]` - ); + it('should get the given ID', async () => { + await getSavedQuery('my_id'); + expect(http.get).toBeCalled(); + expect(http.get).toHaveBeenCalledWith('/api/saved_query/my_id'); }); }); describe('deleteSavedQuery', function () { - it('should delete the saved query for the given ID', async () => { - await deleteSavedQuery('foo'); - expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith('query', 'foo'); - }); - }); - - describe('getAllSavedQueries', function () { - it('should return all the saved queries', async () => { - mockSavedObjectsClient.find.mockReturnValue({ - savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }], - }); - const response = await getAllSavedQueries(); - expect(response).toEqual( - expect.objectContaining([ - { - attributes: { - description: 'bar', - query: { language: 'kuery', query: 'response:200' }, - title: 'foo', - }, - id: 'foo', - }, - ]) - ); - expect(mockSavedObjectsClient.find).toHaveBeenCalledWith({ - page: 1, - perPage: 0, - type: 'query', - }); + it('should delete the given ID', async () => { + await deleteSavedQuery('my_id'); + expect(http.delete).toBeCalled(); + expect(http.delete).toHaveBeenCalledWith('/api/saved_query/my_id'); }); }); describe('getSavedQueryCount', function () { - it('should return the total number of saved queries', async () => { - mockSavedObjectsClient.find.mockReturnValue({ - total: 1, - }); - const response = await getSavedQueryCount(); - expect(response).toEqual(1); + it('should get the total', async () => { + await getSavedQueryCount(); + expect(http.get).toBeCalled(); + expect(http.get).toHaveBeenCalledWith('/api/saved_query/_count'); }); }); }); diff --git a/src/plugins/data/public/query/saved_query/saved_query_service.ts b/src/plugins/data/public/query/saved_query/saved_query_service.ts index 89a357a66d370..8ec9167a3a0c2 100644 --- a/src/plugins/data/public/query/saved_query/saved_query_service.ts +++ b/src/plugins/data/public/query/saved_query/saved_query_service.ts @@ -6,163 +6,61 @@ * Side Public License, v 1. */ -import { isObject } from 'lodash'; -import { SavedObjectsClientContract, SavedObjectAttributes } from 'src/core/public'; -import { SavedQueryAttributes, SavedQuery, SavedQueryService } from './types'; - -type SerializedSavedQueryAttributes = SavedObjectAttributes & - SavedQueryAttributes & { - query: { - query: string; - language: string; - }; +import { HttpStart } from 'src/core/public'; +import { SavedQuery } from './types'; +import { SavedQueryAttributes } from '../../../common'; + +export const createSavedQueryService = (http: HttpStart) => { + const createQuery = async (attributes: SavedQueryAttributes, { overwrite = false } = {}) => { + const savedQuery = await http.post('/api/saved_query/_create', { + body: JSON.stringify(attributes), + }); + return savedQuery; }; -export const createSavedQueryService = ( - savedObjectsClient: SavedObjectsClientContract -): SavedQueryService => { - const saveQuery = async (attributes: SavedQueryAttributes, { overwrite = false } = {}) => { - if (!attributes.title.length) { - // title is required extra check against circumventing the front end - throw new Error('Cannot create saved query without a title'); - } - - const query = { - query: - typeof attributes.query.query === 'string' - ? attributes.query.query - : JSON.stringify(attributes.query.query), - language: attributes.query.language, - }; - - const queryObject: SerializedSavedQueryAttributes = { - title: attributes.title.trim(), // trim whitespace before save as an extra precaution against circumventing the front end - description: attributes.description, - query, - }; - - if (attributes.filters) { - queryObject.filters = attributes.filters; - } - - if (attributes.timefilter) { - queryObject.timefilter = attributes.timefilter; - } - - let rawQueryResponse; - if (!overwrite) { - rawQueryResponse = await savedObjectsClient.create('query', queryObject, { - id: attributes.title, - }); - } else { - rawQueryResponse = await savedObjectsClient.create('query', queryObject, { - id: attributes.title, - overwrite: true, - }); - } - - if (rawQueryResponse.error) { - throw new Error(rawQueryResponse.error.message); - } - - return parseSavedQueryObject(rawQueryResponse); + const updateQuery = async (id: string, attributes: SavedQueryAttributes) => { + const savedQuery = await http.put(`/api/saved_query/${id}`, { + body: JSON.stringify(attributes), + }); + return savedQuery; }; + // we have to tell the saved objects client how many to fetch, otherwise it defaults to fetching 20 per page const getAllSavedQueries = async (): Promise => { - const count = await getSavedQueryCount(); - const response = await savedObjectsClient.find({ - type: 'query', - perPage: count, - page: 1, + const { savedQueries } = await http.post('/api/saved_query/_find', { + body: JSON.stringify({ perPage: 10000 }), }); - return response.savedObjects.map( - (savedObject: { id: string; attributes: SerializedSavedQueryAttributes }) => - parseSavedQueryObject(savedObject) - ); + return savedQueries; }; + // findSavedQueries will do a 'match_all' if no search string is passed in const findSavedQueries = async ( - searchText: string = '', + search: string = '', perPage: number = 50, - activePage: number = 1 + page: number = 1 ): Promise<{ total: number; queries: SavedQuery[] }> => { - const response = await savedObjectsClient.find({ - type: 'query', - search: searchText, - searchFields: ['title^5', 'description'], - sortField: '_score', - perPage, - page: activePage, + const { total, savedQueries: queries } = await http.post('/api/saved_query/_find', { + body: JSON.stringify({ page, perPage, search }), }); - return { - total: response.total, - queries: response.savedObjects.map( - (savedObject: { id: string; attributes: SerializedSavedQueryAttributes }) => - parseSavedQueryObject(savedObject) - ), - }; - }; - - const getSavedQuery = async (id: string): Promise => { - const { saved_object: savedObject, outcome } = - await savedObjectsClient.resolve('query', id); - if (outcome === 'conflict') { - throw new Error(`Multiple saved queries found with ID: ${id} (legacy URL alias conflict)`); - } else if (savedObject.error) { - throw new Error(savedObject.error.message); - } - return parseSavedQueryObject(savedObject); + return { total, queries }; }; - const deleteSavedQuery = async (id: string) => { - return await savedObjectsClient.delete('query', id); + const getSavedQuery = (id: string): Promise => { + return http.get(`/api/saved_query/${id}`); }; - const parseSavedQueryObject = (savedQuery: { - id: string; - attributes: SerializedSavedQueryAttributes; - }) => { - let queryString: string | object = savedQuery.attributes.query.query; - - try { - const parsedQueryString: object = JSON.parse(savedQuery.attributes.query.query); - if (isObject(parsedQueryString)) { - queryString = parsedQueryString; - } - } catch (e) {} // eslint-disable-line no-empty - - const savedQueryItems: SavedQueryAttributes = { - title: savedQuery.attributes.title || '', - description: savedQuery.attributes.description || '', - query: { - query: queryString, - language: savedQuery.attributes.query.language, - }, - }; - if (savedQuery.attributes.filters) { - savedQueryItems.filters = savedQuery.attributes.filters; - } - if (savedQuery.attributes.timefilter) { - savedQueryItems.timefilter = savedQuery.attributes.timefilter; - } - return { - id: savedQuery.id, - attributes: savedQueryItems, - }; + const deleteSavedQuery = (id: string) => { + return http.delete(`/api/saved_query/${id}`); }; const getSavedQueryCount = async (): Promise => { - const response = await savedObjectsClient.find({ - type: 'query', - perPage: 0, - page: 1, - }); - return response.total; + return http.get('/api/saved_query/_count'); }; return { - saveQuery, + createQuery, + updateQuery, getAllSavedQueries, findSavedQueries, getSavedQuery, diff --git a/src/plugins/data/public/query/saved_query/types.ts b/src/plugins/data/public/query/saved_query/types.ts index bd53bb7d77b30..0f1763433e72a 100644 --- a/src/plugins/data/public/query/saved_query/types.ts +++ b/src/plugins/data/public/query/saved_query/types.ts @@ -26,10 +26,8 @@ export interface SavedQueryAttributes { } export interface SavedQueryService { - saveQuery: ( - attributes: SavedQueryAttributes, - config?: { overwrite: boolean } - ) => Promise; + createQuery: (attributes: SavedQueryAttributes) => Promise; + updateQuery: (id: string, attributes: SavedQueryAttributes) => Promise; getAllSavedQueries: () => Promise; findSavedQueries: ( searchText?: string, diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts index b4ec4934233d0..857a932d9157b 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts @@ -74,7 +74,7 @@ describe('connect_to_global_state', () => { queryServiceStart = queryService.start({ uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), - savedObjectsClient: startMock.savedObjects.client, + http: startMock.http, }); filterManager = queryServiceStart.filterManager; timeFilter = queryServiceStart.timefilter.timefilter; @@ -308,7 +308,7 @@ describe('connect_to_app_state', () => { queryServiceStart = queryService.start({ uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), - savedObjectsClient: startMock.savedObjects.client, + http: startMock.http, }); filterManager = queryServiceStart.filterManager; @@ -487,7 +487,7 @@ describe('filters with different state', () => { queryServiceStart = queryService.start({ uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), - savedObjectsClient: startMock.savedObjects.client, + http: startMock.http, }); filterManager = queryServiceStart.filterManager; diff --git a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts index 73f78eb98968d..2e48a11efd69c 100644 --- a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts +++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts @@ -68,7 +68,7 @@ describe('sync_query_state_with_url', () => { queryServiceStart = queryService.start({ uiSettings: startMock.uiSettings, storage: new Storage(new StubBrowserStorage()), - savedObjectsClient: startMock.savedObjects.client, + http: startMock.http, }); filterManager = queryServiceStart.filterManager; timefilter = queryServiceStart.timefilter.timefilter; diff --git a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx index d0221658f3e08..c7a79658fac88 100644 --- a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx +++ b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx @@ -24,10 +24,9 @@ import { import { i18n } from '@kbn/i18n'; import { sortBy, isEqual } from 'lodash'; import { SavedQuery, SavedQueryService } from '../..'; -import { SavedQueryAttributes } from '../../query'; interface Props { - savedQuery?: SavedQueryAttributes; + savedQuery?: SavedQuery; savedQueryService: SavedQueryService; onSave: (savedQueryMeta: SavedQueryMeta) => void; onClose: () => void; @@ -36,6 +35,7 @@ interface Props { } export interface SavedQueryMeta { + id?: string; title: string; description: string; shouldIncludeFilters: boolean; @@ -50,18 +50,18 @@ export function SaveQueryForm({ showFilterOption = true, showTimeFilterOption = true, }: Props) { - const [title, setTitle] = useState(savedQuery ? savedQuery.title : ''); + const [title, setTitle] = useState(savedQuery?.attributes.title ?? ''); const [enabledSaveButton, setEnabledSaveButton] = useState(Boolean(savedQuery)); - const [description, setDescription] = useState(savedQuery ? savedQuery.description : ''); + const [description, setDescription] = useState(savedQuery?.attributes.description ?? ''); const [savedQueries, setSavedQueries] = useState([]); const [shouldIncludeFilters, setShouldIncludeFilters] = useState( - savedQuery ? !!savedQuery.filters : true + Boolean(savedQuery?.attributes.filters ?? true) ); // Defaults to false because saved queries are meant to be as portable as possible and loading // a saved query with a time filter will override whatever the current value of the global timepicker // is. We expect this option to be used rarely and only when the user knows they want this behavior. const [shouldIncludeTimefilter, setIncludeTimefilter] = useState( - savedQuery ? !!savedQuery.timefilter : false + Boolean(savedQuery?.attributes.timefilter ?? false) ); const [formErrors, setFormErrors] = useState([]); @@ -82,7 +82,7 @@ export function SaveQueryForm({ useEffect(() => { const fetchQueries = async () => { const allSavedQueries = await savedQueryService.getAllSavedQueries(); - const sortedAllSavedQueries = sortBy(allSavedQueries, 'attributes.title') as SavedQuery[]; + const sortedAllSavedQueries = sortBy(allSavedQueries, 'attributes.title'); setSavedQueries(sortedAllSavedQueries); }; fetchQueries(); @@ -109,13 +109,22 @@ export function SaveQueryForm({ const onClickSave = useCallback(() => { if (validate()) { onSave({ + id: savedQuery?.id, title, description, shouldIncludeFilters, shouldIncludeTimefilter, }); } - }, [validate, onSave, title, description, shouldIncludeFilters, shouldIncludeTimefilter]); + }, [ + validate, + onSave, + savedQuery?.id, + title, + description, + shouldIncludeFilters, + shouldIncludeTimefilter, + ]); const onInputChange = useCallback((event) => { setEnabledSaveButton(Boolean(event.target.value)); diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index db0bebf97578b..bd48dcd6cd34c 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -245,11 +245,12 @@ class SearchBarUI extends Component { try { let response; if (this.props.savedQuery && !saveAsNew) { - response = await this.savedQueryService.saveQuery(savedQueryAttributes, { - overwrite: true, - }); + response = await this.savedQueryService.updateQuery( + savedQueryMeta.id!, + savedQueryAttributes + ); } else { - response = await this.savedQueryService.saveQuery(savedQueryAttributes); + response = await this.savedQueryService.createQuery(savedQueryAttributes); } this.services.notifications.toasts.addSuccess( @@ -423,7 +424,7 @@ class SearchBarUI extends Component { {this.state.showSaveQueryModal ? ( this.setState({ showSaveQueryModal: false })} diff --git a/src/plugins/data/server/query/query_service.ts b/src/plugins/data/server/query/query_service.ts index 1bf5ff901e90f..173abeda0c951 100644 --- a/src/plugins/data/server/query/query_service.ts +++ b/src/plugins/data/server/query/query_service.ts @@ -8,11 +8,21 @@ import { CoreSetup, Plugin } from 'kibana/server'; import { querySavedObjectType } from '../saved_objects'; -import { extract, inject, telemetry, getAllMigrations } from '../../common/query/persistable_state'; +import { extract, getAllMigrations, inject, telemetry } from '../../common/query/persistable_state'; +import { registerSavedQueryRoutes } from './routes'; +import { + registerSavedQueryRouteHandlerContext, + SavedQueryRouteHandlerContext, +} from './route_handler_context'; export class QueryService implements Plugin { public setup(core: CoreSetup) { core.savedObjects.registerType(querySavedObjectType); + core.http.registerRouteHandlerContext( + 'savedQuery', + registerSavedQueryRouteHandlerContext + ); + registerSavedQueryRoutes(core); return { filterManager: { diff --git a/src/plugins/data/server/query/route_handler_context.test.ts b/src/plugins/data/server/query/route_handler_context.test.ts new file mode 100644 index 0000000000000..cc7686a06cb67 --- /dev/null +++ b/src/plugins/data/server/query/route_handler_context.test.ts @@ -0,0 +1,566 @@ +/* + * 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 { coreMock } from '../../../../core/server/mocks'; +import { + DATA_VIEW_SAVED_OBJECT_TYPE, + FilterStateStore, + SavedObject, + SavedQueryAttributes, +} from '../../common'; +import { registerSavedQueryRouteHandlerContext } from './route_handler_context'; +import { SavedObjectsFindResponse, SavedObjectsUpdateResponse } from 'kibana/server'; + +const mockContext = { + core: coreMock.createRequestHandlerContext(), +}; +const { + core: { + savedObjects: { client: mockSavedObjectsClient }, + }, +} = mockContext; +const context = registerSavedQueryRouteHandlerContext(mockContext); + +const savedQueryAttributes: SavedQueryAttributes = { + title: 'foo', + description: 'bar', + query: { + language: 'kuery', + query: 'response:200', + }, + filters: [], +}; +const savedQueryAttributesBar: SavedQueryAttributes = { + title: 'bar', + description: 'baz', + query: { + language: 'kuery', + query: 'response:200', + }, +}; + +const savedQueryAttributesWithFilters: SavedQueryAttributes = { + ...savedQueryAttributes, + filters: [ + { + query: { match_all: {} }, + $state: { store: FilterStateStore.APP_STATE }, + meta: { + index: 'my-index', + disabled: false, + negate: false, + alias: null, + }, + }, + ], + timefilter: { + to: 'now', + from: 'now-15m', + refreshInterval: { + pause: false, + value: 0, + }, + }, +}; + +const savedQueryReferences = [ + { + type: DATA_VIEW_SAVED_OBJECT_TYPE, + name: 'my-index', + id: 'my-index', + }, +]; + +describe('saved query route handler context', () => { + beforeEach(() => { + mockSavedObjectsClient.create.mockClear(); + mockSavedObjectsClient.resolve.mockClear(); + mockSavedObjectsClient.find.mockClear(); + mockSavedObjectsClient.delete.mockClear(); + }); + + describe('create', function () { + it('should create a saved object for the given attributes', async () => { + const mockResponse: SavedObject = { + id: 'foo', + type: 'query', + attributes: savedQueryAttributes, + references: [], + }; + mockSavedObjectsClient.create.mockResolvedValue(mockResponse); + + const response = await context.create(savedQueryAttributes); + + expect(mockSavedObjectsClient.create).toHaveBeenCalledWith('query', savedQueryAttributes, { + references: [], + }); + expect(response).toEqual({ + id: 'foo', + attributes: savedQueryAttributes, + }); + }); + + it('should optionally accept query in object format', async () => { + const savedQueryAttributesWithQueryObject: SavedQueryAttributes = { + ...savedQueryAttributes, + query: { + language: 'lucene', + query: { match_all: {} }, + }, + }; + const mockResponse: SavedObject = { + id: 'foo', + type: 'query', + attributes: savedQueryAttributesWithQueryObject, + references: [], + }; + mockSavedObjectsClient.create.mockResolvedValue(mockResponse); + + const { attributes } = await context.create(savedQueryAttributesWithQueryObject); + + expect(attributes).toEqual(savedQueryAttributesWithQueryObject); + }); + + it('should optionally accept filters and timefilters in object format', async () => { + const serializedSavedQueryAttributesWithFilters = { + ...savedQueryAttributesWithFilters, + filters: savedQueryAttributesWithFilters.filters, + timefilter: savedQueryAttributesWithFilters.timefilter, + }; + const mockResponse: SavedObject = { + id: 'foo', + type: 'query', + attributes: serializedSavedQueryAttributesWithFilters, + references: [], + }; + mockSavedObjectsClient.create.mockResolvedValue(mockResponse); + + await context.create(savedQueryAttributesWithFilters); + + const [[type, attributes]] = mockSavedObjectsClient.create.mock.calls; + const { filters = [], timefilter } = attributes as SavedQueryAttributes; + expect(type).toEqual('query'); + expect(filters.length).toBe(1); + expect(timefilter).toEqual(savedQueryAttributesWithFilters.timefilter); + }); + + it('should throw an error when saved objects client returns error', async () => { + mockSavedObjectsClient.create.mockResolvedValue({ + error: { + error: '123', + message: 'An Error', + }, + } as SavedObject); + + const response = context.create(savedQueryAttributes); + + expect(response).rejects.toMatchInlineSnapshot(`[Error: An Error]`); + }); + + it('should throw an error if the saved query does not have a title', async () => { + const response = context.create({ ...savedQueryAttributes, title: '' }); + expect(response).rejects.toMatchInlineSnapshot( + `[Error: Cannot create saved query without a title]` + ); + }); + }); + + describe('update', function () { + it('should update a saved object for the given attributes', async () => { + const mockResponse: SavedObject = { + id: 'foo', + type: 'query', + attributes: savedQueryAttributes, + references: [], + }; + mockSavedObjectsClient.update.mockResolvedValue(mockResponse); + + const response = await context.update('foo', savedQueryAttributes); + + expect(mockSavedObjectsClient.update).toHaveBeenCalledWith( + 'query', + 'foo', + savedQueryAttributes, + { + references: [], + } + ); + expect(response).toEqual({ + id: 'foo', + attributes: savedQueryAttributes, + }); + }); + + it('should throw an error when saved objects client returns error', async () => { + mockSavedObjectsClient.update.mockResolvedValue({ + error: { + error: '123', + message: 'An Error', + }, + } as SavedObjectsUpdateResponse); + + const response = context.update('foo', savedQueryAttributes); + + expect(response).rejects.toMatchInlineSnapshot(`[Error: An Error]`); + }); + + it('should throw an error if the saved query does not have a title', async () => { + const response = context.create({ ...savedQueryAttributes, title: '' }); + expect(response).rejects.toMatchInlineSnapshot( + `[Error: Cannot create saved query without a title]` + ); + }); + }); + + describe('find', function () { + it('should find and return saved queries without search text or pagination parameters', async () => { + const mockResponse: SavedObjectsFindResponse = { + page: 0, + per_page: 0, + saved_objects: [ + { + id: 'foo', + type: 'query', + score: 0, + attributes: savedQueryAttributes, + references: [], + }, + ], + total: 5, + }; + mockSavedObjectsClient.find.mockResolvedValue(mockResponse); + + const response = await context.find(); + + expect(response.savedQueries).toEqual([{ id: 'foo', attributes: savedQueryAttributes }]); + }); + + it('should return the total count along with the requested queries', async () => { + const mockResponse: SavedObjectsFindResponse = { + page: 0, + per_page: 0, + saved_objects: [ + { id: 'foo', type: 'query', score: 0, attributes: savedQueryAttributes, references: [] }, + ], + total: 5, + }; + mockSavedObjectsClient.find.mockResolvedValue(mockResponse); + + const response = await context.find(); + + expect(response.total).toEqual(5); + }); + + it('should find and return saved queries with search text matching the title field', async () => { + const mockResponse: SavedObjectsFindResponse = { + page: 0, + per_page: 0, + saved_objects: [ + { id: 'foo', type: 'query', score: 0, attributes: savedQueryAttributes, references: [] }, + ], + total: 5, + }; + mockSavedObjectsClient.find.mockResolvedValue(mockResponse); + + const response = await context.find({ search: 'foo' }); + + expect(mockSavedObjectsClient.find).toHaveBeenCalledWith({ + page: 1, + perPage: 50, + search: 'foo', + type: 'query', + }); + expect(response.savedQueries).toEqual([{ id: 'foo', attributes: savedQueryAttributes }]); + }); + + it('should find and return parsed filters and timefilters items', async () => { + const mockResponse: SavedObjectsFindResponse = { + page: 0, + per_page: 0, + saved_objects: [ + { + id: 'foo', + type: 'query', + score: 0, + attributes: savedQueryAttributesWithFilters, + references: savedQueryReferences, + }, + ], + total: 5, + }; + mockSavedObjectsClient.find.mockResolvedValue(mockResponse); + + const response = await context.find({ search: 'bar' }); + + expect(response.savedQueries).toEqual([ + { id: 'foo', attributes: savedQueryAttributesWithFilters }, + ]); + }); + + it('should return an array of saved queries', async () => { + const mockResponse: SavedObjectsFindResponse = { + page: 0, + per_page: 0, + saved_objects: [ + { id: 'foo', type: 'query', score: 0, attributes: savedQueryAttributes, references: [] }, + ], + total: 5, + }; + mockSavedObjectsClient.find.mockResolvedValue(mockResponse); + + const response = await context.find(); + + expect(response.savedQueries).toEqual( + expect.objectContaining([ + { + attributes: { + description: 'bar', + query: { language: 'kuery', query: 'response:200' }, + filters: [], + title: 'foo', + }, + id: 'foo', + }, + ]) + ); + }); + + it('should accept perPage and page properties', async () => { + const mockResponse: SavedObjectsFindResponse = { + page: 0, + per_page: 0, + saved_objects: [ + { id: 'foo', type: 'query', score: 0, attributes: savedQueryAttributes, references: [] }, + { + id: 'bar', + type: 'query', + score: 0, + attributes: savedQueryAttributesBar, + references: [], + }, + ], + total: 5, + }; + mockSavedObjectsClient.find.mockResolvedValue(mockResponse); + + const response = await context.find({ + page: 1, + perPage: 2, + }); + + expect(mockSavedObjectsClient.find).toHaveBeenCalledWith({ + page: 1, + perPage: 2, + search: '', + type: 'query', + }); + expect(response.savedQueries).toEqual( + expect.objectContaining([ + { + attributes: { + description: 'bar', + query: { language: 'kuery', query: 'response:200' }, + filters: [], + title: 'foo', + }, + id: 'foo', + }, + { + attributes: { + description: 'baz', + query: { language: 'kuery', query: 'response:200' }, + filters: [], + title: 'bar', + }, + id: 'bar', + }, + ]) + ); + }); + }); + + describe('get', function () { + it('should retrieve a saved query by id', async () => { + mockSavedObjectsClient.resolve.mockResolvedValue({ + saved_object: { + id: 'foo', + type: 'query', + attributes: savedQueryAttributes, + references: [], + }, + outcome: 'exactMatch', + }); + + const response = await context.get('foo'); + expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributes }); + }); + + it('should only return saved queries', async () => { + mockSavedObjectsClient.resolve.mockResolvedValue({ + saved_object: { + id: 'foo', + type: 'query', + attributes: savedQueryAttributes, + references: [], + }, + outcome: 'exactMatch', + }); + + await context.get('foo'); + expect(mockSavedObjectsClient.resolve).toHaveBeenCalledWith('query', 'foo'); + }); + + it('should parse a json query', async () => { + mockSavedObjectsClient.resolve.mockResolvedValue({ + saved_object: { + id: 'food', + type: 'query', + attributes: { + title: 'food', + description: 'bar', + query: { + language: 'kuery', + query: '{"x": "y"}', + }, + }, + references: [], + }, + outcome: 'exactMatch', + }); + + const response = await context.get('food'); + expect(response.attributes.query.query).toEqual({ x: 'y' }); + }); + + it('should handle null string', async () => { + mockSavedObjectsClient.resolve.mockResolvedValue({ + saved_object: { + id: 'food', + type: 'query', + attributes: { + title: 'food', + description: 'bar', + query: { + language: 'kuery', + query: 'null', + }, + }, + references: [], + }, + outcome: 'exactMatch', + }); + + const response = await context.get('food'); + expect(response.attributes.query.query).toEqual('null'); + }); + + it('should handle null quoted string', async () => { + mockSavedObjectsClient.resolve.mockResolvedValue({ + saved_object: { + id: 'food', + type: 'query', + attributes: { + title: 'food', + description: 'bar', + query: { + language: 'kuery', + query: '"null"', + }, + }, + references: [], + }, + outcome: 'exactMatch', + }); + + const response = await context.get('food'); + expect(response.attributes.query.query).toEqual('"null"'); + }); + + it('should not lose quotes', async () => { + mockSavedObjectsClient.resolve.mockResolvedValue({ + saved_object: { + id: 'food', + type: 'query', + attributes: { + title: 'food', + description: 'bar', + query: { + language: 'kuery', + query: '"Bob"', + }, + }, + references: [], + }, + outcome: 'exactMatch', + }); + + const response = await context.get('food'); + expect(response.attributes.query.query).toEqual('"Bob"'); + }); + + it('should inject references', async () => { + mockSavedObjectsClient.resolve.mockResolvedValue({ + saved_object: { + id: 'food', + type: 'query', + attributes: savedQueryAttributesWithFilters, + references: [ + { + id: 'my-new-index', + type: DATA_VIEW_SAVED_OBJECT_TYPE, + name: 'my-index', + }, + ], + }, + outcome: 'exactMatch', + }); + + const response = await context.get('food'); + expect(response.attributes.filters[0].meta.index).toBe('my-new-index'); + }); + + it('should throw if conflict', async () => { + mockSavedObjectsClient.resolve.mockResolvedValue({ + saved_object: { + id: 'foo', + type: 'query', + attributes: savedQueryAttributes, + references: [], + }, + outcome: 'conflict', + }); + + const result = context.get('food'); + expect(result).rejects.toMatchInlineSnapshot( + `[Error: Multiple saved queries found with ID: food (legacy URL alias conflict)]` + ); + }); + }); + + describe('delete', function () { + it('should delete the saved query for the given ID', async () => { + await context.delete('foo'); + expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith('query', 'foo'); + }); + }); + + describe('count', function () { + it('should return the total number of saved queries', async () => { + mockSavedObjectsClient.find.mockResolvedValue({ + total: 1, + page: 0, + per_page: 0, + saved_objects: [], + }); + + const response = await context.count(); + + expect(response).toEqual(1); + }); + }); +}); diff --git a/src/plugins/data/server/query/route_handler_context.ts b/src/plugins/data/server/query/route_handler_context.ts new file mode 100644 index 0000000000000..3c60b33559b72 --- /dev/null +++ b/src/plugins/data/server/query/route_handler_context.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 { RequestHandlerContext, SavedObject } from 'kibana/server'; +import { isFilters } from '@kbn/es-query'; +import { isQuery, SavedQueryAttributes } from '../../common'; +import { extract, inject } from '../../common/query/persistable_state'; + +function injectReferences({ + id, + attributes, + references, +}: Pick, 'id' | 'attributes' | 'references'>) { + const { query } = attributes; + if (typeof query.query === 'string') { + try { + const parsed = JSON.parse(query.query); + query.query = parsed instanceof Object ? parsed : query.query; + } catch (e) { + // Just keep it as a string + } + } + const filters = inject(attributes.filters ?? [], references); + return { id, attributes: { ...attributes, filters } }; +} + +function extractReferences({ + title, + description, + query, + filters = [], + timefilter, +}: SavedQueryAttributes) { + const { state: extractedFilters, references } = extract(filters); + + const attributes: SavedQueryAttributes = { + title: title.trim(), + description: description.trim(), + query: { + ...query, + query: typeof query.query === 'string' ? query.query : JSON.stringify(query.query), + }, + filters: extractedFilters, + ...(timefilter && { timefilter }), + }; + + return { attributes, references }; +} + +function verifySavedQuery({ title, query, filters = [] }: SavedQueryAttributes) { + if (!isQuery(query)) { + throw new Error(`Invalid query: ${query}`); + } + + if (!isFilters(filters)) { + throw new Error(`Invalid filters: ${filters}`); + } + + if (!title.trim().length) { + throw new Error('Cannot create saved query without a title'); + } +} + +export function registerSavedQueryRouteHandlerContext(context: RequestHandlerContext) { + const createSavedQuery = async (attrs: SavedQueryAttributes) => { + verifySavedQuery(attrs); + const { attributes, references } = extractReferences(attrs); + + const savedObject = await context.core.savedObjects.client.create( + 'query', + attributes, + { + references, + } + ); + + // TODO: Handle properly + if (savedObject.error) throw new Error(savedObject.error.message); + + return injectReferences(savedObject); + }; + + const updateSavedQuery = async (id: string, attrs: SavedQueryAttributes) => { + verifySavedQuery(attrs); + const { attributes, references } = extractReferences(attrs); + + const savedObject = await context.core.savedObjects.client.update( + 'query', + id, + attributes, + { + references, + } + ); + + // TODO: Handle properly + if (savedObject.error) throw new Error(savedObject.error.message); + + return injectReferences({ id, attributes, references }); + }; + + const getSavedQuery = async (id: string) => { + const { saved_object: savedObject, outcome } = + await context.core.savedObjects.client.resolve('query', id); + if (outcome === 'conflict') { + throw new Error(`Multiple saved queries found with ID: ${id} (legacy URL alias conflict)`); + } else if (savedObject.error) { + throw new Error(savedObject.error.message); + } + return injectReferences(savedObject); + }; + + const getSavedQueriesCount = async () => { + const { total } = await context.core.savedObjects.client.find({ + type: 'query', + }); + return total; + }; + + const findSavedQueries = async ({ page = 1, perPage = 50, search = '' } = {}) => { + const { total, saved_objects: savedObjects } = + await context.core.savedObjects.client.find({ + type: 'query', + page, + perPage, + search, + }); + + const savedQueries = savedObjects.map(injectReferences); + + return { total, savedQueries }; + }; + + const deleteSavedQuery = (id: string) => { + return context.core.savedObjects.client.delete('query', id); + }; + + return { + create: createSavedQuery, + update: updateSavedQuery, + get: getSavedQuery, + count: getSavedQueriesCount, + find: findSavedQueries, + delete: deleteSavedQuery, + }; +} + +export interface SavedQueryRouteHandlerContext extends RequestHandlerContext { + savedQuery: ReturnType; +} diff --git a/src/plugins/data/server/query/routes.ts b/src/plugins/data/server/query/routes.ts new file mode 100644 index 0000000000000..cdf9e6f43dccc --- /dev/null +++ b/src/plugins/data/server/query/routes.ts @@ -0,0 +1,144 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { CoreSetup } from 'kibana/server'; +import { SavedQueryRouteHandlerContext } from './route_handler_context'; + +const SAVED_QUERY_PATH = '/api/saved_query'; +const SAVED_QUERY_ID_CONFIG = schema.object({ + id: schema.string(), +}); +const SAVED_QUERY_ATTRS_CONFIG = schema.object({ + title: schema.string(), + description: schema.string(), + query: schema.object({ + query: schema.oneOf([schema.string(), schema.object({}, { unknowns: 'allow' })]), + language: schema.string(), + }), + filters: schema.maybe(schema.arrayOf(schema.any())), + timefilter: schema.maybe(schema.any()), +}); + +export function registerSavedQueryRoutes({ http }: CoreSetup): void { + const router = http.createRouter(); + + router.post( + { + path: `${SAVED_QUERY_PATH}/_create`, + validate: { + body: SAVED_QUERY_ATTRS_CONFIG, + }, + }, + async (context, request, response) => { + try { + const body = await context.savedQuery.create(request.body); + return response.ok({ body }); + } catch (e) { + // TODO: Handle properly + return response.customError(e); + } + } + ); + + router.put( + { + path: `${SAVED_QUERY_PATH}/{id}`, + validate: { + params: SAVED_QUERY_ID_CONFIG, + body: SAVED_QUERY_ATTRS_CONFIG, + }, + }, + async (context, request, response) => { + const { id } = request.params; + try { + const body = await context.savedQuery.update(id, request.body); + return response.ok({ body }); + } catch (e) { + // TODO: Handle properly + return response.customError(e); + } + } + ); + + router.get( + { + path: `${SAVED_QUERY_PATH}/{id}`, + validate: { + params: SAVED_QUERY_ID_CONFIG, + }, + }, + async (context, request, response) => { + const { id } = request.params; + try { + const body = await context.savedQuery.get(id); + return response.ok({ body }); + } catch (e) { + // TODO: Handle properly + return response.customError(e); + } + } + ); + + router.get( + { + path: `${SAVED_QUERY_PATH}/_count`, + validate: {}, + }, + async (context, request, response) => { + try { + const count = await context.savedQuery.count(); + return response.ok({ body: `${count}` }); + } catch (e) { + // TODO: Handle properly + return response.customError(e); + } + } + ); + + router.post( + { + path: `${SAVED_QUERY_PATH}/_find`, + validate: { + body: schema.object({ + search: schema.string({ defaultValue: '' }), + perPage: schema.number({ defaultValue: 50 }), + page: schema.number({ defaultValue: 1 }), + }), + }, + }, + async (context, request, response) => { + try { + const body = await context.savedQuery.find(request.body); + return response.ok({ body }); + } catch (e) { + // TODO: Handle properly + return response.customError(e); + } + } + ); + + router.delete( + { + path: `${SAVED_QUERY_PATH}/{id}`, + validate: { + params: SAVED_QUERY_ID_CONFIG, + }, + }, + async (context, request, response) => { + const { id } = request.params; + try { + const body = await context.savedQuery.delete(id); + return response.ok({ body }); + } catch (e) { + // TODO: Handle properly + return response.customError(e); + } + } + ); +} diff --git a/src/plugins/data/server/saved_objects/migrations/query.ts b/src/plugins/data/server/saved_objects/migrations/query.ts new file mode 100644 index 0000000000000..9640725e3edd4 --- /dev/null +++ b/src/plugins/data/server/saved_objects/migrations/query.ts @@ -0,0 +1,42 @@ +/* + * 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 { mapValues } from 'lodash'; +import { SavedObject } from 'kibana/server'; +import { SavedQueryAttributes } from '../../../common'; +import { extract, getAllMigrations } from '../../../common/query/persistable_state'; +import { mergeMigrationFunctionMaps } from '../../../../kibana_utils/common'; + +const extractFilterReferences = (doc: SavedObject) => { + const { state: filters, references } = extract(doc.attributes.filters ?? []); + return { + ...doc, + attributes: { + ...doc.attributes, + filters, + }, + references, + }; +}; + +const filterMigrations = mapValues(getAllMigrations(), (migrate) => { + return (doc: SavedObject) => ({ + ...doc, + attributes: { + ...doc.attributes, + filters: migrate(doc.attributes.filters), + }, + }); +}); + +export const savedQueryMigrations = mergeMigrationFunctionMaps( + { + '7.16.0': extractFilterReferences, + }, + filterMigrations +); diff --git a/src/plugins/data/server/saved_objects/query.ts b/src/plugins/data/server/saved_objects/query.ts index 2e8b80cf3f080..604dd671cfd85 100644 --- a/src/plugins/data/server/saved_objects/query.ts +++ b/src/plugins/data/server/saved_objects/query.ts @@ -7,6 +7,7 @@ */ import { SavedObjectsType } from 'kibana/server'; +import { savedQueryMigrations } from './migrations/query'; export const querySavedObjectType: SavedObjectsType = { name: 'query', @@ -37,5 +38,5 @@ export const querySavedObjectType: SavedObjectsType = { timefilter: { type: 'object', enabled: false }, }, }, - migrations: {}, + migrations: savedQueryMigrations, }; diff --git a/src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx b/src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx index 350c46591c8b4..a04374e1500b4 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx +++ b/src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx @@ -190,7 +190,7 @@ export function DiscoverHistogram({ tooltip={tooltipProps} theme={chartTheme} baseTheme={chartBaseTheme} - allowBrushingLastHistogramBucket={true} + allowBrushingLastHistogramBin={true} /> + ), + description: ( + ), tabs, diff --git a/src/plugins/home/server/services/sample_data/lib/register_with_integrations.ts b/src/plugins/home/server/services/sample_data/lib/register_with_integrations.ts index e33cd58910fd6..d06dcacff18d9 100644 --- a/src/plugins/home/server/services/sample_data/lib/register_with_integrations.ts +++ b/src/plugins/home/server/services/sample_data/lib/register_with_integrations.ts @@ -22,7 +22,7 @@ export function registerSampleDatasetWithIntegration( defaultMessage: 'Sample Data', }), description: i18n.translate('home.sampleData.customIntegrationsDescription', { - defaultMessage: 'Add sample data and assets to Elasticsearch and Kibana.', + defaultMessage: 'Explore data in Kibana with these one-click data sets.', }), uiInternalPath: `${HOME_APP_BASE_PATH}#/tutorial_directory/sampleData`, isBeta: false, diff --git a/src/plugins/home/server/tutorials/activemq_logs/index.ts b/src/plugins/home/server/tutorials/activemq_logs/index.ts index 64a6fa575f5b6..a277b37838562 100644 --- a/src/plugins/home/server/tutorials/activemq_logs/index.ts +++ b/src/plugins/home/server/tutorials/activemq_logs/index.ts @@ -24,12 +24,12 @@ export function activemqLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'activemqLogs', name: i18n.translate('home.tutorials.activemqLogs.nameTitle', { - defaultMessage: 'ActiveMQ logs', + defaultMessage: 'ActiveMQ Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.activemqLogs.shortDescription', { - defaultMessage: 'Collect ActiveMQ logs with Filebeat.', + defaultMessage: 'Collect and parse logs from ActiveMQ instances with Filebeat.', }), longDescription: i18n.translate('home.tutorials.activemqLogs.longDescription', { defaultMessage: 'Collect ActiveMQ logs with Filebeat. \ diff --git a/src/plugins/home/server/tutorials/activemq_metrics/index.ts b/src/plugins/home/server/tutorials/activemq_metrics/index.ts index 7a59d6d4b70d1..9a001c149cda0 100644 --- a/src/plugins/home/server/tutorials/activemq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/activemq_metrics/index.ts @@ -23,16 +23,16 @@ export function activemqMetricsSpecProvider(context: TutorialContext): TutorialS return { id: 'activemqMetrics', name: i18n.translate('home.tutorials.activemqMetrics.nameTitle', { - defaultMessage: 'ActiveMQ metrics', + defaultMessage: 'ActiveMQ Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.activemqMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from ActiveMQ instances.', + defaultMessage: 'Collect metrics from ActiveMQ instances with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.activemqMetrics.longDescription', { defaultMessage: - 'The `activemq` Metricbeat module fetches monitoring metrics from ActiveMQ instances \ + 'The `activemq` Metricbeat module fetches metrics from ActiveMQ instances \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-activemq.html', diff --git a/src/plugins/home/server/tutorials/aerospike_metrics/index.ts b/src/plugins/home/server/tutorials/aerospike_metrics/index.ts index 75dd45272db69..3e574f2c75496 100644 --- a/src/plugins/home/server/tutorials/aerospike_metrics/index.ts +++ b/src/plugins/home/server/tutorials/aerospike_metrics/index.ts @@ -23,17 +23,17 @@ export function aerospikeMetricsSpecProvider(context: TutorialContext): Tutorial return { id: 'aerospikeMetrics', name: i18n.translate('home.tutorials.aerospikeMetrics.nameTitle', { - defaultMessage: 'Aerospike metrics', + defaultMessage: 'Aerospike Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.aerospikeMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the Aerospike server.', + defaultMessage: 'Collect metrics from Aerospike servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.aerospikeMetrics.longDescription', { defaultMessage: - 'The `aerospike` Metricbeat module fetches internal metrics from Aerospike. \ + 'The `aerospike` Metricbeat module fetches metrics from Aerospike. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-aerospike.html', diff --git a/src/plugins/home/server/tutorials/apache_logs/index.ts b/src/plugins/home/server/tutorials/apache_logs/index.ts index 8606a40fe0a23..6e588fd86588d 100644 --- a/src/plugins/home/server/tutorials/apache_logs/index.ts +++ b/src/plugins/home/server/tutorials/apache_logs/index.ts @@ -24,12 +24,12 @@ export function apacheLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'apacheLogs', name: i18n.translate('home.tutorials.apacheLogs.nameTitle', { - defaultMessage: 'Apache logs', + defaultMessage: 'Apache HTTP Server Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.apacheLogs.shortDescription', { - defaultMessage: 'Collect and parse access and error logs created by the Apache HTTP server.', + defaultMessage: 'Collect and parse logs from Apache HTTP servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.apacheLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/apache_metrics/index.ts b/src/plugins/home/server/tutorials/apache_metrics/index.ts index f013f3da737f0..17b495d1460c5 100644 --- a/src/plugins/home/server/tutorials/apache_metrics/index.ts +++ b/src/plugins/home/server/tutorials/apache_metrics/index.ts @@ -23,16 +23,16 @@ export function apacheMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: 'apacheMetrics', name: i18n.translate('home.tutorials.apacheMetrics.nameTitle', { - defaultMessage: 'Apache metrics', + defaultMessage: 'Apache HTTP Server Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.apacheMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the Apache 2 HTTP server.', + defaultMessage: 'Collect metrics from Apache HTTP servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.apacheMetrics.longDescription', { defaultMessage: - 'The `apache` Metricbeat module fetches internal metrics from the Apache 2 HTTP server. \ + 'The `apache` Metricbeat module fetches metrics from Apache 2 HTTP server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-apache.html', diff --git a/src/plugins/home/server/tutorials/auditbeat/index.ts b/src/plugins/home/server/tutorials/auditbeat/index.ts index 8bd6450b1daa4..96e5d4bcda393 100644 --- a/src/plugins/home/server/tutorials/auditbeat/index.ts +++ b/src/plugins/home/server/tutorials/auditbeat/index.ts @@ -24,12 +24,12 @@ export function auditbeatSpecProvider(context: TutorialContext): TutorialSchema return { id: 'auditbeat', name: i18n.translate('home.tutorials.auditbeat.nameTitle', { - defaultMessage: 'Auditbeat', + defaultMessage: 'Auditbeat Events', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.auditbeat.shortDescription', { - defaultMessage: 'Collect audit data from your hosts.', + defaultMessage: 'Collect events from your servers with Auditbeat.', }), longDescription: i18n.translate('home.tutorials.auditbeat.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/auditd_logs/index.ts b/src/plugins/home/server/tutorials/auditd_logs/index.ts index a0d6f5f683e2c..6993196d93417 100644 --- a/src/plugins/home/server/tutorials/auditd_logs/index.ts +++ b/src/plugins/home/server/tutorials/auditd_logs/index.ts @@ -24,16 +24,16 @@ export function auditdLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'auditdLogs', name: i18n.translate('home.tutorials.auditdLogs.nameTitle', { - defaultMessage: 'Auditd logs', + defaultMessage: 'Auditd Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.auditdLogs.shortDescription', { - defaultMessage: 'Collect logs from the Linux auditd daemon.', + defaultMessage: 'Collect and parse logs from Linux audit daemon with Filebeat.', }), longDescription: i18n.translate('home.tutorials.auditdLogs.longDescription', { defaultMessage: - 'The module collects and parses logs from the audit daemon ( `auditd`). \ + 'The module collects and parses logs from audit daemon ( `auditd`). \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-auditd.html', diff --git a/src/plugins/home/server/tutorials/aws_logs/index.ts b/src/plugins/home/server/tutorials/aws_logs/index.ts index 3458800b33f0a..62fbcc4eebc18 100644 --- a/src/plugins/home/server/tutorials/aws_logs/index.ts +++ b/src/plugins/home/server/tutorials/aws_logs/index.ts @@ -24,12 +24,12 @@ export function awsLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'awsLogs', name: i18n.translate('home.tutorials.awsLogs.nameTitle', { - defaultMessage: 'AWS S3 based logs', + defaultMessage: 'AWS S3 based Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.awsLogs.shortDescription', { - defaultMessage: 'Collect AWS logs from S3 bucket with Filebeat.', + defaultMessage: 'Collect and parse logs from AWS S3 buckets with Filebeat.', }), longDescription: i18n.translate('home.tutorials.awsLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/aws_metrics/index.ts b/src/plugins/home/server/tutorials/aws_metrics/index.ts index 7c3a15a47d784..6bf1bf64bff9f 100644 --- a/src/plugins/home/server/tutorials/aws_metrics/index.ts +++ b/src/plugins/home/server/tutorials/aws_metrics/index.ts @@ -23,17 +23,17 @@ export function awsMetricsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'awsMetrics', name: i18n.translate('home.tutorials.awsMetrics.nameTitle', { - defaultMessage: 'AWS metrics', + defaultMessage: 'AWS Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.awsMetrics.shortDescription', { defaultMessage: - 'Fetch monitoring metrics for EC2 instances from the AWS APIs and Cloudwatch.', + 'Collect metrics for EC2 instances from AWS APIs and Cloudwatch with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.awsMetrics.longDescription', { defaultMessage: - 'The `aws` Metricbeat module fetches monitoring metrics from the AWS APIs and Cloudwatch. \ + 'The `aws` Metricbeat module fetches metrics from AWS APIs and Cloudwatch. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-aws.html', diff --git a/src/plugins/home/server/tutorials/azure_logs/index.ts b/src/plugins/home/server/tutorials/azure_logs/index.ts index 2bf1527a79c40..3c9438d9a6298 100644 --- a/src/plugins/home/server/tutorials/azure_logs/index.ts +++ b/src/plugins/home/server/tutorials/azure_logs/index.ts @@ -24,13 +24,13 @@ export function azureLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'azureLogs', name: i18n.translate('home.tutorials.azureLogs.nameTitle', { - defaultMessage: 'Azure logs', + defaultMessage: 'Azure Logs', }), moduleName, isBeta: true, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.azureLogs.shortDescription', { - defaultMessage: 'Collects Azure activity and audit related logs.', + defaultMessage: 'Collect and parse logs from Azure with Filebeat.', }), longDescription: i18n.translate('home.tutorials.azureLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/azure_metrics/index.ts b/src/plugins/home/server/tutorials/azure_metrics/index.ts index 4a6112510b333..310f954104634 100644 --- a/src/plugins/home/server/tutorials/azure_metrics/index.ts +++ b/src/plugins/home/server/tutorials/azure_metrics/index.ts @@ -23,13 +23,13 @@ export function azureMetricsSpecProvider(context: TutorialContext): TutorialSche return { id: 'azureMetrics', name: i18n.translate('home.tutorials.azureMetrics.nameTitle', { - defaultMessage: 'Azure metrics', + defaultMessage: 'Azure Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.azureMetrics.shortDescription', { - defaultMessage: 'Fetch Azure Monitor metrics.', + defaultMessage: 'Collect metrics from Azure with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.azureMetrics.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/barracuda_logs/index.ts b/src/plugins/home/server/tutorials/barracuda_logs/index.ts index 35ce10e00892e..cdfd75b9728b9 100644 --- a/src/plugins/home/server/tutorials/barracuda_logs/index.ts +++ b/src/plugins/home/server/tutorials/barracuda_logs/index.ts @@ -24,12 +24,13 @@ export function barracudaLogsSpecProvider(context: TutorialContext): TutorialSch return { id: 'barracudaLogs', name: i18n.translate('home.tutorials.barracudaLogs.nameTitle', { - defaultMessage: 'Barracuda logs', + defaultMessage: 'Barracuda Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.barracudaLogs.shortDescription', { - defaultMessage: 'Collect Barracuda Web Application Firewall logs over syslog or from a file.', + defaultMessage: + 'Collect and parse logs from Barracuda Web Application Firewall with Filebeat.', }), longDescription: i18n.translate('home.tutorials.barracudaLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/bluecoat_logs/index.ts b/src/plugins/home/server/tutorials/bluecoat_logs/index.ts index 85c7dff85d3e6..a7db5b04ee40d 100644 --- a/src/plugins/home/server/tutorials/bluecoat_logs/index.ts +++ b/src/plugins/home/server/tutorials/bluecoat_logs/index.ts @@ -24,12 +24,12 @@ export function bluecoatLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'bluecoatLogs', name: i18n.translate('home.tutorials.bluecoatLogs.nameTitle', { - defaultMessage: 'Bluecoat logs', + defaultMessage: 'Bluecoat Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.bluecoatLogs.shortDescription', { - defaultMessage: 'Collect Blue Coat Director logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Blue Coat Director with Filebeat.', }), longDescription: i18n.translate('home.tutorials.bluecoatLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/cef_logs/index.ts b/src/plugins/home/server/tutorials/cef_logs/index.ts index cfd267f661d2a..1366198d610d7 100644 --- a/src/plugins/home/server/tutorials/cef_logs/index.ts +++ b/src/plugins/home/server/tutorials/cef_logs/index.ts @@ -24,12 +24,12 @@ export function cefLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'cefLogs', name: i18n.translate('home.tutorials.cefLogs.nameTitle', { - defaultMessage: 'CEF logs', + defaultMessage: 'CEF Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.cefLogs.shortDescription', { - defaultMessage: 'Collect Common Event Format (CEF) log data over syslog.', + defaultMessage: 'Collect and parse logs from Common Event Format (CEF) with Filebeat.', }), longDescription: i18n.translate('home.tutorials.cefLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/ceph_metrics/index.ts b/src/plugins/home/server/tutorials/ceph_metrics/index.ts index 821067d87c905..6a53789d26f7c 100644 --- a/src/plugins/home/server/tutorials/ceph_metrics/index.ts +++ b/src/plugins/home/server/tutorials/ceph_metrics/index.ts @@ -23,17 +23,17 @@ export function cephMetricsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'cephMetrics', name: i18n.translate('home.tutorials.cephMetrics.nameTitle', { - defaultMessage: 'Ceph metrics', + defaultMessage: 'Ceph Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.cephMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the Ceph server.', + defaultMessage: 'Collect metrics from Ceph servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.cephMetrics.longDescription', { defaultMessage: - 'The `ceph` Metricbeat module fetches internal metrics from Ceph. \ + 'The `ceph` Metricbeat module fetches metrics from Ceph. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-ceph.html', diff --git a/src/plugins/home/server/tutorials/checkpoint_logs/index.ts b/src/plugins/home/server/tutorials/checkpoint_logs/index.ts index 9c0d5591ae35b..b5ea6be42403b 100644 --- a/src/plugins/home/server/tutorials/checkpoint_logs/index.ts +++ b/src/plugins/home/server/tutorials/checkpoint_logs/index.ts @@ -24,12 +24,12 @@ export function checkpointLogsSpecProvider(context: TutorialContext): TutorialSc return { id: 'checkpointLogs', name: i18n.translate('home.tutorials.checkpointLogs.nameTitle', { - defaultMessage: 'Check Point logs', + defaultMessage: 'Check Point Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.checkpointLogs.shortDescription', { - defaultMessage: 'Collect Check Point firewall logs.', + defaultMessage: 'Collect and parse logs from Check Point firewalls with Filebeat.', }), longDescription: i18n.translate('home.tutorials.checkpointLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/cisco_logs/index.ts b/src/plugins/home/server/tutorials/cisco_logs/index.ts index 50b79f448b316..922cfbf1e23ee 100644 --- a/src/plugins/home/server/tutorials/cisco_logs/index.ts +++ b/src/plugins/home/server/tutorials/cisco_logs/index.ts @@ -24,12 +24,12 @@ export function ciscoLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'ciscoLogs', name: i18n.translate('home.tutorials.ciscoLogs.nameTitle', { - defaultMessage: 'Cisco logs', + defaultMessage: 'Cisco Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.ciscoLogs.shortDescription', { - defaultMessage: 'Collect Cisco network device logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Cisco network devices with Filebeat.', }), longDescription: i18n.translate('home.tutorials.ciscoLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts index cf0c27ed9be73..5564d11be4d19 100644 --- a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts +++ b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts @@ -23,12 +23,12 @@ export function cloudwatchLogsSpecProvider(context: TutorialContext): TutorialSc return { id: 'cloudwatchLogs', name: i18n.translate('home.tutorials.cloudwatchLogs.nameTitle', { - defaultMessage: 'AWS Cloudwatch logs', + defaultMessage: 'AWS Cloudwatch Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.cloudwatchLogs.shortDescription', { - defaultMessage: 'Collect Cloudwatch logs with Functionbeat.', + defaultMessage: 'Collect and parse logs from AWS Cloudwatch with Functionbeat.', }), longDescription: i18n.translate('home.tutorials.cloudwatchLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts b/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts index e43d05a0a098f..535c8aaa90768 100644 --- a/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts @@ -23,16 +23,16 @@ export function cockroachdbMetricsSpecProvider(context: TutorialContext): Tutori return { id: 'cockroachdbMetrics', name: i18n.translate('home.tutorials.cockroachdbMetrics.nameTitle', { - defaultMessage: 'CockroachDB metrics', + defaultMessage: 'CockroachDB Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.cockroachdbMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from the CockroachDB server.', + defaultMessage: 'Collect metrics from CockroachDB servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.cockroachdbMetrics.longDescription', { defaultMessage: - 'The `cockroachdb` Metricbeat module fetches monitoring metrics from CockroachDB. \ + 'The `cockroachdb` Metricbeat module fetches metrics from CockroachDB. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-cockroachdb.html', diff --git a/src/plugins/home/server/tutorials/consul_metrics/index.ts b/src/plugins/home/server/tutorials/consul_metrics/index.ts index 915920db5882c..ca7179d55fd89 100644 --- a/src/plugins/home/server/tutorials/consul_metrics/index.ts +++ b/src/plugins/home/server/tutorials/consul_metrics/index.ts @@ -23,16 +23,16 @@ export function consulMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: 'consulMetrics', name: i18n.translate('home.tutorials.consulMetrics.nameTitle', { - defaultMessage: 'Consul metrics', + defaultMessage: 'Consul Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.consulMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from the Consul server.', + defaultMessage: 'Collect metrics from Consul servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.consulMetrics.longDescription', { defaultMessage: - 'The `consul` Metricbeat module fetches monitoring metrics from Consul. \ + 'The `consul` Metricbeat module fetches metrics from Consul. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-consul.html', diff --git a/src/plugins/home/server/tutorials/coredns_logs/index.ts b/src/plugins/home/server/tutorials/coredns_logs/index.ts index 298464651f7fc..1261c67135001 100644 --- a/src/plugins/home/server/tutorials/coredns_logs/index.ts +++ b/src/plugins/home/server/tutorials/coredns_logs/index.ts @@ -24,12 +24,12 @@ export function corednsLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'corednsLogs', name: i18n.translate('home.tutorials.corednsLogs.nameTitle', { - defaultMessage: 'CoreDNS logs', + defaultMessage: 'CoreDNS Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.corednsLogs.shortDescription', { - defaultMessage: 'Collect CoreDNS logs.', + defaultMessage: 'Collect and parse logs from CoreDNS servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.corednsLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/coredns_metrics/index.ts b/src/plugins/home/server/tutorials/coredns_metrics/index.ts index 34912efb31a81..3abc14314a6ba 100644 --- a/src/plugins/home/server/tutorials/coredns_metrics/index.ts +++ b/src/plugins/home/server/tutorials/coredns_metrics/index.ts @@ -23,16 +23,16 @@ export function corednsMetricsSpecProvider(context: TutorialContext): TutorialSc return { id: 'corednsMetrics', name: i18n.translate('home.tutorials.corednsMetrics.nameTitle', { - defaultMessage: 'CoreDNS metrics', + defaultMessage: 'CoreDNS Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.corednsMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from the CoreDNS server.', + defaultMessage: 'Collect metrics from CoreDNS servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.corednsMetrics.longDescription', { defaultMessage: - 'The `coredns` Metricbeat module fetches monitoring metrics from CoreDNS. \ + 'The `coredns` Metricbeat module fetches metrics from CoreDNS. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-coredns.html', diff --git a/src/plugins/home/server/tutorials/couchbase_metrics/index.ts b/src/plugins/home/server/tutorials/couchbase_metrics/index.ts index 1860991fd17b2..5c29aa2d9a524 100644 --- a/src/plugins/home/server/tutorials/couchbase_metrics/index.ts +++ b/src/plugins/home/server/tutorials/couchbase_metrics/index.ts @@ -23,17 +23,17 @@ export function couchbaseMetricsSpecProvider(context: TutorialContext): Tutorial return { id: 'couchbaseMetrics', name: i18n.translate('home.tutorials.couchbaseMetrics.nameTitle', { - defaultMessage: 'Couchbase metrics', + defaultMessage: 'Couchbase Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.couchbaseMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from Couchbase.', + defaultMessage: 'Collect metrics from Couchbase databases with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.couchbaseMetrics.longDescription', { defaultMessage: - 'The `couchbase` Metricbeat module fetches internal metrics from Couchbase. \ + 'The `couchbase` Metricbeat module fetches metrics from Couchbase. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-couchbase.html', diff --git a/src/plugins/home/server/tutorials/couchdb_metrics/index.ts b/src/plugins/home/server/tutorials/couchdb_metrics/index.ts index a6c57f56cf2e1..00bea11d13d99 100644 --- a/src/plugins/home/server/tutorials/couchdb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/couchdb_metrics/index.ts @@ -23,16 +23,16 @@ export function couchdbMetricsSpecProvider(context: TutorialContext): TutorialSc return { id: 'couchdbMetrics', name: i18n.translate('home.tutorials.couchdbMetrics.nameTitle', { - defaultMessage: 'CouchDB metrics', + defaultMessage: 'CouchDB Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.couchdbMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from the CouchdB server.', + defaultMessage: 'Collect metrics from CouchDB servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.couchdbMetrics.longDescription', { defaultMessage: - 'The `couchdb` Metricbeat module fetches monitoring metrics from CouchDB. \ + 'The `couchdb` Metricbeat module fetches metrics from CouchDB. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-couchdb.html', diff --git a/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts b/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts index baaaef50a641f..a48ed4288210b 100644 --- a/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts +++ b/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts @@ -24,12 +24,13 @@ export function crowdstrikeLogsSpecProvider(context: TutorialContext): TutorialS return { id: 'crowdstrikeLogs', name: i18n.translate('home.tutorials.crowdstrikeLogs.nameTitle', { - defaultMessage: 'CrowdStrike logs', + defaultMessage: 'CrowdStrike Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.crowdstrikeLogs.shortDescription', { - defaultMessage: 'Collect CrowdStrike Falcon logs using the Falcon SIEM Connector.', + defaultMessage: + 'Collect and parse logs from CrowdStrike Falcon using the Falcon SIEM Connector with Filebeat.', }), longDescription: i18n.translate('home.tutorials.crowdstrikeLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/cylance_logs/index.ts b/src/plugins/home/server/tutorials/cylance_logs/index.ts index 9766f417b8870..64b79a41cd2e0 100644 --- a/src/plugins/home/server/tutorials/cylance_logs/index.ts +++ b/src/plugins/home/server/tutorials/cylance_logs/index.ts @@ -24,12 +24,12 @@ export function cylanceLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'cylanceLogs', name: i18n.translate('home.tutorials.cylanceLogs.nameTitle', { - defaultMessage: 'CylancePROTECT logs', + defaultMessage: 'CylancePROTECT Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.cylanceLogs.shortDescription', { - defaultMessage: 'Collect CylancePROTECT logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from CylancePROTECT with Filebeat.', }), longDescription: i18n.translate('home.tutorials.cylanceLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/docker_metrics/index.ts b/src/plugins/home/server/tutorials/docker_metrics/index.ts index 6a8687ef5d66e..ab80e6d644dbc 100644 --- a/src/plugins/home/server/tutorials/docker_metrics/index.ts +++ b/src/plugins/home/server/tutorials/docker_metrics/index.ts @@ -23,16 +23,16 @@ export function dockerMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: 'dockerMetrics', name: i18n.translate('home.tutorials.dockerMetrics.nameTitle', { - defaultMessage: 'Docker metrics', + defaultMessage: 'Docker Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.dockerMetrics.shortDescription', { - defaultMessage: 'Fetch metrics about your Docker containers.', + defaultMessage: 'Collect metrics from Docker containers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.dockerMetrics.longDescription', { defaultMessage: - 'The `docker` Metricbeat module fetches metrics from the Docker server. \ + 'The `docker` Metricbeat module fetches metrics from Docker server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-docker.html', diff --git a/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts b/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts index 86be26dd12ca7..9864d376966bb 100644 --- a/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts +++ b/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts @@ -23,17 +23,17 @@ export function dropwizardMetricsSpecProvider(context: TutorialContext): Tutoria return { id: 'dropwizardMetrics', name: i18n.translate('home.tutorials.dropwizardMetrics.nameTitle', { - defaultMessage: 'Dropwizard metrics', + defaultMessage: 'Dropwizard Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.dropwizardMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from Dropwizard Java application.', + defaultMessage: 'Collect metrics from Dropwizard Java applciations with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.dropwizardMetrics.longDescription', { defaultMessage: - 'The `dropwizard` Metricbeat module fetches internal metrics from Dropwizard Java Application. \ + 'The `dropwizard` Metricbeat module fetches metrics from Dropwizard Java Application. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-dropwizard.html', diff --git a/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts b/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts index 1886a912fdcd2..6415781d02c06 100644 --- a/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts +++ b/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts @@ -24,13 +24,13 @@ export function elasticsearchLogsSpecProvider(context: TutorialContext): Tutoria return { id: 'elasticsearchLogs', name: i18n.translate('home.tutorials.elasticsearchLogs.nameTitle', { - defaultMessage: 'Elasticsearch logs', + defaultMessage: 'Elasticsearch Logs', }), moduleName, category: TutorialsCategory.LOGGING, isBeta: true, shortDescription: i18n.translate('home.tutorials.elasticsearchLogs.shortDescription', { - defaultMessage: 'Collect and parse logs created by Elasticsearch.', + defaultMessage: 'Collect and parse logs from Elasticsearch clusters with Filebeat.', }), longDescription: i18n.translate('home.tutorials.elasticsearchLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts b/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts index 2adc2fd90fa70..3961d7f78c86c 100644 --- a/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts +++ b/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts @@ -23,17 +23,17 @@ export function elasticsearchMetricsSpecProvider(context: TutorialContext): Tuto return { id: 'elasticsearchMetrics', name: i18n.translate('home.tutorials.elasticsearchMetrics.nameTitle', { - defaultMessage: 'Elasticsearch metrics', + defaultMessage: 'Elasticsearch Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.elasticsearchMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from Elasticsearch.', + defaultMessage: 'Collect metrics from Elasticsearch clusters with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.elasticsearchMetrics.longDescription', { defaultMessage: - 'The `elasticsearch` Metricbeat module fetches internal metrics from Elasticsearch. \ + 'The `elasticsearch` Metricbeat module fetches metrics from Elasticsearch. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-elasticsearch.html', diff --git a/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts b/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts index fda69a2467b25..55c85a5bdd2a4 100644 --- a/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts +++ b/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts @@ -24,12 +24,12 @@ export function envoyproxyLogsSpecProvider(context: TutorialContext): TutorialSc return { id: 'envoyproxyLogs', name: i18n.translate('home.tutorials.envoyproxyLogs.nameTitle', { - defaultMessage: 'Envoy Proxy logs', + defaultMessage: 'Envoy Proxy Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.envoyproxyLogs.shortDescription', { - defaultMessage: 'Collect Envoy Proxy logs.', + defaultMessage: 'Collect and parse logs from Envoy Proxy with Filebeat.', }), longDescription: i18n.translate('home.tutorials.envoyproxyLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts b/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts index 263d1a2036fd0..e2f3b84739685 100644 --- a/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts +++ b/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts @@ -23,16 +23,16 @@ export function envoyproxyMetricsSpecProvider(context: TutorialContext): Tutoria return { id: 'envoyproxyMetrics', name: i18n.translate('home.tutorials.envoyproxyMetrics.nameTitle', { - defaultMessage: 'Envoy Proxy metrics', + defaultMessage: 'Envoy Proxy Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.envoyproxyMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from Envoy Proxy.', + defaultMessage: 'Collect metrics from Envoy Proxy with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.envoyproxyMetrics.longDescription', { defaultMessage: - 'The `envoyproxy` Metricbeat module fetches monitoring metrics from Envoy Proxy. \ + 'The `envoyproxy` Metricbeat module fetches metrics from Envoy Proxy. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-envoyproxy.html', diff --git a/src/plugins/home/server/tutorials/etcd_metrics/index.ts b/src/plugins/home/server/tutorials/etcd_metrics/index.ts index cda16ecf68e34..9ed153c21c257 100644 --- a/src/plugins/home/server/tutorials/etcd_metrics/index.ts +++ b/src/plugins/home/server/tutorials/etcd_metrics/index.ts @@ -23,17 +23,17 @@ export function etcdMetricsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'etcdMetrics', name: i18n.translate('home.tutorials.etcdMetrics.nameTitle', { - defaultMessage: 'Etcd metrics', + defaultMessage: 'Etcd Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.etcdMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the Etcd server.', + defaultMessage: 'Collect metrics from Etcd servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.etcdMetrics.longDescription', { defaultMessage: - 'The `etcd` Metricbeat module fetches internal metrics from Etcd. \ + 'The `etcd` Metricbeat module fetches metrics from Etcd. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-etcd.html', diff --git a/src/plugins/home/server/tutorials/f5_logs/index.ts b/src/plugins/home/server/tutorials/f5_logs/index.ts index ebcdd4ece7f45..a407d1d3d5142 100644 --- a/src/plugins/home/server/tutorials/f5_logs/index.ts +++ b/src/plugins/home/server/tutorials/f5_logs/index.ts @@ -24,12 +24,12 @@ export function f5LogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'f5Logs', name: i18n.translate('home.tutorials.f5Logs.nameTitle', { - defaultMessage: 'F5 logs', + defaultMessage: 'F5 Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.f5Logs.shortDescription', { - defaultMessage: 'Collect F5 Big-IP Access Policy Manager logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from F5 Big-IP Access Policy Manager with Filebeat.', }), longDescription: i18n.translate('home.tutorials.f5Logs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/fortinet_logs/index.ts b/src/plugins/home/server/tutorials/fortinet_logs/index.ts index 3e7923b680c6e..2f6af3ba47280 100644 --- a/src/plugins/home/server/tutorials/fortinet_logs/index.ts +++ b/src/plugins/home/server/tutorials/fortinet_logs/index.ts @@ -24,12 +24,12 @@ export function fortinetLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'fortinetLogs', name: i18n.translate('home.tutorials.fortinetLogs.nameTitle', { - defaultMessage: 'Fortinet logs', + defaultMessage: 'Fortinet Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.fortinetLogs.shortDescription', { - defaultMessage: 'Collect Fortinet FortiOS logs over syslog.', + defaultMessage: 'Collect and parse logs from Fortinet FortiOS with Filebeat.', }), longDescription: i18n.translate('home.tutorials.fortinetLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/gcp_logs/index.ts b/src/plugins/home/server/tutorials/gcp_logs/index.ts index feef7d673c5d9..23d8e3364eb69 100644 --- a/src/plugins/home/server/tutorials/gcp_logs/index.ts +++ b/src/plugins/home/server/tutorials/gcp_logs/index.ts @@ -24,12 +24,12 @@ export function gcpLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'gcpLogs', name: i18n.translate('home.tutorials.gcpLogs.nameTitle', { - defaultMessage: 'Google Cloud logs', + defaultMessage: 'Google Cloud Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.gcpLogs.shortDescription', { - defaultMessage: 'Collect Google Cloud audit, firewall, and VPC flow logs.', + defaultMessage: 'Collect and parse logs from Google Cloud Platform with Filebeat.', }), longDescription: i18n.translate('home.tutorials.gcpLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/gcp_metrics/index.ts b/src/plugins/home/server/tutorials/gcp_metrics/index.ts index 5f198ed5f3cf2..7f397c1e1be7b 100644 --- a/src/plugins/home/server/tutorials/gcp_metrics/index.ts +++ b/src/plugins/home/server/tutorials/gcp_metrics/index.ts @@ -23,17 +23,16 @@ export function gcpMetricsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'gcpMetrics', name: i18n.translate('home.tutorials.gcpMetrics.nameTitle', { - defaultMessage: 'Google Cloud metrics', + defaultMessage: 'Google Cloud Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.gcpMetrics.shortDescription', { - defaultMessage: - 'Fetch monitoring metrics from Google Cloud Platform using Stackdriver Monitoring API.', + defaultMessage: 'Collect metrics from Google Cloud Platform with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.gcpMetrics.longDescription', { defaultMessage: - 'The `gcp` Metricbeat module fetches monitoring metrics from Google Cloud Platform using Stackdriver Monitoring API. \ + 'The `gcp` Metricbeat module fetches metrics from Google Cloud Platform using Stackdriver Monitoring API. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-gcp.html', diff --git a/src/plugins/home/server/tutorials/golang_metrics/index.ts b/src/plugins/home/server/tutorials/golang_metrics/index.ts index 85937e0dda0e0..50d09e42e8791 100644 --- a/src/plugins/home/server/tutorials/golang_metrics/index.ts +++ b/src/plugins/home/server/tutorials/golang_metrics/index.ts @@ -23,17 +23,17 @@ export function golangMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: moduleName + 'Metrics', name: i18n.translate('home.tutorials.golangMetrics.nameTitle', { - defaultMessage: 'Golang metrics', + defaultMessage: 'Golang Metrics', }), moduleName, isBeta: true, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.golangMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from a Golang app.', + defaultMessage: 'Collect metrics from Golang applications with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.golangMetrics.longDescription', { defaultMessage: - 'The `{moduleName}` Metricbeat module fetches internal metrics from a Golang app. \ + 'The `{moduleName}` Metricbeat module fetches metrics from a Golang app. \ [Learn more]({learnMoreLink}).', values: { moduleName, diff --git a/src/plugins/home/server/tutorials/gsuite_logs/index.ts b/src/plugins/home/server/tutorials/gsuite_logs/index.ts index 4d23c6b1cfdce..718558321cf78 100644 --- a/src/plugins/home/server/tutorials/gsuite_logs/index.ts +++ b/src/plugins/home/server/tutorials/gsuite_logs/index.ts @@ -24,16 +24,16 @@ export function gsuiteLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'gsuiteLogs', name: i18n.translate('home.tutorials.gsuiteLogs.nameTitle', { - defaultMessage: 'GSuite logs', + defaultMessage: 'GSuite Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.gsuiteLogs.shortDescription', { - defaultMessage: 'Collect GSuite activity reports.', + defaultMessage: 'Collect and parse activity reports from GSuite with Filebeat.', }), longDescription: i18n.translate('home.tutorials.gsuiteLogs.longDescription', { defaultMessage: - 'This is a module for ingesting data from the different GSuite audit reports APIs. \ + 'This is a module for ingesting data from different GSuite audit reports APIs. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-gsuite.html', diff --git a/src/plugins/home/server/tutorials/haproxy_logs/index.ts b/src/plugins/home/server/tutorials/haproxy_logs/index.ts index 0b0fd35f07058..c3765317ecbe0 100644 --- a/src/plugins/home/server/tutorials/haproxy_logs/index.ts +++ b/src/plugins/home/server/tutorials/haproxy_logs/index.ts @@ -24,12 +24,12 @@ export function haproxyLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'haproxyLogs', name: i18n.translate('home.tutorials.haproxyLogs.nameTitle', { - defaultMessage: 'HAProxy logs', + defaultMessage: 'HAProxy Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.haproxyLogs.shortDescription', { - defaultMessage: 'Collect HAProxy logs.', + defaultMessage: 'Collect and parse logs from HAProxy servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.haproxyLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/haproxy_metrics/index.ts b/src/plugins/home/server/tutorials/haproxy_metrics/index.ts index e37f0ffc4b916..49f1d32dc4c82 100644 --- a/src/plugins/home/server/tutorials/haproxy_metrics/index.ts +++ b/src/plugins/home/server/tutorials/haproxy_metrics/index.ts @@ -23,17 +23,17 @@ export function haproxyMetricsSpecProvider(context: TutorialContext): TutorialSc return { id: 'haproxyMetrics', name: i18n.translate('home.tutorials.haproxyMetrics.nameTitle', { - defaultMessage: 'HAProxy metrics', + defaultMessage: 'HAProxy Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.haproxyMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the HAProxy server.', + defaultMessage: 'Collect metrics from HAProxy servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.haproxyMetrics.longDescription', { defaultMessage: - 'The `haproxy` Metricbeat module fetches internal metrics from HAProxy. \ + 'The `haproxy` Metricbeat module fetches metrics from HAProxy. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-haproxy.html', diff --git a/src/plugins/home/server/tutorials/ibmmq_logs/index.ts b/src/plugins/home/server/tutorials/ibmmq_logs/index.ts index 646747d1a49f8..21b60a9ab5a5c 100644 --- a/src/plugins/home/server/tutorials/ibmmq_logs/index.ts +++ b/src/plugins/home/server/tutorials/ibmmq_logs/index.ts @@ -24,12 +24,12 @@ export function ibmmqLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'ibmmqLogs', name: i18n.translate('home.tutorials.ibmmqLogs.nameTitle', { - defaultMessage: 'IBM MQ logs', + defaultMessage: 'IBM MQ Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.ibmmqLogs.shortDescription', { - defaultMessage: 'Collect IBM MQ logs with Filebeat.', + defaultMessage: 'Collect and parse logs from IBM MQ with Filebeat.', }), longDescription: i18n.translate('home.tutorials.ibmmqLogs.longDescription', { defaultMessage: 'Collect IBM MQ logs with Filebeat. \ diff --git a/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts b/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts index 3862bd9ca85eb..706003f0eab48 100644 --- a/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts @@ -23,16 +23,16 @@ export function ibmmqMetricsSpecProvider(context: TutorialContext): TutorialSche return { id: 'ibmmqMetrics', name: i18n.translate('home.tutorials.ibmmqMetrics.nameTitle', { - defaultMessage: 'IBM MQ metrics', + defaultMessage: 'IBM MQ Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.ibmmqMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from IBM MQ instances.', + defaultMessage: 'Collect metrics from IBM MQ instances with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.ibmmqMetrics.longDescription', { defaultMessage: - 'The `ibmmq` Metricbeat module fetches monitoring metrics from IBM MQ instances \ + 'The `ibmmq` Metricbeat module fetches metrics from IBM MQ instances \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-ibmmq.html', diff --git a/src/plugins/home/server/tutorials/icinga_logs/index.ts b/src/plugins/home/server/tutorials/icinga_logs/index.ts index 0dae93b70343b..dc730022262c2 100644 --- a/src/plugins/home/server/tutorials/icinga_logs/index.ts +++ b/src/plugins/home/server/tutorials/icinga_logs/index.ts @@ -24,12 +24,12 @@ export function icingaLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'icingaLogs', name: i18n.translate('home.tutorials.icingaLogs.nameTitle', { - defaultMessage: 'Icinga logs', + defaultMessage: 'Icinga Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.icingaLogs.shortDescription', { - defaultMessage: 'Collect Icinga main, debug, and startup logs.', + defaultMessage: 'Collect and parse main, debug, and startup logs from Icinga with Filebeat.', }), longDescription: i18n.translate('home.tutorials.icingaLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/iis_logs/index.ts b/src/plugins/home/server/tutorials/iis_logs/index.ts index 5393edf6ab148..0dbc5bbdc75b8 100644 --- a/src/plugins/home/server/tutorials/iis_logs/index.ts +++ b/src/plugins/home/server/tutorials/iis_logs/index.ts @@ -24,12 +24,13 @@ export function iisLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'iisLogs', name: i18n.translate('home.tutorials.iisLogs.nameTitle', { - defaultMessage: 'IIS logs', + defaultMessage: 'IIS Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.iisLogs.shortDescription', { - defaultMessage: 'Collect and parse access and error logs created by the IIS HTTP server.', + defaultMessage: + 'Collect and parse access and error logs from IIS HTTP servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.iisLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/iis_metrics/index.ts b/src/plugins/home/server/tutorials/iis_metrics/index.ts index dbfa474dc9c89..d57e4688ba753 100644 --- a/src/plugins/home/server/tutorials/iis_metrics/index.ts +++ b/src/plugins/home/server/tutorials/iis_metrics/index.ts @@ -28,7 +28,7 @@ export function iisMetricsSpecProvider(context: TutorialContext): TutorialSchema moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.iisMetrics.shortDescription', { - defaultMessage: 'Collect IIS server related metrics.', + defaultMessage: 'Collect metrics from IIS HTTP servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.iisMetrics.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/imperva_logs/index.ts b/src/plugins/home/server/tutorials/imperva_logs/index.ts index 71c3af3809e2e..1cbe707f813ee 100644 --- a/src/plugins/home/server/tutorials/imperva_logs/index.ts +++ b/src/plugins/home/server/tutorials/imperva_logs/index.ts @@ -24,12 +24,12 @@ export function impervaLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'impervaLogs', name: i18n.translate('home.tutorials.impervaLogs.nameTitle', { - defaultMessage: 'Imperva logs', + defaultMessage: 'Imperva Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.impervaLogs.shortDescription', { - defaultMessage: 'Collect Imperva SecureSphere logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Imperva SecureSphere with Filebeat.', }), longDescription: i18n.translate('home.tutorials.impervaLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/infoblox_logs/index.ts b/src/plugins/home/server/tutorials/infoblox_logs/index.ts index 5329444dfa85f..8dce2bf00b2e2 100644 --- a/src/plugins/home/server/tutorials/infoblox_logs/index.ts +++ b/src/plugins/home/server/tutorials/infoblox_logs/index.ts @@ -24,12 +24,12 @@ export function infobloxLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'infobloxLogs', name: i18n.translate('home.tutorials.infobloxLogs.nameTitle', { - defaultMessage: 'Infoblox logs', + defaultMessage: 'Infoblox Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.infobloxLogs.shortDescription', { - defaultMessage: 'Collect Infoblox NIOS logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Infoblox NIOS with Filebeat.', }), longDescription: i18n.translate('home.tutorials.infobloxLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/iptables_logs/index.ts b/src/plugins/home/server/tutorials/iptables_logs/index.ts index 85faf169f8714..6d298e88a2dfb 100644 --- a/src/plugins/home/server/tutorials/iptables_logs/index.ts +++ b/src/plugins/home/server/tutorials/iptables_logs/index.ts @@ -24,12 +24,12 @@ export function iptablesLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'iptablesLogs', name: i18n.translate('home.tutorials.iptablesLogs.nameTitle', { - defaultMessage: 'Iptables logs', + defaultMessage: 'Iptables Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.iptablesLogs.shortDescription', { - defaultMessage: 'Collect iptables and ip6tables logs.', + defaultMessage: 'Collect and parse logs from iptables and ip6tables with Filebeat.', }), longDescription: i18n.translate('home.tutorials.iptablesLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/juniper_logs/index.ts b/src/plugins/home/server/tutorials/juniper_logs/index.ts index f9174d8a089e0..7430e4705a5f4 100644 --- a/src/plugins/home/server/tutorials/juniper_logs/index.ts +++ b/src/plugins/home/server/tutorials/juniper_logs/index.ts @@ -29,7 +29,7 @@ export function juniperLogsSpecProvider(context: TutorialContext): TutorialSchem moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.juniperLogs.shortDescription', { - defaultMessage: 'Collect Juniper JUNOS logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Juniper JUNOS with Filebeat.', }), longDescription: i18n.translate('home.tutorials.juniperLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/kafka_logs/index.ts b/src/plugins/home/server/tutorials/kafka_logs/index.ts index 5b877cadcbec6..9ccc06eb222c7 100644 --- a/src/plugins/home/server/tutorials/kafka_logs/index.ts +++ b/src/plugins/home/server/tutorials/kafka_logs/index.ts @@ -24,12 +24,12 @@ export function kafkaLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'kafkaLogs', name: i18n.translate('home.tutorials.kafkaLogs.nameTitle', { - defaultMessage: 'Kafka logs', + defaultMessage: 'Kafka Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.kafkaLogs.shortDescription', { - defaultMessage: 'Collect and parse logs created by Kafka.', + defaultMessage: 'Collect and parse logs from Kafka servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.kafkaLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/kafka_metrics/index.ts b/src/plugins/home/server/tutorials/kafka_metrics/index.ts index 92f6744b91cbe..973ec06b58fdf 100644 --- a/src/plugins/home/server/tutorials/kafka_metrics/index.ts +++ b/src/plugins/home/server/tutorials/kafka_metrics/index.ts @@ -23,17 +23,17 @@ export function kafkaMetricsSpecProvider(context: TutorialContext): TutorialSche return { id: 'kafkaMetrics', name: i18n.translate('home.tutorials.kafkaMetrics.nameTitle', { - defaultMessage: 'Kafka metrics', + defaultMessage: 'Kafka Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.kafkaMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the Kafka server.', + defaultMessage: 'Collect metrics from Kafka servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.kafkaMetrics.longDescription', { defaultMessage: - 'The `kafka` Metricbeat module fetches internal metrics from Kafka. \ + 'The `kafka` Metricbeat module fetches metrics from Kafka. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-kafka.html', diff --git a/src/plugins/home/server/tutorials/kibana_logs/index.ts b/src/plugins/home/server/tutorials/kibana_logs/index.ts index 988af821ef9e3..9863a53700a55 100644 --- a/src/plugins/home/server/tutorials/kibana_logs/index.ts +++ b/src/plugins/home/server/tutorials/kibana_logs/index.ts @@ -29,7 +29,7 @@ export function kibanaLogsSpecProvider(context: TutorialContext): TutorialSchema moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.kibanaLogs.shortDescription', { - defaultMessage: 'Collect Kibana logs.', + defaultMessage: 'Collect and parse logs from Kibana with Filebeat.', }), longDescription: i18n.translate('home.tutorials.kibanaLogs.longDescription', { defaultMessage: 'This is the Kibana module. \ diff --git a/src/plugins/home/server/tutorials/kibana_metrics/index.ts b/src/plugins/home/server/tutorials/kibana_metrics/index.ts index dfe4efe4f7337..3d0eb691ede51 100644 --- a/src/plugins/home/server/tutorials/kibana_metrics/index.ts +++ b/src/plugins/home/server/tutorials/kibana_metrics/index.ts @@ -23,17 +23,17 @@ export function kibanaMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: 'kibanaMetrics', name: i18n.translate('home.tutorials.kibanaMetrics.nameTitle', { - defaultMessage: 'Kibana metrics', + defaultMessage: 'Kibana Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.kibanaMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from Kibana.', + defaultMessage: 'Collect metrics from Kibana with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.kibanaMetrics.longDescription', { defaultMessage: - 'The `kibana` Metricbeat module fetches internal metrics from Kibana. \ + 'The `kibana` Metricbeat module fetches metrics from Kibana. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-kibana.html', diff --git a/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts b/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts index 4a694560f5c28..9c66125ee0cfe 100644 --- a/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts +++ b/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts @@ -23,16 +23,16 @@ export function kubernetesMetricsSpecProvider(context: TutorialContext): Tutoria return { id: 'kubernetesMetrics', name: i18n.translate('home.tutorials.kubernetesMetrics.nameTitle', { - defaultMessage: 'Kubernetes metrics', + defaultMessage: 'Kubernetes Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.kubernetesMetrics.shortDescription', { - defaultMessage: 'Fetch metrics from your Kubernetes installation.', + defaultMessage: 'Collect metrics from Kubernetes installations with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.kubernetesMetrics.longDescription', { defaultMessage: - 'The `kubernetes` Metricbeat module fetches metrics from the Kubernetes APIs. \ + 'The `kubernetes` Metricbeat module fetches metrics from Kubernetes APIs. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-kubernetes.html', diff --git a/src/plugins/home/server/tutorials/logstash_logs/index.ts b/src/plugins/home/server/tutorials/logstash_logs/index.ts index 55491d45df28c..688ad8245b78d 100644 --- a/src/plugins/home/server/tutorials/logstash_logs/index.ts +++ b/src/plugins/home/server/tutorials/logstash_logs/index.ts @@ -24,12 +24,12 @@ export function logstashLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'logstashLogs', name: i18n.translate('home.tutorials.logstashLogs.nameTitle', { - defaultMessage: 'Logstash logs', + defaultMessage: 'Logstash Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.logstashLogs.shortDescription', { - defaultMessage: 'Collect Logstash main and slow logs.', + defaultMessage: 'Collect and parse main and slow logs from Logstash with Filebeat.', }), longDescription: i18n.translate('home.tutorials.logstashLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/logstash_metrics/index.ts b/src/plugins/home/server/tutorials/logstash_metrics/index.ts index e7d3fae011bd2..9ae4bcdcecbf1 100644 --- a/src/plugins/home/server/tutorials/logstash_metrics/index.ts +++ b/src/plugins/home/server/tutorials/logstash_metrics/index.ts @@ -23,17 +23,17 @@ export function logstashMetricsSpecProvider(context: TutorialContext): TutorialS return { id: moduleName + 'Metrics', name: i18n.translate('home.tutorials.logstashMetrics.nameTitle', { - defaultMessage: 'Logstash metrics', + defaultMessage: 'Logstash Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.logstashMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from a Logstash server.', + defaultMessage: 'Collect metrics from Logstash servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.logstashMetrics.longDescription', { defaultMessage: - 'The `{moduleName}` Metricbeat module fetches internal metrics from a Logstash server. \ + 'The `{moduleName}` Metricbeat module fetches metrics from a Logstash server. \ [Learn more]({learnMoreLink}).', values: { moduleName, diff --git a/src/plugins/home/server/tutorials/memcached_metrics/index.ts b/src/plugins/home/server/tutorials/memcached_metrics/index.ts index 15df179b44a9e..891567f72ca7c 100644 --- a/src/plugins/home/server/tutorials/memcached_metrics/index.ts +++ b/src/plugins/home/server/tutorials/memcached_metrics/index.ts @@ -23,17 +23,17 @@ export function memcachedMetricsSpecProvider(context: TutorialContext): Tutorial return { id: 'memcachedMetrics', name: i18n.translate('home.tutorials.memcachedMetrics.nameTitle', { - defaultMessage: 'Memcached metrics', + defaultMessage: 'Memcached Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.memcachedMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the Memcached server.', + defaultMessage: 'Collect metrics from Memcached servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.memcachedMetrics.longDescription', { defaultMessage: - 'The `memcached` Metricbeat module fetches internal metrics from Memcached. \ + 'The `memcached` Metricbeat module fetches metrics from Memcached. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-memcached.html', diff --git a/src/plugins/home/server/tutorials/microsoft_logs/index.ts b/src/plugins/home/server/tutorials/microsoft_logs/index.ts index 52401df1f9eb7..88893e22bc9ff 100644 --- a/src/plugins/home/server/tutorials/microsoft_logs/index.ts +++ b/src/plugins/home/server/tutorials/microsoft_logs/index.ts @@ -24,12 +24,12 @@ export function microsoftLogsSpecProvider(context: TutorialContext): TutorialSch return { id: 'microsoftLogs', name: i18n.translate('home.tutorials.microsoftLogs.nameTitle', { - defaultMessage: 'Microsoft Defender ATP logs', + defaultMessage: 'Microsoft Defender ATP Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.microsoftLogs.shortDescription', { - defaultMessage: 'Collect Microsoft Defender ATP alerts.', + defaultMessage: 'Collect and parse alerts from Microsoft Defender ATP with Filebeat.', }), longDescription: i18n.translate('home.tutorials.microsoftLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/misp_logs/index.ts b/src/plugins/home/server/tutorials/misp_logs/index.ts index b7611b543bab1..ea2147a296534 100644 --- a/src/plugins/home/server/tutorials/misp_logs/index.ts +++ b/src/plugins/home/server/tutorials/misp_logs/index.ts @@ -24,12 +24,12 @@ export function mispLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'mispLogs', name: i18n.translate('home.tutorials.mispLogs.nameTitle', { - defaultMessage: 'MISP threat intel logs', + defaultMessage: 'MISP threat intel Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.mispLogs.shortDescription', { - defaultMessage: 'Collect MISP threat intelligence data with Filebeat.', + defaultMessage: 'Collect and parse logs from MISP threat intelligence with Filebeat.', }), longDescription: i18n.translate('home.tutorials.mispLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/mongodb_logs/index.ts b/src/plugins/home/server/tutorials/mongodb_logs/index.ts index 3c189c04da43b..a7f9869d440ed 100644 --- a/src/plugins/home/server/tutorials/mongodb_logs/index.ts +++ b/src/plugins/home/server/tutorials/mongodb_logs/index.ts @@ -24,12 +24,12 @@ export function mongodbLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'mongodbLogs', name: i18n.translate('home.tutorials.mongodbLogs.nameTitle', { - defaultMessage: 'MongoDB logs', + defaultMessage: 'MongoDB Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.mongodbLogs.shortDescription', { - defaultMessage: 'Collect MongoDB logs.', + defaultMessage: 'Collect and parse logs from MongoDB servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.mongodbLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/mongodb_metrics/index.ts b/src/plugins/home/server/tutorials/mongodb_metrics/index.ts index 121310fba6f3a..cc0ecc0574fa9 100644 --- a/src/plugins/home/server/tutorials/mongodb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mongodb_metrics/index.ts @@ -23,16 +23,16 @@ export function mongodbMetricsSpecProvider(context: TutorialContext): TutorialSc return { id: 'mongodbMetrics', name: i18n.translate('home.tutorials.mongodbMetrics.nameTitle', { - defaultMessage: 'MongoDB metrics', + defaultMessage: 'MongoDB Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.mongodbMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from MongoDB.', + defaultMessage: 'Collect metrics from MongoDB servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.mongodbMetrics.longDescription', { defaultMessage: - 'The `mongodb` Metricbeat module fetches internal metrics from the MongoDB server. \ + 'The `mongodb` Metricbeat module fetches metrics from MongoDB server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-mongodb.html', diff --git a/src/plugins/home/server/tutorials/mssql_logs/index.ts b/src/plugins/home/server/tutorials/mssql_logs/index.ts index 567080910b7fe..06cafd95283c8 100644 --- a/src/plugins/home/server/tutorials/mssql_logs/index.ts +++ b/src/plugins/home/server/tutorials/mssql_logs/index.ts @@ -24,12 +24,12 @@ export function mssqlLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'mssqlLogs', name: i18n.translate('home.tutorials.mssqlLogs.nameTitle', { - defaultMessage: 'MSSQL logs', + defaultMessage: 'Microsoft SQL Server Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.mssqlLogs.shortDescription', { - defaultMessage: 'Collect MSSQL logs.', + defaultMessage: 'Collect and parse logs from Microsoft SQL Server instances with Filebeat.', }), longDescription: i18n.translate('home.tutorials.mssqlLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/mssql_metrics/index.ts b/src/plugins/home/server/tutorials/mssql_metrics/index.ts index 998cefe2de004..e3c9e3c338209 100644 --- a/src/plugins/home/server/tutorials/mssql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mssql_metrics/index.ts @@ -28,7 +28,7 @@ export function mssqlMetricsSpecProvider(context: TutorialContext): TutorialSche moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.mssqlMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from a Microsoft SQL Server instance', + defaultMessage: 'Collect metrics from Microsoft SQL Server instances with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.mssqlMetrics.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/munin_metrics/index.ts b/src/plugins/home/server/tutorials/munin_metrics/index.ts index 1abd321e4c738..12621d05d0766 100644 --- a/src/plugins/home/server/tutorials/munin_metrics/index.ts +++ b/src/plugins/home/server/tutorials/munin_metrics/index.ts @@ -23,18 +23,18 @@ export function muninMetricsSpecProvider(context: TutorialContext): TutorialSche return { id: 'muninMetrics', name: i18n.translate('home.tutorials.muninMetrics.nameTitle', { - defaultMessage: 'Munin metrics', + defaultMessage: 'Munin Metrics', }), moduleName, euiIconType: '/plugins/home/assets/logos/munin.svg', isBeta: true, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.muninMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the Munin server.', + defaultMessage: 'Collect metrics from Munin servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.muninMetrics.longDescription', { defaultMessage: - 'The `munin` Metricbeat module fetches internal metrics from Munin. \ + 'The `munin` Metricbeat module fetches metrics from Munin. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-munin.html', diff --git a/src/plugins/home/server/tutorials/mysql_logs/index.ts b/src/plugins/home/server/tutorials/mysql_logs/index.ts index a788e736d2964..b0c6f0e69dcfb 100644 --- a/src/plugins/home/server/tutorials/mysql_logs/index.ts +++ b/src/plugins/home/server/tutorials/mysql_logs/index.ts @@ -24,12 +24,12 @@ export function mysqlLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'mysqlLogs', name: i18n.translate('home.tutorials.mysqlLogs.nameTitle', { - defaultMessage: 'MySQL logs', + defaultMessage: 'MySQL Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.mysqlLogs.shortDescription', { - defaultMessage: 'Collect and parse error and slow logs created by MySQL.', + defaultMessage: 'Collect and parse logs from MySQL servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.mysqlLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/mysql_metrics/index.ts b/src/plugins/home/server/tutorials/mysql_metrics/index.ts index 078a96f8110df..09c55dc81ff84 100644 --- a/src/plugins/home/server/tutorials/mysql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mysql_metrics/index.ts @@ -23,16 +23,16 @@ export function mysqlMetricsSpecProvider(context: TutorialContext): TutorialSche return { id: 'mysqlMetrics', name: i18n.translate('home.tutorials.mysqlMetrics.nameTitle', { - defaultMessage: 'MySQL metrics', + defaultMessage: 'MySQL Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.mysqlMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from MySQL.', + defaultMessage: 'Collect metrics from MySQL servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.mysqlMetrics.longDescription', { defaultMessage: - 'The `mysql` Metricbeat module fetches internal metrics from the MySQL server. \ + 'The `mysql` Metricbeat module fetches metrics from MySQL server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-mysql.html', diff --git a/src/plugins/home/server/tutorials/nats_logs/index.ts b/src/plugins/home/server/tutorials/nats_logs/index.ts index a1dc24080bc0d..b6ef0a192d92f 100644 --- a/src/plugins/home/server/tutorials/nats_logs/index.ts +++ b/src/plugins/home/server/tutorials/nats_logs/index.ts @@ -24,13 +24,13 @@ export function natsLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'natsLogs', name: i18n.translate('home.tutorials.natsLogs.nameTitle', { - defaultMessage: 'NATS logs', + defaultMessage: 'NATS Logs', }), moduleName, category: TutorialsCategory.LOGGING, isBeta: true, shortDescription: i18n.translate('home.tutorials.natsLogs.shortDescription', { - defaultMessage: 'Collect and parse logs created by Nats.', + defaultMessage: 'Collect and parse logs from NATS servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.natsLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/nats_metrics/index.ts b/src/plugins/home/server/tutorials/nats_metrics/index.ts index 11494e5dc57d0..54f034ad44b19 100644 --- a/src/plugins/home/server/tutorials/nats_metrics/index.ts +++ b/src/plugins/home/server/tutorials/nats_metrics/index.ts @@ -23,16 +23,16 @@ export function natsMetricsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'natsMetrics', name: i18n.translate('home.tutorials.natsMetrics.nameTitle', { - defaultMessage: 'NATS metrics', + defaultMessage: 'NATS Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.natsMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from the Nats server.', + defaultMessage: 'Collect metrics from NATS servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.natsMetrics.longDescription', { defaultMessage: - 'The `nats` Metricbeat module fetches monitoring metrics from Nats. \ + 'The `nats` Metricbeat module fetches metrics from Nats. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-nats.html', diff --git a/src/plugins/home/server/tutorials/netflow_logs/index.ts b/src/plugins/home/server/tutorials/netflow_logs/index.ts index e8404e93ae355..c659d9c1d31b1 100644 --- a/src/plugins/home/server/tutorials/netflow_logs/index.ts +++ b/src/plugins/home/server/tutorials/netflow_logs/index.ts @@ -24,12 +24,12 @@ export function netflowLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'netflowLogs', name: i18n.translate('home.tutorials.netflowLogs.nameTitle', { - defaultMessage: 'NetFlow / IPFIX Collector', + defaultMessage: 'NetFlow / IPFIX Records', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.netflowLogs.shortDescription', { - defaultMessage: 'Collect NetFlow and IPFIX flow records.', + defaultMessage: 'Collect records from NetFlow and IPFIX flow with Filebeat.', }), longDescription: i18n.translate('home.tutorials.netflowLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/netscout_logs/index.ts b/src/plugins/home/server/tutorials/netscout_logs/index.ts index 395fbb8b49d39..e6c22947f8057 100644 --- a/src/plugins/home/server/tutorials/netscout_logs/index.ts +++ b/src/plugins/home/server/tutorials/netscout_logs/index.ts @@ -24,12 +24,12 @@ export function netscoutLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'netscoutLogs', name: i18n.translate('home.tutorials.netscoutLogs.nameTitle', { - defaultMessage: 'Arbor Peakflow logs', + defaultMessage: 'Arbor Peakflow Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.netscoutLogs.shortDescription', { - defaultMessage: 'Collect Netscout Arbor Peakflow SP logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Netscout Arbor Peakflow SP with Filebeat.', }), longDescription: i18n.translate('home.tutorials.netscoutLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/nginx_logs/index.ts b/src/plugins/home/server/tutorials/nginx_logs/index.ts index 90ec6737c2461..e6f2fc4efb01c 100644 --- a/src/plugins/home/server/tutorials/nginx_logs/index.ts +++ b/src/plugins/home/server/tutorials/nginx_logs/index.ts @@ -24,12 +24,12 @@ export function nginxLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'nginxLogs', name: i18n.translate('home.tutorials.nginxLogs.nameTitle', { - defaultMessage: 'Nginx logs', + defaultMessage: 'Nginx Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.nginxLogs.shortDescription', { - defaultMessage: 'Collect and parse access and error logs created by the Nginx HTTP server.', + defaultMessage: 'Collect and parse logs from Nginx HTTP servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.nginxLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/nginx_metrics/index.ts b/src/plugins/home/server/tutorials/nginx_metrics/index.ts index 12f67a26dcf29..680dd664912d3 100644 --- a/src/plugins/home/server/tutorials/nginx_metrics/index.ts +++ b/src/plugins/home/server/tutorials/nginx_metrics/index.ts @@ -23,16 +23,16 @@ export function nginxMetricsSpecProvider(context: TutorialContext): TutorialSche return { id: 'nginxMetrics', name: i18n.translate('home.tutorials.nginxMetrics.nameTitle', { - defaultMessage: 'Nginx metrics', + defaultMessage: 'Nginx Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.nginxMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the Nginx HTTP server.', + defaultMessage: 'Collect metrics from Nginx HTTP servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.nginxMetrics.longDescription', { defaultMessage: - 'The `nginx` Metricbeat module fetches internal metrics from the Nginx HTTP server. \ + 'The `nginx` Metricbeat module fetches metrics from Nginx HTTP server. \ The module scrapes the server status data from the web page generated by the \ {statusModuleLink}, \ which must be enabled in your Nginx installation. \ diff --git a/src/plugins/home/server/tutorials/o365_logs/index.ts b/src/plugins/home/server/tutorials/o365_logs/index.ts index e3663e2c3cd78..3cd4d3a5c5e18 100644 --- a/src/plugins/home/server/tutorials/o365_logs/index.ts +++ b/src/plugins/home/server/tutorials/o365_logs/index.ts @@ -24,12 +24,12 @@ export function o365LogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'o365Logs', name: i18n.translate('home.tutorials.o365Logs.nameTitle', { - defaultMessage: 'Office 365 logs', + defaultMessage: 'Office 365 Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.o365Logs.shortDescription', { - defaultMessage: 'Collect Office 365 activity logs via the Office 365 API.', + defaultMessage: 'Collect and parse logs from Office 365 with Filebeat.', }), longDescription: i18n.translate('home.tutorials.o365Logs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/okta_logs/index.ts b/src/plugins/home/server/tutorials/okta_logs/index.ts index 62cde4b5128c3..aad18409de329 100644 --- a/src/plugins/home/server/tutorials/okta_logs/index.ts +++ b/src/plugins/home/server/tutorials/okta_logs/index.ts @@ -24,12 +24,12 @@ export function oktaLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'oktaLogs', name: i18n.translate('home.tutorials.oktaLogs.nameTitle', { - defaultMessage: 'Okta logs', + defaultMessage: 'Okta Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.oktaLogs.shortDescription', { - defaultMessage: 'Collect the Okta system log via the Okta API.', + defaultMessage: 'Collect and parse logs from the Okta API with Filebeat.', }), longDescription: i18n.translate('home.tutorials.oktaLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts b/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts index acbddf5169881..02625b341549b 100644 --- a/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts +++ b/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts @@ -23,12 +23,13 @@ export function openmetricsMetricsSpecProvider(context: TutorialContext): Tutori return { id: 'openmetricsMetrics', name: i18n.translate('home.tutorials.openmetricsMetrics.nameTitle', { - defaultMessage: 'OpenMetrics metrics', + defaultMessage: 'OpenMetrics Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.openmetricsMetrics.shortDescription', { - defaultMessage: 'Fetch metrics from an endpoint that serves metrics in OpenMetrics format.', + defaultMessage: + 'Collect metrics from an endpoint that serves metrics in OpenMetrics format with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.openmetricsMetrics.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/oracle_metrics/index.ts b/src/plugins/home/server/tutorials/oracle_metrics/index.ts index 42560a7b46225..bfb0452941bd4 100644 --- a/src/plugins/home/server/tutorials/oracle_metrics/index.ts +++ b/src/plugins/home/server/tutorials/oracle_metrics/index.ts @@ -23,17 +23,17 @@ export function oracleMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: moduleName + 'Metrics', name: i18n.translate('home.tutorials.oracleMetrics.nameTitle', { - defaultMessage: 'oracle metrics', + defaultMessage: 'oracle Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.oracleMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from a Oracle server.', + defaultMessage: 'Collect metrics from Oracle servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.oracleMetrics.longDescription', { defaultMessage: - 'The `{moduleName}` Metricbeat module fetches internal metrics from a Oracle server. \ + 'The `{moduleName}` Metricbeat module fetches metrics from a Oracle server. \ [Learn more]({learnMoreLink}).', values: { moduleName, diff --git a/src/plugins/home/server/tutorials/osquery_logs/index.ts b/src/plugins/home/server/tutorials/osquery_logs/index.ts index 6bacbed57792c..4f87fc4e256e1 100644 --- a/src/plugins/home/server/tutorials/osquery_logs/index.ts +++ b/src/plugins/home/server/tutorials/osquery_logs/index.ts @@ -24,12 +24,12 @@ export function osqueryLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'osqueryLogs', name: i18n.translate('home.tutorials.osqueryLogs.nameTitle', { - defaultMessage: 'Osquery logs', + defaultMessage: 'Osquery Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.osqueryLogs.shortDescription', { - defaultMessage: 'Collect osquery logs in JSON format.', + defaultMessage: 'Collect and parse logs from Osquery with Filebeat.', }), longDescription: i18n.translate('home.tutorials.osqueryLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/panw_logs/index.ts b/src/plugins/home/server/tutorials/panw_logs/index.ts index 3ca839556d756..f5158c48f30d5 100644 --- a/src/plugins/home/server/tutorials/panw_logs/index.ts +++ b/src/plugins/home/server/tutorials/panw_logs/index.ts @@ -24,13 +24,13 @@ export function panwLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'panwLogs', name: i18n.translate('home.tutorials.panwLogs.nameTitle', { - defaultMessage: 'Palo Alto Networks PAN-OS logs', + defaultMessage: 'Palo Alto Networks PAN-OS Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.panwLogs.shortDescription', { defaultMessage: - 'Collect Palo Alto Networks PAN-OS threat and traffic logs over syslog or from a log file.', + 'Collect and parse threat and traffic logs from Palo Alto Networks PAN-OS with Filebeat.', }), longDescription: i18n.translate('home.tutorials.panwLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts b/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts index ed67960ab5a1c..40b35984fb17a 100644 --- a/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts +++ b/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts @@ -23,17 +23,17 @@ export function phpfpmMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: 'phpfpmMetrics', name: i18n.translate('home.tutorials.phpFpmMetrics.nameTitle', { - defaultMessage: 'PHP-FPM metrics', + defaultMessage: 'PHP-FPM Metrics', }), moduleName, category: TutorialsCategory.METRICS, isBeta: false, shortDescription: i18n.translate('home.tutorials.phpFpmMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from PHP-FPM.', + defaultMessage: 'Collect metrics from PHP-FPM with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.phpFpmMetrics.longDescription', { defaultMessage: - 'The `php_fpm` Metricbeat module fetches internal metrics from the PHP-FPM server. \ + 'The `php_fpm` Metricbeat module fetches metrics from PHP-FPM server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-php_fpm.html', diff --git a/src/plugins/home/server/tutorials/postgresql_logs/index.ts b/src/plugins/home/server/tutorials/postgresql_logs/index.ts index c5f5d879ac35d..3a092e61b0bd9 100644 --- a/src/plugins/home/server/tutorials/postgresql_logs/index.ts +++ b/src/plugins/home/server/tutorials/postgresql_logs/index.ts @@ -24,12 +24,12 @@ export function postgresqlLogsSpecProvider(context: TutorialContext): TutorialSc return { id: 'postgresqlLogs', name: i18n.translate('home.tutorials.postgresqlLogs.nameTitle', { - defaultMessage: 'PostgreSQL logs', + defaultMessage: 'PostgreSQL Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.postgresqlLogs.shortDescription', { - defaultMessage: 'Collect and parse error and slow logs created by PostgreSQL.', + defaultMessage: 'Collect and parse logs from PostgreSQL servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.postgresqlLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/postgresql_metrics/index.ts b/src/plugins/home/server/tutorials/postgresql_metrics/index.ts index ca20efb44bca7..501ea252cd16f 100644 --- a/src/plugins/home/server/tutorials/postgresql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/postgresql_metrics/index.ts @@ -23,17 +23,17 @@ export function postgresqlMetricsSpecProvider(context: TutorialContext): Tutoria return { id: 'postgresqlMetrics', name: i18n.translate('home.tutorials.postgresqlMetrics.nameTitle', { - defaultMessage: 'PostgreSQL metrics', + defaultMessage: 'PostgreSQL Metrics', }), moduleName, category: TutorialsCategory.METRICS, isBeta: false, shortDescription: i18n.translate('home.tutorials.postgresqlMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from PostgreSQL.', + defaultMessage: 'Collect metrics from PostgreSQL servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.postgresqlMetrics.longDescription', { defaultMessage: - 'The `postgresql` Metricbeat module fetches internal metrics from the PostgreSQL server. \ + 'The `postgresql` Metricbeat module fetches metrics from PostgreSQL server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-postgresql.html', diff --git a/src/plugins/home/server/tutorials/prometheus_metrics/index.ts b/src/plugins/home/server/tutorials/prometheus_metrics/index.ts index ee05770d65108..2f422e5e3be70 100644 --- a/src/plugins/home/server/tutorials/prometheus_metrics/index.ts +++ b/src/plugins/home/server/tutorials/prometheus_metrics/index.ts @@ -23,13 +23,13 @@ export function prometheusMetricsSpecProvider(context: TutorialContext): Tutoria return { id: moduleName + 'Metrics', name: i18n.translate('home.tutorials.prometheusMetrics.nameTitle', { - defaultMessage: 'Prometheus metrics', + defaultMessage: 'Prometheus Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.prometheusMetrics.shortDescription', { - defaultMessage: 'Fetch metrics from a Prometheus exporter.', + defaultMessage: 'Collect metrics from Prometheus exporters with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.prometheusMetrics.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts b/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts index 0fbdb48236832..8a1634e7da038 100644 --- a/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts +++ b/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts @@ -24,12 +24,12 @@ export function rabbitmqLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'rabbitmqLogs', name: i18n.translate('home.tutorials.rabbitmqLogs.nameTitle', { - defaultMessage: 'RabbitMQ logs', + defaultMessage: 'RabbitMQ Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.rabbitmqLogs.shortDescription', { - defaultMessage: 'Collect RabbitMQ logs.', + defaultMessage: 'Collect and parse logs from RabbitMQ servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.rabbitmqLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts b/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts index b58f936f205b2..abfc895088d91 100644 --- a/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts @@ -23,16 +23,16 @@ export function rabbitmqMetricsSpecProvider(context: TutorialContext): TutorialS return { id: 'rabbitmqMetrics', name: i18n.translate('home.tutorials.rabbitmqMetrics.nameTitle', { - defaultMessage: 'RabbitMQ metrics', + defaultMessage: 'RabbitMQ Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.rabbitmqMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the RabbitMQ server.', + defaultMessage: 'Collect metrics from RabbitMQ servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.rabbitmqMetrics.longDescription', { defaultMessage: - 'The `rabbitmq` Metricbeat module fetches internal metrics from the RabbitMQ server. \ + 'The `rabbitmq` Metricbeat module fetches metrics from RabbitMQ server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-rabbitmq.html', diff --git a/src/plugins/home/server/tutorials/radware_logs/index.ts b/src/plugins/home/server/tutorials/radware_logs/index.ts index 28392cf9c4362..3e918a0a4064c 100644 --- a/src/plugins/home/server/tutorials/radware_logs/index.ts +++ b/src/plugins/home/server/tutorials/radware_logs/index.ts @@ -24,12 +24,12 @@ export function radwareLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'radwareLogs', name: i18n.translate('home.tutorials.radwareLogs.nameTitle', { - defaultMessage: 'Radware DefensePro logs', + defaultMessage: 'Radware DefensePro Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.radwareLogs.shortDescription', { - defaultMessage: 'Collect Radware DefensePro logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Radware DefensePro with Filebeat.', }), longDescription: i18n.translate('home.tutorials.radwareLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/redis_logs/index.ts b/src/plugins/home/server/tutorials/redis_logs/index.ts index 0f3a5aa812f49..f6aada27dec48 100644 --- a/src/plugins/home/server/tutorials/redis_logs/index.ts +++ b/src/plugins/home/server/tutorials/redis_logs/index.ts @@ -24,12 +24,12 @@ export function redisLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'redisLogs', name: i18n.translate('home.tutorials.redisLogs.nameTitle', { - defaultMessage: 'Redis logs', + defaultMessage: 'Redis Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.redisLogs.shortDescription', { - defaultMessage: 'Collect and parse error and slow logs created by Redis.', + defaultMessage: 'Collect and parse logs from Redis servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.redisLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/redis_metrics/index.ts b/src/plugins/home/server/tutorials/redis_metrics/index.ts index 1b4ee7290a6d0..2bb300c48ff65 100644 --- a/src/plugins/home/server/tutorials/redis_metrics/index.ts +++ b/src/plugins/home/server/tutorials/redis_metrics/index.ts @@ -23,16 +23,16 @@ export function redisMetricsSpecProvider(context: TutorialContext): TutorialSche return { id: 'redisMetrics', name: i18n.translate('home.tutorials.redisMetrics.nameTitle', { - defaultMessage: 'Redis metrics', + defaultMessage: 'Redis Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.redisMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from Redis.', + defaultMessage: 'Collect metrics from Redis servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.redisMetrics.longDescription', { defaultMessage: - 'The `redis` Metricbeat module fetches internal metrics from the Redis server. \ + 'The `redis` Metricbeat module fetches metrics from Redis server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-redis.html', diff --git a/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts b/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts index be8de9c3eab4d..62e1386f29dbb 100644 --- a/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts +++ b/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts @@ -23,16 +23,16 @@ export function redisenterpriseMetricsSpecProvider(context: TutorialContext): Tu return { id: 'redisenterpriseMetrics', name: i18n.translate('home.tutorials.redisenterpriseMetrics.nameTitle', { - defaultMessage: 'Redis Enterprise metrics', + defaultMessage: 'Redis Enterprise Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.redisenterpriseMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from Redis Enterprise Server.', + defaultMessage: 'Collect metrics from Redis Enterprise servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.redisenterpriseMetrics.longDescription', { defaultMessage: - 'The `redisenterprise` Metricbeat module fetches monitoring metrics from Redis Enterprise Server \ + 'The `redisenterprise` Metricbeat module fetches metrics from Redis Enterprise Server \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-redisenterprise.html', diff --git a/src/plugins/home/server/tutorials/santa_logs/index.ts b/src/plugins/home/server/tutorials/santa_logs/index.ts index 10d1506438b62..da9f2e940066e 100644 --- a/src/plugins/home/server/tutorials/santa_logs/index.ts +++ b/src/plugins/home/server/tutorials/santa_logs/index.ts @@ -24,12 +24,12 @@ export function santaLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'santaLogs', name: i18n.translate('home.tutorials.santaLogs.nameTitle', { - defaultMessage: 'Google Santa logs', + defaultMessage: 'Google Santa Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.santaLogs.shortDescription', { - defaultMessage: 'Collect Google Santa logs about process executions on MacOS.', + defaultMessage: 'Collect and parse logs from Google Santa systems with Filebeat.', }), longDescription: i18n.translate('home.tutorials.santaLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/sonicwall_logs/index.ts b/src/plugins/home/server/tutorials/sonicwall_logs/index.ts index 1fa711327a07d..04bf7a3968320 100644 --- a/src/plugins/home/server/tutorials/sonicwall_logs/index.ts +++ b/src/plugins/home/server/tutorials/sonicwall_logs/index.ts @@ -24,12 +24,12 @@ export function sonicwallLogsSpecProvider(context: TutorialContext): TutorialSch return { id: 'sonicwallLogs', name: i18n.translate('home.tutorials.sonicwallLogs.nameTitle', { - defaultMessage: 'Sonicwall FW logs', + defaultMessage: 'Sonicwall FW Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.sonicwallLogs.shortDescription', { - defaultMessage: 'Collect Sonicwall-FW logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Sonicwall-FW with Filebeat.', }), longDescription: i18n.translate('home.tutorials.sonicwallLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/sophos_logs/index.ts b/src/plugins/home/server/tutorials/sophos_logs/index.ts index 35b27973a55ec..4fadcecb6e1bd 100644 --- a/src/plugins/home/server/tutorials/sophos_logs/index.ts +++ b/src/plugins/home/server/tutorials/sophos_logs/index.ts @@ -24,12 +24,12 @@ export function sophosLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'sophosLogs', name: i18n.translate('home.tutorials.sophosLogs.nameTitle', { - defaultMessage: 'Sophos logs', + defaultMessage: 'Sophos Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.sophosLogs.shortDescription', { - defaultMessage: 'Collect Sophos XG SFOS logs over syslog.', + defaultMessage: 'Collect and parse logs from Sophos XG SFOS with Filebeat.', }), longDescription: i18n.translate('home.tutorials.sophosLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/squid_logs/index.ts b/src/plugins/home/server/tutorials/squid_logs/index.ts index d8d0bb6c0829b..2d8f055d7fa6b 100644 --- a/src/plugins/home/server/tutorials/squid_logs/index.ts +++ b/src/plugins/home/server/tutorials/squid_logs/index.ts @@ -24,12 +24,12 @@ export function squidLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'squidLogs', name: i18n.translate('home.tutorials.squidLogs.nameTitle', { - defaultMessage: 'Squid logs', + defaultMessage: 'Squid Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.squidLogs.shortDescription', { - defaultMessage: 'Collect Squid logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Squid servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.squidLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/stan_metrics/index.ts b/src/plugins/home/server/tutorials/stan_metrics/index.ts index ceb6084b539e6..0b3c0352b663d 100644 --- a/src/plugins/home/server/tutorials/stan_metrics/index.ts +++ b/src/plugins/home/server/tutorials/stan_metrics/index.ts @@ -23,16 +23,16 @@ export function stanMetricsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'stanMetrics', name: i18n.translate('home.tutorials.stanMetrics.nameTitle', { - defaultMessage: 'STAN metrics', + defaultMessage: 'STAN Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.stanMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from the STAN server.', + defaultMessage: 'Collect metrics from STAN servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.stanMetrics.longDescription', { defaultMessage: - 'The `stan` Metricbeat module fetches monitoring metrics from STAN. \ + 'The `stan` Metricbeat module fetches metrics from STAN. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-stan.html', diff --git a/src/plugins/home/server/tutorials/statsd_metrics/index.ts b/src/plugins/home/server/tutorials/statsd_metrics/index.ts index 472c1406db386..1be010a01d5a6 100644 --- a/src/plugins/home/server/tutorials/statsd_metrics/index.ts +++ b/src/plugins/home/server/tutorials/statsd_metrics/index.ts @@ -20,16 +20,16 @@ export function statsdMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: 'statsdMetrics', name: i18n.translate('home.tutorials.statsdMetrics.nameTitle', { - defaultMessage: 'Statsd metrics', + defaultMessage: 'Statsd Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.statsdMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from statsd.', + defaultMessage: 'Collect metrics from Statsd servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.statsdMetrics.longDescription', { defaultMessage: - 'The `statsd` Metricbeat module fetches monitoring metrics from statsd. \ + 'The `statsd` Metricbeat module fetches metrics from statsd. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-statsd.html', diff --git a/src/plugins/home/server/tutorials/suricata_logs/index.ts b/src/plugins/home/server/tutorials/suricata_logs/index.ts index 3bb2b93b6301a..373522e333379 100644 --- a/src/plugins/home/server/tutorials/suricata_logs/index.ts +++ b/src/plugins/home/server/tutorials/suricata_logs/index.ts @@ -24,12 +24,12 @@ export function suricataLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'suricataLogs', name: i18n.translate('home.tutorials.suricataLogs.nameTitle', { - defaultMessage: 'Suricata logs', + defaultMessage: 'Suricata Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.suricataLogs.shortDescription', { - defaultMessage: 'Collect Suricata IDS/IPS/NSM logs.', + defaultMessage: 'Collect and parse logs from Suricata IDS/IPS/NSM with Filebeat.', }), longDescription: i18n.translate('home.tutorials.suricataLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/system_logs/index.ts b/src/plugins/home/server/tutorials/system_logs/index.ts index 6f403a6d0a71a..fcc5745f48252 100644 --- a/src/plugins/home/server/tutorials/system_logs/index.ts +++ b/src/plugins/home/server/tutorials/system_logs/index.ts @@ -24,7 +24,7 @@ export function systemLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'systemLogs', name: i18n.translate('home.tutorials.systemLogs.nameTitle', { - defaultMessage: 'System logs', + defaultMessage: 'System Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, diff --git a/src/plugins/home/server/tutorials/system_metrics/index.ts b/src/plugins/home/server/tutorials/system_metrics/index.ts index 08979a3d3b003..1348535d9bb72 100644 --- a/src/plugins/home/server/tutorials/system_metrics/index.ts +++ b/src/plugins/home/server/tutorials/system_metrics/index.ts @@ -23,16 +23,17 @@ export function systemMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: 'systemMetrics', name: i18n.translate('home.tutorials.systemMetrics.nameTitle', { - defaultMessage: 'System metrics', + defaultMessage: 'System Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.systemMetrics.shortDescription', { - defaultMessage: 'Collect CPU, memory, network, and disk statistics from the host.', + defaultMessage: + 'Collect CPU, memory, network, and disk metrics from System hosts with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.systemMetrics.longDescription', { defaultMessage: - 'The `system` Metricbeat module collects CPU, memory, network, and disk statistics from the host. \ + 'The `system` Metricbeat module collects CPU, memory, network, and disk statistics from host. \ It collects system wide statistics and statistics per process and filesystem. \ [Learn more]({learnMoreLink}).', values: { diff --git a/src/plugins/home/server/tutorials/tomcat_logs/index.ts b/src/plugins/home/server/tutorials/tomcat_logs/index.ts index 5ce4096ad4628..3258d3eff5a16 100644 --- a/src/plugins/home/server/tutorials/tomcat_logs/index.ts +++ b/src/plugins/home/server/tutorials/tomcat_logs/index.ts @@ -24,12 +24,12 @@ export function tomcatLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'tomcatLogs', name: i18n.translate('home.tutorials.tomcatLogs.nameTitle', { - defaultMessage: 'Tomcat logs', + defaultMessage: 'Tomcat Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.tomcatLogs.shortDescription', { - defaultMessage: 'Collect Apache Tomcat logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Apache Tomcat servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.tomcatLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/traefik_logs/index.ts b/src/plugins/home/server/tutorials/traefik_logs/index.ts index 6bbc905bbd6aa..30b9db4022137 100644 --- a/src/plugins/home/server/tutorials/traefik_logs/index.ts +++ b/src/plugins/home/server/tutorials/traefik_logs/index.ts @@ -24,12 +24,12 @@ export function traefikLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'traefikLogs', name: i18n.translate('home.tutorials.traefikLogs.nameTitle', { - defaultMessage: 'Traefik logs', + defaultMessage: 'Traefik Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.traefikLogs.shortDescription', { - defaultMessage: 'Collect Traefik access logs.', + defaultMessage: 'Collect and parse logs from Traefik with Filebeat.', }), longDescription: i18n.translate('home.tutorials.traefikLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/traefik_metrics/index.ts b/src/plugins/home/server/tutorials/traefik_metrics/index.ts index 35d54317c8ede..6f76be3056110 100644 --- a/src/plugins/home/server/tutorials/traefik_metrics/index.ts +++ b/src/plugins/home/server/tutorials/traefik_metrics/index.ts @@ -20,16 +20,16 @@ export function traefikMetricsSpecProvider(context: TutorialContext): TutorialSc return { id: 'traefikMetrics', name: i18n.translate('home.tutorials.traefikMetrics.nameTitle', { - defaultMessage: 'Traefik metrics', + defaultMessage: 'Traefik Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.traefikMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from Traefik.', + defaultMessage: 'Collect metrics from Traefik with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.traefikMetrics.longDescription', { defaultMessage: - 'The `traefik` Metricbeat module fetches monitoring metrics from Traefik. \ + 'The `traefik` Metricbeat module fetches metrics from Traefik. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-traefik.html', diff --git a/src/plugins/home/server/tutorials/uptime_monitors/index.ts b/src/plugins/home/server/tutorials/uptime_monitors/index.ts index 6e949d5410115..118174d0e5717 100644 --- a/src/plugins/home/server/tutorials/uptime_monitors/index.ts +++ b/src/plugins/home/server/tutorials/uptime_monitors/index.ts @@ -28,7 +28,7 @@ export function uptimeMonitorsSpecProvider(context: TutorialContext): TutorialSc moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.uptimeMonitors.shortDescription', { - defaultMessage: 'Monitor services for their availability', + defaultMessage: 'Monitor availability of the services with Heartbeat.', }), longDescription: i18n.translate('home.tutorials.uptimeMonitors.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts b/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts index d9cfcc9f7fb75..b1dbeb89bdb26 100644 --- a/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts +++ b/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts @@ -23,16 +23,16 @@ export function uwsgiMetricsSpecProvider(context: TutorialContext): TutorialSche return { id: 'uwsgiMetrics', name: i18n.translate('home.tutorials.uwsgiMetrics.nameTitle', { - defaultMessage: 'uWSGI metrics', + defaultMessage: 'uWSGI Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.uwsgiMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the uWSGI server.', + defaultMessage: 'Collect metrics from uWSGI servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.uwsgiMetrics.longDescription', { defaultMessage: - 'The `uwsgi` Metricbeat module fetches internal metrics from the uWSGI server. \ + 'The `uwsgi` Metricbeat module fetches metrics from uWSGI server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-uwsgi.html', diff --git a/src/plugins/home/server/tutorials/vsphere_metrics/index.ts b/src/plugins/home/server/tutorials/vsphere_metrics/index.ts index bcbcec59c36e4..14a574872221a 100644 --- a/src/plugins/home/server/tutorials/vsphere_metrics/index.ts +++ b/src/plugins/home/server/tutorials/vsphere_metrics/index.ts @@ -23,16 +23,16 @@ export function vSphereMetricsSpecProvider(context: TutorialContext): TutorialSc return { id: 'vsphereMetrics', name: i18n.translate('home.tutorials.vsphereMetrics.nameTitle', { - defaultMessage: 'vSphere metrics', + defaultMessage: 'vSphere Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.vsphereMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from vSphere.', + defaultMessage: 'Collect metrics from vSphere with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.vsphereMetrics.longDescription', { defaultMessage: - 'The `vsphere` Metricbeat module fetches internal metrics from a vSphere cluster. \ + 'The `vsphere` Metricbeat module fetches metrics from a vSphere cluster. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-vsphere.html', diff --git a/src/plugins/home/server/tutorials/windows_event_logs/index.ts b/src/plugins/home/server/tutorials/windows_event_logs/index.ts index 0df7fa906e085..008468487ea64 100644 --- a/src/plugins/home/server/tutorials/windows_event_logs/index.ts +++ b/src/plugins/home/server/tutorials/windows_event_logs/index.ts @@ -23,17 +23,17 @@ export function windowsEventLogsSpecProvider(context: TutorialContext): Tutorial return { id: 'windowsEventLogs', name: i18n.translate('home.tutorials.windowsEventLogs.nameTitle', { - defaultMessage: 'Windows Event Log', + defaultMessage: 'Windows Event Logs', }), moduleName, isBeta: false, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.windowsEventLogs.shortDescription', { - defaultMessage: 'Fetch logs from the Windows Event Log.', + defaultMessage: 'Collect and parse logs from Windows Event Logs with WinLogBeat.', }), longDescription: i18n.translate('home.tutorials.windowsEventLogs.longDescription', { defaultMessage: - 'Use Winlogbeat to collect the logs from the Windows Event Log. \ + 'Use Winlogbeat to collect the logs from Windows Event Logs. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.winlogbeat}/index.html', diff --git a/src/plugins/home/server/tutorials/windows_metrics/index.ts b/src/plugins/home/server/tutorials/windows_metrics/index.ts index 6c663fbb13d4d..31d9b3f8962ce 100644 --- a/src/plugins/home/server/tutorials/windows_metrics/index.ts +++ b/src/plugins/home/server/tutorials/windows_metrics/index.ts @@ -23,17 +23,17 @@ export function windowsMetricsSpecProvider(context: TutorialContext): TutorialSc return { id: 'windowsMetrics', name: i18n.translate('home.tutorials.windowsMetrics.nameTitle', { - defaultMessage: 'Windows metrics', + defaultMessage: 'Windows Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.windowsMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from Windows.', + defaultMessage: 'Collect metrics from Windows with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.windowsMetrics.longDescription', { defaultMessage: - 'The `windows` Metricbeat module fetches internal metrics from Windows. \ + 'The `windows` Metricbeat module fetches metrics from Windows. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-windows.html', diff --git a/src/plugins/home/server/tutorials/zeek_logs/index.ts b/src/plugins/home/server/tutorials/zeek_logs/index.ts index 5434dcc8527ff..df86518978c52 100644 --- a/src/plugins/home/server/tutorials/zeek_logs/index.ts +++ b/src/plugins/home/server/tutorials/zeek_logs/index.ts @@ -24,12 +24,12 @@ export function zeekLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'zeekLogs', name: i18n.translate('home.tutorials.zeekLogs.nameTitle', { - defaultMessage: 'Zeek logs', + defaultMessage: 'Zeek Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.zeekLogs.shortDescription', { - defaultMessage: 'Collect Zeek network security monitoring logs.', + defaultMessage: 'Collect and parse logs from Zeek network security with Filebeat.', }), longDescription: i18n.translate('home.tutorials.zeekLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts b/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts index 85ca03acacfd4..8f732969a07f3 100644 --- a/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts +++ b/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts @@ -23,18 +23,18 @@ export function zookeeperMetricsSpecProvider(context: TutorialContext): Tutorial return { id: moduleName + 'Metrics', name: i18n.translate('home.tutorials.zookeeperMetrics.nameTitle', { - defaultMessage: 'Zookeeper metrics', + defaultMessage: 'Zookeeper Metrics', }), moduleName, euiIconType: '/plugins/home/assets/logos/zookeeper.svg', isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.zookeeperMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from a Zookeeper server.', + defaultMessage: 'Collect metrics from Zookeeper servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.zookeeperMetrics.longDescription', { defaultMessage: - 'The `{moduleName}` Metricbeat module fetches internal metrics from a Zookeeper server. \ + 'The `{moduleName}` Metricbeat module fetches metrics from a Zookeeper server. \ [Learn more]({learnMoreLink}).', values: { moduleName, diff --git a/src/plugins/home/server/tutorials/zscaler_logs/index.ts b/src/plugins/home/server/tutorials/zscaler_logs/index.ts index a2eb41a257a92..977bbb242c62a 100644 --- a/src/plugins/home/server/tutorials/zscaler_logs/index.ts +++ b/src/plugins/home/server/tutorials/zscaler_logs/index.ts @@ -29,7 +29,7 @@ export function zscalerLogsSpecProvider(context: TutorialContext): TutorialSchem moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.zscalerLogs.shortDescription', { - defaultMessage: 'This is a module for receiving Zscaler NSS logs over Syslog or a file.', + defaultMessage: 'Collect and parse logs from Zscaler NSS with Filebeat.', }), longDescription: i18n.translate('home.tutorials.zscalerLogs.longDescription', { defaultMessage: diff --git a/src/plugins/vis_default_editor/kibana.json b/src/plugins/vis_default_editor/kibana.json index efed1eab1e494..253edc74f87b4 100644 --- a/src/plugins/vis_default_editor/kibana.json +++ b/src/plugins/vis_default_editor/kibana.json @@ -3,7 +3,7 @@ "version": "kibana", "ui": true, "optionalPlugins": ["visualize"], - "requiredBundles": ["kibanaUtils", "kibanaReact", "data", "fieldFormats", "discover"], + "requiredBundles": ["kibanaUtils", "kibanaReact", "data", "fieldFormats", "discover", "esUiShared"], "owner": { "name": "Vis Editors", "githubTeam": "kibana-vis-editors" diff --git a/src/plugins/vis_default_editor/public/components/controls/raw_json.tsx b/src/plugins/vis_default_editor/public/components/controls/raw_json.tsx index f9486b8ab3cf6..cd642e51a9c17 100644 --- a/src/plugins/vis_default_editor/public/components/controls/raw_json.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/raw_json.tsx @@ -14,6 +14,7 @@ import { EuiFormRow, EuiIconTip, EuiScreenReaderOnly } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { XJsonLang } from '@kbn/monaco'; import { CodeEditor } from '../../../../kibana_react/public'; +import { XJson } from '../../../../es_ui_shared/public'; import { AggParamEditorProps } from '../agg_param_props'; @@ -60,7 +61,7 @@ function RawJsonParamEditor({ let isJsonValid = true; try { if (newValue) { - JSON.parse(newValue); + JSON.parse(XJson.collapseLiteralStrings(newValue)); } } catch (e) { isJsonValid = false; diff --git a/src/plugins/vis_types/pie/server/plugin.ts b/src/plugins/vis_types/pie/server/plugin.ts index 48576bdff5d33..49b74e63b8c3c 100644 --- a/src/plugins/vis_types/pie/server/plugin.ts +++ b/src/plugins/vis_types/pie/server/plugin.ts @@ -14,8 +14,8 @@ import { CoreSetup, Plugin, UiSettingsParams } from 'kibana/server'; import { LEGACY_PIE_CHARTS_LIBRARY } from '../common'; export const getUiSettingsConfig: () => Record> = () => ({ - // TODO: Remove this when vis_type_vislib is removed - // https://github.com/elastic/kibana/issues/56143 + // TODO: Remove this when vislib pie is removed + // https://github.com/elastic/kibana/issues/111246 [LEGACY_PIE_CHARTS_LIBRARY]: { name: i18n.translate('visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.name', { defaultMessage: 'Pie legacy charts library', @@ -33,7 +33,7 @@ export const getUiSettingsConfig: () => Record 'visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.deprecation', { defaultMessage: - 'The legacy charts library for pie in visualize is deprecated and will not be supported as of 8.0.', + 'The legacy charts library for pie in visualize is deprecated and will not be supported in a future version.', } ), docLinksKey: 'visualizationSettings', diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_editor.tsx b/src/plugins/vis_types/timeseries/public/application/components/vis_editor.tsx index 9e46427e33c2e..caf7ac638af78 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_editor.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_editor.tsx @@ -79,6 +79,15 @@ export class VisEditor extends Component ({ + [TIME_RANGE_MODE_KEY]: + this.props.vis.title && + this.props.vis.params.type !== 'timeseries' && + val.override_index_pattern + ? TIME_RANGE_DATA_MODES.LAST_VALUE + : TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + ...val, + })), }, extractedIndexPatterns: [''], }; diff --git a/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js index 6f6ddbbb7c414..2158283bb80d5 100644 --- a/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js @@ -144,7 +144,7 @@ export const TimeSeries = ({ debugState={window._echDebugStateFlag ?? false} showLegend={legend} showLegendExtra={true} - allowBrushingLastHistogramBucket={true} + allowBrushingLastHistogramBin={true} legendPosition={legendPosition} onBrushEnd={onBrushEndListener} onElementClick={(args) => handleElementClick(args)} diff --git a/src/plugins/vis_types/xy/public/components/xy_settings.tsx b/src/plugins/vis_types/xy/public/components/xy_settings.tsx index 8775ed18e6bf4..f920d405dd482 100644 --- a/src/plugins/vis_types/xy/public/components/xy_settings.tsx +++ b/src/plugins/vis_types/xy/public/components/xy_settings.tsx @@ -171,7 +171,7 @@ export const XYSettings: FC = ({ baseTheme={baseTheme} showLegend={showLegend} legendPosition={legendPosition} - allowBrushingLastHistogramBucket={isTimeChart} + allowBrushingLastHistogramBin={isTimeChart} roundHistogramBrushValues={enableHistogramMode && !isTimeChart} legendColorPicker={legendColorPicker} onElementClick={onElementClick} diff --git a/src/setup_node_env/dist.js b/src/setup_node_env/dist.js index 1d901b9ef5f06..3628a27a7793f 100644 --- a/src/setup_node_env/dist.js +++ b/src/setup_node_env/dist.js @@ -6,5 +6,5 @@ * Side Public License, v 1. */ -require('./no_transpilation'); +require('./no_transpilation_dist'); require('./polyfill'); diff --git a/src/setup_node_env/no_transpilation.js b/src/setup_node_env/no_transpilation.js index 1826f5bb0297d..b9497734b40bc 100644 --- a/src/setup_node_env/no_transpilation.js +++ b/src/setup_node_env/no_transpilation.js @@ -7,12 +7,4 @@ */ require('./ensure_node_preserve_symlinks'); - -// The following require statements MUST be executed before any others - BEGIN -require('./exit_on_warning'); -require('./harden'); -// The following require statements MUST be executed before any others - END - -require('symbol-observable'); -require('source-map-support/register'); -require('./node_version_validator'); +require('./no_transpilation_dist'); diff --git a/src/setup_node_env/no_transpilation_dist.js b/src/setup_node_env/no_transpilation_dist.js new file mode 100644 index 0000000000000..c52eba70f4ad3 --- /dev/null +++ b/src/setup_node_env/no_transpilation_dist.js @@ -0,0 +1,16 @@ +/* + * 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. + */ + +// The following require statements MUST be executed before any others - BEGIN +require('./exit_on_warning'); +require('./harden'); +// The following require statements MUST be executed before any others - END + +require('symbol-observable'); +require('source-map-support/register'); +require('./node_version_validator'); diff --git a/test/accessibility/apps/dashboard.ts b/test/accessibility/apps/dashboard.ts index 408e7d402a8f0..54eb5e7df4178 100644 --- a/test/accessibility/apps/dashboard.ts +++ b/test/accessibility/apps/dashboard.ts @@ -15,8 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const listingTable = getService('listingTable'); - // FLAKY: https://github.com/elastic/kibana/issues/105171 - describe.skip('Dashboard', () => { + describe('Dashboard', () => { const dashboardName = 'Dashboard Listing A11y'; const clonedDashboardName = 'Dashboard Listing A11y Copy'; diff --git a/test/accessibility/apps/dashboard_panel.ts b/test/accessibility/apps/dashboard_panel.ts index b2fc073949d73..83c7776049d16 100644 --- a/test/accessibility/apps/dashboard_panel.ts +++ b/test/accessibility/apps/dashboard_panel.ts @@ -14,8 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const inspector = getService('inspector'); - // FLAKY: https://github.com/elastic/kibana/issues/112920 - describe.skip('Dashboard Panel', () => { + describe('Dashboard Panel', () => { before(async () => { await PageObjects.common.navigateToApp('dashboard'); await testSubjects.click('dashboardListingTitleLink-[Flights]-Global-Flight-Dashboard'); diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index e05f3e2bc091d..867e146e64ca3 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -92,8 +92,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.saveCurrentSavedQuery(); }); - // issue - https://github.com/elastic/kibana/issues/78488 - it.skip('a11y test on saved queries list panel', async () => { + it('a11y test on saved queries list panel', async () => { await PageObjects.discover.clickSavedQueriesPopOver(); await testSubjects.moveMouseTo( 'saved-query-list-item load-saved-query-test-button saved-query-list-item-selected saved-query-list-item-selected' diff --git a/test/functional/apps/saved_objects_management/edit_saved_object.ts b/test/functional/apps/saved_objects_management/edit_saved_object.ts index 6a5e0dc394496..912976a49bcdd 100644 --- a/test/functional/apps/saved_objects_management/edit_saved_object.ts +++ b/test/functional/apps/saved_objects_management/edit_saved_object.ts @@ -49,16 +49,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const focusAndClickButton = async (buttonSubject: string) => { const button = await testSubjects.find(buttonSubject); await button.scrollIntoViewIfNecessary(); - await delay(10); + await delay(100); await button.focus(); - await delay(10); + await delay(100); await button.click(); // Allow some time for the transition/animations to occur before assuming the click is done - await delay(10); + await delay(100); }; - // FLAKY: https://github.com/elastic/kibana/issues/68400 - describe.skip('saved objects edition page', () => { + describe('saved objects edition page', () => { beforeEach(async () => { await esArchiver.load( 'test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object' @@ -119,7 +118,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { shouldUseHashForSubUrl: false, } ); - + // wait for the Edit view to load await focusAndClickButton('savedObjectEditDelete'); await PageObjects.common.clickConfirmOnModal(); diff --git a/test/functional/apps/visualize/_area_chart.ts b/test/functional/apps/visualize/_area_chart.ts index 2a5be39403002..76bb1d2f58d05 100644 --- a/test/functional/apps/visualize/_area_chart.ts +++ b/test/functional/apps/visualize/_area_chart.ts @@ -95,11 +95,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show correct chart', async function () { const xAxisLabels = [ - '2015-09-19 12:00', - '2015-09-20 12:00', - '2015-09-21 12:00', - '2015-09-22 12:00', - '2015-09-23 12:00', + '2015-09-20 00:00', + '2015-09-21 00:00', + '2015-09-22 00:00', + '2015-09-23 00:00', ]; const yAxisLabels = ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400']; const expectedAreaChartData = [ diff --git a/test/functional/apps/visualize/_point_series_options.ts b/test/functional/apps/visualize/_point_series_options.ts index dbe26ba099590..a2d2831c87933 100644 --- a/test/functional/apps/visualize/_point_series_options.ts +++ b/test/functional/apps/visualize/_point_series_options.ts @@ -230,10 +230,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('timezones', async function () { it('should show round labels in default timezone', async function () { const expectedLabels = [ - '2015-09-19 12:00', - '2015-09-20 12:00', - '2015-09-21 12:00', - '2015-09-22 12:00', + '2015-09-20 00:00', + '2015-09-21 00:00', + '2015-09-22 00:00', + '2015-09-23 00:00', ]; await initChart(); const labels = await PageObjects.visChart.getXAxisLabels(xyChartSelector); @@ -242,11 +242,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show round labels in different timezone', async function () { const expectedLabels = [ - '2015-09-19 12:00', - '2015-09-20 12:00', - '2015-09-21 12:00', - '2015-09-22 12:00', - '2015-09-23 12:00', + '2015-09-20 00:00', + '2015-09-21 00:00', + '2015-09-22 00:00', + '2015-09-23 00:00', ]; await kibanaServer.uiSettings.update({ 'dateFormat:tz': 'America/Phoenix' }); diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts index 29fdd1453b0e0..11b304cdbbf9d 100644 --- a/test/functional/page_objects/home_page.ts +++ b/test/functional/page_objects/home_page.ts @@ -13,6 +13,7 @@ export class HomePageObject extends FtrService { private readonly retry = this.ctx.getService('retry'); private readonly find = this.ctx.getService('find'); private readonly common = this.ctx.getPageObject('common'); + private readonly log = this.ctx.getService('log'); async clickSynopsis(title: string) { await this.testSubjects.click(`homeSynopsisLink${title}`); @@ -27,7 +28,10 @@ export class HomePageObject extends FtrService { } async isSampleDataSetInstalled(id: string) { - return !(await this.testSubjects.exists(`addSampleDataSet${id}`)); + const sampleDataCard = await this.testSubjects.find(`sampleDataSetCard${id}`); + const sampleDataCardInnerHTML = await sampleDataCard.getAttribute('innerHTML'); + this.log.debug(sampleDataCardInnerHTML); + return sampleDataCardInnerHTML.includes('removeSampleDataSet'); } async isWelcomeInterstitialDisplayed() { diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts index 9f48a6f57c8d8..c33e86b42692d 100644 --- a/test/functional/page_objects/management/saved_objects_page.ts +++ b/test/functional/page_objects/management/saved_objects_page.ts @@ -107,6 +107,7 @@ export class SavedObjectsPageObject extends FtrService { if (isLoaded) { return true; } else { + this.log.debug(`still waiting for the table to load ${isLoaded}`); throw new Error('Waiting'); } }); diff --git a/test/functional/services/lib/compare_pngs.ts b/test/functional/services/lib/compare_pngs.ts index fe1a1a359052b..521781c5a6d2b 100644 --- a/test/functional/services/lib/compare_pngs.ts +++ b/test/functional/services/lib/compare_pngs.ts @@ -10,26 +10,56 @@ import { parse, join } from 'path'; import Jimp from 'jimp'; import { ToolingLog } from '@kbn/dev-utils'; +interface PngDescriptor { + path: string; + + /** + * If a buffer is provided this will avoid the extra step of reading from disk + */ + buffer?: Buffer; +} + +const toDescriptor = (imageInfo: string | PngDescriptor): PngDescriptor => { + if (typeof imageInfo === 'string') { + return { path: imageInfo }; + } + return { + ...imageInfo, + }; +}; + +/** + * Override Jimp types that expect to be mapped to either string or buffer even though Jimp + * accepts both https://www.npmjs.com/package/jimp#basic-usage. + */ +const toJimp = (imageInfo: string | Buffer): Promise => { + return (Jimp.read as (value: string | Buffer) => Promise)(imageInfo); +}; + /** * Comparing pngs and writing result to provided directory * - * @param sessionPath - * @param baselinePath + * @param session + * @param baseline * @param diffPath * @param sessionDirectory * @param log * @returns Percent */ export async function comparePngs( - sessionPath: string, - baselinePath: string, + sessionInfo: string | PngDescriptor, + baselineInfo: string | PngDescriptor, diffPath: string, sessionDirectory: string, log: ToolingLog ) { - log.debug(`comparePngs: ${sessionPath} vs ${baselinePath}`); - const session = (await Jimp.read(sessionPath)).clone(); - const baseline = (await Jimp.read(baselinePath)).clone(); + const sessionDescriptor = toDescriptor(sessionInfo); + const baselineDescriptor = toDescriptor(baselineInfo); + + log.debug(`comparePngs: ${sessionDescriptor.path} vs ${baselineDescriptor.path}`); + + const session = (await toJimp(sessionDescriptor.buffer ?? sessionDescriptor.path)).clone(); + const baseline = (await toJimp(baselineDescriptor.buffer ?? baselineDescriptor.path)).clone(); if ( session.bitmap.width !== baseline.bitmap.width || @@ -63,8 +93,12 @@ export async function comparePngs( image.write(diffPath); // For debugging purposes it'll help to see the resized images and how they compare. - session.write(join(sessionDirectory, `${parse(sessionPath).name}-session-resized.png`)); - baseline.write(join(sessionDirectory, `${parse(baselinePath).name}-baseline-resized.png`)); + session.write( + join(sessionDirectory, `${parse(sessionDescriptor.path).name}-session-resized.png`) + ); + baseline.write( + join(sessionDirectory, `${parse(baselineDescriptor.path).name}-baseline-resized.png`) + ); } return percent; } diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 238b16ca1d41f..5fb284def4bdc 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -10,6 +10,7 @@ "xpack.canvas": "plugins/canvas", "xpack.cases": "plugins/cases", "xpack.cloud": "plugins/cloud", + "xpack.code": "plugins/code", "xpack.dashboard": "plugins/dashboard_enhanced", "xpack.discover": "plugins/discover_enhanced", "xpack.crossClusterReplication": "plugins/cross_cluster_replication", diff --git a/x-pack/examples/reporting_example/common/index.ts b/x-pack/examples/reporting_example/common/index.ts index ba2fcd21c8c70..893bd4dee8ae1 100644 --- a/x-pack/examples/reporting_example/common/index.ts +++ b/x-pack/examples/reporting_example/common/index.ts @@ -8,6 +8,8 @@ export const PLUGIN_ID = 'reportingExample'; export const PLUGIN_NAME = 'reportingExample'; +export { MyForwardableState } from './types'; + export { REPORTING_EXAMPLE_LOCATOR_ID, ReportingExampleLocatorDefinition, diff --git a/x-pack/examples/reporting_example/common/locator.ts b/x-pack/examples/reporting_example/common/locator.ts index fc39ec1c52654..cbb7c7d110571 100644 --- a/x-pack/examples/reporting_example/common/locator.ts +++ b/x-pack/examples/reporting_example/common/locator.ts @@ -8,6 +8,7 @@ import { SerializableRecord } from '@kbn/utility-types'; import type { LocatorDefinition } from '../../../../src/plugins/share/public'; import { PLUGIN_ID } from '../common'; +import type { MyForwardableState } from '../public/types'; export const REPORTING_EXAMPLE_LOCATOR_ID = 'REPORTING_EXAMPLE_LOCATOR_ID'; @@ -20,10 +21,11 @@ export class ReportingExampleLocatorDefinition implements LocatorDefinition<{}> '1.0.0': (state: {}) => ({ ...state, migrated: true }), }; - public readonly getLocation = async (params: {}) => { + public readonly getLocation = async (params: MyForwardableState) => { + const path = Boolean(params.captureTest) ? '/captureTest' : '/'; return { app: PLUGIN_ID, - path: '/', + path, state: params, }; }; diff --git a/x-pack/examples/reporting_example/common/types.ts b/x-pack/examples/reporting_example/common/types.ts new file mode 100644 index 0000000000000..f05ba3a274525 --- /dev/null +++ b/x-pack/examples/reporting_example/common/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Ensure, SerializableRecord } from '@kbn/utility-types'; + +export type MyForwardableState = Ensure< + SerializableRecord & { captureTest: 'A' }, + SerializableRecord +>; diff --git a/x-pack/examples/reporting_example/public/application.tsx b/x-pack/examples/reporting_example/public/application.tsx index d945048ecd73e..3e1afd7c517a2 100644 --- a/x-pack/examples/reporting_example/public/application.tsx +++ b/x-pack/examples/reporting_example/public/application.tsx @@ -7,23 +7,29 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { Router, Route, Switch } from 'react-router-dom'; import { AppMountParameters, CoreStart } from '../../../../src/core/public'; -import { ReportingExampleApp } from './components/app'; +import { CaptureTest } from './containers/capture_test'; +import { Main } from './containers/main'; +import { ApplicationContextProvider } from './application_context'; import { SetupDeps, StartDeps, MyForwardableState } from './types'; +import { ROUTES } from './constants'; export const renderApp = ( coreStart: CoreStart, deps: Omit, - { appBasePath, element }: AppMountParameters, // FIXME: appBasePath is deprecated + { appBasePath, element, history }: AppMountParameters, // FIXME: appBasePath is deprecated forwardedParams: MyForwardableState ) => { ReactDOM.render( - , + + + + } /> +
} /> + + + , element ); diff --git a/x-pack/examples/reporting_example/public/application_context.tsx b/x-pack/examples/reporting_example/public/application_context.tsx new file mode 100644 index 0000000000000..4ec16808f3f42 --- /dev/null +++ b/x-pack/examples/reporting_example/public/application_context.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext, createContext, FC } from 'react'; + +import type { MyForwardableState } from './types'; + +interface ContextValue { + forwardedState?: MyForwardableState; +} + +const ApplicationContext = createContext(undefined); + +export const ApplicationContextProvider: FC<{ forwardedState: ContextValue['forwardedState'] }> = ({ + forwardedState, + children, +}) => { + return ( + {children} + ); +}; + +export const useApplicationContext = (): ContextValue => { + const ctx = useContext(ApplicationContext); + if (!ctx) { + throw new Error('useApplicationContext called outside of ApplicationContext!'); + } + return ctx; +}; diff --git a/x-pack/examples/reporting_example/public/components/index.ts b/x-pack/examples/reporting_example/public/components/index.ts new file mode 100644 index 0000000000000..7b138d90bb0a3 --- /dev/null +++ b/x-pack/examples/reporting_example/public/components/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { TestImageA } from './test_image_a'; diff --git a/x-pack/examples/reporting_example/public/components/test_image_a.tsx b/x-pack/examples/reporting_example/public/components/test_image_a.tsx new file mode 100644 index 0000000000000..1ce94f35fdd29 --- /dev/null +++ b/x-pack/examples/reporting_example/public/components/test_image_a.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 type { FunctionComponent } from 'react'; +import React from 'react'; +import { VIS } from '../constants'; + +type Props = React.DetailedHTMLProps, HTMLImageElement>; + +export const TestImageA: FunctionComponent = ({ + width = VIS.width, + height = VIS.height, + ...restProps +}) => { + return ( + Test image + ); +}; + +const testImage = `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vP32O7LIYkuW7tdLVf/tystK65Ny8M4778hVf7+69CMWGDJEll7629eiD3T2uksbMUAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEGlwgrEF66y03iDbx7K3VG6TrrLtB6QzSTz/9zL51qeUM0u40SLX4zDPPJP+86TpXky+9yNXXWLt0dqXe98RjDxab2unxZll21iDV13/a6WfK8KOOLb6VYbvsJHvvtbvrbX388ccy7/wLlWpos/rmG0eX1rszuOOOu2SrrbcVPW6WWnIJueSvFxXPEO7JBuk//3mrbL7FVqW3se02v5eDDtyvtK6Dzl532ZNYQQABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQaWIAGadDO0csJX3TRxaVq9TRIF/rJgrLiikOLNfTsztGjrys20EpFC4PjjztaNv71hvauusbt0CBVmP/+738yccKE4uVr64IyG0U2SLWsfofta6+/LnPPNVfppzR6g7Sz1116AwwQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgQYXaPgG6UsvvSwPPfxI4dKjb8sUU0whs8wyiyy91BJdnkWpl1h94IEH5eFHHi1c+ncOWXSRnxabZPfc+x957733i7uld+9esvJKK3X4bs/nnn9ennn6WXnzrbeK32U5a+Fn/nieH5U1svL9GtEg3WbrLeXggw4oldZLuq619q/KzvT87eabyvAjD5P77ntA3v3vf0vPHTJkPplj9q8vcZzufP31N+SRRx9LqzJL4QzUPn36yKuvvS4HH3KYvPXW26XHTjrxWOnff+rid57ONdf/le7XwcSJEwuWD8nzL7xQtJvnxz+Sn/50IRk8eLqy5+Ur+voeKfi/8cabxUbvzDPNWNwXCy+8kPTq1St/etV1fQ1PPvmU3H3PvYV9P60stNBPimcsVzuD9MabbpZx476+HK7u69VXW7XDz6h1X99627/kvf+9J8N227NUQ8/oPfSQg4rrSy25uAwaNEhefvkVeaLwOtNN759mmmnl/gcekP8UvnP2//7v/2TNNVaXBx98SN4qXE433ZYpXMpWL//bWYNUL397//0PFi9Z/dPCe19kkYWLvw9p+7Ts6j0//fQz8sKLL6WnF85eXVymnHJK+ectt8mjjz4qp552ZumxlVYaKhttuEFhX01WtNPvb+3sdZc2+mbQnd/b3Gy5ZZcumE0j/yt431PY36mJvNhii3T5e5+/DtYRQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgVygYRukn3z6qey40zDRy35Wuu2+2zDZY/dhlR6Ss84+Vw47fHiHx84641S5+JJL5fbb7yg99sB9d8tMhcad3l597TXZf/+D5ZZbbyuu5/9acegKcvRRR8iss86SPySTokGqP0QbVkcd/fVlYnV9qy23kMMOPUhOOnmEHH/CyXpX8bbl734rhx92cFotLtVALdJth+23lTFjxsgFF/453dVhuduuu8iee+xaul8baltvu4O8aJpq6UG9pPIF558tP/jBHOmu4nLs2LFy8KGHy5///O0ZtfYJut3w4YfJkkssbu/udHz33ffIdjvsXGyY2SftuMN2xYbvKSNOLd2dvoNU7/jxvAuUnYH7xmsvlp7X3X297PIrVTRIBS8tXCJ32UJjT88i1mMh3fR7Y88485xSk1svpzvy8r8W388114xKT5MbrrtGhgyZv2KDVPftttvvVHpuGhxy8AGy9e+3TKvFZbX3rE849rgTxXqdc/YZxf954KeLLFFWJ195/tknio1U3Q+VXnd6fj2/t7mZWl47arT85eJLUtnicvrpB8uIU06U5Zdbtux+VhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACB7gg0ZINUm3ib/3YruavQGKt200aeNvTs7R9XXyM77Fi5carP0yaLnpmWbqlBqmdr/mq9DYtnyKXHKi1/+MM5ZdQ1V8m0005b9vCkaJB+9dVXsspqa5a+51R/oDZ511prDXn++Rdk+aGrlF6Dns14/3/uKq3rIG/qXTf6arn88itqbpC+8sqrsvY665V5lf2Awop6/uOqK8qapEcOP0ZOP+Os/Kkd1q8vvJ4FFhjS4X57h77PNX+xTlmj0z6e789aGqT17Ovc0r4GHXfWINWzel959dXS07vbIC1t2Mng7DNPkzXX/Hnp0Z5skNb7e5s3SJdeeim5887yY7n0BguD9Dtr72OMAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCNQq0JAN0i223Fpuuumfpfcw9dT95RdrrSl6dtq1144u3a+DYwpndG666SbF+/Synssst2LZ47rt4ov/TPSxSmdBalNRm4vnX/AnOfCgQ0vb6n165p7eLrn08rJtjz1muPxmk1+XnquDiAapNobW/dUvi3Vfeumlwpl6o8uaa6usspKcf+5ZpcvTrrzqGmXN09tvu7lwCdcfFrfXptxSS69QHOu/tFF31523yajR18uthTNkr7l2VFnTcb111yleslUvq/rz1VeTDz/8ULS+vQyvNiPXXOPnctttt5e9Lr3/7jv/JVNNNVXxTEn9flN723mn7YuXRr7u+hvKzt791Tpry6l//PYsWLuNjvWyusssu2LZz9L71UmbcXq51/xWS4O0nn094o+nyQsvvChXXHlV6Uem41Lv2HrrrYqXJ86bfaUnFwb6/AUXWKBbZ5Cm7bUxP+ecP5B7C5eI/vTTz9LdxeXdd/1LZp9ttuK4ngbp0BWWk0MLZxu/8sorZftHf+biP1usWPeo4YfL5JNP3umZr/qken9vOzNTr/y96s/Zdpvfy0EH7qdDbggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAtwUarkGq37W48KJLlr2Rf916k6Tvxbz0spGyx577lB6fd9555OYbv26aXnHF3zp8R6Se7TnjjDPKhAkTZM+9/1A8g7K0cWGQGqRaU2un23HHHiWbbLxRcVW/A/F3W25TbHANHDhQVipcanezzX6TnlpcRjRIywpmKyccf4ysv966Zd+XevY558mhhx1ZeqZ+N6l+R6ne9PKk++y7f+mxvfbcTXYdtnNpXZuYjz32eGn9icceFH1v6aaNaHtZ1w03XF+OO+ao4s/XM1uPOvo4Oe30M9PT5bRTT5F1fvkLubLQQNxl1z1K99sm6Pjx44uOH370kQwsfN/mbLN9v/h9qqUnZ4O8yasP/+3Ky0pNO21c77nXvmVb1dIgrXdff/zxxzLv/AuVfp499tKdlZp9+t2ym2+2abHBqd+N27dv304bjfl3kGrdjX+9oRxz9JHSu3dvefPNt4pn9drGdbLX59bTIF3j56vppsXLWW++xVbFsf6rUiOys0vsen5vK5npmbGrFb4z9v0P3pf99jtItLmebsstt4xccvGf0ypLBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQKBbAg3XIL3xppuLTbT0LvSMxj9feF5alS+++EL+b+75Sus6eOapx4rNy4MOPkzOO//C0mPaENTGYLppQzA/uzE1SM8553w55LAj0lOLl47dpnBW4LLLLlM8M1CbWtVuk7pBqj9bz2g99JADS2eQvvHGm/KzJZYpvSxrteVW28oNN95UesyeXap3dtUgzb+/9KI/ny/63aHp9mrhDNUNNvq2Saxn1OqZtY88+pissebXZ8Gm5+r3ZK622ioyX6GZPaDQGK31dv0NN8pWv9+u9PSFF/6pXPOPK0vreqncOeb89jXpA7U0SOvd1/U0SPVM5Hvv/nexuVl64YVBZ43GSg3Sm24YJfPNN29p8+FHHVvWnN5+u23kgP2/bhT3VIPU83ubN0gXW2wR+fvfvv2fFfQ7aNff8OuzxBWh0uWkSzgMEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIEuBBquQXrMscfLiD+eXnrZBx7wB9lu261L6zrYoNAssd9PeuUVl8oShcvo/uKX65VddvWyS/4iyyyzVGlbbajNv8BCZZftTA3S555/XlYYumrpuflAmzZ6edlNNv619O8/Vf5wyCV29cy4zTb9uun46aefyI03/rPszDn9odpsPOTgA0o/f931NpJ7/3Nfaf2F556UySabTH4417cNNf2eT/2+T3vrqkG65lrryMOPPGo3qTpesXBWrTZR9UzdhRddotPvLdXLturlkn/96w1Kl4XtrPAJJ54iJ550SunhvOGtD+SN4FoapPXu63oapHvsPkx2321Y6T2kQa0NUr3M7JOPP1zWYL2lcInkzTbfMpWS9L2mekdPNUg9v7d5g3SfvfeQXXbesfT+xo0bJz/44Y9L6zRISxQMEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoA6BhmuQ7rDjMPnH1deU3so5Z58h6RKg6c6999lPLv7rpWlVRpx8gqy33q9k0Z8tVfadmfbSvOnJQ1dcTZ597rm0WrrErt5x9z33ynbb79Rpc0+fo2dRXnjhOcXv9NT1dIs4g1QvxXrwQd82P7X2v/99p/x6k83SjykuX3z+qeJlWnUlv5TupX+9qNgg3Wjjry+1q8/Rhqo2Vu2tqwZp3miz21Ya27M7X3/9Ddl+x13KmtX5Ntr4O/us02X55ZbNHyqt6+Vz9TK66XbiCcfKRoVL/dpbfqZrLQ1S3b6efV1Pg/SIww+R322xuX3JxXGtDdJKl/HNG7zp+2W1cL7f3njtxbKffexxJ8opI04t3Wd/v/75z1ul3kvsen5v8wZpbqbfRTvbHHOVXjMN0hIFAwQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEKhDoOEapPq9lnoJ0XTbacft5A/77p1Wi8u8yXnDddfIkCHzdzibMG+offDBBzJkwUXKaqUzSNOdHxW+H/O2f90ud911jzzw4EPy1FNPp4dKS/u9munOSdUg1e/7XGjhn5U1bfXyo3pGq97+97/35Cc/XSy9DNlh+22LDVL7/aD5e9Qnd9Ug3fg3m8vtt99RqjvilBOLdUt3ZINZZpm5eBZvulu/b/TOu+4uNnjvf+ABue++B9JDpeX00w+Whx64t3TJ4NID3wzy71hNl/G1z8vPdK21Qao1uruve6JBqq/z2acfLztrOf+u3dVWXUXOP+8sfWqHBmm+7e+33r7srOSoBqnn95YGaXHX8S8EEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBD4jgQarkGaf9/gQj9ZUEZd+/cSx3vvvS8LLrRoaV0Hr7z0rPTp06dwad7T5JhjTyg99otfrClnnv7H0vqoUdfJNtt9e+lOfSA1D7XuZ59/Jp99+pmMKzT3FixcllZvH374oZxz7gVy8inf1tGzH59+8tGyhuGkapC+/PIrsvSyQ4uvJf3rb1deJov/7NumqF5uVS+7qje9hK3eXnzxpeLSXn61eMc3/8obpI88dF/xe1fTc/TStnqJ23TLz8bVs/oee/wJmbp/f5lm2mlkwLTTFs9q/eSTT+Tjjz8pWn7+2ecyzzw/ln79+snYsWMLlwy+WbYtnKFrbzffOFr0LMlKtzvuuEvsmbD5mYN5w1Jr1NIgrXdf5z9Pzya+9ZYbyl56V82+9ORazyDV5+tZwcsuu3TaVHbbY2+5/PIrSuv6Pbt6+WG95WdRn3XGqbLWWmsUH9Njef4FFi6O07+qNUjzyznrNp29bs/vbVdmnEGa9hZLBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQiBD4Thukp592ikwzzTQVX/dss31f5p5rLvnk009lnnkXLHvOccceJb/eaINik22fffeXK668qvS4bQDm382oT9LL1q6wwvLy5ptvySGHHl72/aP6uDZIZ5ppxg5naf75wvNkpZW+bkzef/+D8stffXtp10qXPY1okP589dVki99+fTndsePGil6q9pxzzy81O/X16u2lF56WKaaY4uuVwr//dtU/ZOdddiut28EJxx9TtLP36Tj/vtbhRx4maxcayv0LDU+tnVtqQ1Yviatnfer3jOplWm0DVS8jq5dG3XX3vWTkyCtLP27HHbaT/f7w9RnA2mBcbPGly/bBY488INNNN6j0fDvIz47Vx7bfbhvRs4q//PJL2f+AQ8rOhtTHu2qQVjojt9Z9/fnnn8vcP/66ca4/S2+jR/1Dvj/rrDJ48HTF9a6afcUnFf7VWaPx7bffkUUWWzI9rbjUpvfZZ54mP/jBHDJ69PWyy657lD1+4QXnyCorr1S87zebblE8Azo9QZvK227ze5lu0CC58M9/6XDZY9sgzS/nrD/38ksvlimn7CcDBw4sluzsdXt+b7syo0Ga9iZLBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQiBD4Thuk1V6wbaSdetqZctTR315mt9p2/7jqCll00a/Pihs3bpxs8butyxpE1bbVx9IZpHpZX3tZWn1sgcJZpFNNOaXc+5/7dLV023+/fYqXsi3dURhENEhtvc7GlS7vq2dtzjPfTypu8vijD8igQnMsv+Xf75keP+/cM2X11VYtNiC32HLrssvs6nP0jN7nX3ihrMmp9990wyiZb755Jf8eS31MvyNzzjl/IHqp3U8LZ+im2wrLLycX/+XCtFpxudPOu8pVf7+64mOV7uyqQarbePZ1foZmeg2PPnx/sUnaVbMvPb+zRmOlBmnaptJSm5jXjbpa9KxmvZ151jly+BFHVXpqxftsg/S111+XJZZcrsPz7Jmynb1u3aje39uuzGiQdtgl3IEAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIOgYZskOr70QapNlyq3fJLj+pzPytc1nXDjTaRhx95tMOm2lz84IMPyxqoD9x3d/EMUm3c6fduPlj43tFqt6WXXqp42d78rMfvokG6/nq/kmOOPrJ4ydr8NeaNK33cfjdl/vz/3He//GrdDfO7ZZedd5B99t6zeL+eMbnJpr+t+P2hdsOjhh8um2/2m+JdeobmQQcfJudf8Cf7lA5jPbPxgvPOLjahOzxo7hgzZoysv8HGFfenPm3FoSuULi+s67U0SD37Ov+uTf2ZevvrxX+S5ZdbVrpq9n397NrPINWG9OtvvFH2HbSphjZFb7x+lMwxx+zpLvniiy9kxZVWl1defbV0Xxro2b96pu9xx5+U7hLbINV9t/lvtyrzTE98+qlHZZqpp+70zNf0vHp+b7syo0GadFkigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAhEBYg1RfTDqL7bQ/niIrr7yiLLX0ChUbNZVeuH6Hon6XYrpps+bCP11UPKvzrbfeTncXl3q51z1231WWXrr8UqTpSdok1e9EvPPuu+WRhx+Vnyy0oCyz1FLF73H83ZbbVGyQ6rZ6mVBt1px19rkdGlJ6Ft2vf72B/H6r30nv3r3TjyotJ1WDVN/rT36ygCyy8MKy5po/L/ve09IPLwyuv+FG2er329m7xH7/ZNkD36xcd/0NcvjhR5XtI/2+St0u3fTsVG2oXXb5yLKzP/Xx5ZZbRnbecXtZaqny/aANrauvGVVocJ8hTz31dCpVXGpjdJVVVpI/7LOXTFv43tJabu+++26xWT76uuslHQt6Vurxxx1V/B7Uww4fXirzpwvOLR57eseP512g9Jr12HzmqcdKz6t3X+t708b9aaefUaqtRfUSxb/dfFO55NLLRc/OTbejC83jzb5pHqf7dLnLsN3lyr99+926/7z5epnnxz+S/AxSvezygQf+QXbbba/Smcz6XvQs360Kx2L6rlxb+4MPPpADDzq0dOatPn+lFVeU7bbbWp5//oWyyzHbBqnW0G2POPJoufSykbak3HDdNTJkyPydvu705Hp+b7syyxukuu/vuvO29CNZIoAAAggggAACCCCAAAIIIIAAAggggAACCCCAAALdEghtkKafPOKUE2W9dddJq66lNkf++9//yvvvf1BsDmqDbcCAAVVr6qV233zrLZlpxhmlb9++pefqmYPaNLO3p554uEOjTps8eqbpO++8U/yZehlT+52fafsXXnix+L2oun5y4Ts5r712dHpI7vz3rcXvjCzdMYkH+fek6o977pnHZaqppuryJ2tT7L333pcBAwfI9IMHV2zCqunbBY8PCy5ac5ZZZi58N+WUXdZW87fffls+L5wJOmfhOzQ7+w7aLgt984Q33nizeAZt+s7PWrfr7Hm17ut8ez0u3333v4Uzlj+T731vBvf7yutXWtfG/5tvvVlwnFP69OnYpM+3GT9+grz19lsy80wz1/R8u336Hfpq4lfFM6z79etnH+5yXM/vbZdFeQICCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggECBQd4P00MOPlLPPPq/iS4hskFb8AZ3cqQ3KQw47onSWoV5S97hjjyo28saOHVs4M/Q8OfqY40pb6yVHH3mo/PtFSw/WMFh51TU6nCGZNvsuGqTa3Bt5xZXFBu8+++5fdtarXkr1iMMPSS+HJQIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIFATqbpDqGWZffjm2ImK/fn0LZ6z1qfjYpLxTz/hceNHyy73qz1tggSHy2GOPd/jRf9h3b9lpx/LL0nZ4UpU7erpBqpev1deQ37Txe8ftt3wnZzXmP5t1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBpZoO4GaaO+qVGjr5dttt2hy5e39167F5qj21f8PtEuN/7mCQcceIg8+eRTFZ9+2qmniF4OeFLe9Lswjzr62A4/4q8X/0mWX27ZDvdzBwIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAALtLtByDVLdoXpm5ZlnnSPX33Cj6Hdgptscs88uQ4bMJ7/73W9lySUWT3c37fLCP10kI0deWfy+1EGDBsqPfvwj2arw3oYMmb9p3xMvHAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIFJKdCSDVIL9v77H8gnn34i35thhuJ3kdrHGCOAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQHsJtHyDtL12J+8WAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgg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`; diff --git a/x-pack/examples/reporting_example/public/constants.ts b/x-pack/examples/reporting_example/public/constants.ts new file mode 100644 index 0000000000000..909b656c5e514 --- /dev/null +++ b/x-pack/examples/reporting_example/public/constants.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// Values based on A4 page size +export const VIS = { + width: 1950 / 2, + height: 1200 / 2, +}; + +export const ROUTES = { + captureTest: '/captureTest', + main: '/', +}; diff --git a/x-pack/examples/reporting_example/public/containers/capture_test.scss b/x-pack/examples/reporting_example/public/containers/capture_test.scss new file mode 100644 index 0000000000000..4ecd869544b32 --- /dev/null +++ b/x-pack/examples/reporting_example/public/containers/capture_test.scss @@ -0,0 +1,10 @@ +.reportingExample { + &__captureContainer { + display: flex; + flex-direction: column; + align-items: center; + + margin-top: $euiSizeM; + margin-bottom: $euiSizeM; + } +} diff --git a/x-pack/examples/reporting_example/public/containers/capture_test.tsx b/x-pack/examples/reporting_example/public/containers/capture_test.tsx new file mode 100644 index 0000000000000..81528f8136dff --- /dev/null +++ b/x-pack/examples/reporting_example/public/containers/capture_test.tsx @@ -0,0 +1,91 @@ +/* + * 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 { FunctionComponent } from 'react'; +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { parsePath } from 'history'; +import { + EuiTabbedContent, + EuiTabbedContentTab, + EuiSpacer, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiPage, + EuiPageHeader, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, +} from '@elastic/eui'; + +import { TestImageA } from '../components'; +import { useApplicationContext } from '../application_context'; +import { MyForwardableState } from '../types'; +import { ROUTES } from '../constants'; + +import './capture_test.scss'; + +const ItemsContainer: FunctionComponent<{ count: string }> = ({ count, children }) => ( +
+ {children} +
+); + +const tabs: Array = [ + { + id: 'A', + name: 'Test A', + content: ( + + + + + + + ), + }, +]; + +export const CaptureTest: FunctionComponent = () => { + const { forwardedState } = useApplicationContext(); + const tabToRender = forwardedState?.captureTest; + const history = useHistory(); + return ( + + + + + + + + Back to main + + + + + + + tab.id === tabToRender) : undefined + } + /> + + + + + ); +}; diff --git a/x-pack/examples/reporting_example/public/components/app.tsx b/x-pack/examples/reporting_example/public/containers/main.tsx similarity index 61% rename from x-pack/examples/reporting_example/public/components/app.tsx rename to x-pack/examples/reporting_example/public/containers/main.tsx index 3e2f08fc89c7b..8673c476fdc7b 100644 --- a/x-pack/examples/reporting_example/public/components/app.tsx +++ b/x-pack/examples/reporting_example/public/containers/main.tsx @@ -23,41 +23,42 @@ import { EuiTitle, EuiCodeBlock, EuiSpacer, + EuiLink, + EuiContextMenuProps, } from '@elastic/eui'; import moment from 'moment'; import { I18nProvider } from '@kbn/i18n/react'; import React, { useEffect, useState } from 'react'; -import { BrowserRouter as Router } from 'react-router-dom'; +import { parsePath } from 'history'; +import { BrowserRouter as Router, useHistory } from 'react-router-dom'; import * as Rx from 'rxjs'; import { takeWhile } from 'rxjs/operators'; import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public'; -import { constants, ReportingStart } from '../../../../../x-pack/plugins/reporting/public'; +import { constants, ReportingStart } from '../../../../plugins/reporting/public'; import type { JobParamsPDFV2 } from '../../../../plugins/reporting/server/export_types/printable_pdf_v2/types'; import type { JobParamsPNGV2 } from '../../../../plugins/reporting/server/export_types/png_v2/types'; import { REPORTING_EXAMPLE_LOCATOR_ID } from '../../common'; -import { MyForwardableState } from '../types'; +import { useApplicationContext } from '../application_context'; +import { ROUTES } from '../constants'; +import type { MyForwardableState } from '../types'; interface ReportingExampleAppProps { basename: string; reporting: ReportingStart; screenshotMode: ScreenshotModePluginSetup; - forwardedParams?: MyForwardableState; } const sourceLogos = ['Beats', 'Cloud', 'Logging', 'Kibana']; -export const ReportingExampleApp = ({ - basename, - reporting, - screenshotMode, - forwardedParams, -}: ReportingExampleAppProps) => { +export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAppProps) => { + const history = useHistory(); + const { forwardedState } = useApplicationContext(); useEffect(() => { // eslint-disable-next-line no-console - console.log('forwardedParams', forwardedParams); - }, [forwardedParams]); + console.log('forwardedState', forwardedState); + }, [forwardedState]); // Context Menu const [isPopoverOpen, setPopover] = useState(false); @@ -123,12 +124,54 @@ export const ReportingExampleApp = ({ }; }; - const panels = [ + const getCaptureTestPNGJobParams = (): JobParamsPNGV2 => { + return { + version: '8.0.0', + layout: { + id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT, + }, + locatorParams: { + id: REPORTING_EXAMPLE_LOCATOR_ID, + version: '0.5.0', + params: { captureTest: 'A' } as MyForwardableState, + }, + objectType: 'develeloperExample', + title: 'Reporting Developer Example', + browserTimezone: moment.tz.guess(), + }; + }; + + const getCaptureTestPDFJobParams = (print: boolean) => (): JobParamsPDFV2 => { + return { + version: '8.0.0', + layout: { + id: print ? constants.LAYOUT_TYPES.PRINT : constants.LAYOUT_TYPES.PRESERVE_LAYOUT, + dimensions: { + // Magic numbers based on height of components not rendered on this screen :( + height: 2400, + width: 1822, + }, + }, + locatorParams: [ + { + id: REPORTING_EXAMPLE_LOCATOR_ID, + version: '0.5.0', + params: { captureTest: 'A' } as MyForwardableState, + }, + ], + objectType: 'develeloperExample', + title: 'Reporting Developer Example', + browserTimezone: moment.tz.guess(), + }; + }; + + const panels: EuiContextMenuProps['panels'] = [ { id: 0, items: [ { name: 'PDF Reports', icon: 'document', panel: 1 }, { name: 'PNG Reports', icon: 'document', panel: 7 }, + { name: 'Capture test', icon: 'document', panel: 8, 'data-test-subj': 'captureTestPanel' }, ], }, { @@ -141,6 +184,31 @@ export const ReportingExampleApp = ({ { name: 'Canvas Layout Option', icon: 'canvasApp', panel: 3 }, ], }, + { + id: 8, + initialFocusedItemIndex: 0, + title: 'Capture test', + items: [ + { + name: 'Capture test A - PNG', + icon: 'document', + panel: 9, + 'data-test-subj': 'captureTestPNG', + }, + { + name: 'Capture test A - PDF', + icon: 'document', + panel: 10, + 'data-test-subj': 'captureTestPDF', + }, + { + name: 'Capture test A - PDF print optimized', + icon: 'document', + panel: 11, + 'data-test-subj': 'captureTestPDFPrint', + }, + ], + }, { id: 7, initialFocusedItemIndex: 0, @@ -188,6 +256,37 @@ export const ReportingExampleApp = ({ /> ), }, + { + id: 9, + title: 'Test A', + content: ( + + ), + }, + { + id: 10, + title: 'Test A', + content: ( + + ), + }, + { + id: 11, + title: 'Test A', + content: ( + + ), + }, ]; return ( @@ -207,30 +306,45 @@ export const ReportingExampleApp = ({ - Share} - isOpen={isPopoverOpen} - closePopover={closePopover} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - + + + + Share + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + + + + + Go to capture test + + + +
- {forwardedParams ? ( + {forwardedState ? ( <>

Forwarded app state

- {JSON.stringify(forwardedParams)} + {JSON.stringify(forwardedState)} ) : ( <> diff --git a/x-pack/examples/reporting_example/public/types.ts b/x-pack/examples/reporting_example/public/types.ts index fb28293ab63a3..732de505acf76 100644 --- a/x-pack/examples/reporting_example/public/types.ts +++ b/x-pack/examples/reporting_example/public/types.ts @@ -10,6 +10,7 @@ import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public'; import { SharePluginSetup } from 'src/plugins/share/public'; import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public'; import { ReportingStart } from '../../../plugins/reporting/public'; +import type { MyForwardableState } from '../common'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PluginSetup {} @@ -26,4 +27,4 @@ export interface StartDeps { reporting: ReportingStart; } -export type MyForwardableState = Record; +export type { MyForwardableState }; diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index b3e019a1bf46d..9f3c352190213 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -57,7 +57,9 @@ export const plugin = (initContext: PluginInitializerContext) => new ActionsPlug export const config: PluginConfigDescriptor = { schema: configSchema, deprecations: ({ renameFromRoot, unused }) => [ - renameFromRoot('xpack.actions.whitelistedHosts', 'xpack.actions.allowedHosts'), + renameFromRoot('xpack.actions.whitelistedHosts', 'xpack.actions.allowedHosts', { + level: 'warning', + }), (settings, fromPath, addDeprecation) => { const actions = get(settings, fromPath); const customHostSettings = actions?.customHostSettings ?? []; @@ -69,6 +71,7 @@ export const config: PluginConfigDescriptor = { ) ) { addDeprecation({ + level: 'warning', configPath: 'xpack.actions.customHostSettings.ssl.rejectUnauthorized', message: `"xpack.actions.customHostSettings[].ssl.rejectUnauthorized" is deprecated.` + @@ -97,6 +100,7 @@ export const config: PluginConfigDescriptor = { const actions = get(settings, fromPath); if (actions?.hasOwnProperty('rejectUnauthorized')) { addDeprecation({ + level: 'warning', configPath: `${fromPath}.rejectUnauthorized`, message: `"xpack.actions.rejectUnauthorized" is deprecated. Use "xpack.actions.verificationMode" instead, ` + @@ -124,6 +128,7 @@ export const config: PluginConfigDescriptor = { const actions = get(settings, fromPath); if (actions?.hasOwnProperty('proxyRejectUnauthorizedCertificates')) { addDeprecation({ + level: 'warning', configPath: `${fromPath}.proxyRejectUnauthorizedCertificates`, message: `"xpack.actions.proxyRejectUnauthorizedCertificates" is deprecated. Use "xpack.actions.proxyVerificationMode" instead, ` + diff --git a/x-pack/plugins/actions/server/usage/actions_telemetry.ts b/x-pack/plugins/actions/server/usage/actions_telemetry.ts index 1cb6bf8bfc74c..803a2122fe7f8 100644 --- a/x-pack/plugins/actions/server/usage/actions_telemetry.ts +++ b/x-pack/plugins/actions/server/usage/actions_telemetry.ts @@ -43,6 +43,7 @@ export async function getTotalCount( const { body: searchResult } = await esClient.search({ index: kibanaIndex, + size: 0, body: { query: { bool: { @@ -224,6 +225,7 @@ export async function getInUseTotalCount( const { body: actionResults } = await esClient.search({ index: kibanaIndex, + size: 0, body: { query: { bool: { diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index b7a510a37661b..a57f165898d1d 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -49,14 +49,16 @@ export const plugin = (initContext: PluginInitializerContext) => new AlertingPlu export const config: PluginConfigDescriptor = { schema: configSchema, deprecations: ({ renameFromRoot }) => [ - renameFromRoot('xpack.alerts.healthCheck', 'xpack.alerting.healthCheck'), + renameFromRoot('xpack.alerts.healthCheck', 'xpack.alerting.healthCheck', { level: 'warning' }), renameFromRoot( 'xpack.alerts.invalidateApiKeysTask.interval', - 'xpack.alerting.invalidateApiKeysTask.interval' + 'xpack.alerting.invalidateApiKeysTask.interval', + { level: 'warning' } ), renameFromRoot( 'xpack.alerts.invalidateApiKeysTask.removalDelay', - 'xpack.alerting.invalidateApiKeysTask.removalDelay' + 'xpack.alerting.invalidateApiKeysTask.removalDelay', + { level: 'warning' } ), (settings, fromPath, addDeprecation) => { const alerting = get(settings, fromPath); diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts index 15fa6e63ac561..03a96d19b8e8a 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts @@ -7,7 +7,7 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from '../../../../../src/core/server/elasticsearch/client/mocks'; -import { getTotalCountInUse } from './alerts_telemetry'; +import { getTotalCountAggregations, getTotalCountInUse } from './alerts_telemetry'; describe('alerts telemetry', () => { test('getTotalCountInUse should replace first "." symbol to "__" in alert types names', async () => { @@ -49,6 +49,69 @@ Object { "countNamespaces": 1, "countTotal": 4, } +`); + }); + + test('getTotalCountAggregations should return min/max connectors in use', async () => { + const mockEsClient = elasticsearchClientMock.createClusterClient().asScoped().asInternalUser; + mockEsClient.search.mockReturnValue( + // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + elasticsearchClientMock.createSuccessTransportRequestPromise({ + aggregations: { + byAlertTypeId: { + value: { + ruleTypes: { + '.index-threshold': 2, + 'logs.alert.document.count': 1, + 'document.test.': 1, + }, + }, + }, + max_throttle_time: { value: 60 }, + min_throttle_time: { value: 0 }, + avg_throttle_time: { value: 30 }, + max_interval_time: { value: 10 }, + min_interval_time: { value: 1 }, + avg_interval_time: { value: 4.5 }, + max_actions_count: { value: 4 }, + min_actions_count: { value: 0 }, + avg_actions_count: { value: 2.5 }, + }, + hits: { + hits: [], + }, + }) + ); + + const telemetry = await getTotalCountAggregations(mockEsClient, 'test'); + + expect(mockEsClient.search).toHaveBeenCalledTimes(1); + + expect(telemetry).toMatchInlineSnapshot(` +Object { + "connectors_per_alert": Object { + "avg": 2.5, + "max": 4, + "min": 0, + }, + "count_by_type": Object { + "__index-threshold": 2, + "document.test__": 1, + "logs.alert.document.count": 1, + }, + "count_rules_namespaces": 0, + "count_total": 4, + "schedule_time": Object { + "avg": 4.5, + "max": 10, + "min": 1, + }, + "throttle_time": Object { + "avg": 30, + "max": 60, + "min": 0, + }, +} `); }); }); diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts index 18fa9b590b4e1..7ff9538c1aa26 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts @@ -52,218 +52,128 @@ export async function getTotalCountAggregations( | 'count_rules_namespaces' > > { - const throttleTimeMetric = { - scripted_metric: { - init_script: 'state.min = 0; state.max = 0; state.totalSum = 0; state.totalCount = 0;', - map_script: ` - if (doc['alert.throttle'].size() > 0) { - def throttle = doc['alert.throttle'].value; + const { body: results } = await esClient.search({ + index: kibanaInex, + body: { + size: 0, + query: { + bool: { + filter: [{ term: { type: 'alert' } }], + }, + }, + runtime_mappings: { + alert_action_count: { + type: 'long', + script: { + source: ` + def alert = params._source['alert']; + if (alert != null) { + def actions = alert.actions; + if (actions != null) { + emit(actions.length); + } else { + emit(0); + } + }`, + }, + }, + alert_interval: { + type: 'long', + script: { + source: ` + int parsed = 0; + if (doc['alert.schedule.interval'].size() > 0) { + def interval = doc['alert.schedule.interval'].value; - if (throttle.length() > 1) { - // get last char - String timeChar = throttle.substring(throttle.length() - 1); - // remove last char - throttle = throttle.substring(0, throttle.length() - 1); + if (interval.length() > 1) { + // get last char + String timeChar = interval.substring(interval.length() - 1); + // remove last char + interval = interval.substring(0, interval.length() - 1); - if (throttle.chars().allMatch(Character::isDigit)) { - // using of regex is not allowed in painless language - int parsed = Integer.parseInt(throttle); + if (interval.chars().allMatch(Character::isDigit)) { + // using of regex is not allowed in painless language + parsed = Integer.parseInt(interval); - if (timeChar.equals("s")) { - parsed = parsed; - } else if (timeChar.equals("m")) { - parsed = parsed * 60; - } else if (timeChar.equals("h")) { - parsed = parsed * 60 * 60; - } else if (timeChar.equals("d")) { - parsed = parsed * 24 * 60 * 60; - } - if (state.min === 0 || parsed < state.min) { - state.min = parsed; - } - if (parsed > state.max) { - state.max = parsed; + if (timeChar.equals("s")) { + parsed = parsed; + } else if (timeChar.equals("m")) { + parsed = parsed * 60; + } else if (timeChar.equals("h")) { + parsed = parsed * 60 * 60; + } else if (timeChar.equals("d")) { + parsed = parsed * 24 * 60 * 60; + } + emit(parsed); + } } - state.totalSum += parsed; - state.totalCount++; } - } - } - `, - // Combine script is executed per cluster, but we already have a key-value pair per cluster. - // Despite docs that say this is optional, this script can't be blank. - combine_script: 'return state', - // Reduce script is executed across all clusters, so we need to add up all the total from each cluster - // This also needs to account for having no data - reduce_script: ` - double min = 0; - double max = 0; - long totalSum = 0; - long totalCount = 0; - for (Map m : states.toArray()) { - if (m !== null) { - min = min > 0 ? Math.min(min, m.min) : m.min; - max = Math.max(max, m.max); - totalSum += m.totalSum; - totalCount += m.totalCount; - } - } - Map result = new HashMap(); - result.min = min; - result.max = max; - result.totalSum = totalSum; - result.totalCount = totalCount; - return result; - `, - }, - }; - - const intervalTimeMetric = { - scripted_metric: { - init_script: 'state.min = 0; state.max = 0; state.totalSum = 0; state.totalCount = 0;', - map_script: ` - if (doc['alert.schedule.interval'].size() > 0) { - def interval = doc['alert.schedule.interval'].value; + emit(parsed); + `, + }, + }, + alert_throttle: { + type: 'long', + script: { + source: ` + int parsed = 0; + if (doc['alert.throttle'].size() > 0) { + def throttle = doc['alert.throttle'].value; - if (interval.length() > 1) { - // get last char - String timeChar = interval.substring(interval.length() - 1); - // remove last char - interval = interval.substring(0, interval.length() - 1); + if (throttle.length() > 1) { + // get last char + String timeChar = throttle.substring(throttle.length() - 1); + // remove last char + throttle = throttle.substring(0, throttle.length() - 1); - if (interval.chars().allMatch(Character::isDigit)) { - // using of regex is not allowed in painless language - int parsed = Integer.parseInt(interval); + if (throttle.chars().allMatch(Character::isDigit)) { + // using of regex is not allowed in painless language + parsed = Integer.parseInt(throttle); - if (timeChar.equals("s")) { - parsed = parsed; - } else if (timeChar.equals("m")) { - parsed = parsed * 60; - } else if (timeChar.equals("h")) { - parsed = parsed * 60 * 60; - } else if (timeChar.equals("d")) { - parsed = parsed * 24 * 60 * 60; - } - if (state.min === 0 || parsed < state.min) { - state.min = parsed; - } - if (parsed > state.max) { - state.max = parsed; - } - state.totalSum += parsed; - state.totalCount++; + if (timeChar.equals("s")) { + parsed = parsed; + } else if (timeChar.equals("m")) { + parsed = parsed * 60; + } else if (timeChar.equals("h")) { + parsed = parsed * 60 * 60; + } else if (timeChar.equals("d")) { + parsed = parsed * 24 * 60 * 60; + } + emit(parsed); + } } - } - } - `, - // Combine script is executed per cluster, but we already have a key-value pair per cluster. - // Despite docs that say this is optional, this script can't be blank. - combine_script: 'return state', - // Reduce script is executed across all clusters, so we need to add up all the total from each cluster - // This also needs to account for having no data - reduce_script: ` - double min = 0; - double max = 0; - long totalSum = 0; - long totalCount = 0; - for (Map m : states.toArray()) { - if (m !== null) { - min = min > 0 ? Math.min(min, m.min) : m.min; - max = Math.max(max, m.max); - totalSum += m.totalSum; - totalCount += m.totalCount; - } - } - Map result = new HashMap(); - result.min = min; - result.max = max; - result.totalSum = totalSum; - result.totalCount = totalCount; - return result; - `, - }, - }; - - const connectorsMetric = { - scripted_metric: { - init_script: - 'state.currentAlertActions = 0; state.min = 0; state.max = 0; state.totalActionsCount = 0;', - map_script: ` - String refName = doc['alert.actions.actionRef'].value; - if (refName == 'action_0') { - if (state.currentAlertActions !== 0 && state.currentAlertActions < state.min) { - state.min = state.currentAlertActions; - } - if (state.currentAlertActions !== 0 && state.currentAlertActions > state.max) { - state.max = state.currentAlertActions; - } - state.currentAlertActions = 1; - } else { - state.currentAlertActions++; - } - state.totalActionsCount++; - `, - // Combine script is executed per cluster, but we already have a key-value pair per cluster. - // Despite docs that say this is optional, this script can't be blank. - combine_script: 'return state', - // Reduce script is executed across all clusters, so we need to add up all the total from each cluster - // This also needs to account for having no data - reduce_script: ` - double min = 0; - double max = 0; - long totalActionsCount = 0; - long currentAlertActions = 0; - for (Map m : states.toArray()) { - if (m !== null) { - min = min > 0 ? Math.min(min, m.min) : m.min; - max = Math.max(max, m.max); - currentAlertActions += m.currentAlertActions; - totalActionsCount += m.totalActionsCount; - } - } - Map result = new HashMap(); - result.min = min; - result.max = max; - result.currentAlertActions = currentAlertActions; - result.totalActionsCount = totalActionsCount; - return result; - `, - }, - }; - - const { body: results } = await esClient.search({ - index: kibanaInex, - body: { - query: { - bool: { - filter: [{ term: { type: 'alert' } }], + } + emit(parsed); + `, + }, }, }, aggs: { byAlertTypeId: alertTypeMetric, - throttleTime: throttleTimeMetric, - intervalTime: intervalTimeMetric, - connectorsAgg: { - nested: { - path: 'alert.actions', - }, - aggs: { - connectors: connectorsMetric, - }, - }, + max_throttle_time: { max: { field: 'alert_throttle' } }, + min_throttle_time: { min: { field: 'alert_throttle' } }, + avg_throttle_time: { avg: { field: 'alert_throttle' } }, + max_interval_time: { max: { field: 'alert_interval' } }, + min_interval_time: { min: { field: 'alert_interval' } }, + avg_interval_time: { avg: { field: 'alert_interval' } }, + max_actions_count: { max: { field: 'alert_action_count' } }, + min_actions_count: { min: { field: 'alert_action_count' } }, + avg_actions_count: { avg: { field: 'alert_action_count' } }, }, }, }); const aggregations = results.aggregations as { byAlertTypeId: { value: { ruleTypes: Record } }; - throttleTime: { value: { min: number; max: number; totalCount: number; totalSum: number } }; - intervalTime: { value: { min: number; max: number; totalCount: number; totalSum: number } }; - connectorsAgg: { - connectors: { - value: { min: number; max: number; totalActionsCount: number; totalAlertsCount: number }; - }; - }; + max_throttle_time: { value: number }; + min_throttle_time: { value: number }; + avg_throttle_time: { value: number }; + max_interval_time: { value: number }; + min_interval_time: { value: number }; + avg_interval_time: { value: number }; + max_actions_count: { value: number }; + min_actions_count: { value: number }; + avg_actions_count: { value: number }; }; const totalAlertsCount = Object.keys(aggregations.byAlertTypeId.value.ruleTypes).reduce( @@ -284,30 +194,19 @@ export async function getTotalCountAggregations( {} ), throttle_time: { - min: `${aggregations.throttleTime.value.min}s`, - avg: `${ - aggregations.throttleTime.value.totalCount > 0 - ? aggregations.throttleTime.value.totalSum / aggregations.throttleTime.value.totalCount - : 0 - }s`, - max: `${aggregations.throttleTime.value.max}s`, + min: aggregations.min_throttle_time.value, + avg: aggregations.avg_throttle_time.value, + max: aggregations.max_throttle_time.value, }, schedule_time: { - min: `${aggregations.intervalTime.value.min}s`, - avg: `${ - aggregations.intervalTime.value.totalCount > 0 - ? aggregations.intervalTime.value.totalSum / aggregations.intervalTime.value.totalCount - : 0 - }s`, - max: `${aggregations.intervalTime.value.max}s`, + min: aggregations.min_interval_time.value, + avg: aggregations.avg_interval_time.value, + max: aggregations.max_interval_time.value, }, connectors_per_alert: { - min: aggregations.connectorsAgg.connectors.value.min, - avg: - totalAlertsCount > 0 - ? aggregations.connectorsAgg.connectors.value.totalActionsCount / totalAlertsCount - : 0, - max: aggregations.connectorsAgg.connectors.value.max, + min: aggregations.min_actions_count.value, + avg: aggregations.avg_actions_count.value, + max: aggregations.max_actions_count.value, }, count_rules_namespaces: 0, }; @@ -316,6 +215,7 @@ export async function getTotalCountAggregations( export async function getTotalCountInUse(esClient: ElasticsearchClient, kibanaInex: string) { const { body: searchResult } = await esClient.search({ index: kibanaInex, + size: 0, body: { query: { bool: { diff --git a/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts b/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts index ecea721dfad92..e9405c51dbf15 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts @@ -75,14 +75,14 @@ export function createAlertsUsageCollector( count_active_total: 0, count_disabled_total: 0, throttle_time: { - min: '0s', - avg: '0s', - max: '0s', + min: 0, + avg: 0, + max: 0, }, schedule_time: { - min: '0s', - avg: '0s', - max: '0s', + min: 0, + avg: 0, + max: 0, }, connectors_per_alert: { min: 0, @@ -100,14 +100,14 @@ export function createAlertsUsageCollector( count_active_total: { type: 'long' }, count_disabled_total: { type: 'long' }, throttle_time: { - min: { type: 'keyword' }, - avg: { type: 'keyword' }, - max: { type: 'keyword' }, + min: { type: 'long' }, + avg: { type: 'float' }, + max: { type: 'long' }, }, schedule_time: { - min: { type: 'keyword' }, - avg: { type: 'keyword' }, - max: { type: 'keyword' }, + min: { type: 'long' }, + avg: { type: 'float' }, + max: { type: 'long' }, }, connectors_per_alert: { min: { type: 'long' }, diff --git a/x-pack/plugins/alerting/server/usage/types.ts b/x-pack/plugins/alerting/server/usage/types.ts index 5e420b54e37cb..0e489893a1bbc 100644 --- a/x-pack/plugins/alerting/server/usage/types.ts +++ b/x-pack/plugins/alerting/server/usage/types.ts @@ -13,14 +13,14 @@ export interface AlertsUsage { count_active_by_type: Record; count_rules_namespaces: number; throttle_time: { - min: string; - avg: string; - max: string; + min: number; + avg: number; + max: number; }; schedule_time: { - min: string; - avg: string; - max: string; + min: number; + avg: number; + max: number; }; connectors_per_alert: { min: number; diff --git a/x-pack/plugins/apm/common/runtime_types/comparison_type_rt.ts b/x-pack/plugins/apm/common/runtime_types/comparison_type_rt.ts new file mode 100644 index 0000000000000..93c0a31c40cde --- /dev/null +++ b/x-pack/plugins/apm/common/runtime_types/comparison_type_rt.ts @@ -0,0 +1,21 @@ +/* + * 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 * as t from 'io-ts'; + +export enum TimeRangeComparisonEnum { + WeekBefore = 'week', + DayBefore = 'day', + PeriodBefore = 'period', +} + +export const comparisonTypeRt = t.union([ + t.literal('day'), + t.literal('week'), + t.literal('period'), +]); + +export type TimeRangeComparisonType = t.TypeOf; diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx index be493f8a98b1c..57efea4ffdcac 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx @@ -74,7 +74,7 @@ export function BackendDetailDependenciesTable() { serviceName={location.serviceName} agentName={location.agentName} query={{ - comparisonEnabled: comparisonEnabled ? 'true' : 'false', + comparisonEnabled, comparisonType, environment, kuery, diff --git a/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx index 05eb9892fc108..c214c4348bbe7 100644 --- a/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx @@ -68,7 +68,7 @@ export function BackendInventoryDependenciesTable() { type={location.spanType} subtype={location.spanSubtype} query={{ - comparisonEnabled: comparisonEnabled ? 'true' : 'false', + comparisonEnabled, comparisonType, environment, kuery, diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.stories.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.stories.tsx index 4601f1db0277d..4efc00ef71b91 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.stories.tsx @@ -12,6 +12,7 @@ import { ApmPluginContextValue, } from '../../../../context/apm_plugin/apm_plugin_context'; import { ErrorDistribution } from './'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; export default { title: 'app/ErrorGroupDetails/Distribution', @@ -39,9 +40,26 @@ export default { export function Example() { const distribution = { - noHits: false, bucketSize: 62350, - buckets: [ + currentPeriod: [ + { key: 1624279912350, count: 6 }, + { key: 1624279974700, count: 1 }, + { key: 1624280037050, count: 2 }, + { key: 1624280099400, count: 3 }, + { key: 1624280161750, count: 13 }, + { key: 1624280224100, count: 1 }, + { key: 1624280286450, count: 2 }, + { key: 1624280348800, count: 0 }, + { key: 1624280411150, count: 4 }, + { key: 1624280473500, count: 4 }, + { key: 1624280535850, count: 1 }, + { key: 1624280598200, count: 4 }, + { key: 1624280660550, count: 0 }, + { key: 1624280722900, count: 2 }, + { key: 1624280785250, count: 3 }, + { key: 1624280847600, count: 0 }, + ], + previousPeriod: [ { key: 1624279912350, count: 6 }, { key: 1624279974700, count: 1 }, { key: 1624280037050, count: 2 }, @@ -61,16 +79,23 @@ export function Example() { ], }; - return ; + return ( + + ); } export function EmptyState() { return ( diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx index 3d1d0ee564ba4..429ad989b9738 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx @@ -8,28 +8,30 @@ import { Axis, Chart, - HistogramBarSeries, + BarSeries, niceTimeFormatter, Position, ScaleType, Settings, - SettingsSpec, - TooltipValue, } from '@elastic/charts'; import { EuiTitle } from '@elastic/eui'; -import d3 from 'd3'; import React, { Suspense, useState } from 'react'; import type { ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_TYPED } from '@kbn/rule-data-utils'; // @ts-expect-error import { ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED } from '@kbn/rule-data-utils/target_node/technical_field_names'; +import { i18n } from '@kbn/i18n'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { asRelativeDateTimeRange } from '../../../../../common/utils/formatters'; +import { offsetPreviousPeriodCoordinates } from '../../../../../common/utils/offset_previous_period_coordinate'; import { useTheme } from '../../../../hooks/use_theme'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { AlertType } from '../../../../../common/alert_types'; import { getAlertAnnotations } from '../../../shared/charts/helper/get_alert_annotations'; +import { ChartContainer } from '../../../shared/charts/chart_container'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { LazyAlertsFlyout } from '../../../../../../observability/public'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { Coordinate } from '../../../../../typings/timeseries'; const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = ALERT_RULE_TYPE_ID_NON_TYPED; @@ -37,70 +39,85 @@ const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = type ErrorDistributionAPIResponse = APIReturnType<'GET /internal/apm/services/{serviceName}/errors/distribution'>; -interface FormattedBucket { - x0: number; - x: number; - y: number | undefined; -} - -export function getFormattedBuckets( - buckets: ErrorDistributionAPIResponse['buckets'], - bucketSize: number -): FormattedBucket[] { +export function getCoordinatedBuckets( + buckets: + | ErrorDistributionAPIResponse['currentPeriod'] + | ErrorDistributionAPIResponse['previousPeriod'] +): Coordinate[] { return buckets.map(({ count, key }) => { return { - x0: key, - x: key + bucketSize, + x: key, y: count, }; }); } - interface Props { + fetchStatus: FETCH_STATUS; distribution: ErrorDistributionAPIResponse; title: React.ReactNode; } -export function ErrorDistribution({ distribution, title }: Props) { +export function ErrorDistribution({ distribution, title, fetchStatus }: Props) { const theme = useTheme(); - const buckets = getFormattedBuckets( - distribution.buckets, - distribution.bucketSize - ); + const currentPeriod = getCoordinatedBuckets(distribution.currentPeriod); + const previousPeriod = getCoordinatedBuckets(distribution.previousPeriod); - const xMin = d3.min(buckets, (d) => d.x0); - const xMax = d3.max(buckets, (d) => d.x0); + const { urlParams } = useUrlParams(); + const { comparisonEnabled } = urlParams; - const xFormatter = niceTimeFormatter([xMin, xMax]); - const { observabilityRuleTypeRegistry } = useApmPluginContext(); + const timeseries = [ + { + data: currentPeriod, + color: theme.eui.euiColorVis1, + title: i18n.translate('xpack.apm.errorGroup.chart.ocurrences', { + defaultMessage: 'Occurences', + }), + }, + ...(comparisonEnabled + ? [ + { + data: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: currentPeriod, + previousPeriodTimeseries: previousPeriod, + }), + color: theme.eui.euiColorMediumShade, + title: i18n.translate( + 'xpack.apm.errorGroup.chart.ocurrences.previousPeriodLabel', + { defaultMessage: 'Previous period' } + ), + }, + ] + : []), + ]; + + const xValues = timeseries.flatMap(({ data }) => data.map(({ x }) => x)); + const min = Math.min(...xValues); + const max = Math.max(...xValues); + + const xFormatter = niceTimeFormatter([min, max]); + const { observabilityRuleTypeRegistry } = useApmPluginContext(); const { alerts } = useApmServiceContext(); const { getFormatter } = observabilityRuleTypeRegistry; const [selectedAlertId, setSelectedAlertId] = useState( undefined ); - const tooltipProps: SettingsSpec['tooltip'] = { - stickTo: 'top', - headerFormatter: (tooltip: TooltipValue) => { - const serie = buckets.find((bucket) => bucket.x0 === tooltip.value); - if (serie) { - return asRelativeDateTimeRange(serie.x0, serie.x); - } - return `${tooltip.value}`; - }, - }; - return ( <> {title} -
+ - + + {timeseries.map((serie) => { + return ( + + ); + })} {getAlertAnnotations({ alerts: alerts?.filter( (alert) => alert[ALERT_RULE_TYPE_ID]?.[0] === AlertType.ErrorCount ), - chartStartTime: buckets[0]?.x0, + chartStartTime: xValues[0], getFormatter, selectedAlertId, setSelectedAlertId, @@ -150,7 +172,7 @@ export function ErrorDistribution({ distribution, title }: Props) { /> -
+ ); } diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx index 0114348892984..bc12b0c64f179 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx @@ -146,7 +146,7 @@ export function ErrorGroupDetails() { [environment, kuery, serviceName, start, end, groupId] ); - const { errorDistributionData } = useErrorGroupDistributionFetcher({ + const { errorDistributionData, status } = useErrorGroupDistributionFetcher({ serviceName, groupId, environment, @@ -209,6 +209,7 @@ export function ErrorGroupDetails() { )} - - - - - + + + + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx index 945d977e30362..d62955b593df1 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx @@ -13,6 +13,7 @@ import { MemoryRouter } from 'react-router-dom'; import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; import { ServiceHealthStatus } from '../../../../common/service_health_status'; +import { TimeRangeComparisonEnum } from '../../../../common/runtime_types/comparison_type_rt'; import { ServiceInventory } from '.'; import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { @@ -25,7 +26,6 @@ import * as useDynamicIndexPatternHooks from '../../../hooks/use_dynamic_index_p import { SessionStorageMock } from '../../../services/__mocks__/SessionStorageMock'; import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; import * as hook from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context'; -import { TimeRangeComparisonType } from '../../shared/time_comparison/get_time_range_comparison'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiCounter: () => {} }, @@ -60,7 +60,7 @@ function wrapper({ children }: { children?: ReactNode }) { start: '2021-02-12T13:20:43.344Z', end: '2021-02-12T13:20:58.344Z', comparisonEnabled: true, - comparisonType: TimeRangeComparisonType.DayBefore, + comparisonType: TimeRangeComparisonEnum.DayBefore, }} > {children} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index c73d412fb4506..557854dd2692b 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -16,7 +16,7 @@ import { ChartPointerEventContextProvider } from '../../../context/chart_pointer import { useBreakpoints } from '../../../hooks/use_breakpoints'; import { LatencyChart } from '../../shared/charts/latency_chart'; import { TransactionBreakdownChart } from '../../shared/charts/transaction_breakdown_chart'; -import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart'; +import { FailedTransactionRateChart } from '../../shared/charts/failed_transaction_rate_chart'; import { ServiceOverviewDependenciesTable } from './service_overview_dependencies_table'; import { ServiceOverviewErrorsTable } from './service_overview_errors_table'; import { ServiceOverviewInstancesChartAndTable } from './service_overview_instances_chart_and_table'; @@ -138,7 +138,7 @@ export function ServiceOverview() { > {!isRumAgent && ( - , params: t.partial({ query: t.partial({ - comparisonEnabled: t.string, - comparisonType: t.string, + comparisonEnabled: toBooleanRt, + comparisonType: comparisonTypeRt, }), }), children: [ diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx index 16cba23da6423..4afa10cbf9a5d 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx @@ -8,6 +8,8 @@ import * as t from 'io-ts'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { Outlet } from '@kbn/typed-react-router-config'; +import { toBooleanRt } from '@kbn/io-ts-utils'; +import { comparisonTypeRt } from '../../../../common/runtime_types/comparison_type_rt'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { environmentRt } from '../../../../common/environment_rt'; import { ServiceOverview } from '../../app/service_overview'; @@ -79,8 +81,8 @@ export const serviceDetail = { kuery: t.string, }), t.partial({ - comparisonEnabled: t.string, - comparisonType: t.string, + comparisonEnabled: toBooleanRt, + comparisonType: comparisonTypeRt, latencyAggregationType: t.string, transactionType: t.string, refreshPaused: t.union([t.literal('true'), t.literal('false')]), @@ -162,6 +164,9 @@ export const serviceDetail = { defaultMessage: 'Errors', }), element: , + searchBarOptions: { + showTimeComparison: true, + }, }), params: t.partial({ query: t.partial({ diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/failed_transaction_rate_chart/index.tsx similarity index 99% rename from x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/failed_transaction_rate_chart/index.tsx index 95b73a5276b8a..cf57f618940b4 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/failed_transaction_rate_chart/index.tsx @@ -51,7 +51,7 @@ const INITIAL_STATE: ErrorRate = { }, }; -export function TransactionErrorRateChart({ +export function FailedTransactionRateChart({ height, showAnnotations = true, environment, diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx index 4fdce0dfa705e..9ff128657dbb1 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx @@ -12,7 +12,7 @@ import { ChartPointerEventContextProvider } from '../../../../context/chart_poin import { ServiceOverviewThroughputChart } from '../../../app/service_overview/service_overview_throughput_chart'; import { LatencyChart } from '../latency_chart'; import { TransactionBreakdownChart } from '../transaction_breakdown_chart'; -import { TransactionErrorRateChart } from '../transaction_error_rate_chart/'; +import { FailedTransactionRateChart } from '../failed_transaction_rate_chart'; export function TransactionCharts({ kuery, @@ -55,7 +55,7 @@ export function TransactionCharts({ - diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx index 695c941c61ed4..19abd2059c903 100644 --- a/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx +++ b/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx @@ -50,7 +50,7 @@ export function IconPopover({ } diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_types.ts b/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_types.ts index 0115718ac07a9..97754cd91fd3e 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_types.ts +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_types.ts @@ -6,8 +6,8 @@ */ import moment from 'moment'; +import { TimeRangeComparisonEnum } from '../../../../common/runtime_types/comparison_type_rt'; import { getDateDifference } from '../../../../common/utils/formatters'; -import { TimeRangeComparisonType } from './get_time_range_comparison'; export function getComparisonTypes({ start, @@ -29,17 +29,17 @@ export function getComparisonTypes({ // Less than or equals to one day if (dateDiff <= 1) { return [ - TimeRangeComparisonType.DayBefore, - TimeRangeComparisonType.WeekBefore, + TimeRangeComparisonEnum.DayBefore, + TimeRangeComparisonEnum.WeekBefore, ]; } // Less than or equals to one week if (dateDiff <= 7) { - return [TimeRangeComparisonType.WeekBefore]; + return [TimeRangeComparisonEnum.WeekBefore]; } // } // above one week or when rangeTo is not "now" - return [TimeRangeComparisonType.PeriodBefore]; + return [TimeRangeComparisonEnum.PeriodBefore]; } diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/get_time_range_comparison.test.ts b/x-pack/plugins/apm/public/components/shared/time_comparison/get_time_range_comparison.test.ts index da903e42bd3c7..7e67d76c2ada2 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/get_time_range_comparison.test.ts +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/get_time_range_comparison.test.ts @@ -4,10 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { - getTimeRangeComparison, - TimeRangeComparisonType, -} from './get_time_range_comparison'; +import { TimeRangeComparisonEnum } from '../../../../common/runtime_types/comparison_type_rt'; +import { getTimeRangeComparison } from './get_time_range_comparison'; describe('getTimeRangeComparison', () => { describe('return empty object', () => { @@ -16,7 +14,7 @@ describe('getTimeRangeComparison', () => { const result = getTimeRangeComparison({ start: undefined, end, - comparisonType: TimeRangeComparisonType.DayBefore, + comparisonType: TimeRangeComparisonEnum.DayBefore, comparisonEnabled: false, }); expect(result).toEqual({}); @@ -26,7 +24,7 @@ describe('getTimeRangeComparison', () => { const result = getTimeRangeComparison({ start: undefined, end, - comparisonType: TimeRangeComparisonType.DayBefore, + comparisonType: TimeRangeComparisonEnum.DayBefore, comparisonEnabled: true, }); expect(result).toEqual({}); @@ -37,7 +35,7 @@ describe('getTimeRangeComparison', () => { const result = getTimeRangeComparison({ start, end: undefined, - comparisonType: TimeRangeComparisonType.DayBefore, + comparisonType: TimeRangeComparisonEnum.DayBefore, comparisonEnabled: true, }); expect(result).toEqual({}); @@ -50,7 +48,7 @@ describe('getTimeRangeComparison', () => { const start = '2021-01-28T14:45:00.000Z'; const end = '2021-01-28T15:00:00.000Z'; const result = getTimeRangeComparison({ - comparisonType: TimeRangeComparisonType.DayBefore, + comparisonType: TimeRangeComparisonEnum.DayBefore, comparisonEnabled: true, start, end, @@ -65,7 +63,7 @@ describe('getTimeRangeComparison', () => { const start = '2021-01-28T14:45:00.000Z'; const end = '2021-01-28T15:00:00.000Z'; const result = getTimeRangeComparison({ - comparisonType: TimeRangeComparisonType.WeekBefore, + comparisonType: TimeRangeComparisonEnum.WeekBefore, comparisonEnabled: true, start, end, @@ -82,7 +80,7 @@ describe('getTimeRangeComparison', () => { const result = getTimeRangeComparison({ start, end, - comparisonType: TimeRangeComparisonType.PeriodBefore, + comparisonType: TimeRangeComparisonEnum.PeriodBefore, comparisonEnabled: true, }); expect(result).toEqual({ @@ -100,7 +98,7 @@ describe('getTimeRangeComparison', () => { const start = '2021-01-26T15:00:00.000Z'; const end = '2021-01-28T15:00:00.000Z'; const result = getTimeRangeComparison({ - comparisonType: TimeRangeComparisonType.WeekBefore, + comparisonType: TimeRangeComparisonEnum.WeekBefore, comparisonEnabled: true, start, end, @@ -117,7 +115,7 @@ describe('getTimeRangeComparison', () => { const start = '2021-01-10T15:00:00.000Z'; const end = '2021-01-18T15:00:00.000Z'; const result = getTimeRangeComparison({ - comparisonType: TimeRangeComparisonType.PeriodBefore, + comparisonType: TimeRangeComparisonEnum.PeriodBefore, comparisonEnabled: true, start, end, @@ -131,7 +129,7 @@ describe('getTimeRangeComparison', () => { const start = '2021-01-01T15:00:00.000Z'; const end = '2021-01-31T15:00:00.000Z'; const result = getTimeRangeComparison({ - comparisonType: TimeRangeComparisonType.PeriodBefore, + comparisonType: TimeRangeComparisonEnum.PeriodBefore, comparisonEnabled: true, start, end, diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/get_time_range_comparison.ts b/x-pack/plugins/apm/public/components/shared/time_comparison/get_time_range_comparison.ts index d9f9a249f1320..547be69ff6298 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/get_time_range_comparison.ts +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/get_time_range_comparison.ts @@ -7,14 +7,12 @@ import moment from 'moment'; import { EuiTheme } from 'src/plugins/kibana_react/common'; +import { + TimeRangeComparisonType, + TimeRangeComparisonEnum, +} from '../../../../common/runtime_types/comparison_type_rt'; import { getDateDifference } from '../../../../common/utils/formatters'; -export enum TimeRangeComparisonType { - WeekBefore = 'week', - DayBefore = 'day', - PeriodBefore = 'period', -} - export function getComparisonChartTheme(theme: EuiTheme) { return { areaSeriesStyle: { @@ -63,17 +61,17 @@ export function getTimeRangeComparison({ let offset: string; switch (comparisonType) { - case TimeRangeComparisonType.DayBefore: + case TimeRangeComparisonEnum.DayBefore: diff = oneDayInMilliseconds; offset = '1d'; break; - case TimeRangeComparisonType.WeekBefore: + case TimeRangeComparisonEnum.WeekBefore: diff = oneWeekInMilliseconds; offset = '1w'; break; - case TimeRangeComparisonType.PeriodBefore: + case TimeRangeComparisonEnum.PeriodBefore: diff = getDateDifference({ start: startMoment, end: endMoment, diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx index ce7d05d467291..e20a6df12ad46 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx @@ -16,10 +16,13 @@ import { import { getSelectOptions, TimeComparison } from './'; import * as urlHelpers from '../../shared/Links/url_helpers'; import moment from 'moment'; -import { TimeRangeComparisonType } from './get_time_range_comparison'; import { getComparisonTypes } from './get_comparison_types'; import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; +import { + TimeRangeComparisonType, + TimeRangeComparisonEnum, +} from '../../../../common/runtime_types/comparison_type_rt'; import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; function getWrapper({ @@ -68,8 +71,8 @@ describe('TimeComparison', () => { end: '2021-06-04T16:32:02.335Z', }) ).toEqual([ - TimeRangeComparisonType.DayBefore.valueOf(), - TimeRangeComparisonType.WeekBefore.valueOf(), + TimeRangeComparisonEnum.DayBefore.valueOf(), + TimeRangeComparisonEnum.WeekBefore.valueOf(), ]); }); @@ -80,8 +83,8 @@ describe('TimeComparison', () => { end: '2021-06-05T03:59:59.999Z', }) ).toEqual([ - TimeRangeComparisonType.DayBefore.valueOf(), - TimeRangeComparisonType.WeekBefore.valueOf(), + TimeRangeComparisonEnum.DayBefore.valueOf(), + TimeRangeComparisonEnum.WeekBefore.valueOf(), ]); }); @@ -92,8 +95,8 @@ describe('TimeComparison', () => { end: '2021-06-04T16:31:35.748Z', }) ).toEqual([ - TimeRangeComparisonType.DayBefore.valueOf(), - TimeRangeComparisonType.WeekBefore.valueOf(), + TimeRangeComparisonEnum.DayBefore.valueOf(), + TimeRangeComparisonEnum.WeekBefore.valueOf(), ]); }); @@ -104,8 +107,8 @@ describe('TimeComparison', () => { end: '2021-10-14T00:52:59.553Z', }) ).toEqual([ - TimeRangeComparisonType.DayBefore.valueOf(), - TimeRangeComparisonType.WeekBefore.valueOf(), + TimeRangeComparisonEnum.DayBefore.valueOf(), + TimeRangeComparisonEnum.WeekBefore.valueOf(), ]); }); @@ -115,7 +118,7 @@ describe('TimeComparison', () => { start: '2021-06-02T12:32:00.000Z', end: '2021-06-03T13:32:09.079Z', }) - ).toEqual([TimeRangeComparisonType.WeekBefore.valueOf()]); + ).toEqual([TimeRangeComparisonEnum.WeekBefore.valueOf()]); }); it('shows week before when 7 days is selected', () => { @@ -124,7 +127,7 @@ describe('TimeComparison', () => { start: '2021-05-28T16:32:17.520Z', end: '2021-06-04T16:32:17.520Z', }) - ).toEqual([TimeRangeComparisonType.WeekBefore.valueOf()]); + ).toEqual([TimeRangeComparisonEnum.WeekBefore.valueOf()]); }); it('shows period before when 8 days is selected', () => { expect( @@ -132,7 +135,7 @@ describe('TimeComparison', () => { start: '2021-05-27T16:32:46.747Z', end: '2021-06-04T16:32:46.747Z', }) - ).toEqual([TimeRangeComparisonType.PeriodBefore.valueOf()]); + ).toEqual([TimeRangeComparisonEnum.PeriodBefore.valueOf()]); }); }); @@ -141,24 +144,24 @@ describe('TimeComparison', () => { expect( getSelectOptions({ comparisonTypes: [ - TimeRangeComparisonType.DayBefore, - TimeRangeComparisonType.WeekBefore, - TimeRangeComparisonType.PeriodBefore, + TimeRangeComparisonEnum.DayBefore, + TimeRangeComparisonEnum.WeekBefore, + TimeRangeComparisonEnum.PeriodBefore, ], start: '2021-05-27T16:32:46.747Z', end: '2021-06-04T16:32:46.747Z', }) ).toEqual([ { - value: TimeRangeComparisonType.DayBefore.valueOf(), + value: TimeRangeComparisonEnum.DayBefore.valueOf(), text: 'Day before', }, { - value: TimeRangeComparisonType.WeekBefore.valueOf(), + value: TimeRangeComparisonEnum.WeekBefore.valueOf(), text: 'Week before', }, { - value: TimeRangeComparisonType.PeriodBefore.valueOf(), + value: TimeRangeComparisonEnum.PeriodBefore.valueOf(), text: '19/05 18:32 - 27/05 18:32', }, ]); @@ -167,13 +170,13 @@ describe('TimeComparison', () => { it('formats period before as DD/MM/YY HH:mm when range years are different', () => { expect( getSelectOptions({ - comparisonTypes: [TimeRangeComparisonType.PeriodBefore], + comparisonTypes: [TimeRangeComparisonEnum.PeriodBefore], start: '2020-05-27T16:32:46.747Z', end: '2021-06-04T16:32:46.747Z', }) ).toEqual([ { - value: TimeRangeComparisonType.PeriodBefore.valueOf(), + value: TimeRangeComparisonEnum.PeriodBefore.valueOf(), text: '20/05/19 18:32 - 27/05/20 18:32', }, ]); @@ -195,7 +198,7 @@ describe('TimeComparison', () => { expect(spy).toHaveBeenCalledWith(expect.anything(), { query: { comparisonEnabled: 'true', - comparisonType: TimeRangeComparisonType.DayBefore, + comparisonType: TimeRangeComparisonEnum.DayBefore, }, }); }); @@ -204,7 +207,7 @@ describe('TimeComparison', () => { exactStart: '2021-06-04T16:17:02.335Z', exactEnd: '2021-06-04T16:32:02.335Z', comparisonEnabled: true, - comparisonType: TimeRangeComparisonType.DayBefore, + comparisonType: TimeRangeComparisonEnum.DayBefore, }); const component = render(, { wrapper: Wrapper }); expectTextsInDocument(component, ['Day before', 'Week before']); @@ -219,7 +222,7 @@ describe('TimeComparison', () => { exactStart: '2021-06-03T16:31:35.748Z', exactEnd: '2021-06-04T16:31:35.748Z', comparisonEnabled: true, - comparisonType: TimeRangeComparisonType.DayBefore, + comparisonType: TimeRangeComparisonEnum.DayBefore, }); const component = render(, { wrapper: Wrapper }); expectTextsInDocument(component, ['Day before', 'Week before']); @@ -236,7 +239,7 @@ describe('TimeComparison', () => { exactStart: '2021-06-02T12:32:00.000Z', exactEnd: '2021-06-03T13:32:09.079Z', comparisonEnabled: true, - comparisonType: TimeRangeComparisonType.WeekBefore, + comparisonType: TimeRangeComparisonEnum.WeekBefore, }); const component = render(, { wrapper: Wrapper, @@ -255,7 +258,7 @@ describe('TimeComparison', () => { expect(spy).toHaveBeenCalledWith(expect.anything(), { query: { comparisonEnabled: 'true', - comparisonType: TimeRangeComparisonType.WeekBefore, + comparisonType: TimeRangeComparisonEnum.WeekBefore, }, }); }); @@ -264,7 +267,7 @@ describe('TimeComparison', () => { exactStart: '2021-06-02T12:32:00.000Z', exactEnd: '2021-06-03T13:32:09.079Z', comparisonEnabled: true, - comparisonType: TimeRangeComparisonType.WeekBefore, + comparisonType: TimeRangeComparisonEnum.WeekBefore, }); const component = render(, { wrapper: Wrapper, @@ -284,7 +287,7 @@ describe('TimeComparison', () => { exactStart: '2021-05-27T16:32:46.747Z', exactEnd: '2021-06-04T16:32:46.747Z', comparisonEnabled: true, - comparisonType: TimeRangeComparisonType.PeriodBefore, + comparisonType: TimeRangeComparisonEnum.PeriodBefore, }); const component = render(, { wrapper: Wrapper, @@ -302,7 +305,7 @@ describe('TimeComparison', () => { exactStart: '2020-05-27T16:32:46.747Z', exactEnd: '2021-06-04T16:32:46.747Z', comparisonEnabled: true, - comparisonType: TimeRangeComparisonType.PeriodBefore, + comparisonType: TimeRangeComparisonEnum.PeriodBefore, }); const component = render(, { wrapper: Wrapper, diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx index d5e38a3df7aac..35a6bc7634813 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx @@ -12,16 +12,14 @@ import React from 'react'; import { useHistory } from 'react-router-dom'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useUiTracker } from '../../../../../observability/public'; +import { TimeRangeComparisonEnum } from '../../../../common/runtime_types/comparison_type_rt'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useBreakpoints } from '../../../hooks/use_breakpoints'; import { useTimeRange } from '../../../hooks/use_time_range'; import * as urlHelpers from '../../shared/Links/url_helpers'; import { getComparisonTypes } from './get_comparison_types'; -import { - getTimeRangeComparison, - TimeRangeComparisonType, -} from './get_time_range_comparison'; +import { getTimeRangeComparison } from './get_time_range_comparison'; const PrependContainer = euiStyled.div` display: flex; @@ -66,13 +64,13 @@ export function getSelectOptions({ start, end, }: { - comparisonTypes: TimeRangeComparisonType[]; + comparisonTypes: TimeRangeComparisonEnum[]; start?: string; end?: string; }) { return comparisonTypes.map((value) => { switch (value) { - case TimeRangeComparisonType.DayBefore: { + case TimeRangeComparisonEnum.DayBefore: { return { value, text: i18n.translate('xpack.apm.timeComparison.select.dayBefore', { @@ -80,7 +78,7 @@ export function getSelectOptions({ }), }; } - case TimeRangeComparisonType.WeekBefore: { + case TimeRangeComparisonEnum.WeekBefore: { return { value, text: i18n.translate('xpack.apm.timeComparison.select.weekBefore', { @@ -88,9 +86,9 @@ export function getSelectOptions({ }), }; } - case TimeRangeComparisonType.PeriodBefore: { + case TimeRangeComparisonEnum.PeriodBefore: { const { comparisonStart, comparisonEnd } = getTimeRangeComparison({ - comparisonType: TimeRangeComparisonType.PeriodBefore, + comparisonType: TimeRangeComparisonEnum.PeriodBefore, start, end, comparisonEnabled: true, diff --git a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts index 32771bd56a72a..845fdb175bb65 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts +++ b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts @@ -6,12 +6,12 @@ */ import { Location } from 'history'; +import { TimeRangeComparisonType } from '../../../common/runtime_types/comparison_type_rt'; import { uxLocalUIFilterNames } from '../../../common/ux_ui_filter'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { LatencyAggregationType } from '../../../common/latency_aggregation_types'; import { pickKeys } from '../../../common/utils/pick_keys'; import { toQuery } from '../../components/shared/Links/url_helpers'; -import { TimeRangeComparisonType } from '../../components/shared/time_comparison/get_time_range_comparison'; import { getDateRange, removeUndefinedProps, diff --git a/x-pack/plugins/apm/public/context/url_params_context/types.ts b/x-pack/plugins/apm/public/context/url_params_context/types.ts index 4deef1662c236..8f167fc0ab734 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/types.ts +++ b/x-pack/plugins/apm/public/context/url_params_context/types.ts @@ -5,9 +5,9 @@ * 2.0. */ +import { TimeRangeComparisonType } from '../../../common/runtime_types/comparison_type_rt'; import { LatencyAggregationType } from '../../../common/latency_aggregation_types'; import { UxLocalUIFilterName } from '../../../common/ux_ui_filter'; -import { TimeRangeComparisonType } from '../../components/shared/time_comparison/get_time_range_comparison'; export type UrlParams = { detailTab?: string; diff --git a/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx b/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx index 120cbba952f3e..2878353da8eb7 100644 --- a/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { getTimeRangeComparison } from '../components/shared/time_comparison/get_time_range_comparison'; import { useApmParams } from './use_apm_params'; import { useFetcher } from './use_fetcher'; import { useTimeRange } from './use_time_range'; @@ -21,12 +21,18 @@ export function useErrorGroupDistributionFetcher({ environment: string; }) { const { - query: { rangeFrom, rangeTo }, - } = useApmParams('/services/{serviceName}'); + query: { rangeFrom, rangeTo, comparisonEnabled, comparisonType }, + } = useApmParams('/services/{serviceName}/errors'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + const { comparisonStart, comparisonEnd } = getTimeRangeComparison({ + start, + end, + comparisonType, + comparisonEnabled, + }); - const { data } = useFetcher( + const { data, status } = useFetcher( (callApmApi) => { if (start && end) { return callApmApi({ @@ -39,14 +45,25 @@ export function useErrorGroupDistributionFetcher({ kuery, start, end, + comparisonStart, + comparisonEnd, groupId, }, }, }); } }, - [environment, kuery, serviceName, start, end, groupId] + [ + environment, + kuery, + serviceName, + start, + end, + comparisonStart, + comparisonEnd, + groupId, + ] ); - return { errorDistributionData: data }; + return { errorDistributionData: data, status }; } diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index 8ec6111fd2b8b..31c533814e697 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -86,7 +86,6 @@ export async function getBuckets({ ); return { - noHits: resp.hits.total.value === 0, buckets: resp.hits.total.value > 0 ? buckets : [], }; } diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts index 5f88452d45b32..7c2eaf38be6a7 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts @@ -21,6 +21,8 @@ export async function getErrorDistribution({ setup, start, end, + comparisonStart, + comparisonEnd, }: { environment: string; kuery: string; @@ -29,22 +31,40 @@ export async function getErrorDistribution({ setup: Setup; start: number; end: number; + comparisonStart?: number; + comparisonEnd?: number; }) { const bucketSize = getBucketSize({ start, end }); - const { buckets, noHits } = await getBuckets({ + const commonProps = { environment, kuery, serviceName, groupId, - bucketSize, setup, + bucketSize, + }; + const currentPeriodPromise = getBuckets({ + ...commonProps, start, end, }); + const previousPeriodPromise = + comparisonStart && comparisonEnd + ? getBuckets({ + ...commonProps, + start: comparisonStart, + end: comparisonEnd, + }) + : { buckets: [], bucketSize: null }; + + const [currentPeriod, previousPeriod] = await Promise.all([ + currentPeriodPromise, + previousPeriodPromise, + ]); return { - noHits, - buckets, + currentPeriod: currentPeriod.buckets, + previousPeriod: previousPeriod.buckets, bucketSize, }; } diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts index 0864276b67fee..3a6e07acd14bc 100644 --- a/x-pack/plugins/apm/server/routes/errors.ts +++ b/x-pack/plugins/apm/server/routes/errors.ts @@ -11,7 +11,12 @@ import { getErrorDistribution } from '../lib/errors/distribution/get_distributio import { getErrorGroupSample } from '../lib/errors/get_error_group_sample'; import { getErrorGroups } from '../lib/errors/get_error_groups'; import { setupRequest } from '../lib/helpers/setup_request'; -import { environmentRt, kueryRt, rangeRt } from './default_api_types'; +import { + environmentRt, + kueryRt, + rangeRt, + comparisonRangeRt, +} from './default_api_types'; import { createApmServerRouteRepository } from './create_apm_server_route_repository'; const errorsRoute = createApmServerRoute({ @@ -94,6 +99,7 @@ const errorDistributionRoute = createApmServerRoute({ environmentRt, kueryRt, rangeRt, + comparisonRangeRt, ]), }), options: { tags: ['access:apm'] }, @@ -101,7 +107,15 @@ const errorDistributionRoute = createApmServerRoute({ const setup = await setupRequest(resources); const { params } = resources; const { serviceName } = params.path; - const { environment, kuery, groupId, start, end } = params.query; + const { + environment, + kuery, + groupId, + start, + end, + comparisonStart, + comparisonEnd, + } = params.query; return getErrorDistribution({ environment, kuery, @@ -110,6 +124,8 @@ const errorDistributionRoute = createApmServerRoute({ setup, start, end, + comparisonStart, + comparisonEnd, }); }, }); diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts index 5caf2b4372483..7bb5454d52176 100644 --- a/x-pack/plugins/apm/server/tutorial/index.ts +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -22,7 +22,7 @@ import apmIndexPattern from './index_pattern.json'; const apmIntro = i18n.translate('xpack.apm.tutorial.introduction', { defaultMessage: - 'Collect in-depth performance metrics and errors from inside your applications.', + 'Collect performance metrics from your applications with Elastic APM.', }); const moduleName = 'apm'; diff --git a/x-pack/plugins/code/server/plugin.test.ts b/x-pack/plugins/code/server/plugin.test.ts index 512658ca4da82..f2963d416409b 100644 --- a/x-pack/plugins/code/server/plugin.test.ts +++ b/x-pack/plugins/code/server/plugin.test.ts @@ -5,19 +5,37 @@ * 2.0. */ +import type { RegisterDeprecationsConfig, GetDeprecationsContext } from 'src/core/server'; import { coreMock } from '../../../../src/core/server/mocks'; import { CodePlugin } from './plugin'; describe('Code Plugin', () => { + let deprecationConfigs: RegisterDeprecationsConfig[]; + + beforeEach(() => { + deprecationConfigs = []; + }); + + const getDeprecationContextMock = () => ({} as GetDeprecationsContext); + describe('setup()', () => { it('does not log deprecation message if no xpack.code.* configurations are set', async () => { const context = coreMock.createPluginInitializerContext(); const plugin = new CodePlugin(context); - await plugin.setup(); + const coreSetup = coreMock.createSetup(); + coreSetup.deprecations.registerDeprecations.mockImplementation((deprecationConfig) => { + deprecationConfigs.push(deprecationConfig); + }); + + await plugin.setup(coreSetup); - expect(context.logger.get).not.toHaveBeenCalled(); + expect(coreSetup.deprecations.registerDeprecations).toHaveBeenCalledTimes(1); + expect(deprecationConfigs).toHaveLength(1); + + const deprecations = await deprecationConfigs[0].getDeprecations(getDeprecationContextMock()); + expect(deprecations).toHaveLength(0); }); it('logs deprecation message if any xpack.code.* configurations are set', async () => { @@ -28,12 +46,28 @@ describe('Code Plugin', () => { context.logger.get = jest.fn().mockReturnValue({ warn }); const plugin = new CodePlugin(context); - await plugin.setup(); + const coreSetup = coreMock.createSetup(); + coreSetup.deprecations.registerDeprecations.mockImplementation((deprecationConfig) => { + deprecationConfigs.push(deprecationConfig); + }); + + await plugin.setup(coreSetup); - expect(context.logger.get).toHaveBeenCalledWith('config', 'deprecation'); - expect(warn.mock.calls[0][0]).toMatchInlineSnapshot( - `"The experimental app \\"Code\\" has been removed from Kibana. Remove all xpack.code.* configurations from kibana.yml so Kibana does not fail to start up in the next major version."` - ); + expect(coreSetup.deprecations.registerDeprecations).toHaveBeenCalledTimes(1); + expect(deprecationConfigs).toHaveLength(1); + + const deprecations = await deprecationConfigs[0].getDeprecations(getDeprecationContextMock()); + expect(deprecations).toHaveLength(1); + expect(deprecations[0]).toEqual({ + level: 'critical', + deprecationType: 'feature', + title: expect.any(String), + message: expect.any(String), + requireRestart: true, + correctiveActions: expect.objectContaining({ + manualSteps: expect.any(Array), + }), + }); }); }); }); diff --git a/x-pack/plugins/code/server/plugin.ts b/x-pack/plugins/code/server/plugin.ts index eb7481d12387d..183cce69d057b 100644 --- a/x-pack/plugins/code/server/plugin.ts +++ b/x-pack/plugins/code/server/plugin.ts @@ -5,8 +5,9 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; import { TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext, Plugin } from 'src/core/server'; +import { PluginInitializerContext, Plugin, CoreSetup, DeprecationsDetails } from 'src/core/server'; import { CodeConfigSchema } from './config'; /** @@ -15,17 +16,37 @@ import { CodeConfigSchema } from './config'; export class CodePlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} - public async setup() { + public async setup(core: CoreSetup) { const config = this.initializerContext.config.get>(); - if (config && Object.keys(config).length > 0) { - this.initializerContext.logger - .get('config', 'deprecation') - .warn( - 'The experimental app "Code" has been removed from Kibana. Remove all xpack.code.* ' + - 'configurations from kibana.yml so Kibana does not fail to start up in the next major version.' - ); - } + core.deprecations.registerDeprecations({ + getDeprecations: (context) => { + const deprecations: DeprecationsDetails[] = []; + if (config && Object.keys(config).length > 0) { + deprecations.push({ + level: 'critical', + deprecationType: 'feature', + title: i18n.translate('xpack.code.deprecations.removed.title', { + defaultMessage: 'The experimental plugin "Code" has been removed from Kibana', + }), + message: i18n.translate('xpack.code.deprecations.removed.message', { + defaultMessage: + 'The experimental plugin "Code" has been removed from Kibana. The associated configuration ' + + 'properties need to be removed from the Kibana configuration file.', + }), + requireRestart: true, + correctiveActions: { + manualSteps: [ + i18n.translate('xpack.code.deprecations.removed.manualSteps1', { + defaultMessage: 'Remove all xpack.code.* properties from the Kibana config file.', + }), + ], + }, + }); + } + return deprecations; + }, + }); } public start() {} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx index 944d8315452b0..ddc9c69a35c8d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx @@ -15,7 +15,7 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiBadge, EuiButton, EuiLoadingSpinner, EuiTab } from '@elastic/eui'; +import { EuiBadge, EuiButton, EuiTab } from '@elastic/eui'; import { getPageHeaderActions, getPageHeaderTabs, getPageTitle } from '../../../../test_helpers'; @@ -24,15 +24,14 @@ jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() })); import { AppSearchPageTemplate } from '../../layout'; import { AutomatedCuration } from './automated_curation'; +import { AutomatedCurationHistory } from './automated_curation_history'; import { CurationLogic } from './curation_logic'; import { DeleteCurationButton } from './delete_curation_button'; import { PromotedDocuments, OrganicDocuments } from './documents'; -import { History } from './history'; describe('AutomatedCuration', () => { const values = { - dataLoading: false, queries: ['query A', 'query B'], isFlyoutOpen: false, curation: { @@ -49,6 +48,7 @@ describe('AutomatedCuration', () => { const actions = { convertToManual: jest.fn(), + onSelectPageTab: jest.fn(), }; beforeEach(() => { @@ -62,48 +62,41 @@ describe('AutomatedCuration', () => { const wrapper = shallow(); expect(wrapper.is(AppSearchPageTemplate)); - expect(wrapper.find(PromotedDocuments)).toHaveLength(1); - expect(wrapper.find(OrganicDocuments)).toHaveLength(1); - expect(wrapper.find(History)).toHaveLength(0); }); - it('includes tabs', () => { + it('includes set of tabs in the page header', () => { const wrapper = shallow(); - let tabs = getPageHeaderTabs(wrapper).find(EuiTab); - expect(tabs).toHaveLength(3); + const tabs = getPageHeaderTabs(wrapper).find(EuiTab); - expect(tabs.at(0).prop('isSelected')).toBe(true); + tabs.at(0).simulate('click'); + expect(actions.onSelectPageTab).toHaveBeenNthCalledWith(1, 'promoted'); - expect(tabs.at(1).prop('onClick')).toBeUndefined(); - expect(tabs.at(1).prop('isSelected')).toBe(false); expect(tabs.at(1).prop('disabled')).toBe(true); - expect(tabs.at(2).prop('isSelected')).toBe(false); - - // Clicking on the History tab shows the history view tabs.at(2).simulate('click'); + expect(actions.onSelectPageTab).toHaveBeenNthCalledWith(2, 'history'); + }); - tabs = getPageHeaderTabs(wrapper).find(EuiTab); - - expect(tabs.at(0).prop('isSelected')).toBe(false); - expect(tabs.at(2).prop('isSelected')).toBe(true); + it('renders promoted and organic documents when the promoted tab is selected', () => { + setMockValues({ ...values, selectedPageTab: 'promoted' }); + const wrapper = shallow(); + const tabs = getPageHeaderTabs(wrapper).find(EuiTab); - expect(wrapper.find(PromotedDocuments)).toHaveLength(0); - expect(wrapper.find(OrganicDocuments)).toHaveLength(0); - expect(wrapper.find(History)).toHaveLength(1); + expect(tabs.at(0).prop('isSelected')).toEqual(true); - // Clicking back to the Promoted tab shows promoted documents - tabs.at(0).simulate('click'); + expect(wrapper.find(PromotedDocuments)).toHaveLength(1); + expect(wrapper.find(OrganicDocuments)).toHaveLength(1); + }); - tabs = getPageHeaderTabs(wrapper).find(EuiTab); + it('renders curation history when the history tab is selected', () => { + setMockValues({ ...values, selectedPageTab: 'history' }); + const wrapper = shallow(); + const tabs = getPageHeaderTabs(wrapper).find(EuiTab); - expect(tabs.at(0).prop('isSelected')).toBe(true); - expect(tabs.at(2).prop('isSelected')).toBe(false); + expect(tabs.at(2).prop('isSelected')).toEqual(true); - expect(wrapper.find(PromotedDocuments)).toHaveLength(1); - expect(wrapper.find(OrganicDocuments)).toHaveLength(1); - expect(wrapper.find(History)).toHaveLength(0); + expect(wrapper.find(AutomatedCurationHistory)).toHaveLength(1); }); it('initializes CurationLogic with a curationId prop from URL param', () => { @@ -121,15 +114,6 @@ describe('AutomatedCuration', () => { expect(pageTitle.find(EuiBadge)).toHaveLength(1); }); - it('displays a spinner in the title when loading', () => { - setMockValues({ ...values, dataLoading: true }); - - const wrapper = shallow(); - const pageTitle = shallow(
{getPageTitle(wrapper)}
); - - expect(pageTitle.find(EuiLoadingSpinner)).toHaveLength(1); - }); - it('contains a button to delete the curation', () => { const wrapper = shallow(); const pageHeaderActions = getPageHeaderActions(wrapper); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx index 276b40ba88677..0351d4c113d13 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx @@ -5,12 +5,12 @@ * 2.0. */ -import React, { useState } from 'react'; +import React from 'react'; import { useParams } from 'react-router-dom'; import { useValues, useActions } from 'kea'; -import { EuiButton, EuiBadge, EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiButton, EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EngineLogic } from '../../engine'; @@ -25,29 +25,25 @@ import { import { getCurationsBreadcrumbs } from '../utils'; +import { AutomatedCurationHistory } from './automated_curation_history'; import { HIDDEN_DOCUMENTS_TITLE, PROMOTED_DOCUMENTS_TITLE } from './constants'; import { CurationLogic } from './curation_logic'; import { DeleteCurationButton } from './delete_curation_button'; import { PromotedDocuments, OrganicDocuments } from './documents'; -import { History } from './history'; - -const PROMOTED = 'promoted'; -const HISTORY = 'history'; export const AutomatedCuration: React.FC = () => { const { curationId } = useParams<{ curationId: string }>(); const logic = CurationLogic({ curationId }); - const { convertToManual } = useActions(logic); - const { activeQuery, dataLoading, queries, curation } = useValues(logic); + const { convertToManual, onSelectPageTab } = useActions(logic); + const { activeQuery, queries, curation, selectedPageTab } = useValues(logic); const { engineName } = useValues(EngineLogic); - const [selectedPageTab, setSelectedPageTab] = useState(PROMOTED); const pageTabs = [ { label: PROMOTED_DOCUMENTS_TITLE, append: {curation.promoted.length}, - isSelected: selectedPageTab === PROMOTED, - onClick: () => setSelectedPageTab(PROMOTED), + isSelected: selectedPageTab === 'promoted', + onClick: () => onSelectPageTab('promoted'), }, { label: HIDDEN_DOCUMENTS_TITLE, @@ -62,8 +58,8 @@ export const AutomatedCuration: React.FC = () => { defaultMessage: 'History', } ), - isSelected: selectedPageTab === HISTORY, - onClick: () => setSelectedPageTab(HISTORY), + isSelected: selectedPageTab === 'history', + onClick: () => onSelectPageTab('history'), }, ]; @@ -73,7 +69,7 @@ export const AutomatedCuration: React.FC = () => { pageHeader={{ pageTitle: ( <> - {dataLoading ? : activeQuery}{' '} + {activeQuery}{' '} {AUTOMATED_LABEL} @@ -100,12 +96,11 @@ export const AutomatedCuration: React.FC = () => { ], tabs: pageTabs, }} - isLoading={dataLoading} > - {selectedPageTab === PROMOTED && } - {selectedPageTab === PROMOTED && } - {selectedPageTab === HISTORY && ( - + {selectedPageTab === 'promoted' && } + {selectedPageTab === 'promoted' && } + {selectedPageTab === 'history' && ( + )} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.test.tsx similarity index 68% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.test.tsx index a7f83fb0c61d9..b7d1b6f9ed809 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.test.tsx @@ -11,13 +11,13 @@ import { shallow } from 'enzyme'; import { EntSearchLogStream } from '../../../../shared/log_stream'; -import { History } from './history'; +import { AutomatedCurationHistory } from './automated_curation_history'; -describe('History', () => { +describe('AutomatedCurationHistory', () => { it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(EntSearchLogStream).prop('query')).toEqual( - 'appsearch.search_relevance_suggestions.query: some text and event.kind: event and event.dataset: search-relevance-suggestions and appsearch.search_relevance_suggestions.engine: foo and event.action: curation_suggestion' + 'appsearch.search_relevance_suggestions.query: some text and event.kind: event and event.dataset: search-relevance-suggestions and appsearch.search_relevance_suggestions.engine: foo and event.action: curation_suggestion and appsearch.search_relevance_suggestions.suggestion.new_status: automated' ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.tsx similarity index 90% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.tsx index 744141372469c..f523beeb0a821 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.tsx @@ -17,13 +17,14 @@ interface Props { engineName: string; } -export const History: React.FC = ({ query, engineName }) => { +export const AutomatedCurationHistory: React.FC = ({ query, engineName }) => { const filters = [ `appsearch.search_relevance_suggestions.query: ${query}`, 'event.kind: event', 'event.dataset: search-relevance-suggestions', `appsearch.search_relevance_suggestions.engine: ${engineName}`, 'event.action: curation_suggestion', + 'appsearch.search_relevance_suggestions.suggestion.new_status: automated', ]; return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx index 62c3a6c7d4578..dce56a05f8f8c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx @@ -14,6 +14,7 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { EnterpriseSearchPageTemplate } from '../../../../shared/layout'; import { rerender } from '../../../../test_helpers'; jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() })); @@ -26,6 +27,7 @@ import { Curation } from './'; describe('Curation', () => { const values = { + dataLoading: false, isAutomated: true, }; @@ -49,6 +51,13 @@ describe('Curation', () => { expect(actions.loadCuration).toHaveBeenCalledTimes(2); }); + it('renders a loading view when loading', () => { + setMockValues({ dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.is(EnterpriseSearchPageTemplate)).toBe(true); + }); + it('renders a view for automated curations', () => { setMockValues({ isAutomated: true }); const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx index 19b6542e96c4b..d1b0f43d976a8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx @@ -10,6 +10,8 @@ import { useParams } from 'react-router-dom'; import { useValues, useActions } from 'kea'; +import { EnterpriseSearchPageTemplate } from '../../../../shared/layout'; + import { AutomatedCuration } from './automated_curation'; import { CurationLogic } from './curation_logic'; import { ManualCuration } from './manual_curation'; @@ -17,11 +19,14 @@ import { ManualCuration } from './manual_curation'; export const Curation: React.FC = () => { const { curationId } = useParams() as { curationId: string }; const { loadCuration } = useActions(CurationLogic({ curationId })); - const { isAutomated } = useValues(CurationLogic({ curationId })); + const { dataLoading, isAutomated } = useValues(CurationLogic({ curationId })); useEffect(() => { loadCuration(); }, [curationId]); + if (dataLoading) { + return ; + } return isAutomated ? : ; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts index 5c3ac6d700de4..b1f16944c985b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts @@ -250,7 +250,7 @@ describe('CurationLogic', () => { }); describe('onSelectPageTab', () => { - it('should set the selected page tab', () => { + it('should set the selected page tab and clears flash messages', () => { mount({ selectedPageTab: 'promoted', }); @@ -261,6 +261,7 @@ describe('CurationLogic', () => { ...DEFAULT_VALUES, selectedPageTab: 'hidden', }); + expect(clearFlashMessages).toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts index 7b617dd89e962..6393ccf974225 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts @@ -21,7 +21,7 @@ import { DELETE_SUCCESS_MESSAGE } from '../constants'; import { Curation } from '../types'; import { addDocument, removeDocument } from '../utils'; -type CurationPageTabs = 'promoted' | 'hidden'; +type CurationPageTabs = 'promoted' | 'history' | 'hidden'; interface CurationValues { dataLoading: boolean; @@ -271,6 +271,9 @@ export const CurationLogic = kea { + clearFlashMessages(); + }, setActiveQuery: () => actions.updateCuration(), setPromotedIds: () => actions.updateCuration(), addPromotedId: () => actions.updateCuration(), diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.test.tsx index 103d7be37535b..548d111d6f96e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.test.tsx @@ -22,7 +22,7 @@ jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() })); import { CurationLogic } from './curation_logic'; import { DeleteCurationButton } from './delete_curation_button'; -import { PromotedDocuments, HiddenDocuments } from './documents'; +import { PromotedDocuments, HiddenDocuments, OrganicDocuments } from './documents'; import { ManualCuration } from './manual_curation'; import { ActiveQuerySelect, ManageQueriesModal } from './queries'; import { AddResultFlyout } from './results'; @@ -30,7 +30,6 @@ import { SuggestedDocumentsCallout } from './suggested_documents_callout'; describe('ManualCuration', () => { const values = { - dataLoading: false, queries: ['query A', 'query B'], isFlyoutOpen: false, selectedPageTab: 'promoted', @@ -74,13 +73,13 @@ describe('ManualCuration', () => { expect(actions.onSelectPageTab).toHaveBeenNthCalledWith(2, 'hidden'); }); - it('contains a suggested documents callout when the selectedPageTab is ', () => { + it('contains a suggested documents callout', () => { const wrapper = shallow(); expect(wrapper.find(SuggestedDocumentsCallout)).toHaveLength(1); }); - it('renders promoted documents when that tab is selected', () => { + it('renders promoted and organic documents when the promoted tab is selected', () => { setMockValues({ ...values, selectedPageTab: 'promoted' }); const wrapper = shallow(); const tabs = getPageHeaderTabs(wrapper).find(EuiTab); @@ -88,9 +87,10 @@ describe('ManualCuration', () => { expect(tabs.at(0).prop('isSelected')).toEqual(true); expect(wrapper.find(PromotedDocuments)).toHaveLength(1); + expect(wrapper.find(OrganicDocuments)).toHaveLength(1); }); - it('renders hidden documents when that tab is selected', () => { + it('renders hidden documents when the hidden tab is selected', () => { setMockValues({ ...values, selectedPageTab: 'hidden' }); const wrapper = shallow(); const tabs = getPageHeaderTabs(wrapper).find(EuiTab); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.tsx index 3aee306e3d2ff..45b1b6212f504 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.tsx @@ -26,10 +26,10 @@ import { SuggestedDocumentsCallout } from './suggested_documents_callout'; export const ManualCuration: React.FC = () => { const { curationId } = useParams() as { curationId: string }; - const { onSelectPageTab } = useActions(CurationLogic({ curationId })); - const { dataLoading, queries, selectedPageTab, curation } = useValues( - CurationLogic({ curationId }) - ); + const logic = CurationLogic({ curationId }); + const { onSelectPageTab } = useActions(logic); + const { queries, selectedPageTab, curation } = useValues(logic); + const { isFlyoutOpen } = useValues(AddResultLogic); const pageTabs = [ @@ -64,7 +64,6 @@ export const ManualCuration: React.FC = () => { ], tabs: pageTabs, }} - isLoading={dataLoading} > {selectedPageTab === 'promoted' && } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts index 42c3985e4dcf1..44ff66e5f46eb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts @@ -92,7 +92,7 @@ describe('CurationsLogic', () => { }); describe('onSelectPageTab', () => { - it('should set the selected page tab', () => { + it('should set the selected page tab and clear flash messages', () => { mount(); CurationsLogic.actions.onSelectPageTab('settings'); @@ -101,6 +101,7 @@ describe('CurationsLogic', () => { ...DEFAULT_VALUES, selectedPageTab: 'settings', }); + expect(clearFlashMessages).toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts index 4419603efddf0..487072584583f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts @@ -126,5 +126,8 @@ export const CurationsLogic = kea { + clearFlashMessages(); + }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx index a0fd778ac7dde..c0278c765e85e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx @@ -94,7 +94,7 @@ describe('CurationsRouter', () => { expect(MOCK_ACTIONS.loadCurationsSettings).toHaveBeenCalledTimes(1); }); - it('skips loading curation settings when log retention is enabled', () => { + it('skips loading curation settings when log retention is disabled', () => { setMockValues({ ...MOCK_VALUES, logRetention: { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.ts index b206c0c79ed26..8e6c3a9c6a6ae 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.ts @@ -137,7 +137,7 @@ export const CurationSuggestionLogic = kea< setQueuedSuccessMessage( i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.suggestedCuration.successfullyAppliedMessage', - { defaultMessage: 'Suggestion was succefully applied.' } + { defaultMessage: 'Suggestion was successfully applied.' } ) ); if (suggestion!.operation === 'delete') { @@ -177,7 +177,7 @@ export const CurationSuggestionLogic = kea< 'xpack.enterpriseSearch.appSearch.engine.curations.suggestedCuration.successfullyAutomatedMessage', { defaultMessage: - 'Suggestion was succefully applied and all future suggestions for the query "{query}" will be automatically applied.', + 'Suggestion was successfully applied and all future suggestions for the query "{query}" will be automatically applied.', values: { query: suggestion!.query }, } ) @@ -208,7 +208,7 @@ export const CurationSuggestionLogic = kea< i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.suggestedCuration.successfullyRejectedMessage', { - defaultMessage: 'Suggestion was succefully rejected.', + defaultMessage: 'Suggestion was successfully rejected.', } ) ); @@ -230,7 +230,7 @@ export const CurationSuggestionLogic = kea< 'xpack.enterpriseSearch.appSearch.engine.curations.suggestedCuration.successfullyDisabledMessage', { defaultMessage: - 'Suggestion was succefully rejected and you will no longer receive suggestions for the query "{query}".', + 'Suggestion was successfully rejected and you will no longer receive suggestions for the query "{query}".', values: { query: suggestion!.query }, } ) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx index aacabf0ac7303..4e09dadc6c836 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx @@ -47,6 +47,10 @@ describe('Curations', () => { }, }, selectedPageTab: 'overview', + // CurationsSettingsLogic + curationsSettings: { + enabled: true, + }, // LicensingLogic hasPlatinumLicense: true, }; @@ -78,8 +82,6 @@ describe('Curations', () => { tabs.at(2).simulate('click'); expect(actions.onSelectPageTab).toHaveBeenNthCalledWith(3, 'settings'); - // The settings tab should NOT have an icon next to it - expect(tabs.at(2).prop('prepend')).toBeUndefined(); }); it('renders less tabs when less than platinum license', () => { @@ -90,8 +92,47 @@ describe('Curations', () => { const tabs = getPageHeaderTabs(wrapper).find(EuiTab); expect(tabs.length).toBe(2); - // The settings tab should have an icon next to it - expect(tabs.at(1).prop('prepend')).not.toBeUndefined(); + }); + + it('renders a New! badge when less than platinum license', () => { + setMockValues({ ...values, hasPlatinumLicense: false }); + const wrapper = shallow(); + + expect(getPageTitle(wrapper)).toEqual('Curated results'); + + const tabs = getPageHeaderTabs(wrapper).find(EuiTab); + expect(tabs.at(1).prop('append')).not.toBeUndefined(); + }); + + it('renders a New! badge when suggestions are disabled', () => { + setMockValues({ + ...values, + curationsSettings: { + enabled: false, + }, + }); + const wrapper = shallow(); + + expect(getPageTitle(wrapper)).toEqual('Curated results'); + + const tabs = getPageHeaderTabs(wrapper).find(EuiTab); + expect(tabs.at(2).prop('append')).not.toBeUndefined(); + }); + + it('hides the badge when suggestions are enabled and the user has a platinum license', () => { + setMockValues({ + ...values, + hasPlatinumLicense: true, + curationsSettings: { + enabled: true, + }, + }); + const wrapper = shallow(); + + expect(getPageTitle(wrapper)).toEqual('Curated results'); + + const tabs = getPageHeaderTabs(wrapper).find(EuiTab); + expect(tabs.at(2).prop('append')).toBeUndefined(); }); it('renders an overview view', () => { @@ -125,18 +166,20 @@ describe('Curations', () => { }); describe('loading state', () => { - it('renders a full-page loading state on initial page load', () => { + it('renders a full-page loading state and hides tabs on initial page load', () => { setMockValues({ ...values, dataLoading: true }); const wrapper = shallow(); expect(wrapper.prop('isLoading')).toEqual(true); + expect(wrapper.prop('tabs')).toBeUndefined(); }); - it('does not re-render a full-page loading state when data is loaded', () => { + it('does not re-render a full-page loading and shows tabs state when data is loaded', () => { setMockValues({ ...values, dataLoading: false }); const wrapper = shallow(); expect(wrapper.prop('isLoading')).toEqual(false); + expect(typeof wrapper.prop('tabs')).not.toBeUndefined(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx index 3d4751fcb343f..1cd8313743536 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx @@ -9,7 +9,7 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; -import { EuiIcon } from '@elastic/eui'; +import { EuiBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { LicensingLogic } from '../../../../shared/licensing'; @@ -31,7 +31,12 @@ export const Curations: React.FC = () => { const { dataLoading: curationsDataLoading, meta, selectedPageTab } = useValues(CurationsLogic); const { loadCurations, onSelectPageTab } = useActions(CurationsLogic); const { hasPlatinumLicense } = useValues(LicensingLogic); - const { dataLoading: curationsSettingsDataLoading } = useValues(CurationsSettingsLogic); + const { + dataLoading: curationsSettingsDataLoading, + curationsSettings: { enabled: curationsSettingsEnabled }, + } = useValues(CurationsSettingsLogic); + + const suggestionsEnabled = hasPlatinumLicense && curationsSettingsEnabled; const OVERVIEW_TAB = { label: i18n.translate( @@ -61,22 +66,25 @@ export const Curations: React.FC = () => { ), isSelected: selectedPageTab === 'settings', onClick: () => onSelectPageTab('settings'), + append: suggestionsEnabled ? undefined : ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.newBadgeLabel', { + defaultMessage: 'New!', + })} + + ), }; const pageTabs = hasPlatinumLicense ? [OVERVIEW_TAB, HISTORY_TAB, SETTINGS_TAB] - : [ - OVERVIEW_TAB, - { - ...SETTINGS_TAB, - prepend: , - }, - ]; + : [OVERVIEW_TAB, SETTINGS_TAB]; useEffect(() => { loadCurations(); }, [meta.page.current]); + const isLoading = curationsSettingsDataLoading || curationsDataLoading; + return ( { {CREATE_NEW_CURATION_TITLE} , ], - tabs: pageTabs, + tabs: isLoading ? undefined : pageTabs, }} - isLoading={curationsSettingsDataLoading || curationsDataLoading} + isLoading={isLoading} > {selectedPageTab === 'overview' && } {selectedPageTab === 'history' && } diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index 131cc276fc073..734d578687bcd 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -15,6 +15,10 @@ export const FLEET_SERVER_PACKAGE = 'fleet_server'; export const FLEET_ENDPOINT_PACKAGE = 'endpoint'; export const FLEET_APM_PACKAGE = 'apm'; export const FLEET_SYNTHETICS_PACKAGE = 'synthetics'; +export const FLEET_KUBERNETES_PACKAGE = 'kubernetes'; +export const KUBERNETES_RUN_INSTRUCTIONS = + 'kubectl apply -f elastic-agent-standalone-kubernetes.yaml'; +export const STANDALONE_RUN_INSTRUCTIONS = './elastic-agent install'; /* Package rules: diff --git a/x-pack/plugins/fleet/common/services/agent_cm_to_yaml.ts b/x-pack/plugins/fleet/common/services/agent_cm_to_yaml.ts new file mode 100644 index 0000000000000..5987110d7752f --- /dev/null +++ b/x-pack/plugins/fleet/common/services/agent_cm_to_yaml.ts @@ -0,0 +1,34 @@ +/* + * 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 { safeDump } from 'js-yaml'; + +import type { FullAgentConfigMap } from '../types/models/agent_cm'; + +const CM_KEYS_ORDER = ['apiVersion', 'kind', 'metadata', 'data']; + +export const fullAgentConfigMapToYaml = ( + policy: FullAgentConfigMap, + toYaml: typeof safeDump +): string => { + return toYaml(policy, { + skipInvalid: true, + sortKeys: (keyA: string, keyB: string) => { + const indexA = CM_KEYS_ORDER.indexOf(keyA); + const indexB = CM_KEYS_ORDER.indexOf(keyB); + if (indexA >= 0 && indexB < 0) { + return -1; + } + + if (indexA < 0 && indexB >= 0) { + return 1; + } + + return indexA - indexB; + }, + }); +}; diff --git a/x-pack/plugins/fleet/common/types/models/agent_cm.ts b/x-pack/plugins/fleet/common/types/models/agent_cm.ts new file mode 100644 index 0000000000000..bd8200c96ad88 --- /dev/null +++ b/x-pack/plugins/fleet/common/types/models/agent_cm.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FullAgentPolicy } from './agent_policy'; + +export interface FullAgentConfigMap { + apiVersion: string; + kind: string; + metadata: Metadata; + data: AgentYML; +} + +interface Metadata { + name: string; + namespace: string; + labels: Labels; +} + +interface Labels { + 'k8s-app': string; +} + +interface AgentYML { + 'agent.yml': FullAgentPolicy; +} diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts index 927368694693a..0975b1e28fb8b 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts @@ -78,3 +78,7 @@ export interface GetFullAgentPolicyRequest { export interface GetFullAgentPolicyResponse { item: FullAgentPolicy; } + +export interface GetFullAgentConfigMapResponse { + item: string; +} diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx index d7b9ae2aef08a..99e8809923140 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect } from 'react'; import { EuiSteps, EuiText, @@ -23,16 +23,27 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { safeDump } from 'js-yaml'; -import { useStartServices, useLink, sendGetOneAgentPolicyFull } from '../../hooks'; +import { + useStartServices, + useLink, + sendGetOneAgentPolicyFull, + sendGetOneAgentPolicy, +} from '../../hooks'; import { fullAgentPolicyToYaml, agentPolicyRouteService } from '../../services'; +import type { PackagePolicy } from '../../../common'; + +import { + FLEET_KUBERNETES_PACKAGE, + KUBERNETES_RUN_INSTRUCTIONS, + STANDALONE_RUN_INSTRUCTIONS, +} from '../../../common'; + import { DownloadStep, AgentPolicySelectionStep } from './steps'; import type { BaseProps } from './types'; type Props = BaseProps; -const RUN_INSTRUCTIONS = './elastic-agent install'; - export const StandaloneInstructions = React.memo(({ agentPolicy, agentPolicies }) => { const { getHref } = useLink(); const core = useStartServices(); @@ -40,12 +51,34 @@ export const StandaloneInstructions = React.memo(({ agentPolicy, agentPol const [selectedPolicyId, setSelectedPolicyId] = useState(agentPolicy?.id); const [fullAgentPolicy, setFullAgentPolicy] = useState(); + const [isK8s, setIsK8s] = useState<'IS_LOADING' | 'IS_KUBERNETES' | 'IS_NOT_KUBERNETES'>( + 'IS_LOADING' + ); + const [yaml, setYaml] = useState(''); + const runInstructions = + isK8s === 'IS_KUBERNETES' ? KUBERNETES_RUN_INSTRUCTIONS : STANDALONE_RUN_INSTRUCTIONS; - const downloadLink = selectedPolicyId - ? core.http.basePath.prepend( - `${agentPolicyRouteService.getInfoFullDownloadPath(selectedPolicyId)}?standalone=true` - ) - : undefined; + useEffect(() => { + async function checkifK8s() { + if (!selectedPolicyId) { + return; + } + const agentPolicyRequest = await sendGetOneAgentPolicy(selectedPolicyId); + const agentPol = agentPolicyRequest.data ? agentPolicyRequest.data.item : null; + + if (!agentPol) { + setIsK8s('IS_NOT_KUBERNETES'); + return; + } + const k8s = (pkg: PackagePolicy) => pkg.package?.name === FLEET_KUBERNETES_PACKAGE; + setIsK8s( + (agentPol.package_policies as PackagePolicy[]).some(k8s) + ? 'IS_KUBERNETES' + : 'IS_NOT_KUBERNETES' + ); + } + checkifK8s(); + }, [selectedPolicyId, notifications.toasts]); useEffect(() => { async function fetchFullPolicy() { @@ -53,7 +86,11 @@ export const StandaloneInstructions = React.memo(({ agentPolicy, agentPol if (!selectedPolicyId) { return; } - const res = await sendGetOneAgentPolicyFull(selectedPolicyId, { standalone: true }); + let query = { standalone: true, kubernetes: false }; + if (isK8s === 'IS_KUBERNETES') { + query = { standalone: true, kubernetes: true }; + } + const res = await sendGetOneAgentPolicyFull(selectedPolicyId, query); if (res.error) { throw res.error; } @@ -61,7 +98,6 @@ export const StandaloneInstructions = React.memo(({ agentPolicy, agentPol if (!res.data) { throw new Error('No data while fetching full agent policy'); } - setFullAgentPolicy(res.data.item); } catch (error) { notifications.toasts.addError(error, { @@ -69,10 +105,86 @@ export const StandaloneInstructions = React.memo(({ agentPolicy, agentPol }); } } - fetchFullPolicy(); - }, [selectedPolicyId, notifications.toasts]); + if (isK8s !== 'IS_LOADING') { + fetchFullPolicy(); + } + }, [selectedPolicyId, notifications.toasts, isK8s, core.http.basePath]); + + useEffect(() => { + if (isK8s === 'IS_KUBERNETES') { + if (typeof fullAgentPolicy === 'object') { + return; + } + setYaml(fullAgentPolicy); + } else { + if (typeof fullAgentPolicy === 'string') { + return; + } + setYaml(fullAgentPolicyToYaml(fullAgentPolicy, safeDump)); + } + }, [fullAgentPolicy, isK8s]); + + const policyMsg = + isK8s === 'IS_KUBERNETES' ? ( + ES_USERNAME, + ESPasswordVariable: ES_PASSWORD, + }} + /> + ) : ( + elastic-agent.yml, + ESUsernameVariable: ES_USERNAME, + ESPasswordVariable: ES_PASSWORD, + outputSection: outputs, + }} + /> + ); + + let downloadLink = ''; + if (selectedPolicyId) { + downloadLink = + isK8s === 'IS_KUBERNETES' + ? core.http.basePath.prepend( + `${agentPolicyRouteService.getInfoFullDownloadPath(selectedPolicyId)}?kubernetes=true` + ) + : core.http.basePath.prepend( + `${agentPolicyRouteService.getInfoFullDownloadPath(selectedPolicyId)}?standalone=true` + ); + } + + const downloadMsg = + isK8s === 'IS_KUBERNETES' ? ( + + ) : ( + + ); + + const applyMsg = + isK8s === 'IS_KUBERNETES' ? ( + + ) : ( + + ); - const yaml = useMemo(() => fullAgentPolicyToYaml(fullAgentPolicy, safeDump), [fullAgentPolicy]); const steps = [ DownloadStep(), !agentPolicy @@ -85,16 +197,7 @@ export const StandaloneInstructions = React.memo(({ agentPolicy, agentPol children: ( <> - elastic-agent.yml, - ESUsernameVariable: ES_USERNAME, - ESPasswordVariable: ES_PASSWORD, - outputSection: outputs, - }} - /> + <>{policyMsg} @@ -111,10 +214,7 @@ export const StandaloneInstructions = React.memo(({ agentPolicy, agentPol - + <>{downloadMsg} @@ -133,14 +233,11 @@ export const StandaloneInstructions = React.memo(({ agentPolicy, agentPol children: ( <> - + <>{applyMsg} - {RUN_INSTRUCTIONS} + {runInstructions} - + {(copy) => ( { export const sendGetOneAgentPolicyFull = ( agentPolicyId: string, - query: { standalone?: boolean } = {} + query: { standalone?: boolean; kubernetes?: boolean } = {} ) => { return sendRequest({ path: agentPolicyRouteService.getInfoFullPath(agentPolicyId), diff --git a/x-pack/plugins/fleet/public/search_provider.test.ts b/x-pack/plugins/fleet/public/search_provider.test.ts index ef6bda44d512b..97ed199c44502 100644 --- a/x-pack/plugins/fleet/public/search_provider.test.ts +++ b/x-pack/plugins/fleet/public/search_provider.test.ts @@ -87,22 +87,22 @@ describe('Package search provider', () => { ).toBe('--(a|)', { a: [ { - id: 'test-test', + id: 'test', score: 80, title: 'test', type: 'integration', url: { - path: 'undefined/detail/test-test/overview', + path: 'undefined/detail/test/overview', prependBasePath: false, }, }, { - id: 'test1-test1', + id: 'test1', score: 80, title: 'test1', type: 'integration', url: { - path: 'undefined/detail/test1-test1/overview', + path: 'undefined/detail/test1/overview', prependBasePath: false, }, }, @@ -170,12 +170,12 @@ describe('Package search provider', () => { ).toBe('--(a|)', { a: [ { - id: 'test1-test1', + id: 'test1', score: 80, title: 'test1', type: 'integration', url: { - path: 'undefined/detail/test1-test1/overview', + path: 'undefined/detail/test1/overview', prependBasePath: false, }, }, @@ -226,22 +226,22 @@ describe('Package search provider', () => { ).toBe('--(a|)', { a: [ { - id: 'test-test', + id: 'test', score: 80, title: 'test', type: 'integration', url: { - path: 'undefined/detail/test-test/overview', + path: 'undefined/detail/test/overview', prependBasePath: false, }, }, { - id: 'test1-test1', + id: 'test1', score: 80, title: 'test1', type: 'integration', url: { - path: 'undefined/detail/test1-test1/overview', + path: 'undefined/detail/test1/overview', prependBasePath: false, }, }, @@ -269,12 +269,12 @@ describe('Package search provider', () => { ).toBe('--(a|)', { a: [ { - id: 'test1-test1', + id: 'test1', score: 80, title: 'test1', type: 'integration', url: { - path: 'undefined/detail/test1-test1/overview', + path: 'undefined/detail/test1/overview', prependBasePath: false, }, }, diff --git a/x-pack/plugins/fleet/public/search_provider.ts b/x-pack/plugins/fleet/public/search_provider.ts index 403abf89715c8..d919462f38c28 100644 --- a/x-pack/plugins/fleet/public/search_provider.ts +++ b/x-pack/plugins/fleet/public/search_provider.ts @@ -53,21 +53,19 @@ export const toSearchResult = ( pkg: PackageListItem, application: ApplicationStart, basePath: IBasePath -): GlobalSearchProviderResult => { - const pkgkey = `${pkg.name}-${pkg.version}`; - return { - id: pkgkey, - type: packageType, - title: pkg.title, - score: 80, - icon: getEuiIconType(pkg, basePath), - url: { - // prettier-ignore - path: `${application.getUrlForApp(INTEGRATIONS_PLUGIN_ID)}${pagePathGetters.integration_details_overview({ pkgkey })[1]}`, - prependBasePath: false, - }, - }; -}; +): GlobalSearchProviderResult => ({ + id: pkg.name, + type: packageType, + title: pkg.title, + score: 80, + icon: getEuiIconType(pkg, basePath), + url: { + path: `${application.getUrlForApp(INTEGRATIONS_PLUGIN_ID)}${ + pagePathGetters.integration_details_overview({ pkgkey: pkg.name })[1] + }`, + prependBasePath: false, + }, +}); export const createPackageSearchProvider = (core: CoreSetup): GlobalSearchResultProvider => { const coreStart$ = from(core.getStartServices()).pipe( diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index b3197d918d231..c3da75183f581 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -34,6 +34,7 @@ import type { CopyAgentPolicyResponse, DeleteAgentPolicyResponse, GetFullAgentPolicyResponse, + GetFullAgentConfigMapResponse, } from '../../../common'; import { defaultIngestErrorHandler } from '../../errors'; @@ -232,27 +233,52 @@ export const getFullAgentPolicy: RequestHandler< > = async (context, request, response) => { const soClient = context.core.savedObjects.client; - try { - const fullAgentPolicy = await agentPolicyService.getFullAgentPolicy( - soClient, - request.params.agentPolicyId, - { standalone: request.query.standalone === true } - ); - if (fullAgentPolicy) { - const body: GetFullAgentPolicyResponse = { - item: fullAgentPolicy, - }; - return response.ok({ - body, - }); - } else { - return response.customError({ - statusCode: 404, - body: { message: 'Agent policy not found' }, - }); + if (request.query.kubernetes === true) { + try { + const fullAgentConfigMap = await agentPolicyService.getFullAgentConfigMap( + soClient, + request.params.agentPolicyId, + { standalone: request.query.standalone === true } + ); + if (fullAgentConfigMap) { + const body: GetFullAgentConfigMapResponse = { + item: fullAgentConfigMap, + }; + return response.ok({ + body, + }); + } else { + return response.customError({ + statusCode: 404, + body: { message: 'Agent config map not found' }, + }); + } + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } + } else { + try { + const fullAgentPolicy = await agentPolicyService.getFullAgentPolicy( + soClient, + request.params.agentPolicyId, + { standalone: request.query.standalone === true } + ); + if (fullAgentPolicy) { + const body: GetFullAgentPolicyResponse = { + item: fullAgentPolicy, + }; + return response.ok({ + body, + }); + } else { + return response.customError({ + statusCode: 404, + body: { message: 'Agent policy not found' }, + }); + } + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } - } catch (error) { - return defaultIngestErrorHandler({ error, response }); } }; @@ -265,27 +291,55 @@ export const downloadFullAgentPolicy: RequestHandler< params: { agentPolicyId }, } = request; - try { - const fullAgentPolicy = await agentPolicyService.getFullAgentPolicy(soClient, agentPolicyId, { - standalone: request.query.standalone === true, - }); - if (fullAgentPolicy) { - const body = fullAgentPolicyToYaml(fullAgentPolicy, safeDump); - const headers: ResponseHeaders = { - 'content-type': 'text/x-yaml', - 'content-disposition': `attachment; filename="elastic-agent.yml"`, - }; - return response.ok({ - body, - headers, - }); - } else { - return response.customError({ - statusCode: 404, - body: { message: 'Agent policy not found' }, + if (request.query.kubernetes === true) { + try { + const fullAgentConfigMap = await agentPolicyService.getFullAgentConfigMap( + soClient, + request.params.agentPolicyId, + { standalone: request.query.standalone === true } + ); + if (fullAgentConfigMap) { + const body = fullAgentConfigMap; + const headers: ResponseHeaders = { + 'content-type': 'text/x-yaml', + 'content-disposition': `attachment; filename="elastic-agent-standalone-kubernetes.yaml"`, + }; + return response.ok({ + body, + headers, + }); + } else { + return response.customError({ + statusCode: 404, + body: { message: 'Agent config map not found' }, + }); + } + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } + } else { + try { + const fullAgentPolicy = await agentPolicyService.getFullAgentPolicy(soClient, agentPolicyId, { + standalone: request.query.standalone === true, }); + if (fullAgentPolicy) { + const body = fullAgentPolicyToYaml(fullAgentPolicy, safeDump); + const headers: ResponseHeaders = { + 'content-type': 'text/x-yaml', + 'content-disposition': `attachment; filename="elastic-agent.yml"`, + }; + return response.ok({ + body, + headers, + }); + } else { + return response.customError({ + statusCode: 404, + body: { message: 'Agent policy not found' }, + }); + } + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } - } catch (error) { - return defaultIngestErrorHandler({ error, response }); } }; diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index f0b51b19dda33..e8fda952f17e6 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -44,7 +44,7 @@ import { } from './migrations/to_v7_13_0'; import { migratePackagePolicyToV7140, migrateInstallationToV7140 } from './migrations/to_v7_14_0'; import { migratePackagePolicyToV7150 } from './migrations/to_v7_15_0'; -import { migrateInstallationToV7160 } from './migrations/to_v7_16_0'; +import { migrateInstallationToV7160, migratePackagePolicyToV7160 } from './migrations/to_v7_16_0'; /* * Saved object types and mappings @@ -294,6 +294,7 @@ const getSavedObjectTypes = ( '7.13.0': migratePackagePolicyToV7130, '7.14.0': migratePackagePolicyToV7140, '7.15.0': migratePackagePolicyToV7150, + '7.16.0': migratePackagePolicyToV7160, }, }, [PACKAGES_SAVED_OBJECT_TYPE]: { diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_16_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_16_0.ts index 7d12c550ec406..b69523434408b 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_16_0.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_16_0.ts @@ -7,9 +7,11 @@ import type { SavedObjectMigrationFn } from 'kibana/server'; -import type { Installation } from '../../../common'; +import type { Installation, PackagePolicy } from '../../../common'; import { AUTO_UPDATE_PACKAGES, DEFAULT_PACKAGES } from '../../../common'; +import { migratePackagePolicyToV7160 as SecSolMigratePackagePolicyToV7160 } from './security_solution'; + export const migrateInstallationToV7160: SavedObjectMigrationFn = ( installationDoc, migrationContext @@ -26,3 +28,17 @@ export const migrateInstallationToV7160: SavedObjectMigrationFn = ( + packagePolicyDoc, + migrationContext +) => { + let updatedPackagePolicyDoc = packagePolicyDoc; + + // Endpoint specific migrations + if (packagePolicyDoc.attributes.package?.name === 'endpoint') { + updatedPackagePolicyDoc = SecSolMigratePackagePolicyToV7160(packagePolicyDoc, migrationContext); + } + + return updatedPackagePolicyDoc; +}; diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts index 561c463b998d4..60cf9c8d96257 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts @@ -70,12 +70,14 @@ export async function getFullAgentPolicy( if (!monitoringOutput) { throw new Error(`Monitoring output not found ${monitoringOutputId}`); } - const fullAgentPolicy: FullAgentPolicy = { id: agentPolicy.id, outputs: { ...outputs.reduce((acc, output) => { - acc[getOutputIdForAgentPolicy(output)] = transformOutputToFullPolicyOutput(output); + acc[getOutputIdForAgentPolicy(output)] = transformOutputToFullPolicyOutput( + output, + standalone + ); return acc; }, {}), @@ -179,8 +181,8 @@ function transformOutputToFullPolicyOutput( if (standalone) { delete newOutput.api_key; - newOutput.username = 'ES_USERNAME'; - newOutput.password = 'ES_PASSWORD'; + newOutput.username = '{ES_USERNAME}'; + newOutput.password = '{ES_PASSWORD}'; } return newOutput; diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 6ebe890aeaef2..321bc7f289594 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -13,6 +13,8 @@ import type { SavedObjectsBulkUpdateResponse, } from 'src/core/server'; +import { safeDump } from 'js-yaml'; + import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import type { AuthenticatedUser } from '../../../security/server'; @@ -41,6 +43,12 @@ import type { } from '../../common'; import { AgentPolicyNameExistsError, HostedAgentPolicyRestrictionRelatedError } from '../errors'; +import type { FullAgentConfigMap } from '../../common/types/models/agent_cm'; + +import { fullAgentConfigMapToYaml } from '../../common/services/agent_cm_to_yaml'; + +import { elasticAgentManifest } from './elastic_agent_manifest'; + import { getPackageInfo } from './epm/packages'; import { getAgentsByKuery } from './agents'; import { packagePolicyService } from './package_policy'; @@ -49,7 +57,6 @@ import { agentPolicyUpdateEventHandler } from './agent_policy_update'; import { normalizeKuery, escapeSearchQueryPhrase } from './saved_object'; import { appContextService } from './app_context'; import { getFullAgentPolicy } from './agent_policies'; - const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE; class AgentPolicyService { @@ -717,6 +724,40 @@ class AgentPolicyService { return res.body.hits.hits[0]._source; } + public async getFullAgentConfigMap( + soClient: SavedObjectsClientContract, + id: string, + options?: { standalone: boolean } + ): Promise { + const fullAgentPolicy = await getFullAgentPolicy(soClient, id, options); + if (fullAgentPolicy) { + const fullAgentConfigMap: FullAgentConfigMap = { + apiVersion: 'v1', + kind: 'ConfigMap', + metadata: { + name: 'agent-node-datastreams', + namespace: 'kube-system', + labels: { + 'k8s-app': 'elastic-agent', + }, + }, + data: { + 'agent.yml': fullAgentPolicy, + }, + }; + + const configMapYaml = fullAgentConfigMapToYaml(fullAgentConfigMap, safeDump); + const updateManifestVersion = elasticAgentManifest.replace( + 'VERSION', + appContextService.getKibanaVersion() + ); + const fixedAgentYML = configMapYaml.replace('agent.yml:', 'agent.yml: |-'); + return [fixedAgentYML, updateManifestVersion].join('\n'); + } else { + return ''; + } + } + public async getFullAgentPolicy( soClient: SavedObjectsClientContract, id: string, diff --git a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts new file mode 100644 index 0000000000000..392ee170d02ad --- /dev/null +++ b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts @@ -0,0 +1,222 @@ +/* + * 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. + */ + +export const elasticAgentManifest = ` +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: elastic-agent + namespace: kube-system + labels: + app: elastic-agent +spec: + selector: + matchLabels: + app: elastic-agent + template: + metadata: + labels: + app: elastic-agent + spec: + tolerations: + - key: node-role.kubernetes.io/master + effect: NoSchedule + serviceAccountName: elastic-agent + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + containers: + - name: elastic-agent + image: docker.elastic.co/beats/elastic-agent:VERSION + args: [ + "-c", "/etc/agent.yml", + "-e", + "-d", "'*'", + ] + env: + - name: ES_USERNAME + value: "elastic" + - name: ES_PASSWORD + value: "changeme" + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + securityContext: + runAsUser: 0 + resources: + limits: + memory: 500Mi + requests: + cpu: 100m + memory: 200Mi + volumeMounts: + - name: datastreams + mountPath: /etc/agent.yml + readOnly: true + subPath: agent.yml + - name: proc + mountPath: /hostfs/proc + readOnly: true + - name: cgroup + mountPath: /hostfs/sys/fs/cgroup + readOnly: true + - name: varlibdockercontainers + mountPath: /var/lib/docker/containers + readOnly: true + - name: varlog + mountPath: /var/log + readOnly: true + volumes: + - name: datastreams + configMap: + defaultMode: 0640 + name: agent-node-datastreams + - name: proc + hostPath: + path: /proc + - name: cgroup + hostPath: + path: /sys/fs/cgroup + - name: varlibdockercontainers + hostPath: + path: /var/lib/docker/containers + - name: varlog + hostPath: + path: /var/log +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: elastic-agent +subjects: + - kind: ServiceAccount + name: elastic-agent + namespace: kube-system +roleRef: + kind: ClusterRole + name: elastic-agent + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + namespace: kube-system + name: elastic-agent +subjects: + - kind: ServiceAccount + name: elastic-agent + namespace: kube-system +roleRef: + kind: Role + name: elastic-agent + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: elastic-agent-kubeadm-config + namespace: kube-system +subjects: + - kind: ServiceAccount + name: elastic-agent + namespace: kube-system +roleRef: + kind: Role + name: elastic-agent-kubeadm-config + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: elastic-agent + labels: + k8s-app: elastic-agent +rules: + - apiGroups: [""] + resources: + - nodes + - namespaces + - events + - pods + - services + - configmaps + verbs: ["get", "list", "watch"] + # Enable this rule only if planing to use kubernetes_secrets provider + #- apiGroups: [""] + # resources: + # - secrets + # verbs: ["get"] + - apiGroups: ["extensions"] + resources: + - replicasets + verbs: ["get", "list", "watch"] + - apiGroups: ["apps"] + resources: + - statefulsets + - deployments + - replicasets + verbs: ["get", "list", "watch"] + - apiGroups: ["batch"] + resources: + - jobs + verbs: ["get", "list", "watch"] + - apiGroups: + - "" + resources: + - nodes/stats + verbs: + - get + # required for apiserver + - nonResourceURLs: + - "/metrics" + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: elastic-agent + # should be the namespace where elastic-agent is running + namespace: kube-system + labels: + k8s-app: elastic-agent +rules: + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: ["get", "create", "update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: elastic-agent-kubeadm-config + namespace: kube-system + labels: + k8s-app: elastic-agent +rules: + - apiGroups: [""] + resources: + - configmaps + resourceNames: + - kubeadm-config + verbs: ["get"] +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: elastic-agent + namespace: kube-system + labels: + k8s-app: elastic-agent +--- +`; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/cleanup.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/cleanup.test.ts index 482e42a46060e..07fd2d400b8d5 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/cleanup.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/cleanup.test.ts @@ -17,7 +17,7 @@ import { removeOldAssets } from './cleanup'; jest.mock('../..', () => ({ appContextService: { getLogger: () => ({ - info: jest.fn(), + debug: jest.fn(), }), }, })); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/cleanup.ts b/x-pack/plugins/fleet/server/services/epm/packages/cleanup.ts index d70beb53eddab..87eaa82aa85f0 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/cleanup.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/cleanup.ts @@ -57,7 +57,7 @@ async function removeAssetsFromVersion( if (total > 0) { appContextService .getLogger() - .info(`Package "${pkgName}-${oldVersion}" still being used by policies`); + .debug(`Package "${pkgName}-${oldVersion}" still being used by policies`); return; } diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index 9dc05ee2cb4ba..c25a1db753c73 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -128,6 +128,12 @@ jest.mock('./agent_policy', () => { }; }); +jest.mock('./epm/packages/cleanup', () => { + return { + removeOldAssets: jest.fn(), + }; +}); + const mockedFetchInfo = fetchInfo as jest.Mock>; type CombinedExternalCallback = PutPackagePolicyUpdateCallback | PostPackagePolicyCreateCallback; diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 9928ce3063159..fa9df22eb5e8c 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -424,7 +424,17 @@ class PackagePolicyService { user: options?.user, }); - return (await this.get(soClient, id)) as PackagePolicy; + const newPolicy = (await this.get(soClient, id)) as PackagePolicy; + + if (packagePolicy.package) { + await removeOldAssets({ + soClient, + pkgName: packagePolicy.package.name, + currentVersion: packagePolicy.package.version, + }); + } + + return newPolicy; } public async delete( @@ -596,11 +606,6 @@ class PackagePolicyService { name: packagePolicy.name, success: true, }); - await removeOldAssets({ - soClient, - pkgName: packageInfo.name, - currentVersion: packageInfo.version, - }); } catch (error) { // We only want to specifically handle validation errors for the new package policy. If a more severe or // general error is thrown elsewhere during the upgrade process, we want to surface that directly in diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts index 714ffab922dd9..64d142f150bfd 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts @@ -56,5 +56,6 @@ export const GetFullAgentPolicyRequestSchema = { query: schema.object({ download: schema.maybe(schema.boolean()), standalone: schema.maybe(schema.boolean()), + kubernetes: schema.maybe(schema.boolean()), }), }; diff --git a/x-pack/plugins/infra/server/deprecations.ts b/x-pack/plugins/infra/server/deprecations.ts index 27c2b235f769b..70131cd96d117 100644 --- a/x-pack/plugins/infra/server/deprecations.ts +++ b/x-pack/plugins/infra/server/deprecations.ts @@ -142,7 +142,7 @@ const FIELD_DEPRECATION_FACTORIES: Record Dep }), }; -export const configDeprecations: ConfigDeprecationProvider = () => [ +export const configDeprecations: ConfigDeprecationProvider = ({ deprecate }) => [ ...Object.keys(FIELD_DEPRECATION_FACTORIES).map( (key): ConfigDeprecation => (completeConfig, rootPath, addDeprecation) => { @@ -179,6 +179,8 @@ export const configDeprecations: ConfigDeprecationProvider = () => [ return completeConfig; } ), + deprecate('sources.default.logAlias', '8.0.0'), + deprecate('sources.default.metricAlias', '8.0.0'), ]; export const getInfraDeprecationsFactory = diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx deleted file mode 100644 index 5c285f70b2ed9..0000000000000 --- a/x-pack/plugins/lens/public/mocks.tsx +++ /dev/null @@ -1,540 +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'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { ReactWrapper } from 'enzyme'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { mountWithIntl as mount } from '@kbn/test/jest'; -import { Observable, Subject } from 'rxjs'; -import { coreMock } from 'src/core/public/mocks'; -import moment from 'moment'; -import { Provider } from 'react-redux'; -import { act } from 'react-dom/test-utils'; -import { ReactExpressionRendererProps } from 'src/plugins/expressions/public'; -import { PreloadedState } from '@reduxjs/toolkit'; -import { LensPublicStart } from '.'; -import { visualizationTypes } from './xy_visualization/types'; -import { navigationPluginMock } from '../../../../src/plugins/navigation/public/mocks'; -import { LensAppServices } from './app_plugin/types'; -import { DOC_TYPE, layerTypes } from '../common'; -import { DataPublicPluginStart, esFilters, UI_SETTINGS } from '../../../../src/plugins/data/public'; -import { inspectorPluginMock } from '../../../../src/plugins/inspector/public/mocks'; -import { spacesPluginMock } from '../../spaces/public/mocks'; -import { dashboardPluginMock } from '../../../../src/plugins/dashboard/public/mocks'; -import type { - LensByValueInput, - LensByReferenceInput, - ResolvedLensSavedObjectAttributes, -} from './embeddable/embeddable'; -import { - mockAttributeService, - createEmbeddableStateTransferMock, -} from '../../../../src/plugins/embeddable/public/mocks'; -import { fieldFormatsServiceMock } from '../../../../src/plugins/field_formats/public/mocks'; -import type { LensAttributeService } from './lens_attribute_service'; -import type { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/public'; - -import { - makeConfigureStore, - LensAppState, - LensState, - LensStoreDeps, -} from './state_management/index'; -import { getResolvedDateRange } from './utils'; -import { presentationUtilPluginMock } from '../../../../src/plugins/presentation_util/public/mocks'; -import { - DatasourcePublicAPI, - Datasource, - Visualization, - FramePublicAPI, - FrameDatasourceAPI, - DatasourceMap, - VisualizationMap, -} from './types'; -import { getLensInspectorService } from './lens_inspector_service'; - -export function mockDatasourceStates() { - return { - testDatasource: { - state: {}, - isLoading: false, - }, - }; -} - -export function createMockVisualization(id = 'testVis'): jest.Mocked { - return { - id, - clearLayer: jest.fn((state, _layerId) => state), - removeLayer: jest.fn(), - getLayerIds: jest.fn((_state) => ['layer1']), - getSupportedLayers: jest.fn(() => [{ type: layerTypes.DATA, label: 'Data Layer' }]), - getLayerType: jest.fn((_state, _layerId) => layerTypes.DATA), - visualizationTypes: [ - { - icon: 'empty', - id, - label: 'TEST', - groupLabel: `${id}Group`, - }, - ], - appendLayer: jest.fn(), - getVisualizationTypeId: jest.fn((_state) => 'empty'), - getDescription: jest.fn((_state) => ({ label: '' })), - switchVisualizationType: jest.fn((_, x) => x), - getSuggestions: jest.fn((_options) => []), - initialize: jest.fn((_frame, _state?) => ({ newState: 'newState' })), - getConfiguration: jest.fn((props) => ({ - groups: [ - { - groupId: 'a', - groupLabel: 'a', - layerId: 'layer1', - supportsMoreColumns: true, - accessors: [], - filterOperations: jest.fn(() => true), - dataTestSubj: 'mockVisA', - }, - ], - })), - toExpression: jest.fn((_state, _frame) => null), - toPreviewExpression: jest.fn((_state, _frame) => null), - - setDimension: jest.fn(), - removeDimension: jest.fn(), - getErrorMessages: jest.fn((_state) => undefined), - renderDimensionEditor: jest.fn(), - }; -} - -export const visualizationMap = { - testVis: createMockVisualization(), - testVis2: createMockVisualization(), -}; - -export type DatasourceMock = jest.Mocked & { - publicAPIMock: jest.Mocked; -}; - -export function createMockDatasource(id: string): DatasourceMock { - const publicAPIMock: jest.Mocked = { - datasourceId: id, - getTableSpec: jest.fn(() => []), - getOperationForColumnId: jest.fn(), - }; - - return { - id: 'testDatasource', - clearLayer: jest.fn((state, _layerId) => state), - getDatasourceSuggestionsForField: jest.fn((_state, _item, filterFn) => []), - getDatasourceSuggestionsForVisualizeField: jest.fn((_state, _indexpatternId, _fieldName) => []), - getDatasourceSuggestionsFromCurrentState: jest.fn((_state) => []), - getPersistableState: jest.fn((x) => ({ - state: x, - savedObjectReferences: [{ type: 'index-pattern', id: 'mockip', name: 'mockip' }], - })), - getPublicAPI: jest.fn().mockReturnValue(publicAPIMock), - initialize: jest.fn((_state?) => Promise.resolve()), - renderDataPanel: jest.fn(), - renderLayerPanel: jest.fn(), - toExpression: jest.fn((_frame, _state) => null), - insertLayer: jest.fn((_state, _newLayerId) => ({})), - removeLayer: jest.fn((_state, _layerId) => {}), - removeColumn: jest.fn((props) => {}), - getLayers: jest.fn((_state) => []), - uniqueLabels: jest.fn((_state) => ({})), - renderDimensionTrigger: jest.fn(), - renderDimensionEditor: jest.fn(), - getDropProps: jest.fn(), - onDrop: jest.fn(), - - // this is an additional property which doesn't exist on real datasources - // but can be used to validate whether specific API mock functions are called - publicAPIMock, - getErrorMessages: jest.fn((_state) => undefined), - checkIntegrity: jest.fn((_state) => []), - isTimeBased: jest.fn(), - isValidColumn: jest.fn(), - }; -} - -export const mockDatasource: DatasourceMock = createMockDatasource('testDatasource'); -export const mockDatasource2: DatasourceMock = createMockDatasource('testDatasource2'); - -export const datasourceMap = { - testDatasource2: mockDatasource2, - testDatasource: mockDatasource, -}; - -export function createExpressionRendererMock(): jest.Mock< - React.ReactElement, - [ReactExpressionRendererProps] -> { - return jest.fn((_) => ); -} - -export type FrameMock = jest.Mocked; -export function createMockFramePublicAPI(): FrameMock { - return { - datasourceLayers: {}, - }; -} - -export type FrameDatasourceMock = jest.Mocked; -export function createMockFrameDatasourceAPI(): FrameDatasourceMock { - return { - datasourceLayers: {}, - dateRange: { fromDate: 'now-7d', toDate: 'now' }, - query: { query: '', language: 'lucene' }, - filters: [], - }; -} - -export type Start = jest.Mocked; - -const createStartContract = (): Start => { - const startContract: Start = { - EmbeddableComponent: jest.fn(() => { - return Lens Embeddable Component; - }), - SaveModalComponent: jest.fn(() => { - return Lens Save Modal Component; - }), - canUseEditor: jest.fn(() => true), - navigateToPrefilledEditor: jest.fn(), - getXyVisTypes: jest.fn().mockReturnValue(new Promise((resolve) => resolve(visualizationTypes))), - }; - return startContract; -}; - -export const lensPluginMock = { - createStartContract, -}; - -export const defaultDoc = { - savedObjectId: '1234', - title: 'An extremely cool default document!', - expression: 'definitely a valid expression', - visualizationType: 'testVis', - state: { - query: 'kuery', - filters: [{ query: { match_phrase: { src: 'test' } } }], - datasourceStates: { - testDatasource: 'datasource', - }, - visualization: {}, - }, - references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], -} as unknown as Document; - -export function createMockTimefilter() { - const unsubscribe = jest.fn(); - - let timeFilter = { from: 'now-7d', to: 'now' }; - let subscriber: () => void; - return { - getTime: jest.fn(() => timeFilter), - setTime: jest.fn((newTimeFilter) => { - timeFilter = newTimeFilter; - if (subscriber) { - subscriber(); - } - }), - getTimeUpdate$: () => ({ - subscribe: ({ next }: { next: () => void }) => { - subscriber = next; - return unsubscribe; - }, - }), - calculateBounds: jest.fn(() => ({ - min: moment('2021-01-10T04:00:00.000Z'), - max: moment('2021-01-10T08:00:00.000Z'), - })), - getBounds: jest.fn(() => timeFilter), - getRefreshInterval: () => {}, - getRefreshIntervalDefaults: () => {}, - getAutoRefreshFetch$: () => new Observable(), - }; -} - -export const exactMatchDoc = { - ...defaultDoc, - sharingSavedObjectProps: { - outcome: 'exactMatch', - }, -}; - -export const mockStoreDeps = (deps?: { - lensServices?: LensAppServices; - datasourceMap?: DatasourceMap; - visualizationMap?: VisualizationMap; -}) => { - return { - datasourceMap: deps?.datasourceMap || datasourceMap, - visualizationMap: deps?.visualizationMap || visualizationMap, - lensServices: deps?.lensServices || makeDefaultServices(), - }; -}; - -export function mockDataPlugin( - sessionIdSubject = new Subject(), - initialSessionId?: string -) { - function createMockSearchService() { - let sessionIdCounter = initialSessionId ? 1 : 0; - let currentSessionId: string | undefined = initialSessionId; - const start = () => { - currentSessionId = `sessionId-${++sessionIdCounter}`; - return currentSessionId; - }; - return { - session: { - start: jest.fn(start), - clear: jest.fn(), - getSessionId: jest.fn(() => currentSessionId), - getSession$: jest.fn(() => sessionIdSubject.asObservable()), - }, - }; - } - - function createMockFilterManager() { - const unsubscribe = jest.fn(); - - let subscriber: () => void; - let filters: unknown = []; - - return { - getUpdates$: () => ({ - subscribe: ({ next }: { next: () => void }) => { - subscriber = next; - return unsubscribe; - }, - }), - setFilters: jest.fn((newFilters: unknown[]) => { - filters = newFilters; - if (subscriber) subscriber(); - }), - setAppFilters: jest.fn((newFilters: unknown[]) => { - filters = newFilters; - if (subscriber) subscriber(); - }), - getFilters: () => filters, - getGlobalFilters: () => { - // @ts-ignore - return filters.filter(esFilters.isFilterPinned); - }, - removeAll: () => { - filters = []; - subscriber(); - }, - }; - } - function createMockQueryString() { - return { - getQuery: jest.fn(() => ({ query: '', language: 'lucene' })), - setQuery: jest.fn(), - getDefaultQuery: jest.fn(() => ({ query: '', language: 'lucene' })), - }; - } - return { - query: { - filterManager: createMockFilterManager(), - timefilter: { - timefilter: createMockTimefilter(), - }, - queryString: createMockQueryString(), - state$: new Observable(), - }, - indexPatterns: { - get: jest.fn().mockImplementation((id) => Promise.resolve({ id, isTimeBased: () => true })), - }, - search: createMockSearchService(), - nowProvider: { - get: jest.fn(), - }, - fieldFormats: { - deserialize: jest.fn(), - }, - } as unknown as DataPublicPluginStart; -} - -export function makeDefaultServices( - sessionIdSubject = new Subject(), - sessionId: string | undefined = undefined, - doc = defaultDoc -): jest.Mocked { - const core = coreMock.createStart({ basePath: '/testbasepath' }); - core.uiSettings.get.mockImplementation( - jest.fn((type) => { - if (type === UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS) { - return { from: 'now-7d', to: 'now' }; - } else if (type === UI_SETTINGS.SEARCH_QUERY_LANGUAGE) { - return 'kuery'; - } else if (type === 'state:storeInSessionStorage') { - return false; - } else { - return []; - } - }) - ); - - const navigationStartMock = navigationPluginMock.createStartContract(); - - jest.spyOn(navigationStartMock.ui.TopNavMenu.prototype, 'constructor').mockImplementation(() => { - return
; - }); - - function makeAttributeService(): LensAttributeService { - const attributeServiceMock = mockAttributeService< - ResolvedLensSavedObjectAttributes, - LensByValueInput, - LensByReferenceInput - >( - DOC_TYPE, - { - saveMethod: jest.fn(), - unwrapMethod: jest.fn(), - checkForDuplicateTitle: jest.fn(), - }, - core - ); - attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue(exactMatchDoc); - attributeServiceMock.wrapAttributes = jest.fn().mockResolvedValue({ - savedObjectId: (doc as unknown as LensByReferenceInput).savedObjectId, - }); - - return attributeServiceMock; - } - - return { - http: core.http, - chrome: core.chrome, - overlays: core.overlays, - uiSettings: core.uiSettings, - navigation: navigationStartMock, - notifications: core.notifications, - attributeService: makeAttributeService(), - inspector: { - adapters: getLensInspectorService(inspectorPluginMock.createStartContract()).adapters, - inspect: jest.fn(), - close: jest.fn(), - }, - dashboard: dashboardPluginMock.createStartContract(), - presentationUtil: presentationUtilPluginMock.createStartContract(core), - savedObjectsClient: core.savedObjects.client, - dashboardFeatureFlag: { allowByValueEmbeddables: false }, - stateTransfer: createEmbeddableStateTransferMock() as EmbeddableStateTransfer, - getOriginatingAppName: jest.fn(() => 'defaultOriginatingApp'), - application: { - ...core.application, - capabilities: { - ...core.application.capabilities, - visualize: { save: true, saveQuery: true, show: true }, - }, - getUrlForApp: jest.fn((appId: string) => `/testbasepath/app/${appId}#/`), - }, - data: mockDataPlugin(sessionIdSubject, sessionId), - fieldFormats: fieldFormatsServiceMock.createStartContract(), - storage: { - get: jest.fn(), - set: jest.fn(), - remove: jest.fn(), - clear: jest.fn(), - }, - spaces: spacesPluginMock.createStartContract(), - }; -} - -export const defaultState = { - searchSessionId: 'sessionId-1', - filters: [], - query: { language: 'lucene', query: '' }, - resolvedDateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' }, - isFullscreenDatasource: false, - isSaveable: false, - isLoading: false, - isLinkedToOriginatingApp: false, - activeDatasourceId: 'testDatasource', - visualization: { - state: {}, - activeId: 'testVis', - }, - datasourceStates: mockDatasourceStates(), -}; - -export function makeLensStore({ - preloadedState, - dispatch, - storeDeps = mockStoreDeps(), -}: { - storeDeps?: LensStoreDeps; - preloadedState?: Partial; - dispatch?: jest.Mock; -}) { - const data = storeDeps.lensServices.data; - const store = makeConfigureStore(storeDeps, { - lens: { - ...defaultState, - query: data.query.queryString.getQuery(), - filters: data.query.filterManager.getGlobalFilters(), - resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter), - ...preloadedState, - }, - } as PreloadedState); - - const origDispatch = store.dispatch; - store.dispatch = jest.fn(dispatch || origDispatch); - return { store, deps: storeDeps }; -} - -export const mountWithProvider = async ( - component: React.ReactElement, - store?: { - storeDeps?: LensStoreDeps; - preloadedState?: Partial; - dispatch?: jest.Mock; - }, - options?: { - wrappingComponent?: React.FC<{ - children: React.ReactNode; - }>; - attachTo?: HTMLElement; - } -) => { - const { store: lensStore, deps } = makeLensStore(store || {}); - - let wrappingComponent: React.FC<{ - children: React.ReactNode; - }> = ({ children }) => {children}; - - let restOptions: { - attachTo?: HTMLElement | undefined; - }; - if (options) { - const { wrappingComponent: _wrappingComponent, ...rest } = options; - restOptions = rest; - - if (_wrappingComponent) { - wrappingComponent = ({ children }) => { - return _wrappingComponent({ - children: {children}, - }); - }; - } - } - - let instance: ReactWrapper = {} as ReactWrapper; - - await act(async () => { - instance = mount(component, { - wrappingComponent, - ...restOptions, - } as unknown as ReactWrapper); - }); - return { instance, lensStore, deps }; -}; diff --git a/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts b/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts new file mode 100644 index 0000000000000..daab2566b28fe --- /dev/null +++ b/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts @@ -0,0 +1,122 @@ +/* + * 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 { Observable, Subject } from 'rxjs'; +import moment from 'moment'; +import { DataPublicPluginStart, esFilters } from '../../../../../src/plugins/data/public'; + +function createMockTimefilter() { + const unsubscribe = jest.fn(); + + let timeFilter = { from: 'now-7d', to: 'now' }; + let subscriber: () => void; + return { + getTime: jest.fn(() => timeFilter), + setTime: jest.fn((newTimeFilter) => { + timeFilter = newTimeFilter; + if (subscriber) { + subscriber(); + } + }), + getTimeUpdate$: () => ({ + subscribe: ({ next }: { next: () => void }) => { + subscriber = next; + return unsubscribe; + }, + }), + calculateBounds: jest.fn(() => ({ + min: moment('2021-01-10T04:00:00.000Z'), + max: moment('2021-01-10T08:00:00.000Z'), + })), + getBounds: jest.fn(() => timeFilter), + getRefreshInterval: () => {}, + getRefreshIntervalDefaults: () => {}, + getAutoRefreshFetch$: () => new Observable(), + }; +} + +export function mockDataPlugin( + sessionIdSubject = new Subject(), + initialSessionId?: string +) { + function createMockSearchService() { + let sessionIdCounter = initialSessionId ? 1 : 0; + let currentSessionId: string | undefined = initialSessionId; + const start = () => { + currentSessionId = `sessionId-${++sessionIdCounter}`; + return currentSessionId; + }; + return { + session: { + start: jest.fn(start), + clear: jest.fn(), + getSessionId: jest.fn(() => currentSessionId), + getSession$: jest.fn(() => sessionIdSubject.asObservable()), + }, + }; + } + + function createMockFilterManager() { + const unsubscribe = jest.fn(); + + let subscriber: () => void; + let filters: unknown = []; + + return { + getUpdates$: () => ({ + subscribe: ({ next }: { next: () => void }) => { + subscriber = next; + return unsubscribe; + }, + }), + setFilters: jest.fn((newFilters: unknown[]) => { + filters = newFilters; + if (subscriber) subscriber(); + }), + setAppFilters: jest.fn((newFilters: unknown[]) => { + filters = newFilters; + if (subscriber) subscriber(); + }), + getFilters: () => filters, + getGlobalFilters: () => { + // @ts-ignore + return filters.filter(esFilters.isFilterPinned); + }, + removeAll: () => { + filters = []; + subscriber(); + }, + }; + } + function createMockQueryString() { + return { + getQuery: jest.fn(() => ({ query: '', language: 'lucene' })), + setQuery: jest.fn(), + getDefaultQuery: jest.fn(() => ({ query: '', language: 'lucene' })), + }; + } + return { + query: { + filterManager: createMockFilterManager(), + timefilter: { + timefilter: createMockTimefilter(), + }, + queryString: createMockQueryString(), + state$: new Observable(), + }, + indexPatterns: { + get: jest.fn().mockImplementation((id) => Promise.resolve({ id, isTimeBased: () => true })), + }, + search: createMockSearchService(), + nowProvider: { + get: jest.fn(), + }, + fieldFormats: { + deserialize: jest.fn(), + }, + } as unknown as DataPublicPluginStart; +} diff --git a/x-pack/plugins/lens/public/mocks/datasource_mock.ts b/x-pack/plugins/lens/public/mocks/datasource_mock.ts new file mode 100644 index 0000000000000..2614b1d5fdc94 --- /dev/null +++ b/x-pack/plugins/lens/public/mocks/datasource_mock.ts @@ -0,0 +1,78 @@ +/* + * 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 { DatasourcePublicAPI, Datasource } from '../types'; + +export type DatasourceMock = jest.Mocked & { + publicAPIMock: jest.Mocked; +}; + +export function createMockDatasource(id: string): DatasourceMock { + const publicAPIMock: jest.Mocked = { + datasourceId: id, + getTableSpec: jest.fn(() => []), + getOperationForColumnId: jest.fn(), + }; + + return { + id: 'testDatasource', + clearLayer: jest.fn((state, _layerId) => state), + getDatasourceSuggestionsForField: jest.fn((_state, _item, filterFn) => []), + getDatasourceSuggestionsForVisualizeField: jest.fn((_state, _indexpatternId, _fieldName) => []), + getDatasourceSuggestionsFromCurrentState: jest.fn((_state) => []), + getPersistableState: jest.fn((x) => ({ + state: x, + savedObjectReferences: [{ type: 'index-pattern', id: 'mockip', name: 'mockip' }], + })), + getPublicAPI: jest.fn().mockReturnValue(publicAPIMock), + initialize: jest.fn((_state?) => Promise.resolve()), + renderDataPanel: jest.fn(), + renderLayerPanel: jest.fn(), + toExpression: jest.fn((_frame, _state) => null), + insertLayer: jest.fn((_state, _newLayerId) => ({})), + removeLayer: jest.fn((_state, _layerId) => {}), + removeColumn: jest.fn((props) => {}), + getLayers: jest.fn((_state) => []), + uniqueLabels: jest.fn((_state) => ({})), + renderDimensionTrigger: jest.fn(), + renderDimensionEditor: jest.fn(), + getDropProps: jest.fn(), + onDrop: jest.fn(), + + // this is an additional property which doesn't exist on real datasources + // but can be used to validate whether specific API mock functions are called + publicAPIMock, + getErrorMessages: jest.fn((_state) => undefined), + checkIntegrity: jest.fn((_state) => []), + isTimeBased: jest.fn(), + isValidColumn: jest.fn(), + }; +} + +export function mockDatasourceMap() { + const datasource = createMockDatasource('testDatasource'); + datasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ + { + state: {}, + table: { + columns: [], + isMultiRow: true, + layerId: 'a', + changeType: 'unchanged', + }, + keptLayerIds: ['a'], + }, + ]); + + datasource.getLayers.mockReturnValue(['a']); + return { + testDatasource2: createMockDatasource('testDatasource2'), + testDatasource: datasource, + }; +} + +export const datasourceMap = mockDatasourceMap(); diff --git a/x-pack/plugins/lens/public/mocks/expression_renderer_mock.tsx b/x-pack/plugins/lens/public/mocks/expression_renderer_mock.tsx new file mode 100644 index 0000000000000..644021e8a69c2 --- /dev/null +++ b/x-pack/plugins/lens/public/mocks/expression_renderer_mock.tsx @@ -0,0 +1,16 @@ +/* + * 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 { ReactExpressionRendererProps } from 'src/plugins/expressions/public'; + +export function createExpressionRendererMock(): jest.Mock< + React.ReactElement, + [ReactExpressionRendererProps] +> { + return jest.fn((_) => ); +} diff --git a/x-pack/plugins/lens/public/mocks/index.ts b/x-pack/plugins/lens/public/mocks/index.ts new file mode 100644 index 0000000000000..2dd32a1679f1b --- /dev/null +++ b/x-pack/plugins/lens/public/mocks/index.ts @@ -0,0 +1,42 @@ +/* + * 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 { FramePublicAPI, FrameDatasourceAPI } from '../types'; +export { mockDataPlugin } from './data_plugin_mock'; +export { + visualizationMap, + createMockVisualization, + mockVisualizationMap, +} from './visualization_mock'; +export { datasourceMap, mockDatasourceMap, createMockDatasource } from './datasource_mock'; +export type { DatasourceMock } from './datasource_mock'; +export { createExpressionRendererMock } from './expression_renderer_mock'; +export { defaultDoc, exactMatchDoc, makeDefaultServices } from './services_mock'; +export { + mockStoreDeps, + mockDatasourceStates, + defaultState, + makeLensStore, + MountStoreProps, + mountWithProvider, +} from './store_mocks'; +export { lensPluginMock } from './lens_plugin_mock'; + +export type FrameMock = jest.Mocked; + +export const createMockFramePublicAPI = (): FrameMock => ({ + datasourceLayers: {}, +}); + +export type FrameDatasourceMock = jest.Mocked; + +export const createMockFrameDatasourceAPI = (): FrameDatasourceMock => ({ + datasourceLayers: {}, + dateRange: { fromDate: 'now-7d', toDate: 'now' }, + query: { query: '', language: 'lucene' }, + filters: [], +}); diff --git a/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx b/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx new file mode 100644 index 0000000000000..a92533a89ba67 --- /dev/null +++ b/x-pack/plugins/lens/public/mocks/lens_plugin_mock.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 React from 'react'; +import { LensPublicStart } from '..'; +import { visualizationTypes } from '../xy_visualization/types'; + +type Start = jest.Mocked; + +export const lensPluginMock = { + createStartContract: (): Start => { + const startContract: Start = { + EmbeddableComponent: jest.fn(() => { + return Lens Embeddable Component; + }), + SaveModalComponent: jest.fn(() => { + return Lens Save Modal Component; + }), + canUseEditor: jest.fn(() => true), + navigateToPrefilledEditor: jest.fn(), + getXyVisTypes: jest + .fn() + .mockReturnValue(new Promise((resolve) => resolve(visualizationTypes))), + }; + return startContract; + }, +}; diff --git a/x-pack/plugins/lens/public/mocks/services_mock.tsx b/x-pack/plugins/lens/public/mocks/services_mock.tsx new file mode 100644 index 0000000000000..c6db0dfb6aae8 --- /dev/null +++ b/x-pack/plugins/lens/public/mocks/services_mock.tsx @@ -0,0 +1,143 @@ +/* + * 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 { Subject } from 'rxjs'; +import { coreMock } from 'src/core/public/mocks'; +import { navigationPluginMock } from '../../../../../src/plugins/navigation/public/mocks'; +import { LensAppServices } from '../app_plugin/types'; +import { DOC_TYPE } from '../../common'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; +import { inspectorPluginMock } from '../../../../../src/plugins/inspector/public/mocks'; +import { spacesPluginMock } from '../../../spaces/public/mocks'; +import { dashboardPluginMock } from '../../../../../src/plugins/dashboard/public/mocks'; +import type { + LensByValueInput, + LensByReferenceInput, + ResolvedLensSavedObjectAttributes, +} from '../embeddable/embeddable'; +import { + mockAttributeService, + createEmbeddableStateTransferMock, +} from '../../../../../src/plugins/embeddable/public/mocks'; +import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; +import type { LensAttributeService } from '../lens_attribute_service'; +import type { EmbeddableStateTransfer } from '../../../../../src/plugins/embeddable/public'; + +import { presentationUtilPluginMock } from '../../../../../src/plugins/presentation_util/public/mocks'; +import { mockDataPlugin } from './data_plugin_mock'; +import { getLensInspectorService } from '../lens_inspector_service'; + +export const defaultDoc = { + savedObjectId: '1234', + title: 'An extremely cool default document!', + expression: 'definitely a valid expression', + visualizationType: 'testVis', + state: { + query: 'kuery', + filters: [{ query: { match_phrase: { src: 'test' } } }], + datasourceStates: { + testDatasource: 'datasource', + }, + visualization: {}, + }, + references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], +} as unknown as Document; + +export const exactMatchDoc = { + ...defaultDoc, + sharingSavedObjectProps: { + outcome: 'exactMatch', + }, +}; + +export function makeDefaultServices( + sessionIdSubject = new Subject(), + sessionId: string | undefined = undefined, + doc = defaultDoc +): jest.Mocked { + const core = coreMock.createStart({ basePath: '/testbasepath' }); + core.uiSettings.get.mockImplementation( + jest.fn((type) => { + if (type === UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS) { + return { from: 'now-7d', to: 'now' }; + } else if (type === UI_SETTINGS.SEARCH_QUERY_LANGUAGE) { + return 'kuery'; + } else if (type === 'state:storeInSessionStorage') { + return false; + } else { + return []; + } + }) + ); + + const navigationStartMock = navigationPluginMock.createStartContract(); + + jest.spyOn(navigationStartMock.ui.TopNavMenu.prototype, 'constructor').mockImplementation(() => { + return
; + }); + + function makeAttributeService(): LensAttributeService { + const attributeServiceMock = mockAttributeService< + ResolvedLensSavedObjectAttributes, + LensByValueInput, + LensByReferenceInput + >( + DOC_TYPE, + { + saveMethod: jest.fn(), + unwrapMethod: jest.fn(), + checkForDuplicateTitle: jest.fn(), + }, + core + ); + attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue(exactMatchDoc); + attributeServiceMock.wrapAttributes = jest.fn().mockResolvedValue({ + savedObjectId: (doc as unknown as LensByReferenceInput).savedObjectId, + }); + + return attributeServiceMock; + } + + return { + http: core.http, + chrome: core.chrome, + overlays: core.overlays, + uiSettings: core.uiSettings, + navigation: navigationStartMock, + notifications: core.notifications, + attributeService: makeAttributeService(), + inspector: { + adapters: getLensInspectorService(inspectorPluginMock.createStartContract()).adapters, + inspect: jest.fn(), + close: jest.fn(), + }, + dashboard: dashboardPluginMock.createStartContract(), + presentationUtil: presentationUtilPluginMock.createStartContract(core), + savedObjectsClient: core.savedObjects.client, + dashboardFeatureFlag: { allowByValueEmbeddables: false }, + stateTransfer: createEmbeddableStateTransferMock() as EmbeddableStateTransfer, + getOriginatingAppName: jest.fn(() => 'defaultOriginatingApp'), + application: { + ...core.application, + capabilities: { + ...core.application.capabilities, + visualize: { save: true, saveQuery: true, show: true }, + }, + getUrlForApp: jest.fn((appId: string) => `/testbasepath/app/${appId}#/`), + }, + data: mockDataPlugin(sessionIdSubject, sessionId), + fieldFormats: fieldFormatsServiceMock.createStartContract(), + storage: { + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), + }, + spaces: spacesPluginMock.createStartContract(), + }; +} diff --git a/x-pack/plugins/lens/public/mocks/store_mocks.tsx b/x-pack/plugins/lens/public/mocks/store_mocks.tsx new file mode 100644 index 0000000000000..1b1d83ef2892d --- /dev/null +++ b/x-pack/plugins/lens/public/mocks/store_mocks.tsx @@ -0,0 +1,140 @@ +/* + * 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'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { ReactWrapper } from 'enzyme'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { mountWithIntl as mount } from '@kbn/test/jest'; +import { Provider } from 'react-redux'; +import { act } from 'react-dom/test-utils'; +import { PreloadedState } from '@reduxjs/toolkit'; +import { LensAppServices } from '../app_plugin/types'; + +import { + makeConfigureStore, + LensAppState, + LensState, + LensStoreDeps, +} from '../state_management/index'; +import { getResolvedDateRange } from '../utils'; +import { DatasourceMap, VisualizationMap } from '../types'; +import { mockVisualizationMap } from './visualization_mock'; +import { mockDatasourceMap } from './datasource_mock'; +import { makeDefaultServices } from './services_mock'; + +export const mockStoreDeps = (deps?: { + lensServices?: LensAppServices; + datasourceMap?: DatasourceMap; + visualizationMap?: VisualizationMap; +}) => { + return { + datasourceMap: deps?.datasourceMap || mockDatasourceMap(), + visualizationMap: deps?.visualizationMap || mockVisualizationMap(), + lensServices: deps?.lensServices || makeDefaultServices(), + }; +}; + +export function mockDatasourceStates() { + return { + testDatasource: { + state: {}, + isLoading: false, + }, + }; +} + +export const defaultState = { + searchSessionId: 'sessionId-1', + filters: [], + query: { language: 'lucene', query: '' }, + resolvedDateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' }, + isFullscreenDatasource: false, + isSaveable: false, + isLoading: false, + isLinkedToOriginatingApp: false, + activeDatasourceId: 'testDatasource', + visualization: { + state: {}, + activeId: 'testVis', + }, + datasourceStates: mockDatasourceStates(), +}; + +export function makeLensStore({ + preloadedState, + dispatch, + storeDeps = mockStoreDeps(), +}: { + storeDeps?: LensStoreDeps; + preloadedState?: Partial; + dispatch?: jest.Mock; +}) { + const data = storeDeps.lensServices.data; + const store = makeConfigureStore(storeDeps, { + lens: { + ...defaultState, + query: data.query.queryString.getQuery(), + filters: data.query.filterManager.getGlobalFilters(), + resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter), + ...preloadedState, + }, + } as PreloadedState); + + const origDispatch = store.dispatch; + store.dispatch = jest.fn(dispatch || origDispatch); + return { store, deps: storeDeps }; +} + +export interface MountStoreProps { + storeDeps?: LensStoreDeps; + preloadedState?: Partial; + dispatch?: jest.Mock; +} + +export const mountWithProvider = async ( + component: React.ReactElement, + store?: MountStoreProps, + options?: { + wrappingComponent?: React.FC<{ + children: React.ReactNode; + }>; + attachTo?: HTMLElement; + } +) => { + const { store: lensStore, deps } = makeLensStore(store || {}); + + let wrappingComponent: React.FC<{ + children: React.ReactNode; + }> = ({ children }) => {children}; + + let restOptions: { + attachTo?: HTMLElement | undefined; + }; + if (options) { + const { wrappingComponent: _wrappingComponent, ...rest } = options; + restOptions = rest; + + if (_wrappingComponent) { + wrappingComponent = ({ children }) => { + return _wrappingComponent({ + children: {children}, + }); + }; + } + } + + let instance: ReactWrapper = {} as ReactWrapper; + + await act(async () => { + instance = mount(component, { + wrappingComponent, + ...restOptions, + } as unknown as ReactWrapper); + }); + return { instance, lensStore, deps }; +}; diff --git a/x-pack/plugins/lens/public/mocks/visualization_mock.ts b/x-pack/plugins/lens/public/mocks/visualization_mock.ts new file mode 100644 index 0000000000000..199bf9a9db77a --- /dev/null +++ b/x-pack/plugins/lens/public/mocks/visualization_mock.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { layerTypes } from '../../common'; +import { Visualization, VisualizationMap } from '../types'; + +export function createMockVisualization(id = 'testVis'): jest.Mocked { + return { + id, + clearLayer: jest.fn((state, _layerId) => state), + removeLayer: jest.fn(), + getLayerIds: jest.fn((_state) => ['layer1']), + getSupportedLayers: jest.fn(() => [{ type: layerTypes.DATA, label: 'Data Layer' }]), + getLayerType: jest.fn((_state, _layerId) => layerTypes.DATA), + visualizationTypes: [ + { + icon: 'empty', + id, + label: 'TEST', + groupLabel: `${id}Group`, + }, + ], + appendLayer: jest.fn(), + getVisualizationTypeId: jest.fn((_state) => 'empty'), + getDescription: jest.fn((_state) => ({ label: '' })), + switchVisualizationType: jest.fn((_, x) => x), + getSuggestions: jest.fn((_options) => []), + initialize: jest.fn((_frame, _state?) => ({ newState: 'newState' })), + getConfiguration: jest.fn((props) => ({ + groups: [ + { + groupId: 'a', + groupLabel: 'a', + layerId: 'layer1', + supportsMoreColumns: true, + accessors: [], + filterOperations: jest.fn(() => true), + dataTestSubj: 'mockVisA', + }, + ], + })), + toExpression: jest.fn((_state, _frame) => null), + toPreviewExpression: jest.fn((_state, _frame) => null), + + setDimension: jest.fn(), + removeDimension: jest.fn(), + getErrorMessages: jest.fn((_state) => undefined), + renderDimensionEditor: jest.fn(), + }; +} + +export const mockVisualizationMap = (): VisualizationMap => { + return { + testVis: createMockVisualization(), + testVis2: createMockVisualization(), + }; +}; + +export const visualizationMap = mockVisualizationMap(); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx index 33e9154235147..ad4e30cd6e89f 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx @@ -302,12 +302,13 @@ describe('PieVisualization component', () => { `); }); - test('does not set click listener on non-interactive mode', () => { + test('does not set click listener and legend actions on non-interactive mode', () => { const defaultArgs = getDefaultArgs(); const component = shallow( ); expect(component.find(Settings).first().prop('onElementClick')).toBeUndefined(); + expect(component.find(Settings).first().prop('legendAction')).toBeUndefined(); }); test('it renders the empty placeholder when metric contains only falsy data', () => { diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 834fecb95fc35..449b152523881 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -290,7 +290,7 @@ export function PieComponent( legendPosition={legendPosition || Position.Right} legendMaxDepth={nestedLegend ? undefined : 1 /* Color is based only on first layer */} onElementClick={props.interactive ?? true ? onElementClickHandler : undefined} - legendAction={getLegendAction(firstTable, onClickValue)} + legendAction={props.interactive ? getLegendAction(firstTable, onClickValue) : undefined} theme={{ ...chartTheme, background: { diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap index 0fad522624975..b058c42d8b4d1 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap @@ -5,7 +5,7 @@ exports[`xy_expression XYChart component it renders area 1`] = ` renderer="canvas" > { expect(wrapper.find(Settings).first().prop('onBrushEnd')).toBeUndefined(); }); - test('allowBrushingLastHistogramBucket is true for date histogram data', () => { + test('allowBrushingLastHistogramBin is true for date histogram data', () => { const { args } = sampleArgs(); const wrapper = mountWithIntl( @@ -1182,7 +1182,7 @@ describe('xy_expression', () => { }} /> ); - expect(wrapper.find(Settings).at(0).prop('allowBrushingLastHistogramBucket')).toEqual(true); + expect(wrapper.find(Settings).at(0).prop('allowBrushingLastHistogramBin')).toEqual(true); }); test('onElementClick returns correct context data', () => { @@ -1445,7 +1445,7 @@ describe('xy_expression', () => { }); }); - test('allowBrushingLastHistogramBucket should be fakse for ordinal data', () => { + test('allowBrushingLastHistogramBin should be fakse for ordinal data', () => { const { args, data } = sampleArgs(); const wrapper = mountWithIntl( @@ -1472,7 +1472,7 @@ describe('xy_expression', () => { /> ); - expect(wrapper.find(Settings).at(0).prop('allowBrushingLastHistogramBucket')).toEqual(false); + expect(wrapper.find(Settings).at(0).prop('allowBrushingLastHistogramBin')).toEqual(false); }); test('onElementClick is not triggering event on non-interactive mode', () => { @@ -1485,6 +1485,16 @@ describe('xy_expression', () => { expect(wrapper.find(Settings).first().prop('onElementClick')).toBeUndefined(); }); + test('legendAction is not triggering event on non-interactive mode', () => { + const { args, data } = sampleArgs(); + + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find(Settings).first().prop('legendAction')).toBeUndefined(); + }); + test('it renders stacked bar', () => { const { data, args } = sampleArgs(); const component = shallow( diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 36f1b92b8a1f4..32ca4c982c10e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -594,18 +594,22 @@ export function XYChart({ boundary: document.getElementById('app-fixed-viewport') ?? undefined, headerFormatter: (d) => safeXAccessorLabelRenderer(d.value), }} - allowBrushingLastHistogramBucket={Boolean(isTimeViz)} + allowBrushingLastHistogramBin={Boolean(isTimeViz)} rotation={shouldRotate ? 90 : 0} xDomain={xDomain} onBrushEnd={interactive ? (brushHandler as BrushEndListener) : undefined} onElementClick={interactive ? clickHandler : undefined} - legendAction={getLegendAction( - filteredLayers, - data.tables, - onClickValue, - formatFactory, - layersAlreadyFormatted - )} + legendAction={ + interactive + ? getLegendAction( + filteredLayers, + data.tables, + onClickValue, + formatFactory, + layersAlreadyFormatted + ) + : undefined + } showLegendExtra={isHistogramViz && valuesInLegend} /> diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index 01fbbd892a118..973501816bc3e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -538,6 +538,214 @@ describe('xy_visualization', () => { expect(ops.filter(filterOperations).map((x) => x.dataType)).toEqual(['number']); }); + describe('breakdown group: percentage chart checks', () => { + const baseState = exampleState(); + + it('should require break down group with one accessor + one split accessor configuration', () => { + const [, , splitGroup] = xyVisualization.getConfiguration({ + state: { + ...baseState, + layers: [ + { ...baseState.layers[0], accessors: ['a'], seriesType: 'bar_percentage_stacked' }, + ], + }, + frame, + layerId: 'first', + }).groups; + expect(splitGroup.required).toBe(true); + }); + + test.each([ + [ + 'multiple accessors on the same layer', + [ + { + ...baseState.layers[0], + splitAccessor: undefined, + seriesType: 'bar_percentage_stacked', + }, + ], + ], + [ + 'multiple accessors spread on compatible layers', + [ + { + ...baseState.layers[0], + accessors: ['a'], + splitAccessor: undefined, + seriesType: 'bar_percentage_stacked', + }, + { + ...baseState.layers[0], + splitAccessor: undefined, + xAccessor: 'd', + accessors: ['e'], + seriesType: 'bar_percentage_stacked', + }, + ], + ], + ] as Array<[string, State['layers']]>)( + 'should not require break down group for %s', + (_, layers) => { + const [, , splitGroup] = xyVisualization.getConfiguration({ + state: { ...baseState, layers }, + frame, + layerId: 'first', + }).groups; + expect(splitGroup.required).toBe(false); + } + ); + + it.each([ + [ + 'one accessor only', + [ + { + ...baseState.layers[0], + accessors: ['a'], + seriesType: 'bar_percentage_stacked', + splitAccessor: undefined, + xAccessor: undefined, + }, + ], + ], + [ + 'one accessor only with split accessor', + [ + { + ...baseState.layers[0], + accessors: ['a'], + seriesType: 'bar_percentage_stacked', + xAccessor: undefined, + }, + ], + ], + [ + 'one accessor only with xAccessor', + [ + { + ...baseState.layers[0], + accessors: ['a'], + seriesType: 'bar_percentage_stacked', + splitAccessor: undefined, + }, + ], + ], + [ + 'multiple accessors spread on incompatible layers (different xAccessor)', + [ + { + ...baseState.layers[0], + accessors: ['a'], + seriesType: 'bar_percentage_stacked', + splitAccessor: undefined, + }, + { + ...baseState.layers[0], + accessors: ['e'], + seriesType: 'bar_percentage_stacked', + splitAccessor: undefined, + xAccessor: undefined, + }, + ], + ], + [ + 'multiple accessors spread on incompatible layers (different splitAccessor)', + [ + { + ...baseState.layers[0], + accessors: ['a'], + seriesType: 'bar_percentage_stacked', + }, + { + ...baseState.layers[0], + accessors: ['e'], + seriesType: 'bar_percentage_stacked', + splitAccessor: undefined, + xAccessor: undefined, + }, + ], + ], + [ + 'multiple accessors spread on incompatible layers (different seriesType)', + [ + { + ...baseState.layers[0], + accessors: ['a'], + seriesType: 'bar_percentage_stacked', + }, + { + ...baseState.layers[0], + accessors: ['e'], + seriesType: 'bar', + }, + ], + ], + [ + 'one data layer with one accessor + one reference layer', + [ + { + ...baseState.layers[0], + accessors: ['a'], + seriesType: 'bar_percentage_stacked', + }, + { + ...baseState.layers[0], + accessors: ['e'], + seriesType: 'bar_percentage_stacked', + layerType: layerTypes.REFERENCELINE, + }, + ], + ], + + [ + 'multiple accessors on the same layers with different axis assigned', + [ + { + ...baseState.layers[0], + splitAccessor: undefined, + seriesType: 'bar_percentage_stacked', + yConfig: [ + { forAccessor: 'a', axisMode: 'left' }, + { forAccessor: 'b', axisMode: 'right' }, + ], + }, + ], + ], + [ + 'multiple accessors spread on multiple layers with different axis assigned', + [ + { + ...baseState.layers[0], + accessors: ['a'], + xAccessor: undefined, + splitAccessor: undefined, + seriesType: 'bar_percentage_stacked', + yConfig: [{ forAccessor: 'a', axisMode: 'left' }], + }, + { + ...baseState.layers[0], + accessors: ['b'], + xAccessor: undefined, + splitAccessor: undefined, + seriesType: 'bar_percentage_stacked', + yConfig: [{ forAccessor: 'b', axisMode: 'right' }], + }, + ], + ], + ] as Array<[string, State['layers']]>)( + 'should require break down group for %s', + (_, layers) => { + const [, , splitGroup] = xyVisualization.getConfiguration({ + state: { ...baseState, layers }, + frame, + layerId: 'first', + }).groups; + expect(splitGroup.required).toBe(true); + } + ); + }); + describe('reference lines', () => { beforeEach(() => { frame.datasourceLayers = { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index db1a2aeffb670..c23eccb196744 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -40,6 +40,7 @@ import { checkXAccessorCompatibility, getAxisName, } from './visualization_helpers'; +import { groupAxesByType } from './axes_configuration'; const defaultIcon = LensIconChartBarStacked; const defaultSeriesType = 'bar_stacked'; @@ -378,6 +379,40 @@ export const getXyVisualization = ({ }; } + const { left, right } = groupAxesByType([layer], frame.activeData); + // Check locally if it has one accessor OR one accessor per axis + const layerHasOnlyOneAccessor = Boolean( + layer.accessors.length < 2 || + (left.length && left.length < 2) || + (right.length && right.length < 2) + ); + // Check also for multiple layers that can stack for percentage charts + // Make sure that if multiple dimensions are defined for a single layer, they should belong to the same axis + const hasOnlyOneAccessor = + layerHasOnlyOneAccessor && + getLayersByType(state, layerTypes.DATA).filter( + // check that the other layers are compatible with this one + (dataLayer) => { + if ( + dataLayer.seriesType === layer.seriesType && + Boolean(dataLayer.xAccessor) === Boolean(layer.xAccessor) && + Boolean(dataLayer.splitAccessor) === Boolean(layer.splitAccessor) + ) { + const { left: localLeft, right: localRight } = groupAxesByType( + [dataLayer], + frame.activeData + ); + // return true only if matching axis are found + return ( + dataLayer.accessors.length && + (Boolean(localLeft.length) === Boolean(left.length) || + Boolean(localRight.length) === Boolean(right.length)) + ); + } + return false; + } + ).length < 2; + return { groups: [ { @@ -417,7 +452,7 @@ export const getXyVisualization = ({ filterOperations: isBucketed, supportsMoreColumns: !layer.splitAccessor, dataTestSubj: 'lnsXY_splitDimensionPanel', - required: layer.seriesType.includes('percentage'), + required: layer.seriesType.includes('percentage') && hasOnlyOneAccessor, enableDimensionEditor: true, }, ], diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index 48b0a416b5f0f..b912e8c52e680 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -32,7 +32,7 @@ import { getEventHandlers, ResultMeta, } from '../reducers/non_serializable_instances'; -import { cleanTooltipStateForLayer } from './tooltip_actions'; +import { updateTooltipStateForLayer } from './tooltip_actions'; import { LAYER_DATA_LOAD_ENDED, LAYER_DATA_LOAD_ERROR, @@ -61,7 +61,7 @@ export type DataRequestContext = { ): void; onLoadError(dataId: string, requestToken: symbol, errorMessage: string): void; onJoinError(errorMessage: string): void; - updateSourceData(newData: unknown): void; + updateSourceData(newData: object): void; isRequestStillActive(dataId: string, requestToken: symbol): boolean; registerCancelCallback(requestToken: symbol, callback: () => void): void; dataFilters: DataFilters; @@ -280,27 +280,30 @@ function endDataLoad( throw new DataRequestAbortError(); } - const features = data && 'features' in data ? (data as FeatureCollection).features : []; + if (dataId === SOURCE_DATA_REQUEST_ID) { + const features = data && 'features' in data ? (data as FeatureCollection).features : []; + + const eventHandlers = getEventHandlers(getState()); + if (eventHandlers && eventHandlers.onDataLoadEnd) { + const layer = getLayerById(layerId, getState()); + const resultMeta: ResultMeta = {}; + if (layer && layer.getType() === LAYER_TYPE.VECTOR) { + const featuresWithoutCentroids = features.filter((feature) => { + return feature.properties ? !feature.properties[KBN_IS_CENTROID_FEATURE] : true; + }); + resultMeta.featuresCount = featuresWithoutCentroids.length; + } - const eventHandlers = getEventHandlers(getState()); - if (eventHandlers && eventHandlers.onDataLoadEnd) { - const layer = getLayerById(layerId, getState()); - const resultMeta: ResultMeta = {}; - if (layer && layer.getType() === LAYER_TYPE.VECTOR) { - const featuresWithoutCentroids = features.filter((feature) => { - return feature.properties ? !feature.properties[KBN_IS_CENTROID_FEATURE] : true; + eventHandlers.onDataLoadEnd({ + layerId, + dataId, + resultMeta, }); - resultMeta.featuresCount = featuresWithoutCentroids.length; } - eventHandlers.onDataLoadEnd({ - layerId, - dataId, - resultMeta, - }); + dispatch(updateTooltipStateForLayer(layerId, features)); } - dispatch(cleanTooltipStateForLayer(layerId, features)); dispatch({ type: LAYER_DATA_LOAD_ENDED, layerId, @@ -331,16 +334,19 @@ function onDataLoadError( ) => { dispatch(unregisterCancelCallback(requestToken)); - const eventHandlers = getEventHandlers(getState()); - if (eventHandlers && eventHandlers.onDataLoadError) { - eventHandlers.onDataLoadError({ - layerId, - dataId, - errorMessage, - }); + if (dataId === SOURCE_DATA_REQUEST_ID) { + const eventHandlers = getEventHandlers(getState()); + if (eventHandlers && eventHandlers.onDataLoadError) { + eventHandlers.onDataLoadError({ + layerId, + dataId, + errorMessage, + }); + } + + dispatch(updateTooltipStateForLayer(layerId)); } - dispatch(cleanTooltipStateForLayer(layerId)); dispatch({ type: LAYER_DATA_LOAD_ERROR, layerId, @@ -361,6 +367,10 @@ export function updateSourceDataRequest(layerId: string, newData: object) { newData, }); + if ('features' in newData) { + dispatch(updateTooltipStateForLayer(layerId, (newData as FeatureCollection).features)); + } + dispatch(updateStyleMeta(layerId)); }; } diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index d67aef645b03a..9e937d86515e2 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -41,7 +41,7 @@ import { UPDATE_SOURCE_PROP, } from './map_action_constants'; import { clearDataRequests, syncDataForLayerId, updateStyleMeta } from './data_request_actions'; -import { cleanTooltipStateForLayer } from './tooltip_actions'; +import { updateTooltipStateForLayer } from './tooltip_actions'; import { Attribution, JoinDescriptor, @@ -217,7 +217,7 @@ export function setLayerVisibility(layerId: string, makeVisible: boolean) { } if (!makeVisible) { - dispatch(cleanTooltipStateForLayer(layerId)); + dispatch(updateTooltipStateForLayer(layerId)); } dispatch({ @@ -504,7 +504,7 @@ function removeLayerFromLayerList(layerId: string) { layerGettingRemoved.getInFlightRequestTokens().forEach((requestToken) => { dispatch(cancelRequest(requestToken)); }); - dispatch(cleanTooltipStateForLayer(layerId)); + dispatch(updateTooltipStateForLayer(layerId)); layerGettingRemoved.destroy(); dispatch({ type: REMOVE_LAYER, diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index ba52203ce486b..cf1e22ab90f88 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -60,7 +60,7 @@ import { addLayer, addLayerWithoutDataSync } from './layer_actions'; import { MapSettings } from '../reducers/map'; import { DrawState, MapCenterAndZoom, MapExtent, Timeslice } from '../../common/descriptor_types'; import { INITIAL_LOCATION } from '../../common/constants'; -import { cleanTooltipStateForLayer } from './tooltip_actions'; +import { updateTooltipStateForLayer } from './tooltip_actions'; import { VectorLayer } from '../classes/layers/vector_layer'; import { SET_DRAW_MODE } from './ui_actions'; import { expandToTileBoundaries } from '../../common/geo_tile_utils'; @@ -171,7 +171,7 @@ export function mapExtentChanged(mapExtentState: MapExtentState) { if (prevZoom !== nextZoom) { getLayerList(getState()).map((layer) => { if (!layer.showAtZoomLevel(nextZoom)) { - dispatch(cleanTooltipStateForLayer(layer.getId())); + dispatch(updateTooltipStateForLayer(layer.getId())); } }); } diff --git a/x-pack/plugins/maps/public/actions/tooltip_actions.ts b/x-pack/plugins/maps/public/actions/tooltip_actions.ts index c1b5f8190a73a..67b6842caeb46 100644 --- a/x-pack/plugins/maps/public/actions/tooltip_actions.ts +++ b/x-pack/plugins/maps/public/actions/tooltip_actions.ts @@ -10,8 +10,8 @@ import { Dispatch } from 'redux'; import { Feature } from 'geojson'; import { getOpenTooltips } from '../selectors/map_selectors'; import { SET_OPEN_TOOLTIPS } from './map_action_constants'; -import { FEATURE_ID_PROPERTY_NAME } from '../../common/constants'; -import { TooltipState } from '../../common/descriptor_types'; +import { FEATURE_ID_PROPERTY_NAME, FEATURE_VISIBLE_PROPERTY_NAME } from '../../common/constants'; +import { TooltipFeature, TooltipState } from '../../common/descriptor_types'; import { MapStoreState } from '../reducers/store'; export function closeOnClickTooltip(tooltipId: string) { @@ -62,26 +62,36 @@ export function openOnHoverTooltip(tooltipState: TooltipState) { }; } -export function cleanTooltipStateForLayer(layerId: string, layerFeatures: Feature[] = []) { +export function updateTooltipStateForLayer(layerId: string, layerFeatures: Feature[] = []) { return (dispatch: Dispatch, getState: () => MapStoreState) => { - let featuresRemoved = false; const openTooltips = getOpenTooltips(getState()) .map((tooltipState) => { - const nextFeatures = tooltipState.features.filter((tooltipFeature) => { + const nextFeatures: TooltipFeature[] = []; + tooltipState.features.forEach((tooltipFeature) => { if (tooltipFeature.layerId !== layerId) { // feature from another layer, keep it - return true; + nextFeatures.push(tooltipFeature); } - // Keep feature if it is still in layer - return layerFeatures.some((layerFeature) => { - return layerFeature.properties![FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id; + const updatedFeature = layerFeatures.find((layerFeature) => { + const isVisible = + layerFeature.properties![FEATURE_VISIBLE_PROPERTY_NAME] !== undefined + ? layerFeature.properties![FEATURE_VISIBLE_PROPERTY_NAME] + : true; + return ( + isVisible && layerFeature.properties![FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id + ); }); - }); - if (tooltipState.features.length !== nextFeatures.length) { - featuresRemoved = true; - } + if (updatedFeature) { + nextFeatures.push({ + ...tooltipFeature, + mbProperties: { + ...updatedFeature.properties, + }, + }); + } + }); return { ...tooltipState, features: nextFeatures }; }) @@ -89,11 +99,9 @@ export function cleanTooltipStateForLayer(layerId: string, layerFeatures: Featur return tooltipState.features.length > 0; }); - if (featuresRemoved) { - dispatch({ - type: SET_OPEN_TOOLTIPS, - openTooltips, - }); - } + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips, + }); }; } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_properties.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_properties.tsx index 4d9de61ffa819..570c06ff4ae7f 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_properties.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_properties.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import _ from 'lodash'; import React, { Component, CSSProperties, RefObject, ReactNode } from 'react'; import { EuiCallOut, @@ -57,6 +58,7 @@ export class FeatureProperties extends Component { private _isMounted = false; private _prevLayerId: string = ''; private _prevFeatureId?: string | number = ''; + private _prevMbProperties?: GeoJsonProperties; private readonly _tableRef: RefObject = React.createRef(); state: State = { @@ -118,13 +120,18 @@ export class FeatureProperties extends Component { nextFeatureId?: string | number; mbProperties: GeoJsonProperties; }) => { - if (this._prevLayerId === nextLayerId && this._prevFeatureId === nextFeatureId) { + if ( + this._prevLayerId === nextLayerId && + this._prevFeatureId === nextFeatureId && + _.isEqual(this._prevMbProperties, mbProperties) + ) { // do not reload same feature properties return; } this._prevLayerId = nextLayerId; this._prevFeatureId = nextFeatureId; + this._prevMbProperties = mbProperties; this.setState({ properties: null, loadPropertiesErrorMsg: null, diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/features_tooltip.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/features_tooltip.tsx index c0f792f626989..0d2ba07a5c956 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/features_tooltip.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/features_tooltip.tsx @@ -62,8 +62,20 @@ export class FeaturesTooltip extends Component { static getDerivedStateFromProps(nextProps: Props, prevState: State) { if (nextProps.features !== prevState.prevFeatures) { + let nextCurrentFeature = nextProps.features ? nextProps.features[0] : null; + if (prevState.currentFeature) { + const updatedCurrentFeature = nextProps.features.find((tooltipFeature) => { + return ( + tooltipFeature.id === prevState.currentFeature!.id && + tooltipFeature.layerId === prevState.currentFeature!.layerId + ); + }); + if (updatedCurrentFeature) { + nextCurrentFeature = updatedCurrentFeature; + } + } return { - currentFeature: nextProps.features ? nextProps.features[0] : null, + currentFeature: nextCurrentFeature, view: PROPERTIES_VIEW, prevFeatures: nextProps.features, }; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.tsx index 0b7ba3468d30c..181952a142ede 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.tsx @@ -44,15 +44,12 @@ interface Props { interface State { x?: number; y?: number; - isVisible: boolean; } export class TooltipPopover extends Component { private readonly _popoverRef: RefObject = React.createRef(); - state: State = { - isVisible: true, - }; + state: State = {}; componentDidMount() { this._updatePopoverPosition(); @@ -74,15 +71,19 @@ export class TooltipPopover extends Component { const lat = this.props.location[LAT_INDEX]; const lon = this.props.location[LON_INDEX]; const bounds = this.props.mbMap.getBounds(); - this.setState({ - x: nextPoint.x, - y: nextPoint.y, - isVisible: - lat < bounds.getNorth() && - lat > bounds.getSouth() && - lon > bounds.getWest() && - lon < bounds.getEast(), - }); + const isVisible = + lat < bounds.getNorth() && + lat > bounds.getSouth() && + lon > bounds.getWest() && + lon < bounds.getEast(); + if (!isVisible) { + this.props.closeTooltip(); + } else { + this.setState({ + x: nextPoint.x, + y: nextPoint.y, + }); + } }; _loadFeatureProperties = async ({ @@ -104,8 +105,15 @@ export class TooltipPopover extends Component { targetFeature = tooltipLayer.getFeatureById(featureId); } - const properties = targetFeature ? targetFeature.properties : mbProperties; - return await tooltipLayer.getPropertiesForTooltip(properties ? properties : {}); + let properties: GeoJsonProperties | undefined; + if (mbProperties) { + properties = mbProperties; + } else if (targetFeature?.properties) { + properties = targetFeature?.properties; + } else { + properties = {}; + } + return await tooltipLayer.getPropertiesForTooltip(properties); }; _getLayerName = async (layerId: string) => { @@ -143,7 +151,7 @@ export class TooltipPopover extends Component { }; render() { - if (!this.state.isVisible || this.state.x === undefined || this.state.y === undefined) { + if (this.state.x === undefined || this.state.y === undefined) { return null; } diff --git a/x-pack/plugins/maps/server/tutorials/ems/index.ts b/x-pack/plugins/maps/server/tutorials/ems/index.ts index ba8720a7bc8eb..47cb5476c4b90 100644 --- a/x-pack/plugins/maps/server/tutorials/ems/index.ts +++ b/x-pack/plugins/maps/server/tutorials/ems/index.ts @@ -65,7 +65,7 @@ export function emsBoundariesSpecProvider({ }), category: TutorialsCategory.OTHER, shortDescription: i18n.translate('xpack.maps.tutorials.ems.shortDescription', { - defaultMessage: 'Administrative boundaries from the Elastic Maps Service.', + defaultMessage: 'Add administrative boundaries to your data with Elastic Maps Service.', }), longDescription: i18n.translate('xpack.maps.tutorials.ems.longDescription', { defaultMessage: diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx index ab06179efbb04..8d7b9add409da 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx @@ -13,7 +13,7 @@ import { PageTemplate } from '../page_template'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { GlobalStateContext } from '../../contexts/global_state_context'; // @ts-ignore -import { CcrShardReact } from '../../../components/elasticsearch/ccr_shard'; +import { CcrShard } from '../../../components/elasticsearch/ccr_shard'; import { ComponentProps } from '../../route_init'; import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; @@ -113,7 +113,7 @@ export const ElasticsearchCcrShardPage: React.FC = ({ clusters } render={({ flyoutComponent, bottomBarComponent }: SetupModeProps) => ( {flyoutComponent} - + {bottomBarComponent} )} diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx index edc10cda5509c..b0e91ac20fb30 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx @@ -11,7 +11,7 @@ import { find } from 'lodash'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { GlobalStateContext } from '../../contexts/global_state_context'; // @ts-ignore -import { IndexReact } from '../../../components/elasticsearch/index/index_react'; +import { Index } from '../../../components/elasticsearch/index/index'; import { ComponentProps } from '../../route_init'; import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; @@ -118,7 +118,7 @@ export const ElasticsearchIndexPage: React.FC = ({ clusters }) = render={({ setupMode, flyoutComponent, bottomBarComponent }: SetupModeProps) => ( {flyoutComponent} - = ({ clusters }) => render={({ setupMode, flyoutComponent, bottomBarComponent }: SetupModeProps) => ( {flyoutComponent} - = ({ clusters } const shardActivityData = shardActivity && filterShardActivityData(shardActivity); // no filter on data = null return ( - = ({ clusters }) => { const globalState = useContext(GlobalStateContext); @@ -42,7 +43,9 @@ export const LogStashNodeAdvancedPage: React.FC = ({ clusters }) const ccs = globalState.ccs; const cluster = find(clusters, { cluster_uuid: clusterUuid, - }); + }) as any; + + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const [data, setData] = useState({} as any); const [alerts, setAlerts] = useState({}); @@ -108,6 +111,16 @@ export const LogStashNodeAdvancedPage: React.FC = ({ clusters }) ]; }, [data.metrics]); + useEffect(() => { + if (cluster && data.nodeSummary) { + generateBreadcrumbs(cluster.cluster_name, { + inLogstash: true, + instance: data.nodeSummary.host, + name: 'nodes', + }); + } + }, [cluster, data, generateBreadcrumbs]); + return ( = ({ clusters }) => { const match = useRouteMatch<{ uuid: string | undefined }>(); const globalState = useContext(GlobalStateContext); const { services } = useKibana<{ data: any }>(); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const clusterUuid = globalState.cluster_uuid; const ccs = globalState.ccs; const cluster = find(clusters, { cluster_uuid: clusterUuid, - }); + }) as any; const [data, setData] = useState({} as any); const [alerts, setAlerts] = useState({}); const { zoomInfo, onBrush } = useCharts(); @@ -60,6 +62,16 @@ export const LogStashNodePage: React.FC = ({ clusters }) => { }, }); + useEffect(() => { + if (cluster && data.nodeSummary) { + generateBreadcrumbs(cluster.cluster_name, { + inLogstash: true, + instance: data.nodeSummary.host, + name: 'nodes', + }); + } + }, [cluster, data, generateBreadcrumbs]); + const getPageData = useCallback(async () => { const url = `../api/monitoring/v1/clusters/${clusterUuid}/logstash/node/${match.params.uuid}`; const bounds = services.data?.query.timefilter.timefilter.getBounds(); diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx index 740202da57d24..0d89adeaeadd7 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useContext, useState, useCallback } from 'react'; +import React, { useContext, useState, useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; // @ts-ignore @@ -25,6 +25,7 @@ import { useTable } from '../../hooks/use_table'; // @ts-ignore import { PipelineListing } from '../../../components/logstash/pipeline_listing/pipeline_listing'; import { useCharts } from '../../hooks/use_charts'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; export const LogStashNodePipelinesPage: React.FC = ({ clusters }) => { const globalState = useContext(GlobalStateContext); @@ -35,7 +36,9 @@ export const LogStashNodePipelinesPage: React.FC = ({ clusters } const { onBrush, zoomInfo } = useCharts(); const cluster = find(clusters, { cluster_uuid: clusterUuid, - }); + }) as any; + + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const { getPaginationTableProps, getPaginationRouteOptions, updateTotalItemCount } = useTable('logstash.pipelines'); @@ -83,6 +86,16 @@ export const LogStashNodePipelinesPage: React.FC = ({ clusters } match.params.uuid, ]); + useEffect(() => { + if (cluster && data.nodeSummary) { + generateBreadcrumbs(cluster.cluster_name, { + inLogstash: true, + instance: data.nodeSummary.host, + name: 'nodes', + }); + } + }, [cluster, data, generateBreadcrumbs]); + return ( = ({ clusters }) => { const globalState = useContext(GlobalStateContext); const { services } = useKibana<{ data: any }>(); const clusterUuid = globalState.cluster_uuid; const ccs = globalState.ccs; + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const cluster = find(clusters, { cluster_uuid: clusterUuid, - }); + }) as any; const [data, setData] = useState({} as any); const [alerts, setAlerts] = useState({}); const { getPaginationTableProps } = useTable('logstash.nodes'); @@ -69,6 +71,14 @@ export const LogStashNodesPage: React.FC = ({ clusters }) => { } }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inLogstash: true, + }); + } + }, [cluster, generateBreadcrumbs]); + return ( = ({ clusters }) => { const globalState = useContext(GlobalStateContext); @@ -23,7 +24,8 @@ export const LogStashOverviewPage: React.FC = ({ clusters }) => const ccs = globalState.ccs; const cluster = find(clusters, { cluster_uuid: clusterUuid, - }); + }) as any; + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const [data, setData] = useState(null); // const [showShardActivityHistory, setShowShardActivityHistory] = useState(false); @@ -53,6 +55,14 @@ export const LogStashOverviewPage: React.FC = ({ clusters }) => setData(response); }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inLogstash: true, + }); + } + }, [cluster, data, generateBreadcrumbs]); + const renderOverview = (overviewData: any) => { if (overviewData === null) { return null; diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx index 20f1caee2b1d8..1f56ea22839e2 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx @@ -29,6 +29,7 @@ import { formatTimestampToDuration } from '../../../../common'; import { CALCULATE_DURATION_SINCE } from '../../../../common/constants'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { PipelineVersions } from './pipeline_versions_dropdown'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; export const LogStashPipelinePage: React.FC = ({ clusters }) => { const match = useRouteMatch<{ id: string | undefined; hash: string | undefined }>(); @@ -43,7 +44,7 @@ export const LogStashPipelinePage: React.FC = ({ clusters }) => const ccs = globalState.ccs; const cluster = find(clusters, { cluster_uuid: clusterUuid, - }); + }) as any; const [data, setData] = useState({} as any); const [detailVertexId, setDetailVertexId] = useState(null); const { updateTotalItemCount } = useTable('logstash.pipelines'); @@ -125,6 +126,7 @@ export const LogStashPipelinePage: React.FC = ({ clusters }) => }, [data]); const timeseriesTooltipXValueFormatter = (xValue: any) => moment(xValue).format(dateFormat); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const onVertexChange = useCallback( (vertex: any) => { @@ -145,6 +147,15 @@ export const LogStashPipelinePage: React.FC = ({ clusters }) => ); }, [pipelineId, pipelineHash]); + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inLogstash: true, + page: 'pipeline', + }); + } + }, [cluster, data, generateBreadcrumbs]); + return ( = ({ clusters }) => { const globalState = useContext(GlobalStateContext); @@ -29,7 +30,7 @@ export const LogStashPipelinesPage: React.FC = ({ clusters }) => const cluster = find(clusters, { cluster_uuid: clusterUuid, - }); + }) as any; const [data, setData] = useState(null); const { getPaginationTableProps, getPaginationRouteOptions, updateTotalItemCount } = useTable('logstash.pipelines'); @@ -42,6 +43,8 @@ export const LogStashPipelinesPage: React.FC = ({ clusters }) => defaultMessage: 'Logstash pipelines', }); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); + const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/logstash/pipelines`; @@ -69,6 +72,14 @@ export const LogStashPipelinesPage: React.FC = ({ clusters }) => updateTotalItemCount, ]); + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inLogstash: true, + }); + } + }, [cluster, data, generateBreadcrumbs]); + const renderOverview = (pageData: any) => { if (pageData === null) { return null; diff --git a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx index c0030cfcfe55c..a508714612c28 100644 --- a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx @@ -49,6 +49,7 @@ export const PageTemplate: React.FC = ({ const { currentTimerange } = useContext(MonitoringTimeContainer.Context); const [loaded, setLoaded] = useState(false); + const [isRequestPending, setIsRequestPending] = useState(false); const history = useHistory(); const [hasError, setHasError] = useState(false); const handleRequestError = useRequestErrorHandler(); @@ -62,6 +63,7 @@ export const PageTemplate: React.FC = ({ ); useEffect(() => { + setIsRequestPending(true); getPageData?.() .then(getPageDataResponseHandler) .catch((err: IHttpFetchError) => { @@ -70,11 +72,20 @@ export const PageTemplate: React.FC = ({ }) .finally(() => { setLoaded(true); + setIsRequestPending(false); }); }, [getPageData, currentTimerange, getPageDataResponseHandler, handleRequestError]); const onRefresh = () => { - getPageData?.().then(getPageDataResponseHandler).catch(handleRequestError); + // don't refresh when a request is pending + if (isRequestPending) return; + setIsRequestPending(true); + getPageData?.() + .then(getPageDataResponseHandler) + .catch(handleRequestError) + .finally(() => { + setIsRequestPending(false); + }); if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { updateSetupModeData(); diff --git a/x-pack/plugins/monitoring/public/components/chart/get_chart_options.js b/x-pack/plugins/monitoring/public/components/chart/get_chart_options.js index 641125dd3e943..001589a0cddb1 100644 --- a/x-pack/plugins/monitoring/public/components/chart/get_chart_options.js +++ b/x-pack/plugins/monitoring/public/components/chart/get_chart_options.js @@ -10,17 +10,8 @@ import { merge } from 'lodash'; import { CHART_LINE_COLOR, CHART_TEXT_COLOR } from '../../../common/constants'; export async function getChartOptions(axisOptions) { - let timezone; - try { - const $injector = Legacy.shims.getAngularInjector(); - timezone = $injector.get('config').get('dateFormat:tz'); - } catch (error) { - if (error.message === 'Angular has been removed.') { - timezone = Legacy.shims.uiSettings?.get('dateFormat:tz'); - } else { - throw error; - } - } + const timezone = Legacy.shims.uiSettings?.get('dateFormat:tz'); + const opts = { legend: { show: false, diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap index 72ff704916e28..faaf0762dbc7a 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap @@ -146,7 +146,7 @@ exports[`CcrShard that it renders normally 1`] = ` size="s" >

- September 27, 2018 1:32:09 PM + September 27, 2018 9:32:09 AM

diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard_react.test.js.snap b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard_react.test.js.snap deleted file mode 100644 index 9302c86a222b1..0000000000000 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard_react.test.js.snap +++ /dev/null @@ -1,185 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CcrShardReact that is renders an exception properly 1`] = ` - - - -`; - -exports[`CcrShardReact that it renders normally 1`] = ` - - - - - - - - - - - - - - - - - - - - - - -

- -

- - } - id="ccrLatestStat" - initialIsOpen={false} - isLoading={false} - isLoadingMessage={false} - paddingSize="l" - > - -

- September 27, 2018 9:32:09 AM -

-
- - - { - "read_exceptions": [], - "follower_global_checkpoint": 3049, - "follower_index": "follower", - "follower_max_seq_no": 3049, - "last_requested_seq_no": 3049, - "leader_global_checkpoint": 3049, - "leader_index": "leader", - "leader_max_seq_no": 3049, - "mapping_version": 2, - "number_of_concurrent_reads": 1, - "number_of_concurrent_writes": 0, - "number_of_failed_bulk_operations": 0, - "failed_read_requests": 0, - "operations_written": 3050, - "number_of_queued_writes": 0, - "number_of_successful_bulk_operations": 3050, - "number_of_successful_fetches": 3050, - "operations_received": 3050, - "shard_id": 0, - "time_since_last_read_millis": 9402, - "total_fetch_time_millis": 44128980, - "total_index_time_millis": 41827, - "total_transferred_bytes": 234156 -} - -
-
-
-`; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js index ef16c119d8613..9765d83e31f41 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js @@ -5,8 +5,8 @@ * 2.0. */ -import React, { Fragment, PureComponent } from 'react'; -import { Legacy } from '../../../legacy_shims'; +import React, { Fragment } from 'react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { EuiPage, EuiPageBody, @@ -28,9 +28,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { AlertsCallout } from '../../../alerts/callout'; -export class CcrShard extends PureComponent { - renderCharts() { - const { metrics } = this.props; +export function CcrShard(props) { + const { services } = useKibana(); + const timezone = services.uiSettings?.get('dateFormat:tz'); + const { metrics, stat, timestamp, oldestStat, formattedLeader, alerts } = props; + const renderCharts = () => { const seriesToShow = [metrics.ccr_sync_lag_ops, metrics.ccr_sync_lag_time]; const charts = seriesToShow.map((data, index) => ( @@ -42,10 +44,9 @@ export class CcrShard extends PureComponent { )); return {charts}; - } + }; - renderErrors() { - const { stat } = this.props; + const renderErrors = () => { if (stat.read_exceptions && stat.read_exceptions.length > 0) { return ( @@ -91,13 +92,9 @@ export class CcrShard extends PureComponent { ); } return null; - } - - renderLatestStat() { - const { stat, timestamp } = this.props; - const injector = Legacy.shims.getAngularInjector(); - const timezone = injector.get('config').get('dateFormat:tz'); + }; + const renderLatestStat = () => { return ( ); - } - - render() { - const { stat, oldestStat, formattedLeader, alerts } = this.props; + }; - return ( - - - - - - - - - {this.renderErrors()} - {this.renderCharts()} - - {this.renderLatestStat()} - - - ); - } + return ( + + + + + + + + + {renderErrors()} + {renderCharts()} + + {renderLatestStat()} + + + ); } diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js index 90d9efecce40a..6b7b43016baf9 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js @@ -9,14 +9,6 @@ import React from 'react'; import { shallow } from 'enzyme'; import { CcrShard } from './ccr_shard'; -jest.mock('../../../legacy_shims', () => { - return { - Legacy: { - shims: { getAngularInjector: () => ({ get: () => ({ get: () => 'utc' }) }) }, - }, - }; -}); - jest.mock('../../chart', () => ({ MonitoringTimeseriesContainer: () => 'MonitoringTimeseriesContainer', })); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard_react.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard_react.js deleted file mode 100644 index 65586d602c85e..0000000000000 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard_react.js +++ /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 React, { Fragment } from 'react'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { - EuiPage, - EuiPageBody, - EuiPanel, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiTitle, - EuiBasicTable, - EuiCodeBlock, - EuiTextColor, - EuiHorizontalRule, - EuiAccordion, -} from '@elastic/eui'; -import { MonitoringTimeseriesContainer } from '../../chart'; -import { Status } from './status'; -import { formatDateTimeLocal } from '../../../../common/formatting'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { AlertsCallout } from '../../../alerts/callout'; - -export function CcrShardReact(props) { - const { services } = useKibana(); - const timezone = services.uiSettings?.get('dateFormat:tz'); - const { metrics, stat, timestamp, oldestStat, formattedLeader, alerts } = props; - const renderCharts = () => { - const seriesToShow = [metrics.ccr_sync_lag_ops, metrics.ccr_sync_lag_time]; - - const charts = seriesToShow.map((data, index) => ( - - - - - - )); - - return {charts}; - }; - - const renderErrors = () => { - if (stat.read_exceptions && stat.read_exceptions.length > 0) { - return ( - - - -

- - - -

-
- - -
- -
- ); - } - return null; - }; - - const renderLatestStat = () => { - return ( - -

- -

- - } - paddingSize="l" - > - - -

{formatDateTimeLocal(timestamp, timezone)}

-
- - {JSON.stringify(stat, null, 2)} -
-
- ); - }; - - return ( - - - - - - - - - {renderErrors()} - {renderCharts()} - - {renderLatestStat()} - - - ); -} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard_react.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard_react.test.js deleted file mode 100644 index afd289a33457a..0000000000000 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard_react.test.js +++ /dev/null @@ -1,82 +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 { shallow } from 'enzyme'; -import { CcrShardReact } from './ccr_shard_react'; - -jest.mock('../../../legacy_shims', () => { - return { - Legacy: { - shims: { getAngularInjector: () => ({ get: () => ({ get: () => 'utc' }) }) }, - }, - }; -}); - -jest.mock('../../chart', () => ({ - MonitoringTimeseriesContainer: () => 'MonitoringTimeseriesContainer', -})); - -describe('CcrShardReact', () => { - const props = { - formattedLeader: 'leader on remote', - metrics: [], - stat: { - read_exceptions: [], - follower_global_checkpoint: 3049, - follower_index: 'follower', - follower_max_seq_no: 3049, - last_requested_seq_no: 3049, - leader_global_checkpoint: 3049, - leader_index: 'leader', - leader_max_seq_no: 3049, - mapping_version: 2, - number_of_concurrent_reads: 1, - number_of_concurrent_writes: 0, - number_of_failed_bulk_operations: 0, - failed_read_requests: 0, - operations_written: 3050, - number_of_queued_writes: 0, - number_of_successful_bulk_operations: 3050, - number_of_successful_fetches: 3050, - operations_received: 3050, - shard_id: 0, - time_since_last_read_millis: 9402, - total_fetch_time_millis: 44128980, - total_index_time_millis: 41827, - total_transferred_bytes: 234156, - }, - oldestStat: { - failed_read_requests: 0, - operations_written: 2976, - }, - timestamp: '2018-09-27T13:32:09.412Z', - }; - - test('that it renders normally', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - test('that is renders an exception properly', () => { - const localProps = { - ...props, - stat: { - ...props.stat, - read_exceptions: [ - { - type: 'something_is_wrong', - reason: 'not sure but something happened', - }, - ], - }, - }; - - const component = shallow(); - expect(component.find('EuiPanel').get(0)).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/index.js index 036a21e9b8a72..4cfd362b8ab0c 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/index.js @@ -6,4 +6,3 @@ */ export { CcrShard } from './ccr_shard'; -export { CcrShardReact } from './ccr_shard_react'; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/index.ts index 657617c698696..2cb688689438c 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/index.ts +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/index.ts @@ -6,8 +6,7 @@ */ export { ElasticsearchOverview } from './overview'; -export { ElasticsearchOverviewReact } from './overview'; export { ElasticsearchNodes } from './nodes'; -export { NodeReact } from './node'; +export { Node } from './node'; export { ElasticsearchIndices } from './indices'; export { ElasticsearchMLJobs } from './ml_jobs'; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index/index.js index 294fc15ce4c47..9bdaa513998b5 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/index/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/index/index.js @@ -22,7 +22,6 @@ import { Logs } from '../../logs'; import { AlertsCallout } from '../../../alerts/callout'; export const Index = ({ - scope, indexSummary, metrics, clusterUuid, @@ -63,7 +62,7 @@ export const Index = ({ - + diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index/index_react.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index/index_react.js deleted file mode 100644 index 70bac52a0926c..0000000000000 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/index/index_react.js +++ /dev/null @@ -1,70 +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 { - EuiPage, - EuiPageContent, - EuiPageBody, - EuiPanel, - EuiSpacer, - EuiFlexGrid, - EuiFlexItem, -} from '@elastic/eui'; -import { IndexDetailStatus } from '../index_detail_status'; -import { MonitoringTimeseriesContainer } from '../../chart'; -import { ShardAllocationReact } from '../shard_allocation/shard_allocation_react'; -import { Logs } from '../../logs'; -import { AlertsCallout } from '../../../alerts/callout'; - -export const IndexReact = ({ - indexSummary, - metrics, - clusterUuid, - indexUuid, - logs, - alerts, - ...props -}) => { - const metricsToShow = [ - metrics.index_mem, - metrics.index_size, - metrics.index_search_request_rate, - metrics.index_request_rate, - metrics.index_segment_count, - metrics.index_document_count, - ]; - - return ( - - - - - - - - - - - {metricsToShow.map((metric, index) => ( - - - - - ))} - - - - - - - - - - - ); -}; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/index.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/node/index.ts index 3b7153c5940d9..074749ae06e83 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/index.ts +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/index.ts @@ -7,4 +7,3 @@ export { NodeStatusIcon } from './status_icon'; export { Node } from './node'; -export { NodeReact } from './node_react'; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.d.ts index 9d7a062e942bb..17f05d98ee042 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.d.ts +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.d.ts @@ -15,6 +15,5 @@ export interface NodeProps { alerts: unknown; nodeId: unknown; clusterUuid: unknown; - scope: unknown; [key: string]: any; } diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js index 3570f75df7334..0b03f1077f9cb 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js @@ -14,23 +14,16 @@ import { EuiFlexGrid, EuiFlexItem, EuiPanel, + EuiScreenReaderOnly, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { NodeDetailStatus } from '../node_detail_status'; -import { Logs } from '../../logs/'; +import { Logs } from '../../logs'; import { MonitoringTimeseriesContainer } from '../../chart'; -import { ShardAllocation } from '../shard_allocation/shard_allocation'; import { AlertsCallout } from '../../../alerts/callout'; +import { ShardAllocation } from '../shard_allocation'; -export const Node = ({ - nodeSummary, - metrics, - logs, - alerts, - nodeId, - clusterUuid, - scope, - ...props -}) => { +export const Node = ({ nodeSummary, metrics, logs, alerts, nodeId, clusterUuid, ...props }) => { /* // This isn't doing anything due to a possible bug. https://github.com/elastic/kibana/issues/106309 if (alerts) { @@ -61,6 +54,14 @@ export const Node = ({ return ( + +

+ +

+
@@ -82,7 +83,7 @@ export const Node = ({ - +
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node_react.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node_react.d.ts deleted file mode 100644 index e0c4f6b301fdb..0000000000000 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node_react.d.ts +++ /dev/null @@ -1,19 +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 { FunctionComponent } from 'react'; - -export const NodeReact: FunctionComponent; -export interface NodeReactProps { - nodeSummary: unknown; - metrics: unknown; - logs: unknown; - alerts: unknown; - nodeId: unknown; - clusterUuid: unknown; - [key: string]: any; -} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node_react.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node_react.js deleted file mode 100644 index 38b03d1aa748f..0000000000000 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node_react.js +++ /dev/null @@ -1,99 +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 { - EuiPage, - EuiPageContent, - EuiPageBody, - EuiSpacer, - EuiFlexGrid, - EuiFlexItem, - EuiPanel, - EuiScreenReaderOnly, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { NodeDetailStatus } from '../node_detail_status'; -import { Logs } from '../../logs'; -import { MonitoringTimeseriesContainer } from '../../chart'; -import { AlertsCallout } from '../../../alerts/callout'; -import { ShardAllocationReact } from '../shard_allocation'; - -export const NodeReact = ({ - nodeSummary, - metrics, - logs, - alerts, - nodeId, - clusterUuid, - ...props -}) => { - /* - // This isn't doing anything due to a possible bug. https://github.com/elastic/kibana/issues/106309 - if (alerts) { - for (const alertTypeId of Object.keys(alerts)) { - const alertInstance = alerts[alertTypeId]; - for (const { meta } of alertInstance.states) { - const metricList = get(meta, 'metrics', []); - for (const metric of metricList) { - if (metrics[metric]) { - metrics[metric].alerts = metrics[metric].alerts || {}; - metrics[metric].alerts[alertTypeId] = alertInstance; - } - } - } - } - } - */ - const metricsToShow = [ - metrics.node_jvm_mem, - metrics.node_mem, - metrics.node_total_io, - metrics.node_cpu_metric, - metrics.node_load_average, - metrics.node_latency, - metrics.node_segment_count, - ]; - - return ( - - - -

- -

-
- - - - - - - - {metricsToShow.map((metric, index) => ( - - - - - ))} - - - - - - - - - - -
-
- ); -}; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/overview/index.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/overview/index.ts index dd7e63c14fc53..b56c381395ef7 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/overview/index.ts +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/overview/index.ts @@ -5,6 +5,5 @@ * 2.0. */ -export { ElasticsearchOverview } from './overview'; // @ts-ignore -export { ElasticsearchOverviewReact } from './overview_react'; +export { ElasticsearchOverview } from './overview'; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview.d.ts deleted file mode 100644 index d4c893f87cbd2..0000000000000 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview.d.ts +++ /dev/null @@ -1,18 +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 { FunctionComponent } from 'react'; - -export const ElasticsearchOverview: FunctionComponent; -export interface ElasticsearchOverviewProps { - clusterStatus: unknown; - metrics: unknown; - logs: unknown; - cluster: unknown; - shardActivity: unknown; - [key: string]: any; -} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview_react.js b/x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview_react.js deleted file mode 100644 index ff4e531e31744..0000000000000 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview_react.js +++ /dev/null @@ -1,66 +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 { ClusterStatus } from '../cluster_status'; -import { ShardActivityReact } from '../shard_activity'; -import { MonitoringTimeseriesContainer } from '../../chart'; -import { - EuiPage, - EuiFlexGrid, - EuiFlexItem, - EuiPanel, - EuiSpacer, - EuiPageBody, - EuiPageContent, -} from '@elastic/eui'; -import { Logs } from '../../logs/logs'; - -export function ElasticsearchOverviewReact({ - clusterStatus, - metrics, - logs, - cluster, - shardActivity, - ...props -}) { - const metricsToShow = [ - metrics.cluster_search_request_rate, - metrics.cluster_query_latency, - metrics.cluster_index_request_rate, - metrics.cluster_index_latency, - ]; - - return ( - - - - - - - - - {metricsToShow.map((metric, index) => ( - - - - - ))} - - - - - - - - - - - - - ); -} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/index.js index 8c0b8b4c9c82d..bcdbbe715f86e 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/index.js @@ -6,4 +6,3 @@ */ export { ShardActivity } from './shard_activity'; -export { ShardActivityReact } from './shard_activity_react'; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js index 1f0ed47adf387..9a102c52aa1f1 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js @@ -5,7 +5,6 @@ * 2.0. */ -import { Legacy } from '../../../legacy_shims'; import { capitalize } from 'lodash'; import { formatMetric } from '../../../lib/format_number'; import { formatDateTimeLocal } from '../../../../common/formatting'; @@ -42,21 +41,12 @@ export const parseProps = (props) => { const { files, size } = index; - let thisTimezone; - // react version passes timezone while Angular uses injector - if (!timezone) { - const injector = Legacy.shims.getAngularInjector(); - thisTimezone = injector.get('config').get('dateFormat:tz'); - } else { - thisTimezone = timezone; - } - return { name: indexName || index.name, shard: `${id} / ${isPrimary ? 'Primary' : 'Replica'}`, relocationType: type === 'PRIMARY_RELOCATION' ? 'Primary Relocation' : normalizeString(type), stage: normalizeString(stage), - startTime: formatDateTimeLocal(startTimeInMillis, thisTimezone), + startTime: formatDateTimeLocal(startTimeInMillis, timezone), totalTime: formatMetric(Math.floor(totalTimeInMillis / 1000), '00:00:00'), isCopiedFromPrimary: !isPrimary || type === 'PRIMARY_RELOCATION', sourceName: source.name === undefined ? 'n/a' : source.name, diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js index 7b939f0fee8e6..e55cb793574a9 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js @@ -15,6 +15,7 @@ import { FilesProgress, BytesProgress, TranslogProgress } from './progress'; import { parseProps } from './parse_props'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; const columns = [ { @@ -67,14 +68,19 @@ const columns = [ }, ]; -export class ShardActivity extends React.Component { - constructor(props) { - super(props); - this.getNoDataMessage = this.getNoDataMessage.bind(this); - } - - getNoDataMessage() { - if (this.props.showShardActivityHistory) { +export const ShardActivity = (props) => { + const { + data: rawData, + sorting, + pagination, + onTableChange, + toggleShardActivityHistory, + showShardActivityHistory, + } = props; + const { services } = useKibana(); + const timezone = services.uiSettings?.get('dateFormat:tz'); + const getNoDataMessage = () => { + if (showShardActivityHistory) { return i18n.translate('xpack.monitoring.elasticsearch.shardActivity.noDataMessage', { defaultMessage: 'There are no historical shard activity records for the selected time range.', @@ -92,7 +98,7 @@ export class ShardActivity extends React.Component { defaultMessage="Try viewing {shardActivityHistoryLink}." values={{ shardActivityHistoryLink: ( - +
); - } - - render() { - // data prop is an array of table row data, or null (which triggers no data message) - const { - data: rawData, - sorting, - pagination, - onTableChange, - toggleShardActivityHistory, - showShardActivityHistory, - } = this.props; - - if (rawData === null) { - return null; - } + }; - const rows = rawData.map(parseProps); + const rows = rawData.map((data) => parseProps({ ...data, timezone })); - return ( - - - -

- -

-
-
- - + + +

- } - onChange={toggleShardActivityHistory} - checked={showShardActivityHistory} - /> - - - - ); - } -} +

+
+
+ + + } + onChange={toggleShardActivityHistory} + checked={showShardActivityHistory} + /> + + +
+ ); +}; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity_react.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity_react.js deleted file mode 100644 index cc219ff0fff32..0000000000000 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity_react.js +++ /dev/null @@ -1,156 +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, { Fragment } from 'react'; -import { EuiText, EuiTitle, EuiLink, EuiSpacer, EuiSwitch } from '@elastic/eui'; -import { EuiMonitoringTable } from '../../table'; -import { RecoveryIndex } from './recovery_index'; -import { TotalTime } from './total_time'; -import { SourceDestination } from './source_destination'; -import { FilesProgress, BytesProgress, TranslogProgress } from './progress'; -import { parseProps } from './parse_props'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; - -const columns = [ - { - name: i18n.translate('xpack.monitoring.kibana.shardActivity.indexTitle', { - defaultMessage: 'Index', - }), - field: 'name', - render: (_name, shard) => , - }, - { - name: i18n.translate('xpack.monitoring.kibana.shardActivity.stageTitle', { - defaultMessage: 'Stage', - }), - field: 'stage', - }, - { - name: i18n.translate('xpack.monitoring.kibana.shardActivity.totalTimeTitle', { - defaultMessage: 'Total Time', - }), - field: null, - render: (shard) => , - }, - { - name: i18n.translate('xpack.monitoring.kibana.shardActivity.sourceDestinationTitle', { - defaultMessage: 'Source / Destination', - }), - field: null, - render: (shard) => , - }, - { - name: i18n.translate('xpack.monitoring.kibana.shardActivity.filesTitle', { - defaultMessage: 'Files', - }), - field: null, - render: (shard) => , - }, - { - name: i18n.translate('xpack.monitoring.kibana.shardActivity.bytesTitle', { - defaultMessage: 'Bytes', - }), - field: null, - render: (shard) => , - }, - { - name: i18n.translate('xpack.monitoring.kibana.shardActivity.translogTitle', { - defaultMessage: 'Translog', - }), - field: null, - render: (shard) => , - }, -]; - -export const ShardActivityReact = (props) => { - const { - data: rawData, - sorting, - pagination, - onTableChange, - toggleShardActivityHistory, - showShardActivityHistory, - } = props; - const { services } = useKibana(); - const timezone = services.uiSettings?.get('dateFormat:tz'); - const getNoDataMessage = () => { - if (showShardActivityHistory) { - return i18n.translate('xpack.monitoring.elasticsearch.shardActivity.noDataMessage', { - defaultMessage: - 'There are no historical shard activity records for the selected time range.', - }); - } - return ( - - -
- - - - ), - }} - /> -
- ); - }; - - const rows = rawData.map((data) => parseProps({ ...data, timezone })); - - return ( - - - -

- -

-
-
- - - } - onChange={toggleShardActivityHistory} - checked={showShardActivityHistory} - /> - - -
- ); -}; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js index a637703a98cdc..a004c7fae8e95 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js @@ -8,67 +8,22 @@ import React from 'react'; import { TableHead } from './table_head'; import { TableBody } from './table_body'; -import { i18n } from '@kbn/i18n'; -export class ClusterView extends React.Component { - static displayName = i18n.translate( - 'xpack.monitoring.elasticsearch.shardAllocation.clusterViewDisplayName', - { - defaultMessage: 'ClusterView', - } +export const ClusterView = (props) => { + return ( + + + +
); - - constructor(props) { - super(props); - - this.state = { - labels: props.scope.labels || [], - showing: props.scope.showing || [], - shardStats: props.scope.pageData.shardStats, - showSystemIndices: props.showSystemIndices, - toggleShowSystemIndices: props.toggleShowSystemIndices, - }; - } - - setShowing = (data) => { - if (data) { - this.setState({ showing: data }); - } - }; - - setShardStats = (stats) => { - this.setState({ shardStats: stats }); - }; - - UNSAFE_componentWillMount() { - this.props.scope.$watch('showing', this.setShowing); - this.props.scope.$watch(() => this.props.scope.pageData.shardStats, this.setShardStats); - } - - hasUnassigned = () => { - return ( - this.state.showing.length && - this.state.showing[0].unassigned && - this.state.showing[0].unassigned.length - ); - }; - - render() { - return ( - - - -
- ); - } -} +}; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view_react.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view_react.js deleted file mode 100644 index 2d0c4b59df4b8..0000000000000 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view_react.js +++ /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. - */ - -import React from 'react'; -import { TableHeadReact } from './table_head_react'; -import { TableBody } from './table_body'; - -export const ClusterViewReact = (props) => { - return ( - - - -
- ); -}; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js index d4d4da050d37a..b5316bb624a80 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js @@ -14,18 +14,15 @@ class IndexLabel extends React.Component { constructor(props) { super(props); this.state = { - showSystemIndices: props.scope.showSystemIndices, + showSystemIndices: props.showSystemIndices, }; this.toggleShowSystemIndicesState = this.toggleShowSystemIndicesState.bind(this); } - // See also public/directives/index_listing/index toggleShowSystemIndicesState(e) { const isChecked = e.target.checked; this.setState({ showSystemIndices: isChecked }); - this.props.scope.$evalAsync(() => { - this.props.toggleShowSystemIndices(isChecked); - }); + this.props.toggleShowSystemIndices(isChecked); } render() { @@ -70,7 +67,7 @@ export class TableHead extends React.Component { } render() { - const propLabels = this.props.scope.labels || []; + const propLabels = this.props.labels || []; const labelColumns = propLabels .map((label) => { const column = { @@ -81,8 +78,8 @@ export class TableHead extends React.Component { // override text label content with a JSX component column.content = ( ); } else { diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head_react.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head_react.js deleted file mode 100644 index 5f914792ec70b..0000000000000 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head_react.js +++ /dev/null @@ -1,99 +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 { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -class IndexLabel extends React.Component { - constructor(props) { - super(props); - this.state = { - showSystemIndices: props.showSystemIndices, - }; - this.toggleShowSystemIndicesState = this.toggleShowSystemIndicesState.bind(this); - } - - toggleShowSystemIndicesState(e) { - const isChecked = e.target.checked; - this.setState({ showSystemIndices: isChecked }); - this.props.toggleShowSystemIndices(isChecked); - } - - render() { - return ( - - - - - - - - - ); - } -} - -// eslint-disable-next-line react/no-multi-comp -export class TableHeadReact extends React.Component { - constructor(props) { - super(props); - } - - createColumn({ key, content }) { - return ( - - {content} - - ); - } - - render() { - const propLabels = this.props.labels || []; - const labelColumns = propLabels - .map((label) => { - const column = { - key: label.content.toLowerCase(), - }; - - if (label.showToggleSystemIndicesComponent) { - // override text label content with a JSX component - column.content = ( - - ); - } else { - column.content = label.content; - } - - return column; - }) - .map(this.createColumn); - - return ( - - {labelColumns} - - ); - } -} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/index.js index dd4121b69574c..247bad7527846 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/index.js @@ -6,4 +6,3 @@ */ export { ShardAllocation } from './shard_allocation'; -export { ShardAllocationReact } from './shard_allocation_react'; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js index f02b93eba8f90..7ca24853a9ccb 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js @@ -9,10 +9,10 @@ import React from 'react'; import { EuiTitle, EuiBadge, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { ClusterView } from './components/cluster_view'; import './shard_allocation.scss'; +import { ClusterView } from './components/cluster_view'; -export const ShardAllocation = ({ scope, type, shardStats }) => { +export const ShardAllocation = (props) => { const types = [ { label: i18n.translate('xpack.monitoring.elasticsearch.shardAllocation.primaryLabel', { @@ -77,13 +77,7 @@ export const ShardAllocation = ({ scope, type, shardStats }) => { ))} - +
); }; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation_react.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation_react.js deleted file mode 100644 index 502d93d5411d2..0000000000000 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation_react.js +++ /dev/null @@ -1,83 +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 { EuiTitle, EuiBadge, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import './shard_allocation.scss'; -import { ClusterViewReact } from './components/cluster_view_react'; - -export const ShardAllocationReact = (props) => { - const types = [ - { - label: i18n.translate('xpack.monitoring.elasticsearch.shardAllocation.primaryLabel', { - defaultMessage: 'Primary', - }), - color: 'primary', - }, - { - label: i18n.translate('xpack.monitoring.elasticsearch.shardAllocation.replicaLabel', { - defaultMessage: 'Replica', - }), - color: 'secondary', - }, - { - label: i18n.translate('xpack.monitoring.elasticsearch.shardAllocation.relocatingLabel', { - defaultMessage: 'Relocating', - }), - color: 'accent', - }, - { - label: i18n.translate('xpack.monitoring.elasticsearch.shardAllocation.initializingLabel', { - defaultMessage: 'Initializing', - }), - color: 'default', - }, - { - label: i18n.translate( - 'xpack.monitoring.elasticsearch.shardAllocation.unassignedPrimaryLabel', - { - defaultMessage: 'Unassigned Primary', - } - ), - color: 'danger', - }, - { - label: i18n.translate( - 'xpack.monitoring.elasticsearch.shardAllocation.unassignedReplicaLabel', - { - defaultMessage: 'Unassigned Replica', - } - ), - color: 'warning', - }, - ]; - - return ( -
- -

- -

-
- - - {types.map((type) => ( - - {type.label} - - ))} - - - -
- ); -}; diff --git a/x-pack/plugins/monitoring/public/components/index.ts b/x-pack/plugins/monitoring/public/components/index.ts index 6f0b9bb88667f..b8e9adbf27fa3 100644 --- a/x-pack/plugins/monitoring/public/components/index.ts +++ b/x-pack/plugins/monitoring/public/components/index.ts @@ -12,4 +12,4 @@ export { NoData } from './no_data'; export { License } from './license'; export { PageLoading } from './page_loading'; -export { ElasticsearchOverview, ElasticsearchNodes, ElasticsearchIndices } from './elasticsearch'; +export { ElasticsearchNodes, ElasticsearchIndices } from './elasticsearch'; diff --git a/x-pack/plugins/monitoring/public/components/logs/logs.js b/x-pack/plugins/monitoring/public/components/logs/logs.js index 3021240a157d3..52c1c1373caf5 100644 --- a/x-pack/plugins/monitoring/public/components/logs/logs.js +++ b/x-pack/plugins/monitoring/public/components/logs/logs.js @@ -16,18 +16,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Reason } from './reason'; const getFormattedDateTimeLocal = (timestamp) => { - try { - const injector = Legacy.shims.getAngularInjector(); - const timezone = injector.get('config').get('dateFormat:tz'); - return formatDateTimeLocal(timestamp, timezone); - } catch (error) { - if (error.message === 'Angular has been removed.') { - const timezone = Legacy.shims.uiSettings?.get('dateFormat:tz'); - return formatDateTimeLocal(timestamp, timezone); - } else { - throw error; - } - } + const timezone = Legacy.shims.uiSettings?.get('dateFormat:tz'); + return formatDateTimeLocal(timestamp, timezone); }; const columnTimestampTitle = i18n.translate('xpack.monitoring.logs.listing.timestampTitle', { diff --git a/x-pack/plugins/monitoring/public/legacy_shims.ts b/x-pack/plugins/monitoring/public/legacy_shims.ts index 48484421839bd..7c7e7642cac81 100644 --- a/x-pack/plugins/monitoring/public/legacy_shims.ts +++ b/x-pack/plugins/monitoring/public/legacy_shims.ts @@ -37,14 +37,9 @@ export interface KFetchKibanaOptions { prependBasePath?: boolean; } -const angularNoop = () => { - throw new Error('Angular has been removed.'); -}; - export interface IShims { toastNotifications: CoreStart['notifications']['toasts']; capabilities: CoreStart['application']['capabilities']; - getAngularInjector: typeof angularNoop; getBasePath: () => string; getInjected: (name: string, defaultValue?: unknown) => unknown; breadcrumbs: { @@ -84,7 +79,6 @@ export class Legacy { this._shims = { toastNotifications: core.notifications.toasts, capabilities: core.application.capabilities, - getAngularInjector: angularNoop, getBasePath: (): string => core.http.basePath.get(), getInjected: (name: string, defaultValue?: unknown): string | unknown => core.injectedMetadata.getInjectedVar(name, defaultValue), diff --git a/x-pack/plugins/observability/public/pages/alerts/index.tsx b/x-pack/plugins/observability/public/pages/alerts/index.tsx index bba3b426598df..142ba11407c5a 100644 --- a/x-pack/plugins/observability/public/pages/alerts/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/index.tsx @@ -16,9 +16,12 @@ import type { AlertWorkflowStatus } from '../../../common/typings'; import { ExperimentalBadge } from '../../components/shared/experimental_badge'; import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; import { useFetcher } from '../../hooks/use_fetcher'; +import { useHasData } from '../../hooks/use_has_data'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { RouteParams } from '../../routes'; import { callObservabilityApi } from '../../services/call_observability_api'; +import { getNoDataConfig } from '../../utils/no_data_config'; +import { LoadingObservability } from '../overview/loading_observability'; import { AlertsSearchBar } from './alerts_search_bar'; import { AlertsTableTGrid } from './alerts_table_t_grid'; import './styles.scss'; @@ -141,8 +144,25 @@ export function AlertsPage({ routeParams }: AlertsPageProps) { refetch.current = ref; }, []); + const { hasAnyData, isAllRequestsComplete } = useHasData(); + + // If there is any data, set hasData to true otherwise we need to wait till all the data is loaded before setting hasData to true or false; undefined indicates the data is still loading. + const hasData = hasAnyData === true || (isAllRequestsComplete === false ? undefined : false); + + if (!hasAnyData && !isAllRequestsComplete) { + return ; + } + + const noDataConfig = getNoDataConfig({ + hasData, + basePath: core.http.basePath, + docsLink: core.docLinks.links.observability.guide, + }); + return ( diff --git a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx index 442104a710601..4ac7c4cfd92a5 100644 --- a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx @@ -16,16 +16,35 @@ import { usePluginContext } from '../../hooks/use_plugin_context'; import { useReadonlyHeader } from '../../hooks/use_readonly_header'; import { casesBreadcrumbs } from './links'; import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; +import { useHasData } from '../../hooks/use_has_data'; +import { LoadingObservability } from '../overview/loading_observability'; +import { getNoDataConfig } from '../../utils/no_data_config'; export const AllCasesPage = React.memo(() => { const userPermissions = useGetUserCasesPermissions(); - const { ObservabilityPageTemplate } = usePluginContext(); + const { core, ObservabilityPageTemplate } = usePluginContext(); useReadonlyHeader(); - useBreadcrumbs([casesBreadcrumbs.cases]); + const { hasAnyData, isAllRequestsComplete } = useHasData(); + + if (!hasAnyData && !isAllRequestsComplete) { + return ; + } + + // If there is any data, set hasData to true otherwise we need to wait till all the data is loaded before setting hasData to true or false; undefined indicates the data is still loading. + const hasData = hasAnyData === true || (isAllRequestsComplete === false ? undefined : false); + + const noDataConfig = getNoDataConfig({ + hasData, + basePath: core.http.basePath, + docsLink: core.docLinks.links.observability.guide, + }); + return userPermissions == null || userPermissions?.read ? ( {i18n.PAGE_TITLE}, }} diff --git a/x-pack/plugins/security/server/config_deprecations.ts b/x-pack/plugins/security/server/config_deprecations.ts index b867c50f589e0..de4a6daf2f80e 100644 --- a/x-pack/plugins/security/server/config_deprecations.ts +++ b/x-pack/plugins/security/server/config_deprecations.ts @@ -225,7 +225,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ } }, // Default values for session expiration timeouts. - (settings, fromPath, addDeprecation) => { + (settings, fromPath, addDeprecation, { branch }) => { if (settings?.xpack?.security?.session?.idleTimeout === undefined) { addDeprecation({ configPath: 'xpack.security.session.idleTimeout', @@ -237,6 +237,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ defaultMessage: 'User sessions will automatically time out after 8 hours of inactivity starting in 8.0. Override this value to change the timeout.', }), + documentationUrl: `https://www.elastic.co/guide/en/kibana/${branch}/xpack-security-session-management.html#session-idle-timeout`, correctiveActions: { manualSteps: [ i18n.translate('xpack.security.deprecations.idleTimeout.manualStepOneMessage', { @@ -261,6 +262,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ defaultMessage: 'Users are automatically required to log in again after 30 days starting in 8.0. Override this value to change the timeout.', }), + documentationUrl: `https://www.elastic.co/guide/en/kibana/${branch}/xpack-security-session-management.html#session-lifespan`, correctiveActions: { manualSteps: [ i18n.translate('xpack.security.deprecations.lifespan.manualStepOneMessage', { diff --git a/x-pack/plugins/security_solution/public/common/utils/saved_query_services/index.tsx b/x-pack/plugins/security_solution/public/common/utils/saved_query_services/index.tsx index b15a466af4d79..a5c4d81b1728c 100644 --- a/x-pack/plugins/security_solution/public/common/utils/saved_query_services/index.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/saved_query_services/index.tsx @@ -15,14 +15,14 @@ import { useKibana } from '../../lib/kibana'; export const useSavedQueryServices = () => { const kibana = useKibana(); - const client = kibana.services.savedObjects.client; + const { http } = kibana.services; const [savedQueryService, setSavedQueryService] = useState( - createSavedQueryService(client) + createSavedQueryService(http) ); useEffect(() => { - setSavedQueryService(createSavedQueryService(client)); - }, [client]); + setSavedQueryService(createSavedQueryService(http)); + }, [http]); return savedQueryService; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx index 8e0d8c544563a..6034ed875c02b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx @@ -171,10 +171,10 @@ const PolicyAdvanced = React.memo( - {configPath.join('.')} + + {configPath.join('.')} {documentation && ( - + )} diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 4885538269264..f016c9712c650 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -384,10 +384,8 @@ export class Plugin implements IPlugin { .testSubject('resolver:graph-controls:node-legend:description') .map((description) => description.text()) ) - ).toYieldEqualTo(['Running Process', 'Terminated Process', 'Loading Process', 'Error']); + ).toYieldEqualTo([ + 'Running Process', + 'Terminated Process', + 'Loading Process', + 'Error Process', + ]); }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx index 96a59383b1a4e..570f444814d7f 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx @@ -569,7 +569,7 @@ const NodeLegend = ({ > {i18n.translate('xpack.securitySolution.resolver.graphControls.errorCube', { - defaultMessage: 'Error', + defaultMessage: 'Error Process', })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx index b2b304e16c4a0..daafec3005eb8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx @@ -244,27 +244,19 @@ export const QueryBarTimeline = memo( (f) => f.meta.controlledBy === TIMELINE_FILTER_DROP_AREA ) : -1; - savedQueryServices.saveQuery( - { - ...newSavedQuery.attributes, - filters: - newSavedQuery.attributes.filters != null - ? dataProviderFilterExists > -1 - ? [ - ...newSavedQuery.attributes.filters.slice(0, dataProviderFilterExists), - getDataProviderFilter(dataProvidersDsl), - ...newSavedQuery.attributes.filters.slice(dataProviderFilterExists + 1), - ] - : [ - ...newSavedQuery.attributes.filters, - getDataProviderFilter(dataProvidersDsl), - ] - : [], - }, - { - overwrite: true, - } - ); + savedQueryServices.updateQuery(newSavedQuery.id, { + ...newSavedQuery.attributes, + filters: + newSavedQuery.attributes.filters != null + ? dataProviderFilterExists > -1 + ? [ + ...newSavedQuery.attributes.filters.slice(0, dataProviderFilterExists), + getDataProviderFilter(dataProvidersDsl), + ...newSavedQuery.attributes.filters.slice(dataProviderFilterExists + 1), + ] + : [...newSavedQuery.attributes.filters, getDataProviderFilter(dataProvidersDsl)] + : [], + }); } } else { setSavedQueryId(null); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.test.ts index 2f63a184875f1..f28d78e5c0304 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.test.ts @@ -77,17 +77,6 @@ describe('legacy_inject_rule_id_references', () => { expect(logger.error).not.toHaveBeenCalled(); }); - test('logs an error if found with a different saved object reference id', () => { - legacyInjectRuleIdReferences({ - logger, - ruleAlertId: '456', - savedObjectReferences: mockSavedObjectReferences(), - }); - expect(logger.error).toBeCalledWith( - 'The id of the "saved object reference id": 123 is not the same as the "saved object id": 456. Preferring and using the "saved object reference id" instead of the "saved object id"' - ); - }); - test('logs an error if the saved object references is empty', () => { legacyInjectRuleIdReferences({ logger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.ts index 5cb32c6563157..b6ad98eb864ed 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.ts @@ -32,19 +32,6 @@ export const legacyInjectRuleIdReferences = ({ return reference.name === 'alert_0'; }); if (referenceFound) { - if (referenceFound.id !== ruleAlertId) { - // This condition should not be reached but we log an error if we encounter it to help if we migrations - // did not run correctly or we create a regression in the future. - logger.error( - [ - 'The id of the "saved object reference id": ', - referenceFound.id, - ' is not the same as the "saved object id": ', - ruleAlertId, - '. Preferring and using the "saved object reference id" instead of the "saved object id"', - ].join('') - ); - } return referenceFound.id; } else { logger.error( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts index d4357c45fd373..799412a33ffbc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts @@ -24,6 +24,7 @@ import { filterExportedRulesCounts, filterExceptions, createLimitStream, + filterExportedCounts, } from '../../../utils/read_stream/create_stream_from_ndjson'; export const validateRules = (): Transform => { @@ -60,6 +61,7 @@ export const createRulesStreamFromNdJson = (ruleLimit: number) => { return [ createSplitStream('\n'), parseNdjsonStrings(), + filterExportedCounts(), filterExportedRulesCounts(), filterExceptions(), validateRules(), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts index f0ff1b6072479..1212b73a6250e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts @@ -105,17 +105,6 @@ describe('inject_exceptions_list', () => { ).toEqual([{ ...mockExceptionsList()[0], id: '456' }]); }); - test('logs an error if found with a different saved object reference id', () => { - injectExceptionsReferences({ - logger, - exceptionsList: mockExceptionsList(), - savedObjectReferences: [{ ...mockSavedObjectReferences()[0], id: '456' }], - }); - expect(logger.error).toBeCalledWith( - 'The id of the "saved object reference id": 456 is not the same as the "saved object id": 123. Preferring and using the "saved object reference id" instead of the "saved object id"' - ); - }); - test('returns exceptionItem if the saved object reference cannot match as a fall back', () => { expect( injectExceptionsReferences({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts index 2e6559fbf18cf..baaaa2eb60ce9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts @@ -7,11 +7,7 @@ import { Logger, SavedObjectReference } from 'src/core/server'; import { RuleParams } from '../../schemas/rule_schemas'; -import { - getSavedObjectReferenceForExceptionsList, - logMissingSavedObjectError, - logWarningIfDifferentReferencesDetected, -} from './utils'; +import { getSavedObjectReferenceForExceptionsList, logMissingSavedObjectError } from './utils'; /** * This injects any "exceptionsList" "id"'s from saved object reference and returns the "exceptionsList" using the saved object reference. If for @@ -44,11 +40,6 @@ export const injectExceptionsReferences = ({ savedObjectReferences, }); if (savedObjectReference != null) { - logWarningIfDifferentReferencesDetected({ - logger, - savedObjectReferenceId: savedObjectReference.id, - savedObjectId: exceptionItem.id, - }); const reference: RuleParams['exceptionsList'][0] = { ...exceptionItem, id: savedObjectReference.id, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts index ca88dae364a4b..3a3d559a6ed39 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts @@ -11,4 +11,3 @@ export * from './get_saved_object_name_pattern'; export * from './get_saved_object_reference_for_exceptions_list'; export * from './get_saved_object_reference'; export * from './log_missing_saved_object_error'; -export * from './log_warning_if_different_references_detected'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.test.ts deleted file mode 100644 index a27faa6356c2b..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { loggingSystemMock } from 'src/core/server/mocks'; - -import { logWarningIfDifferentReferencesDetected } from '.'; - -describe('log_warning_if_different_references_detected', () => { - let logger = loggingSystemMock.create().get('security_solution'); - - beforeEach(() => { - logger = loggingSystemMock.create().get('security_solution'); - }); - - test('logs expect error message if the two ids are different', () => { - logWarningIfDifferentReferencesDetected({ - logger, - savedObjectReferenceId: '123', - savedObjectId: '456', - }); - expect(logger.error).toBeCalledWith( - 'The id of the "saved object reference id": 123 is not the same as the "saved object id": 456. Preferring and using the "saved object reference id" instead of the "saved object id"' - ); - }); - - test('logs nothing if the two ids are the same', () => { - logWarningIfDifferentReferencesDetected({ - logger, - savedObjectReferenceId: '123', - savedObjectId: '123', - }); - expect(logger.error).not.toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.ts deleted file mode 100644 index 9f80ba6d8ce83..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.ts +++ /dev/null @@ -1,36 +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 { Logger } from 'src/core/server'; - -/** - * This will log a warning that the saved object reference id and the saved object id are not the same if that is true. - * @param logger The kibana injected logger - * @param savedObjectReferenceId The saved object reference id from "references: [{ id: ...}]" - * @param savedObjectId The saved object id from a structure such as exceptions { exceptionsList: { "id": "..." } } - */ -export const logWarningIfDifferentReferencesDetected = ({ - logger, - savedObjectReferenceId, - savedObjectId, -}: { - logger: Logger; - savedObjectReferenceId: string; - savedObjectId: string; -}): void => { - if (savedObjectReferenceId !== savedObjectId) { - logger.error( - [ - 'The id of the "saved object reference id": ', - savedObjectReferenceId, - ' is not the same as the "saved object id": ', - savedObjectId, - '. Preferring and using the "saved object reference id" instead of the "saved object id"', - ].join('') - ); - } -}; diff --git a/x-pack/plugins/stack_alerts/server/index.ts b/x-pack/plugins/stack_alerts/server/index.ts index 1ac774a2d6c3f..b6b117ceb7075 100644 --- a/x-pack/plugins/stack_alerts/server/index.ts +++ b/x-pack/plugins/stack_alerts/server/index.ts @@ -18,6 +18,7 @@ export const config: PluginConfigDescriptor = { const stackAlerts = get(settings, fromPath); if (stackAlerts?.enabled === false || stackAlerts?.enabled === true) { addDeprecation({ + level: 'critical', configPath: 'xpack.stack_alerts.enabled', message: `"xpack.stack_alerts.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, correctiveActions: { diff --git a/x-pack/plugins/stack_alerts/server/plugin.test.ts b/x-pack/plugins/stack_alerts/server/plugin.test.ts index b2bf076eaf49d..b9263553173d2 100644 --- a/x-pack/plugins/stack_alerts/server/plugin.test.ts +++ b/x-pack/plugins/stack_alerts/server/plugin.test.ts @@ -11,8 +11,7 @@ import { alertsMock } from '../../alerting/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; import { BUILT_IN_ALERTS_FEATURE } from './feature'; -// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 -describe.skip('AlertingBuiltins Plugin', () => { +describe('AlertingBuiltins Plugin', () => { describe('setup()', () => { let context: ReturnType; let plugin: AlertingBuiltinsPlugin; diff --git a/x-pack/plugins/task_manager/server/index.ts b/x-pack/plugins/task_manager/server/index.ts index 2a360fc1a1d90..e608f3cfc30ee 100644 --- a/x-pack/plugins/task_manager/server/index.ts +++ b/x-pack/plugins/task_manager/server/index.ts @@ -49,6 +49,7 @@ export const config: PluginConfigDescriptor = { const taskManager = get(settings, fromPath); if (taskManager?.index) { addDeprecation({ + level: 'critical', configPath: `${fromPath}.index`, documentationUrl: 'https://ela.st/kbn-remove-legacy-multitenancy', message: `"${fromPath}.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details`, @@ -62,6 +63,7 @@ export const config: PluginConfigDescriptor = { } if (taskManager?.max_workers > MAX_WORKERS_LIMIT) { addDeprecation({ + level: 'critical', configPath: `${fromPath}.max_workers`, message: `setting "${fromPath}.max_workers" (${taskManager?.max_workers}) greater than ${MAX_WORKERS_LIMIT} is deprecated. Values greater than ${MAX_WORKERS_LIMIT} will not be supported starting in 8.0.`, correctiveActions: { diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 9e509172ecdd5..932947c1b01a9 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -144,26 +144,26 @@ "throttle_time": { "properties": { "min": { - "type": "keyword" + "type": "long" }, "avg": { - "type": "keyword" + "type": "float" }, "max": { - "type": "keyword" + "type": "long" } } }, "schedule_time": { "properties": { "min": { - "type": "keyword" + "type": "long" }, "avg": { - "type": "keyword" + "type": "float" }, "max": { - "type": "keyword" + "type": "long" } } }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2d49b2ba8dea4..3bbcad3da0aad 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6415,7 +6415,6 @@ "xpack.apm.errorGroupDetails.occurrencesChartLabel": "オカレンス", "xpack.apm.errorGroupDetails.relatedTransactionSample": "関連トランザクションサンプル", "xpack.apm.errorGroupDetails.unhandledLabel": "未対応", - "xpack.apm.errorRate": "失敗したトランザクション率", "xpack.apm.errorRate.chart.errorRate": "失敗したトランザクション率(平均)", "xpack.apm.errorRate.chart.errorRate.previousPeriodLabel": "前の期間", "xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel": "エラーメッセージと原因", @@ -18197,7 +18196,6 @@ "xpack.monitoring.elasticsearch.shardActivity.totalTimeTooltip": "開始:{startTime}", "xpack.monitoring.elasticsearch.shardActivity.unknownTargetAddressContent": "不明", "xpack.monitoring.elasticsearch.shardActivityTitle": "シャードアクティビティ", - "xpack.monitoring.elasticsearch.shardAllocation.clusterViewDisplayName": "ClusterView", "xpack.monitoring.elasticsearch.shardAllocation.decorateShards.relocatingFromTextMessage": "{nodeName} から移動しています", "xpack.monitoring.elasticsearch.shardAllocation.decorateShards.relocatingToTextMessage": "{nodeName} に移動しています", "xpack.monitoring.elasticsearch.shardAllocation.initializingLabel": "初期化中", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c926e6c19090b..ded23eccefbbe 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6467,7 +6467,6 @@ "xpack.apm.errorGroupDetails.relatedTransactionSample": "相关的事务样本", "xpack.apm.errorGroupDetails.unhandledLabel": "未处理", "xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel": "在 Discover 中查看 {occurrencesCount} 次{occurrencesCount, plural, other {发生}}", - "xpack.apm.errorRate": "失败事务率", "xpack.apm.errorRate.chart.errorRate": "失败事务率(平均值)", "xpack.apm.errorRate.chart.errorRate.previousPeriodLabel": "上一时段", "xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel": "错误消息和原因", @@ -18473,7 +18472,6 @@ "xpack.monitoring.elasticsearch.shardActivity.totalTimeTooltip": "已启动:{startTime}", "xpack.monitoring.elasticsearch.shardActivity.unknownTargetAddressContent": "未知", "xpack.monitoring.elasticsearch.shardActivityTitle": "分片活动", - "xpack.monitoring.elasticsearch.shardAllocation.clusterViewDisplayName": "ClusterView", "xpack.monitoring.elasticsearch.shardAllocation.decorateShards.relocatingFromTextMessage": "正在从 {nodeName} 迁移", "xpack.monitoring.elasticsearch.shardAllocation.decorateShards.relocatingToTextMessage": "正在迁移至 {nodeName}", "xpack.monitoring.elasticsearch.shardAllocation.initializingLabel": "正在初始化", diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx index df4c73908b627..425f35b067026 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx @@ -8,15 +8,17 @@ import React, { useContext, useEffect, useState } from 'react'; import useIntersection from 'react-use/lib/useIntersection'; import styled from 'styled-components'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + import { isScreenshotImageBlob, isScreenshotRef, ScreenshotRefImageData, -} from '../../../../../../common/runtime_types/ping'; +} from '../../../../../../common/runtime_types'; import { useFetcher, FETCH_STATUS } from '../../../../../../../observability/public'; import { getJourneyScreenshot } from '../../../../../state/api/journey'; import { UptimeSettingsContext } from '../../../../../contexts'; + import { NoImageDisplay } from './no_image_display'; import { StepImageCaption } from './step_image_caption'; import { StepImagePopover } from './step_image_popover'; @@ -129,9 +131,12 @@ export const PingTimestamp = ({ label, checkGroup, initialStepNo = 1 }: Props) = )} - - {label} - + + {label && ( + + {label} + + )} ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx index a2858348ed59c..3fa94e45f8937 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx @@ -6,10 +6,13 @@ */ import React, { MouseEvent, useEffect } from 'react'; -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { nextAriaLabel, prevAriaLabel } from './translations'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui'; + import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { ScreenshotRefImageData } from '../../../../../../common/runtime_types'; +import { useBreakpoints } from '../../../../../hooks'; + +import { nextAriaLabel, prevAriaLabel } from './translations'; export interface StepImageCaptionProps { captionContent: string; @@ -23,13 +26,6 @@ export interface StepImageCaptionProps { isLoading: boolean; } -const ImageCaption = euiStyled.div` - background-color: ${(props) => props.theme.eui.euiColorLightestShade}; - display: inline-block; - width: 100%; - text-decoration: none; -`; - export const StepImageCaption: React.FC = ({ captionContent, imgRef, @@ -41,6 +37,9 @@ export const StepImageCaption: React.FC = ({ label, onVisible, }) => { + const { euiTheme } = useEuiTheme(); + const breakpoints = useBreakpoints(); + useEffect(() => { onVisible(true); return () => { @@ -49,8 +48,10 @@ export const StepImageCaption: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const isSmall = breakpoints.down('m'); + return ( - { // we don't want this to be captured by row click which leads to step list page evt.stopPropagation(); @@ -59,8 +60,9 @@ export const StepImageCaption: React.FC = ({
{(imgSrc || imgRef) && ( - + ) => { setStepNumber(stepNumber - 1); @@ -74,10 +76,11 @@ export const StepImageCaption: React.FC = ({ - {captionContent} + {captionContent} - + ) => { setStepNumber(stepNumber + 1); @@ -93,8 +96,21 @@ export const StepImageCaption: React.FC = ({ )} - {label} + + {label} +
-
+ ); }; + +const CaptionWrapper = euiStyled.div` + background-color: ${(props) => props.theme.eui.euiColorLightestShade}; + display: inline-block; + width: 100%; + text-decoration: none; +`; + +const SecondaryText = euiStyled(EuiText)((props) => ({ + color: props.theme.eui.euiTextColor, +})); diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx index 26ca69a5b89c7..7aa763c15ca1f 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx @@ -199,6 +199,22 @@ describe('useExpandedROw', () => { expect(Object.keys(result.current.expandedRows)).toEqual(['0']); }); + it('returns expected browser consoles', async () => { + const { result } = renderHook(() => + useExpandedRow({ + steps: defaultSteps, + allSteps: [...defaultSteps, browserConsoleStep], + loading: false, + }) + ); + + result.current.toggleExpand({ journeyStep: defaultSteps[0] }); + + expect(result.current.expandedRows[0].props.browserConsoles).toEqual([ + browserConsoleStep.synthetics.payload.text, + ]); + }); + describe('getExpandedStepCallback', () => { it('matches step index to key', () => { const callback = getExpandedStepCallback(2); @@ -207,3 +223,44 @@ describe('useExpandedROw', () => { }); }); }); + +const browserConsoleStep = { + _id: 'IvT1oXwB5ds00bB_FVXP', + observer: { + hostname: '16Elastic', + }, + agent: { + name: '16Elastic', + id: '77def92c-1a78-4353-b9e5-45d31920b1b7', + type: 'heartbeat', + ephemeral_id: '3a9ca86c-08d0-4f3f-b857-aeef540b3cac', + version: '8.0.0', + }, + '@timestamp': '2021-10-21T08:25:25.221Z', + package: { name: '@elastic/synthetics', version: '1.0.0-beta.14' }, + ecs: { version: '1.12.0' }, + os: { platform: 'darwin' }, + synthetics: { + package_version: '1.0.0-beta.14', + journey: { name: 'inline', id: 'inline' }, + payload: { + text: "Refused to execute inline script because it violates the following Content Security Policy directive: \"script-src 'unsafe-eval' 'self'\". Either the 'unsafe-inline' keyword, a hash ('sha256-P5polb1UreUSOe5V/Pv7tc+yeZuJXiOi/3fqhGsU7BE='), or a nonce ('nonce-...') is required to enable inline execution.\n", + type: 'error', + }, + index: 755, + step: { duration: { us: 0 }, name: 'goto kibana', index: 1, status: '' }, + type: 'journey/browserconsole', + isFullScreenshot: false, + isScreenshotRef: true, + }, + monitor: { + name: 'cnn-monitor - inline', + timespan: { lt: '2021-10-21T08:27:04.662Z', gte: '2021-10-21T08:26:04.662Z' }, + check_group: '70acec60-3248-11ec-9921-acde48001122', + id: 'cnn-monitor-inline', + type: 'browser', + status: 'up', + }, + event: { dataset: 'browser' }, + timestamp: '2021-10-21T08:25:25.221Z', +}; diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx index e58e1cca8660b..1b3a641033dd7 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx @@ -28,13 +28,15 @@ export const useExpandedRow = ({ loading, steps, allSteps }: HookProps) => { const { checkGroupId } = useParams<{ checkGroupId: string }>(); - const getBrowserConsole = useCallback( + const getBrowserConsoles = useCallback( (index: number) => { - return allSteps.find( - (stepF) => - stepF.synthetics?.type === 'journey/browserconsole' && - stepF.synthetics?.step?.index! === index - )?.synthetics?.payload?.text; + return allSteps + .filter( + (stepF) => + stepF.synthetics?.type === 'journey/browserconsole' && + stepF.synthetics?.step?.index! === index + ) + .map((stepF) => stepF.synthetics?.payload?.text!); }, [allSteps] ); @@ -48,7 +50,7 @@ export const useExpandedRow = ({ loading, steps, allSteps }: HookProps) => { expandedRowsN[expandedRowKey] = ( @@ -77,7 +79,7 @@ export const useExpandedRow = ({ loading, steps, allSteps }: HookProps) => { [stepIndex]: ( diff --git a/x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx index 04fcf382fd861..f9876593a03db 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx @@ -71,14 +71,15 @@ describe('ExecutedStep', () => { }); it('renders accordions for console output', () => { - const browserConsole = - "Refused to execute script from because its MIME type ('image/gif') is not executable"; + const browserConsole = [ + "Refused to execute script from because its MIME type ('image/gif') is not executable", + ]; const { getByText } = render( - + ); expect(getByText('Console output')); - expect(getByText(browserConsole)); + expect(getByText(browserConsole[0])); }); }); diff --git a/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx b/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx index add34c3f71f0d..57b94544e5983 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx @@ -19,7 +19,7 @@ interface ExecutedStepProps { step: JourneyStep; index: number; loading: boolean; - browserConsole?: string; + browserConsoles?: string[]; } const Label = euiStyled.div` @@ -40,12 +40,7 @@ const ExpandedRow = euiStyled.div` width: 100%; `; -export const ExecutedStep: FC = ({ - loading, - step, - index, - browserConsole = '', -}) => { +export const ExecutedStep: FC = ({ loading, step, index, browserConsoles }) => { const isSucceeded = step.synthetics?.payload?.status === 'succeeded'; return ( @@ -94,7 +89,12 @@ export const ExecutedStep: FC = ({ initialIsOpen={!isSucceeded} > <> - {browserConsole} + {browserConsoles?.map((browserConsole) => ( + <> + {browserConsole} + + + ))} diff --git a/x-pack/plugins/uptime/public/hooks/index.ts b/x-pack/plugins/uptime/public/hooks/index.ts index 3e4714384e654..e96d746a05514 100644 --- a/x-pack/plugins/uptime/public/hooks/index.ts +++ b/x-pack/plugins/uptime/public/hooks/index.ts @@ -12,4 +12,5 @@ export * from './use_search_text'; export * from './use_cert_status'; export * from './use_telemetry'; export * from './use_url_params'; +export * from './use_breakpoints'; export { useIndexPattern } from '../contexts/uptime_index_pattern_context'; diff --git a/x-pack/plugins/uptime/public/hooks/use_breakpoints.test.ts b/x-pack/plugins/uptime/public/hooks/use_breakpoints.test.ts new file mode 100644 index 0000000000000..d417d98dcb76d --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_breakpoints.test.ts @@ -0,0 +1,96 @@ +/* + * 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 { BREAKPOINTS } from '@elastic/eui'; +import { renderHook } from '@testing-library/react-hooks'; +import { useBreakpoints } from './use_breakpoints'; + +describe('use_breakpoints', () => { + describe('useBreakpoints', () => { + const width = global.innerWidth; + + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + }); + + afterAll(() => { + (global as { innerWidth: number }).innerWidth = width; + }); + + it('should only return up => false and down => true for "xs" when width is less than BREAKPOINTS.xs', () => { + (global as { innerWidth: number }).innerWidth = BREAKPOINTS.xs - 1; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.up('xs')).toBeFalsy(); + expect(result.current.down('xs')).toBeTruthy(); + }); + + it('should only return up => true and down => false for "xs" when width is above or equal BREAKPOINTS.xs', () => { + (global as { innerWidth: number }).innerWidth = BREAKPOINTS.xs; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.up('xs')).toBeTruthy(); + expect(result.current.down('xs')).toBeFalsy(); + }); + + it('should return down => true for "m" when width equals BREAKPOINTS.l', () => { + (global as { innerWidth: number }).innerWidth = BREAKPOINTS.l; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.up('m')).toBeTruthy(); + expect(result.current.down('m')).toBeFalsy(); + }); + + it('should return `between` => true for "m" and "xl" when width equals BREAKPOINTS.l', () => { + (global as { innerWidth: number }).innerWidth = BREAKPOINTS.l; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.between('m', 'xl')).toBeTruthy(); + }); + + it('should return `between` => true for "s" and "m" when width equals BREAKPOINTS.s', () => { + (global as { innerWidth: number }).innerWidth = BREAKPOINTS.s; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.between('s', 'm')).toBeTruthy(); + }); + + it('should return up => true for all when size is > xxxl+', () => { + (global as { innerWidth: number }).innerWidth = 3000; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.up('xs')).toBeTruthy(); + expect(result.current.up('s')).toBeTruthy(); + expect(result.current.up('m')).toBeTruthy(); + expect(result.current.up('l')).toBeTruthy(); + expect(result.current.up('xl')).toBeTruthy(); + expect(result.current.up('xxl')).toBeTruthy(); + expect(result.current.up('xxxl')).toBeTruthy(); + }); + + it('should determine `isIpad (Portrait)', () => { + (global as { innerWidth: number }).innerWidth = 768; + const { result } = renderHook(() => useBreakpoints()); + + const isIpad = result.current.up('m') && result.current.down('l'); + expect(isIpad).toEqual(true); + }); + + it('should determine `isMobile (Portrait)`', () => { + (global as { innerWidth: number }).innerWidth = 480; + const { result } = renderHook(() => useBreakpoints()); + + const isMobile = result.current.up('xs') && result.current.down('s'); + expect(isMobile).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/hooks/use_breakpoints.ts b/x-pack/plugins/uptime/public/hooks/use_breakpoints.ts new file mode 100644 index 0000000000000..9398a5fcd15fe --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_breakpoints.ts @@ -0,0 +1,114 @@ +/* + * 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 { useCallback, useState } from 'react'; +import useWindowSize from 'react-use/lib/useWindowSize'; +import useDebounce from 'react-use/lib/useDebounce'; + +import { BREAKPOINTS, EuiBreakpointSize } from '@elastic/eui'; + +// Custom breakpoints +const BREAKPOINT_XL = 1599; // Overriding the theme's default 'xl' breakpoint +const BREAKPOINT_XXL = 1599; +const BREAKPOINT_XXXL = 2000; + +export type BreakpointKey = EuiBreakpointSize | 'xxl' | 'xxxl'; + +type BreakpointPredicate = (breakpointKey: BreakpointKey) => boolean; +type BreakpointRangePredicate = (from: BreakpointKey, to: BreakpointKey) => boolean; + +/** + * Returns the predicates functions used to determine whether the current device's width is above or below the asked + * breakpoint. (Implementation inspired by React Material UI). + * + * @example + * const { breakpoints } = useBreakpoints(); + * const isMobile = breakpoint.down('m'); + * + * @example + * const { breakpoints } = useBreakpoints(); + * const isTablet = breakpoint.between('m', 'l'); + * + * @param debounce {number} Debounce interval for optimization + * + * @returns { {up: BreakpointPredicate, down: BreakpointPredicate, between: BreakpointRangePredicate} } + * Returns object containing predicates which determine whether the current device's width lies above, below or + * in-between the given breakpoint(s) + * { + * up => Returns `true` if the current width is equal or above (inclusive) the given breakpoint size, + * or `false` otherwise. + * down => Returns `true` if the current width is below (exclusive) the given breakpoint size, or `false` otherwise. + * between => Returns `true` if the current width is equal or above (inclusive) the corresponding size of + * `fromBreakpointKey` AND is below (exclusive) the corresponding width of `toBreakpointKey`. + * Returns `false` otherwise. + * } + */ +export function useBreakpoints(debounce = 50) { + const { width } = useWindowSize(); + const [debouncedWidth, setDebouncedWidth] = useState(width); + + const up = useCallback( + (breakpointKey: BreakpointKey) => isUp(debouncedWidth, breakpointKey), + [debouncedWidth] + ); + const down = useCallback( + (breakpointKey: BreakpointKey) => isDown(debouncedWidth, breakpointKey), + [debouncedWidth] + ); + + const between = useCallback( + (fromBreakpointKey: BreakpointKey, toBreakpointKey: BreakpointKey) => + isBetween(debouncedWidth, fromBreakpointKey, toBreakpointKey), + [debouncedWidth] + ); + + useDebounce( + () => { + setDebouncedWidth(width); + }, + debounce, + [width] + ); + + return { up, down, between, debouncedWidth }; +} + +/** + * Returns the corresponding device width against the provided breakpoint key, either the overridden value or the + * default value from theme. + * @param key {BreakpointKey} string key representing the device breakpoint e.g. 'xs', 's', 'xxxl' + */ +function getSizeForBreakpointKey(key: BreakpointKey): number { + switch (key) { + case 'xxxl': + return BREAKPOINT_XXXL; + case 'xxl': + return BREAKPOINT_XXL; + case 'xl': + return BREAKPOINT_XL; + case 'l': + return BREAKPOINTS.l; + case 'm': + return BREAKPOINTS.m; + case 's': + return BREAKPOINTS.s; + } + + return BREAKPOINTS.xs; +} + +function isUp(size: number, breakpointKey: BreakpointKey) { + return size >= getSizeForBreakpointKey(breakpointKey); +} + +function isDown(size: number, breakpointKey: BreakpointKey) { + return size < getSizeForBreakpointKey(breakpointKey); +} + +function isBetween(size: number, fromBreakpointKey: BreakpointKey, toBreakpointKey: BreakpointKey) { + return isUp(size, fromBreakpointKey) && isDown(size, toBreakpointKey); +} diff --git a/x-pack/test/accessibility/apps/kibana_overview.ts b/x-pack/test/accessibility/apps/kibana_overview.ts index 9f5d91e5b4d54..6ea51cc0b855c 100644 --- a/x-pack/test/accessibility/apps/kibana_overview.ts +++ b/x-pack/test/accessibility/apps/kibana_overview.ts @@ -12,7 +12,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); // FLAKY: https://github.com/elastic/kibana/issues/98463 - describe.skip('Kibana overview', () => { + describe('Kibana overview', () => { const esArchiver = getService('esArchiver'); before(async () => { diff --git a/x-pack/test/accessibility/apps/ml.ts b/x-pack/test/accessibility/apps/ml.ts index e06661b000203..4babe0bd6ff88 100644 --- a/x-pack/test/accessibility/apps/ml.ts +++ b/x-pack/test/accessibility/apps/ml.ts @@ -13,8 +13,7 @@ export default function ({ getService }: FtrProviderContext) { const a11y = getService('a11y'); const ml = getService('ml'); - // Failing: See https://github.com/elastic/kibana/issues/115666 - describe.skip('ml', () => { + describe('ml', () => { const esArchiver = getService('esArchiver'); before(async () => { diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index 699b5b48d604c..933e8e97da397 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -17,10 +17,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { testFiles: [ require.resolve('./apps/login_page'), - require.resolve('./apps/home'), require.resolve('./apps/kibana_overview'), + require.resolve('./apps/home'), require.resolve('./apps/grok_debugger'), require.resolve('./apps/search_profiler'), + require.resolve('./apps/painless_lab'), require.resolve('./apps/uptime'), require.resolve('./apps/spaces'), require.resolve('./apps/advanced_settings'), diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index e44d0cd10e9f2..9b530873ad165 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -44,6 +44,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/categorization'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/categorization_small'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_apache'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_auditbeat'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_apm'); diff --git a/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts b/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts index 4686787ae9b16..9d6009bbb3ea6 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts @@ -64,7 +64,7 @@ const analyzer = { ], }; const defaultRequestBody = { - indexPatternTitle: 'ft_categorization', + indexPatternTitle: 'ft_categorization_small', query: { bool: { must: [{ match_all: {} }] } }, size: 5, timeField: '@timestamp', @@ -286,7 +286,7 @@ export default ({ getService }: FtrProviderContext) => { describe('Categorization example endpoint - ', function () { before(async () => { - await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/categorization'); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/categorization_small'); await ml.testResources.setKibanaTimeZoneToUTC(); }); diff --git a/x-pack/test/apm_api_integration/common/trace_data.ts b/x-pack/test/apm_api_integration/common/trace_data.ts index 84bbb4beea4f4..9799e111cb135 100644 --- a/x-pack/test/apm_api_integration/common/trace_data.ts +++ b/x-pack/test/apm_api_integration/common/trace_data.ts @@ -20,15 +20,20 @@ export async function traceData(context: InheritedFtrProviderContext) { const es = context.getService('es'); return { index: (events: any[]) => { - const esEvents = toElasticsearchOutput( - [ + const esEvents = toElasticsearchOutput({ + events: [ ...events, ...getTransactionMetrics(events), ...getSpanDestinationMetrics(events), ...getBreakdownMetrics(events), ], - '7.14.0' - ); + writeTargets: { + transaction: 'apm-7.14.0-transaction', + span: 'apm-7.14.0-span', + error: 'apm-7.14.0-error', + metric: 'apm-7.14.0-metric', + }, + }); const batches = chunk(esEvents, 1000); const limiter = pLimit(1); diff --git a/x-pack/test/examples/config.ts b/x-pack/test/examples/config.ts index 606f97f9c3de7..cf37cad5bc243 100644 --- a/x-pack/test/examples/config.ts +++ b/x-pack/test/examples/config.ts @@ -33,7 +33,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { reportName: 'X-Pack Example plugin functional tests', }, - testFiles: [require.resolve('./search_examples'), require.resolve('./embedded_lens')], + testFiles: [ + require.resolve('./search_examples'), + require.resolve('./embedded_lens'), + require.resolve('./reporting_examples'), + ], kbnTestServer: { ...xpackFunctionalConfig.get('kbnTestServer'), diff --git a/x-pack/test/examples/reporting_examples/capture_test.ts b/x-pack/test/examples/reporting_examples/capture_test.ts new file mode 100644 index 0000000000000..62460bd140bba --- /dev/null +++ b/x-pack/test/examples/reporting_examples/capture_test.ts @@ -0,0 +1,91 @@ +/* + * 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 path from 'path'; +import type { FtrProviderContext } from '../../functional/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'reporting']); + const compareImages = getService('compareImages'); + const testSubjects = getService('testSubjects'); + + const appId = 'reportingExample'; + + const fixtures = { + baselineAPng: path.resolve(__dirname, 'fixtures/baseline/capture_a.png'), + baselineAPdf: path.resolve(__dirname, 'fixtures/baseline/capture_a.pdf'), + baselineAPdfPrint: path.resolve(__dirname, 'fixtures/baseline/capture_a_print.pdf'), + }; + + describe('Captures', () => { + it('PNG that matches the baseline', async () => { + await PageObjects.common.navigateToApp(appId); + + await (await testSubjects.find('shareButton')).click(); + await (await testSubjects.find('captureTestPanel')).click(); + await (await testSubjects.find('captureTestPNG')).click(); + + await PageObjects.reporting.clickGenerateReportButton(); + const url = await PageObjects.reporting.getReportURL(60000); + const captureData = await PageObjects.reporting.getRawPdfReportData(url); + + const pngSessionFilePath = await compareImages.writeToSessionFile( + 'capture_test_baseline_a', + captureData + ); + + expect( + await compareImages.checkIfPngsMatch(pngSessionFilePath, fixtures.baselineAPng) + ).to.be.lessThan(0.09); + }); + + it('PDF that matches the baseline', async () => { + await PageObjects.common.navigateToApp(appId); + + await (await testSubjects.find('shareButton')).click(); + await (await testSubjects.find('captureTestPanel')).click(); + await (await testSubjects.find('captureTestPDF')).click(); + + await PageObjects.reporting.clickGenerateReportButton(); + const url = await PageObjects.reporting.getReportURL(60000); + const captureData = await PageObjects.reporting.getRawPdfReportData(url); + + const pdfSessionFilePath = await compareImages.writeToSessionFile( + 'capture_test_baseline_a', + captureData + ); + + expect( + await compareImages.checkIfPdfsMatch(pdfSessionFilePath, fixtures.baselineAPdf) + ).to.be.lessThan(0.001); + }); + + it('print-optimized PDF that matches the baseline', async () => { + await PageObjects.common.navigateToApp(appId); + + await (await testSubjects.find('shareButton')).click(); + await (await testSubjects.find('captureTestPanel')).click(); + await (await testSubjects.find('captureTestPDFPrint')).click(); + + await PageObjects.reporting.checkUsePrintLayout(); + await PageObjects.reporting.clickGenerateReportButton(); + const url = await PageObjects.reporting.getReportURL(60000); + const captureData = await PageObjects.reporting.getRawPdfReportData(url); + + const pdfSessionFilePath = await compareImages.writeToSessionFile( + 'capture_test_baseline_a', + captureData + ); + + expect( + await compareImages.checkIfPdfsMatch(pdfSessionFilePath, fixtures.baselineAPdfPrint) + ).to.be.lessThan(0.001); + }); + }); +} diff --git a/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a.pdf b/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a.pdf new file mode 100644 index 0000000000000..3966d4406b7b2 Binary files /dev/null and b/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a.pdf differ diff --git a/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a.png b/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a.png new file mode 100644 index 0000000000000..7c121804e4296 Binary files /dev/null and b/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a.png differ diff --git a/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a_print.pdf b/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a_print.pdf new file mode 100644 index 0000000000000..9036a87c6397c Binary files /dev/null and b/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a_print.pdf differ diff --git a/x-pack/test/examples/reporting_examples/index.ts b/x-pack/test/examples/reporting_examples/index.ts new file mode 100644 index 0000000000000..a0e8689a26586 --- /dev/null +++ b/x-pack/test/examples/reporting_examples/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; + +// eslint-disable-next-line import/no-default-export +export default function ({ loadTestFile }: PluginFunctionalProviderContext) { + describe('reporting examples', function () { + this.tags('ciGroup13'); + + loadTestFile(require.resolve('./capture_test')); + }); +} diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json index b3d33f5d45345..449731d9e4ab2 100644 --- a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json +++ b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json @@ -4,7 +4,7 @@ "id": "3KVN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "963b081e-60d1-482c-befd-a5815fa8290f", "version": "6.6.1", @@ -26,7 +26,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d14", "kind": "metric", "category": [ @@ -74,7 +74,7 @@ "id": "3aVN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "b3412d6f-b022-4448-8fee-21cc936ea86b", "version": "6.0.0", @@ -96,7 +96,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d15", "kind": "metric", "category": [ @@ -143,7 +143,7 @@ "id": "3qVN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "3838df35-a095-4af4-8fce-0b6d78793f2e", "version": "6.8.0", @@ -165,7 +165,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d16", "kind": "metric", "category": [ @@ -210,7 +210,7 @@ "id": "36VN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "963b081e-60d1-482c-befd-a5815fa8290f", "version": "6.6.1", @@ -232,7 +232,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d18", "kind": "metric", "category": [ @@ -280,7 +280,7 @@ "id": "4KVN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "b3412d6f-b022-4448-8fee-21cc936ea86b", "version": "6.0.0", @@ -302,7 +302,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d19", "kind": "metric", "category": [ @@ -348,7 +348,7 @@ "id": "4aVN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "3838df35-a095-4af4-8fce-0b6d78793f2e", "version": "6.8.0", @@ -370,7 +370,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d39", "kind": "metric", "category": [ @@ -416,7 +416,7 @@ "id": "4qVN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "963b081e-60d1-482c-befd-a5815fa8290f", "version": "6.6.1", @@ -438,7 +438,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d31", "kind": "metric", "category": [ @@ -485,7 +485,7 @@ "id": "46VN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "b3412d6f-b022-4448-8fee-21cc936ea86b", "version": "6.0.0", @@ -507,7 +507,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d23", "kind": "metric", "category": [ @@ -553,7 +553,7 @@ "id": "5KVN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "3838df35-a095-4af4-8fce-0b6d78793f2e", "version": "6.8.0", @@ -575,7 +575,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d35", "kind": "metric", "category": [ diff --git a/x-pack/test/functional/es_archives/ml/categorization_small/data.json.gz b/x-pack/test/functional/es_archives/ml/categorization_small/data.json.gz new file mode 100644 index 0000000000000..76ac07831dec1 Binary files /dev/null and b/x-pack/test/functional/es_archives/ml/categorization_small/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/ml/categorization_small/mappings.json b/x-pack/test/functional/es_archives/ml/categorization_small/mappings.json new file mode 100644 index 0000000000000..b73babf361625 --- /dev/null +++ b/x-pack/test/functional/es_archives/ml/categorization_small/mappings.json @@ -0,0 +1,41 @@ +{ + "type": "index", + "value": { + "aliases": {}, + "index": "ft_categorization_small", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "field1": { + "type": "text" + }, + "field2": { + "type": "text" + }, + "field3": { + "type": "text" + }, + "field4": { + "type": "text" + }, + "field5": { + "type": "text" + }, + "field6": { + "type": "text" + }, + "field7": { + "type": "text" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/page_objects/observability_page.ts b/x-pack/test/functional/page_objects/observability_page.ts index d9e413d473adf..f89dafe4f3a73 100644 --- a/x-pack/test/functional/page_objects/observability_page.ts +++ b/x-pack/test/functional/page_objects/observability_page.ts @@ -11,7 +11,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function ObservabilityPageProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); - const find = getService('find'); return { async expectCreateCaseButtonEnabled() { @@ -32,6 +31,10 @@ export function ObservabilityPageProvider({ getService, getPageObjects }: FtrPro await testSubjects.missingOrFail('case-callout-e41900b01c9ef0fa81dd6ff326083fb3'); }, + async expectNoDataPage() { + await testSubjects.existOrFail('noDataPage'); + }, + async expectCreateCase() { await testSubjects.existOrFail('case-creation-form-steps'); }, @@ -47,7 +50,7 @@ export function ObservabilityPageProvider({ getService, getPageObjects }: FtrPro }, async expectForbidden() { - const h2 = await find.byCssSelector('body', 20000); + const h2 = await testSubjects.find('no_feature_permissions', 20000); const text = await h2.getVisibleText(); expect(text).to.contain('Kibana feature privileges required'); }, diff --git a/x-pack/test/functional/services/compare_images.ts b/x-pack/test/functional/services/compare_images.ts new file mode 100644 index 0000000000000..9ad98dff3819c --- /dev/null +++ b/x-pack/test/functional/services/compare_images.ts @@ -0,0 +1,149 @@ +/* + * 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 path from 'path'; +import { promises as fs } from 'fs'; +import { pdf as pdfToPng } from 'pdf-to-img'; +import { comparePngs } from '../../../../test/functional/services/lib/compare_pngs'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export function CompareImagesProvider({ getService }: FtrProviderContext) { + const log = getService('log'); + const config = getService('config'); + + const screenshotsDir = config.get('screenshots.directory'); + + const writeToSessionFile = async (name: string, rawPdf: Buffer) => { + const sessionDirectory = path.resolve(screenshotsDir, 'session'); + await fs.mkdir(sessionDirectory, { recursive: true }); + const sessionReportPath = path.resolve(sessionDirectory, `${name}.png`); + await fs.writeFile(sessionReportPath, rawPdf); + return sessionReportPath; + }; + + return { + writeToSessionFile, + async checkIfPngsMatch( + actualPngPath: string, + baselinePngPath: string, + screenshotsDirectory: string = screenshotsDir + ) { + log.debug(`checkIfPngsMatch: ${baselinePngPath}`); + // Copy the pngs into the screenshot session directory, as that's where the generated pngs will automatically be + // stored. + const sessionDirectoryPath = path.resolve(screenshotsDirectory, 'session'); + const failureDirectoryPath = path.resolve(screenshotsDirectory, 'failure'); + + await fs.mkdir(sessionDirectoryPath, { recursive: true }); + await fs.mkdir(failureDirectoryPath, { recursive: true }); + + const actualPngFileName = path.basename(actualPngPath, '.png'); + const baselinePngFileName = path.basename(baselinePngPath, '.png'); + + const baselineCopyPath = path.resolve( + sessionDirectoryPath, + `${baselinePngFileName}_baseline.png` + ); + // Don't cause a test failure if the baseline snapshot doesn't exist - we don't have all OS's covered and we + // don't want to start causing failures for other devs working on OS's which are lacking snapshots. We have + // mac and linux covered which is better than nothing for now. + try { + log.debug(`writeFile: ${baselineCopyPath}`); + await fs.writeFile(baselineCopyPath, await fs.readFile(baselinePngPath)); + } catch (error) { + throw new Error(`No baseline png found at ${baselinePngPath}`); + } + + const actualCopyPath = path.resolve(sessionDirectoryPath, `${actualPngFileName}_actual.png`); + log.debug(`writeFile: ${actualCopyPath}`); + await fs.writeFile(actualCopyPath, await fs.readFile(actualPngPath)); + + let diffTotal = 0; + + const diffPngPath = path.resolve(failureDirectoryPath, `${baselinePngFileName}-${1}.png`); + diffTotal += await comparePngs( + actualCopyPath, + baselineCopyPath, + diffPngPath, + sessionDirectoryPath, + log + ); + + return diffTotal; + }, + async checkIfPdfsMatch( + actualPdfPath: string, + baselinePdfPath: string, + screenshotsDirectory = screenshotsDir + ) { + log.debug(`checkIfPdfsMatch: ${actualPdfPath} vs ${baselinePdfPath}`); + // Copy the pdfs into the screenshot session directory, as that's where the generated pngs will automatically be + // stored. + const sessionDirectoryPath = path.resolve(screenshotsDirectory, 'session'); + const failureDirectoryPath = path.resolve(screenshotsDirectory, 'failure'); + + await fs.mkdir(sessionDirectoryPath, { recursive: true }); + await fs.mkdir(failureDirectoryPath, { recursive: true }); + + const actualPdfFileName = path.basename(actualPdfPath, '.pdf'); + const baselinePdfFileName = path.basename(baselinePdfPath, '.pdf'); + + const baselineCopyPath = path.resolve( + sessionDirectoryPath, + `${baselinePdfFileName}_baseline.pdf` + ); + const actualCopyPath = path.resolve(sessionDirectoryPath, `${actualPdfFileName}_actual.pdf`); + + // Don't cause a test failure if the baseline snapshot doesn't exist - we don't have all OS's covered and we + // don't want to start causing failures for other devs working on OS's which are lacking snapshots. We have + // mac and linux covered which is better than nothing for now. + try { + log.debug(`writeFileSync: ${baselineCopyPath}`); + await fs.writeFile(baselineCopyPath, await fs.readFile(baselinePdfPath)); + } catch (error) { + log.error(`No baseline pdf found at ${baselinePdfPath}`); + return 0; + } + log.debug(`writeFileSync: ${actualCopyPath}`); + await fs.writeFile(actualCopyPath, await fs.readFile(actualPdfPath)); + + const actualPdf = await pdfToPng(actualCopyPath); + const baselinePdf = await pdfToPng(baselineCopyPath); + + log.debug(`Checking number of pages`); + + if (actualPdf.length !== baselinePdf.length) { + throw new Error( + `Expected ${baselinePdf.length} pages but got ${actualPdf.length} in PDFs expected: "${baselineCopyPath}" actual: "${actualCopyPath}".` + ); + } + + let diffTotal = 0; + let pageNum = 1; + + for await (const actualPage of actualPdf) { + for await (const baselinePage of baselinePdf) { + const diffPngPath = path.resolve( + failureDirectoryPath, + `${baselinePdfFileName}-${pageNum}.png` + ); + diffTotal += await comparePngs( + { path: path.resolve(screenshotsDirectory, '_actual.png'), buffer: actualPage }, + { path: path.resolve(screenshotsDirectory, '_baseline.png'), buffer: baselinePage }, + diffPngPath, + sessionDirectoryPath, + log + ); + ++pageNum; + break; + } + } + + return diffTotal; + }, + }; +} diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 5e40eb040178b..3e69a5f43928a 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -61,6 +61,7 @@ import { } from './dashboard'; import { SearchSessionsService } from './search_sessions'; import { ObservabilityProvider } from './observability'; +import { CompareImagesProvider } from './compare_images'; // define the name and providers for services that should be // available to your tests. If you don't specify anything here @@ -112,4 +113,5 @@ export const services = { reporting: ReportingFunctionalProvider, searchSessions: SearchSessionsService, observability: ObservabilityProvider, + compareImages: CompareImagesProvider, }; diff --git a/x-pack/test/functional/services/observability/alerts/common.ts b/x-pack/test/functional/services/observability/alerts/common.ts index d5a2ce2a18c41..866febc9b9f5b 100644 --- a/x-pack/test/functional/services/observability/alerts/common.ts +++ b/x-pack/test/functional/services/observability/alerts/common.ts @@ -66,6 +66,10 @@ export function ObservabilityAlertsCommonProvider({ return await testSubjects.existOrFail(ALERTS_TABLE_CONTAINER_SELECTOR); }; + const getNoDataPageOrFail = async () => { + return await testSubjects.existOrFail('noDataPage'); + }; + const getNoDataStateOrFail = async () => { return await testSubjects.existOrFail('tGridEmptyState'); }; @@ -193,6 +197,7 @@ export function ObservabilityAlertsCommonProvider({ getCopyToClipboardButton, getFilterForValueButton, copyToClipboardButtonExists, + getNoDataPageOrFail, getNoDataStateOrFail, getTableCells, getTableCellsInRows, diff --git a/x-pack/test/observability_functional/apps/observability/alerts/add_to_case.ts b/x-pack/test/observability_functional/apps/observability/alerts/add_to_case.ts index f29111f2cb66b..5e80a5769b44d 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/add_to_case.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/add_to_case.ts @@ -17,9 +17,11 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); + await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); }); after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'); await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts'); }); diff --git a/x-pack/test/observability_functional/apps/observability/alerts/index.ts b/x-pack/test/observability_functional/apps/observability/alerts/index.ts index 14019472eb2ca..a247f42da5821 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/index.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/index.ts @@ -38,7 +38,22 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts'); }); + describe('With no data', () => { + it('Shows the no data screen', async () => { + await observability.alerts.common.getNoDataPageOrFail(); + }); + }); + describe('Alerts table', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); + await observability.alerts.common.navigateToTimeWithData(); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'); + }); + it('Renders the table', async () => { await observability.alerts.common.getTableOrFail(); }); diff --git a/x-pack/test/observability_functional/apps/observability/alerts/workflow_status.ts b/x-pack/test/observability_functional/apps/observability/alerts/workflow_status.ts index a68636b8cb0c0..879cef01c2ada 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/workflow_status.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/workflow_status.ts @@ -21,11 +21,13 @@ export default ({ getService }: FtrProviderContext) => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); + await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); await observability.alerts.common.navigateToTimeWithData(); }); after(async () => { await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts'); + await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'); }); it('is filtered to only show "open" alerts by default', async () => { diff --git a/x-pack/test/observability_functional/apps/observability/feature_controls/observability_security.ts b/x-pack/test/observability_functional/apps/observability/feature_controls/observability_security.ts index 69bf995c49ae4..85512f8333038 100644 --- a/x-pack/test/observability_functional/apps/observability/feature_controls/observability_security.ts +++ b/x-pack/test/observability_functional/apps/observability/feature_controls/observability_security.ts @@ -31,8 +31,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.unload('x-pack/test/functional/es_archives/cases/default'); }); + it('Shows the no data page on load', async () => { + await PageObjects.common.navigateToActualUrl('observabilityCases'); + await PageObjects.observability.expectNoDataPage(); + }); + describe('observability cases all privileges', () => { before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); await observability.users.setTestUserRole( observability.users.defineBasicObservabilityRole({ observabilityCases: ['all'], @@ -42,6 +48,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'); await observability.users.restoreDefaultTestUserRole(); }); @@ -83,6 +90,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('observability cases read-only privileges', () => { before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); await observability.users.setTestUserRole( observability.users.defineBasicObservabilityRole({ observabilityCases: ['read'], @@ -92,6 +100,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'); await observability.users.restoreDefaultTestUserRole(); }); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts index 75c2dec6e9c9d..48c0aea825048 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts @@ -20,8 +20,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const endpointTestResources = getService('endpointTestResources'); const policyTestResources = getService('policyTestResources'); - // Skipping Flakey test: https://github.com/elastic/kibana/issues/110309 - describe.skip('Endpoint permissions:', () => { + describe('Endpoint permissions:', () => { let indexedData: IndexedHostsAndAlertsResponse; before(async () => { @@ -62,7 +61,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.existOrFail('noIngestPermissions'); }); - it('should display endpoint data on Host Details', async () => { + // FIXME:PT skipped. need to fix security-team bug #1929 + it.skip('should display endpoint data on Host Details', async () => { const endpoint = indexedData.hosts[0]; await PageObjects.hosts.navigateToHostDetails(endpoint.host.name); const endpointSummary = await PageObjects.hosts.hostDetailsEndpointOverviewData(); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index 2dcf36cc42ae2..afdc364ffd970 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -24,8 +24,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - // Failing: See https://github.com/elastic/kibana/issues/115488 - describe.skip('test metadata api', () => { + describe('test metadata api', () => { // TODO add this after endpoint package changes are merged and in snapshot // describe('with .metrics-endpoint.metadata_united_default index', () => { // }); @@ -242,7 +241,7 @@ export default function ({ getService }: FtrProviderContext) { (ip: string) => ip === targetEndpointIp ); expect(resultIp).to.eql([targetEndpointIp]); - expect(body.hosts[0].metadata.event.created).to.eql(1626897841950); + expect(body.hosts[0].metadata.event.created).to.eql(1634656952181); expect(body.hosts.length).to.eql(1); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); @@ -284,7 +283,7 @@ export default function ({ getService }: FtrProviderContext) { const resultElasticAgentId: string = body.hosts[0].metadata.elastic.agent.id; expect(resultHostId).to.eql(targetEndpointId); expect(resultElasticAgentId).to.eql(targetElasticAgentId); - expect(body.hosts[0].metadata.event.created).to.eql(1626897841950); + expect(body.hosts[0].metadata.event.created).to.eql(1634656952181); expect(body.hosts[0].host_status).to.eql('unhealthy'); expect(body.hosts.length).to.eql(1); expect(body.request_page_size).to.eql(10); diff --git a/yarn.lock b/yarn.lock index c7b641b79b56e..205c85fcbb76a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2337,10 +2337,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@37.0.0": - version "37.0.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-37.0.0.tgz#a94526461c404b449953cca4fe34f8bf3620413e" - integrity sha512-Pfm58/voERWVPJlxy13DphwgRoBGYhnSyz65kdsPg6lYGxN5ngWvuTuJ3477fyApYV01Pz4Ckt9yj1BSQue80Q== +"@elastic/charts@38.0.1": + version "38.0.1" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-38.0.1.tgz#9c1db7e0f1de869e0b2b505e192bbb9d62d60dc8" + integrity sha512-i9mIA3Ji9jSjuFDtuh9gV1xpCl3sbBEDgJiOgLVt04pr/qZH2W+tr3AV5yHvjsR7Te0Pmh/Cm5wLBvFKaI1nIA== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" @@ -4114,6 +4114,21 @@ resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz#f60b6a55a5d8e5ee908347d2ce4250b15103dc8e" integrity sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg== +"@mapbox/node-pre-gyp@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz#2a0b32fcb416fb3f2250fd24cb2a81421a4f5950" + integrity sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA== + dependencies: + detect-libc "^1.0.3" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.1" + nopt "^5.0.0" + npmlog "^4.1.2" + rimraf "^3.0.2" + semver "^7.3.4" + tar "^6.1.0" + "@mapbox/point-geometry@0.1.0", "@mapbox/point-geometry@^0.1.0", "@mapbox/point-geometry@~0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz#8a83f9335c7860effa2eeeca254332aa0aeed8f2" @@ -10131,6 +10146,15 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001097, caniuse-lite@^1.0.30001109, can resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001261.tgz#96d89813c076ea061209a4e040d8dcf0c66a1d01" integrity sha512-vM8D9Uvp7bHIN0fZ2KQ4wnmYFpJo/Etb4Vwsuc+ka0tfGDHvOPrFm6S/7CCNLSOkAUjenT2HnUPESdOIL91FaA== +canvas@2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/canvas/-/canvas-2.8.0.tgz#f99ca7f25e6e26686661ffa4fec1239bbef74461" + integrity sha512-gLTi17X8WY9Cf5GZ2Yns8T5lfBOcGgFehDFb+JQwDqdOoBOcECS9ZWMEAqMSVcMYwXD659J8NyzjRY/2aE+C2Q== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.0" + nan "^2.14.0" + simple-get "^3.0.3" + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -19343,18 +19367,17 @@ listr@^0.14.1: p-map "^2.0.0" rxjs "^6.3.3" -lmdb-store@^1.6.8: - version "1.6.8" - resolved "https://registry.yarnpkg.com/lmdb-store/-/lmdb-store-1.6.8.tgz#f57c1fa4a8e8e7a73d58523d2bfbcee96782311f" - integrity sha512-Ltok13VVAfgO5Fdj/jVzXjPJZjefl1iENEHerZyAfAlzFUhvOrA73UdKItqmEPC338U29mm56ZBQr5NJQiKXow== +lmdb-store@^1.6.11: + version "1.6.11" + resolved "https://registry.yarnpkg.com/lmdb-store/-/lmdb-store-1.6.11.tgz#801da597af8c7a01c81f87d5cc7a7497e381236d" + integrity sha512-hIvoGmHGsFhb2VRCmfhodA/837ULtJBwRHSHKIzhMB7WtPH6BRLPsvXp1MwD3avqGzuZfMyZDUp3tccLvr721Q== dependencies: - mkdirp "^1.0.4" nan "^2.14.2" node-gyp-build "^4.2.3" ordered-binary "^1.0.0" weak-lru-cache "^1.0.0" optionalDependencies: - msgpackr "^1.3.7" + msgpackr "^1.4.7" load-bmfont@^1.3.1, load-bmfont@^1.4.0: version "1.4.0" @@ -20823,7 +20846,7 @@ ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -msgpackr-extract@^1.0.13: +msgpackr-extract@^1.0.14: version "1.0.14" resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-1.0.14.tgz#87d3fe825d226e7f3d9fe136375091137f958561" integrity sha512-t8neMf53jNZRF+f0H9VvEUVvtjGZ21odSBRmFfjZiyxr9lKYY0mpY3kSWZAIc7YWXtCZGOvDQVx2oqcgGiRBrw== @@ -20831,12 +20854,12 @@ msgpackr-extract@^1.0.13: nan "^2.14.2" node-gyp-build "^4.2.3" -msgpackr@^1.3.7: - version "1.4.2" - resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.4.2.tgz#52ddf0130ccdb1067957fe61c8be828e82bb29ce" - integrity sha512-6gvaU+3xIflium8eJcruT66kLQr14lgTEmXtDm7KKzBSWHljD7pqu3VBQv1PDipFD5UGXLTIxGg5hGbO/jTvxQ== +msgpackr@^1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.4.7.tgz#d802ade841e7d2e873000b491cdda6574a3d5748" + integrity sha512-bhC8Ed1au3L3oHaR/fe4lk4w7PLGFcWQ5XY/Tk9N6tzDRz8YndjCG68TD8zcvYZoxNtw767eF/7VpaTpU9kf9w== optionalDependencies: - msgpackr-extract "^1.0.13" + msgpackr-extract "^1.0.14" multicast-dns-service-types@^1.1.0: version "1.1.0" @@ -22473,6 +22496,19 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" +pdf-to-img@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pdf-to-img/-/pdf-to-img-1.1.1.tgz#1918738477c3cc95a6786877bb1e36de81909400" + integrity sha512-e+4BpKSDhU+BZt34yo2P5OAqO0CRRy8xSNGDP7HhpT2FMEo5H7mzNcXdymYKRcj7xIr0eK1gYFhyjpWwHGp46Q== + dependencies: + canvas "2.8.0" + pdfjs-dist "2.9.359" + +pdfjs-dist@2.9.359: + version "2.9.359" + resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.9.359.tgz#e67bafebf20e50fc41f1a5c189155ad008ac4f81" + integrity sha512-P2nYtkacdlZaNNwrBLw1ZyMm0oE2yY/5S/GDCAmMJ7U4+ciL/D0mrlEC/o4HZZc/LNE3w8lEVzBEyVgEQlPVKQ== + pdfkit@>=0.8.1, pdfkit@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/pdfkit/-/pdfkit-0.11.0.tgz#9cdb2fc42bd2913587fe3ddf48cc5bbb3c36f7de" @@ -27749,7 +27785,7 @@ tar@6.1.9: mkdirp "^1.0.3" yallist "^4.0.0" -tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: +tar@^6.0.2, tar@^6.1.0, tar@^6.1.11, tar@^6.1.2: version "6.1.11" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==