From 39a29cc7698578c4a9c3923141a54f63a97de4e2 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 29 Nov 2021 10:14:33 +0100 Subject: [PATCH 001/224] [APM] Generate stack monitoring data (#118302) * Generate stack monitoring data * Update file import * Fix imports after bad merge from upstream * A cluster stats generator * Wiring kibana docs to ES docs * Adding fields to get kibana cards rendering * [apm-synthtrace] Export types Fields, ApmException, ApmSynthtraceEsClient * [APM] Update integration tests with synthtrace changes * [APM] Update Cypress E2E tests with synthtrace changes * Fix lint errors Co-authored-by: Milton Hultgren Co-authored-by: Mat Schaffer --- packages/elastic-apm-synthtrace/src/index.ts | 16 +- .../src/lib/{ => apm}/apm_error.ts | 10 +- .../src/lib/apm/apm_fields.ts | 78 +++++++ .../src/lib/{ => apm}/base_span.ts | 12 +- .../src/lib/{ => apm}/browser.ts | 7 +- .../client/apm_synthtrace_es_client.ts} | 39 ++-- .../get_chrome_user_agent_defaults.ts | 4 +- .../defaults/get_observer_defaults.ts | 4 +- .../src/lib/apm/index.ts | 34 +++ .../src/lib/{ => apm}/instance.ts | 7 +- .../src/lib/{ => apm}/metricset.ts | 8 +- .../src/lib/{ => apm}/rum_span.ts | 0 .../src/lib/{ => apm}/rum_transaction.ts | 0 .../src/lib/{ => apm}/service.ts | 5 +- .../src/lib/{ => apm}/span.ts | 6 +- .../src/lib/{ => apm}/transaction.ts | 6 +- .../src/lib/{ => apm}/utils/aggregate.ts | 8 +- .../apm_events_to_elasticsearch_output.ts} | 35 ++- .../src/lib/{ => apm}/utils/create_picker.ts | 0 .../utils/get_apm_write_targets.ts} | 6 +- .../{ => apm}/utils/get_breakdown_metrics.ts | 8 +- .../utils/get_span_destination_metrics.ts | 4 +- .../utils/get_transaction_metrics.ts | 4 +- .../elastic-apm-synthtrace/src/lib/entity.ts | 77 +------ .../src/lib/serializable.ts | 6 +- .../src/lib/stack_monitoring/cluster.ts | 38 ++++ .../src/lib/stack_monitoring/cluster_stats.ts | 30 +++ .../src/lib/stack_monitoring/index.ts | 12 + .../src/lib/stack_monitoring/kibana.ts | 19 ++ .../src/lib/stack_monitoring/kibana_stats.ts | 26 +++ .../stack_monitoring_fields.ts | 29 +++ .../src/lib/utils/clean_write_targets.ts | 9 +- .../src/lib/utils/dedot.ts | 16 ++ .../src/lib/utils/logger.ts | 67 ------ .../src/lib/utils/to_elasticsearch_output.ts | 44 ++++ .../src/scripts/examples/01_simple_trace.ts | 150 ++++++++----- .../src/scripts/examples/02_kibana_stats.ts | 48 ++++ .../src/scripts/examples/03_monitoring.ts | 70 ++++++ .../elastic-apm-synthtrace/src/scripts/run.ts | 211 ++++++++---------- .../src/scripts/scenario.ts | 13 ++ .../src/scripts/utils/get_common_services.ts | 25 +++ .../src/scripts/utils/get_scenario.ts | 4 +- ...on_resources.ts => parse_run_cli_flags.ts} | 44 +--- .../utils/start_historical_data_upload.ts | 52 ++--- .../scripts/utils/start_live_data_upload.ts | 56 ++--- .../src/scripts/utils/upload_events.ts | 22 +- .../src/scripts/utils/upload_next_batch.ts | 63 ++++-- ...pm_events_to_elasticsearch_output.test.ts} | 14 +- .../test/scenarios/01_simple_trace.test.ts | 4 +- .../scenarios/02_transaction_metrics.test.ts | 6 +- .../03_span_destination_metrics.test.ts | 6 +- .../scenarios/04_breakdown_metrics.test.ts | 10 +- .../05_transactions_with_errors.test.ts | 6 +- .../scenarios/06_application_metrics.test.ts | 6 +- .../cypress/fixtures/synthtrace/opbeans.ts | 20 +- .../read_only_user/errors/generate_data.ts | 11 +- .../header_filters/generate_data.ts | 11 +- .../apm/ftr_e2e/cypress/plugins/index.ts | 5 +- .../common/synthtrace_es_client_service.ts | 4 +- .../tests/dependencies/generate_data.ts | 8 +- .../tests/error_rate/service_apis.spec.ts | 8 +- .../tests/errors/error_group_list.spec.ts | 6 +- .../tests/errors/generate_data.ts | 7 +- .../tests/latency/service_apis.spec.ts | 15 +- .../observability_overview.spec.ts | 21 +- .../instances_main_statistics.spec.ts | 6 +- .../services/error_groups/generate_data.ts | 8 +- .../tests/services/throughput.spec.ts | 22 +- .../tests/services/top_services.spec.ts | 28 +-- .../throughput/dependencies_apis.spec.ts | 14 +- .../tests/throughput/service_apis.spec.ts | 15 +- ...actions_groups_detailed_statistics.spec.ts | 8 +- 72 files changed, 1022 insertions(+), 679 deletions(-) rename packages/elastic-apm-synthtrace/src/lib/{ => apm}/apm_error.ts (74%) create mode 100644 packages/elastic-apm-synthtrace/src/lib/apm/apm_fields.ts rename packages/elastic-apm-synthtrace/src/lib/{ => apm}/base_span.ts (86%) rename packages/elastic-apm-synthtrace/src/lib/{ => apm}/browser.ts (86%) rename packages/elastic-apm-synthtrace/src/lib/{client/synthtrace_es_client.ts => apm/client/apm_synthtrace_es_client.ts} (58%) rename packages/elastic-apm-synthtrace/src/lib/{ => apm}/defaults/get_chrome_user_agent_defaults.ts (84%) rename packages/elastic-apm-synthtrace/src/lib/{ => apm}/defaults/get_observer_defaults.ts (82%) create mode 100644 packages/elastic-apm-synthtrace/src/lib/apm/index.ts rename packages/elastic-apm-synthtrace/src/lib/{ => apm}/instance.ts (87%) rename packages/elastic-apm-synthtrace/src/lib/{ => apm}/metricset.ts (72%) rename packages/elastic-apm-synthtrace/src/lib/{ => apm}/rum_span.ts (100%) rename packages/elastic-apm-synthtrace/src/lib/{ => apm}/rum_transaction.ts (100%) rename packages/elastic-apm-synthtrace/src/lib/{ => apm}/service.ts (85%) rename packages/elastic-apm-synthtrace/src/lib/{ => apm}/span.ts (88%) rename packages/elastic-apm-synthtrace/src/lib/{ => apm}/transaction.ts (93%) rename packages/elastic-apm-synthtrace/src/lib/{ => apm}/utils/aggregate.ts (82%) rename packages/elastic-apm-synthtrace/src/lib/{output/to_elasticsearch_output.ts => apm/utils/apm_events_to_elasticsearch_output.ts} (57%) rename packages/elastic-apm-synthtrace/src/lib/{ => apm}/utils/create_picker.ts (100%) rename packages/elastic-apm-synthtrace/src/lib/{utils/get_write_targets.ts => apm/utils/get_apm_write_targets.ts} (90%) rename packages/elastic-apm-synthtrace/src/lib/{ => apm}/utils/get_breakdown_metrics.ts (95%) rename packages/elastic-apm-synthtrace/src/lib/{ => apm}/utils/get_span_destination_metrics.ts (90%) rename packages/elastic-apm-synthtrace/src/lib/{ => apm}/utils/get_transaction_metrics.ts (94%) create mode 100644 packages/elastic-apm-synthtrace/src/lib/stack_monitoring/cluster.ts create mode 100644 packages/elastic-apm-synthtrace/src/lib/stack_monitoring/cluster_stats.ts create mode 100644 packages/elastic-apm-synthtrace/src/lib/stack_monitoring/index.ts create mode 100644 packages/elastic-apm-synthtrace/src/lib/stack_monitoring/kibana.ts create mode 100644 packages/elastic-apm-synthtrace/src/lib/stack_monitoring/kibana_stats.ts create mode 100644 packages/elastic-apm-synthtrace/src/lib/stack_monitoring/stack_monitoring_fields.ts create mode 100644 packages/elastic-apm-synthtrace/src/lib/utils/dedot.ts delete mode 100644 packages/elastic-apm-synthtrace/src/lib/utils/logger.ts create mode 100644 packages/elastic-apm-synthtrace/src/lib/utils/to_elasticsearch_output.ts create mode 100644 packages/elastic-apm-synthtrace/src/scripts/examples/02_kibana_stats.ts create mode 100644 packages/elastic-apm-synthtrace/src/scripts/examples/03_monitoring.ts create mode 100644 packages/elastic-apm-synthtrace/src/scripts/scenario.ts create mode 100644 packages/elastic-apm-synthtrace/src/scripts/utils/get_common_services.ts rename packages/elastic-apm-synthtrace/src/scripts/utils/{get_common_resources.ts => parse_run_cli_flags.ts} (59%) rename packages/elastic-apm-synthtrace/src/test/{to_elasticsearch_output.test.ts => apm_events_to_elasticsearch_output.test.ts} (76%) diff --git a/packages/elastic-apm-synthtrace/src/index.ts b/packages/elastic-apm-synthtrace/src/index.ts index 931215c75fde..381222ee10ef 100644 --- a/packages/elastic-apm-synthtrace/src/index.ts +++ b/packages/elastic-apm-synthtrace/src/index.ts @@ -6,17 +6,11 @@ * Side Public License, v 1. */ -export type { Exception } from './lib/entity'; -export { service } from './lib/service'; -export { browser } from './lib/browser'; export { timerange } from './lib/timerange'; -export { getTransactionMetrics } from './lib/utils/get_transaction_metrics'; -export { getSpanDestinationMetrics } from './lib/utils/get_span_destination_metrics'; -export { getObserverDefaults } from './lib/defaults/get_observer_defaults'; -export { getChromeUserAgentDefaults } from './lib/defaults/get_chrome_user_agent_defaults'; -export { toElasticsearchOutput } from './lib/output/to_elasticsearch_output'; -export { getBreakdownMetrics } from './lib/utils/get_breakdown_metrics'; +export { apm } from './lib/apm'; +export { stackMonitoring } from './lib/stack_monitoring'; export { cleanWriteTargets } from './lib/utils/clean_write_targets'; -export { getWriteTargets } from './lib/utils/get_write_targets'; -export { SynthtraceEsClient } from './lib/client/synthtrace_es_client'; export { createLogger, LogLevel } from './lib/utils/create_logger'; + +export type { Fields } from './lib/entity'; +export type { ApmException, ApmSynthtraceEsClient } from './lib/apm'; diff --git a/packages/elastic-apm-synthtrace/src/lib/apm_error.ts b/packages/elastic-apm-synthtrace/src/lib/apm/apm_error.ts similarity index 74% rename from packages/elastic-apm-synthtrace/src/lib/apm_error.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/apm_error.ts index 5a48093a26db..334c0f296851 100644 --- a/packages/elastic-apm-synthtrace/src/lib/apm_error.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/apm_error.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import { Fields } from './entity'; -import { Serializable } from './serializable'; -import { generateLongId, generateShortId } from './utils/generate_id'; +import { ApmFields } from './apm_fields'; +import { Serializable } from '../serializable'; +import { generateLongId, generateShortId } from '../utils/generate_id'; -export class ApmError extends Serializable { - constructor(fields: Fields) { +export class ApmError extends Serializable { + constructor(fields: ApmFields) { super({ ...fields, 'processor.event': 'error', diff --git a/packages/elastic-apm-synthtrace/src/lib/apm/apm_fields.ts b/packages/elastic-apm-synthtrace/src/lib/apm/apm_fields.ts new file mode 100644 index 000000000000..a7a826d144d0 --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/apm/apm_fields.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 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 { Fields } from '../entity'; + +export type ApmApplicationMetricFields = Partial<{ + 'system.process.memory.size': number; + 'system.memory.actual.free': number; + 'system.memory.total': number; + 'system.cpu.total.norm.pct': number; + 'system.process.memory.rss.bytes': number; + 'system.process.cpu.total.norm.pct': number; +}>; + +export type ApmUserAgentFields = Partial<{ + 'user_agent.original': string; + 'user_agent.os.name': string; + 'user_agent.name': string; + 'user_agent.device.name': string; + 'user_agent.version': number; +}>; + +export interface ApmException { + message: string; +} + +export type ApmFields = Fields & + Partial<{ + 'agent.name': string; + 'agent.version': string; + 'container.id': string; + 'ecs.version': string; + 'event.outcome': string; + 'event.ingested': number; + 'error.id': string; + 'error.exception': ApmException[]; + 'error.grouping_name': string; + 'error.grouping_key': string; + 'host.name': string; + 'kubernetes.pod.uid': string; + 'metricset.name': string; + 'observer.version': string; + 'observer.version_major': number; + 'parent.id': string; + 'processor.event': string; + 'processor.name': string; + 'trace.id': string; + 'transaction.name': string; + 'transaction.type': string; + 'transaction.id': string; + 'transaction.duration.us': number; + 'transaction.duration.histogram': { + values: number[]; + counts: number[]; + }; + 'transaction.sampled': true; + 'service.name': string; + 'service.environment': string; + 'service.node.name': string; + 'span.id': string; + 'span.name': string; + 'span.type': string; + 'span.subtype': string; + 'span.duration.us': number; + 'span.destination.service.name': string; + 'span.destination.service.resource': string; + 'span.destination.service.type': string; + 'span.destination.service.response_time.sum.us': number; + 'span.destination.service.response_time.count': number; + 'span.self_time.count': number; + 'span.self_time.sum.us': number; + }> & + ApmApplicationMetricFields; diff --git a/packages/elastic-apm-synthtrace/src/lib/base_span.ts b/packages/elastic-apm-synthtrace/src/lib/apm/base_span.ts similarity index 86% rename from packages/elastic-apm-synthtrace/src/lib/base_span.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/base_span.ts index f762bf730a71..ba2af8ce9ee5 100644 --- a/packages/elastic-apm-synthtrace/src/lib/base_span.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/base_span.ts @@ -6,16 +6,16 @@ * Side Public License, v 1. */ -import { Fields } from './entity'; -import { Serializable } from './serializable'; +import { Serializable } from '../serializable'; import { Span } from './span'; import { Transaction } from './transaction'; -import { generateLongId } from './utils/generate_id'; +import { generateLongId } from '../utils/generate_id'; +import { ApmFields } from './apm_fields'; -export class BaseSpan extends Serializable { +export class BaseSpan extends Serializable { private readonly _children: BaseSpan[] = []; - constructor(fields: Fields) { + constructor(fields: ApmFields) { super({ ...fields, 'event.outcome': 'unknown', @@ -60,7 +60,7 @@ export class BaseSpan extends Serializable { return this; } - serialize(): Fields[] { + serialize(): ApmFields[] { return [this.fields, ...this._children.flatMap((child) => child.serialize())]; } diff --git a/packages/elastic-apm-synthtrace/src/lib/browser.ts b/packages/elastic-apm-synthtrace/src/lib/apm/browser.ts similarity index 86% rename from packages/elastic-apm-synthtrace/src/lib/browser.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/browser.ts index 0fd8b44b6985..ebba6a0e89a6 100644 --- a/packages/elastic-apm-synthtrace/src/lib/browser.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/browser.ts @@ -6,11 +6,12 @@ * Side Public License, v 1. */ -import { Entity, UserAgentFields } from './entity'; +import { ApmFields, ApmUserAgentFields } from './apm_fields'; +import { Entity } from '../entity'; import { RumSpan } from './rum_span'; import { RumTransaction } from './rum_transaction'; -export class Browser extends Entity { +export class Browser extends Entity { transaction(transactionName: string, transactionType: string = 'page-load') { return new RumTransaction({ ...this.fields, @@ -29,7 +30,7 @@ export class Browser extends Entity { } } -export function browser(serviceName: string, production: string, userAgent: UserAgentFields) { +export function browser(serviceName: string, production: string, userAgent: ApmUserAgentFields) { return new Browser({ 'agent.name': 'rum-js', 'service.name': serviceName, diff --git a/packages/elastic-apm-synthtrace/src/lib/client/synthtrace_es_client.ts b/packages/elastic-apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client.ts similarity index 58% rename from packages/elastic-apm-synthtrace/src/lib/client/synthtrace_es_client.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client.ts index 546214f83c36..4a25d7009ad0 100644 --- a/packages/elastic-apm-synthtrace/src/lib/client/synthtrace_es_client.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client.ts @@ -7,45 +7,52 @@ */ import { Client } from '@elastic/elasticsearch'; -import { uploadEvents } from '../../scripts/utils/upload_events'; -import { Fields } from '../entity'; -import { cleanWriteTargets } from '../utils/clean_write_targets'; +import { uploadEvents } from '../../../scripts/utils/upload_events'; +import { Fields } from '../../entity'; +import { cleanWriteTargets } from '../../utils/clean_write_targets'; import { getBreakdownMetrics } from '../utils/get_breakdown_metrics'; import { getSpanDestinationMetrics } from '../utils/get_span_destination_metrics'; import { getTransactionMetrics } from '../utils/get_transaction_metrics'; -import { getWriteTargets } from '../utils/get_write_targets'; -import { Logger } from '../utils/logger'; +import { getApmWriteTargets } from '../utils/get_apm_write_targets'; +import { Logger } from '../../utils/create_logger'; +import { apmEventsToElasticsearchOutput } from '../utils/apm_events_to_elasticsearch_output'; -export class SynthtraceEsClient { +export class ApmSynthtraceEsClient { constructor(private readonly client: Client, private readonly logger: Logger) {} private getWriteTargets() { - return getWriteTargets({ client: this.client }); + return getApmWriteTargets({ client: this.client }); } clean() { return this.getWriteTargets().then((writeTargets) => - cleanWriteTargets({ client: this.client, writeTargets, logger: this.logger }) + cleanWriteTargets({ + client: this.client, + targets: Object.values(writeTargets), + logger: this.logger, + }) ); } async index(events: Fields[]) { - const eventsToIndex = [ - ...events, - ...getTransactionMetrics(events), - ...getSpanDestinationMetrics(events), - ...getBreakdownMetrics(events), - ]; - const writeTargets = await this.getWriteTargets(); + const eventsToIndex = apmEventsToElasticsearchOutput({ + events: [ + ...events, + ...getTransactionMetrics(events), + ...getSpanDestinationMetrics(events), + ...getBreakdownMetrics(events), + ], + writeTargets, + }); + await uploadEvents({ batchSize: 1000, client: this.client, clientWorkers: 5, events: eventsToIndex, logger: this.logger, - writeTargets, }); return this.client.indices.refresh({ diff --git a/packages/elastic-apm-synthtrace/src/lib/defaults/get_chrome_user_agent_defaults.ts b/packages/elastic-apm-synthtrace/src/lib/apm/defaults/get_chrome_user_agent_defaults.ts similarity index 84% rename from packages/elastic-apm-synthtrace/src/lib/defaults/get_chrome_user_agent_defaults.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/defaults/get_chrome_user_agent_defaults.ts index 0031456248c1..9a919e505185 100644 --- a/packages/elastic-apm-synthtrace/src/lib/defaults/get_chrome_user_agent_defaults.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/defaults/get_chrome_user_agent_defaults.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { UserAgentFields } from '../entity'; +import { ApmUserAgentFields } from '../../apm/apm_fields'; -export function getChromeUserAgentDefaults(): UserAgentFields { +export function getChromeUserAgentDefaults(): ApmUserAgentFields { return { 'user_agent.original': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36', diff --git a/packages/elastic-apm-synthtrace/src/lib/defaults/get_observer_defaults.ts b/packages/elastic-apm-synthtrace/src/lib/apm/defaults/get_observer_defaults.ts similarity index 82% rename from packages/elastic-apm-synthtrace/src/lib/defaults/get_observer_defaults.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/defaults/get_observer_defaults.ts index 67a4d5773b93..882029a50e47 100644 --- a/packages/elastic-apm-synthtrace/src/lib/defaults/get_observer_defaults.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/defaults/get_observer_defaults.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { Fields } from '../entity'; +import { ApmFields } from '../apm_fields'; -export function getObserverDefaults(): Fields { +export function getObserverDefaults(): ApmFields { return { 'observer.version': '7.16.0', 'observer.version_major': 7, diff --git a/packages/elastic-apm-synthtrace/src/lib/apm/index.ts b/packages/elastic-apm-synthtrace/src/lib/apm/index.ts new file mode 100644 index 000000000000..f020d9a1282e --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/apm/index.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 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 { service } from './service'; +import { browser } from './browser'; +import { getTransactionMetrics } from './utils/get_transaction_metrics'; +import { getSpanDestinationMetrics } from './utils/get_span_destination_metrics'; +import { getObserverDefaults } from './defaults/get_observer_defaults'; +import { getChromeUserAgentDefaults } from './defaults/get_chrome_user_agent_defaults'; +import { apmEventsToElasticsearchOutput } from './utils/apm_events_to_elasticsearch_output'; +import { getBreakdownMetrics } from './utils/get_breakdown_metrics'; +import { getApmWriteTargets } from './utils/get_apm_write_targets'; +import { ApmSynthtraceEsClient } from './client/apm_synthtrace_es_client'; + +import type { ApmException } from './apm_fields'; + +export const apm = { + service, + browser, + getTransactionMetrics, + getSpanDestinationMetrics, + getObserverDefaults, + getChromeUserAgentDefaults, + apmEventsToElasticsearchOutput, + getBreakdownMetrics, + getApmWriteTargets, + ApmSynthtraceEsClient, +}; + +export type { ApmSynthtraceEsClient, ApmException }; diff --git a/packages/elastic-apm-synthtrace/src/lib/instance.ts b/packages/elastic-apm-synthtrace/src/lib/apm/instance.ts similarity index 87% rename from packages/elastic-apm-synthtrace/src/lib/instance.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/instance.ts index 08444fde48ba..4051d7e8241d 100644 --- a/packages/elastic-apm-synthtrace/src/lib/instance.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/instance.ts @@ -7,12 +7,13 @@ */ import { ApmError } from './apm_error'; -import { ApplicationMetricFields, Entity } from './entity'; +import { Entity } from '../entity'; import { Metricset } from './metricset'; import { Span } from './span'; import { Transaction } from './transaction'; +import { ApmApplicationMetricFields, ApmFields } from './apm_fields'; -export class Instance extends Entity { +export class Instance extends Entity { transaction(transactionName: string, transactionType = 'request') { return new Transaction({ ...this.fields, @@ -43,7 +44,7 @@ export class Instance extends Entity { return this; } - appMetrics(metrics: ApplicationMetricFields) { + appMetrics(metrics: ApmApplicationMetricFields) { return new Metricset({ ...this.fields, 'metricset.name': 'app', diff --git a/packages/elastic-apm-synthtrace/src/lib/metricset.ts b/packages/elastic-apm-synthtrace/src/lib/apm/metricset.ts similarity index 72% rename from packages/elastic-apm-synthtrace/src/lib/metricset.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/metricset.ts index c1ebbea31312..88177e816a85 100644 --- a/packages/elastic-apm-synthtrace/src/lib/metricset.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/metricset.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import { Fields } from './entity'; -import { Serializable } from './serializable'; +import { Serializable } from '../serializable'; +import { ApmFields } from './apm_fields'; -export class Metricset extends Serializable { - constructor(fields: Fields) { +export class Metricset extends Serializable { + constructor(fields: ApmFields) { super({ 'processor.event': 'metric', 'processor.name': 'metric', diff --git a/packages/elastic-apm-synthtrace/src/lib/rum_span.ts b/packages/elastic-apm-synthtrace/src/lib/apm/rum_span.ts similarity index 100% rename from packages/elastic-apm-synthtrace/src/lib/rum_span.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/rum_span.ts diff --git a/packages/elastic-apm-synthtrace/src/lib/rum_transaction.ts b/packages/elastic-apm-synthtrace/src/lib/apm/rum_transaction.ts similarity index 100% rename from packages/elastic-apm-synthtrace/src/lib/rum_transaction.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/rum_transaction.ts diff --git a/packages/elastic-apm-synthtrace/src/lib/service.ts b/packages/elastic-apm-synthtrace/src/lib/apm/service.ts similarity index 85% rename from packages/elastic-apm-synthtrace/src/lib/service.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/service.ts index 859afa18aab0..16917821c7ee 100644 --- a/packages/elastic-apm-synthtrace/src/lib/service.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/service.ts @@ -6,10 +6,11 @@ * Side Public License, v 1. */ -import { Entity } from './entity'; +import { Entity } from '../entity'; +import { ApmFields } from './apm_fields'; import { Instance } from './instance'; -export class Service extends Entity { +export class Service extends Entity { instance(instanceName: string) { return new Instance({ ...this.fields, diff --git a/packages/elastic-apm-synthtrace/src/lib/span.ts b/packages/elastic-apm-synthtrace/src/lib/apm/span.ts similarity index 88% rename from packages/elastic-apm-synthtrace/src/lib/span.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/span.ts index 3c8d90f56b78..91cbacadf59c 100644 --- a/packages/elastic-apm-synthtrace/src/lib/span.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/span.ts @@ -7,11 +7,11 @@ */ import { BaseSpan } from './base_span'; -import { Fields } from './entity'; -import { generateShortId } from './utils/generate_id'; +import { generateShortId } from '../utils/generate_id'; +import { ApmFields } from './apm_fields'; export class Span extends BaseSpan { - constructor(fields: Fields) { + constructor(fields: ApmFields) { super({ ...fields, 'processor.event': 'span', diff --git a/packages/elastic-apm-synthtrace/src/lib/transaction.ts b/packages/elastic-apm-synthtrace/src/lib/apm/transaction.ts similarity index 93% rename from packages/elastic-apm-synthtrace/src/lib/transaction.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/transaction.ts index 3a8d32e1843f..47924e49e9b8 100644 --- a/packages/elastic-apm-synthtrace/src/lib/transaction.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/transaction.ts @@ -8,14 +8,14 @@ import { ApmError } from './apm_error'; import { BaseSpan } from './base_span'; -import { Fields } from './entity'; -import { generateShortId } from './utils/generate_id'; +import { generateShortId } from '../utils/generate_id'; +import { ApmFields } from './apm_fields'; export class Transaction extends BaseSpan { private _sampled: boolean = true; private readonly _errors: ApmError[] = []; - constructor(fields: Fields) { + constructor(fields: ApmFields) { super({ ...fields, 'processor.event': 'transaction', diff --git a/packages/elastic-apm-synthtrace/src/lib/utils/aggregate.ts b/packages/elastic-apm-synthtrace/src/lib/apm/utils/aggregate.ts similarity index 82% rename from packages/elastic-apm-synthtrace/src/lib/utils/aggregate.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/utils/aggregate.ts index 81b72f6fa01e..505f7452fe5d 100644 --- a/packages/elastic-apm-synthtrace/src/lib/utils/aggregate.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/utils/aggregate.ts @@ -8,15 +8,15 @@ import moment from 'moment'; import { pickBy } from 'lodash'; import objectHash from 'object-hash'; -import { Fields } from '../entity'; +import { ApmFields } from '../apm_fields'; import { createPicker } from './create_picker'; -export function aggregate(events: Fields[], fields: string[]) { +export function aggregate(events: ApmFields[], fields: string[]) { const picker = createPicker(fields); - const metricsets = new Map(); + const metricsets = new Map(); - function getMetricsetKey(span: Fields) { + function getMetricsetKey(span: ApmFields) { const timestamp = moment(span['@timestamp']).valueOf(); return { '@timestamp': timestamp - (timestamp % (60 * 1000)), diff --git a/packages/elastic-apm-synthtrace/src/lib/output/to_elasticsearch_output.ts b/packages/elastic-apm-synthtrace/src/lib/apm/utils/apm_events_to_elasticsearch_output.ts similarity index 57% rename from packages/elastic-apm-synthtrace/src/lib/output/to_elasticsearch_output.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/utils/apm_events_to_elasticsearch_output.ts index 016f1c5362fb..46456098df4a 100644 --- a/packages/elastic-apm-synthtrace/src/lib/output/to_elasticsearch_output.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/utils/apm_events_to_elasticsearch_output.ts @@ -6,16 +6,12 @@ * Side Public License, v 1. */ -import { set } from 'lodash'; -import { getObserverDefaults } from '../..'; -import { Fields } from '../entity'; +import { getObserverDefaults } from '../defaults/get_observer_defaults'; +import { ApmFields } from '../apm_fields'; +import { dedot } from '../../utils/dedot'; +import { ElasticsearchOutput } from '../../utils/to_elasticsearch_output'; -export interface ElasticsearchOutput { - _index: string; - _source: unknown; -} - -export interface ElasticsearchOutputWriteTargets { +export interface ApmElasticsearchOutputWriteTargets { transaction: string; span: string; error: string; @@ -30,16 +26,14 @@ const esDocumentDefaults = { }, }; -// eslint-disable-next-line guard-for-in -for (const key in observerDefaults) { - set(esDocumentDefaults, key, observerDefaults[key as keyof typeof observerDefaults]); -} -export function toElasticsearchOutput({ +dedot(observerDefaults, esDocumentDefaults); + +export function apmEventsToElasticsearchOutput({ events, writeTargets, }: { - events: Fields[]; - writeTargets: ElasticsearchOutputWriteTargets; + events: ApmFields[]; + writeTargets: ApmElasticsearchOutputWriteTargets; }): ElasticsearchOutput[] { return events.map((event) => { const values = {}; @@ -55,15 +49,12 @@ export function toElasticsearchOutput({ Object.assign(document, esDocumentDefaults); - // eslint-disable-next-line guard-for-in - for (const key in values) { - const val = values[key as keyof typeof values]; - set(document, key, val); - } + dedot(values, document); return { - _index: writeTargets[event['processor.event'] as keyof ElasticsearchOutputWriteTargets], + _index: writeTargets[event['processor.event'] as keyof ApmElasticsearchOutputWriteTargets], _source: document, + timestamp: event['@timestamp']!, }; }); } diff --git a/packages/elastic-apm-synthtrace/src/lib/utils/create_picker.ts b/packages/elastic-apm-synthtrace/src/lib/apm/utils/create_picker.ts similarity index 100% rename from packages/elastic-apm-synthtrace/src/lib/utils/create_picker.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/utils/create_picker.ts diff --git a/packages/elastic-apm-synthtrace/src/lib/utils/get_write_targets.ts b/packages/elastic-apm-synthtrace/src/lib/apm/utils/get_apm_write_targets.ts similarity index 90% rename from packages/elastic-apm-synthtrace/src/lib/utils/get_write_targets.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/utils/get_apm_write_targets.ts index fbe11d295e09..f040ca46a9db 100644 --- a/packages/elastic-apm-synthtrace/src/lib/utils/get_write_targets.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/utils/get_apm_write_targets.ts @@ -7,13 +7,13 @@ */ import { Client } from '@elastic/elasticsearch'; -import { ElasticsearchOutputWriteTargets } from '../../lib/output/to_elasticsearch_output'; +import { ApmElasticsearchOutputWriteTargets } from './apm_events_to_elasticsearch_output'; -export async function getWriteTargets({ +export async function getApmWriteTargets({ client, }: { client: Client; -}): Promise { +}): Promise { const [indicesResponse, datastreamsResponse] = await Promise.all([ client.indices.getAlias({ index: 'apm-*', diff --git a/packages/elastic-apm-synthtrace/src/lib/utils/get_breakdown_metrics.ts b/packages/elastic-apm-synthtrace/src/lib/apm/utils/get_breakdown_metrics.ts similarity index 95% rename from packages/elastic-apm-synthtrace/src/lib/utils/get_breakdown_metrics.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/utils/get_breakdown_metrics.ts index 8eae0941c6bd..4f29a31d5d27 100644 --- a/packages/elastic-apm-synthtrace/src/lib/utils/get_breakdown_metrics.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/utils/get_breakdown_metrics.ts @@ -7,7 +7,7 @@ */ import objectHash from 'object-hash'; import { groupBy, pickBy } from 'lodash'; -import { Fields } from '../entity'; +import { ApmFields } from '../apm_fields'; import { createPicker } from './create_picker'; const instanceFields = [ @@ -29,7 +29,7 @@ const metricsetPicker = createPicker([ 'span.subtype', ]); -export function getBreakdownMetrics(events: Fields[]) { +export function getBreakdownMetrics(events: ApmFields[]) { const txWithSpans = groupBy( events.filter( (event) => event['processor.event'] === 'span' || event['processor.event'] === 'transaction' @@ -37,13 +37,13 @@ export function getBreakdownMetrics(events: Fields[]) { (event) => event['transaction.id'] ); - const metricsets: Map = new Map(); + const metricsets: Map = new Map(); Object.keys(txWithSpans).forEach((transactionId) => { const txEvents = txWithSpans[transactionId]; const transaction = txEvents.find((event) => event['processor.event'] === 'transaction')!; - const eventsById: Record = {}; + const eventsById: Record = {}; const activityByParentId: Record> = {}; for (const event of txEvents) { const id = diff --git a/packages/elastic-apm-synthtrace/src/lib/utils/get_span_destination_metrics.ts b/packages/elastic-apm-synthtrace/src/lib/apm/utils/get_span_destination_metrics.ts similarity index 90% rename from packages/elastic-apm-synthtrace/src/lib/utils/get_span_destination_metrics.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/utils/get_span_destination_metrics.ts index decf2f71a9be..7adcdaa6ff94 100644 --- a/packages/elastic-apm-synthtrace/src/lib/utils/get_span_destination_metrics.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/utils/get_span_destination_metrics.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ -import { Fields } from '../entity'; +import { ApmFields } from '../apm_fields'; import { aggregate } from './aggregate'; -export function getSpanDestinationMetrics(events: Fields[]) { +export function getSpanDestinationMetrics(events: ApmFields[]) { const exitSpans = events.filter((event) => !!event['span.destination.service.resource']); const metricsets = aggregate(exitSpans, [ diff --git a/packages/elastic-apm-synthtrace/src/lib/utils/get_transaction_metrics.ts b/packages/elastic-apm-synthtrace/src/lib/apm/utils/get_transaction_metrics.ts similarity index 94% rename from packages/elastic-apm-synthtrace/src/lib/utils/get_transaction_metrics.ts rename to packages/elastic-apm-synthtrace/src/lib/apm/utils/get_transaction_metrics.ts index 4d46461c6dcc..1595e5895722 100644 --- a/packages/elastic-apm-synthtrace/src/lib/utils/get_transaction_metrics.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/utils/get_transaction_metrics.ts @@ -7,7 +7,7 @@ */ import { sortBy } from 'lodash'; -import { Fields } from '../entity'; +import { ApmFields } from '../apm_fields'; import { aggregate } from './aggregate'; function sortAndCompressHistogram(histogram?: { values: number[]; counts: number[] }) { @@ -28,7 +28,7 @@ function sortAndCompressHistogram(histogram?: { values: number[]; counts: number ); } -export function getTransactionMetrics(events: Fields[]) { +export function getTransactionMetrics(events: ApmFields[]) { const transactions = events .filter((event) => event['processor.event'] === 'transaction') .map((transaction) => { diff --git a/packages/elastic-apm-synthtrace/src/lib/entity.ts b/packages/elastic-apm-synthtrace/src/lib/entity.ts index c6e0c7193f8b..f1b11a3905df 100644 --- a/packages/elastic-apm-synthtrace/src/lib/entity.ts +++ b/packages/elastic-apm-synthtrace/src/lib/entity.ts @@ -6,83 +6,18 @@ * Side Public License, v 1. */ -export type ApplicationMetricFields = Partial<{ - 'system.process.memory.size': number; - 'system.memory.actual.free': number; - 'system.memory.total': number; - 'system.cpu.total.norm.pct': number; - 'system.process.memory.rss.bytes': number; - 'system.process.cpu.total.norm.pct': number; -}>; - -export type UserAgentFields = Partial<{ - 'user_agent.original': string; - 'user_agent.os.name': string; - 'user_agent.name': string; - 'user_agent.device.name': string; - 'user_agent.version': number; -}>; - -export interface Exception { - message: string; +export interface Fields { + '@timestamp'?: number; } -export type Fields = Partial<{ - '@timestamp': number; - 'agent.name': string; - 'agent.version': string; - 'container.id': string; - 'ecs.version': string; - 'event.outcome': string; - 'event.ingested': number; - 'error.id': string; - 'error.exception': Exception[]; - 'error.grouping_name': string; - 'error.grouping_key': string; - 'host.name': string; - 'kubernetes.pod.uid': string; - 'metricset.name': string; - 'observer.version': string; - 'observer.version_major': number; - 'parent.id': string; - 'processor.event': string; - 'processor.name': string; - 'trace.id': string; - 'transaction.name': string; - 'transaction.type': string; - 'transaction.id': string; - 'transaction.duration.us': number; - 'transaction.duration.histogram': { - values: number[]; - counts: number[]; - }; - 'transaction.sampled': true; - 'service.name': string; - 'service.environment': string; - 'service.node.name': string; - 'span.id': string; - 'span.name': string; - 'span.type': string; - 'span.subtype': string; - 'span.duration.us': number; - 'span.destination.service.name': string; - 'span.destination.service.resource': string; - 'span.destination.service.type': string; - 'span.destination.service.response_time.sum.us': number; - 'span.destination.service.response_time.count': number; - 'span.self_time.count': number; - 'span.self_time.sum.us': number; -}> & - ApplicationMetricFields; - -export class Entity { - constructor(public readonly fields: Fields) { +export class Entity { + constructor(public readonly fields: TFields) { this.fields = fields; } - defaults(defaults: Fields) { + defaults(defaults: TFields) { Object.keys(defaults).forEach((key) => { - const fieldName: keyof Fields = key as any; + const fieldName: keyof TFields = key as any; if (!(fieldName in this.fields)) { this.fields[fieldName] = defaults[fieldName] as any; diff --git a/packages/elastic-apm-synthtrace/src/lib/serializable.ts b/packages/elastic-apm-synthtrace/src/lib/serializable.ts index 3a92dc539855..e9ffe3ae9699 100644 --- a/packages/elastic-apm-synthtrace/src/lib/serializable.ts +++ b/packages/elastic-apm-synthtrace/src/lib/serializable.ts @@ -8,8 +8,8 @@ import { Entity, Fields } from './entity'; -export class Serializable extends Entity { - constructor(fields: Fields) { +export class Serializable extends Entity { + constructor(fields: TFields) { super({ ...fields, }); @@ -19,7 +19,7 @@ export class Serializable extends Entity { this.fields['@timestamp'] = time; return this; } - serialize(): Fields[] { + serialize(): TFields[] { return [this.fields]; } } diff --git a/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/cluster.ts b/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/cluster.ts new file mode 100644 index 000000000000..7a665522abba --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/cluster.ts @@ -0,0 +1,38 @@ +/* + * 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 { Entity } from '../entity'; +import { generateShortId } from '../utils/generate_id'; +import { Kibana } from './kibana'; +import { StackMonitoringFields } from './stack_monitoring_fields'; +import { ClusterStats } from './cluster_stats'; + +export class Cluster extends Entity { + kibana(name: string, index: string = '.kibana') { + return new Kibana({ + cluster_uuid: this.fields.cluster_uuid, + 'kibana_stats.kibana.name': name, + 'kibana_stats.kibana.uuid': generateShortId(), + 'kibana_stats.kibana.index': index, + type: 'kibana_stats', + }); + } + + stats() { + return new ClusterStats({ + ...this.fields, + }); + } +} + +export function cluster(name: string) { + return new Cluster({ + cluster_name: name, + cluster_uuid: generateShortId(), + }); +} diff --git a/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/cluster_stats.ts b/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/cluster_stats.ts new file mode 100644 index 000000000000..0995013cbcbb --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/cluster_stats.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Serializable } from '../serializable'; +import { StackMonitoringFields } from './stack_monitoring_fields'; + +export class ClusterStats extends Serializable { + constructor(fields: StackMonitoringFields) { + super(fields); + + this.fields.type = 'cluster_stats'; + this.fields['license.status'] = 'active'; + } + + timestamp(timestamp: number) { + super.timestamp(timestamp); + this.fields['cluster_stats.timestamp'] = new Date(timestamp).toISOString(); + return this; + } + + indices(count: number) { + this.fields['cluster_stats.indices.count'] = count; + return this; + } +} diff --git a/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/index.ts b/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/index.ts new file mode 100644 index 000000000000..ee926269ea36 --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { cluster } from './cluster'; + +export const stackMonitoring = { + cluster, +}; diff --git a/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/kibana.ts b/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/kibana.ts new file mode 100644 index 000000000000..fec244bc19dc --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/kibana.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Serializable } from '../serializable'; +import { StackMonitoringFields } from './stack_monitoring_fields'; +import { KibanaStats } from './kibana_stats'; + +export class Kibana extends Serializable { + stats() { + return new KibanaStats({ + ...this.fields, + }); + } +} diff --git a/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/kibana_stats.ts b/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/kibana_stats.ts new file mode 100644 index 000000000000..495e5f013600 --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/kibana_stats.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { Serializable } from '../serializable'; +import { StackMonitoringFields } from './stack_monitoring_fields'; + +export class KibanaStats extends Serializable { + timestamp(timestamp: number) { + super.timestamp(timestamp); + this.fields['kibana_stats.timestamp'] = new Date(timestamp).toISOString(); + this.fields['kibana_stats.response_times.max'] = 250; + this.fields['kibana_stats.kibana.status'] = 'green'; + return this; + } + + requests(disconnects: number, total: number) { + this.fields['kibana_stats.requests.disconnects'] = disconnects; + this.fields['kibana_stats.requests.total'] = total; + return this; + } +} diff --git a/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/stack_monitoring_fields.ts b/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/stack_monitoring_fields.ts new file mode 100644 index 000000000000..3e80d1e9f733 --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/stack_monitoring/stack_monitoring_fields.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 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 { Fields } from '../entity'; + +export type StackMonitoringFields = Fields & + Partial<{ + cluster_name: string; + cluster_uuid: string; + type: string; + + 'cluster_stats.timestamp': string; + 'cluster_stats.indices.count': number; + 'license.status': string; + + 'kibana_stats.kibana.name': string; + 'kibana_stats.kibana.uuid': string; + 'kibana_stats.kibana.status': string; + 'kibana_stats.kibana.index': string; + 'kibana_stats.requests.disconnects': number; + 'kibana_stats.requests.total': number; + 'kibana_stats.timestamp': string; + 'kibana_stats.response_times.max': number; + }>; diff --git a/packages/elastic-apm-synthtrace/src/lib/utils/clean_write_targets.ts b/packages/elastic-apm-synthtrace/src/lib/utils/clean_write_targets.ts index 4a2ab281a284..91b8e0084b27 100644 --- a/packages/elastic-apm-synthtrace/src/lib/utils/clean_write_targets.ts +++ b/packages/elastic-apm-synthtrace/src/lib/utils/clean_write_targets.ts @@ -7,20 +7,17 @@ */ import { Client } from '@elastic/elasticsearch'; -import { ElasticsearchOutputWriteTargets } from '../../lib/output/to_elasticsearch_output'; -import { Logger } from './logger'; +import { Logger } from './create_logger'; export async function cleanWriteTargets({ - writeTargets, + targets, client, logger, }: { - writeTargets: ElasticsearchOutputWriteTargets; + targets: string[]; client: Client; logger: Logger; }) { - const targets = Object.values(writeTargets); - logger.info(`Cleaning indices: ${targets.join(', ')}`); const response = await client.deleteByQuery({ diff --git a/packages/elastic-apm-synthtrace/src/lib/utils/dedot.ts b/packages/elastic-apm-synthtrace/src/lib/utils/dedot.ts new file mode 100644 index 000000000000..4f38a7025f3b --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/utils/dedot.ts @@ -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. + */ +import { set } from 'lodash'; + +export function dedot(source: Record, target: Record) { + // eslint-disable-next-line guard-for-in + for (const key in source) { + const val = source[key as keyof typeof source]; + set(target, key, val); + } +} diff --git a/packages/elastic-apm-synthtrace/src/lib/utils/logger.ts b/packages/elastic-apm-synthtrace/src/lib/utils/logger.ts deleted file mode 100644 index 4afdda74105c..000000000000 --- a/packages/elastic-apm-synthtrace/src/lib/utils/logger.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 { isPromise } from 'util/types'; - -export enum LogLevel { - trace = 0, - debug = 1, - info = 2, - error = 3, -} - -function getTimeString() { - return `[${new Date().toLocaleTimeString()}]`; -} - -export function createLogger(logLevel: LogLevel) { - function logPerf(name: string, start: bigint) { - // eslint-disable-next-line no-console - console.debug( - getTimeString(), - `${name}: ${Number(process.hrtime.bigint() - start) / 1000000}ms` - ); - } - return { - perf: (name: string, cb: () => T): T => { - if (logLevel <= LogLevel.trace) { - const start = process.hrtime.bigint(); - const val = cb(); - if (isPromise(val)) { - val.then(() => { - logPerf(name, start); - }); - } else { - logPerf(name, start); - } - return val; - } - return cb(); - }, - debug: (...args: any[]) => { - if (logLevel <= LogLevel.debug) { - // eslint-disable-next-line no-console - console.debug(getTimeString(), ...args); - } - }, - info: (...args: any[]) => { - if (logLevel <= LogLevel.info) { - // eslint-disable-next-line no-console - console.log(getTimeString(), ...args); - } - }, - error: (...args: any[]) => { - if (logLevel <= LogLevel.error) { - // eslint-disable-next-line no-console - console.log(getTimeString(), ...args); - } - }, - }; -} - -export type Logger = ReturnType; diff --git a/packages/elastic-apm-synthtrace/src/lib/utils/to_elasticsearch_output.ts b/packages/elastic-apm-synthtrace/src/lib/utils/to_elasticsearch_output.ts new file mode 100644 index 000000000000..58bafffaff69 --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/utils/to_elasticsearch_output.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Fields } from '../entity'; +import { dedot } from './dedot'; + +export interface ElasticsearchOutput { + _index: string; + _source: unknown; + timestamp: number; +} + +export function eventsToElasticsearchOutput({ + events, + writeTarget, +}: { + events: Fields[]; + writeTarget: string; +}): ElasticsearchOutput[] { + return events.map((event) => { + const values = {}; + + const timestamp = event['@timestamp']!; + + Object.assign(values, event, { + '@timestamp': new Date(timestamp).toISOString(), + }); + + const document = {}; + + dedot(values, document); + + return { + _index: writeTarget, + _source: document, + timestamp, + }; + }); +} diff --git a/packages/elastic-apm-synthtrace/src/scripts/examples/01_simple_trace.ts b/packages/elastic-apm-synthtrace/src/scripts/examples/01_simple_trace.ts index 8c1f24bd5e64..559c636cfaee 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/examples/01_simple_trace.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/examples/01_simple_trace.ts @@ -6,75 +6,103 @@ * Side Public License, v 1. */ -import { service, timerange, getTransactionMetrics, getSpanDestinationMetrics } from '../..'; -import { getBreakdownMetrics } from '../../lib/utils/get_breakdown_metrics'; +import { apm, timerange } from '../../index'; +import { apmEventsToElasticsearchOutput } from '../../lib/apm/utils/apm_events_to_elasticsearch_output'; +import { getApmWriteTargets } from '../../lib/apm/utils/get_apm_write_targets'; +import { Scenario } from '../scenario'; +import { getCommonServices } from '../utils/get_common_services'; -export default function ({ from, to }: { from: number; to: number }) { - const numServices = 3; +const scenario: Scenario = async ({ target, logLevel }) => { + const { client, logger } = getCommonServices({ target, logLevel }); + const writeTargets = await getApmWriteTargets({ client }); - const range = timerange(from, to); + return { + generate: ({ from, to }) => { + const numServices = 3; - const transactionName = '240rpm/75% 1000ms'; + const range = timerange(from, to); - const successfulTimestamps = range.interval('1s').rate(3); + const transactionName = '240rpm/75% 1000ms'; - const failedTimestamps = range.interval('1s').rate(1); + const successfulTimestamps = range.interval('1s').rate(3); - return new Array(numServices).fill(undefined).flatMap((_, index) => { - const instance = service(`opbeans-go-${index}`, 'production', 'go').instance('instance'); + const failedTimestamps = range.interval('1s').rate(1); - const successfulTraceEvents = successfulTimestamps.flatMap((timestamp) => - instance - .transaction(transactionName) - .timestamp(timestamp) - .duration(1000) - .success() - .children( - instance - .span('GET apm-*/_search', 'db', 'elasticsearch') - .duration(1000) - .success() - .destination('elasticsearch') - .timestamp(timestamp), - instance.span('custom_operation', 'custom').duration(100).success().timestamp(timestamp) - ) - .serialize() - ); + return new Array(numServices).fill(undefined).flatMap((_, index) => { + const events = logger.perf('generating_apm_events', () => { + const instance = apm + .service(`opbeans-go-${index}`, 'production', 'go') + .instance('instance'); - const failedTraceEvents = failedTimestamps.flatMap((timestamp) => - instance - .transaction(transactionName) - .timestamp(timestamp) - .duration(1000) - .failure() - .errors( - instance.error('[ResponseError] index_not_found_exception').timestamp(timestamp + 50) - ) - .serialize() - ); + const successfulTraceEvents = successfulTimestamps.flatMap((timestamp) => + instance + .transaction(transactionName) + .timestamp(timestamp) + .duration(1000) + .success() + .children( + instance + .span('GET apm-*/_search', 'db', 'elasticsearch') + .duration(1000) + .success() + .destination('elasticsearch') + .timestamp(timestamp), + instance + .span('custom_operation', 'custom') + .duration(100) + .success() + .timestamp(timestamp) + ) + .serialize() + ); - const metricsets = range - .interval('30s') - .rate(1) - .flatMap((timestamp) => - instance - .appMetrics({ - 'system.memory.actual.free': 800, - 'system.memory.total': 1000, - 'system.cpu.total.norm.pct': 0.6, - 'system.process.cpu.total.norm.pct': 0.7, + const failedTraceEvents = failedTimestamps.flatMap((timestamp) => + instance + .transaction(transactionName) + .timestamp(timestamp) + .duration(1000) + .failure() + .errors( + instance + .error('[ResponseError] index_not_found_exception') + .timestamp(timestamp + 50) + ) + .serialize() + ); + + const metricsets = range + .interval('30s') + .rate(1) + .flatMap((timestamp) => + instance + .appMetrics({ + 'system.memory.actual.free': 800, + 'system.memory.total': 1000, + 'system.cpu.total.norm.pct': 0.6, + 'system.process.cpu.total.norm.pct': 0.7, + }) + .timestamp(timestamp) + .serialize() + ); + return [...successfulTraceEvents, ...failedTraceEvents, ...metricsets]; + }); + + return logger.perf('apm_events_to_es_output', () => + apmEventsToElasticsearchOutput({ + events: [ + ...events, + ...logger.perf('get_transaction_metrics', () => apm.getTransactionMetrics(events)), + ...logger.perf('get_span_destination_metrics', () => + apm.getSpanDestinationMetrics(events) + ), + ...logger.perf('get_breakdown_metrics', () => apm.getBreakdownMetrics(events)), + ], + writeTargets, }) - .timestamp(timestamp) - .serialize() - ); - const events = successfulTraceEvents.concat(failedTraceEvents); + ); + }); + }, + }; +}; - return [ - ...events, - ...metricsets, - ...getTransactionMetrics(events), - ...getSpanDestinationMetrics(events), - ...getBreakdownMetrics(events), - ]; - }); -} +export default scenario; diff --git a/packages/elastic-apm-synthtrace/src/scripts/examples/02_kibana_stats.ts b/packages/elastic-apm-synthtrace/src/scripts/examples/02_kibana_stats.ts new file mode 100644 index 000000000000..2ba3c4a29c52 --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/scripts/examples/02_kibana_stats.ts @@ -0,0 +1,48 @@ +/* + * 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 { stackMonitoring, timerange } from '../../index'; +import { eventsToElasticsearchOutput } from '../../lib/utils/to_elasticsearch_output'; +import { Scenario } from '../scenario'; +import { getCommonServices } from '../utils/get_common_services'; + +const scenario: Scenario = async ({ target, writeTarget, logLevel }) => { + const { logger } = getCommonServices({ target, logLevel }); + + if (!writeTarget) { + throw new Error('Write target is not defined'); + } + + return { + generate: ({ from, to }) => { + const kibanaStats = stackMonitoring.cluster('cluster-01').kibana('kibana-01').stats(); + + const range = timerange(from, to); + return range + .interval('30s') + .rate(1) + .flatMap((timestamp) => { + const events = logger.perf('generating_sm_events', () => { + return kibanaStats.timestamp(timestamp).requests(10, 20).serialize(); + }); + + return logger.perf('sm_events_to_es_output', () => { + const smEvents = eventsToElasticsearchOutput({ events, writeTarget }); + smEvents.forEach((event: any) => { + const ts = event._source['@timestamp']; + delete event._source['@timestamp']; + event._source.timestamp = ts; + }); + return smEvents; + }); + }); + }, + }; +}; + +export default scenario; diff --git a/packages/elastic-apm-synthtrace/src/scripts/examples/03_monitoring.ts b/packages/elastic-apm-synthtrace/src/scripts/examples/03_monitoring.ts new file mode 100644 index 000000000000..53dcd820f551 --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/scripts/examples/03_monitoring.ts @@ -0,0 +1,70 @@ +/* + * 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. + */ + +// Run with: node ./src/scripts/run ./src/scripts/examples/03_monitoring.ts --target=http://elastic:changeme@localhost:9200 + +import { stackMonitoring, timerange } from '../../index'; +import { + ElasticsearchOutput, + eventsToElasticsearchOutput, +} from '../../lib/utils/to_elasticsearch_output'; +import { Scenario } from '../scenario'; +import { getCommonServices } from '../utils/get_common_services'; +import { StackMonitoringFields } from '../../lib/stack_monitoring/stack_monitoring_fields'; + +// TODO (mat): move this into a function like utils/apm_events_to_elasticsearch_output.ts +function smEventsToElasticsearchOutput( + events: StackMonitoringFields[], + writeTarget: string +): ElasticsearchOutput[] { + const smEvents = eventsToElasticsearchOutput({ events, writeTarget }); + smEvents.forEach((event: any) => { + const ts = event._source['@timestamp']; + delete event._source['@timestamp']; + event._source.timestamp = ts; + }); + return smEvents; +} + +const scenario: Scenario = async ({ target, logLevel }) => { + const { logger } = getCommonServices({ target, logLevel }); + + return { + generate: ({ from, to }) => { + const cluster = stackMonitoring.cluster('test-cluster'); + const clusterStats = cluster.stats(); + const kibanaStats = cluster.kibana('kibana-01').stats(); + + const range = timerange(from, to); + return range + .interval('10s') + .rate(1) + .flatMap((timestamp) => { + const clusterEvents = logger.perf('generating_es_events', () => { + return clusterStats.timestamp(timestamp).indices(115).serialize(); + }); + const clusterOutputs = smEventsToElasticsearchOutput( + clusterEvents, + '.monitoring-es-7-synthtrace' + ); + + const kibanaEvents = logger.perf('generating_kb_events', () => { + return kibanaStats.timestamp(timestamp).requests(10, 20).serialize(); + }); + const kibanaOutputs = smEventsToElasticsearchOutput( + kibanaEvents, + '.monitoring-kibana-7-synthtrace' + ); + + return [...clusterOutputs, ...kibanaOutputs]; + }); + }, + }; +}; + +export default scenario; diff --git a/packages/elastic-apm-synthtrace/src/scripts/run.ts b/packages/elastic-apm-synthtrace/src/scripts/run.ts index aa427d8e211a..4078c848aa48 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/run.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/run.ts @@ -7,136 +7,109 @@ */ import datemath from '@elastic/datemath'; import yargs from 'yargs/yargs'; -import { cleanWriteTargets } from '../lib/utils/clean_write_targets'; +import { Argv } from 'yargs'; 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'; +import { parseRunCliFlags } from './utils/parse_run_cli_flags'; +import { getCommonServices } from './utils/get_common_services'; -yargs(process.argv.slice(2)) - .command( - '*', - 'Generate data and index into Elasticsearch', - (y) => { - return y - .positional('file', { - describe: 'File that contains the trace scenario', - demandOption: true, - string: true, - }) - .option('target', { - describe: 'Elasticsearch target, including username/password', - demandOption: true, - string: true, - }) - .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, - }) - .option('clean', { - describe: 'Clean APM indices before indexing new data', - default: false, - boolean: true, - }) - .option('workers', { - describe: 'Amount of Node.js worker threads', - default: 5, - }) - .option('bucketSize', { - describe: 'Size of bucket for which to generate data', - default: '15m', - }) - .option('interval', { - describe: 'The interval at which to index data', - default: '10s', - }) - .option('clientWorkers', { - describe: 'Number of concurrently connected ES clients', - default: 5, - }) - .option('batchSize', { - describe: 'Number of documents per bulk index request', - default: 1000, - }) - .option('logLevel', { - describe: 'Log level', - default: 'info', - }) - .conflicts('to', 'live'); - }, - async (argv) => { - const file = String(argv.file || argv._[0]); +function options(y: Argv) { + return y + .positional('file', { + describe: 'File that contains the trace scenario', + demandOption: true, + string: true, + }) + .option('target', { + describe: 'Elasticsearch target, including username/password', + demandOption: true, + string: true, + }) + .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, + }) + .option('clean', { + describe: 'Clean APM indices before indexing new data', + default: false, + boolean: true, + }) + .option('workers', { + describe: 'Amount of Node.js worker threads', + default: 5, + }) + .option('bucketSize', { + describe: 'Size of bucket for which to generate data', + default: '15m', + }) + .option('interval', { + describe: 'The interval at which to index data', + default: '10s', + }) + .option('clientWorkers', { + describe: 'Number of concurrently connected ES clients', + default: 5, + }) + .option('batchSize', { + describe: 'Number of documents per bulk index request', + default: 1000, + }) + .option('logLevel', { + describe: 'Log level', + default: 'info', + }) + .option('writeTarget', { + describe: 'Target to index', + string: true, + }) + .conflicts('to', 'live'); +} + +export type RunCliFlags = ReturnType['argv']; - const { target, workers, clean, clientWorkers, batchSize } = argv; +yargs(process.argv.slice(2)) + .command('*', 'Generate data and index into Elasticsearch', options, async (argv) => { + const runOptions = parseRunCliFlags(argv); - const { scenario, intervalInMs, bucketSizeInMs, logger, writeTargets, client, logLevel } = - await getCommonResources({ - ...argv, - file, - }); + const { logger } = getCommonServices(runOptions); - 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 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; - const live = argv.live; + logger.info( + `Starting data generation\n: ${JSON.stringify( + { + ...runOptions, + from: new Date(from).toISOString(), + to: new Date(to).toISOString(), + }, + null, + 2 + )}` + ); - 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({ + ...runOptions, + from, + to, + }); - startHistoricalDataUpload({ - from, - to, - file, - bucketSizeInMs, - client, - workers, - clientWorkers, - batchSize, - writeTargets, - logger, - logLevel, - target, + if (live) { + startLiveDataUpload({ + ...runOptions, + start: to, }); - - if (live) { - startLiveDataUpload({ - bucketSizeInMs, - client, - intervalInMs, - logger, - scenario, - start: to, - clientWorkers, - batchSize, - writeTargets, - }); - } } - ) + }) .parse(); diff --git a/packages/elastic-apm-synthtrace/src/scripts/scenario.ts b/packages/elastic-apm-synthtrace/src/scripts/scenario.ts new file mode 100644 index 000000000000..c134c08cd835 --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/scripts/scenario.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 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 { ElasticsearchOutput } from '../lib/utils/to_elasticsearch_output'; +import { RunOptions } from './utils/parse_run_cli_flags'; + +type Generate = (range: { from: number; to: number }) => ElasticsearchOutput[]; +export type Scenario = (options: RunOptions) => Promise<{ generate: Generate }>; diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/get_common_services.ts b/packages/elastic-apm-synthtrace/src/scripts/utils/get_common_services.ts new file mode 100644 index 000000000000..0dee6dbc951e --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/get_common_services.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 { Client } from '@elastic/elasticsearch'; +import { createLogger, LogLevel } from '../../lib/utils/create_logger'; + +export function getCommonServices({ target, logLevel }: { target: string; logLevel: LogLevel }) { + const client = new Client({ + node: target, + }); + + const logger = createLogger(logLevel); + + return { + logger, + client, + }; +} + +export type RunServices = ReturnType; diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/get_scenario.ts b/packages/elastic-apm-synthtrace/src/scripts/utils/get_scenario.ts index f8c59cff4feb..43f9e4f5e998 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/utils/get_scenario.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/get_scenario.ts @@ -6,10 +6,8 @@ * Side Public License, v 1. */ import Path from 'path'; -import { Fields } from '../../lib/entity'; import { Logger } from '../../lib/utils/create_logger'; - -export type Scenario = (options: { from: number; to: number }) => Fields[]; +import { Scenario } from '../scenario'; export function getScenario({ file, logger }: { file: unknown; logger: Logger }) { const location = Path.join(process.cwd(), String(file)); diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/get_common_resources.ts b/packages/elastic-apm-synthtrace/src/scripts/utils/parse_run_cli_flags.ts similarity index 59% rename from packages/elastic-apm-synthtrace/src/scripts/utils/get_common_resources.ts rename to packages/elastic-apm-synthtrace/src/scripts/utils/parse_run_cli_flags.ts index baa1d8758c3c..5c081707bb75 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/utils/get_common_resources.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/parse_run_cli_flags.ts @@ -6,25 +6,16 @@ * Side Public License, v 1. */ -import { Client } from '@elastic/elasticsearch'; -import { getScenario } from './get_scenario'; -import { getWriteTargets } from '../../lib/utils/get_write_targets'; +import { pick } from 'lodash'; +import { LogLevel } from '../../lib/utils/create_logger'; +import { RunCliFlags } from '../run'; import { intervalToMs } from './interval_to_ms'; -import { createLogger, LogLevel } from '../../lib/utils/create_logger'; -export async function getCommonResources({ - file, - interval, - bucketSize, - target, - logLevel, -}: { - file: string; - interval: string; - bucketSize: string; - target: string; - logLevel: string; -}) { +export function parseRunCliFlags(flags: RunCliFlags) { + const { file, _, logLevel, interval, bucketSize } = flags; + + const parsedFile = String(file || _[0]); + let parsedLogLevel = LogLevel.info; switch (logLevel) { case 'trace': @@ -44,8 +35,6 @@ export async function getCommonResources({ break; } - const logger = createLogger(parsedLogLevel); - const intervalInMs = intervalToMs(interval); if (!intervalInMs) { throw new Error('Invalid interval'); @@ -57,22 +46,13 @@ export async function getCommonResources({ throw new Error('Invalid bucket size'); } - const client = new Client({ - node: target, - }); - - const [scenario, writeTargets] = await Promise.all([ - getScenario({ file, logger }), - getWriteTargets({ client }), - ]); - return { - scenario, - writeTargets, - logger, - client, + ...pick(flags, 'target', 'workers', 'clientWorkers', 'batchSize', 'writeTarget'), intervalInMs, bucketSizeInMs, logLevel: parsedLogLevel, + file: parsedFile, }; } + +export type RunOptions = ReturnType; diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/start_historical_data_upload.ts b/packages/elastic-apm-synthtrace/src/scripts/utils/start_historical_data_upload.ts index dc568170a974..dd848d9f66c6 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/utils/start_historical_data_upload.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/start_historical_data_upload.ts @@ -5,41 +5,30 @@ * 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 pLimit from 'p-limit'; import Path from 'path'; import { Worker } from 'worker_threads'; -import { ElasticsearchOutputWriteTargets } from '../../lib/output/to_elasticsearch_output'; -import { Logger, LogLevel } from '../../lib/utils/create_logger'; +import { getCommonServices } from './get_common_services'; +import { RunOptions } from './parse_run_cli_flags'; +import { WorkerData } from './upload_next_batch'; export async function startHistoricalDataUpload({ from, to, + intervalInMs, bucketSizeInMs, workers, clientWorkers, batchSize, - writeTargets, logLevel, - logger, target, file, -}: { - from: number; - to: number; - bucketSizeInMs: number; - client: Client; - workers: number; - clientWorkers: number; - batchSize: number; - writeTargets: ElasticsearchOutputWriteTargets; - logger: Logger; - logLevel: LogLevel; - target: string; - file: string; -}) { + writeTarget, +}: RunOptions & { from: number; to: number }) { let requestedUntil: number = from; + const { logger } = getCommonServices({ target, logLevel }); + function processNextBatch() { const bucketFrom = requestedUntil; const bucketTo = Math.min(to, bucketFrom + bucketSizeInMs); @@ -56,17 +45,22 @@ export async function startHistoricalDataUpload({ ).toISOString()}` ); + const workerData: WorkerData = { + bucketFrom, + bucketTo, + file, + logLevel, + batchSize, + bucketSizeInMs, + clientWorkers, + intervalInMs, + target, + workers, + writeTarget, + }; + const worker = new Worker(Path.join(__dirname, './upload_next_batch.js'), { - workerData: { - bucketFrom, - bucketTo, - logLevel, - writeTargets, - target, - file, - clientWorkers, - batchSize, - }, + workerData, }); logger.perf('created_worker', () => { diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/start_live_data_upload.ts b/packages/elastic-apm-synthtrace/src/scripts/utils/start_live_data_upload.ts index cec0970420d1..3610ffae3c7e 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/utils/start_live_data_upload.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/start_live_data_upload.ts @@ -6,44 +6,49 @@ * 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 '../../lib/utils/create_logger'; +import { getScenario } from './get_scenario'; import { uploadEvents } from './upload_events'; +import { RunOptions } from './parse_run_cli_flags'; +import { getCommonServices } from './get_common_services'; +import { ElasticsearchOutput } from '../../lib/utils/to_elasticsearch_output'; -export function startLiveDataUpload({ +export async function startLiveDataUpload({ + file, start, bucketSizeInMs, intervalInMs, clientWorkers, batchSize, - writeTargets, - scenario, - client, - logger, -}: { - start: number; - bucketSizeInMs: number; - intervalInMs: number; - clientWorkers: number; - batchSize: number; - writeTargets: ElasticsearchOutputWriteTargets; - scenario: Scenario; - client: Client; - logger: Logger; -}) { - let queuedEvents: Fields[] = []; + target, + logLevel, + workers, + writeTarget, +}: RunOptions & { start: number }) { + let queuedEvents: ElasticsearchOutput[] = []; let requestedUntil: number = start; + const { logger, client } = getCommonServices({ target, logLevel }); + + const scenario = await getScenario({ file, logger }); + const { generate } = await scenario({ + batchSize, + bucketSizeInMs, + clientWorkers, + file, + intervalInMs, + logLevel, + target, + workers, + writeTarget, + }); + function uploadNextBatch() { const end = new Date().getTime(); if (end > requestedUntil) { const bucketFrom = requestedUntil; const bucketTo = requestedUntil + bucketSizeInMs; - const nextEvents = scenario({ from: bucketFrom, to: bucketTo }); + const nextEvents = generate({ from: bucketFrom, to: bucketTo }); logger.debug( `Requesting ${new Date(bucketFrom).toISOString()} to ${new Date( bucketTo @@ -55,7 +60,7 @@ export function startLiveDataUpload({ const [eventsToUpload, eventsToRemainInQueue] = partition( queuedEvents, - (event) => event['@timestamp']! <= end + (event) => event.timestamp <= end ); logger.info(`Uploading until ${new Date(end).toISOString()}, events: ${eventsToUpload.length}`); @@ -64,11 +69,10 @@ export function startLiveDataUpload({ uploadEvents({ events: eventsToUpload, - client, clientWorkers, batchSize, - writeTargets, logger, + client, }); } diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/upload_events.ts b/packages/elastic-apm-synthtrace/src/scripts/utils/upload_events.ts index 738294852598..d68a1b88132b 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/utils/upload_events.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/upload_events.ts @@ -9,11 +9,7 @@ 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 { ElasticsearchOutput } from '../../lib/utils/to_elasticsearch_output'; import { Logger } from '../../lib/utils/create_logger'; export function uploadEvents({ @@ -21,24 +17,23 @@ export function uploadEvents({ client, clientWorkers, batchSize, - writeTargets, logger, }: { - events: Fields[]; + events: ElasticsearchOutput[]; client: Client; clientWorkers: number; batchSize: number; - writeTargets: ElasticsearchOutputWriteTargets; logger: Logger; }) { - const esDocuments = logger.perf('to_elasticsearch_output', () => { - return toElasticsearchOutput({ events, writeTargets }); - }); const fn = pLimit(clientWorkers); - const batches = chunk(esDocuments, batchSize); + const batches = chunk(events, batchSize); + + if (!batches.length) { + return; + } - logger.debug(`Uploading ${esDocuments.length} in ${batches.length} batches`); + logger.debug(`Uploading ${events.length} in ${batches.length} batches`); const time = new Date().getTime(); @@ -47,7 +42,6 @@ export function uploadEvents({ fn(() => { return logger.perf('bulk_upload', () => client.bulk({ - require_alias: true, refresh: false, body: batch.flatMap((doc) => { return [{ index: { _index: doc._index } }, doc._source]; diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/upload_next_batch.ts b/packages/elastic-apm-synthtrace/src/scripts/utils/upload_next_batch.ts index 2fe5f9b6a6d6..c25fc7ca9f1c 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/utils/upload_next_batch.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/upload_next_batch.ts @@ -9,22 +9,37 @@ // add this to workerExample.js file. import { Client } from '@elastic/elasticsearch'; import { workerData } from 'worker_threads'; -import { ElasticsearchOutputWriteTargets } from '../../lib/output/to_elasticsearch_output'; import { getScenario } from './get_scenario'; import { createLogger, LogLevel } from '../../lib/utils/create_logger'; import { uploadEvents } from './upload_events'; -const { bucketFrom, bucketTo, file, logLevel, target, writeTargets, clientWorkers, batchSize } = - workerData as { - bucketFrom: number; - bucketTo: number; - file: string; - logLevel: LogLevel; - target: string; - writeTargets: ElasticsearchOutputWriteTargets; - clientWorkers: number; - batchSize: number; - }; +export interface WorkerData { + bucketFrom: number; + bucketTo: number; + file: string; + logLevel: LogLevel; + clientWorkers: number; + batchSize: number; + intervalInMs: number; + bucketSizeInMs: number; + target: string; + workers: number; + writeTarget?: string; +} + +const { + bucketFrom, + bucketTo, + file, + logLevel, + clientWorkers, + batchSize, + intervalInMs, + bucketSizeInMs, + workers, + target, + writeTarget, +} = workerData as WorkerData; async function uploadNextBatch() { if (bucketFrom === bucketTo) { @@ -38,8 +53,20 @@ async function uploadNextBatch() { const scenario = await logger.perf('get_scenario', () => getScenario({ file, logger })); + const { generate } = await scenario({ + intervalInMs, + bucketSizeInMs, + logLevel, + file, + clientWorkers, + batchSize, + target, + workers, + writeTarget, + }); + const events = logger.perf('execute_scenario', () => - scenario({ from: bucketFrom, to: bucketTo }) + generate({ from: bucketFrom, to: bucketTo }) ); return uploadEvents({ @@ -47,7 +74,6 @@ async function uploadNextBatch() { client, clientWorkers, batchSize, - writeTargets, logger, }); } @@ -56,6 +82,11 @@ uploadNextBatch() .then(() => { process.exit(0); }) - .catch(() => { - process.exit(1); + .catch((error) => { + // eslint-disable-next-line + console.log(error); + // make sure error shows up in console before process is killed + setTimeout(() => { + process.exit(1); + }, 100); }); diff --git a/packages/elastic-apm-synthtrace/src/test/to_elasticsearch_output.test.ts b/packages/elastic-apm-synthtrace/src/test/apm_events_to_elasticsearch_output.test.ts similarity index 76% rename from packages/elastic-apm-synthtrace/src/test/to_elasticsearch_output.test.ts rename to packages/elastic-apm-synthtrace/src/test/apm_events_to_elasticsearch_output.test.ts index 02d17f6b561a..b8d030255892 100644 --- a/packages/elastic-apm-synthtrace/src/test/to_elasticsearch_output.test.ts +++ b/packages/elastic-apm-synthtrace/src/test/apm_events_to_elasticsearch_output.test.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { Fields } from '../lib/entity'; -import { toElasticsearchOutput } from '../lib/output/to_elasticsearch_output'; +import { apmEventsToElasticsearchOutput } from '../lib/apm/utils/apm_events_to_elasticsearch_output'; +import { ApmFields } from '../lib/apm/apm_fields'; const writeTargets = { transaction: 'apm-8.0.0-transaction', @@ -16,8 +16,8 @@ const writeTargets = { error: 'apm-8.0.0-error', }; -describe('output to elasticsearch', () => { - let event: Fields; +describe('output apm events to elasticsearch', () => { + let event: ApmFields; beforeEach(() => { event = { @@ -29,13 +29,13 @@ describe('output to elasticsearch', () => { }); it('properly formats @timestamp', () => { - const doc = toElasticsearchOutput({ events: [event], writeTargets })[0] as any; + const doc = apmEventsToElasticsearchOutput({ 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({ events: [event], writeTargets })[0] as any; + const doc = apmEventsToElasticsearchOutput({ events: [event], writeTargets })[0] as any; expect(doc._source.processor).toEqual({ event: 'transaction', @@ -44,7 +44,7 @@ describe('output to elasticsearch', () => { }); it('formats all fields consistently', () => { - const doc = toElasticsearchOutput({ events: [event], writeTargets })[0] as any; + const doc = apmEventsToElasticsearchOutput({ events: [event], writeTargets })[0] as any; expect(doc._source).toMatchInlineSnapshot(` Object { diff --git a/packages/elastic-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts b/packages/elastic-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts index fc20202e210f..b38d34266f3a 100644 --- a/packages/elastic-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts +++ b/packages/elastic-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts @@ -6,14 +6,14 @@ * Side Public License, v 1. */ -import { service } from '../../lib/service'; +import { apm } from '../../lib/apm'; import { timerange } from '../../lib/timerange'; describe('simple trace', () => { let events: Array>; beforeEach(() => { - const javaService = service('opbeans-java', 'production', 'java'); + const javaService = apm.service('opbeans-java', 'production', 'java'); const javaInstance = javaService.instance('instance-1'); const range = timerange( diff --git a/packages/elastic-apm-synthtrace/src/test/scenarios/02_transaction_metrics.test.ts b/packages/elastic-apm-synthtrace/src/test/scenarios/02_transaction_metrics.test.ts index 58b28f71b9af..d074bcbf6c1f 100644 --- a/packages/elastic-apm-synthtrace/src/test/scenarios/02_transaction_metrics.test.ts +++ b/packages/elastic-apm-synthtrace/src/test/scenarios/02_transaction_metrics.test.ts @@ -6,15 +6,15 @@ * Side Public License, v 1. */ -import { service } from '../../lib/service'; +import { apm } from '../../lib/apm'; import { timerange } from '../../lib/timerange'; -import { getTransactionMetrics } from '../../lib/utils/get_transaction_metrics'; +import { getTransactionMetrics } from '../../lib/apm/utils/get_transaction_metrics'; describe('transaction metrics', () => { let events: Array>; beforeEach(() => { - const javaService = service('opbeans-java', 'production', 'java'); + const javaService = apm.service('opbeans-java', 'production', 'java'); const javaInstance = javaService.instance('instance-1'); const range = timerange( diff --git a/packages/elastic-apm-synthtrace/src/test/scenarios/03_span_destination_metrics.test.ts b/packages/elastic-apm-synthtrace/src/test/scenarios/03_span_destination_metrics.test.ts index 0bf59f044bf0..fe4734c65739 100644 --- a/packages/elastic-apm-synthtrace/src/test/scenarios/03_span_destination_metrics.test.ts +++ b/packages/elastic-apm-synthtrace/src/test/scenarios/03_span_destination_metrics.test.ts @@ -6,15 +6,15 @@ * Side Public License, v 1. */ -import { service } from '../../lib/service'; +import { apm } from '../../lib/apm'; import { timerange } from '../../lib/timerange'; -import { getSpanDestinationMetrics } from '../../lib/utils/get_span_destination_metrics'; +import { getSpanDestinationMetrics } from '../../lib/apm/utils/get_span_destination_metrics'; describe('span destination metrics', () => { let events: Array>; beforeEach(() => { - const javaService = service('opbeans-java', 'production', 'java'); + const javaService = apm.service('opbeans-java', 'production', 'java'); const javaInstance = javaService.instance('instance-1'); const range = timerange( diff --git a/packages/elastic-apm-synthtrace/src/test/scenarios/04_breakdown_metrics.test.ts b/packages/elastic-apm-synthtrace/src/test/scenarios/04_breakdown_metrics.test.ts index 469f56b99c5f..817f0aad9f5e 100644 --- a/packages/elastic-apm-synthtrace/src/test/scenarios/04_breakdown_metrics.test.ts +++ b/packages/elastic-apm-synthtrace/src/test/scenarios/04_breakdown_metrics.test.ts @@ -6,13 +6,13 @@ * Side Public License, v 1. */ import { sumBy } from 'lodash'; -import { Fields } from '../../lib/entity'; -import { service } from '../../lib/service'; +import { apm } from '../../lib/apm'; import { timerange } from '../../lib/timerange'; -import { getBreakdownMetrics } from '../../lib/utils/get_breakdown_metrics'; +import { getBreakdownMetrics } from '../../lib/apm/utils/get_breakdown_metrics'; +import { ApmFields } from '../../lib/apm/apm_fields'; describe('breakdown metrics', () => { - let events: Fields[]; + let events: ApmFields[]; const LIST_RATE = 2; const LIST_SPANS = 2; @@ -21,7 +21,7 @@ describe('breakdown metrics', () => { const INTERVALS = 6; beforeEach(() => { - const javaService = service('opbeans-java', 'production', 'java'); + const javaService = apm.service('opbeans-java', 'production', 'java'); const javaInstance = javaService.instance('instance-1'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); diff --git a/packages/elastic-apm-synthtrace/src/test/scenarios/05_transactions_with_errors.test.ts b/packages/elastic-apm-synthtrace/src/test/scenarios/05_transactions_with_errors.test.ts index 63fdb691e8e5..b9b12aeab075 100644 --- a/packages/elastic-apm-synthtrace/src/test/scenarios/05_transactions_with_errors.test.ts +++ b/packages/elastic-apm-synthtrace/src/test/scenarios/05_transactions_with_errors.test.ts @@ -6,15 +6,15 @@ * Side Public License, v 1. */ import { pick } from 'lodash'; -import { service } from '../../index'; -import { Instance } from '../../lib/instance'; +import { apm } from '../../lib/apm'; +import { Instance } from '../../lib/apm/instance'; describe('transactions with errors', () => { let instance: Instance; const timestamp = new Date('2021-01-01T00:00:00.000Z').getTime(); beforeEach(() => { - instance = service('opbeans-java', 'production', 'java').instance('instance'); + instance = apm.service('opbeans-java', 'production', 'java').instance('instance'); }); it('generates error events', () => { const events = instance diff --git a/packages/elastic-apm-synthtrace/src/test/scenarios/06_application_metrics.test.ts b/packages/elastic-apm-synthtrace/src/test/scenarios/06_application_metrics.test.ts index 59ca8f0edbe8..7bae1e51f1ab 100644 --- a/packages/elastic-apm-synthtrace/src/test/scenarios/06_application_metrics.test.ts +++ b/packages/elastic-apm-synthtrace/src/test/scenarios/06_application_metrics.test.ts @@ -6,15 +6,15 @@ * Side Public License, v 1. */ import { pick } from 'lodash'; -import { service } from '../../index'; -import { Instance } from '../../lib/instance'; +import { apm } from '../../lib/apm'; +import { Instance } from '../../lib/apm/instance'; describe('application metrics', () => { let instance: Instance; const timestamp = new Date('2021-01-01T00:00:00.000Z').getTime(); beforeEach(() => { - instance = service('opbeans-java', 'production', 'java').instance('instance'); + instance = apm.service('opbeans-java', 'production', 'java').instance('instance'); }); it('generates application metricsets', () => { const events = instance diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/synthtrace/opbeans.ts b/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/synthtrace/opbeans.ts index bd01c83b9cc6..a6d2454de99f 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/synthtrace/opbeans.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/synthtrace/opbeans.ts @@ -4,28 +4,24 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { - service, - browser, - timerange, - getChromeUserAgentDefaults, -} from '@elastic/apm-synthtrace'; +import { apm, timerange } from '@elastic/apm-synthtrace'; export function opbeans({ from, to }: { from: number; to: number }) { const range = timerange(from, to); - const opbeansJava = service('opbeans-java', 'production', 'java') + const opbeansJava = apm + .service('opbeans-java', 'production', 'java') .instance('opbeans-java-prod-1') .podId('opbeans-java-prod-1-pod'); - const opbeansNode = service('opbeans-node', 'production', 'nodejs').instance( - 'opbeans-node-prod-1' - ); + const opbeansNode = apm + .service('opbeans-node', 'production', 'nodejs') + .instance('opbeans-node-prod-1'); - const opbeansRum = browser( + const opbeansRum = apm.browser( 'opbeans-rum', 'production', - getChromeUserAgentDefaults() + apm.getChromeUserAgentDefaults() ); return [ diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/generate_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/generate_data.ts index 7f1c14ac2551..7215d2f435e1 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/generate_data.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/generate_data.ts @@ -4,18 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { service, timerange } from '@elastic/apm-synthtrace'; +import { apm, timerange } from '@elastic/apm-synthtrace'; export function generateData({ from, to }: { from: number; to: number }) { const range = timerange(from, to); - const opbeansJava = service('opbeans-java', 'production', 'java') + const opbeansJava = apm + .service('opbeans-java', 'production', 'java') .instance('opbeans-java-prod-1') .podId('opbeans-java-prod-1-pod'); - const opbeansNode = service('opbeans-node', 'production', 'nodejs').instance( - 'opbeans-node-prod-1' - ); + const opbeansNode = apm + .service('opbeans-node', 'production', 'nodejs') + .instance('opbeans-node-prod-1'); return [ ...range diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.ts index 9ebaa1747d90..d4a2cdf10302 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { service, timerange } from '@elastic/apm-synthtrace'; +import { apm, timerange } from '@elastic/apm-synthtrace'; export function generateData({ from, @@ -17,13 +17,14 @@ export function generateData({ }) { const range = timerange(from, to); - const service1 = service(specialServiceName, 'production', 'java') + const service1 = apm + .service(specialServiceName, 'production', 'java') .instance('service-1-prod-1') .podId('service-1-prod-1-pod'); - const opbeansNode = service('opbeans-node', 'production', 'nodejs').instance( - 'opbeans-node-prod-1' - ); + const opbeansNode = apm + .service('opbeans-node', 'production', 'nodejs') + .instance('opbeans-node-prod-1'); return [ ...range diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/plugins/index.ts b/x-pack/plugins/apm/ftr_e2e/cypress/plugins/index.ts index 350d90ccb3fe..90cf96469127 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/plugins/index.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/plugins/index.ts @@ -6,8 +6,7 @@ */ import Fs from 'fs'; import { Client, HttpConnection } from '@elastic/elasticsearch'; -import { SynthtraceEsClient } from '@elastic/apm-synthtrace'; -import { createLogger, LogLevel } from '@elastic/apm-synthtrace'; +import { apm, createLogger, LogLevel } from '@elastic/apm-synthtrace'; import { CA_CERT_PATH } from '@kbn/dev-utils'; // *********************************************************** @@ -41,7 +40,7 @@ const plugin: Cypress.PluginConfig = (on, config) => { ...(isCloud ? { tls: { ca: Fs.readFileSync(CA_CERT_PATH, 'utf-8') } } : {}), }); - const synthtraceEsClient = new SynthtraceEsClient( + const synthtraceEsClient = new apm.ApmSynthtraceEsClient( client, createLogger(LogLevel.info) ); diff --git a/x-pack/test/apm_api_integration/common/synthtrace_es_client_service.ts b/x-pack/test/apm_api_integration/common/synthtrace_es_client_service.ts index 14e746a55a3d..0ff00d415e7a 100644 --- a/x-pack/test/apm_api_integration/common/synthtrace_es_client_service.ts +++ b/x-pack/test/apm_api_integration/common/synthtrace_es_client_service.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { SynthtraceEsClient, createLogger, LogLevel } from '@elastic/apm-synthtrace'; +import { apm, createLogger, LogLevel } from '@elastic/apm-synthtrace'; import { InheritedFtrProviderContext } from './ftr_provider_context'; export async function synthtraceEsClientService(context: InheritedFtrProviderContext) { const es = context.getService('es'); - return new SynthtraceEsClient(es, createLogger(LogLevel.info)); + return new apm.ApmSynthtraceEsClient(es, createLogger(LogLevel.info)); } diff --git a/x-pack/test/apm_api_integration/tests/dependencies/generate_data.ts b/x-pack/test/apm_api_integration/tests/dependencies/generate_data.ts index e36e99b447aa..21af5d91d14e 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/generate_data.ts @@ -4,8 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { service, timerange } from '@elastic/apm-synthtrace'; -import type { SynthtraceEsClient } from '@elastic/apm-synthtrace'; +import { apm, timerange } from '@elastic/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace'; export const dataConfig = { rate: 20, @@ -26,11 +26,11 @@ export async function generateData({ start, end, }: { - synthtraceEsClient: SynthtraceEsClient; + synthtraceEsClient: ApmSynthtraceEsClient; start: number; end: number; }) { - const instance = service('synth-go', 'production', 'go').instance('instance-a'); + const instance = apm.service('synth-go', 'production', 'go').instance('instance-a'); const { rate, transaction, span } = dataConfig; await synthtraceEsClient.index( diff --git a/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts b/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts index 7aca21f4fc7f..f1aefa06304a 100644 --- a/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { service, timerange } from '@elastic/apm-synthtrace'; +import { apm, timerange } from '@elastic/apm-synthtrace'; import expect from '@kbn/expect'; import { mean, meanBy, sumBy } from 'lodash'; import { LatencyAggregationType } from '../../../../plugins/apm/common/latency_aggregation_types'; @@ -121,9 +121,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { const GO_PROD_ID_RATE = 50; const GO_PROD_ID_ERROR_RATE = 50; before(async () => { - const serviceGoProdInstance = service(serviceName, 'production', 'go').instance( - 'instance-a' - ); + const serviceGoProdInstance = apm + .service(serviceName, 'production', 'go') + .instance('instance-a'); const transactionNameProductList = 'GET /api/product/list'; const transactionNameProductId = 'GET /api/product/:id'; diff --git a/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts b/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts index ce27183e84ca..421b536c6d5a 100644 --- a/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts +++ b/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ import expect from '@kbn/expect'; -import { service, timerange } from '@elastic/apm-synthtrace'; +import { apm, timerange } from '@elastic/apm-synthtrace'; import { APIClientRequestParamsOf, APIReturnType, @@ -72,7 +72,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { }; before(async () => { - const serviceInstance = service(serviceName, 'production', 'go').instance('instance-a'); + const serviceInstance = apm + .service(serviceName, 'production', 'go') + .instance('instance-a'); await synthtraceEsClient.index([ ...timerange(start, end) diff --git a/x-pack/test/apm_api_integration/tests/errors/generate_data.ts b/x-pack/test/apm_api_integration/tests/errors/generate_data.ts index f7874b1c6149..b9ac77ca3442 100644 --- a/x-pack/test/apm_api_integration/tests/errors/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/errors/generate_data.ts @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { service, SynthtraceEsClient, timerange } from '@elastic/apm-synthtrace'; +import { apm, timerange } from '@elastic/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace'; export const config = { appleTransaction: { @@ -25,12 +26,12 @@ export async function generateData({ start, end, }: { - synthtraceEsClient: SynthtraceEsClient; + synthtraceEsClient: ApmSynthtraceEsClient; serviceName: string; start: number; end: number; }) { - const serviceGoProdInstance = service(serviceName, 'production', 'go').instance('instance-a'); + const serviceGoProdInstance = apm.service(serviceName, 'production', 'go').instance('instance-a'); const interval = '1m'; diff --git a/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts b/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts index e87f03efeeef..1c0185c39655 100644 --- a/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { service, timerange } from '@elastic/apm-synthtrace'; +import { apm, timerange } from '@elastic/apm-synthtrace'; import expect from '@kbn/expect'; import { meanBy, sumBy } from 'lodash'; import { LatencyAggregationType } from '../../../../plugins/apm/common/latency_aggregation_types'; @@ -123,12 +123,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { const GO_PROD_DURATION = 1000; const GO_DEV_DURATION = 500; before(async () => { - const serviceGoProdInstance = service(serviceName, 'production', 'go').instance( - 'instance-a' - ); - const serviceGoDevInstance = service(serviceName, 'development', 'go').instance( - 'instance-b' - ); + const serviceGoProdInstance = apm + .service(serviceName, 'production', 'go') + .instance('instance-a'); + const serviceGoDevInstance = apm + .service(serviceName, 'development', 'go') + .instance('instance-b'); + await synthtraceEsClient.index([ ...timerange(start, end) .interval('1m') diff --git a/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.spec.ts b/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.spec.ts index 6b6d61fdb1d3..4c1e1850c177 100644 --- a/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.spec.ts +++ b/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.spec.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { service, timerange } from '@elastic/apm-synthtrace'; +import { apm, timerange } from '@elastic/apm-synthtrace'; import expect from '@kbn/expect'; import { meanBy, sumBy } from 'lodash'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -92,15 +92,16 @@ export default function ApiTest({ getService }: FtrProviderContext) { const GO_DEV_RATE = 5; const JAVA_PROD_RATE = 45; before(async () => { - const serviceGoProdInstance = service('synth-go', 'production', 'go').instance( - 'instance-a' - ); - const serviceGoDevInstance = service('synth-go', 'development', 'go').instance( - 'instance-b' - ); - const serviceJavaInstance = service('synth-java', 'production', 'java').instance( - 'instance-c' - ); + const serviceGoProdInstance = apm + .service('synth-go', 'production', 'go') + .instance('instance-a'); + const serviceGoDevInstance = apm + .service('synth-go', 'development', 'go') + .instance('instance-b'); + + const serviceJavaInstance = apm + .service('synth-java', 'production', 'java') + .instance('instance-c'); await synthtraceEsClient.index([ ...timerange(start, end) diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts index 5800ddf00480..a60da5f2bd5c 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { pick, sortBy } from 'lodash'; import moment from 'moment'; -import { service, timerange } from '@elastic/apm-synthtrace'; +import { apm, timerange } from '@elastic/apm-synthtrace'; import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; import { isFiniteNumber } from '../../../../plugins/apm/common/utils/is_finite_number'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -298,8 +298,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { const rangeEnd = new Date('2021-01-01T12:15:00.000Z').getTime() - 1; before(async () => { - const goService = service('opbeans-go', 'production', 'go'); - const javaService = service('opbeans-java', 'production', 'java'); + const goService = apm.service('opbeans-go', 'production', 'go'); + const javaService = apm.service('opbeans-java', 'production', 'java'); const goInstanceA = goService.instance('go-instance-a'); const goInstanceB = goService.instance('go-instance-b'); diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts b/x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts index f02f1e7493ff..ef77cd4003a1 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts @@ -4,8 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { service, timerange } from '@elastic/apm-synthtrace'; -import type { SynthtraceEsClient } from '@elastic/apm-synthtrace'; +import { apm, timerange } from '@elastic/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace'; export const config = { PROD_LIST_RATE: 75, @@ -22,12 +22,12 @@ export async function generateData({ start, end, }: { - synthtraceEsClient: SynthtraceEsClient; + synthtraceEsClient: ApmSynthtraceEsClient; serviceName: string; start: number; end: number; }) { - const serviceGoProdInstance = service(serviceName, 'production', 'go').instance('instance-a'); + const serviceGoProdInstance = apm.service(serviceName, 'production', 'go').instance('instance-a'); const transactionNameProductList = 'GET /api/product/list'; const transactionNameProductId = 'GET /api/product/:id'; diff --git a/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts b/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts index 077119156c64..87c1f5a04ed2 100644 --- a/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { service, timerange } from '@elastic/apm-synthtrace'; +import { apm, timerange } from '@elastic/apm-synthtrace'; import expect from '@kbn/expect'; import { first, last, meanBy } from 'lodash'; import moment from 'moment'; @@ -73,16 +73,16 @@ export default function ApiTest({ getService }: FtrProviderContext) { const JAVA_PROD_RATE = 45; before(async () => { - const serviceGoProdInstance = service(serviceName, 'production', 'go').instance( - 'instance-a' - ); - const serviceGoDevInstance = service(serviceName, 'development', 'go').instance( - 'instance-b' - ); - - const serviceJavaInstance = service('synth-java', 'development', 'java').instance( - 'instance-c' - ); + const serviceGoProdInstance = apm + .service(serviceName, 'production', 'go') + .instance('instance-a'); + const serviceGoDevInstance = apm + .service(serviceName, 'development', 'go') + .instance('instance-b'); + + const serviceJavaInstance = apm + .service('synth-java', 'development', 'java') + .instance('instance-c'); await synthtraceEsClient.index([ ...timerange(start, end) diff --git a/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts b/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts index 375206d0a0bc..d4dacadfee03 100644 --- a/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { sortBy } from 'lodash'; -import { service, timerange } from '@elastic/apm-synthtrace'; +import { apm, timerange } from '@elastic/apm-synthtrace'; import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; import { PromiseReturnType } from '../../../../plugins/observability/typings/common'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -65,21 +65,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { const transactionInterval = range.interval('1s'); const metricInterval = range.interval('30s'); - const multipleEnvServiceProdInstance = service( - 'multiple-env-service', - 'production', - 'go' - ).instance('multiple-env-service-production'); - - const multipleEnvServiceDevInstance = service( - 'multiple-env-service', - 'development', - 'go' - ).instance('multiple-env-service-development'); - - const metricOnlyInstance = service('metric-only-service', 'production', 'java').instance( - 'metric-only-production' - ); + const multipleEnvServiceProdInstance = apm + .service('multiple-env-service', 'production', 'go') + .instance('multiple-env-service-production'); + + const multipleEnvServiceDevInstance = apm + .service('multiple-env-service', 'development', 'go') + .instance('multiple-env-service-development'); + + const metricOnlyInstance = apm + .service('metric-only-service', 'production', 'java') + .instance('metric-only-production'); const config = { multiple: { diff --git a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts b/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts index 1b2c919f538a..bc2118f55f65 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { service, timerange } from '@elastic/apm-synthtrace'; +import { apm, timerange } from '@elastic/apm-synthtrace'; import expect from '@kbn/expect'; import { meanBy, sumBy } from 'lodash'; import { BackendNode, ServiceNode } from '../../../../plugins/apm/common/connections'; @@ -94,12 +94,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { const GO_PROD_RATE = 75; const JAVA_PROD_RATE = 25; before(async () => { - const serviceGoProdInstance = service('synth-go', 'production', 'go').instance( - 'instance-a' - ); - const serviceJavaInstance = service('synth-java', 'development', 'java').instance( - 'instance-c' - ); + const serviceGoProdInstance = apm + .service('synth-go', 'production', 'go') + .instance('instance-a'); + const serviceJavaInstance = apm + .service('synth-java', 'development', 'java') + .instance('instance-c'); await synthtraceEsClient.index([ ...timerange(start, end) diff --git a/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts b/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts index 7318fc449fcd..3492d2967a35 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { service, timerange } from '@elastic/apm-synthtrace'; +import { apm, timerange } from '@elastic/apm-synthtrace'; import expect from '@kbn/expect'; import { meanBy, sumBy } from 'lodash'; import { LatencyAggregationType } from '../../../../plugins/apm/common/latency_aggregation_types'; @@ -109,12 +109,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { const GO_PROD_RATE = 80; const GO_DEV_RATE = 20; before(async () => { - const serviceGoProdInstance = service(serviceName, 'production', 'go').instance( - 'instance-a' - ); - const serviceGoDevInstance = service(serviceName, 'development', 'go').instance( - 'instance-b' - ); + const serviceGoProdInstance = apm + .service(serviceName, 'production', 'go') + .instance('instance-a'); + const serviceGoDevInstance = apm + .service(serviceName, 'development', 'go') + .instance('instance-b'); + await synthtraceEsClient.index([ ...timerange(start, end) .interval('1m') diff --git a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.spec.ts index 72a0cdbbee48..be60c655ce50 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.spec.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { service, timerange } from '@elastic/apm-synthtrace'; +import { apm, timerange } from '@elastic/apm-synthtrace'; import expect from '@kbn/expect'; import { first, isEmpty, last, meanBy } from 'lodash'; import moment from 'moment'; @@ -84,9 +84,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { const GO_PROD_RATE = 75; const GO_PROD_ERROR_RATE = 25; before(async () => { - const serviceGoProdInstance = service(serviceName, 'production', 'go').instance( - 'instance-a' - ); + const serviceGoProdInstance = apm + .service(serviceName, 'production', 'go') + .instance('instance-a'); const transactionName = 'GET /api/product/list'; From e570b8783de73ad95c8d041e9871d569606b431b Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Mon, 29 Nov 2021 11:55:44 +0100 Subject: [PATCH 002/224] Show information when doc summary cuts fields (#119744) * Show information when doc summary cuts fields * Fix jest tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../get_render_cell_value.test.tsx | 2 +- .../doc_table/lib/row_formatter.tsx | 7 +++++-- .../discover/public/utils/format_hit.test.ts | 4 ++++ .../discover/public/utils/format_hit.ts | 20 +++++++++++++++++-- src/plugins/discover/server/ui_settings.ts | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 7 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx index 260cbf42c4d8..e97a2b2901f3 100644 --- a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx @@ -23,7 +23,7 @@ jest.mock('../../../../kibana_react/public', () => ({ jest.mock('../../kibana_services', () => ({ getServices: () => ({ uiSettings: { - get: jest.fn(), + get: jest.fn((key) => key === 'discover:maxDocFieldsDisplayed' && 200), }, fieldFormats: { getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => (value ? value : '-') })), diff --git a/src/plugins/discover/public/components/doc_table/lib/row_formatter.tsx b/src/plugins/discover/public/components/doc_table/lib/row_formatter.tsx index 0ec611a30751..53ede4e3f710 100644 --- a/src/plugins/discover/public/components/doc_table/lib/row_formatter.tsx +++ b/src/plugins/discover/public/components/doc_table/lib/row_formatter.tsx @@ -16,14 +16,17 @@ import { formatHit } from '../../../utils/format_hit'; import './row_formatter.scss'; interface Props { - defPairs: Array<[string, string]>; + defPairs: Array; } const TemplateComponent = ({ defPairs }: Props) => { return (
{defPairs.map((pair, idx) => ( -
{pair[0]}:
+
+ {pair[0]} + {!!pair[1] && ':'} +
{ (dataViewMock.getFormatterForField as jest.Mock).mockReturnValue({ convert: (value: unknown) => `formatted:${value}`, }); + (discoverServiceMock.uiSettings.get as jest.Mock).mockImplementation( + (key) => key === MAX_DOC_FIELDS_DISPLAYED && 220 + ); }); afterEach(() => { @@ -72,6 +75,7 @@ describe('formatHit', () => { expect(formatted).toEqual([ ['extension', 'formatted:png'], ['message', 'formatted:foobar'], + ['and 3 more fields', ''], ]); }); diff --git a/src/plugins/discover/public/utils/format_hit.ts b/src/plugins/discover/public/utils/format_hit.ts index b1bbfcd5aa87..4a06162714a2 100644 --- a/src/plugins/discover/public/utils/format_hit.ts +++ b/src/plugins/discover/public/utils/format_hit.ts @@ -7,6 +7,7 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { i18n } from '@kbn/i18n'; import { DataView, flattenHit } from '../../../data/common'; import { MAX_DOC_FIELDS_DISPLAYED } from '../../common'; import { getServices } from '../kibana_services'; @@ -14,7 +15,7 @@ import { formatFieldValue } from './format_value'; const formattedHitCache = new WeakMap(); -type FormattedHit = Array<[fieldName: string, formattedValue: string]>; +type FormattedHit = Array; /** * Returns a formatted document in form of key/value pairs of the fields name and a formatted value. @@ -61,7 +62,22 @@ export function formatHit( } }); const maxEntries = getServices().uiSettings.get(MAX_DOC_FIELDS_DISPLAYED); - const formatted = [...highlightPairs, ...sourcePairs].slice(0, maxEntries); + const pairs = [...highlightPairs, ...sourcePairs]; + const formatted = + // If document has more formatted fields than configured via MAX_DOC_FIELDS_DISPLAYED we cut + // off additional fields and instead show a summary how many more field exists. + pairs.length <= maxEntries + ? pairs + : [ + ...pairs.slice(0, maxEntries), + [ + i18n.translate('discover.utils.formatHit.moreFields', { + defaultMessage: 'and {count} more {count, plural, one {field} other {fields}}', + values: { count: pairs.length - maxEntries }, + }), + '', + ] as const, + ]; formattedHitCache.set(hit, formatted); return formatted; } diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index e9aa51a7384b..a58770ed1c1d 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -49,7 +49,7 @@ export const getUiSettings: () => Record = () => ({ }), value: 200, description: i18n.translate('discover.advancedSettings.maxDocFieldsDisplayedText', { - defaultMessage: 'Maximum number of fields rendered in the document column', + defaultMessage: 'Maximum number of fields rendered in the document summary', }), category: ['discover'], schema: schema.number(), diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b983eaaeaae4..9fc49535e18e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1583,7 +1583,6 @@ "discover.advancedSettings.docTableHideTimeColumnTitle": "ă€Œæ™‚ćˆ»ă€ćˆ—ă‚’éžèĄšç€ș", "discover.advancedSettings.fieldsPopularLimitText": "æœ€ă‚‚é »çčă«äœżç”šă•ă‚Œă‚‹ăƒ•ă‚ŁăƒŒăƒ«ăƒ‰ăźăƒˆăƒƒăƒ—Nă‚’èĄšç€șă—ăŸă™", "discover.advancedSettings.fieldsPopularLimitTitle": "é »çčă«äœżç”šă•ă‚Œă‚‹ăƒ•ă‚ŁăƒŒăƒ«ăƒ‰ăźćˆ¶é™", - "discover.advancedSettings.maxDocFieldsDisplayedText": "ăƒ‰ă‚­ăƒ„ăƒĄăƒłăƒˆćˆ—ă§ăƒŹăƒłăƒ€ăƒȘăƒłă‚°ă•ă‚ŒăŸăƒ•ă‚ŁăƒŒăƒ«ăƒ‰ăźæœ€ć€§æ•°", "discover.advancedSettings.maxDocFieldsDisplayedTitle": "èĄšç€șă•ă‚Œă‚‹æœ€ć€§ăƒ‰ă‚­ăƒ„ăƒĄăƒłăƒˆăƒ•ă‚ŁăƒŒăƒ«ăƒ‰æ•°", "discover.advancedSettings.sampleSizeText": "èĄšă«èĄšç€șă™ă‚‹èĄŒæ•°ă§ă™", "discover.advancedSettings.sampleSizeTitle": "èĄŒæ•°", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8b0686d0a309..102051ac2a9d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1595,7 +1595,6 @@ "discover.advancedSettings.docTableHideTimeColumnTitle": "éšè—â€œæ—¶é—Žâ€ćˆ—", "discover.advancedSettings.fieldsPopularLimitText": "芁星ç€șçš„æŽ’ćć‰ N æœ€ćžžè§ć­—æź”", "discover.advancedSettings.fieldsPopularLimitTitle": "ćžžè§ć­—æź”é™ćˆ¶", - "discover.advancedSettings.maxDocFieldsDisplayedText": "ćœšæ–‡æĄŁćˆ—äž­æžČæŸ“çš„æœ€ć€§ć­—æź”æ•°ç›ź", "discover.advancedSettings.maxDocFieldsDisplayedTitle": "星ç€șçš„æœ€ć€§æ–‡æĄŁć­—æź”æ•°", "discover.advancedSettings.sampleSizeText": "èŠćœšèĄšäž­æ˜Ÿç€șçš„èĄŒæ•°ç›ź", "discover.advancedSettings.sampleSizeTitle": "èĄŒæ•°ç›ź", From ee3cb46a682c918004b51cf2237e699c0b7b8936 Mon Sep 17 00:00:00 2001 From: Faisal Kanout Date: Mon, 29 Nov 2021 15:03:33 +0300 Subject: [PATCH 003/224] [8.0][RAC] 117686 replace alert workflow status in alerts view (#118723) * Add AlertStatus types * Add alert status filter component * Remove Filter in action from the t grid table * Update group buttons to applied Alert status filter instead of Workflow status * Keep the Alert status button in sync when typing and first page load * Fix data test object name and translation keys label * Add possibility to hide the bulk actions * Update how hide the bulk actions * Fix showCheckboxes hardcoded "true". Instead use the leadingControlColumns props * Hide the leading checkboxes in the T Grid with the bulk actions * Update showCheckboxes to false * Fix test as the leading checkboxes are hidden * Update tests * Get back disabledCellActions as it's required by T Grid * Update tests to skip test related to Workflow action buttons * Skip workflow tests * Revert fix showCheckboxes * Remove unused imports * Revert the o11y tests as the checkBoxes fix is reverted * Reactive the tests effected by checkBoxes * Skip alert workflow status * [Code review] use predefined types * Remove unused prop * Use the alert-data index name in the RegEx * Detect * in KQL as "show al"l alert filter Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/observability/common/typings.ts | 13 +++ .../pages/alerts/alerts_status_filter.tsx | 79 +++++++++++++++ .../pages/alerts/alerts_table_t_grid.tsx | 98 +++++++++---------- .../public/pages/alerts/index.tsx | 86 ++++++++++------ .../apps/observability/alerts/index.ts | 2 +- .../alerts/state_synchronization.ts | 12 +-- .../observability/alerts/workflow_status.ts | 3 +- 7 files changed, 202 insertions(+), 91 deletions(-) create mode 100644 x-pack/plugins/observability/public/pages/alerts/alerts_status_filter.tsx diff --git a/x-pack/plugins/observability/common/typings.ts b/x-pack/plugins/observability/common/typings.ts index bccb0f449100..1e247fd06739 100644 --- a/x-pack/plugins/observability/common/typings.ts +++ b/x-pack/plugins/observability/common/typings.ts @@ -7,6 +7,10 @@ import * as t from 'io-ts'; export type Maybe = T | null | undefined; +import { + ALERT_STATUS_ACTIVE, + ALERT_STATUS_RECOVERED, +} from '@kbn/rule-data-utils/alerts_as_data_status'; export const alertWorkflowStatusRt = t.keyof({ open: null, @@ -25,3 +29,12 @@ export interface ApmIndicesConfig { apmAgentConfigurationIndex: string; apmCustomLinkIndex: string; } +export type AlertStatusFilterButton = + | typeof ALERT_STATUS_ACTIVE + | typeof ALERT_STATUS_RECOVERED + | ''; +export interface AlertStatusFilter { + status: AlertStatusFilterButton; + query: string; + label: string; +} diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_status_filter.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_status_filter.tsx new file mode 100644 index 000000000000..38c753bbebf3 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_status_filter.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButtonGroup, EuiButtonGroupOptionProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { + ALERT_STATUS_ACTIVE, + ALERT_STATUS_RECOVERED, +} from '@kbn/rule-data-utils/alerts_as_data_status'; +import { ALERT_STATUS } from '@kbn/rule-data-utils/technical_field_names'; +import { AlertStatusFilterButton } from '../../../common/typings'; +import { AlertStatusFilter } from '../../../common/typings'; + +export interface AlertStatusFilterProps { + status: AlertStatusFilterButton; + onChange: (id: string, value: string) => void; +} + +export const allAlerts: AlertStatusFilter = { + status: '', + query: '', + label: i18n.translate('xpack.observability.alerts.alertStatusFilter.showAll', { + defaultMessage: 'Show all', + }), +}; + +export const activeAlerts: AlertStatusFilter = { + status: ALERT_STATUS_ACTIVE, + query: `${ALERT_STATUS}: "${ALERT_STATUS_ACTIVE}"`, + label: i18n.translate('xpack.observability.alerts.alertStatusFilter.active', { + defaultMessage: 'Active', + }), +}; + +export const recoveredAlerts: AlertStatusFilter = { + status: ALERT_STATUS_RECOVERED, + query: `${ALERT_STATUS}: "${ALERT_STATUS_RECOVERED}"`, + label: i18n.translate('xpack.observability.alerts.alertStatusFilter.recovered', { + defaultMessage: 'Recovered', + }), +}; + +const options: EuiButtonGroupOptionProps[] = [ + { + id: allAlerts.status, + label: allAlerts.label, + value: allAlerts.query, + 'data-test-subj': 'alert-status-filter-show-all-button', + }, + { + id: activeAlerts.status, + label: activeAlerts.label, + value: activeAlerts.query, + 'data-test-subj': 'alert-status-filter-active-button', + }, + { + id: recoveredAlerts.status, + label: recoveredAlerts.label, + value: recoveredAlerts.query, + 'data-test-subj': 'alert-status-filter-recovered-button', + }, +]; + +export function AlertsStatusFilter({ status, onChange }: AlertStatusFilterProps) { + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx index 69a6672db6e9..4b64ae07ddf0 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx @@ -13,8 +13,6 @@ import { ALERT_DURATION, ALERT_REASON, - ALERT_RULE_CONSUMER, - ALERT_RULE_PRODUCER, ALERT_STATUS, ALERT_WORKFLOW_STATUS, TIMESTAMP, @@ -34,11 +32,8 @@ import { import styled from 'styled-components'; import React, { Suspense, useMemo, useState, useCallback, useEffect } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; -import { get, pick } from 'lodash'; -import { - getAlertsPermissions, - useGetUserAlertsPermissions, -} from '../../hooks/use_alert_permission'; +import { pick } from 'lodash'; +import { getAlertsPermissions } from '../../hooks/use_alert_permission'; import type { TimelinesUIStart, TGridType, @@ -46,13 +41,14 @@ import type { TGridModel, SortDirection, } from '../../../../timelines/public'; -import { useStatusBulkActionItems } from '../../../../timelines/public'; + import type { TopAlert } from './'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import type { ActionProps, AlertWorkflowStatus, ColumnHeaderOptions, + ControlColumnProps, RowRenderer, } from '../../../../timelines/common'; @@ -60,7 +56,6 @@ import { getRenderCellValue } from './render_cell_value'; import { observabilityAppId, observabilityFeatureId } from '../../../common'; import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; import { usePluginContext } from '../../hooks/use_plugin_context'; -import { getDefaultCellActions } from './default_cell_actions'; import { LazyAlertsFlyout } from '../..'; import { parseAlert } from './parse_alert'; import { CoreStart } from '../../../../../../src/core/public'; @@ -75,7 +70,6 @@ interface AlertsTableTGridProps { kuery: string; workflowStatus: AlertWorkflowStatus; setRefetch: (ref: () => void) => void; - addToQuery: (value: string) => void; } interface ObservabilityActionsProps extends ActionProps { @@ -154,21 +148,21 @@ function ObservabilityActions({ const [openActionsPopoverId, setActionsPopover] = useState(null); const { timelines, - application: { capabilities }, + application: {}, } = useKibana().services; const parseObservabilityAlert = useMemo( () => parseAlert(observabilityRuleTypeRegistry), [observabilityRuleTypeRegistry] ); - const alertDataConsumer = useMemo( - () => get(dataFieldEs, ALERT_RULE_CONSUMER, [''])[0], - [dataFieldEs] - ); - const alertDataProducer = useMemo( - () => get(dataFieldEs, ALERT_RULE_PRODUCER, [''])[0], - [dataFieldEs] - ); + // const alertDataConsumer = useMemo( + // () => get(dataFieldEs, ALERT_RULE_CONSUMER, [''])[0], + // [dataFieldEs] + // ); + // const alertDataProducer = useMemo( + // () => get(dataFieldEs, ALERT_RULE_PRODUCER, [''])[0], + // [dataFieldEs] + // ); const alert = parseObservabilityAlert(dataFieldEs); const { prepend } = core.http.basePath; @@ -194,27 +188,29 @@ function ObservabilityActions({ }; }, [data, eventId, ecsData]); - const onAlertStatusUpdated = useCallback(() => { - setActionsPopover(null); - if (refetch) { - refetch(); - } - }, [setActionsPopover, refetch]); - - const alertPermissions = useGetUserAlertsPermissions( - capabilities, - alertDataConsumer === 'alerts' ? alertDataProducer : alertDataConsumer - ); - - const statusActionItems = useStatusBulkActionItems({ - eventIds: [eventId], - currentStatus, - indexName: ecsData._index ?? '', - setEventsLoading, - setEventsDeleted, - onUpdateSuccess: onAlertStatusUpdated, - onUpdateFailure: onAlertStatusUpdated, - }); + // Hide the WorkFlow filter, but keep its code as required in https://github.com/elastic/kibana/issues/117686 + + // const onAlertStatusUpdated = useCallback(() => { + // setActionsPopover(null); + // if (refetch) { + // refetch(); + // } + // }, [setActionsPopover, refetch]); + + // const alertPermissions = useGetUserAlertsPermissions( + // capabilities, + // alertDataConsumer === 'alerts' ? alertDataProducer : alertDataConsumer + // ); + + // const statusActionItems = useStatusBulkActionItems({ + // eventIds: [eventId], + // currentStatus, + // indexName: ecsData._index ?? '', + // setEventsLoading, + // setEventsDeleted, + // onUpdateSuccess: onAlertStatusUpdated, + // onUpdateFailure: onAlertStatusUpdated, + // }); const ruleId = alert.fields['kibana.alert.rule.uuid'] ?? null; const linkToRule = ruleId ? prepend(paths.management.ruleDetails(ruleId)) : null; @@ -239,7 +235,8 @@ function ObservabilityActions({ }), ] : []), - ...(alertPermissions.crud ? statusActionItems : []), + // Hide the WorkFlow filter, but keep its code as required in https://github.com/elastic/kibana/issues/117686 + // ...(alertPermissions.crud ? statusActionItems : []), ...(!!linkToRule ? [ ); } +// Hide the WorkFlow filter, but keep its code as required in https://github.com/elastic/kibana/issues/117686 const FIELDS_WITHOUT_CELL_ACTIONS = [ '@timestamp', @@ -330,7 +320,7 @@ const FIELDS_WITHOUT_CELL_ACTIONS = [ ]; export function AlertsTableTGrid(props: AlertsTableTGridProps) { - const { indexNames, rangeFrom, rangeTo, kuery, workflowStatus, setRefetch, addToQuery } = props; + const { indexNames, rangeFrom, rangeTo, kuery, workflowStatus, setRefetch } = props; const prevWorkflowStatus = usePrevious(workflowStatus); const { timelines, @@ -382,7 +372,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { } }, []); - const leadingControlColumns = useMemo(() => { + const leadingControlColumns: ControlColumnProps[] = useMemo(() => { return [ { id: 'expand', @@ -428,7 +418,8 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { type, columns: tGridState?.columns ?? columns, deletedEventIds, - defaultCellActions: getDefaultCellActions({ addToQuery }), + // Hide the WorkFlow filter, but keep its code as required in https://github.com/elastic/kibana/issues/117686 + // defaultCellActions: getDefaultCellActions({ addToQuery }), disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS, end: rangeTo, filters: [], @@ -462,7 +453,6 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { }; }, [ casePermissions, - addToQuery, rangeTo, hasAlertsCrudPermissions, indexNames, diff --git a/x-pack/plugins/observability/public/pages/alerts/index.tsx b/x-pack/plugins/observability/public/pages/alerts/index.tsx index a3c060f5dc5d..2636463bcfd7 100644 --- a/x-pack/plugins/observability/public/pages/alerts/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/index.tsx @@ -9,10 +9,13 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { IndexPatternBase } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; -import React, { useCallback, useRef } from 'react'; +import React, { useCallback, useRef, useState, useEffect } from 'react'; import useAsync from 'react-use/lib/useAsync'; +import { AlertStatus } from '@kbn/rule-data-utils/alerts_as_data_status'; +import { ALERT_STATUS } from '@kbn/rule-data-utils/technical_field_names'; + +import { AlertStatusFilterButton } from '../../../common/typings'; import { ParsedTechnicalFields } from '../../../../rule_registry/common/parse_technical_fields'; -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'; @@ -26,7 +29,7 @@ import { AlertsSearchBar } from './alerts_search_bar'; import { AlertsTableTGrid } from './alerts_table_t_grid'; import { Provider, alertsPageStateContainer, useAlertsPageStateContainer } from './state_container'; import './styles.scss'; -import { WorkflowStatusFilter } from './workflow_status_filter'; +import { AlertsStatusFilter } from './alerts_status_filter'; import { AlertsDisclaimer } from './alerts_disclaimer'; export interface TopAlert { @@ -36,25 +39,29 @@ export interface TopAlert { link?: string; active: boolean; } - +const regExpEscape = (str: string) => str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); const NO_INDEX_NAMES: string[] = []; const NO_INDEX_PATTERNS: IndexPatternBase[] = []; +const BASE_ALERT_REGEX = new RegExp(`\\s*${regExpEscape(ALERT_STATUS)}\\s*:\\s*"(.*?|\\*?)"`); +const ALERT_STATUS_REGEX = new RegExp( + `\\s*and\\s*${regExpEscape(ALERT_STATUS)}\\s*:\\s*(".+?"|\\*?)|${regExpEscape( + ALERT_STATUS + )}\\s*:\\s*(".+?"|\\*?)`, + 'gm' +); function AlertsPage() { const { core, plugins, ObservabilityPageTemplate } = usePluginContext(); + const [alertFilterStatus, setAlertFilterStatus] = useState('' as AlertStatusFilterButton); const { prepend } = core.http.basePath; const refetch = useRef<() => void>(); const timefilterService = useTimefilterService(); - const { - rangeFrom, - setRangeFrom, - rangeTo, - setRangeTo, - kuery, - setKuery, - workflowStatus, - setWorkflowStatus, - } = useAlertsPageStateContainer(); + const { rangeFrom, setRangeFrom, rangeTo, setRangeTo, kuery, setKuery, workflowStatus } = + useAlertsPageStateContainer(); + + useEffect(() => { + syncAlertStatusFilterStatus(kuery as string); + }, [kuery]); useBreadcrumbs([ { @@ -103,36 +110,56 @@ function AlertsPage() { ]; }, [indexNames]); - const setWorkflowStatusFilter = useCallback( - (value: AlertWorkflowStatus) => { - setWorkflowStatus(value); - }, - [setWorkflowStatus] - ); + // Keep the Workflow status code commented (no delete) as requested: https://github.com/elastic/kibana/issues/117686 + + // const setWorkflowStatusFilter = useCallback( + // (value: AlertWorkflowStatus) => { + // setWorkflowStatus(value); + // }, + // [setWorkflowStatus] + // ); const onQueryChange = useCallback( ({ dateRange, query }) => { if (rangeFrom === dateRange.from && rangeTo === dateRange.to && kuery === (query ?? '')) { return refetch.current && refetch.current(); } - timefilterService.setTime(dateRange); setRangeFrom(dateRange.from); setRangeTo(dateRange.to); setKuery(query); + syncAlertStatusFilterStatus(query as string); }, [rangeFrom, setRangeFrom, rangeTo, setRangeTo, kuery, setKuery, timefilterService] ); - const addToQuery = useCallback( - (value: string) => { - let output = value; - if (kuery !== '') { - output = `${kuery} and ${value}`; + const syncAlertStatusFilterStatus = (query: string) => { + const [, alertStatus] = BASE_ALERT_REGEX.exec(query) || []; + if (!alertStatus) { + setAlertFilterStatus(''); + return; + } + setAlertFilterStatus(alertStatus.toLowerCase() as AlertStatus); + }; + const setAlertStatusFilter = useCallback( + (id: string, query: string) => { + setAlertFilterStatus(id as AlertStatusFilterButton); + // Updating the KQL query bar alongside with user inputs is tricky. + // To avoid issue, this function always remove the AlertFilter and add it + // at the end of the query, each time the filter is added/updated/removed (Show All) + // NOTE: This (query appending) will be changed entirely: https://github.com/elastic/kibana/issues/116135 + let output = kuery; + if (kuery === '') { + output = query; + } else { + // console.log(ALERT_STATUS_REGEX); + const queryWithoutAlertFilter = kuery.replace(ALERT_STATUS_REGEX, ''); + output = `${queryWithoutAlertFilter} and ${query}`; } onQueryChange({ dateRange: { from: rangeFrom, to: rangeTo }, - query: output, + // Clean up the kuery from unwanted trailing/ahead ANDs after appending and removing filters. + query: output.replace(/^\s*and\s*|\s*and\s*$/gm, ''), }); }, [kuery, onQueryChange, rangeFrom, rangeTo] @@ -194,7 +221,9 @@ function AlertsPage() { - + {/* Keep the Workflow status code commented (no delete) as requested: https://github.com/elastic/kibana/issues/117686*/} + {/* */} + @@ -207,7 +236,6 @@ function AlertsPage() { kuery={kuery} workflowStatus={workflowStatus} setRefetch={setRefetch} - addToQuery={addToQuery} /> 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 bd5f2ada4990..4d2f4b971f08 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/index.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/index.ts @@ -186,7 +186,7 @@ export default ({ getService }: FtrProviderContext) => { }); }); - describe('Cell actions', () => { + describe.skip('Cell actions', () => { beforeEach(async () => { await retry.try(async () => { const cells = await observability.alerts.common.getTableCells(); diff --git a/x-pack/test/observability_functional/apps/observability/alerts/state_synchronization.ts b/x-pack/test/observability_functional/apps/observability/alerts/state_synchronization.ts index 5a03f72e540b..c351b45b2ea9 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/state_synchronization.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/state_synchronization.ts @@ -39,7 +39,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await assertAlertsPageState({ kuery: 'kibana.alert.evaluation.threshold > 75', - workflowStatus: 'Closed', + // workflowStatus: 'Closed', timeRange: '~ a month ago - ~ 10 days ago', }); }); @@ -55,7 +55,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await assertAlertsPageState({ kuery: '', - workflowStatus: 'Open', + // workflowStatus: 'Open', timeRange: 'Last 15 minutes', }); }); @@ -77,15 +77,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { async function assertAlertsPageState(expected: { kuery: string; - workflowStatus: string; + // workflowStatus: string; timeRange: string; }) { expect(await (await observability.alerts.common.getQueryBar()).getVisibleText()).to.be( expected.kuery ); - expect(await observability.alerts.common.getWorkflowStatusFilterValue()).to.be( - expected.workflowStatus - ); + // expect(await observability.alerts.common.getWorkflowStatusFilterValue()).to.be( + // expected.workflowStatus + // ); const timeRange = await observability.alerts.common.getTimeRange(); expect(timeRange).to.be(expected.timeRange); } 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 4976c1c225ab..9f6c78130674 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 @@ -13,7 +13,8 @@ const OPEN_ALERTS_ROWS_COUNT = 33; export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); - describe('alert workflow status', function () { + // Keep the Workflow status code commented (no delete) as requested: https://github.com/elastic/kibana/issues/117686 + describe.skip('alert workflow status', function () { this.tags('includeFirefox'); const observability = getService('observability'); From d2410a9852d99bced0a871e10170b10ee5e64863 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Mon, 29 Nov 2021 13:17:46 +0100 Subject: [PATCH 004/224] [load testing] run pupeteeer scenario based on simulation class (#119778) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/test/load/runner.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/x-pack/test/load/runner.ts b/x-pack/test/load/runner.ts index e9750bd19881..ba40bcf294e5 100644 --- a/x-pack/test/load/runner.ts +++ b/x-pack/test/load/runner.ts @@ -57,16 +57,20 @@ export async function GatlingTestRunner({ getService }: FtrProviderContext) { const log = getService('log'); await withProcRunner(log, async (procs) => { - await procs.run('node build/index.js', { - cmd: 'node', - args: ['build/index.js'], - cwd: puppeteerProjectRootPath, - env: { - ...process.env, - }, - wait: true, - }); for (let i = 0; i < simulationClasses.length; i++) { + await procs.run('node build/index.js', { + cmd: 'node', + args: [ + 'build/index.js', + `--simulation='${simulationClasses[i]}'`, + `--config='./config.json'`, + ], + cwd: puppeteerProjectRootPath, + env: { + ...process.env, + }, + wait: true, + }); await procs.run('gatling: test', { cmd: 'mvn', args: [ From 1c1d607fc2212805629478fab5e6763742069e64 Mon Sep 17 00:00:00 2001 From: "Lucas F. da Costa" Date: Mon, 29 Nov 2021 13:29:22 +0000 Subject: [PATCH 005/224] [Uptime] Inform users when they can't create ML jobs (closes #107994) (#117684) Previously, if users weren't able to create an ML job, they would simply be shown a grayed-out button. Showing this disabled button without explaining _why_ they cannot create ML jobs could be confusing. Therefore, this commit adds a proper callout explaining which roles and privileges are necessary. --- .../components/monitor/ml/ml_flyout.test.tsx | 74 +++++++++++++------ .../components/monitor/ml/ml_flyout.tsx | 15 ++++ .../components/monitor/ml/translations.tsx | 7 ++ .../uptime/public/lib/helper/rtl_helpers.tsx | 22 +++++- 4 files changed, 96 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.test.tsx index c2c4baf0751c..8669bc180f42 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.test.tsx @@ -10,13 +10,23 @@ import { MLFlyoutView } from './ml_flyout'; import { UptimeSettingsContext } from '../../../contexts'; import { CLIENT_DEFAULTS } from '../../../../common/constants'; import * as redux from 'react-redux'; -import { render } from '../../../lib/helper/rtl_helpers'; +import { render, forNearestButton } from '../../../lib/helper/rtl_helpers'; import * as labels from './translations'; describe('ML Flyout component', () => { const createJob = () => {}; const onClose = () => {}; const { DATE_RANGE_START, DATE_RANGE_END } = CLIENT_DEFAULTS; + const defaultContextValue = { + isDevMode: true, + basePath: '', + dateRangeStart: DATE_RANGE_START, + dateRangeEnd: DATE_RANGE_END, + isApmAvailable: true, + isInfraAvailable: true, + isLogsAvailable: true, + config: {}, + }; beforeEach(() => { const spy = jest.spyOn(redux, 'useDispatch'); @@ -32,16 +42,8 @@ describe('ML Flyout component', () => { // return false value for no license spy1.mockReturnValue(false); - const value = { - isDevMode: true, - basePath: '', - dateRangeStart: DATE_RANGE_START, - dateRangeEnd: DATE_RANGE_END, - isApmAvailable: true, - isInfraAvailable: true, - isLogsAvailable: true, - config: {}, - }; + const value = { ...defaultContextValue }; + const { findByText, findAllByText } = render( { }); it('able to create job if valid license is available', async () => { - const value = { - isDevMode: true, - basePath: '', - dateRangeStart: DATE_RANGE_START, - dateRangeEnd: DATE_RANGE_END, - isApmAvailable: true, - isInfraAvailable: true, - isLogsAvailable: true, - config: {}, - }; + const value = { ...defaultContextValue }; + const { queryByText } = render( { expect(queryByText(labels.START_TRAIL)).not.toBeInTheDocument(); }); + + describe("when users don't have Machine Learning privileges", () => { + it('shows an informative callout about the need for ML privileges', async () => { + const value = { ...defaultContextValue }; + + const { queryByText } = render( + + + + ); + + expect( + queryByText('You must have the Kibana privileges for Machine Learning to use this feature.') + ).toBeInTheDocument(); + }); + + it('disables the job creation button', async () => { + const value = { ...defaultContextValue }; + + const { queryByText } = render( + + + + ); + + expect(forNearestButton(queryByText)(labels.CREATE_NEW_JOB)).toBeDisabled(); + }); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx index 5500324e4bdd..c367b60a6501 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx @@ -19,6 +19,7 @@ import { EuiSpacer, EuiText, EuiTitle, + EuiCallOut, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { useSelector } from 'react-redux'; @@ -69,6 +70,20 @@ export function MLFlyoutView({ isCreatingJob, onClickCreate, onClose, canCreateM

+ {!canCreateMLJob && ( + +

+ +

+
+ )} diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx index 1fc4093a67d8..86ca94d5b649 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx @@ -179,3 +179,10 @@ export const ENABLE_MANAGE_JOB = i18n.translate( 'You can enable anomaly detection job or if job is already there you can manage the job or alert.', } ); + +export const ADD_JOB_PERMISSIONS_NEEDED = i18n.translate( + 'xpack.uptime.ml.enableAnomalyDetectionPanel.add_job_permissions_needed', + { + defaultMessage: 'Permissions needed', + } +); diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index fe7fd0918450..84d256630433 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -8,7 +8,12 @@ import React, { ReactElement } from 'react'; import { of } from 'rxjs'; // eslint-disable-next-line import/no-extraneous-dependencies -import { render as reactTestLibRender, RenderOptions } from '@testing-library/react'; +import { + render as reactTestLibRender, + MatcherFunction, + RenderOptions, + Nullish, +} from '@testing-library/react'; import { Router } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; import { CoreStart } from 'kibana/public'; @@ -209,3 +214,18 @@ const getHistoryFromUrl = (url: Url) => { initialEntries: [url.path + stringifyUrlParams(url.queryParams)], }); }; + +// This function allows us to query for the nearest button with test +// no matter whether it has nested tags or not (as EuiButton elements do). +export const forNearestButton = + (getByText: (f: MatcherFunction) => HTMLElement | null) => + (text: string): HTMLElement | null => + getByText((_content: string, node: Nullish) => { + if (!node) return false; + const noOtherButtonHasText = Array.from(node.children).every( + (child) => child && (child.textContent !== text || child.tagName.toLowerCase() !== 'button') + ); + return ( + noOtherButtonHasText && node.textContent === text && node.tagName.toLowerCase() === 'button' + ); + }); From 1b3112ee96aa9e4337d7a7c2eef985602755802c Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 29 Nov 2021 13:33:09 +0000 Subject: [PATCH 006/224] skip flaky suite (#88177) --- x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index d657db443e4e..bb89fa8f683f 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -16,7 +16,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const supertest = getService('supertest'); const retry = getService('retry'); - describe('overview page alert flyout controls', function () { + // FLAKY: https://github.com/elastic/kibana/issues/88177 + describe.skip('overview page alert flyout controls', function () { const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; let alerts: any; From 776e091865340441798b8b918f8c442bdf16f14a Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 29 Nov 2021 13:37:55 +0000 Subject: [PATCH 007/224] skip flaky suite (#92567) --- .../security_solution_endpoint/apps/endpoint/policy_details.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 61773aaf825f..7562f69f673c 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -337,7 +337,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('and the save button is clicked', () => { + // FLAKY: https://github.com/elastic/kibana/issues/92567 + describe.skip('and the save button is clicked', () => { let policyInfo: PolicyTestResourceInfo; beforeEach(async () => { From 43253ecafa1365eae45d31dd284d0a0bf273bc4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Mon, 29 Nov 2021 14:40:01 +0100 Subject: [PATCH 008/224] [APM] Fix bug in documentation on `span.destination` metrics (#119789) --- x-pack/plugins/apm/dev_docs/apm_queries.md | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/apm/dev_docs/apm_queries.md b/x-pack/plugins/apm/dev_docs/apm_queries.md index e6021fa31b9f..4dd9a807eb24 100644 --- a/x-pack/plugins/apm/dev_docs/apm_queries.md +++ b/x-pack/plugins/apm/dev_docs/apm_queries.md @@ -461,6 +461,7 @@ GET apm-*-metric-*,metrics-apm*/_search?terminate_after=1000 "aggs": { "throughput": { "rate": { + "field": "span.destination.service.response_time.count", "unit": "minute" } } From c394b5744bd075863e5169f4d1f2d5cbc4ddc5eb Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 29 Nov 2021 13:46:09 +0000 Subject: [PATCH 009/224] skip flaky suite (#119660) --- x-pack/test/api_integration/apis/search/session.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/search/session.ts b/x-pack/test/api_integration/apis/search/session.ts index 1fa65172cdee..868c91cd9ed1 100644 --- a/x-pack/test/api_integration/apis/search/session.ts +++ b/x-pack/test/api_integration/apis/search/session.ts @@ -16,7 +16,8 @@ export default function ({ getService }: FtrProviderContext) { const retry = getService('retry'); const spacesService = getService('spaces'); - describe('search session', () => { + // FLAKY: https://github.com/elastic/kibana/issues/119660 + describe.skip('search session', () => { describe('session management', () => { it('should fail to create a session with no name', async () => { const sessionId = `my-session-${Math.random()}`; From 1915a8ddfce1840e67c079ef63133172d6550e30 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 29 Nov 2021 14:56:37 +0100 Subject: [PATCH 010/224] fix existing fields query (#119508) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/lens/server/routes/existing_fields.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts index d3a79dc7d8bf..3a57a77a9772 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.ts @@ -183,6 +183,7 @@ async function fetchIndexPatternStats({ { range: { [timeFieldName]: { + format: 'strict_date_optional_time', gte: fromDate, lte: toDate, }, From c6d41d07d727c1257f482880f22564bae30449fb Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 29 Nov 2021 15:36:52 +0100 Subject: [PATCH 011/224] [Dashboard/Reporting] Removal of Reporting injected CSS and JS (#111958) * checkpoint 1: dashboard reading screenshot mode values is screenshot mode and screenshot layout * checkpoint 2: dashboard handling preserve layout * temp setting up print viewport * slight clean up, detect a new view mode "print" * fix types * adde todo comment * added "print" route to dashboard that does not rely on screenshotMode service * updated jest tests and added screenshot mode public mock * try to respect embed settings * fix lint * remove print mode from share data * re-add ViewMode.VIEW to share data * updated TODO comment * remove injected print css * remove double declaration of ScreenshotModePluginStart * re-add missing import :facepalm: * fix types issues * changed css injection removal to use only viewMode.PRINT rather than a new route * turn off defer below fold when in print mode * elastic@ email address * address some CI checks that were failing * use .includes instead of || to check view mode Co-authored-by: Michael Dokolin Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Devon A Thomson Co-authored-by: Michael Dokolin --- src/plugins/dashboard/kibana.json | 1 + .../actions/add_to_library_action.test.tsx | 2 + .../actions/clone_panel_action.test.tsx | 2 + .../actions/expand_panel_action.test.tsx | 2 + .../actions/export_csv_action.test.tsx | 2 + .../library_notification_action.test.tsx | 2 + .../library_notification_popover.test.tsx | 2 + .../actions/replace_panel_action.test.tsx | 2 + .../unlink_from_library_action.test.tsx | 2 + .../public/application/dashboard_app.tsx | 26 ++-- .../public/application/dashboard_router.tsx | 2 +- .../embeddable/dashboard_container.test.tsx | 2 + .../embeddable/dashboard_container.tsx | 2 + .../embeddable/grid/dashboard_grid.test.tsx | 2 + .../embeddable/grid/dashboard_grid.tsx | 7 +- .../embeddable/grid/dashboard_grid_item.tsx | 9 +- .../embeddable/viewport/_index.scss | 1 + .../embeddable/viewport/_print_viewport.scss | 9 ++ .../viewport/dashboard_viewport.test.tsx | 2 + .../hooks/use_dashboard_app_state.test.tsx | 1 - .../hooks/use_dashboard_app_state.ts | 11 +- .../test_helpers/make_default_services.ts | 2 + .../dashboard/public/dashboard_constants.ts | 1 + src/plugins/dashboard/public/locator.ts | 1 + src/plugins/dashboard/public/plugin.tsx | 19 ++- .../public/services/screenshot_mode.ts | 14 ++ src/plugins/dashboard/public/types.ts | 4 +- src/plugins/embeddable/common/types.ts | 1 + .../public/lib/panel/embeddable_panel.tsx | 2 +- src/plugins/newsfeed/public/plugin.test.ts | 7 +- .../common/get_set_browser_screenshot_mode.ts | 30 ++++- src/plugins/screenshot_mode/common/index.ts | 5 + src/plugins/screenshot_mode/public/mocks.ts | 2 + src/plugins/screenshot_mode/public/plugin.ts | 3 +- src/plugins/screenshot_mode/public/types.ts | 5 + src/plugins/screenshot_mode/server/plugin.ts | 3 +- src/plugins/screenshot_mode/server/types.ts | 6 +- .../chromium/driver/chromium_driver.ts | 8 +- x-pack/plugins/reporting/server/core.ts | 11 +- .../server/lib/layouts/preserve_layout.css | 7 +- .../server/lib/layouts/preserve_layout.ts | 2 +- .../reporting/server/lib/layouts/print.css | 122 ------------------ .../server/lib/layouts/print_layout.ts | 41 +----- .../lib/screenshots/observable_handler.ts | 1 + .../server/lib/screenshots/open_url.ts | 8 +- 45 files changed, 197 insertions(+), 199 deletions(-) create mode 100644 src/plugins/dashboard/public/application/embeddable/viewport/_print_viewport.scss create mode 100644 src/plugins/dashboard/public/services/screenshot_mode.ts delete mode 100644 x-pack/plugins/reporting/server/lib/layouts/print.css diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index cb6a5383688d..2be6e9b269e7 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -13,6 +13,7 @@ "navigation", "savedObjects", "share", + "screenshotMode", "uiActions", "urlForwarding", "presentationUtil", diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx index fa484de2180b..40f6f872535f 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx @@ -15,6 +15,7 @@ import { CoreStart } from 'kibana/public'; import { coreMock, uiSettingsServiceMock } from '../../../../../core/public/mocks'; import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; import { getStubPluginServices } from '../../../../presentation_util/public'; +import { screenshotModePluginMock } from '../../../../screenshot_mode/public/mocks'; import { EmbeddableInput, @@ -65,6 +66,7 @@ beforeEach(async () => { uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, presentationUtil: getStubPluginServices(), + screenshotMode: screenshotModePluginMock.createSetupContract(), }; container = new DashboardContainer(getSampleDashboardInput(), containerOptions); diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx index 99665d312d32..fc4c6b299284 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx @@ -23,6 +23,7 @@ import { } from '../../services/embeddable_test_samples'; import { ErrorEmbeddable, IContainer, isErrorEmbeddable } from '../../services/embeddable'; import { getStubPluginServices } from '../../../../presentation_util/public'; +import { screenshotModePluginMock } from '../../../../screenshot_mode/public/mocks'; const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory( @@ -56,6 +57,7 @@ beforeEach(async () => { uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, presentationUtil: getStubPluginServices(), + screenshotMode: screenshotModePluginMock.createSetupContract(), }; const input = getSampleDashboardInput({ panels: { diff --git a/src/plugins/dashboard/public/application/actions/expand_panel_action.test.tsx b/src/plugins/dashboard/public/application/actions/expand_panel_action.test.tsx index 063515233299..b20a96c79aed 100644 --- a/src/plugins/dashboard/public/application/actions/expand_panel_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/expand_panel_action.test.tsx @@ -13,6 +13,7 @@ import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helper import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; import { isErrorEmbeddable } from '../../services/embeddable'; import { getStubPluginServices } from '../../../../presentation_util/public'; +import { screenshotModePluginMock } from '../../../../screenshot_mode/public/mocks'; import { CONTACT_CARD_EMBEDDABLE, @@ -48,6 +49,7 @@ beforeEach(async () => { uiSettings: uiSettingsServiceMock.createStartContract(), http: coreMock.createStart().http, presentationUtil: getStubPluginServices(), + screenshotMode: screenshotModePluginMock.createSetupContract(), }; const input = getSampleDashboardInput({ panels: { diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx index 51c64f187537..797765eda232 100644 --- a/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx @@ -25,6 +25,7 @@ import { DataPublicPluginStart } from '../../../../data/public/types'; import { dataPluginMock } from '../../../../data/public/mocks'; import { LINE_FEED_CHARACTER } from 'src/plugins/data/common/exports/export_csv'; import { getStubPluginServices } from '../../../../presentation_util/public'; +import { screenshotModePluginMock } from '../../../../screenshot_mode/public/mocks'; describe('Export CSV action', () => { const { setup, doStart } = embeddablePluginMock.createInstance(); @@ -61,6 +62,7 @@ describe('Export CSV action', () => { uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, presentationUtil: getStubPluginServices(), + screenshotMode: screenshotModePluginMock.createSetupContract(), }; const input = getSampleDashboardInput({ panels: { diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx index 587f741461bb..ab442bf839e3 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx @@ -28,6 +28,7 @@ import { CONTACT_CARD_EMBEDDABLE, } from '../../services/embeddable_test_samples'; import { getStubPluginServices } from '../../../../presentation_util/public'; +import { screenshotModePluginMock } from '../../../../screenshot_mode/public/mocks'; const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory( @@ -62,6 +63,7 @@ beforeEach(async () => { uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, presentationUtil: getStubPluginServices(), + screenshotMode: screenshotModePluginMock.createSetupContract(), }; container = new DashboardContainer(getSampleDashboardInput(), containerOptions); diff --git a/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx b/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx index b5efa0447e65..de1a475fdbd1 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx @@ -29,6 +29,7 @@ import { ContactCardEmbeddable, } from '../../services/embeddable_test_samples'; import { getStubPluginServices } from '../../../../presentation_util/public'; +import { screenshotModePluginMock } from '../../../../screenshot_mode/public/mocks'; describe('LibraryNotificationPopover', () => { const { setup, doStart } = embeddablePluginMock.createInstance(); @@ -58,6 +59,7 @@ describe('LibraryNotificationPopover', () => { uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, presentationUtil: getStubPluginServices(), + screenshotMode: screenshotModePluginMock.createSetupContract(), }; container = new DashboardContainer(getSampleDashboardInput(), containerOptions); diff --git a/src/plugins/dashboard/public/application/actions/replace_panel_action.test.tsx b/src/plugins/dashboard/public/application/actions/replace_panel_action.test.tsx index f8880ac5618f..fe39f6112a7f 100644 --- a/src/plugins/dashboard/public/application/actions/replace_panel_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/replace_panel_action.test.tsx @@ -22,6 +22,7 @@ import { ContactCardEmbeddableOutput, } from '../../services/embeddable_test_samples'; import { getStubPluginServices } from '../../../../presentation_util/public'; +import { screenshotModePluginMock } from '../../../../screenshot_mode/public/mocks'; const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory( @@ -48,6 +49,7 @@ beforeEach(async () => { uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, presentationUtil: getStubPluginServices(), + screenshotMode: screenshotModePluginMock.createSetupContract(), }; const input = getSampleDashboardInput({ panels: { diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx index 7d87c49bda64..4f10f833f643 100644 --- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx @@ -30,6 +30,7 @@ import { CONTACT_CARD_EMBEDDABLE, } from '../../services/embeddable_test_samples'; import { getStubPluginServices } from '../../../../presentation_util/public'; +import { screenshotModePluginMock } from '../../../../screenshot_mode/public/mocks'; const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory( @@ -57,6 +58,7 @@ beforeEach(async () => { uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, presentationUtil: getStubPluginServices(), + screenshotMode: screenshotModePluginMock.createSetupContract(), }; container = new DashboardContainer(getSampleDashboardInput(), containerOptions); diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index 3e6566f0da0a..7aedbe9e1100 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -17,11 +17,11 @@ import { getDashboardTitle, leaveConfirmStrings, } from '../dashboard_strings'; -import { EmbeddableRenderer } from '../services/embeddable'; +import { createDashboardEditUrl } from '../dashboard_constants'; +import { EmbeddableRenderer, ViewMode } from '../services/embeddable'; import { DashboardTopNav, isCompleteDashboardAppState } from './top_nav/dashboard_top_nav'; import { DashboardAppServices, DashboardEmbedSettings, DashboardRedirect } from '../types'; import { createKbnUrlStateStorage, withNotifyOnErrors } from '../services/kibana_utils'; -import { createDashboardEditUrl } from '../dashboard_constants'; export interface DashboardAppProps { history: History; savedDashboardId?: string; @@ -51,7 +51,6 @@ export function DashboardApp({ const dashboardState = useDashboardSelector((state) => state.dashboardStateReducer); const dashboardAppState = useDashboardAppState({ history, - redirectTo, savedDashboardId, kbnUrlStateStorage, isEmbeddedExternally: Boolean(embedSettings), @@ -101,15 +100,26 @@ export function DashboardApp({ }; }, [data.search.session]); + const printMode = useMemo( + () => dashboardAppState.getLatestDashboardState?.().viewMode === ViewMode.PRINT, + [dashboardAppState] + ); + + useEffect(() => { + if (!embedSettings) chrome.setIsVisible(!printMode); + }, [chrome, printMode, embedSettings]); + return ( <> {isCompleteDashboardAppState(dashboardAppState) && ( <> - + {!printMode && ( + + )} {dashboardAppState.savedDashboard.outcome === 'conflict' && dashboardAppState.savedDashboard.id && diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index 4a22899c12e8..c74ac506e480 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -109,6 +109,7 @@ export async function mountApp({ embeddable: embeddableStart, uiSettings: coreStart.uiSettings, scopedHistory: () => scopedHistory, + screenshotModeService: screenshotMode, indexPatterns: dataStart.indexPatterns, savedQueryService: dataStart.query.savedQueries, savedObjectsClient: coreStart.savedObjects.client, @@ -131,7 +132,6 @@ export async function mountApp({ activeSpaceId || 'default' ), spacesService: spacesApi, - screenshotModeService: screenshotMode, }; const getUrlStateStorage = (history: RouteComponentProps['history']) => diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx index 6cd102a4d477..744d63c1ba04 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx @@ -43,11 +43,13 @@ import { getStubPluginServices } from '../../../../presentation_util/public'; const presentationUtil = getStubPluginServices(); const options: DashboardContainerServices = { + // TODO: clean up use of any application: {} as any, embeddable: {} as any, notifications: {} as any, overlays: {} as any, inspector: {} as any, + screenshotMode: {} as any, SavedObjectFinder: () => null, ExitFullScreenButton: () => null, uiActions: {} as any, diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index 54fa1f05b9c0..d7081bf020d8 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -40,6 +40,7 @@ import { import { PLACEHOLDER_EMBEDDABLE } from './placeholder'; import { DashboardAppCapabilities, DashboardContainerInput } from '../../types'; import { PresentationUtilPluginStart } from '../../services/presentation_util'; +import type { ScreenshotModePluginStart } from '../../services/screenshot_mode'; import { PanelPlacementMethod, IPanelPlacementArgs } from './panel/dashboard_panel_placement'; import { combineDashboardFiltersWithControlGroupFilters, @@ -55,6 +56,7 @@ export interface DashboardContainerServices { application: CoreStart['application']; inspector: InspectorStartContract; overlays: CoreStart['overlays']; + screenshotMode: ScreenshotModePluginStart; uiSettings: IUiSettingsClient; embeddable: EmbeddableStart; uiActions: UiActionsStart; diff --git a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx index 52f04bcead66..7518a36433d3 100644 --- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx @@ -23,6 +23,7 @@ import { } from '../../../services/embeddable_test_samples'; import { coreMock, uiSettingsServiceMock } from '../../../../../../core/public/mocks'; import { getStubPluginServices } from '../../../../../presentation_util/public'; +import { screenshotModePluginMock } from '../../../../../screenshot_mode/public/mocks'; let dashboardContainer: DashboardContainer | undefined; const presentationUtil = getStubPluginServices(); @@ -71,6 +72,7 @@ function prepare(props?: Partial) { uiSettings: uiSettingsServiceMock.createStartContract(), http: coreMock.createStart().http, presentationUtil, + screenshotMode: screenshotModePluginMock.createSetupContract(), }; dashboardContainer = new DashboardContainer(initialInput, options); const defaultTestProps: DashboardGridProps = { diff --git a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx index 9d2afdba36db..09ac0c1dd94b 100644 --- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx @@ -154,7 +154,7 @@ class DashboardGridUi extends React.Component { id: 'dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage', defaultMessage: 'Unable to load dashboard.', }), - body: error.message, + body: (error as { message: string }).message, toastLifeTimeMs: 5000, }); } @@ -254,6 +254,11 @@ class DashboardGridUi extends React.Component { /> )); + // in print mode, dashboard layout is not controlled by React Grid Layout + if (viewMode === ViewMode.PRINT) { + return <>{dashboardPanels}; + } + return ( ; type DivProps = Pick, 'className' | 'style' | 'children'>; @@ -20,6 +21,7 @@ type DivProps = Pick, 'className' | 'style' interface Props extends PanelProps, DivProps { id: DashboardPanelState['explicitInput']['id']; type: DashboardPanelState['type']; + container: DashboardContainer; focusedPanelId?: string; expandedPanelId?: string; key: string; @@ -52,6 +54,8 @@ const Item = React.forwardRef( 'dshDashboardGrid__item--expanded': expandPanel, // eslint-disable-next-line @typescript-eslint/naming-convention 'dshDashboardGrid__item--hidden': hidePanel, + // eslint-disable-next-line @typescript-eslint/naming-convention + printViewport__vis: container.getInput().viewMode === ViewMode.PRINT, }); return ( @@ -116,7 +120,8 @@ export const ObservedItem: FC = (props: Props) => { export const DashboardGridItem: FC = (props: Props) => { const { isProjectEnabled } = useLabs(); - const isEnabled = isProjectEnabled('labs:dashboard:deferBelowFold'); + const isPrintMode = props.container.getInput().viewMode === ViewMode.PRINT; + const isEnabled = !isPrintMode && isProjectEnabled('labs:dashboard:deferBelowFold'); return isEnabled ? : ; }; diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/_index.scss b/src/plugins/dashboard/public/application/embeddable/viewport/_index.scss index 56483d9d1019..02411f5902b3 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/_index.scss +++ b/src/plugins/dashboard/public/application/embeddable/viewport/_index.scss @@ -1 +1,2 @@ @import './dashboard_viewport'; +@import './print_viewport'; diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/_print_viewport.scss b/src/plugins/dashboard/public/application/embeddable/viewport/_print_viewport.scss new file mode 100644 index 000000000000..a451178cc46b --- /dev/null +++ b/src/plugins/dashboard/public/application/embeddable/viewport/_print_viewport.scss @@ -0,0 +1,9 @@ +.printViewport { + &__vis { + height: 600px; // These values might need to be passed in as dimensions for the report. I.e., print should use layout dimensions. + width: 975px; + + // Some vertical space between vis, but center horizontally + margin: 10px auto; + } +} diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx index 7c671ce7736d..f0333cefd612 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx @@ -27,6 +27,7 @@ import { CONTACT_CARD_EMBEDDABLE, } from '../../../../../embeddable/public/lib/test_samples'; import { getStubPluginServices } from '../../../../../presentation_util/public'; +import { screenshotModePluginMock } from '../../../../../screenshot_mode/public/mocks'; let dashboardContainer: DashboardContainer | undefined; const presentationUtil = getStubPluginServices(); @@ -65,6 +66,7 @@ function getProps(props?: Partial): { getTriggerCompatibleActions: (() => []) as any, } as any, presentationUtil, + screenshotMode: screenshotModePluginMock.createSetupContract(), }; const input = getSampleDashboardInput({ diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx index 5561d1676e41..0ef21fca26f2 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx @@ -52,7 +52,6 @@ const createDashboardAppStateProps = (): UseDashboardStateProps => ({ savedDashboardId: 'testDashboardId', history: createBrowserHistory(), isEmbeddedExternally: false, - redirectTo: jest.fn(), }); const createDashboardAppStateServices = () => { diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts index fddcc309e1ef..cb5c7483f261 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts @@ -22,7 +22,6 @@ import { DashboardBuildContext, DashboardAppServices, DashboardAppState, - DashboardRedirect, DashboardState, } from '../../types'; import { DashboardAppLocatorParams } from '../../locator'; @@ -44,14 +43,12 @@ import { export interface UseDashboardStateProps { history: History; savedDashboardId?: string; - redirectTo: DashboardRedirect; isEmbeddedExternally: boolean; kbnUrlStateStorage: IKbnUrlStateStorage; } export const useDashboardAppState = ({ history, - redirectTo, savedDashboardId, kbnUrlStateStorage, isEmbeddedExternally, @@ -184,12 +181,20 @@ export const useDashboardAppState = ({ savedDashboard, }); + // Backwards compatible way of detecting that we are taking a screenshot + const legacyPrintLayoutDetected = + screenshotModeService?.isScreenshotMode() && + screenshotModeService.getScreenshotLayout() === 'print'; + const initialDashboardState = { ...savedDashboardState, ...dashboardSessionStorageState, ...initialDashboardStateFromUrl, ...forwardedAppState, + // if we are in legacy print mode, dashboard needs to be in print viewMode + ...(legacyPrintLayoutDetected ? { viewMode: ViewMode.PRINT } : {}), + // if there is an incoming embeddable, dashboard always needs to be in edit mode to receive it. ...(incomingEmbeddable ? { viewMode: ViewMode.EDIT } : {}), }; diff --git a/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts b/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts index cce2b4eb042e..616fe56102df 100644 --- a/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts +++ b/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts @@ -15,6 +15,7 @@ import { DashboardAppServices, DashboardAppCapabilities } from '../../types'; import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; import { IndexPatternsContract, SavedQueryService } from '../../services/data'; import { savedObjectsPluginMock } from '../../../../saved_objects/public/mocks'; +import { screenshotModePluginMock } from '../../../../screenshot_mode/public/mocks'; import { visualizationsPluginMock } from '../../../../visualizations/public/mocks'; import { PluginInitializerContext, ScopedHistory } from '../../../../../core/public'; import { SavedObjectLoader, SavedObjectLoaderFindOptions } from '../../services/saved_objects'; @@ -72,6 +73,7 @@ export function makeDefaultServices(): DashboardAppServices { } as PluginInitializerContext; return { + screenshotModeService: screenshotModePluginMock.createSetupContract(), visualizations: visualizationsPluginMock.createStartContract(), savedObjects: savedObjectsPluginMock.createStartContract(), embeddable: embeddablePluginMock.createInstance().doStart(), diff --git a/src/plugins/dashboard/public/dashboard_constants.ts b/src/plugins/dashboard/public/dashboard_constants.ts index 409d80e2ef06..6f9a30e3a704 100644 --- a/src/plugins/dashboard/public/dashboard_constants.ts +++ b/src/plugins/dashboard/public/dashboard_constants.ts @@ -14,6 +14,7 @@ export const DashboardConstants = { LANDING_PAGE_PATH: '/list', CREATE_NEW_DASHBOARD_URL: '/create', VIEW_DASHBOARD_URL: '/view', + PRINT_DASHBOARD_URL: '/print', ADD_EMBEDDABLE_ID: 'addEmbeddableId', ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', DASHBOARDS_ID: 'dashboards', diff --git a/src/plugins/dashboard/public/locator.ts b/src/plugins/dashboard/public/locator.ts index a256a65a5d7f..b6655e246de3 100644 --- a/src/plugins/dashboard/public/locator.ts +++ b/src/plugins/dashboard/public/locator.ts @@ -128,6 +128,7 @@ export class DashboardAppLocatorDefinition implements LocatorDefinition => { diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index ff0ac0642ec9..9912aef94314 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -12,7 +12,6 @@ import { filter, map } from 'rxjs/operators'; import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { UrlForwardingSetup, UrlForwardingStart } from 'src/plugins/url_forwarding/public'; -import { ScreenshotModePluginStart } from 'src/plugins/screenshot_mode/public'; import { APP_WRAPPER_CLASS } from '../../../core/public'; import { App, @@ -37,6 +36,10 @@ import { NavigationPublicPluginStart as NavigationStart } from './services/navig import { DataPublicPluginSetup, DataPublicPluginStart, esFilters } from './services/data'; import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from './services/share'; import type { SavedObjectTaggingOssPluginStart } from './services/saved_objects_tagging_oss'; +import type { + ScreenshotModePluginSetup, + ScreenshotModePluginStart, +} from './services/screenshot_mode'; import { getSavedObjectFinder, SavedObjectLoader, @@ -102,6 +105,7 @@ export interface DashboardSetupDependencies { share?: SharePluginSetup; uiActions: UiActionsSetup; usageCollection?: UsageCollectionSetup; + screenshotMode: ScreenshotModePluginSetup; } export interface DashboardStartDependencies { @@ -116,9 +120,9 @@ export interface DashboardStartDependencies { savedObjects: SavedObjectsStart; presentationUtil: PresentationUtilPluginStart; savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart; - screenshotMode?: ScreenshotModePluginStart; spaces?: SpacesPluginStart; visualizations: VisualizationsStart; + screenshotMode: ScreenshotModePluginStart; } export interface DashboardSetup { @@ -162,7 +166,15 @@ export class DashboardPlugin public setup( core: CoreSetup, - { share, embeddable, home, urlForwarding, data, usageCollection }: DashboardSetupDependencies + { + share, + embeddable, + home, + urlForwarding, + data, + usageCollection, + screenshotMode, + }: DashboardSetupDependencies ): DashboardSetup { this.dashboardFeatureFlagConfig = this.initializerContext.config.get(); @@ -197,6 +209,7 @@ export class DashboardPlugin embeddable: deps.embeddable, uiActions: deps.uiActions, inspector: deps.inspector, + screenshotMode: deps.screenshotMode, http: coreStart.http, ExitFullScreenButton, presentationUtil: deps.presentationUtil, diff --git a/src/plugins/dashboard/public/services/screenshot_mode.ts b/src/plugins/dashboard/public/services/screenshot_mode.ts new file mode 100644 index 000000000000..12ec1bca2207 --- /dev/null +++ b/src/plugins/dashboard/public/services/screenshot_mode.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type { + ScreenshotModePluginStart, + ScreenshotModePluginSetup, +} from '../../../screenshot_mode/public'; + +export type { Layout } from '../../../screenshot_mode/common'; diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index d4a6cb20bc55..b7b146aeba34 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -19,7 +19,6 @@ import type { import { History } from 'history'; import { AnyAction, Dispatch } from 'redux'; import { BehaviorSubject, Subject } from 'rxjs'; -import { ScreenshotModePluginStart } from 'src/plugins/screenshot_mode/public'; import { Query, Filter, IndexPattern, RefreshInterval, TimeRange } from './services/data'; import { ContainerInput, EmbeddableInput, ViewMode } from './services/embeddable'; import { SharePluginStart } from './services/share'; @@ -33,6 +32,7 @@ import { SavedObjectsTaggingApi } from './services/saved_objects_tagging_oss'; import { DataPublicPluginStart, IndexPatternsContract } from './services/data'; import { SavedObjectLoader, SavedObjectsStart } from './services/saved_objects'; import { IKbnUrlStateStorage } from './services/kibana_utils'; +import type { ScreenshotModePluginStart } from './services/screenshot_mode'; import type { DashboardContainer, DashboardSavedObject } from '.'; import { VisualizationsStart } from '../../visualizations/public'; import { DashboardAppLocatorParams } from './locator'; @@ -206,9 +206,9 @@ export interface DashboardAppServices { onAppLeave: AppMountParameters['onAppLeave']; savedObjectsTagging?: SavedObjectsTaggingApi; savedObjectsClient: SavedObjectsClientContract; + screenshotModeService: ScreenshotModePluginStart; dashboardSessionStorage: DashboardSessionStorage; setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; savedQueryService: DataPublicPluginStart['query']['savedQueries']; spacesService?: SpacesPluginStart; - screenshotModeService?: ScreenshotModePluginStart; } diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts index c3cac2d5d67d..b9d9d4cc3414 100644 --- a/src/plugins/embeddable/common/types.ts +++ b/src/plugins/embeddable/common/types.ts @@ -13,6 +13,7 @@ import { PersistableStateService, PersistableState } from '../../kibana_utils/co export enum ViewMode { EDIT = 'edit', PREVIEW = 'preview', + PRINT = 'print', VIEW = 'view', } diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index 9807a47698a5..6748e9f3b1d0 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -251,7 +251,7 @@ export class EmbeddablePanel extends React.Component { }; public render() { - const viewOnlyMode = this.state.viewMode === ViewMode.VIEW; + const viewOnlyMode = [ViewMode.VIEW, ViewMode.PRINT].includes(this.state.viewMode); const classes = classNames('embPanel', { 'embPanel--editing': !viewOnlyMode, 'embPanel--loading': this.state.loading, diff --git a/src/plugins/newsfeed/public/plugin.test.ts b/src/plugins/newsfeed/public/plugin.test.ts index 4be69feb79f5..3497a1e52697 100644 --- a/src/plugins/newsfeed/public/plugin.test.ts +++ b/src/plugins/newsfeed/public/plugin.test.ts @@ -10,6 +10,7 @@ import { take } from 'rxjs/operators'; import { coreMock } from '../../../core/public/mocks'; import { NewsfeedPublicPlugin } from './plugin'; import { NewsfeedApiEndpoint } from './lib/api'; +import { screenshotModePluginMock } from '../../screenshot_mode/public/mocks'; describe('Newsfeed plugin', () => { let plugin: NewsfeedPublicPlugin; @@ -46,7 +47,7 @@ describe('Newsfeed plugin', () => { describe('base case', () => { it('makes fetch requests', () => { const startContract = plugin.start(coreMock.createStart(), { - screenshotMode: { isScreenshotMode: () => false }, + screenshotMode: screenshotModePluginMock.createSetupContract(), }); const sub = startContract .createNewsFeed$(NewsfeedApiEndpoint.KIBANA) // Any endpoint will do @@ -60,8 +61,10 @@ describe('Newsfeed plugin', () => { describe('when in screenshot mode', () => { it('makes no fetch requests in screenshot mode', () => { + const screenshotMode = screenshotModePluginMock.createSetupContract(); + screenshotMode.isScreenshotMode.mockReturnValue(true); const startContract = plugin.start(coreMock.createStart(), { - screenshotMode: { isScreenshotMode: () => true }, + screenshotMode, }); const sub = startContract .createNewsFeed$(NewsfeedApiEndpoint.KIBANA) // Any endpoint will do diff --git a/src/plugins/screenshot_mode/common/get_set_browser_screenshot_mode.ts b/src/plugins/screenshot_mode/common/get_set_browser_screenshot_mode.ts index ff79ccf0126f..850f70d2d002 100644 --- a/src/plugins/screenshot_mode/common/get_set_browser_screenshot_mode.ts +++ b/src/plugins/screenshot_mode/common/get_set_browser_screenshot_mode.ts @@ -7,7 +7,7 @@ */ // **PLEASE NOTE** -// The functionality in this file targets a browser environment and is intended to be used both in public and server. +// The functionality in this file targets a browser environment AND is intended to be used both in public and server. // For instance, reporting uses these functions when starting puppeteer to set the current browser into "screenshot" mode. export const KBN_SCREENSHOT_MODE_ENABLED_KEY = '__KBN_SCREENSHOT_MODE_ENABLED_KEY__'; @@ -61,3 +61,31 @@ export const setScreenshotModeDisabled = () => { } ); }; + +/** @deprecated */ +export const KBN_SCREENSHOT_MODE_LAYOUT_KEY = '__KBN_SCREENSHOT_MODE_LAYOUT_KEY__'; + +/** @deprecated */ +export type Layout = 'canvas' | 'preserve_layout' | 'print'; + +/** @deprecated */ +export const setScreenshotLayout = (value: Layout) => { + Object.defineProperty( + window, + '__KBN_SCREENSHOT_MODE_LAYOUT_KEY__', // Literal value to prevent adding an external reference + { + enumerable: true, + writable: true, + configurable: false, + value, + } + ); +}; + +/** @deprecated */ +export const getScreenshotLayout = (): undefined | Layout => { + return ( + (window as unknown as Record)[KBN_SCREENSHOT_MODE_LAYOUT_KEY] || + (window.localStorage.getItem(KBN_SCREENSHOT_MODE_LAYOUT_KEY) as Layout) + ); +}; diff --git a/src/plugins/screenshot_mode/common/index.ts b/src/plugins/screenshot_mode/common/index.ts index 9c8c3d24ef28..949691911fc2 100644 --- a/src/plugins/screenshot_mode/common/index.ts +++ b/src/plugins/screenshot_mode/common/index.ts @@ -11,6 +11,11 @@ export { setScreenshotModeEnabled, setScreenshotModeDisabled, KBN_SCREENSHOT_MODE_ENABLED_KEY, + KBN_SCREENSHOT_MODE_LAYOUT_KEY, + setScreenshotLayout, + getScreenshotLayout, } from './get_set_browser_screenshot_mode'; +export type { Layout } from './get_set_browser_screenshot_mode'; + export { KBN_SCREENSHOT_MODE_HEADER } from './constants'; diff --git a/src/plugins/screenshot_mode/public/mocks.ts b/src/plugins/screenshot_mode/public/mocks.ts index 7fa93ff0bcea..d7e69e9d8921 100644 --- a/src/plugins/screenshot_mode/public/mocks.ts +++ b/src/plugins/screenshot_mode/public/mocks.ts @@ -11,9 +11,11 @@ import type { ScreenshotModePluginSetup, ScreenshotModePluginStart } from './typ export const screenshotModePluginMock = { createSetupContract: (): DeeplyMockedKeys => ({ + getScreenshotLayout: jest.fn(), isScreenshotMode: jest.fn(() => false), }), createStartContract: (): DeeplyMockedKeys => ({ + getScreenshotLayout: jest.fn(), isScreenshotMode: jest.fn(() => false), }), }; diff --git a/src/plugins/screenshot_mode/public/plugin.ts b/src/plugins/screenshot_mode/public/plugin.ts index a005bb7c3d05..bb34fe84e2c3 100644 --- a/src/plugins/screenshot_mode/public/plugin.ts +++ b/src/plugins/screenshot_mode/public/plugin.ts @@ -10,11 +10,12 @@ import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { ScreenshotModePluginSetup, ScreenshotModePluginStart } from './types'; -import { getScreenshotMode } from '../common'; +import { getScreenshotMode, getScreenshotLayout } from '../common'; export class ScreenshotModePlugin implements Plugin { private publicContract = Object.freeze({ isScreenshotMode: () => getScreenshotMode() === true, + getScreenshotLayout, }); public setup(core: CoreSetup): ScreenshotModePluginSetup { diff --git a/src/plugins/screenshot_mode/public/types.ts b/src/plugins/screenshot_mode/public/types.ts index f6963de0cbd6..d1603cbceb26 100644 --- a/src/plugins/screenshot_mode/public/types.ts +++ b/src/plugins/screenshot_mode/public/types.ts @@ -6,12 +6,17 @@ * Side Public License, v 1. */ +import type { Layout } from '../common'; + export interface IScreenshotModeService { /** * Returns a boolean indicating whether the current user agent (browser) would like to view UI optimized for * screenshots or printing. */ isScreenshotMode: () => boolean; + + /** @deprecated */ + getScreenshotLayout: () => undefined | Layout; } export type ScreenshotModePluginSetup = IScreenshotModeService; diff --git a/src/plugins/screenshot_mode/server/plugin.ts b/src/plugins/screenshot_mode/server/plugin.ts index 9295451f640c..b885ff97bf26 100644 --- a/src/plugins/screenshot_mode/server/plugin.ts +++ b/src/plugins/screenshot_mode/server/plugin.ts @@ -30,10 +30,11 @@ export class ScreenshotModePlugin // We use "require" here to ensure the import does not have external references due to code bundling that // commonly happens during transpiling. External references would be missing in the environment puppeteer creates. // eslint-disable-next-line @typescript-eslint/no-var-requires - const { setScreenshotModeEnabled } = require('../common'); + const { setScreenshotModeEnabled, setScreenshotLayout } = require('../common'); return { setScreenshotModeEnabled, + setScreenshotLayout, isScreenshotMode, }; } diff --git a/src/plugins/screenshot_mode/server/types.ts b/src/plugins/screenshot_mode/server/types.ts index 566ae1971945..1b9f3868f096 100644 --- a/src/plugins/screenshot_mode/server/types.ts +++ b/src/plugins/screenshot_mode/server/types.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { RequestHandlerContext, KibanaRequest } from 'src/core/server'; +import type { RequestHandlerContext, KibanaRequest } from 'src/core/server'; +import type { Layout } from '../common'; /** * Any context that requires access to the screenshot mode flag but does not have access @@ -23,6 +24,9 @@ export interface ScreenshotModePluginSetup { * on the page have run to ensure that screenshot mode is detected as early as possible. */ setScreenshotModeEnabled: () => void; + + /** @deprecated */ + setScreenshotLayout: (value: Layout) => void; } export interface ScreenshotModePluginStart { diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index 0947d24f827c..0f2572ff2b2e 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -17,7 +17,7 @@ import { ReportingCore } from '../../..'; import { KBN_SCREENSHOT_MODE_HEADER } from '../../../../../../../src/plugins/screenshot_mode/server'; import { ConditionalHeaders, ConditionalHeadersConditions } from '../../../export_types/common'; import { LevelLogger } from '../../../lib'; -import { ViewZoomWidthHeight } from '../../../lib/layouts/layout'; +import { Layout, ViewZoomWidthHeight } from '../../../lib/layouts/layout'; import { ElementPosition } from '../../../lib/screenshots'; import { allowRequest, NetworkPolicy } from '../../network_policy'; @@ -97,11 +97,13 @@ export class HeadlessChromiumDriver { waitForSelector: pageLoadSelector, timeout, locator, + layout, }: { conditionalHeaders: ConditionalHeaders; waitForSelector: string; timeout: number; locator?: LocatorParams; + layout?: Layout; }, logger: LevelLogger ): Promise { @@ -116,6 +118,10 @@ export class HeadlessChromiumDriver { */ await this.page.evaluateOnNewDocument(this.core.getEnableScreenshotMode()); + if (layout) { + await this.page.evaluateOnNewDocument(this.core.getSetScreenshotLayout(), layout.id); + } + if (locator) { await this.page.evaluateOnNewDocument( (key: string, value: unknown) => { diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index bc74f5463ba3..43aefb73aebb 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -253,9 +253,16 @@ export class ReportingCore { .toPromise(); } + private getScreenshotModeDep() { + return this.getPluginSetupDeps().screenshotMode; + } + public getEnableScreenshotMode() { - const { screenshotMode } = this.getPluginSetupDeps(); - return screenshotMode.setScreenshotModeEnabled; + return this.getScreenshotModeDep().setScreenshotModeEnabled; + } + + public getSetScreenshotLayout() { + return this.getScreenshotModeDep().setScreenshotLayout; } /* diff --git a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css index 508d217cdd03..60513c417165 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css +++ b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css @@ -5,7 +5,7 @@ ****** */ - /** +/** * global */ @@ -27,7 +27,6 @@ filter-bar, * Discover Tweaks */ - /* hide unusable controls */ discover-app .dscTimechart, discover-app .dscSidebar__container, @@ -36,7 +35,6 @@ discover-app .discover-table-footer { display: none; } - /** * The global banner (e.g. "Help us improve Elastic...") should not print. */ @@ -73,7 +71,8 @@ discover-app .discover-table-footer { position: fixed; width: 100%; height: 100%; - top: 0; left: 0; + top: 0; + left: 0; } /** diff --git a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts index 424e85327c22..7f6bc9e5d950 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import path from 'path'; import { CustomPageSize } from 'pdfmake/interfaces'; import { LAYOUT_TYPES } from '../../../common/constants'; @@ -37,6 +36,7 @@ export class PreserveLayout extends Layout implements LayoutInstance { } public getCssOverridesPath() { + // TODO: Remove this path once we have migrated all plugins away from depending on this hiding page elements. return path.join(__dirname, 'preserve_layout.css'); } diff --git a/x-pack/plugins/reporting/server/lib/layouts/print.css b/x-pack/plugins/reporting/server/lib/layouts/print.css deleted file mode 100644 index 7d38ebe81e4e..000000000000 --- a/x-pack/plugins/reporting/server/lib/layouts/print.css +++ /dev/null @@ -1,122 +0,0 @@ -/* - ****** - ****** This is a collection of CSS overrides that make Kibana look better for - ****** generating PDF reports with headless browser - ****** - */ - -/** - * global - */ - -/* elements can hide themselves when shared */ -.hide-for-sharing { - display: none !important; -} - -/* hide unusable controls */ -kbn-top-nav, -filter-bar, -.kbnTopNavMenu__wrapper, -::-webkit-scrollbar, -.euiNavDrawer { - display: none !important; -} - -/** - * Discover Tweaks - */ - -/* hide unusable controls */ -discover-app .dscTimechart, -discover-app .dscSidebar__container, -discover-app .dscCollapsibleSidebar__collapseButton, -discover-app .discover-table-footer { - display: none; -} - -/** - * The global banner (e.g. "Help us improve Elastic...") should not print. - */ - -#globalBannerList { - display: none; -} - -/** - * Visualize Editor Tweaks - */ - -/* hide unusable controls -* !important is required to override resizable panel inline display */ -.visEditor__content .visEditor--default > :not(.visEditor__visualization__wrapper) { - display: none !important; -} -/** THIS IS FOR TSVB UNTIL REFACTOR **/ -.tvbEditorVisualization { - position: static !important; -} -.visualize .tvbVisTimeSeries__legendToggle, -.tvbEditor--hideForReporting { - /* all non-content rows in interface */ - display: none; -} -/** END TSVB BAD BAD HACKS **/ - -/* remove left padding from visualizations so that map lines up with .leaflet-container and -* setting the position to be fixed and to take up the entire screen, because some zoom levels/viewports -* are triggering the media breakpoints that cause the .visEditor__canvas to take up more room than the viewport */ -.visEditor .visEditor__canvas { - padding-left: 0px; - position: fixed; - width: 100%; - height: 100%; - top: 0; - left: 0; -} - -/** - * Visualization tweaks - */ - -/* hide unusable controls */ -.visualize .visLegend__toggle, -.visualize .kbnAggTable__controls/* export raw, export formatted, etc. */, -.visualize .leaflet-container .leaflet-top.leaflet-left/* tilemap controls */, -.visualize paginate-controls { - display: none; -} - -/* Ensure the min-height of the small breakpoint isn't used */ -.vis-editor visualization { - min-height: 0 !important; -} - -/** -* Dashboard tweaks -*/ - -.dashboardViewport .embPanel__header { - /* hide the panel heading with the controls and title */ - display: none !important; -} - -.dashboardViewport .euiPanel { - /* Remove the border from the eui panel */ - border: none !important; -} - -/** - * 1. Reporting manually makes each visualization it wants to screenshot larger, so we need to hide - * the visualizations in the other panels. We can only use properties that will be manually set in - * reporting/export_types/printable_pdf/lib/screenshot.js or this will also hide the visualization - * we want to capture. - * 2. React grid item's transform affects the visualizations, even when they are using fixed positioning. Chrome seems - * to handle this fine, but firefox moves the visualizations around. - */ -.dashboardViewport .react-grid-item { - height: 0 !important; /* 1. */ - width: 0 !important; /* 1. */ - transform: none !important; /* 2. */ - -webkit-transform: none !important; /* 2. */ -} diff --git a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts index 0849f8850f91..68226affb41e 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts @@ -5,13 +5,8 @@ * 2.0. */ -import path from 'path'; import { PageOrientation, PredefinedPageSize } from 'pdfmake/interfaces'; -import { EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; -import { LevelLogger } from '../'; import { DEFAULT_VIEWPORT, LAYOUT_TYPES } from '../../../common/constants'; -import { Size } from '../../../common/types'; -import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig } from '../../types'; import { getDefaultLayoutSelectors, LayoutInstance, LayoutSelectorDictionary } from './'; import { Layout } from './layout'; @@ -31,7 +26,7 @@ export class PrintLayout extends Layout implements LayoutInstance { } public getCssOverridesPath() { - return path.join(__dirname, 'print.css'); + return undefined; } public getBrowserViewport() { @@ -49,40 +44,6 @@ export class PrintLayout extends Layout implements LayoutInstance { height: this.viewport.height * itemsCount, }; } - - public async positionElements( - browser: HeadlessChromiumDriver, - logger: LevelLogger - ): Promise { - logger.debug('positioning elements'); - - const elementSize: Size = { - width: this.viewport.width / this.captureConfig.zoom, - height: this.viewport.height / this.captureConfig.zoom, - }; - const evalOptions: { fn: EvaluateFn; args: SerializableOrJSHandle[] } = { - fn: (selector: string, height: number, width: number) => { - const visualizations = document.querySelectorAll(selector) as NodeListOf; - const visualizationsLength = visualizations.length; - - for (let i = 0; i < visualizationsLength; i++) { - const visualization = visualizations[i]; - const style = visualization.style; - style.position = 'fixed'; - style.top = `${height * i}px`; - style.left = '0'; - style.width = `${width}px`; - style.height = `${height}px`; - style.zIndex = '1'; - style.backgroundColor = 'inherit'; - } - }, - args: [this.selectors.screenshot, elementSize.height, elementSize.width], - }; - - await browser.evaluate(evalOptions, { context: 'PositionElements' }, logger); - } - public getPdfImageSize() { return { width: 500, diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.ts index 1db313b09102..cdbddb8d89c8 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.ts @@ -76,6 +76,7 @@ export class ScreenshotObservableHandler { index, urlOrUrlLocatorTuple, this.conditionalHeaders, + this.layout, this.logger ) ).pipe(this.waitUntil(this.timeouts.openUrl)); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts index 63a5e80289e3..b26037aa917b 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts @@ -10,6 +10,7 @@ import { LevelLogger, startTrace } from '../'; import { LocatorParams, UrlOrUrlLocatorTuple } from '../../../common/types'; import { HeadlessChromiumDriver } from '../../browsers'; import { ConditionalHeaders } from '../../export_types/common'; +import { Layout } from '../layouts'; import { DEFAULT_PAGELOAD_SELECTOR } from './constants'; export const openUrl = async ( @@ -18,6 +19,7 @@ export const openUrl = async ( index: number, urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple, conditionalHeaders: ConditionalHeaders, + layout: undefined | Layout, logger: LevelLogger ): Promise => { // If we're moving to another page in the app, we'll want to wait for the app to tell us @@ -36,7 +38,11 @@ export const openUrl = async ( } try { - await browser.open(url, { conditionalHeaders, waitForSelector, timeout, locator }, logger); + await browser.open( + url, + { conditionalHeaders, waitForSelector, timeout, locator, layout }, + logger + ); } catch (err) { logger.error(err); throw new Error( From 71166d24e52306359cf9ff8294956fb4b6b83ebf Mon Sep 17 00:00:00 2001 From: David Roberts Date: Mon, 29 Nov 2021 14:48:46 +0000 Subject: [PATCH 012/224] [Upgrade assistant] Fix the "Fix" button for ML snapshots in need of upgrade (#119745) The deprecations for ML snapshots that are too old and need upgrading are supposed to have a "Fix" button next to them. Unfortunately this disappeared when a message was reworded in elastic/elasticsearch#79387. This change adjusts the regex that the upgrade assistant uses to detect which deprecations to add the "Fix" button to so that it will match both "Model snapshot" and "model snapshot". The test data is also updated to match the new backend text. This change is designed to work with elastic/elasticsearch#81060. --- .../upgrade_assistant/server/lib/es_deprecations_status.ts | 2 +- .../plugins/upgrade_assistant/server/routes/ml_snapshots.ts | 2 +- .../plugins/upgrade_assistant/server/routes/status.test.ts | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.ts index 2e2c80b790cd..e334d214f246 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.ts @@ -139,7 +139,7 @@ const getCorrectiveAction = ( ); const requiresReindexAction = /Index created before/.test(message); const requiresIndexSettingsAction = Boolean(indexSettingDeprecation); - const requiresMlAction = /model snapshot/.test(message); + const requiresMlAction = /[Mm]odel snapshot/.test(message); if (requiresReindexAction) { return { diff --git a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts index fa6af0f5e422..69c7c24b6312 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts @@ -63,7 +63,7 @@ const verifySnapshotUpgrade = async ( const { body: deprecations } = await esClient.asCurrentUser.migration.deprecations(); const mlSnapshotDeprecations = deprecations.ml_settings.filter((deprecation) => { - return /model snapshot/.test(deprecation.message); + return /[Mm]odel snapshot/.test(deprecation.message); }); // If there are no ML deprecations, we assume the deprecation was resolved successfully diff --git a/x-pack/plugins/upgrade_assistant/server/routes/status.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/status.test.ts index e442d3b4fd11..b11993e2baa5 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/status.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/status.test.ts @@ -49,9 +49,8 @@ describe('Status API', () => { { level: 'critical', message: - 'model snapshot [1] for job [deprecation_check_job] needs to be deleted or upgraded', - details: - 'model snapshot [%s] for job [%s] supports minimum version [%s] and needs to be at least [%s]', + 'Model snapshot [1] for job [deprecation_check_job] has an obsolete minimum version [6.3.0].', + details: 'Delete model snapshot [1] or update it to 7.0.0 or greater.', url: 'doc_url', correctiveAction: { type: 'mlSnapshot', From 81374de4f611e1df4e99f87c54f6d878e640057e Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 29 Nov 2021 14:59:43 +0000 Subject: [PATCH 013/224] skip flaky suite (#116865) --- x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index bb89fa8f683f..1d8a172e57b7 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -142,7 +142,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - describe('tls alert', function () { + // FLAKY: https://github.com/elastic/kibana/issues/116865 + describe.skip('tls alert', function () { const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; let alerts: any; From ae9d51b7fe3ee6f30d0d196c782e0dcabb7ac5ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Mon, 29 Nov 2021 16:12:27 +0100 Subject: [PATCH 014/224] [APM] Remove log-log descriptions from correlation charts (#119700) --- .../app/correlations/chart_title_tool_tip.tsx | 25 +++ .../failed_transactions_correlations.tsx | 175 ++++++------------ ...get_transaction_distribution_chart_data.ts | 61 ++++++ .../app/correlations/latency_correlations.tsx | 80 +++----- .../correlations/use_transaction_colors.ts | 17 -- .../distribution/index.tsx | 139 ++++++-------- ...use_transaction_distribution_chart_data.ts | 36 +--- .../transaction_distribution_chart/index.tsx | 14 +- .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - 10 files changed, 233 insertions(+), 322 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/correlations/chart_title_tool_tip.tsx create mode 100644 x-pack/plugins/apm/public/components/app/correlations/get_transaction_distribution_chart_data.ts delete mode 100644 x-pack/plugins/apm/public/components/app/correlations/use_transaction_colors.ts diff --git a/x-pack/plugins/apm/public/components/app/correlations/chart_title_tool_tip.tsx b/x-pack/plugins/apm/public/components/app/correlations/chart_title_tool_tip.tsx new file mode 100644 index 000000000000..ed2bd4985831 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/chart_title_tool_tip.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiIconTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function ChartTitleToolTip() { + return ( + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx index b2efae74c9c7..c642ca7bd577 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx @@ -18,7 +18,6 @@ import { EuiTitle, EuiBetaBadge, EuiBadge, - EuiText, EuiToolTip, EuiSwitch, EuiIconTip, @@ -27,13 +26,11 @@ import type { EuiTableSortingType } from '@elastic/eui/src/components/basic_tabl import type { Direction } from '@elastic/eui/src/services/sort/sort_direction'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import { useUiTracker } from '../../../../../observability/public'; import { asPercent } from '../../../../common/utils/formatters'; import { FailedTransactionsCorrelation } from '../../../../common/correlations/failed_transactions_correlations/types'; -import { DEFAULT_PERCENTILE_THRESHOLD } from '../../../../common/correlations/constants'; import { FieldStats } from '../../../../common/correlations/field_stats_types'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; @@ -48,18 +45,17 @@ import { CorrelationsTable } from './correlations_table'; import { FailedTransactionsCorrelationsHelpPopover } from './failed_transactions_correlations_help_popover'; import { getFailedTransactionsCorrelationImpactLabel } from './utils/get_failed_transactions_correlation_impact_label'; import { getOverallHistogram } from './utils/get_overall_histogram'; -import { - TransactionDistributionChart, - TransactionDistributionChartData, -} from '../../shared/charts/transaction_distribution_chart'; +import { TransactionDistributionChart } from '../../shared/charts/transaction_distribution_chart'; import { CorrelationsEmptyStatePrompt } from './empty_state_prompt'; import { CrossClusterSearchCompatibilityWarning } from './cross_cluster_search_warning'; import { CorrelationsProgressControls } from './progress_controls'; -import { useTransactionColors } from './use_transaction_colors'; import { CorrelationsContextPopover } from './context_popover'; import { OnAddFilter } from './context_popover/top_values'; import { useFailedTransactionsCorrelations } from './use_failed_transactions_correlations'; +import { getTransactionDistributionChartData } from './get_transaction_distribution_chart_data'; +import { ChartTitleToolTip } from './chart_title_tool_tip'; +import { MIN_TAB_TITLE_HEIGHT } from '../transaction_details/distribution'; export function FailedTransactionsCorrelations({ onFilter, @@ -67,7 +63,6 @@ export function FailedTransactionsCorrelations({ onFilter: () => void; }) { const euiTheme = useTheme(); - const transactionColors = useTransactionColors(); const { core: { notifications }, @@ -427,133 +422,75 @@ export function FailedTransactionsCorrelations({ correlationTerms.length < 1 && (progress.loaded === 1 || !progress.isRunning); - const transactionDistributionChartData: TransactionDistributionChartData[] = - []; - - if (Array.isArray(overallHistogram)) { - transactionDistributionChartData.push({ - id: i18n.translate( - 'xpack.apm.transactionDistribution.chart.allTransactionsLabel', - { defaultMessage: 'All transactions' } - ), - histogram: overallHistogram, - }); - } - - if (Array.isArray(response.errorHistogram)) { - transactionDistributionChartData.push({ - id: i18n.translate( - 'xpack.apm.transactionDistribution.chart.failedTransactionsLabel', - { defaultMessage: 'Failed transactions' } - ), - histogram: response.errorHistogram, - }); - } - - if (selectedTerm && Array.isArray(selectedTerm.histogram)) { - transactionDistributionChartData.push({ - id: `${selectedTerm.fieldName}:${selectedTerm.fieldValue}`, - histogram: selectedTerm.histogram, - }); - } + const transactionDistributionChartData = getTransactionDistributionChartData({ + euiTheme, + allTransactionsHistogram: overallHistogram, + failedTransactionsHistogram: response.errorHistogram, + selectedTerm, + }); return (
- - - - -
- {i18n.translate( - 'xpack.apm.correlations.failedTransactions.panelTitle', - { - defaultMessage: 'Failed transactions latency distribution', - } - )} -
-
-
- - - + + +
+ {i18n.translate( + 'xpack.apm.correlations.failedTransactions.panelTitle', { - defaultMessage: - 'Failed transaction correlations is not GA. Please help us by reporting any bugs.', + defaultMessage: 'Failed transactions latency distribution', } )} - /> - - +
+
+
+ + + + + + + + -
- - {selectedTerm && ( - - , - allTransactions: ( - - - - ), - failedTransactions: ( - - - - ), - focusTransaction: ( - - {selectedTerm?.fieldName}:{selectedTerm?.fieldValue} - - ), - }} - /> - - )} +
diff --git a/x-pack/plugins/apm/public/components/app/correlations/get_transaction_distribution_chart_data.ts b/x-pack/plugins/apm/public/components/app/correlations/get_transaction_distribution_chart_data.ts new file mode 100644 index 000000000000..49ddd8aec0fe --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/get_transaction_distribution_chart_data.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; +import type { + FieldValuePair, + HistogramItem, +} from '../../../../common/correlations/types'; +import { TransactionDistributionChartData } from '../../shared/charts/transaction_distribution_chart'; + +export function getTransactionDistributionChartData({ + euiTheme, + allTransactionsHistogram, + failedTransactionsHistogram, + selectedTerm, +}: { + euiTheme: EuiTheme; + allTransactionsHistogram?: HistogramItem[]; + failedTransactionsHistogram?: HistogramItem[]; + selectedTerm?: FieldValuePair & { histogram: HistogramItem[] }; +}) { + const transactionDistributionChartData: TransactionDistributionChartData[] = + []; + + if (Array.isArray(allTransactionsHistogram)) { + transactionDistributionChartData.push({ + id: i18n.translate( + 'xpack.apm.transactionDistribution.chart.allTransactionsLabel', + { defaultMessage: 'All transactions' } + ), + histogram: allTransactionsHistogram, + areaSeriesColor: euiTheme.eui.euiColorVis1, + }); + } + + if (Array.isArray(failedTransactionsHistogram)) { + transactionDistributionChartData.push({ + id: i18n.translate( + 'xpack.apm.transactionDistribution.chart.failedTransactionsLabel', + { defaultMessage: 'Failed transactions' } + ), + histogram: failedTransactionsHistogram, + areaSeriesColor: euiTheme.eui.euiColorVis7, + }); + } + + if (selectedTerm && Array.isArray(selectedTerm.histogram)) { + transactionDistributionChartData.push({ + id: `${selectedTerm.fieldName}:${selectedTerm.fieldValue}`, + histogram: selectedTerm.histogram, + areaSeriesColor: euiTheme.eui.euiColorVis2, + }); + } + + return transactionDistributionChartData; +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx index 629868fb88bf..f79e95559571 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx @@ -15,7 +15,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, - EuiText, EuiTitle, EuiToolTip, } from '@elastic/eui'; @@ -23,22 +22,17 @@ import { Direction } from '@elastic/eui/src/services/sort/sort_direction'; import { EuiTableSortingType } from '@elastic/eui/src/components/basic_table/table_types'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import { useUiTracker } from '../../../../../observability/public'; import { asPreciseDecimal } from '../../../../common/utils/formatters'; -import { DEFAULT_PERCENTILE_THRESHOLD } from '../../../../common/correlations/constants'; import { LatencyCorrelation } from '../../../../common/correlations/latency_correlations/types'; import { FieldStats } from '../../../../common/correlations/field_stats_types'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { - TransactionDistributionChart, - TransactionDistributionChartData, -} from '../../shared/charts/transaction_distribution_chart'; +import { TransactionDistributionChart } from '../../shared/charts/transaction_distribution_chart'; import { push } from '../../shared/Links/url_helpers'; import { CorrelationsTable } from './correlations_table'; @@ -47,18 +41,21 @@ import { getOverallHistogram } from './utils/get_overall_histogram'; import { CorrelationsEmptyStatePrompt } from './empty_state_prompt'; import { CrossClusterSearchCompatibilityWarning } from './cross_cluster_search_warning'; import { CorrelationsProgressControls } from './progress_controls'; -import { useTransactionColors } from './use_transaction_colors'; import { CorrelationsContextPopover } from './context_popover'; import { OnAddFilter } from './context_popover/top_values'; import { useLatencyCorrelations } from './use_latency_correlations'; +import { getTransactionDistributionChartData } from './get_transaction_distribution_chart_data'; +import { useTheme } from '../../../hooks/use_theme'; +import { ChartTitleToolTip } from './chart_title_tool_tip'; +import { MIN_TAB_TITLE_HEIGHT } from '../transaction_details/distribution'; export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { - const transactionColors = useTransactionColors(); - const { core: { notifications }, } = useApmPluginContext(); + const euiTheme = useTheme(); + const { progress, response, startFetch, cancelFetch } = useLatencyCorrelations(); const { overallHistogram, hasData, status } = getOverallHistogram( @@ -274,30 +271,20 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { const showCorrelationsEmptyStatePrompt = histogramTerms.length < 1 && (progress.loaded === 1 || !progress.isRunning); - const transactionDistributionChartData: TransactionDistributionChartData[] = - []; - - if (Array.isArray(overallHistogram)) { - transactionDistributionChartData.push({ - id: i18n.translate( - 'xpack.apm.transactionDistribution.chart.allTransactionsLabel', - { defaultMessage: 'All transactions' } - ), - histogram: overallHistogram, - }); - } - - if (selectedHistogram && Array.isArray(selectedHistogram.histogram)) { - transactionDistributionChartData.push({ - id: `${selectedHistogram.fieldName}:${selectedHistogram.fieldValue}`, - histogram: selectedHistogram.histogram, - }); - } + const transactionDistributionChartData = getTransactionDistributionChartData({ + euiTheme, + allTransactionsHistogram: overallHistogram, + selectedTerm: selectedHistogram, + }); return (
- - + +
{i18n.translate( @@ -309,40 +296,19 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) {
+ + + + +
- {selectedHistogram && ( - - , - allTransactions: ( - - - - ), - focusTransaction: ( - - {selectedHistogram?.fieldName}:{selectedHistogram?.fieldValue} - - ), - }} - /> - - )} - { - const euiTheme = useTheme(); - return { - ALL_TRANSACTIONS: euiTheme.eui.euiColorVis1, - ALL_FAILED_TRANSACTIONS: euiTheme.eui.euiColorVis7, - FOCUS_TRANSACTION: euiTheme.eui.euiColorVis2, - }; -}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx index e6c189ed0c74..a2f6fd493313 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx @@ -18,18 +18,15 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import { useUiTracker } from '../../../../../../observability/public'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; -import { DEFAULT_PERCENTILE_THRESHOLD } from '../../../../../common/correlations/constants'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { TransactionDistributionChart } from '../../../shared/charts/transaction_distribution_chart'; -import { useTransactionColors } from '../../correlations/use_transaction_colors'; import type { TabContentProps } from '../types'; import { useWaterfallFetcher } from '../use_waterfall_fetcher'; @@ -37,10 +34,11 @@ import { WaterfallWithSummary } from '../waterfall_with_summary'; import { useTransactionDistributionChartData } from './use_transaction_distribution_chart_data'; import { HeightRetainer } from '../../../shared/HeightRetainer'; +import { ChartTitleToolTip } from '../../correlations/chart_title_tool_tip'; // Enforce min height so it's consistent across all tabs on the same level // to prevent "flickering" behavior -const MIN_TAB_TITLE_HEIGHT = 56; +export const MIN_TAB_TITLE_HEIGHT = 56; type Selection = [number, number]; @@ -69,7 +67,6 @@ export function TransactionDistribution({ selection, traceSamples, }: TransactionDistributionProps) { - const transactionColors = useTransactionColors(); const { urlParams } = useLegacyUrlParams(); const { waterfall, status: waterfallStatus } = useWaterfallFetcher(); @@ -108,8 +105,12 @@ export function TransactionDistribution({ return (
- - + +
{i18n.translate( @@ -121,93 +122,65 @@ export function TransactionDistribution({
- {hasData && !selection && ( - - - - - - - {emptySelectionText} + + + + + + + + {selection ? ( + + + {i18n.translate( + 'xpack.apm.transactionDetails.distribution.selectionText', + { + defaultMessage: `Selection: {formattedSelection}`, + values: { + formattedSelection: getFormattedSelection(selection), + }, + } + )} + - - - )} - {hasData && selection && ( - - - {i18n.translate( - 'xpack.apm.transactionDetails.distribution.selectionText', - { - defaultMessage: `Selection: {formattedSelection}`, - values: { - formattedSelection: getFormattedSelection(selection), - }, - } - )} - - - )} + ) : ( + <> + + + + + {emptySelectionText} + + + )} + +
- - - - - ), - failedTransactions: ( - - - - ), - }} - /> - - diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/use_transaction_distribution_chart_data.ts b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/use_transaction_distribution_chart_data.ts index a02fc7fe6665..6d690415d8c6 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/use_transaction_distribution_chart_data.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/use_transaction_distribution_chart_data.ts @@ -6,23 +6,20 @@ */ import { useEffect } from 'react'; - import { i18n } from '@kbn/i18n'; - import { DEFAULT_PERCENTILE_THRESHOLD } from '../../../../../common/correlations/constants'; import { EVENT_OUTCOME } from '../../../../../common/elasticsearch_fieldnames'; import { EventOutcome } from '../../../../../common/event_outcome'; - import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher'; - -import type { TransactionDistributionChartData } from '../../../shared/charts/transaction_distribution_chart'; - import { isErrorMessage } from '../../correlations/utils/is_error_message'; import { useFetchParams } from '../../correlations/use_fetch_params'; +import { getTransactionDistributionChartData } from '../../correlations/get_transaction_distribution_chart_data'; +import { useTheme } from '../../../../hooks/use_theme'; export const useTransactionDistributionChartData = () => { const params = useFetchParams(); + const euiTheme = useTheme(); const { core: { notifications }, @@ -122,28 +119,11 @@ export const useTransactionDistributionChartData = () => { } }, [errorHistogramError, notifications.toasts]); - const transactionDistributionChartData: TransactionDistributionChartData[] = - []; - - if (Array.isArray(overallLatencyHistogram)) { - transactionDistributionChartData.push({ - id: i18n.translate( - 'xpack.apm.transactionDistribution.chart.allTransactionsLabel', - { defaultMessage: 'All transactions' } - ), - histogram: overallLatencyHistogram, - }); - } - - if (Array.isArray(errorHistogramData.overallHistogram)) { - transactionDistributionChartData.push({ - id: i18n.translate( - 'xpack.apm.transactionDistribution.chart.failedTransactionsLabel', - { defaultMessage: 'Failed transactions' } - ), - histogram: errorHistogramData.overallHistogram, - }); - } + const transactionDistributionChartData = getTransactionDistributionChartData({ + euiTheme, + allTransactionsHistogram: overallLatencyHistogram, + failedTransactionsHistogram: errorHistogramData.overallHistogram, + }); return { chartData: transactionDistributionChartData, diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx index d5cd423b2b12..b33f152a6301 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx @@ -33,6 +33,7 @@ import { useChartTheme } from '../../../../../../observability/public'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; import type { HistogramItem } from '../../../../../common/correlations/types'; +import { DEFAULT_PERCENTILE_THRESHOLD } from '../../../../../common/correlations/constants'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useTheme } from '../../../../hooks/use_theme'; @@ -42,6 +43,7 @@ import { ChartContainer } from '../chart_container'; export interface TransactionDistributionChartData { id: string; histogram: HistogramItem[]; + areaSeriesColor: string; } interface TransactionDistributionChartProps { @@ -49,9 +51,7 @@ interface TransactionDistributionChartProps { hasData: boolean; markerCurrentTransaction?: number; markerValue: number; - markerPercentile: number; onChartSelection?: BrushEndListener; - palette?: string[]; selection?: [number, number]; status: FETCH_STATUS; } @@ -98,19 +98,13 @@ export function TransactionDistributionChart({ hasData, markerCurrentTransaction, markerValue, - markerPercentile, onChartSelection, - palette, selection, status, }: TransactionDistributionChartProps) { const chartTheme = useChartTheme(); const euiTheme = useTheme(); - - const areaSeriesColors = palette ?? [ - euiTheme.eui.euiColorVis1, - euiTheme.eui.euiColorVis2, - ]; + const markerPercentile = DEFAULT_PERCENTILE_THRESHOLD; const annotationsDataValues: LineAnnotationDatum[] = [ { @@ -265,7 +259,7 @@ export function TransactionDistributionChart({ curve={CurveType.CURVE_STEP_AFTER} xAccessor="key" yAccessors={['doc_count']} - color={areaSeriesColors[i]} + color={d.areaSeriesColor} fit="lookahead" // To make the area appear without the orphaned points technique, // we changed the original data to replace values of 0 with 0.0001. diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9fc49535e18e..6d54be0664e0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6233,13 +6233,9 @@ "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaDescription": "ć€±æ•—ă—ăŸăƒˆăƒ©ăƒłă‚¶ă‚Żă‚·ăƒ§ăƒłăźç›žé–ąé–ąäż‚ăŻGAă§ăŻă‚ă‚ŠăŸă›ă‚“ă€‚äžć…·ćˆăŒç™șç”Ÿă—ăŸă‚‰ć ±ć‘Šă—ăŠăă ă•ă„ă€‚", "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaLabel": "ăƒ™ăƒŒă‚ż", "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaTitle": "ć€±æ•—ă—ăŸăƒˆăƒ©ăƒłă‚¶ă‚Żă‚·ăƒ§ăƒłăźç›žé–ąé–ąäż‚", - "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsChartAllTransactions": "すăčăŠăźăƒˆăƒ©ăƒłă‚¶ă‚Żă‚·ăƒ§ăƒł", "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsLabel": "ć€±æ•—ă—ăŸăƒˆăƒ©ăƒłă‚¶ă‚Żă‚·ăƒ§ăƒłăźç›žé–ąé–ąäż‚", - "xpack.apm.transactionDetails.tabs.latencyCorrelationsChartAllTransactions": "すăčăŠăźăƒˆăƒ©ăƒłă‚¶ă‚Żă‚·ăƒ§ăƒł", - "xpack.apm.transactionDetails.tabs.latencyCorrelationsChartDescription": "{allTransactions}ず{focusTransaction}た{br}é‡è€‡ă™ă‚‹ćžŻă‚’äœżç”šă—ăŸé…ć»¶ïŒˆxïŒ‰ăšăƒˆăƒ©ăƒłă‚¶ă‚Żă‚·ăƒ§ăƒłïŒˆyïŒ‰ăźäžĄćŻŸæ•°ăƒ—ăƒ­ăƒƒăƒˆă€‚", "xpack.apm.transactionDetails.tabs.latencyLabel": "遅滶た盞閹閹係", "xpack.apm.transactionDetails.tabs.traceSamplesLabel": "ăƒˆăƒŹăƒŒă‚čăźă‚”ăƒłăƒ—ăƒ«", - "xpack.apm.transactionDetails.tabs.transactionDistributionChartAllTransactions": "すăčăŠăźăƒˆăƒ©ăƒłă‚¶ă‚Żă‚·ăƒ§ăƒł", "xpack.apm.transactionDetails.traceNotFound": "éžæŠžă•ă‚ŒăŸăƒˆăƒŹăƒŒă‚čăŒèŠ‹ă€ă‹ă‚ŠăŸă›ă‚“", "xpack.apm.transactionDetails.traceSampleTitle": "ăƒˆăƒŹăƒŒă‚čăźă‚”ăƒłăƒ—ăƒ«", "xpack.apm.transactionDetails.transactionLabel": "ăƒˆăƒ©ăƒłă‚¶ă‚Żă‚·ăƒ§ăƒł", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 102051ac2a9d..f04696d7dbe3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6276,13 +6276,9 @@ "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaDescription": "ć€±èŽ„äș‹ćŠĄç›žć…łæ€§äžæ˜Ż GA ç‰ˆă€‚èŻ·é€šèż‡æŠ„ć‘Šé”™èŻŻæ„ćžźćŠ©æˆ‘ä»Źă€‚", "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaLabel": "ć…Źæ”‹ç‰ˆ", "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaTitle": "ć€±èŽ„äș‹ćŠĄç›žć…łæ€§", - "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsChartAllTransactions": "所有äș‹ćŠĄ", "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsLabel": "ć€±èŽ„äș‹ćŠĄç›žć…łæ€§", - "xpack.apm.transactionDetails.tabs.latencyCorrelationsChartAllTransactions": "所有äș‹ćŠĄ", - "xpack.apm.transactionDetails.tabs.latencyCorrelationsChartDescription": "{allTransactions}撌{focusTransaction}{br}ćžŠé‡ć çš„ć»¶èżŸ (x) 侎äș‹ćŠĄ (y) 揌ćŻčæ•°ćæ ‡ć›Ÿă€‚", "xpack.apm.transactionDetails.tabs.latencyLabel": "ć»¶èżŸç›žć…łæ€§", "xpack.apm.transactionDetails.tabs.traceSamplesLabel": "跟èžȘ样䟋", - "xpack.apm.transactionDetails.tabs.transactionDistributionChartAllTransactions": "所有äș‹ćŠĄ", "xpack.apm.transactionDetails.traceNotFound": "æ‰Ÿäžćˆ°æ‰€é€‰è·ŸèžȘ", "xpack.apm.transactionDetails.traceSampleTitle": "跟èžȘ样䟋", "xpack.apm.transactionDetails.transactionLabel": "äș‹ćŠĄ", From d9ee4d7ee37a7b0eececdef68045028d07084efe Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 29 Nov 2021 18:15:49 +0300 Subject: [PATCH 015/224] Update vega related modules (main) (#119663) * Update vega related modules (main) * update types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 8 +- .../public/vega_inspector/vega_adapter.ts | 38 ++- yarn.lock | 243 +++++++++--------- 3 files changed, 159 insertions(+), 130 deletions(-) diff --git a/package.json b/package.json index 983dddda5d4f..5e8a77f2c55f 100644 --- a/package.json +++ b/package.json @@ -397,12 +397,12 @@ "usng.js": "^0.4.5", "utility-types": "^3.10.0", "uuid": "3.3.2", - "vega": "^5.19.1", + "vega": "^5.21.0", "vega-interpreter": "^1.0.4", - "vega-lite": "^5.0.0", - "vega-schema-url-parser": "^2.1.0", + "vega-lite": "^5.2.0", + "vega-schema-url-parser": "^2.2.0", "vega-spec-injector": "^0.0.2", - "vega-tooltip": "^0.25.1", + "vega-tooltip": "^0.27.0", "venn.js": "0.2.20", "vinyl": "^2.2.0", "vt-pbf": "^3.1.1", diff --git a/src/plugins/vis_types/vega/public/vega_inspector/vega_adapter.ts b/src/plugins/vis_types/vega/public/vega_inspector/vega_adapter.ts index bc90fe35199b..def7fefd5517 100644 --- a/src/plugins/vis_types/vega/public/vega_inspector/vega_adapter.ts +++ b/src/plugins/vis_types/vega/public/vega_inspector/vega_adapter.ts @@ -6,14 +6,35 @@ * Side Public License, v 1. */ +import { i18n } from '@kbn/i18n'; + import { Observable, ReplaySubject, fromEventPattern, merge, timer } from 'rxjs'; import { map, switchMap, filter, debounce } from 'rxjs/operators'; -import { View, Runtime, Spec } from 'vega'; -import { i18n } from '@kbn/i18n'; -import { Assign } from '@kbn/utility-types'; +import type { View, Spec } from 'vega'; +import type { Assign } from '@kbn/utility-types'; interface DebugValues { - view: View; + view: Assign< + { + _runtime: { + data: Record< + string, + { + values: { + value: Array>; + }; + } + >; + signals: Record< + string, + { + value: unknown; + } + >; + }; + }, + View + >; spec: Spec; } @@ -38,8 +59,11 @@ const vegaAdapterValueLabel = i18n.translate('visTypeVega.inspector.vegaAdapter. /** Get Runtime Scope for Vega View * @link https://vega.github.io/vega/docs/api/debugging/#scope **/ -const getVegaRuntimeScope = (debugValues: DebugValues) => - (debugValues.view as any)._runtime as Runtime; +const getVegaRuntimeScope = (debugValues: DebugValues) => { + const { data, signals } = debugValues.view._runtime ?? {}; + + return { data, signals }; +}; const serializeColumns = (item: Record, columns: string[]) => { const nonSerializableFieldLabel = '(..)'; @@ -69,7 +93,7 @@ export class VegaAdapter { const runtimeScope = getVegaRuntimeScope(debugValues); return Object.keys(runtimeScope.data || []).reduce((acc: InspectDataSets[], key) => { - const value = runtimeScope.data[key].values.value; + const { value } = runtimeScope.data[key].values; if (value && value[0]) { const columns = Object.keys(value[0]); diff --git a/yarn.lock b/yarn.lock index 44185306ca38..821b788bc7c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5259,10 +5259,10 @@ resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.9.tgz#d868b6febb02666330410fe7f58f3c4b8258be7b" integrity sha512-MNl+rT5UmZeilaPxAVs6YaPC2m6aA8rofviZbhbxpPpl61uKodfdQVsBtgJGTqGizEf02oW3tsVe7FYB8kK14A== -"@types/clone@~2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@types/clone/-/clone-2.1.0.tgz#cb888a3fe5319275b566ae3a9bc606e310c533d4" - integrity sha512-d/aS/lPOnUSruPhgNtT8jW39fHRVTLQy9sodysP1kkG8EdAtdZu1vt8NJaYA8w/6Z9j8izkAsx1A/yJhcYR1CA== +"@types/clone@~2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@types/clone/-/clone-2.1.1.tgz#9b880d0ce9b1f209b5e0bd6d9caa38209db34024" + integrity sha512-BZIU34bSYye0j/BFcPraiDZ5ka6MJADjcDVELGf7glr9K+iE8NYVjFslJFVWzskSxkLLyCrSPScE82/UUoBSvg== "@types/cmd-shim@^2.0.0": version "2.0.0" @@ -5424,7 +5424,7 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*": +"@types/estree@*", "@types/estree@^0.0.50": version "0.0.50" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== @@ -5468,11 +5468,6 @@ resolved "https://registry.yarnpkg.com/@types/fancy-log/-/fancy-log-1.3.1.tgz#dd94fbc8c2e2ab8ab402ca8d04bb8c34965f0696" integrity sha512-31Dt9JaGfHretvwVxCBrCFL5iC9MQ3zOXpu+8C4qzW0cxc5rJJVGxB5c/vZ+wmeTk/JjPz/D0gv8BZ+Ip6iCqQ== -"@types/fast-json-stable-stringify@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#40363bb847cb86b2c2e1599f1398d11e8329c921" - integrity sha512-mky/O83TXmGY39P1H9YbUpjV6l6voRYlufqfFCvel8l1phuy8HRjdWc1rrPuN53ITBJlbyMSV6z3niOySO5pgQ== - "@types/fetch-mock@^7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@types/fetch-mock/-/fetch-mock-7.3.1.tgz#df7421e8bcb351b430bfbfa5c52bb353826ac94f" @@ -27558,16 +27553,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.2.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== -tslib@^2.3.0: +tslib@^2.3.0, tslib@~2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== -tslib@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" - integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== - tsutils@2.27.2: version "2.27.2" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.27.2.tgz#60ba88a23d6f785ec4b89c6e8179cac9b431f1c7" @@ -28574,14 +28564,14 @@ vega-crossfilter@~4.0.5: vega-dataflow "^5.7.3" vega-util "^1.15.2" -vega-dataflow@^5.7.3, vega-dataflow@~5.7.3: - version "5.7.3" - resolved "https://registry.yarnpkg.com/vega-dataflow/-/vega-dataflow-5.7.3.tgz#66ca06a61f72a210b0732e3b6cc1eec5117197f7" - integrity sha512-2ipzKgQUmbSXcQBH+9XF0BYbXyZrHvjlbJ8ifyRWYQk78w8kMvE6wy/rcdXYK6iVZ6aAbEDDT7jTI+rFt3tGLA== +vega-dataflow@^5.7.3, vega-dataflow@^5.7.4, vega-dataflow@~5.7.4: + version "5.7.4" + resolved "https://registry.yarnpkg.com/vega-dataflow/-/vega-dataflow-5.7.4.tgz#7cafc0a41b9d0b11dd2e34a513f8b7ca345dfd74" + integrity sha512-JGHTpUo8XGETH3b1V892we6hdjzCWB977ybycIu8DPqRoyrZuj6t1fCVImazfMgQD1LAfJlQybWP+alwKDpKig== dependencies: vega-format "^1.0.4" vega-loader "^4.3.2" - vega-util "^1.15.2" + vega-util "^1.16.1" vega-encode@~4.8.3: version "4.8.3" @@ -28594,16 +28584,17 @@ vega-encode@~4.8.3: vega-scale "^7.0.3" vega-util "^1.15.2" -vega-event-selector@^2.0.6, vega-event-selector@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/vega-event-selector/-/vega-event-selector-2.0.6.tgz#6beb00e066b78371dde1a0f40cb5e0bbaecfd8bc" - integrity sha512-UwCu50Sqd8kNZ1X/XgiAY+QAyQUmGFAwyDu7y0T5fs6/TPQnDo/Bo346NgSgINBEhEKOAMY1Nd/rPOk4UEm/ew== +vega-event-selector@^3.0.0, vega-event-selector@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/vega-event-selector/-/vega-event-selector-3.0.0.tgz#7b855ac0c3ddb59bc5b5caa0d96dbbc9fbd33a4c" + integrity sha512-Gls93/+7tEJGE3kUuUnxrBIxtvaNeF01VIFB2Q2Of2hBIBvtHX74jcAdDtkh5UhhoYGD8Q1J30P5cqEBEwtPoQ== -vega-expression@^4.0.1, vega-expression@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/vega-expression/-/vega-expression-4.0.1.tgz#c03e4fc68a00acac49557faa4e4ed6ac8a59c5fd" - integrity sha512-ZrDj0hP8NmrCpdLFf7Rd/xMUHGoSYsAOTaYp7uXZ2dkEH5x0uPy5laECMc8TiQvL8W+8IrN2HAWCMRthTSRe2Q== +vega-expression@^5.0.0, vega-expression@~5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/vega-expression/-/vega-expression-5.0.0.tgz#938f26689693a1e0d26716030cdaed43ca7abdfb" + integrity sha512-y5+c2frq0tGwJ7vYXzZcfVcIRF/QGfhf2e+bV1Z0iQs+M2lI1II1GPDdmOcMKimpoCVp/D61KUJDIGE1DSmk2w== dependencies: + "@types/estree" "^0.0.50" vega-util "^1.16.0" vega-force@~4.0.7: @@ -28626,19 +28617,19 @@ vega-format@^1.0.4, vega-format@~1.0.4: vega-time "^2.0.3" vega-util "^1.15.2" -vega-functions@^5.10.0, vega-functions@^5.12.0, vega-functions@~5.12.0: - version "5.12.0" - resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.12.0.tgz#44bf08a7b20673dc8cf51d6781c8ea1399501668" - integrity sha512-3hljmGs+gR7TbO/yYuvAP9P5laKISf1GKk4yRHLNdM61fWgKm8pI3f6LY2Hvq9cHQFTiJ3/5/Bx2p1SX5R4quQ== +vega-functions@^5.10.0, vega-functions@^5.12.1, vega-functions@~5.12.1: + version "5.12.1" + resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.12.1.tgz#b69f9ad4cd9f777dbc942587c02261b2f4cdba2c" + integrity sha512-7cHfcjXOj27qEbh2FTzWDl7FJK4xGcMFF7+oiyqa0fp7BU/wNT5YdNV0t5kCX9WjV7mfJWACKV74usLJbyM6GA== dependencies: d3-array "^2.7.1" d3-color "^2.0.0" d3-geo "^2.0.1" vega-dataflow "^5.7.3" - vega-expression "^4.0.1" + vega-expression "^5.0.0" vega-scale "^7.1.1" vega-scenegraph "^4.9.3" - vega-selections "^5.3.0" + vega-selections "^5.3.1" vega-statistics "^1.7.9" vega-time "^2.0.4" vega-util "^1.16.0" @@ -28671,38 +28662,37 @@ vega-interpreter@^1.0.4: resolved "https://registry.yarnpkg.com/vega-interpreter/-/vega-interpreter-1.0.4.tgz#291ebf85bc2d1c3550a3da22ff75b3ba0d326a39" integrity sha512-6tpYIa/pJz0cZo5fSxDSkZkAA51pID2LjOtQkOQvbzn+sJiCaWKPFhur8MBqbcmYZ9bnap1OYNwlrvpd2qBLvg== -vega-label@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/vega-label/-/vega-label-1.0.0.tgz#c3bea3a608a62217ca554ecc0f7fe0395d81bd1b" - integrity sha512-hCdm2pcHgkKgxnzW9GvX5JmYNiUMlOXOibtMmBzvFBQHX3NiV9giQ5nsPiQiFbV08VxEPtM+VYXr2HyrIcq5zQ== +vega-label@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vega-label/-/vega-label-1.1.0.tgz#0a11ae3ba18d7aed909c51ec67c2a9dde4426c6f" + integrity sha512-LAThIiDEsZxYvbSkvPLJ93eJF+Ts8RXv1IpBh8gmew8XGmaLJvVkzdsMe7WJJwuaVEsK7ZZFyB/Inkp842GW6w== dependencies: vega-canvas "^1.2.5" vega-dataflow "^5.7.3" vega-scenegraph "^4.9.2" vega-util "^1.15.2" -vega-lite@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/vega-lite/-/vega-lite-5.0.0.tgz#93898a910702736da41048f590882b907d78ac65" - integrity sha512-CrMAy3D2E662qtShrOeGttwwthRxUOZUfdu39THyxkOfLNJBCLkNjfQpFekEidxwbtFTO1zMZzyFIP3AE2I8kQ== +vega-lite@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/vega-lite/-/vega-lite-5.2.0.tgz#bc3c5c70a38d9de8f3fb9644c7dd52f3b9f47a1b" + integrity sha512-Yxcg8MvYfxHcG6BbkaKT0oVCIMIcE19UvqIsEwBmyd/7h2nzW7oRnID81T8UrY7hpDrIr6wa2JADOT2dhGNErw== dependencies: - "@types/clone" "~2.1.0" - "@types/fast-json-stable-stringify" "^2.0.0" + "@types/clone" "~2.1.1" array-flat-polyfill "^1.0.1" clone "~2.1.2" fast-deep-equal "~3.1.3" fast-json-stable-stringify "~2.1.0" json-stringify-pretty-compact "~3.0.0" - tslib "~2.1.0" - vega-event-selector "~2.0.6" - vega-expression "~4.0.1" - vega-util "~1.16.0" - yargs "~16.2.0" + tslib "~2.3.1" + vega-event-selector "~3.0.0" + vega-expression "~5.0.0" + vega-util "~1.17.0" + yargs "~17.2.1" -vega-loader@^4.3.2, vega-loader@^4.3.3, vega-loader@~4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/vega-loader/-/vega-loader-4.4.0.tgz#fc515b7368c46b2be8df1fcf3c35c696c13c453d" - integrity sha512-e5enQECdau7rJob0NFB5pGumh3RaaSWWm90+boxMy3ay2b4Ki/3XIvo+C4F1Lx04qSxvQF7tO2LJcklRm6nqRA== +vega-loader@^4.3.2, vega-loader@^4.3.3, vega-loader@~4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/vega-loader/-/vega-loader-4.4.1.tgz#8f9de46202f33659d1a2737f6e322a9fc3364275" + integrity sha512-dj65i4qlNhK0mOmjuchHgUrF5YUaWrYpx0A8kXA68lBk5Hkx8FNRztkcl07CZJ1+8V81ymEyJii9jzGbhEX0ag== dependencies: d3-dsv "^2.0.0" node-fetch "^2.6.1" @@ -28710,14 +28700,14 @@ vega-loader@^4.3.2, vega-loader@^4.3.3, vega-loader@~4.4.0: vega-format "^1.0.4" vega-util "^1.16.0" -vega-parser@~6.1.3: - version "6.1.3" - resolved "https://registry.yarnpkg.com/vega-parser/-/vega-parser-6.1.3.tgz#df72785e4b086eceb90ee6219a399210933b507b" - integrity sha512-8oiVhhW26GQ4GZBvolId8FVFvhn3s1KGgPlD7Z+4P2wkV+xe5Nqu0TEJ20F/cn3b88fd0Vj48X3BH3dlSeKNFg== +vega-parser@~6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/vega-parser/-/vega-parser-6.1.4.tgz#4868e41af2c9645b6d7daeeb205cfad06b9d465c" + integrity sha512-tORdpWXiH/kkXcpNdbSVEvtaxBuuDtgYp9rBunVW9oLsjFvFXbSWlM1wvJ9ZFSaTfx6CqyTyGMiJemmr1QnTjQ== dependencies: vega-dataflow "^5.7.3" - vega-event-selector "^2.0.6" - vega-functions "^5.12.0" + vega-event-selector "^3.0.0" + vega-functions "^5.12.1" vega-scale "^7.1.1" vega-util "^1.16.0" @@ -28758,10 +28748,10 @@ vega-scale@^7.0.3, vega-scale@^7.1.1, vega-scale@~7.1.1: vega-time "^2.0.4" vega-util "^1.15.2" -vega-scenegraph@^4.9.2, vega-scenegraph@^4.9.3, vega-scenegraph@~4.9.3: - version "4.9.3" - resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.3.tgz#c4720550ea7ff5c8d9d0690f47fe2640547cfc6b" - integrity sha512-lBvqLbXqrqRCTGJmSgzZC/tLR/o+TXfakbdhDzNdpgTavTaQ65S/67Gpj5hPpi77DvsfZUIY9lCEeO37aJhy0Q== +vega-scenegraph@^4.9.2, vega-scenegraph@^4.9.3, vega-scenegraph@^4.9.4, vega-scenegraph@~4.9.4: + version "4.9.4" + resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.4.tgz#468408c1e89703fa9d3450445daabff623de2757" + integrity sha512-QaegQzbFE2yhYLNWAmHwAuguW3yTtQrmwvfxYT8tk0g+KKodrQ5WSmNrphWXhqwtsgVSvtdZkfp2IPeumcOQJg== dependencies: d3-path "^2.0.0" d3-shape "^2.0.0" @@ -28770,17 +28760,17 @@ vega-scenegraph@^4.9.2, vega-scenegraph@^4.9.3, vega-scenegraph@~4.9.3: vega-scale "^7.1.1" vega-util "^1.15.2" -vega-schema-url-parser@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/vega-schema-url-parser/-/vega-schema-url-parser-2.1.0.tgz#847f9cf9f1624f36f8a51abc1adb41ebc6673cb4" - integrity sha512-JHT1PfOyVzOohj89uNunLPirs05Nf59isPT5gnwIkJph96rRgTIBJE7l7yLqndd7fLjr3P8JXHGAryRp74sCaQ== +vega-schema-url-parser@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/vega-schema-url-parser/-/vega-schema-url-parser-2.2.0.tgz#a0d1e02915adfbfcb1fd517c8c2ebe2419985c1e" + integrity sha512-yAtdBnfYOhECv9YC70H2gEiqfIbVkq09aaE4y/9V/ovEFmH9gPKaEgzIZqgT7PSPQjKhsNkb6jk6XvSoboxOBw== -vega-selections@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.3.0.tgz#810f2e7b7642fa836cf98b2e5dcc151093b1f6a7" - integrity sha512-vC4NPsuN+IffruFXfH0L3i2A51RgG4PqpLv85TvrEAIYnSkyKDE4bf+wVraR3aPdnLLkc3+tYuMi6le5FmThIA== +vega-selections@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.3.1.tgz#af5c3cc6532a55a5b692eb0fcc2a1d8d521605a4" + integrity sha512-cm4Srw1WHjcLGXX7GpxiUlfESv8XPu5b6Vh3mqMDPU94P2FO91SR9gei+EtRdt+KCFgIjr//MnRUjg/hAWwjkQ== dependencies: - vega-expression "^4.0.1" + vega-expression "^5.0.0" vega-util "^1.16.0" vega-spec-injector@^0.0.2: @@ -28788,10 +28778,10 @@ vega-spec-injector@^0.0.2: resolved "https://registry.yarnpkg.com/vega-spec-injector/-/vega-spec-injector-0.0.2.tgz#f1d990109dd9d845c524738f818baa4b72a60ca6" integrity sha512-wOMMqmpssn0/ZFPW7wl1v26vbseRX7zHPWzEyS9TwNXTRCu1TcjIBIR+X23lCWocxhoBqFxmqyn8UowMhlGtAg== -vega-statistics@^1.7.9, vega-statistics@~1.7.9: - version "1.7.9" - resolved "https://registry.yarnpkg.com/vega-statistics/-/vega-statistics-1.7.9.tgz#feec01d463e1b50593d890d20631f72138fcb65d" - integrity sha512-T0sd2Z08k/mHxr1Vb4ajLWytPluLFYnsYqyk4SIS5czzUs4errpP2gUu63QJ0B7CKNu33vnS9WdOMOo/Eprr/Q== +vega-statistics@^1.7.9, vega-statistics@~1.7.10: + version "1.7.10" + resolved "https://registry.yarnpkg.com/vega-statistics/-/vega-statistics-1.7.10.tgz#4353637402e5e96bff2ebd16bd58e2c15cac3018" + integrity sha512-QLb12gcfpDZ9K5h3TLGrlz4UXDH9wSPyg9LLfOJZacxvvJEPohacUQNrGEAVtFO9ccUCerRfH9cs25ZtHsOZrw== dependencies: d3-array "^2.7.1" @@ -28804,35 +28794,37 @@ vega-time@^2.0.3, vega-time@^2.0.4, vega-time@~2.0.4: d3-time "^2.0.0" vega-util "^1.15.2" -vega-tooltip@^0.25.1: - version "0.25.1" - resolved "https://registry.yarnpkg.com/vega-tooltip/-/vega-tooltip-0.25.1.tgz#cb7e438438649eb46896e7bee6f54e25d25b3c09" - integrity sha512-ugGwGi2/p3OpB8N15xieuzP8DyV5DreqMWcmJ9zpWT8GlkyKtef4dGRXnvHeHQ+iJFmWrq4oZJ+kLTrdiECjAg== +vega-tooltip@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/vega-tooltip/-/vega-tooltip-0.27.0.tgz#e03c150cdec78f68938a0dab5ef67a24e6d685da" + integrity sha512-FRcHNfMNo9D/7an5nZuP6JC2JGEsc85qcGjyMU7VlPpjQj9eBj1P+sZSNbb54Z20g7inVSBRyd8qgNn5EYTxJA== dependencies: vega-util "^1.16.0" -vega-transforms@~4.9.3: - version "4.9.3" - resolved "https://registry.yarnpkg.com/vega-transforms/-/vega-transforms-4.9.3.tgz#40e5234b956a68eaa03eedf489ed03293075bbfb" - integrity sha512-PdqQd5oPlRyD405M2w+Sz9Bo+i7Rwi8o03SVK7RaeQsJC2FffKGJ6acIaSEgOq+yD1Q2k/1SePmCXcmLUlIiEA== +vega-transforms@~4.9.4: + version "4.9.4" + resolved "https://registry.yarnpkg.com/vega-transforms/-/vega-transforms-4.9.4.tgz#5cf6b91bda9f184bbbaba63838be8e5e6a571235" + integrity sha512-JGBhm5Bf6fiGTUSB5Qr5ckw/KU9FJcSV5xIe/y4IobM/i/KNwI1i1fP45LzP4F4yZc0DMTwJod2UvFHGk9plKA== dependencies: d3-array "^2.7.1" - vega-dataflow "^5.7.3" + vega-dataflow "^5.7.4" vega-statistics "^1.7.9" vega-time "^2.0.4" - vega-util "^1.15.2" + vega-util "^1.16.1" -vega-typings@~0.19.2: - version "0.19.2" - resolved "https://registry.yarnpkg.com/vega-typings/-/vega-typings-0.19.2.tgz#374fc1020c1abb263a0be87de28d1a4bd0526c3f" - integrity sha512-YU/S9rDk4d+t4+4eTa9fzuw87PMNteeVtpcL51kUO8H7HvGaoW7ll8RHKLkR0NYBEGPRoFDKUxnoyMvhgjsdYw== +vega-typings@~0.22.0: + version "0.22.1" + resolved "https://registry.yarnpkg.com/vega-typings/-/vega-typings-0.22.1.tgz#287c646cfa93b1822d0fb6ea11d5543632f8b56e" + integrity sha512-88cIrjmoTxo/0nWTf+GuitkFhirHWVWCfymADiCUXt6s9arpQ6XPP5xjrN5KDc0LZd9xr7p4FIiEgADghgLTgw== dependencies: + vega-event-selector "^3.0.0" + vega-expression "^5.0.0" vega-util "^1.15.2" -vega-util@^1.15.2, vega-util@^1.16.0, vega-util@~1.16.0: - version "1.16.0" - resolved "https://registry.yarnpkg.com/vega-util/-/vega-util-1.16.0.tgz#77405d8df0a94944d106bdc36015f0d43aa2caa3" - integrity sha512-6mmz6mI+oU4zDMeKjgvE2Fjz0Oh6zo6WGATcvCfxH2gXBzhBHmy5d25uW5Zjnkc6QBXSWPLV9Xa6SiqMsrsKog== +vega-util@^1.15.2, vega-util@^1.16.0, vega-util@^1.16.1, vega-util@~1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/vega-util/-/vega-util-1.17.0.tgz#b72ae0baa97f943bf591f8f5bb27ceadf06834ac" + integrity sha512-HTaydZd9De3yf+8jH66zL4dXJ1d1p5OIFyoBzFiOli4IJbwkL1jrefCKz6AHDm1kYBzDJ0X4bN+CzZSCTvNk1w== vega-view-transforms@~4.5.8: version "4.5.8" @@ -28843,10 +28835,10 @@ vega-view-transforms@~4.5.8: vega-scenegraph "^4.9.2" vega-util "^1.15.2" -vega-view@~5.9.2: - version "5.9.2" - resolved "https://registry.yarnpkg.com/vega-view/-/vega-view-5.9.2.tgz#cb957e481a952abbe7b3a11aa2d58cc728f295e7" - integrity sha512-XAwKWyVjLClR3aCbTLCWdZj7aZozOULNg7078GxJIgVcBJOENCAidceI/H7JieyUZ96p3AiEHLQdWr167InBpg== +vega-view@~5.10.1: + version "5.10.1" + resolved "https://registry.yarnpkg.com/vega-view/-/vega-view-5.10.1.tgz#b69348bb32a9845a1bd341fdd946df98684fadc3" + integrity sha512-4xvQ5KZcgKdZx1Z7jjenCUumvlyr/j4XcHLRf9gyeFrFvvS596dVpL92V8twhV6O++DmS2+fj+rHagO8Di4nMg== dependencies: d3-array "^2.7.1" d3-timer "^2.0.0" @@ -28854,8 +28846,8 @@ vega-view@~5.9.2: vega-format "^1.0.4" vega-functions "^5.10.0" vega-runtime "^6.1.3" - vega-scenegraph "^4.9.2" - vega-util "^1.15.2" + vega-scenegraph "^4.9.4" + vega-util "^1.16.1" vega-voronoi@~4.1.5: version "4.1.5" @@ -28877,35 +28869,35 @@ vega-wordcloud@~4.1.3: vega-statistics "^1.7.9" vega-util "^1.15.2" -vega@^5.19.1: - version "5.19.1" - resolved "https://registry.yarnpkg.com/vega/-/vega-5.19.1.tgz#64c8350740fe1a11d56cc6617ab3a76811fd704c" - integrity sha512-UE6/c9q9kzuz4HULFuU9HscBASoZa+zcXqGKdbQP545Nwmhd078QpcH+wZsq9lYfiTxmFtzLK/a0OH0zhkghvA== +vega@^5.21.0: + version "5.21.0" + resolved "https://registry.yarnpkg.com/vega/-/vega-5.21.0.tgz#f3d858d7544bfe4ffa3d8cd43d9ea978bf7391e8" + integrity sha512-yqqRa9nAqYoAxe7sVhRpsh0b001fly7Yx05klPkXmrvzjxXd07gClW1mOuGgSnVQqo7jTp/LYgbO1bD37FbEig== dependencies: vega-crossfilter "~4.0.5" - vega-dataflow "~5.7.3" + vega-dataflow "~5.7.4" vega-encode "~4.8.3" - vega-event-selector "~2.0.6" - vega-expression "~4.0.1" + vega-event-selector "~3.0.0" + vega-expression "~5.0.0" vega-force "~4.0.7" vega-format "~1.0.4" - vega-functions "~5.12.0" + vega-functions "~5.12.1" vega-geo "~4.3.8" vega-hierarchy "~4.0.9" - vega-label "~1.0.0" - vega-loader "~4.4.0" - vega-parser "~6.1.3" + vega-label "~1.1.0" + vega-loader "~4.4.1" + vega-parser "~6.1.4" vega-projection "~1.4.5" vega-regression "~1.0.9" vega-runtime "~6.1.3" vega-scale "~7.1.1" - vega-scenegraph "~4.9.3" - vega-statistics "~1.7.9" + vega-scenegraph "~4.9.4" + vega-statistics "~1.7.10" vega-time "~2.0.4" - vega-transforms "~4.9.3" - vega-typings "~0.19.2" - vega-util "~1.16.0" - vega-view "~5.9.2" + vega-transforms "~4.9.4" + vega-typings "~0.22.0" + vega-util "~1.17.0" + vega-view "~5.10.1" vega-view-transforms "~4.5.8" vega-voronoi "~4.1.5" vega-wordcloud "~4.1.3" @@ -29881,7 +29873,7 @@ yargs@^15.0.2, yargs@^15.3.1, yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.2.0, yargs@~16.2.0: +yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== @@ -29939,6 +29931,19 @@ yargs@^7.1.0: y18n "^3.2.1" yargs-parser "5.0.0-security.0" +yargs@~17.2.1: + version "17.2.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.2.1.tgz#e2c95b9796a0e1f7f3bf4427863b42e0418191ea" + integrity sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yargs@~3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" From 2c4196270abc540f841ab5492061d29fb5184ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ester=20Mart=C3=AD=20Vilaseca?= Date: Mon, 29 Nov 2021 17:06:25 +0100 Subject: [PATCH 016/224] [Unified Observability] Add feature flag for the new overview page (#119193) * Add feature flag to display a blank overview page when enabled * Add tests for overview page feature flag * Fix types * Fix more types * Remove duplicated BucketSize type * fix linter Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/application.test.tsx | 8 +- .../components/app/section/apm/index.test.tsx | 8 +- .../components/app/section/ux/index.test.tsx | 8 +- .../public/hooks/use_time_range.test.ts | 16 ++- x-pack/plugins/observability/public/index.ts | 6 +- .../public/pages/overview/index.test.tsx | 51 +++++++ .../public/pages/overview/index.tsx | 131 ++--------------- .../pages/overview/old_overview_page.tsx | 136 ++++++++++++++++++ .../pages/overview/overview.stories.tsx | 6 +- .../public/pages/overview/overview_page.tsx | 82 +++++++++++ .../public/utils/test_helper.tsx | 8 +- x-pack/plugins/observability/server/index.ts | 1 + 12 files changed, 333 insertions(+), 128 deletions(-) create mode 100644 x-pack/plugins/observability/public/pages/overview/index.test.tsx create mode 100644 x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx create mode 100644 x-pack/plugins/observability/public/pages/overview/overview_page.tsx diff --git a/x-pack/plugins/observability/public/application/application.test.tsx b/x-pack/plugins/observability/public/application/application.test.tsx index 6b5863c8b122..dddc44c3c26e 100644 --- a/x-pack/plugins/observability/public/application/application.test.tsx +++ b/x-pack/plugins/observability/public/application/application.test.tsx @@ -46,7 +46,13 @@ describe('renderApp', () => { uiSettings: { get: () => false }, http: { basePath: { prepend: (path: string) => path } }, } as unknown as CoreStart; - const config = { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } }; + const config = { + unsafe: { + alertingExperience: { enabled: true }, + cases: { enabled: true }, + overviewNext: { enabled: false }, + }, + }; const params = { element: window.document.createElement('div'), history: createMemoryHistory(), diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx index c9c2ed549a1c..35835cd0bc8e 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx @@ -42,7 +42,13 @@ describe('APMSection', () => { http: { basePath: { prepend: jest.fn() } }, } as unknown as CoreStart, appMountParameters: {} as AppMountParameters, - config: { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } }, + config: { + unsafe: { + alertingExperience: { enabled: true }, + cases: { enabled: true }, + overviewNext: { enabled: false }, + }, + }, observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), plugins: { data: { diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx index 8a99b6a53cf0..b4dda3ed3559 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx @@ -42,7 +42,13 @@ describe('UXSection', () => { http: { basePath: { prepend: jest.fn() } }, } as unknown as CoreStart, appMountParameters: {} as AppMountParameters, - config: { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } }, + config: { + unsafe: { + alertingExperience: { enabled: true }, + cases: { enabled: true }, + overviewNext: { enabled: false }, + }, + }, plugins: { data: { query: { diff --git a/x-pack/plugins/observability/public/hooks/use_time_range.test.ts b/x-pack/plugins/observability/public/hooks/use_time_range.test.ts index bf513d8a1a99..bbf3096e5510 100644 --- a/x-pack/plugins/observability/public/hooks/use_time_range.test.ts +++ b/x-pack/plugins/observability/public/hooks/use_time_range.test.ts @@ -24,7 +24,13 @@ describe('useTimeRange', () => { jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ core: {} as CoreStart, appMountParameters: {} as AppMountParameters, - config: { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } }, + config: { + unsafe: { + alertingExperience: { enabled: true }, + cases: { enabled: true }, + overviewNext: { enabled: false }, + }, + }, plugins: { data: { query: { @@ -67,7 +73,13 @@ describe('useTimeRange', () => { jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ core: {} as CoreStart, appMountParameters: {} as AppMountParameters, - config: { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } }, + config: { + unsafe: { + alertingExperience: { enabled: true }, + cases: { enabled: true }, + overviewNext: { enabled: false }, + }, + }, plugins: { data: { query: { diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 0dab3e513571..7646ac9bec9b 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -26,7 +26,11 @@ export type { export { enableInspectEsQueries } from '../common/ui_settings_keys'; export interface ConfigSchema { - unsafe: { alertingExperience: { enabled: boolean }; cases: { enabled: boolean } }; + unsafe: { + alertingExperience: { enabled: boolean }; + cases: { enabled: boolean }; + overviewNext: { enabled: boolean }; + }; } export const plugin: PluginInitializer< diff --git a/x-pack/plugins/observability/public/pages/overview/index.test.tsx b/x-pack/plugins/observability/public/pages/overview/index.test.tsx new file mode 100644 index 000000000000..b37ed1d873ba --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/index.test.tsx @@ -0,0 +1,51 @@ +/* + * 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 * as PluginContext from '../../hooks/use_plugin_context'; +import { PluginContextValue } from '../../context/plugin_context'; +import { OverviewPage } from './'; +import { OverviewPage as OldOverviewPage } from './old_overview_page'; +import { OverviewPage as NewOverviewPage } from './overview_page'; + +describe('Overview page', () => { + it('should render the old overview page when feature flag is disabled', () => { + const pluginContext = { + config: { + unsafe: { + overviewNext: { enabled: false }, + }, + }, + }; + + jest + .spyOn(PluginContext, 'usePluginContext') + .mockReturnValue(pluginContext as PluginContextValue); + + const component = shallow(); + expect(component.find(OldOverviewPage)).toHaveLength(1); + expect(component.find(NewOverviewPage)).toHaveLength(0); + }); + + it('should render the new overview page when feature flag is enabled', () => { + const pluginContext = { + config: { + unsafe: { + overviewNext: { enabled: true }, + }, + }, + }; + + jest + .spyOn(PluginContext, 'usePluginContext') + .mockReturnValue(pluginContext as PluginContextValue); + + const component = shallow(); + expect(component.find(OldOverviewPage)).toHaveLength(0); + expect(component.find(NewOverviewPage)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index 7100a0552876..cc38445e3a0f 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -4,133 +4,24 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPanel } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import React from 'react'; -import { useTrackPageview } from '../..'; -import { EmptySections } from '../../components/app/empty_sections'; -import { ObservabilityHeaderMenu } from '../../components/app/header'; -import { NewsFeed } from '../../components/app/news_feed'; -import { Resources } from '../../components/app/resources'; -import { AlertsSection } from '../../components/app/section/alerts'; -import { DatePicker } from '../../components/shared/date_picker'; -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 { useTimeRange } from '../../hooks/use_time_range'; import { RouteParams } from '../../routes'; -import { getNewsFeed } from '../../services/get_news_feed'; -import { getBucketSize } from '../../utils/get_bucket_size'; -import { getNoDataConfig } from '../../utils/no_data_config'; -import { DataSections } from './data_sections'; -import { LoadingObservability } from './loading_observability'; +import { usePluginContext } from '../../hooks/use_plugin_context'; +import { OverviewPage as OldOverviewPage } from './old_overview_page'; +import { OverviewPage as NewOverviewPage } from './overview_page'; + +export type { BucketSize } from './old_overview_page'; interface Props { routeParams: RouteParams<'/overview'>; } -export type BucketSize = ReturnType; -function calculateBucketSize({ start, end }: { start?: number; end?: number }) { - if (start && end) { - return getBucketSize({ start, end, minInterval: '60s' }); - } -} - -export function OverviewPage({ routeParams }: Props) { - useTrackPageview({ app: 'observability-overview', path: 'overview' }); - useTrackPageview({ app: 'observability-overview', path: 'overview', delay: 15000 }); - useBreadcrumbs([ - { - text: i18n.translate('xpack.observability.breadcrumbs.overviewLinkText', { - defaultMessage: 'Overview', - }), - }, - ]); - - const { core, ObservabilityPageTemplate } = usePluginContext(); - - const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); - const relativeTime = { start: relativeStart, end: relativeEnd }; - const absoluteTime = { start: absoluteStart, end: absoluteEnd }; +export function OverviewPage(props: Props) { + const { config } = usePluginContext(); - const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), [core]); - - const { hasDataMap, hasAnyData, isAllRequestsComplete } = useHasData(); - - if (hasAnyData === undefined) { - return ; + if (config.unsafe.overviewNext.enabled) { + return ; + } else { + return ; } - - const hasData = hasAnyData === true || (isAllRequestsComplete === false ? undefined : false); - - const noDataConfig = getNoDataConfig({ - hasData, - basePath: core.http.basePath, - docsLink: core.docLinks.links.observability.guide, - }); - - const { refreshInterval = 10000, refreshPaused = true } = routeParams.query; - - const bucketSize = calculateBucketSize({ - start: absoluteTime.start, - end: absoluteTime.end, - }); - - return ( - , - ], - } - : undefined - } - > - {hasData && ( - <> - - - - {/* Data sections */} - {hasAnyData && } - - - - - {/* Resources / What's New sections */} - - - - {!!newsFeed?.items?.length && } - - - {hasDataMap?.alert?.hasData && ( - - - - - - )} - - - - - )} - - ); } - -const overviewPageTitle = i18n.translate('xpack.observability.overview.pageTitle', { - defaultMessage: 'Overview', -}); diff --git a/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx b/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx new file mode 100644 index 000000000000..7100a0552876 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPanel } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useTrackPageview } from '../..'; +import { EmptySections } from '../../components/app/empty_sections'; +import { ObservabilityHeaderMenu } from '../../components/app/header'; +import { NewsFeed } from '../../components/app/news_feed'; +import { Resources } from '../../components/app/resources'; +import { AlertsSection } from '../../components/app/section/alerts'; +import { DatePicker } from '../../components/shared/date_picker'; +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 { useTimeRange } from '../../hooks/use_time_range'; +import { RouteParams } from '../../routes'; +import { getNewsFeed } from '../../services/get_news_feed'; +import { getBucketSize } from '../../utils/get_bucket_size'; +import { getNoDataConfig } from '../../utils/no_data_config'; +import { DataSections } from './data_sections'; +import { LoadingObservability } from './loading_observability'; + +interface Props { + routeParams: RouteParams<'/overview'>; +} +export type BucketSize = ReturnType; +function calculateBucketSize({ start, end }: { start?: number; end?: number }) { + if (start && end) { + return getBucketSize({ start, end, minInterval: '60s' }); + } +} + +export function OverviewPage({ routeParams }: Props) { + useTrackPageview({ app: 'observability-overview', path: 'overview' }); + useTrackPageview({ app: 'observability-overview', path: 'overview', delay: 15000 }); + useBreadcrumbs([ + { + text: i18n.translate('xpack.observability.breadcrumbs.overviewLinkText', { + defaultMessage: 'Overview', + }), + }, + ]); + + const { core, ObservabilityPageTemplate } = usePluginContext(); + + const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); + + const relativeTime = { start: relativeStart, end: relativeEnd }; + const absoluteTime = { start: absoluteStart, end: absoluteEnd }; + + const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), [core]); + + const { hasDataMap, hasAnyData, isAllRequestsComplete } = useHasData(); + + if (hasAnyData === undefined) { + return ; + } + + const hasData = hasAnyData === true || (isAllRequestsComplete === false ? undefined : false); + + const noDataConfig = getNoDataConfig({ + hasData, + basePath: core.http.basePath, + docsLink: core.docLinks.links.observability.guide, + }); + + const { refreshInterval = 10000, refreshPaused = true } = routeParams.query; + + const bucketSize = calculateBucketSize({ + start: absoluteTime.start, + end: absoluteTime.end, + }); + + return ( + , + ], + } + : undefined + } + > + {hasData && ( + <> + + + + {/* Data sections */} + {hasAnyData && } + + + + + {/* Resources / What's New sections */} + + + + {!!newsFeed?.items?.length && } + + + {hasDataMap?.alert?.hasData && ( + + + + + + )} + + + + + )} + + ); +} + +const overviewPageTitle = i18n.translate('xpack.observability.overview.pageTitle', { + defaultMessage: 'Overview', +}); diff --git a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx index 6549e892cab1..6213ea3e66d4 100644 --- a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx +++ b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx @@ -66,7 +66,11 @@ const withCore = makeDecorator({ setHeaderActionMenu: () => {}, } as unknown as AppMountParameters, config: { - unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } }, + unsafe: { + alertingExperience: { enabled: true }, + cases: { enabled: true }, + overviewNext: { enabled: false }, + }, }, core: options as CoreStart, plugins: { diff --git a/x-pack/plugins/observability/public/pages/overview/overview_page.tsx b/x-pack/plugins/observability/public/pages/overview/overview_page.tsx new file mode 100644 index 000000000000..f4cdec680af9 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/overview_page.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useTrackPageview } from '../..'; +import { DatePicker } from '../../components/shared/date_picker'; +import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; +import { useHasData } from '../../hooks/use_has_data'; +import { usePluginContext } from '../../hooks/use_plugin_context'; +import { useTimeRange } from '../../hooks/use_time_range'; +import { RouteParams } from '../../routes'; +import { getNoDataConfig } from '../../utils/no_data_config'; +import { LoadingObservability } from './loading_observability'; + +interface Props { + routeParams: RouteParams<'/overview'>; +} + +export function OverviewPage({ routeParams }: Props) { + useTrackPageview({ app: 'observability-overview', path: 'overview' }); + useTrackPageview({ app: 'observability-overview', path: 'overview', delay: 15000 }); + useBreadcrumbs([ + { + text: i18n.translate('xpack.observability.breadcrumbs.overviewLinkText', { + defaultMessage: 'Overview', + }), + }, + ]); + + const { core, ObservabilityPageTemplate } = usePluginContext(); + + const { relativeStart, relativeEnd } = useTimeRange(); + + const relativeTime = { start: relativeStart, end: relativeEnd }; + + const { hasAnyData, isAllRequestsComplete } = useHasData(); + + if (hasAnyData === undefined) { + return ; + } + + const hasData = hasAnyData === true || (isAllRequestsComplete === false ? undefined : false); + + const noDataConfig = getNoDataConfig({ + hasData, + basePath: core.http.basePath, + docsLink: core.docLinks.links.observability.guide, + }); + + const { refreshInterval = 10000, refreshPaused = true } = routeParams.query; + + return ( + , + ], + } + : undefined + } + > + {hasData &&
New observability content goes here
} +
+ ); +} + +const overviewPageTitle = i18n.translate('xpack.observability.overview.pageTitle', { + defaultMessage: 'Overview', +}); diff --git a/x-pack/plugins/observability/public/utils/test_helper.tsx b/x-pack/plugins/observability/public/utils/test_helper.tsx index 544f3feecb2b..a3ec446e5c30 100644 --- a/x-pack/plugins/observability/public/utils/test_helper.tsx +++ b/x-pack/plugins/observability/public/utils/test_helper.tsx @@ -34,7 +34,13 @@ export const core = { }, } as unknown as CoreStart; -const config = { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } }; +const config = { + unsafe: { + alertingExperience: { enabled: true }, + cases: { enabled: true }, + overviewNext: { enabled: false }, + }, +}; const plugins = { data: { query: { timefilter: { timefilter: { setTime: jest.fn() } } } }, diff --git a/x-pack/plugins/observability/server/index.ts b/x-pack/plugins/observability/server/index.ts index d99cf0865c0d..51204c7512a3 100644 --- a/x-pack/plugins/observability/server/index.ts +++ b/x-pack/plugins/observability/server/index.ts @@ -34,6 +34,7 @@ export const config: PluginConfigDescriptor = { unsafe: schema.object({ alertingExperience: schema.object({ enabled: schema.boolean({ defaultValue: true }) }), cases: schema.object({ enabled: schema.boolean({ defaultValue: true }) }), + overviewNext: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), }), }), }; From 75fcfe1c80ff78f2c3ef1f8065b531d171a3a273 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Mon, 29 Nov 2021 09:17:15 -0700 Subject: [PATCH 017/224] Docs/kibana logging links (#119680) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/migration/migrate_8_0.asciidoc | 8 ++++---- docs/settings/logging-settings.asciidoc | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index 59dd36a4fa5e..8936e41762c6 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -65,7 +65,7 @@ If you are currently using one of these settings in your Kibana config, please r ==== Default logging timezone is now the system's timezone *Details:* In prior releases the timezone used in logs defaulted to UTC. We now use the host machine's timezone by default. -*Impact:* To restore the previous behavior, in kibana.yml use the pattern layout, with a date modifier: +*Impact:* To restore the previous behavior, in kibana.yml use the pattern layout, with a {kibana-ref}/logging-configuration.html#date-format[date modifier]: [source,yaml] ------------------- logging: @@ -100,7 +100,7 @@ See https://github.com/elastic/kibana/pull/87939 for more details. [float] ==== Logging destination is specified by the appender -*Details:* Previously log destination would be `stdout` and could be changed to `file` using `logging.dest`. With the new logging configuration, you can specify the destination using appenders. +*Details:* Previously log destination would be `stdout` and could be changed to `file` using `logging.dest`. With the new logging configuration, you can specify the destination using {kibana-ref}/logging-configuration.html#logging-appenders[appenders]. *Impact:* To restore the previous behavior and log records to *stdout*, in `kibana.yml` use an appender with `type: console`. [source,yaml] @@ -131,7 +131,7 @@ logging: [float] ==== Set log verbosity with root -*Details:* Previously logging output would be specified by `logging.silent` (none), `logging.quiet` (error messages only) and `logging.verbose` (all). With the new logging configuration, set the minimum required log level. +*Details:* Previously logging output would be specified by `logging.silent` (none), `logging.quiet` (error messages only) and `logging.verbose` (all). With the new logging configuration, set the minimum required {kibana-ref}/logging-configuration.html#log-level[log level]. *Impact:* To restore the previous behavior, in `kibana.yml` specify `logging.root.level`: [source,yaml] @@ -188,7 +188,7 @@ logging: ==== Configure log rotation with the rolling-file appender *Details:* Previously log rotation would be enabled when `logging.rotate.enabled` was true. -*Impact:* To restore the previous behavior, in `kibana.yml` use the `rolling-file` appender. +*Impact:* To restore the previous behavior, in `kibana.yml` use the {kibana-ref}/logging-configuration.html#rolling-file-appender[`rolling-file`] appender. [source,yaml] ------------------- diff --git a/docs/settings/logging-settings.asciidoc b/docs/settings/logging-settings.asciidoc index cb8237c5aa38..a9053b90ce72 100644 --- a/docs/settings/logging-settings.asciidoc +++ b/docs/settings/logging-settings.asciidoc @@ -30,7 +30,7 @@ The following table serves as a quick reference for different logging configurat | Allows you to specify a fileName to write log records to disk. To write <>, add the file appender to `root.appenders`. If configured, you also need to specify <>. | `logging.appenders[].rolling-file:` -| Similar to Log4j's `RollingFileAppender`, this appender will log to a file and rotate if following a rolling strategy when the configured policy triggers. There are currently two policies supported: `size-limit` and `time-interval`. +| Similar to https://logging.apache.org/log4j/2.x/[Log4j's] `RollingFileAppender`, this appender will log to a file and rotate if following a rolling strategy when the configured policy triggers. There are currently two policies supported: <> and <>. | `logging.appenders[]..type` | The appender type determines where the log messages are sent. Options are `console`, `file`, `rewrite`, `rolling-file`. Required. From 095247f8574fa9c93521fcddd490bc1bdc1b5d9c Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 29 Nov 2021 18:29:30 +0100 Subject: [PATCH 018/224] [Uptime] Monitor management stub crud routes (#119800) * fix uptime config key * crud routes * prefix * test wip * add tests * revert * fixed path --- x-pack/plugins/uptime/common/config.ts | 2 +- .../uptime/common/constants/rest_api.ts | 3 +- .../framework/kibana_framework_adapter.ts | 7 +++ .../plugins/uptime/server/rest_api/index.ts | 12 +++++ .../synthetics_service/add_monitor.ts | 26 ++++++++++ .../synthetics_service/delete_monitor.ts | 34 +++++++++++++ .../synthetics_service/edit_monitor.ts | 31 +++++++++++ .../synthetics_service/get_monitors.ts | 40 +++++++++++++++ .../apis/uptime/rest/add_monitor.ts | 29 +++++++++++ .../apis/uptime/rest/delete_monitor.ts | 36 +++++++++++++ .../apis/uptime/rest/edit_monitor.ts | 37 ++++++++++++++ .../apis/uptime/rest/get_monitor.ts | 51 +++++++++++++++++++ .../api_integration/apis/uptime/rest/index.ts | 7 +++ x-pack/test/api_integration/config.ts | 4 ++ 14 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/uptime/server/rest_api/synthetics_service/add_monitor.ts create mode 100644 x-pack/plugins/uptime/server/rest_api/synthetics_service/delete_monitor.ts create mode 100644 x-pack/plugins/uptime/server/rest_api/synthetics_service/edit_monitor.ts create mode 100644 x-pack/plugins/uptime/server/rest_api/synthetics_service/get_monitors.ts create mode 100644 x-pack/test/api_integration/apis/uptime/rest/add_monitor.ts create mode 100644 x-pack/test/api_integration/apis/uptime/rest/delete_monitor.ts create mode 100644 x-pack/test/api_integration/apis/uptime/rest/edit_monitor.ts create mode 100644 x-pack/test/api_integration/apis/uptime/rest/get_monitor.ts diff --git a/x-pack/plugins/uptime/common/config.ts b/x-pack/plugins/uptime/common/config.ts index ccd5e7b5a2cc..8b7086964564 100644 --- a/x-pack/plugins/uptime/common/config.ts +++ b/x-pack/plugins/uptime/common/config.ts @@ -36,7 +36,7 @@ export const config: PluginConfigDescriptor = { username: schema.string(), password: schema.string(), manifestUrl: schema.string(), - hosts: schema.arrayOf(schema.string()), + hosts: schema.maybe(schema.arrayOf(schema.string())), }) ), }) diff --git a/x-pack/plugins/uptime/common/constants/rest_api.ts b/x-pack/plugins/uptime/common/constants/rest_api.ts index bef84c41796d..9c8098390d12 100644 --- a/x-pack/plugins/uptime/common/constants/rest_api.ts +++ b/x-pack/plugins/uptime/common/constants/rest_api.ts @@ -37,5 +37,6 @@ export enum API_URLS { CONNECTOR_TYPES = '/api/actions/connector_types', // Service end points - INDEX_TEMPLATES = '/api/uptime/service/index_templates', + INDEX_TEMPLATES = '/internal/uptime/service/index_templates', + SYNTHETICS_MONITORS = '/internal/uptime/service/monitors', } diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts index eae9dd5e73ca..d51496d6efaf 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -20,6 +20,7 @@ export class UMKibanaBackendFrameworkAdapter implements UMBackendFrameworkAdapte validate, options, }; + switch (method) { case 'GET': this.server.router.get(routeDefinition, handler); @@ -27,6 +28,12 @@ export class UMKibanaBackendFrameworkAdapter implements UMBackendFrameworkAdapte case 'POST': this.server.router.post(routeDefinition, handler); break; + case 'PUT': + this.server.router.put(routeDefinition, handler); + break; + case 'DELETE': + this.server.router.delete(routeDefinition, handler); + break; default: throw new Error(`Handler for method ${method} is not defined`); } diff --git a/x-pack/plugins/uptime/server/rest_api/index.ts b/x-pack/plugins/uptime/server/rest_api/index.ts index 344dd4d203d8..4eb6ae307125 100644 --- a/x-pack/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/plugins/uptime/server/rest_api/index.ts @@ -28,6 +28,13 @@ import { createNetworkEventsRoute } from './network_events'; import { createJourneyFailedStepsRoute } from './pings/journeys'; import { createLastSuccessfulStepRoute } from './synthetics/last_successful_step'; import { installIndexTemplatesRoute } from './synthetics_service/install_index_templates'; +import { + getAllSyntheticsMonitorRoute, + getSyntheticsMonitorRoute, +} from './synthetics_service/get_monitors'; +import { addSyntheticsMonitorRoute } from './synthetics_service/add_monitor'; +import { editSyntheticsMonitorRoute } from './synthetics_service/edit_monitor'; +import { deleteSyntheticsMonitorRoute } from './synthetics_service/delete_monitor'; export * from './types'; export { createRouteWithAuth } from './create_route_with_auth'; @@ -53,4 +60,9 @@ export const restApiRoutes: UMRestApiRouteFactory[] = [ createLastSuccessfulStepRoute, createJourneyScreenshotBlocksRoute, installIndexTemplatesRoute, + getSyntheticsMonitorRoute, + getAllSyntheticsMonitorRoute, + addSyntheticsMonitorRoute, + editSyntheticsMonitorRoute, + deleteSyntheticsMonitorRoute, ]; diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/add_monitor.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/add_monitor.ts new file mode 100644 index 000000000000..11d7dcedcaa3 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/add_monitor.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { schema } from '@kbn/config-schema'; +import { UMRestApiRouteFactory } from '../types'; +import { API_URLS } from '../../../common/constants'; +import { SyntheticsMonitorSavedObject } from '../../../common/types'; +import { syntheticsMonitorType } from '../../lib/saved_objects/synthetics_monitor'; + +export const addSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ + method: 'POST', + path: API_URLS.SYNTHETICS_MONITORS, + validate: { + body: schema.any(), + }, + handler: async ({ request, savedObjectsClient }): Promise => { + const monitor = request.body as SyntheticsMonitorSavedObject; + + const newMonitor = await savedObjectsClient.create(syntheticsMonitorType, monitor); + // TODO: call to service sync + return newMonitor; + }, +}); diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/delete_monitor.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/delete_monitor.ts new file mode 100644 index 000000000000..68eb8aa130d2 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/delete_monitor.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 { schema } from '@kbn/config-schema'; +import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server'; +import { UMRestApiRouteFactory } from '../types'; +import { API_URLS } from '../../../common/constants'; +import { syntheticsMonitorType } from '../../lib/saved_objects/synthetics_monitor'; + +export const deleteSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ + method: 'DELETE', + path: API_URLS.SYNTHETICS_MONITORS + '/{monitorId}', + validate: { + params: schema.object({ + monitorId: schema.string(), + }), + }, + handler: async ({ request, savedObjectsClient }): Promise => { + const { monitorId } = request.params; + + try { + await savedObjectsClient.delete(syntheticsMonitorType, monitorId); + // TODO: call to service sync + return monitorId; + } catch (getErr) { + if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) { + return 'Not found'; + } + } + }, +}); diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/edit_monitor.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/edit_monitor.ts new file mode 100644 index 000000000000..46a91738c380 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/edit_monitor.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { schema } from '@kbn/config-schema'; +import { UMRestApiRouteFactory } from '../types'; +import { API_URLS } from '../../../common/constants'; +import { SyntheticsMonitorSavedObject } from '../../../common/types'; +import { syntheticsMonitorType } from '../../lib/saved_objects/synthetics_monitor'; + +export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ + method: 'PUT', + path: API_URLS.SYNTHETICS_MONITORS + '/{monitorId}', + validate: { + params: schema.object({ + monitorId: schema.string(), + }), + body: schema.any(), + }, + handler: async ({ request, savedObjectsClient }): Promise => { + const monitor = request.body as SyntheticsMonitorSavedObject['attributes']; + + const { monitorId } = request.params; + + const editMonitor = await savedObjectsClient.update(syntheticsMonitorType, monitorId, monitor); + // TODO: call to service sync + return editMonitor; + }, +}); diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_monitors.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_monitors.ts new file mode 100644 index 000000000000..537d6c77195c --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_monitors.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { schema } from '@kbn/config-schema'; +import { UMRestApiRouteFactory } from '../types'; +import { API_URLS } from '../../../common/constants'; +import { syntheticsMonitorType } from '../../lib/saved_objects/synthetics_monitor'; + +export const getSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ + method: 'GET', + path: API_URLS.SYNTHETICS_MONITORS + '/{monitorId}', + validate: { + params: schema.object({ + monitorId: schema.string(), + }), + }, + handler: async ({ request, savedObjectsClient }): Promise => { + const { monitorId } = request.params; + return await savedObjectsClient.get(syntheticsMonitorType, monitorId); + }, +}); + +export const getAllSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ + method: 'GET', + path: API_URLS.SYNTHETICS_MONITORS, + validate: { + query: schema.object({ + page: schema.maybe(schema.number()), + perPage: schema.maybe(schema.number()), + }), + }, + handler: async ({ request, savedObjectsClient }): Promise => { + const { perPage = 50, page } = request.query; + // TODO: add query/filtering params + return await savedObjectsClient.find({ type: syntheticsMonitorType, perPage, page }); + }, +}); diff --git a/x-pack/test/api_integration/apis/uptime/rest/add_monitor.ts b/x-pack/test/api_integration/apis/uptime/rest/add_monitor.ts new file mode 100644 index 000000000000..a57a03fd3a1f --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/add_monitor.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; + +export default function ({ getService }: FtrProviderContext) { + describe('add synthetics monitor', () => { + const supertest = getService('supertest'); + const newMonitor = { + type: 'http', + name: 'Test monitor', + urls: 'https://www.elastic.co', + }; + + it('returns the newly added monitor', async () => { + const apiResponse = await supertest + .post(API_URLS.SYNTHETICS_MONITORS) + .set('kbn-xsrf', 'true') + .send(newMonitor); + + expect(apiResponse.body.attributes).eql(newMonitor); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/delete_monitor.ts b/x-pack/test/api_integration/apis/uptime/rest/delete_monitor.ts new file mode 100644 index 000000000000..bc49587fab87 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/delete_monitor.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; + +export default function ({ getService }: FtrProviderContext) { + describe('delete synthetics monitor', () => { + const supertest = getService('supertest'); + const newMonitor = { + type: 'http', + name: 'Test monitor', + urls: 'https://www.elastic.co', + }; + + it('deleted monitor by id', async () => { + const apiResponse = await supertest + .post(API_URLS.SYNTHETICS_MONITORS) + .set('kbn-xsrf', 'true') + .send(newMonitor); + + const monitorId = apiResponse.body.id; + + const deleteResponse = await supertest + .delete(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId) + .set('kbn-xsrf', 'true'); + // + expect(deleteResponse.body).eql(monitorId); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/edit_monitor.ts b/x-pack/test/api_integration/apis/uptime/rest/edit_monitor.ts new file mode 100644 index 000000000000..f5d54c40a864 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/edit_monitor.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; +export default function ({ getService }: FtrProviderContext) { + describe('edit synthetics monitor', () => { + const supertest = getService('supertest'); + const newMonitor = { + type: 'http', + name: 'Test monitor', + urls: 'https://www.elastic.co', + }; + + it('edits the monitor', async () => { + const apiResponse = await supertest + .post(API_URLS.SYNTHETICS_MONITORS) + .set('kbn-xsrf', 'true') + .send(newMonitor); + + const monitorId = apiResponse.body.id; + + expect(apiResponse.body.attributes).eql(newMonitor); + + const editResponse = await supertest + .put(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId) + .set('kbn-xsrf', 'true') + .send({ ...newMonitor, name: 'New name' }); + + expect(editResponse.body.attributes.name).eql('New name'); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/get_monitor.ts b/x-pack/test/api_integration/apis/uptime/rest/get_monitor.ts new file mode 100644 index 000000000000..76d27ff8a9d1 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/get_monitor.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; + +export default function ({ getService }: FtrProviderContext) { + describe('get synthetics monitor', () => { + const newMonitor = { + type: 'http', + name: 'Test monitor', + urls: 'https://www.elastic.co', + }; + + const addMonitor = async () => { + const res = await supertest + .post(API_URLS.SYNTHETICS_MONITORS) + .set('kbn-xsrf', 'true') + .send(newMonitor); + return res.body.id; + }; + + const supertest = getService('supertest'); + + it('get all monitors', async () => { + const id1 = await addMonitor(); + const id2 = await addMonitor(); + + const apiResponse = await supertest.get(API_URLS.SYNTHETICS_MONITORS); + + const monitor1 = apiResponse.body.saved_objects.find((obj: any) => obj.id === id1); + const monitor2 = apiResponse.body.saved_objects.find((obj: any) => obj.id === id2); + + expect(monitor1.id).eql(id1); + expect(monitor2.id).eql(id2); + }); + + it('get monitor by id', async () => { + const monitorId = await addMonitor(); + + const apiResponse = await supertest.get(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId); + + expect(apiResponse.body.id).eql(monitorId); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/index.ts b/x-pack/test/api_integration/apis/uptime/rest/index.ts index dc3c00b03f71..f674879552d6 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/index.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/index.ts @@ -71,5 +71,12 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./index_status')); loadTestFile(require.resolve('./monitor_states_real_data')); }); + + describe('uptime CRUD routes', () => { + loadTestFile(require.resolve('./get_monitor')); + loadTestFile(require.resolve('./add_monitor')); + loadTestFile(require.resolve('./edit_monitor')); + loadTestFile(require.resolve('./delete_monitor')); + }); }); } diff --git a/x-pack/test/api_integration/config.ts b/x-pack/test/api_integration/config.ts index e2c2e0b52dfd..bf42a5b0865a 100644 --- a/x-pack/test/api_integration/config.ts +++ b/x-pack/test/api_integration/config.ts @@ -35,6 +35,10 @@ export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProvi '--xpack.ruleRegistry.write.enabled=true', '--xpack.ruleRegistry.write.enabled=true', '--xpack.ruleRegistry.write.cache.enabled=false', + '--xpack.uptime.unsafe.service.enabled=true', + '--xpack.uptime.unsafe.service.password=test', + '--xpack.uptime.unsafe.service.manifestUrl=http://test.com', + '--xpack.uptime.unsafe.service.username=user', `--xpack.securitySolution.enableExperimental=${JSON.stringify(['ruleRegistryEnabled'])}`, ], }, From 8915c90a3127e14bf2b778bd1daa7310a96ce31f Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 29 Nov 2021 19:03:29 +0100 Subject: [PATCH 019/224] [es-query] Fix logic for detecting scripted phrase fields (#119511) * WIP, started updating functions for detecting scripted phrase filters * replace script.script with query.script.script * added test to verify detection of scripted and phrase filters * with elastic@ email Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../build_filters/phrase_filter.test.ts | 25 ++++++++++++++++++- .../filters/build_filters/phrase_filter.ts | 10 +++++--- .../filter_manager/lib/generate_filters.ts | 4 ++- .../filter_manager/lib/mappers/map_phrase.ts | 2 +- .../filter_manager/phrase_filter_manager.ts | 4 +-- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/kbn-es-query/src/filters/build_filters/phrase_filter.test.ts b/packages/kbn-es-query/src/filters/build_filters/phrase_filter.test.ts index 87a7812165a6..7c7f7dd28f6c 100644 --- a/packages/kbn-es-query/src/filters/build_filters/phrase_filter.test.ts +++ b/packages/kbn-es-query/src/filters/build_filters/phrase_filter.test.ts @@ -5,16 +5,19 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import { set } from 'lodash'; import { buildInlineScriptForPhraseFilter, buildPhraseFilter, getPhraseFilterField, PhraseFilter, + isPhraseFilter, + isScriptedPhraseFilter, } from './phrase_filter'; import { fields, getField } from '../stubs'; import { DataViewBase } from '../../es_query'; import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { Filter } from './types'; describe('Phrase filter builder', () => { let indexPattern: DataViewBase; @@ -164,3 +167,23 @@ describe('getPhraseFilterField', function () { expect(result).toBe('extension'); }); }); + +describe('isPhraseFilter', () => { + it('should return true if the filter is a phrases filter false otherwise', () => { + const filter: Filter = set({ meta: {} }, 'query.match_phrase', {}) as Filter; + const unknownFilter = {} as Filter; + + expect(isPhraseFilter(filter)).toBe(true); + expect(isPhraseFilter(unknownFilter)).toBe(false); + }); +}); + +describe('isScriptedPhraseFilter', () => { + it('should return true if the filter is a phrases filter false otherwise', () => { + const filter: Filter = set({ meta: {} }, 'query.script.script.params.value', {}) as Filter; + const unknownFilter = {} as Filter; + + expect(isScriptedPhraseFilter(filter)).toBe(true); + expect(isPhraseFilter(unknownFilter)).toBe(false); + }); +}); diff --git a/packages/kbn-es-query/src/filters/build_filters/phrase_filter.ts b/packages/kbn-es-query/src/filters/build_filters/phrase_filter.ts index 4c1827dc58c0..525463c9de24 100644 --- a/packages/kbn-es-query/src/filters/build_filters/phrase_filter.ts +++ b/packages/kbn-es-query/src/filters/build_filters/phrase_filter.ts @@ -31,8 +31,10 @@ export type PhraseFilter = Filter & { export type ScriptedPhraseFilter = Filter & { meta: PhraseFilterMeta; - script: { - script: estypes.InlineScript; + query: { + script: { + script: estypes.InlineScript; + }; }; }; @@ -58,7 +60,7 @@ export const isPhraseFilter = (filter: Filter): filter is PhraseFilter => { * @public */ export const isScriptedPhraseFilter = (filter: Filter): filter is ScriptedPhraseFilter => - has(filter, 'script.script.params.value'); + has(filter, 'query.script.script.params.value'); /** @internal */ export const getPhraseFilterField = (filter: PhraseFilter) => { @@ -77,7 +79,7 @@ export const getPhraseFilterValue = ( const queryValue = Object.values(queryConfig)[0]; return isPlainObject(queryValue) ? queryValue.query : queryValue; } else { - return filter.script.script.params?.value; + return filter.query?.script?.script?.params?.value; } }; diff --git a/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts b/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts index cfc3ddabe075..58f5cf8e52c9 100644 --- a/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts +++ b/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts @@ -40,7 +40,9 @@ function getExistingFilter( } if (isScriptedPhraseFilter(filter)) { - return filter.meta.field === fieldName && filter.script.script.params?.value === value; + return ( + filter.meta.field === fieldName && filter.query?.script?.script?.params?.value === value + ); } }) as any; } diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrase.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrase.ts index 64576d4978c9..23cae0ee852c 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrase.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrase.ts @@ -20,7 +20,7 @@ import { import { FilterValueFormatter } from '../../../../../common'; const getScriptedPhraseValue = (filter: PhraseFilter) => - get(filter, ['script', 'script', 'params', 'value']); + get(filter, ['query', 'script', 'script', 'params', 'value']); const getFormattedValueFn = (value: any) => { return (formatter?: FilterValueFormatter) => { diff --git a/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts index 8ffeefefd0cc..98ba8b4fbcda 100644 --- a/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts @@ -97,8 +97,8 @@ export class PhraseFilterManager extends FilterManager { } // scripted field filter - if (_.has(kbnFilter, 'script')) { - return _.get(kbnFilter, 'script.script.params.value'); + if (_.has(kbnFilter, 'query.script')) { + return _.get(kbnFilter, 'query.script.script.params.value'); } // single phrase filter From c6db25a3ef5663cc71dce348036601707fac930d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 29 Nov 2021 18:15:06 +0000 Subject: [PATCH 020/224] chore(NA): splits types from code on @kbn/alerts (#119855) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 1 + packages/BUILD.bazel | 1 + packages/kbn-alerts/BUILD.bazel | 27 ++++++++++++++++++++++----- packages/kbn-alerts/package.json | 1 - yarn.lock | 4 ++++ 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 5e8a77f2c55f..b0c7e4659a55 100644 --- a/package.json +++ b/package.json @@ -556,6 +556,7 @@ "@types/json-stable-stringify": "^1.0.32", "@types/json5": "^0.0.30", "@types/kbn__ace": "link:bazel-bin/packages/kbn-ace/npm_module_types", + "@types/kbn__alerts": "link:bazel-bin/packages/kbn-alerts/npm_module_types", "@types/kbn__i18n": "link:bazel-bin/packages/kbn-i18n/npm_module_types", "@types/kbn__i18n-react": "link:bazel-bin/packages/kbn-i18n-react/npm_module_types", "@types/license-checker": "15.0.0", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 07513ac94c85..c9a0f6a759b2 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -78,6 +78,7 @@ filegroup( "//packages/elastic-apm-synthtrace:build_types", "//packages/elastic-datemath:build_types", "//packages/kbn-ace:build_types", + "//packages/kbn-alerts:build_types", "//packages/kbn-i18n:build_types", "//packages/kbn-i18n-react:build_types", ], diff --git a/packages/kbn-alerts/BUILD.bazel b/packages/kbn-alerts/BUILD.bazel index e567c18807df..15dbc163cd28 100644 --- a/packages/kbn-alerts/BUILD.bazel +++ b/packages/kbn-alerts/BUILD.bazel @@ -1,10 +1,10 @@ -load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -load("//src/dev/bazel:index.bzl", "jsts_transpiler") +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") PKG_BASE_NAME = "kbn-alerts" - PKG_REQUIRE_NAME = "@kbn/alerts" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__alerts" SOURCE_FILES = glob( [ @@ -87,7 +87,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) @@ -106,3 +106,20 @@ filegroup( ], visibility = ["//visibility:public"], ) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = TYPES_PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-alerts/package.json b/packages/kbn-alerts/package.json index b52a6efc3513..13b60ad9fa07 100644 --- a/packages/kbn-alerts/package.json +++ b/packages/kbn-alerts/package.json @@ -5,6 +5,5 @@ "license": "SSPL-1.0 OR Elastic License 2.0", "browser": "./target_web/index.js", "main": "./target_node/index.js", - "types": "./target_types/index.d.ts", "private": true } diff --git a/yarn.lock b/yarn.lock index 821b788bc7c8..71b82ffdf5af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5809,6 +5809,10 @@ version "0.0.0" uid "" +"@types/kbn__alerts@link:bazel-bin/packages/kbn-alerts/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__i18n-react@link:bazel-bin/packages/kbn-i18n-react/npm_module_types": version "0.0.0" uid "" From b2f54829d853db84a9aa4829ae664716aaacc9cd Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 29 Nov 2021 10:29:45 -0800 Subject: [PATCH 021/224] [babel] ensure TS preset runs before anything else (#119107) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-babel-preset/common_preset.js | 85 ++++++------ ..._babel_runtime_helpers_in_entry_bundles.ts | 4 +- .../src/worker/emit_stats_plugin.ts | 2 +- packages/kbn-storybook/src/webpack.config.ts | 125 +++++++++++++++--- .../console/public/services/history.ts | 6 +- .../viewport/dashboard_viewport.tsx | 2 +- .../roles/roles_management_app.test.tsx | 6 +- 7 files changed, 168 insertions(+), 62 deletions(-) diff --git a/packages/kbn-babel-preset/common_preset.js b/packages/kbn-babel-preset/common_preset.js index 3a3763693db9..824a73f9b261 100644 --- a/packages/kbn-babel-preset/common_preset.js +++ b/packages/kbn-babel-preset/common_preset.js @@ -6,46 +6,57 @@ * Side Public License, v 1. */ -const plugins = [ - require.resolve('babel-plugin-add-module-exports'), - - // The class properties proposal was merged with the private fields proposal - // into the "class fields" proposal. Babel doesn't support this combined - // proposal yet, which includes private field, so this transform is - // TECHNICALLY stage 2, but for all intents and purposes it's stage 3 - // - // See https://github.com/babel/proposals/issues/12 for progress - require.resolve('@babel/plugin-proposal-class-properties'), - - // Optional Chaining proposal is stage 4 (https://github.com/tc39/proposal-optional-chaining) - // Need this since we are using TypeScript 3.7+ - require.resolve('@babel/plugin-proposal-optional-chaining'), - - // Nullish coalescing proposal is stage 4 (https://github.com/tc39/proposal-nullish-coalescing) - // Need this since we are using TypeScript 3.7+ - require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'), - - // Proposal is on stage 4, and included in ECMA-262 (https://github.com/tc39/proposal-export-ns-from) - // Need this since we are using TypeScript 3.8+ - require.resolve('@babel/plugin-proposal-export-namespace-from'), - - // Proposal is on stage 4, and included in ECMA-262 (https://github.com/tc39/proposal-export-ns-from) - // Need this since we are using TypeScript 3.9+ - require.resolve('@babel/plugin-proposal-private-methods'), - - // It enables the @babel/runtime so we can decrease the bundle sizes of the produced outputs - [ - require.resolve('@babel/plugin-transform-runtime'), +module.exports = { + presets: [ + // plugins always run before presets, but in this case we need the + // @babel/preset-typescript preset to run first so we have to move + // our explicit plugin configs to a sub-preset { - version: '^7.12.5', + plugins: [ + require.resolve('babel-plugin-add-module-exports'), + + // The class properties proposal was merged with the private fields proposal + // into the "class fields" proposal. Babel doesn't support this combined + // proposal yet, which includes private field, so this transform is + // TECHNICALLY stage 2, but for all intents and purposes it's stage 3 + // + // See https://github.com/babel/proposals/issues/12 for progress + require.resolve('@babel/plugin-proposal-class-properties'), + + // Optional Chaining proposal is stage 4 (https://github.com/tc39/proposal-optional-chaining) + // Need this since we are using TypeScript 3.7+ + require.resolve('@babel/plugin-proposal-optional-chaining'), + + // Nullish coalescing proposal is stage 4 (https://github.com/tc39/proposal-nullish-coalescing) + // Need this since we are using TypeScript 3.7+ + require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'), + + // Proposal is on stage 4, and included in ECMA-262 (https://github.com/tc39/proposal-export-ns-from) + // Need this since we are using TypeScript 3.8+ + require.resolve('@babel/plugin-proposal-export-namespace-from'), + + // Proposal is on stage 4, and included in ECMA-262 (https://github.com/tc39/proposal-export-ns-from) + // Need this since we are using TypeScript 3.9+ + require.resolve('@babel/plugin-proposal-private-methods'), + + // It enables the @babel/runtime so we can decrease the bundle sizes of the produced outputs + [ + require.resolve('@babel/plugin-transform-runtime'), + { + version: '^7.12.5', + }, + ], + ], }, - ], -]; -module.exports = { - presets: [ - [require.resolve('@babel/preset-typescript'), { allowNamespaces: true }], require.resolve('@babel/preset-react'), + + [ + require.resolve('@babel/preset-typescript'), + { + allowNamespaces: true, + allowDeclareFields: true, + }, + ], ], - plugins, }; diff --git a/packages/kbn-optimizer/src/babel_runtime_helpers/find_babel_runtime_helpers_in_entry_bundles.ts b/packages/kbn-optimizer/src/babel_runtime_helpers/find_babel_runtime_helpers_in_entry_bundles.ts index beff36023343..f00905f3f492 100644 --- a/packages/kbn-optimizer/src/babel_runtime_helpers/find_babel_runtime_helpers_in_entry_bundles.ts +++ b/packages/kbn-optimizer/src/babel_runtime_helpers/find_babel_runtime_helpers_in_entry_bundles.ts @@ -35,14 +35,14 @@ export async function runFindBabelHelpersInEntryBundlesCli() { } for (const { userRequest } of module.reasons) { - if (userRequest.startsWith('@babel/runtime/')) { + if (userRequest.startsWith('@babel/runtime')) { imports.add(userRequest); } } } } - log.success('found', imports.size, '@babel/register imports in entry bundles'); + log.success('found', imports.size, '@babel/runtime* imports in entry bundles'); log.write( Array.from(imports, (i) => `'${i}',`) .sort() diff --git a/packages/kbn-optimizer/src/worker/emit_stats_plugin.ts b/packages/kbn-optimizer/src/worker/emit_stats_plugin.ts index c964219e1fed..5cb60344037f 100644 --- a/packages/kbn-optimizer/src/worker/emit_stats_plugin.ts +++ b/packages/kbn-optimizer/src/worker/emit_stats_plugin.ts @@ -26,7 +26,7 @@ export class EmitStatsPlugin { (stats) => { Fs.writeFileSync( Path.resolve(this.bundle.outputDir, 'stats.json'), - JSON.stringify(stats.toJson()) + JSON.stringify(stats.toJson(), null, 2) ); } ); diff --git a/packages/kbn-storybook/src/webpack.config.ts b/packages/kbn-storybook/src/webpack.config.ts index 27e887eda65c..53f9c82b8681 100644 --- a/packages/kbn-storybook/src/webpack.config.ts +++ b/packages/kbn-storybook/src/webpack.config.ts @@ -9,11 +9,13 @@ import { externals } from '@kbn/ui-shared-deps-src'; import { stringifyRequest } from 'loader-utils'; import { resolve } from 'path'; -import { Configuration, Stats } from 'webpack'; +import webpack, { Configuration, Stats } from 'webpack'; import webpackMerge from 'webpack-merge'; import { REPO_ROOT } from './lib/constants'; import { IgnoreNotFoundExportPlugin } from './ignore_not_found_export_plugin'; +type Preset = string | [string, Record] | Record; + const stats = { ...Stats.presetToOptions('minimal'), colors: true, @@ -22,6 +24,46 @@ const stats = { moduleTrace: true, }; +function isProgressPlugin(plugin: any) { + return 'handler' in plugin && plugin.showActiveModules && plugin.showModules; +} + +function isHtmlPlugin(plugin: any): plugin is { options: { template: string } } { + return !!(typeof plugin.options?.template === 'string'); +} + +function isBabelLoaderRule(rule: webpack.RuleSetRule): rule is webpack.RuleSetRule & { + use: webpack.RuleSetLoader[]; +} { + return !!( + rule.use && + Array.isArray(rule.use) && + rule.use.some( + (l) => + typeof l === 'object' && typeof l.loader === 'string' && l.loader.includes('babel-loader') + ) + ); +} + +function getPresetPath(preset: Preset) { + if (typeof preset === 'string') return preset; + if (Array.isArray(preset)) return preset[0]; + return undefined; +} + +function getTsPreset(preset: Preset) { + if (getPresetPath(preset)?.includes('preset-typescript')) { + if (typeof preset === 'string') return [preset, {}]; + if (Array.isArray(preset)) return preset; + + throw new Error('unsupported preset-typescript format'); + } +} + +function isDesiredPreset(preset: Preset) { + return !getPresetPath(preset)?.includes('preset-flow'); +} + // Extend the Storybook Webpack config with some customizations /* eslint-disable import/no-default-export */ export default function ({ config: storybookConfig }: { config: Configuration }) { @@ -83,21 +125,72 @@ export default function ({ config: storybookConfig }: { config: Configuration }) stats, }; - // Disable the progress plugin - const progressPlugin: any = (storybookConfig.plugins || []).find((plugin: any) => { - return 'handler' in plugin && plugin.showActiveModules && plugin.showModules; - }); - progressPlugin.handler = () => {}; - - // This is the hacky part. We find something that looks like the - // HtmlWebpackPlugin and mutate its `options.template` to point at our - // revised template. - const htmlWebpackPlugin: any = (storybookConfig.plugins || []).find((plugin: any) => { - return plugin.options && typeof plugin.options.template === 'string'; - }); - if (htmlWebpackPlugin) { - htmlWebpackPlugin.options.template = require.resolve('../templates/index.ejs'); + const updatedModuleRules = []; + // clone and modify the module.rules config provided by storybook so that the default babel plugins run after the typescript preset + for (const originalRule of storybookConfig.module?.rules ?? []) { + const rule = { ...originalRule }; + updatedModuleRules.push(rule); + + if (isBabelLoaderRule(rule)) { + rule.use = [...rule.use]; + const loader = (rule.use[0] = { ...rule.use[0] }); + const options = (loader.options = { ...(loader.options as Record) }); + + // capture the plugins defined at the root level + const plugins: string[] = options.plugins; + options.plugins = []; + + // move the plugins to the top of the preset array so they will run after the typescript preset + options.presets = [ + { + plugins, + }, + ...(options.presets as Preset[]).filter(isDesiredPreset).map((preset) => { + const tsPreset = getTsPreset(preset); + if (!tsPreset) { + return preset; + } + + return [ + tsPreset[0], + { + ...tsPreset[1], + allowNamespaces: true, + allowDeclareFields: true, + }, + ]; + }), + ]; + } } - return webpackMerge(storybookConfig, config); + // copy and modify the webpack plugins added by storybook + const filteredStorybookPlugins = []; + for (const plugin of storybookConfig.plugins ?? []) { + // Remove the progress plugin + if (isProgressPlugin(plugin)) { + continue; + } + + // This is the hacky part. We find something that looks like the + // HtmlWebpackPlugin and mutate its `options.template` to point at our + // revised template. + if (isHtmlPlugin(plugin)) { + plugin.options.template = require.resolve('../templates/index.ejs'); + } + + filteredStorybookPlugins.push(plugin); + } + + return webpackMerge( + { + ...storybookConfig, + plugins: filteredStorybookPlugins, + module: { + ...storybookConfig.module, + rules: updatedModuleRules, + }, + }, + config + ); } diff --git a/src/plugins/console/public/services/history.ts b/src/plugins/console/public/services/history.ts index ee1e97ceb386..972e5283274d 100644 --- a/src/plugins/console/public/services/history.ts +++ b/src/plugins/console/public/services/history.ts @@ -14,9 +14,11 @@ const MAX_NUMBER_OF_HISTORY_ITEMS = 100; export const isQuotaExceededError = (e: Error): boolean => e.name === 'QuotaExceededError'; export class History { - constructor(private readonly storage: Storage) {} + private changeEmitter: BehaviorSubject; - private changeEmitter = new BehaviorSubject(this.getHistory() || []); + constructor(private readonly storage: Storage) { + this.changeEmitter = new BehaviorSubject(this.getHistory() || []); + } getHistoryKeys() { return this.storage diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx index 611a426dd4d7..1e19e495585f 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx +++ b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx @@ -32,7 +32,7 @@ interface State { export class DashboardViewport extends React.Component { static contextType = context; - public readonly context!: DashboardReactContextValue; + public declare readonly context: DashboardReactContextValue; private controlsRoot: React.RefObject; diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx index 007c3e306372..1601ea481cf2 100644 --- a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx @@ -104,7 +104,7 @@ describe('rolesManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
- Role Edit Page: {"action":"edit","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"fieldCache":{}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}} + Role Edit Page: {"action":"edit","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"fieldCache":{},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}}
`); @@ -129,7 +129,7 @@ describe('rolesManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
- Role Edit Page: {"action":"edit","roleName":"role@name","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"fieldCache":{}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@name","search":"","hash":""}}} + Role Edit Page: {"action":"edit","roleName":"role@name","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"fieldCache":{},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@name","search":"","hash":""}}}
`); @@ -154,7 +154,7 @@ describe('rolesManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
- Role Edit Page: {"action":"clone","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"fieldCache":{}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/clone/someRoleName","search":"","hash":""}}} + Role Edit Page: {"action":"clone","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"fieldCache":{},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/clone/someRoleName","search":"","hash":""}}}
`); From 57ae8db66a53a87c976e67d82a297baf3857f1c1 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 29 Nov 2021 12:39:55 -0600 Subject: [PATCH 022/224] [RAC,Security Solution]Update alerts mappings to ECS 1.12 (#118812) * Update output directory for generative script These files were moved in #98935 but the script has become out of date. * Update ECS fieldmap with ECS 1.12 This fieldmap was missing fields from ECS 1.11+. Notable ommissions were the threat.indicator and threat.enrichments fieldsets. * Remove non-additive mappings changes These are incompatible with the current alerts framework. * Add only necessary threat fields for CTI features This could probably be pared down further, as most of these fields are not critical for CTI features. Additionally, these additions now exceed the limit of 1000 fields and is causing an error in the ruleRegistry bootstrapping. * Remove file.pe threat fields * Remove geo threat indicator fields * Remove all threat.indicator mappings These are not relevant for alerts, which will only have enrichments. * increments index mappings total fields limit to 1200 Co-authored-by: Ece Ozalp Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/assets/field_maps/ecs_field_map.ts | 420 ++++++++++++++++++ .../scripts/generate_ecs_fieldmap/index.js | 2 +- .../resource_installer.ts | 2 +- 3 files changed, 422 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts index 859070bd498e..1ea85e5a5434 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts @@ -2550,6 +2550,426 @@ export const ecsFieldMap = { array: false, required: false, }, + 'threat.enrichments': { + type: 'nested', + array: true, + required: false, + }, + 'threat.enrichments.indicator': { + type: 'object', + array: false, + required: false, + }, + 'threat.enrichments.indicator.as.number': { + type: 'long', + array: false, + required: false, + }, + 'threat.enrichments.indicator.as.organization.name': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.confidence': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.description': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.email.address': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.accessed': { + type: 'date', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.attributes': { + type: 'keyword', + array: true, + required: false, + }, + 'threat.enrichments.indicator.file.code_signature.digest_algorithm': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.code_signature.exists': { + type: 'boolean', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.code_signature.signing_id': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.code_signature.status': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.code_signature.subject_name': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.code_signature.team_id': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.code_signature.timestamp': { + type: 'date', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.code_signature.trusted': { + type: 'boolean', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.code_signature.valid': { + type: 'boolean', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.created': { + type: 'date', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.ctime': { + type: 'date', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.device': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.directory': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.drive_letter': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.extension': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.fork_name': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.gid': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.group': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.hash.md5': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.hash.sha1': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.hash.sha256': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.hash.sha512': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.hash.ssdeep': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.inode': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.mime_type': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.mode': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.mtime': { + type: 'date', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.name': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.owner': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.path': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.size': { + type: 'long', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.target_path': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.type': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.file.uid': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.first_seen': { + type: 'date', + array: false, + required: false, + }, + 'threat.enrichments.indicator.ip': { + type: 'ip', + array: false, + required: false, + }, + 'threat.enrichments.indicator.last_seen': { + type: 'date', + array: false, + required: false, + }, + 'threat.enrichments.indicator.marking.tlp': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.modified_at': { + type: 'date', + array: false, + required: false, + }, + 'threat.enrichments.indicator.port': { + type: 'long', + array: false, + required: false, + }, + 'threat.enrichments.indicator.provider': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.reference': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.registry.data.bytes': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.registry.data.strings': { + type: 'wildcard', + array: true, + required: false, + }, + 'threat.enrichments.indicator.registry.data.type': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.registry.hive': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.registry.key': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.registry.path': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.registry.value': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.scanner_stats': { + type: 'long', + array: false, + required: false, + }, + 'threat.enrichments.indicator.sightings': { + type: 'long', + array: false, + required: false, + }, + 'threat.enrichments.indicator.type': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.url.domain': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.url.extension': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.url.fragment': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.url.full': { + type: 'wildcard', + array: false, + required: false, + }, + 'threat.enrichments.indicator.url.original': { + type: 'wildcard', + array: false, + required: false, + }, + 'threat.enrichments.indicator.url.password': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.url.path': { + type: 'wildcard', + array: false, + required: false, + }, + 'threat.enrichments.indicator.url.port': { + type: 'long', + array: false, + required: false, + }, + 'threat.enrichments.indicator.url.query': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.url.registered_domain': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.url.scheme': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.url.subdomain': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.url.top_level_domain': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.indicator.url.username': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.matched.atomic': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.matched.field': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.matched.id': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.matched.index': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.enrichments.matched.type': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.group.alias': { + type: 'keyword', + array: true, + required: false, + }, + 'threat.group.id': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.group.name': { + type: 'keyword', + array: false, + required: false, + }, + 'threat.group.reference': { + type: 'keyword', + array: false, + required: false, + }, 'threat.tactic.id': { type: 'keyword', array: true, diff --git a/x-pack/plugins/rule_registry/scripts/generate_ecs_fieldmap/index.js b/x-pack/plugins/rule_registry/scripts/generate_ecs_fieldmap/index.js index 6b10ca5f837d..bbcf651bd6d6 100644 --- a/x-pack/plugins/rule_registry/scripts/generate_ecs_fieldmap/index.js +++ b/x-pack/plugins/rule_registry/scripts/generate_ecs_fieldmap/index.js @@ -19,7 +19,7 @@ const exec = util.promisify(execCb); const ecsDir = path.resolve(__dirname, '../../../../../../ecs'); const ecsYamlFilename = path.join(ecsDir, 'generated/ecs/ecs_flat.yml'); -const outputDir = path.join(__dirname, '../../common/field_map'); +const outputDir = path.join(__dirname, '../../common/assets/field_maps'); const outputFieldMapFilename = path.join(outputDir, 'ecs_field_map.ts'); diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts index bfdec28a5098..bbfa17c5694f 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts @@ -316,7 +316,7 @@ export class ResourceInstaller { // @ts-expect-error rollover_alias: primaryNamespacedAlias, }, - 'index.mapping.total_fields.limit': 1100, + 'index.mapping.total_fields.limit': 1200, }, mappings: { dynamic: false, From 2a0312cd48704c2bf76fa0ca3289211e7d57f263 Mon Sep 17 00:00:00 2001 From: Ersin Erdal <92688503+ersin-erdal@users.noreply.github.com> Date: Mon, 29 Nov 2021 19:44:17 +0100 Subject: [PATCH 023/224] 119061 refactor observability (#119211) * group containers and components, create index files --- x-pack/plugins/observability/public/index.ts | 4 +- .../{ => components}/alerts_disclaimer.tsx | 0 .../alerts_flyout/alerts_flyout.stories.tsx | 10 ++--- .../alerts_flyout/alerts_flyout.test.tsx | 10 ++--- .../alerts_flyout/alerts_flyout.tsx} | 14 +++---- .../alerts_flyout}/example_data.ts | 0 .../alerts/components/alerts_flyout/index.ts | 8 ++++ .../{ => components}/alerts_search_bar.tsx | 4 +- .../{ => components}/alerts_status_filter.tsx | 4 +- .../{ => components}/default_cell_actions.tsx | 6 +-- .../{ => components}/filter_for_value.tsx | 0 .../public/pages/alerts/components/index.ts | 17 +++++++++ .../alerts/{ => components}/parse_alert.ts | 8 ++-- .../components/render_cell_value/index.ts | 8 ++++ .../render_cell_value.test.tsx | 8 ++-- .../render_cell_value}/render_cell_value.tsx | 16 ++++---- .../alerts/components/severity_badge/index.ts | 8 ++++ .../severity_badge.stories.tsx | 0 .../severity_badge}/severity_badge.tsx | 0 .../workflow_status_filter/index.ts | 8 ++++ .../workflow_status_filter.stories.tsx | 2 +- .../workflow_status_filter.test.tsx | 2 +- .../workflow_status_filter.tsx | 2 +- .../alerts_page/alerts_page.tsx} | 38 +++++++++---------- .../alerts/containers/alerts_page/index.ts | 9 +++++ .../{ => containers/alerts_page}/styles.scss | 0 .../alerts_table_t_grid.tsx | 28 +++++++------- .../containers/alerts_table_t_grid/index.ts | 8 ++++ .../public/pages/alerts/containers/index.ts | 10 +++++ .../state_container/index.tsx | 0 .../state_container/state_container.tsx | 4 +- .../use_alerts_page_state_container.tsx | 6 +-- .../public/pages/alerts/index.ts | 9 +++++ .../public/pages/cases/helpers.ts | 4 +- .../observability/public/routes/index.tsx | 2 +- 35 files changed, 171 insertions(+), 86 deletions(-) rename x-pack/plugins/observability/public/pages/alerts/{ => components}/alerts_disclaimer.tsx (100%) rename x-pack/plugins/observability/public/pages/alerts/{ => components}/alerts_flyout/alerts_flyout.stories.tsx (85%) rename x-pack/plugins/observability/public/pages/alerts/{ => components}/alerts_flyout/alerts_flyout.test.tsx (91%) rename x-pack/plugins/observability/public/pages/alerts/{alerts_flyout/index.tsx => components/alerts_flyout/alerts_flyout.tsx} (89%) rename x-pack/plugins/observability/public/pages/alerts/{ => components/alerts_flyout}/example_data.ts (100%) create mode 100644 x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/index.ts rename x-pack/plugins/observability/public/pages/alerts/{ => components}/alerts_search_bar.tsx (92%) rename x-pack/plugins/observability/public/pages/alerts/{ => components}/alerts_status_filter.tsx (94%) rename x-pack/plugins/observability/public/pages/alerts/{ => components}/default_cell_actions.tsx (85%) rename x-pack/plugins/observability/public/pages/alerts/{ => components}/filter_for_value.tsx (100%) create mode 100644 x-pack/plugins/observability/public/pages/alerts/components/index.ts rename x-pack/plugins/observability/public/pages/alerts/{ => components}/parse_alert.ts (78%) create mode 100644 x-pack/plugins/observability/public/pages/alerts/components/render_cell_value/index.ts rename x-pack/plugins/observability/public/pages/alerts/{ => components/render_cell_value}/render_cell_value.test.tsx (87%) rename x-pack/plugins/observability/public/pages/alerts/{ => components/render_cell_value}/render_cell_value.tsx (86%) create mode 100644 x-pack/plugins/observability/public/pages/alerts/components/severity_badge/index.ts rename x-pack/plugins/observability/public/pages/alerts/{ => components/severity_badge}/severity_badge.stories.tsx (100%) rename x-pack/plugins/observability/public/pages/alerts/{ => components/severity_badge}/severity_badge.tsx (100%) create mode 100644 x-pack/plugins/observability/public/pages/alerts/components/workflow_status_filter/index.ts rename x-pack/plugins/observability/public/pages/alerts/{ => components/workflow_status_filter}/workflow_status_filter.stories.tsx (92%) rename x-pack/plugins/observability/public/pages/alerts/{ => components/workflow_status_filter}/workflow_status_filter.test.tsx (95%) rename x-pack/plugins/observability/public/pages/alerts/{ => components/workflow_status_filter}/workflow_status_filter.tsx (95%) rename x-pack/plugins/observability/public/pages/alerts/{index.tsx => containers/alerts_page/alerts_page.tsx} (87%) create mode 100644 x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/index.ts rename x-pack/plugins/observability/public/pages/alerts/{ => containers/alerts_page}/styles.scss (100%) rename x-pack/plugins/observability/public/pages/alerts/{ => containers/alerts_table_t_grid}/alerts_table_t_grid.tsx (94%) create mode 100644 x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/index.ts create mode 100644 x-pack/plugins/observability/public/pages/alerts/containers/index.ts rename x-pack/plugins/observability/public/pages/alerts/{ => containers}/state_container/index.tsx (100%) rename x-pack/plugins/observability/public/pages/alerts/{ => containers}/state_container/state_container.tsx (92%) rename x-pack/plugins/observability/public/pages/alerts/{ => containers}/state_container/use_alerts_page_state_container.tsx (92%) create mode 100644 x-pack/plugins/observability/public/pages/alerts/index.ts diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 7646ac9bec9b..2383044bc14c 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -64,7 +64,9 @@ export { METRIC_TYPE, } from './hooks/use_track_metric'; -export const LazyAlertsFlyout = lazy(() => import('./pages/alerts/alerts_flyout')); +export const LazyAlertsFlyout = lazy( + () => import('./pages/alerts/components/alerts_flyout/alerts_flyout') +); export { useFetcher, FETCH_STATUS } from './hooks/use_fetcher'; export { useEsSearch, createEsParams } from './hooks/use_es_search'; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_disclaimer.tsx b/x-pack/plugins/observability/public/pages/alerts/components/alerts_disclaimer.tsx similarity index 100% rename from x-pack/plugins/observability/public/pages/alerts/alerts_disclaimer.tsx rename to x-pack/plugins/observability/public/pages/alerts/components/alerts_disclaimer.tsx diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx b/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout.stories.tsx similarity index 85% rename from x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx rename to x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout.stories.tsx index 64d495dbbc79..36b1fc2f2b6e 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout.stories.tsx @@ -7,11 +7,11 @@ import { ALERT_UUID } from '@kbn/rule-data-utils/technical_field_names'; import React, { ComponentType } from 'react'; -import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; -import { PluginContext, PluginContextValue } from '../../../context/plugin_context'; -import { createObservabilityRuleTypeRegistryMock } from '../../../rules/observability_rule_type_registry_mock'; -import { apmAlertResponseExample } from '../example_data'; -import { AlertsFlyout } from './'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { PluginContext, PluginContextValue } from '../../../../context/plugin_context'; +import { createObservabilityRuleTypeRegistryMock } from '../../../../rules/observability_rule_type_registry_mock'; +import { apmAlertResponseExample } from './example_data'; +import { AlertsFlyout } from '..'; interface Args { alerts: Array>; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.test.tsx b/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout.test.tsx similarity index 91% rename from x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.test.tsx rename to x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout.test.tsx index 4fdc8d245799..13fb5d805fb8 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.test.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout.test.tsx @@ -6,11 +6,11 @@ */ import React from 'react'; -import * as useUiSettingHook from '../../../../../../../src/plugins/kibana_react/public/ui_settings/use_ui_setting'; -import { createObservabilityRuleTypeRegistryMock } from '../../../rules/observability_rule_type_registry_mock'; -import { render } from '../../../utils/test_helper'; -import type { TopAlert } from '../'; -import { AlertsFlyout } from './'; +import * as useUiSettingHook from '../../../../../../../../src/plugins/kibana_react/public/ui_settings/use_ui_setting'; +import { createObservabilityRuleTypeRegistryMock } from '../../../../rules/observability_rule_type_registry_mock'; +import { render } from '../../../../utils/test_helper'; +import type { TopAlert } from '../../containers/alerts_page'; +import { AlertsFlyout } from '..'; describe('AlertsFlyout', () => { jest diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx b/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout.tsx similarity index 89% rename from x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx rename to x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout.tsx index c5cad5f3b1c8..ced4896c5f31 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout.tsx @@ -35,14 +35,14 @@ import { } from '@kbn/rule-data-utils/alerts_as_data_status'; import moment from 'moment-timezone'; import React, { useMemo } from 'react'; -import type { TopAlert } from '../'; -import { useKibana, useUiSetting } from '../../../../../../../src/plugins/kibana_react/public'; -import { asDuration } from '../../../../common/utils/formatters'; -import type { ObservabilityRuleTypeRegistry } from '../../../rules/create_observability_rule_type_registry'; +import type { TopAlert } from '../../containers'; +import { useKibana, useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public'; +import { asDuration } from '../../../../../common/utils/formatters'; +import type { ObservabilityRuleTypeRegistry } from '../../../../rules/create_observability_rule_type_registry'; import { parseAlert } from '../parse_alert'; -import { AlertStatusIndicator } from '../../../components/shared/alert_status_indicator'; -import { ExperimentalBadge } from '../../../components/shared/experimental_badge'; -import { translations, paths } from '../../../config'; +import { AlertStatusIndicator } from '../../../../components/shared/alert_status_indicator'; +import { ExperimentalBadge } from '../../../../components/shared/experimental_badge'; +import { translations, paths } from '../../../../config'; type AlertsFlyoutProps = { alert?: TopAlert; diff --git a/x-pack/plugins/observability/public/pages/alerts/example_data.ts b/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/example_data.ts similarity index 100% rename from x-pack/plugins/observability/public/pages/alerts/example_data.ts rename to x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/example_data.ts diff --git a/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/index.ts b/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/index.ts new file mode 100644 index 000000000000..4153ab6e5b59 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/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 { AlertsFlyout } from './alerts_flyout'; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_search_bar.tsx b/x-pack/plugins/observability/public/pages/alerts/components/alerts_search_bar.tsx similarity index 92% rename from x-pack/plugins/observability/public/pages/alerts/alerts_search_bar.tsx rename to x-pack/plugins/observability/public/pages/alerts/components/alerts_search_bar.tsx index 926f03acf01d..14d47d1e7e9d 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_search_bar.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/alerts_search_bar.tsx @@ -8,8 +8,8 @@ import { IndexPatternBase } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import React, { useMemo, useState } from 'react'; -import { SearchBar, TimeHistory } from '../../../../../../src/plugins/data/public'; -import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; +import { SearchBar, TimeHistory } from '../../../../../../../src/plugins/data/public'; +import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; export function AlertsSearchBar({ dynamicIndexPatterns, diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_status_filter.tsx b/x-pack/plugins/observability/public/pages/alerts/components/alerts_status_filter.tsx similarity index 94% rename from x-pack/plugins/observability/public/pages/alerts/alerts_status_filter.tsx rename to x-pack/plugins/observability/public/pages/alerts/components/alerts_status_filter.tsx index 38c753bbebf3..d717e916de2c 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_status_filter.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/alerts_status_filter.tsx @@ -13,8 +13,8 @@ import { ALERT_STATUS_RECOVERED, } from '@kbn/rule-data-utils/alerts_as_data_status'; import { ALERT_STATUS } from '@kbn/rule-data-utils/technical_field_names'; -import { AlertStatusFilterButton } from '../../../common/typings'; -import { AlertStatusFilter } from '../../../common/typings'; +import { AlertStatusFilterButton } from '../../../../common/typings'; +import { AlertStatusFilter } from '../../../../common/typings'; export interface AlertStatusFilterProps { status: AlertStatusFilterButton; diff --git a/x-pack/plugins/observability/public/pages/alerts/default_cell_actions.tsx b/x-pack/plugins/observability/public/pages/alerts/components/default_cell_actions.tsx similarity index 85% rename from x-pack/plugins/observability/public/pages/alerts/default_cell_actions.tsx rename to x-pack/plugins/observability/public/pages/alerts/components/default_cell_actions.tsx index 3adfb0a1d9c8..5126647161fa 100644 --- a/x-pack/plugins/observability/public/pages/alerts/default_cell_actions.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/default_cell_actions.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { getMappedNonEcsValue } from './render_cell_value'; import FilterForValueButton from './filter_for_value'; -import { TimelineNonEcsData } from '../../../../timelines/common/search_strategy'; -import { TGridCellAction } from '../../../../timelines/common/types/timeline'; -import { getPageRowIndex } from '../../../../timelines/public'; +import { TimelineNonEcsData } from '../../../../../timelines/common/search_strategy'; +import { TGridCellAction } from '../../../../../timelines/common/types/timeline'; +import { getPageRowIndex } from '../../../../../timelines/public'; export const FILTER_FOR_VALUE = i18n.translate('xpack.observability.hoverActions.filterForValue', { defaultMessage: 'Filter for value', diff --git a/x-pack/plugins/observability/public/pages/alerts/filter_for_value.tsx b/x-pack/plugins/observability/public/pages/alerts/components/filter_for_value.tsx similarity index 100% rename from x-pack/plugins/observability/public/pages/alerts/filter_for_value.tsx rename to x-pack/plugins/observability/public/pages/alerts/components/filter_for_value.tsx diff --git a/x-pack/plugins/observability/public/pages/alerts/components/index.ts b/x-pack/plugins/observability/public/pages/alerts/components/index.ts new file mode 100644 index 000000000000..57ad311f65d1 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/components/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. + */ + +export * from './alerts_flyout'; +export * from './render_cell_value'; +export * from './severity_badge'; +export * from './workflow_status_filter'; +export * from './alerts_search_bar'; +export * from './alerts_disclaimer'; +export * from './default_cell_actions'; +export * from './filter_for_value'; +export * from './parse_alert'; +export * from './alerts_status_filter'; diff --git a/x-pack/plugins/observability/public/pages/alerts/parse_alert.ts b/x-pack/plugins/observability/public/pages/alerts/components/parse_alert.ts similarity index 78% rename from x-pack/plugins/observability/public/pages/alerts/parse_alert.ts rename to x-pack/plugins/observability/public/pages/alerts/components/parse_alert.ts index 7b2880308406..680798811e9a 100644 --- a/x-pack/plugins/observability/public/pages/alerts/parse_alert.ts +++ b/x-pack/plugins/observability/public/pages/alerts/components/parse_alert.ts @@ -12,10 +12,10 @@ import { ALERT_RULE_NAME, } from '@kbn/rule-data-utils/technical_field_names'; import { ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils/alerts_as_data_status'; -import type { TopAlert } from '.'; -import { parseTechnicalFields } from '../../../../rule_registry/common/parse_technical_fields'; -import { asDuration, asPercent } from '../../../common/utils/formatters'; -import { ObservabilityRuleTypeRegistry } from '../../rules/create_observability_rule_type_registry'; +import type { TopAlert } from '../'; +import { parseTechnicalFields } from '../../../../../rule_registry/common/parse_technical_fields'; +import { asDuration, asPercent } from '../../../../common/utils/formatters'; +import { ObservabilityRuleTypeRegistry } from '../../../rules/create_observability_rule_type_registry'; export const parseAlert = (observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry) => diff --git a/x-pack/plugins/observability/public/pages/alerts/components/render_cell_value/index.ts b/x-pack/plugins/observability/public/pages/alerts/components/render_cell_value/index.ts new file mode 100644 index 000000000000..b6df77f07588 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/components/render_cell_value/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 { getRenderCellValue, getMappedNonEcsValue } from './render_cell_value'; diff --git a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.test.tsx b/x-pack/plugins/observability/public/pages/alerts/components/render_cell_value/render_cell_value.test.tsx similarity index 87% rename from x-pack/plugins/observability/public/pages/alerts/render_cell_value.test.tsx rename to x-pack/plugins/observability/public/pages/alerts/components/render_cell_value/render_cell_value.test.tsx index 79a27faa96c6..25de2e36b08c 100644 --- a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.test.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/render_cell_value/render_cell_value.test.tsx @@ -10,10 +10,10 @@ import { ALERT_STATUS_RECOVERED, } from '@kbn/rule-data-utils/alerts_as_data_status'; import { ALERT_STATUS } from '@kbn/rule-data-utils/technical_field_names'; -import type { CellValueElementProps } from '../../../../timelines/common'; -import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock'; -import * as PluginHook from '../../hooks/use_plugin_context'; -import { render } from '../../utils/test_helper'; +import type { CellValueElementProps } from '../../../../../../timelines/common'; +import { createObservabilityRuleTypeRegistryMock } from '../../../../rules/observability_rule_type_registry_mock'; +import * as PluginHook from '../../../../hooks/use_plugin_context'; +import { render } from '../../../../utils/test_helper'; import { getRenderCellValue } from './render_cell_value'; interface AlertsTableRow { diff --git a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx b/x-pack/plugins/observability/public/pages/alerts/components/render_cell_value/render_cell_value.tsx similarity index 86% rename from x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx rename to x-pack/plugins/observability/public/pages/alerts/components/render_cell_value/render_cell_value.tsx index 80ccd4a69b28..d9fa6c6e2221 100644 --- a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/render_cell_value/render_cell_value.tsx @@ -17,14 +17,14 @@ import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED, } from '@kbn/rule-data-utils/alerts_as_data_status'; -import type { CellValueElementProps, TimelineNonEcsData } from '../../../../timelines/common'; -import { AlertStatusIndicator } from '../../components/shared/alert_status_indicator'; -import { TimestampTooltip } from '../../components/shared/timestamp_tooltip'; -import { asDuration } from '../../../common/utils/formatters'; -import { SeverityBadge } from './severity_badge'; -import { TopAlert } from '.'; -import { parseAlert } from './parse_alert'; -import { usePluginContext } from '../../hooks/use_plugin_context'; +import type { CellValueElementProps, TimelineNonEcsData } from '../../../../../../timelines/common'; +import { AlertStatusIndicator } from '../../../../components/shared/alert_status_indicator'; +import { TimestampTooltip } from '../../../../components/shared/timestamp_tooltip'; +import { asDuration } from '../../../../../common/utils/formatters'; +import { SeverityBadge } from '../severity_badge'; +import { TopAlert } from '../../'; +import { parseAlert } from '../parse_alert'; +import { usePluginContext } from '../../../../hooks/use_plugin_context'; export const getMappedNonEcsValue = ({ data, diff --git a/x-pack/plugins/observability/public/pages/alerts/components/severity_badge/index.ts b/x-pack/plugins/observability/public/pages/alerts/components/severity_badge/index.ts new file mode 100644 index 000000000000..797415632708 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/components/severity_badge/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 { SeverityBadge } from './severity_badge'; diff --git a/x-pack/plugins/observability/public/pages/alerts/severity_badge.stories.tsx b/x-pack/plugins/observability/public/pages/alerts/components/severity_badge/severity_badge.stories.tsx similarity index 100% rename from x-pack/plugins/observability/public/pages/alerts/severity_badge.stories.tsx rename to x-pack/plugins/observability/public/pages/alerts/components/severity_badge/severity_badge.stories.tsx diff --git a/x-pack/plugins/observability/public/pages/alerts/severity_badge.tsx b/x-pack/plugins/observability/public/pages/alerts/components/severity_badge/severity_badge.tsx similarity index 100% rename from x-pack/plugins/observability/public/pages/alerts/severity_badge.tsx rename to x-pack/plugins/observability/public/pages/alerts/components/severity_badge/severity_badge.tsx diff --git a/x-pack/plugins/observability/public/pages/alerts/components/workflow_status_filter/index.ts b/x-pack/plugins/observability/public/pages/alerts/components/workflow_status_filter/index.ts new file mode 100644 index 000000000000..84badecd29dc --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/components/workflow_status_filter/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 { WorkflowStatusFilter } from './workflow_status_filter'; diff --git a/x-pack/plugins/observability/public/pages/alerts/workflow_status_filter.stories.tsx b/x-pack/plugins/observability/public/pages/alerts/components/workflow_status_filter/workflow_status_filter.stories.tsx similarity index 92% rename from x-pack/plugins/observability/public/pages/alerts/workflow_status_filter.stories.tsx rename to x-pack/plugins/observability/public/pages/alerts/components/workflow_status_filter/workflow_status_filter.stories.tsx index e06b5d333a9a..4dce3ee80b83 100644 --- a/x-pack/plugins/observability/public/pages/alerts/workflow_status_filter.stories.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/workflow_status_filter/workflow_status_filter.stories.tsx @@ -6,7 +6,7 @@ */ import React, { ComponentProps, useState } from 'react'; -import type { AlertWorkflowStatus } from '../../../common/typings'; +import type { AlertWorkflowStatus } from '../../../../../common/typings'; import { WorkflowStatusFilter } from './workflow_status_filter'; type Args = ComponentProps; diff --git a/x-pack/plugins/observability/public/pages/alerts/workflow_status_filter.test.tsx b/x-pack/plugins/observability/public/pages/alerts/components/workflow_status_filter/workflow_status_filter.test.tsx similarity index 95% rename from x-pack/plugins/observability/public/pages/alerts/workflow_status_filter.test.tsx rename to x-pack/plugins/observability/public/pages/alerts/components/workflow_status_filter/workflow_status_filter.test.tsx index 29c5e88788a8..a9819a6619dc 100644 --- a/x-pack/plugins/observability/public/pages/alerts/workflow_status_filter.test.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/workflow_status_filter/workflow_status_filter.test.tsx @@ -8,7 +8,7 @@ import { render } from '@testing-library/react'; import { Simulate } from 'react-dom/test-utils'; import React from 'react'; -import type { AlertWorkflowStatus } from '../../../common/typings'; +import type { AlertWorkflowStatus } from '../../../../../common/typings'; import { WorkflowStatusFilter } from './workflow_status_filter'; describe('StatusFilter', () => { diff --git a/x-pack/plugins/observability/public/pages/alerts/workflow_status_filter.tsx b/x-pack/plugins/observability/public/pages/alerts/components/workflow_status_filter/workflow_status_filter.tsx similarity index 95% rename from x-pack/plugins/observability/public/pages/alerts/workflow_status_filter.tsx rename to x-pack/plugins/observability/public/pages/alerts/components/workflow_status_filter/workflow_status_filter.tsx index d857b9d6bd65..86116fb96968 100644 --- a/x-pack/plugins/observability/public/pages/alerts/workflow_status_filter.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/workflow_status_filter/workflow_status_filter.tsx @@ -8,7 +8,7 @@ import { EuiButtonGroup, EuiButtonGroupOptionProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import type { AlertWorkflowStatus } from '../../../common/typings'; +import type { AlertWorkflowStatus } from '../../../../../common/typings'; export interface WorkflowStatusFilterProps { status: AlertWorkflowStatus; diff --git a/x-pack/plugins/observability/public/pages/alerts/index.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx similarity index 87% rename from x-pack/plugins/observability/public/pages/alerts/index.tsx rename to x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx index 2636463bcfd7..b19a1dbe86fe 100644 --- a/x-pack/plugins/observability/public/pages/alerts/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx @@ -14,23 +14,25 @@ import useAsync from 'react-use/lib/useAsync'; import { AlertStatus } from '@kbn/rule-data-utils/alerts_as_data_status'; import { ALERT_STATUS } from '@kbn/rule-data-utils/technical_field_names'; -import { AlertStatusFilterButton } from '../../../common/typings'; -import { ParsedTechnicalFields } from '../../../../rule_registry/common/parse_technical_fields'; -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 { useTimefilterService } from '../../hooks/use_timefilter_service'; -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 { Provider, alertsPageStateContainer, useAlertsPageStateContainer } from './state_container'; +import { AlertStatusFilterButton } from '../../../../../common/typings'; +import { ParsedTechnicalFields } from '../../../../../../rule_registry/common/parse_technical_fields'; +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 { useTimefilterService } from '../../../../hooks/use_timefilter_service'; +import { callObservabilityApi } from '../../../../services/call_observability_api'; +import { getNoDataConfig } from '../../../../utils/no_data_config'; +import { LoadingObservability } from '../../../overview/loading_observability'; +import { AlertsTableTGrid } from '../alerts_table_t_grid'; +import { + Provider, + alertsPageStateContainer, + useAlertsPageStateContainer, +} from '../state_container'; import './styles.scss'; -import { AlertsStatusFilter } from './alerts_status_filter'; -import { AlertsDisclaimer } from './alerts_disclaimer'; +import { AlertsStatusFilter, AlertsDisclaimer, AlertsSearchBar } from '../../components'; export interface TopAlert { fields: ParsedTechnicalFields; @@ -243,12 +245,10 @@ function AlertsPage() { ); } -function WrappedAlertsPage() { +export function WrappedAlertsPage() { return ( ); } - -export { WrappedAlertsPage as AlertsPage }; diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/index.ts b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/index.ts new file mode 100644 index 000000000000..e3509e04b2f2 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { WrappedAlertsPage as AlertsPage } from './alerts_page'; +export type { TopAlert } from './alerts_page'; diff --git a/x-pack/plugins/observability/public/pages/alerts/styles.scss b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/styles.scss similarity index 100% rename from x-pack/plugins/observability/public/pages/alerts/styles.scss rename to x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/styles.scss diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx similarity index 94% rename from x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx rename to x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx index 4b64ae07ddf0..bf99bcedc16b 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx @@ -33,33 +33,33 @@ import styled from 'styled-components'; import React, { Suspense, useMemo, useState, useCallback, useEffect } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; import { pick } from 'lodash'; -import { getAlertsPermissions } from '../../hooks/use_alert_permission'; +import { getAlertsPermissions } from '../../../../hooks/use_alert_permission'; import type { TimelinesUIStart, TGridType, TGridState, TGridModel, SortDirection, -} from '../../../../timelines/public'; +} from '../../../../../../timelines/public'; -import type { TopAlert } from './'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import type { TopAlert } from '../alerts_page/alerts_page'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import type { ActionProps, AlertWorkflowStatus, ColumnHeaderOptions, ControlColumnProps, RowRenderer, -} from '../../../../timelines/common'; - -import { getRenderCellValue } from './render_cell_value'; -import { observabilityAppId, observabilityFeatureId } from '../../../common'; -import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; -import { usePluginContext } from '../../hooks/use_plugin_context'; -import { LazyAlertsFlyout } from '../..'; -import { parseAlert } from './parse_alert'; -import { CoreStart } from '../../../../../../src/core/public'; -import { translations, paths } from '../../config'; +} from '../../../../../../timelines/common'; + +import { getRenderCellValue } from '../../components/render_cell_value'; +import { observabilityAppId, observabilityFeatureId } from '../../../../../common'; +import { useGetUserCasesPermissions } from '../../../../hooks/use_get_user_cases_permissions'; +import { usePluginContext } from '../../../../hooks/use_plugin_context'; +import { LazyAlertsFlyout } from '../../../..'; +import { parseAlert } from '../../components/parse_alert'; +import { CoreStart } from '../../../../../../../../src/core/public'; +import { translations, paths } from '../../../../config'; const ALERT_TABLE_STATE_STORAGE_KEY = 'xpack.observability.alert.tableState'; diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/index.ts b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/index.ts new file mode 100644 index 000000000000..7bbcc43230a4 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/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 { AlertsTableTGrid } from './alerts_table_t_grid'; diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/index.ts b/x-pack/plugins/observability/public/pages/alerts/containers/index.ts new file mode 100644 index 000000000000..074f48f42664 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/containers/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './alerts_page'; +export * from './alerts_table_t_grid'; +export * from './state_container'; diff --git a/x-pack/plugins/observability/public/pages/alerts/state_container/index.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/state_container/index.tsx similarity index 100% rename from x-pack/plugins/observability/public/pages/alerts/state_container/index.tsx rename to x-pack/plugins/observability/public/pages/alerts/containers/state_container/index.tsx diff --git a/x-pack/plugins/observability/public/pages/alerts/state_container/state_container.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/state_container/state_container.tsx similarity index 92% rename from x-pack/plugins/observability/public/pages/alerts/state_container/state_container.tsx rename to x-pack/plugins/observability/public/pages/alerts/containers/state_container/state_container.tsx index 3e0a801fedbe..d00109cc5d63 100644 --- a/x-pack/plugins/observability/public/pages/alerts/state_container/state_container.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/state_container/state_container.tsx @@ -8,8 +8,8 @@ import { createStateContainer, createStateContainerReactHelpers, -} from '../../../../../../../src/plugins/kibana_utils/public'; -import type { AlertWorkflowStatus } from '../../../../common/typings'; +} from '../../../../../../../../src/plugins/kibana_utils/public'; +import type { AlertWorkflowStatus } from '../../../../../common/typings'; interface AlertsPageContainerState { rangeFrom: string; diff --git a/x-pack/plugins/observability/public/pages/alerts/state_container/use_alerts_page_state_container.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/state_container/use_alerts_page_state_container.tsx similarity index 92% rename from x-pack/plugins/observability/public/pages/alerts/state_container/use_alerts_page_state_container.tsx rename to x-pack/plugins/observability/public/pages/alerts/containers/state_container/use_alerts_page_state_container.tsx index dfa4afcd939c..5e81286affba 100644 --- a/x-pack/plugins/observability/public/pages/alerts/state_container/use_alerts_page_state_container.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/state_container/use_alerts_page_state_container.tsx @@ -8,14 +8,14 @@ import { useEffect } from 'react'; import { useHistory } from 'react-router-dom'; -import { TimefilterContract } from '../../../../../../../src/plugins/data/public'; +import { TimefilterContract } from '../../../../../../../../src/plugins/data/public'; import { createKbnUrlStateStorage, syncState, IKbnUrlStateStorage, useContainerSelector, -} from '../../../../../../../src/plugins/kibana_utils/public'; -import { useTimefilterService } from '../../../hooks/use_timefilter_service'; +} from '../../../../../../../../src/plugins/kibana_utils/public'; +import { useTimefilterService } from '../../../../hooks/use_timefilter_service'; import { useContainer, diff --git a/x-pack/plugins/observability/public/pages/alerts/index.ts b/x-pack/plugins/observability/public/pages/alerts/index.ts new file mode 100644 index 000000000000..525f3441c447 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/index.ts @@ -0,0 +1,9 @@ +/* + * 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 * from './components'; +export * from './containers'; diff --git a/x-pack/plugins/observability/public/pages/cases/helpers.ts b/x-pack/plugins/observability/public/pages/cases/helpers.ts index 91f45c711d6a..f4bc5af7f604 100644 --- a/x-pack/plugins/observability/public/pages/cases/helpers.ts +++ b/x-pack/plugins/observability/public/pages/cases/helpers.ts @@ -6,10 +6,8 @@ */ import { useEffect, useState } from 'react'; import { isEmpty } from 'lodash'; - import { usePluginContext } from '../../hooks/use_plugin_context'; -import { parseAlert } from '../../pages/alerts/parse_alert'; -import { TopAlert } from '../../pages/alerts/'; +import { TopAlert, parseAlert } from '../../pages/alerts/'; import { useKibana } from '../../utils/kibana_react'; import { Ecs } from '../../../../cases/common'; diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 169f4b5254c0..6f38a66cdb64 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -8,8 +8,8 @@ import * as t from 'io-ts'; import React from 'react'; import { casesPath } from '../../common'; -import { AlertsPage } from '../pages/alerts'; import { CasesPage } from '../pages/cases'; +import { AlertsPage } from '../pages/alerts/containers/alerts_page'; import { HomePage } from '../pages/home'; import { LandingPage } from '../pages/landing'; import { OverviewPage } from '../pages/overview'; From 7e9b1bce092ae22ffb6ee081e43267b9da6ba9e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ece=20=C3=96zalp?= Date: Mon, 29 Nov 2021 13:44:39 -0500 Subject: [PATCH 024/224] [Security Solution] updates host risk score decimal count (UI) (#119228) * updates host risk score decimal count * fix function * changes isNaN to Number.isNaN Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Ece Ozalp --- .../overview_risky_host_links/risky_hosts_panel_view.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx index eb4e226940c5..87a5710ab037 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx @@ -30,6 +30,8 @@ const columns: Array> = [ align: 'right', field: 'count', name: 'Risk Score', + render: (riskScore) => + Number.isNaN(riskScore) ? riskScore : Number.parseFloat(riskScore).toFixed(2), sortable: true, truncateText: true, width: '15%', From a0ca3d90bceff111cb48b3cb4973f273b58a1499 Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Mon, 29 Nov 2021 19:51:06 +0100 Subject: [PATCH 025/224] [APM] Add service icon for the originating service in traces table (#119421) * [APM] Add service icon for the originating service in traces table * Fix api test * Fix agentName type --- .../app/trace_overview/trace_list.tsx | 17 +++- .../__snapshots__/queries.test.ts.snap | 6 ++ .../server/lib/transaction_groups/fetcher.ts | 5 +- .../get_transaction_group_stats.ts | 13 ++- .../traces/__snapshots__/top_traces.spec.snap | 81 +++++++++++++++++++ .../tests/traces/top_traces.spec.ts | 2 + 6 files changed, 120 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx index 0fc25b28b60e..58179366fa42 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx @@ -5,7 +5,13 @@ * 2.0. */ -import { EuiIcon, EuiToolTip, RIGHT_ALIGNMENT } from '@elastic/eui'; +import { + EuiIcon, + EuiToolTip, + EuiFlexGroup, + EuiFlexItem, + RIGHT_ALIGNMENT, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; @@ -19,6 +25,7 @@ import { EmptyMessage } from '../../shared/EmptyMessage'; import { ImpactBar } from '../../shared/ImpactBar'; import { TransactionDetailLink } from '../../shared/Links/apm/transaction_detail_link'; import { ITableColumn, ManagedTable } from '../../shared/managed_table'; +import { AgentIcon } from '../../shared/agent_icon'; type TraceGroup = APIReturnType<'GET /internal/apm/traces'>['items'][0]; @@ -65,6 +72,14 @@ const traceListColumns: Array> = [ } ), sortable: true, + render: (_: string, { serviceName, agentName }) => ( + + + + + {serviceName} + + ), }, { field: 'averageResponseTime', diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap index 17c43e36e5cc..00440b2b5185 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap @@ -18,6 +18,9 @@ Array [ Object { "field": "transaction.type", }, + Object { + "field": "agent.name", + }, ], "sort": Object { "@timestamp": "desc", @@ -228,6 +231,9 @@ Array [ Object { "field": "transaction.type", }, + Object { + "field": "agent.name", + }, ], "sort": Object { "@timestamp": "desc", diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index aea92d06b758..bca71ed71b1f 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -31,7 +31,7 @@ import { } from '../helpers/transactions'; import { Setup } from '../helpers/setup_request'; import { getAverages, getCounts, getSums } from './get_transaction_group_stats'; - +import { AgentName } from '../../../typings/es_schemas/ui/fields/agent'; export interface TopTraceOptions { environment: string; kuery: string; @@ -51,6 +51,7 @@ export interface TransactionGroup { averageResponseTime: number | null | undefined; transactionsPerMinute: number; impact: number; + agentName: AgentName; } export type ESResponse = Promise<{ items: TransactionGroup[] }>; @@ -142,6 +143,7 @@ function getItemsWithRelativeImpact( avg?: number | null; count?: number | null; transactionType?: string; + agentName?: AgentName; }>, start: number, end: number @@ -166,6 +168,7 @@ function getItemsWithRelativeImpact( item.sum !== null && item.sum !== undefined ? ((item.sum - min) / (max - min)) * 100 || 0 : 0, + agentName: item.agentName as AgentName, }; }); diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts index c79dde721d13..fd638a6731c6 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts @@ -7,11 +7,14 @@ import { merge } from 'lodash'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; +import { + TRANSACTION_TYPE, + AGENT_NAME, +} from '../../../common/elasticsearch_fieldnames'; import { arrayUnionToCallable } from '../../../common/utils/array_union_to_callable'; import { TransactionGroupRequestBase, TransactionGroupSetup } from './fetcher'; import { getTransactionDurationFieldForTransactions } from '../helpers/transactions'; - +import { AgentName } from '../../../typings/es_schemas/ui/fields/agent'; interface MetricParams { request: TransactionGroupRequestBase; setup: TransactionGroupSetup; @@ -79,6 +82,9 @@ export async function getCounts({ request, setup }: MetricParams) { { field: TRANSACTION_TYPE, } as const, + { + field: AGENT_NAME, + } as const, ], }, }, @@ -98,6 +104,9 @@ export async function getCounts({ request, setup }: MetricParams) { transactionType: bucket.transaction_type.top[0].metrics[ TRANSACTION_TYPE ] as string, + agentName: bucket.transaction_type.top[0].metrics[ + AGENT_NAME + ] as AgentName, }; }); } diff --git a/x-pack/test/apm_api_integration/tests/traces/__snapshots__/top_traces.spec.snap b/x-pack/test/apm_api_integration/tests/traces/__snapshots__/top_traces.spec.snap index 604348355f38..528963709712 100644 --- a/x-pack/test/apm_api_integration/tests/traces/__snapshots__/top_traces.spec.snap +++ b/x-pack/test/apm_api_integration/tests/traces/__snapshots__/top_traces.spec.snap @@ -3,6 +3,7 @@ exports[`APM API tests basic apm_8.0.0 Top traces when data is loaded returns the correct buckets 1`] = ` Array [ Object { + "agentName": "java", "averageResponseTime": 1639, "impact": 0, "key": Object { @@ -15,6 +16,7 @@ Array [ "transactionsPerMinute": 0.0333333333333333, }, Object { + "agentName": "nodejs", "averageResponseTime": 3279, "impact": 0.00144735571024101, "key": Object { @@ -27,6 +29,7 @@ Array [ "transactionsPerMinute": 0.0333333333333333, }, Object { + "agentName": "nodejs", "averageResponseTime": 6175, "impact": 0.00400317408637392, "key": Object { @@ -39,6 +42,7 @@ Array [ "transactionsPerMinute": 0.0333333333333333, }, Object { + "agentName": "dotnet", "averageResponseTime": 3495, "impact": 0.00472243927164613, "key": Object { @@ -51,6 +55,7 @@ Array [ "transactionsPerMinute": 0.0666666666666667, }, Object { + "agentName": "python", "averageResponseTime": 7039, "impact": 0.00476568343615943, "key": Object { @@ -63,6 +68,7 @@ Array [ "transactionsPerMinute": 0.0333333333333333, }, Object { + "agentName": "ruby", "averageResponseTime": 6303, "impact": 0.00967875004525193, "key": Object { @@ -75,6 +81,7 @@ Array [ "transactionsPerMinute": 0.0666666666666667, }, Object { + "agentName": "java", "averageResponseTime": 7209.66666666667, "impact": 0.0176418540534865, "key": Object { @@ -87,6 +94,7 @@ Array [ "transactionsPerMinute": 0.1, }, Object { + "agentName": "java", "averageResponseTime": 4511, "impact": 0.0224401912465233, "key": Object { @@ -99,6 +107,7 @@ Array [ "transactionsPerMinute": 0.2, }, Object { + "agentName": "python", "averageResponseTime": 7607, "impact": 0.0254072704525173, "key": Object { @@ -111,6 +120,7 @@ Array [ "transactionsPerMinute": 0.133333333333333, }, Object { + "agentName": "nodejs", "averageResponseTime": 10143, "impact": 0.025408152986487, "key": Object { @@ -123,6 +133,7 @@ Array [ "transactionsPerMinute": 0.1, }, Object { + "agentName": "ruby", "averageResponseTime": 6105.66666666667, "impact": 0.0308842762682221, "key": Object { @@ -135,6 +146,7 @@ Array [ "transactionsPerMinute": 0.2, }, Object { + "agentName": "java", "averageResponseTime": 6116.33333333333, "impact": 0.0309407584422802, "key": Object { @@ -147,6 +159,7 @@ Array [ "transactionsPerMinute": 0.2, }, Object { + "agentName": "java", "averageResponseTime": 12543, "impact": 0.0317623975680329, "key": Object { @@ -159,6 +172,7 @@ Array [ "transactionsPerMinute": 0.1, }, Object { + "agentName": "nodejs", "averageResponseTime": 5551, "impact": 0.0328461492827744, "key": Object { @@ -171,6 +185,7 @@ Array [ "transactionsPerMinute": 0.233333333333333, }, Object { + "agentName": "java", "averageResponseTime": 13183, "impact": 0.0334568627897785, "key": Object { @@ -183,6 +198,7 @@ Array [ "transactionsPerMinute": 0.1, }, Object { + "agentName": "go", "averageResponseTime": 8050.2, "impact": 0.0340764016364792, "key": Object { @@ -195,6 +211,7 @@ Array [ "transactionsPerMinute": 0.166666666666667, }, Object { + "agentName": "ruby", "averageResponseTime": 10079, "impact": 0.0341337663445071, "key": Object { @@ -207,6 +224,7 @@ Array [ "transactionsPerMinute": 0.133333333333333, }, Object { + "agentName": "nodejs", "averageResponseTime": 8463, "impact": 0.0358979517498557, "key": Object { @@ -219,6 +237,7 @@ Array [ "transactionsPerMinute": 0.166666666666667, }, Object { + "agentName": "ruby", "averageResponseTime": 10799, "impact": 0.0366754641771254, "key": Object { @@ -231,6 +250,7 @@ Array [ "transactionsPerMinute": 0.133333333333333, }, Object { + "agentName": "ruby", "averageResponseTime": 7428.33333333333, "impact": 0.0378880658514371, "key": Object { @@ -243,6 +263,7 @@ Array [ "transactionsPerMinute": 0.2, }, Object { + "agentName": "java", "averageResponseTime": 3105.13333333333, "impact": 0.039659311528543, "key": Object { @@ -255,6 +276,7 @@ Array [ "transactionsPerMinute": 0.5, }, Object { + "agentName": "java", "averageResponseTime": 6883.57142857143, "impact": 0.0410784261517549, "key": Object { @@ -267,6 +289,7 @@ Array [ "transactionsPerMinute": 0.233333333333333, }, Object { + "agentName": "dotnet", "averageResponseTime": 3505, "impact": 0.0480460318422139, "key": Object { @@ -279,6 +302,7 @@ Array [ "transactionsPerMinute": 0.533333333333333, }, Object { + "agentName": "java", "averageResponseTime": 5621.4, "impact": 0.0481642913941483, "key": Object { @@ -291,6 +315,7 @@ Array [ "transactionsPerMinute": 0.333333333333333, }, Object { + "agentName": "nodejs", "averageResponseTime": 8428.71428571429, "impact": 0.0506239135675883, "key": Object { @@ -303,6 +328,7 @@ Array [ "transactionsPerMinute": 0.233333333333333, }, Object { + "agentName": "nodejs", "averageResponseTime": 8520.14285714286, "impact": 0.0511887353081702, "key": Object { @@ -315,6 +341,7 @@ Array [ "transactionsPerMinute": 0.233333333333333, }, Object { + "agentName": "nodejs", "averageResponseTime": 6683.44444444444, "impact": 0.0516388276326964, "key": Object { @@ -327,6 +354,7 @@ Array [ "transactionsPerMinute": 0.3, }, Object { + "agentName": "dotnet", "averageResponseTime": 3482.78947368421, "impact": 0.0569534471979838, "key": Object { @@ -339,6 +367,7 @@ Array [ "transactionsPerMinute": 0.633333333333333, }, Object { + "agentName": "python", "averageResponseTime": 16703, "impact": 0.057517386404596, "key": Object { @@ -351,6 +380,7 @@ Array [ "transactionsPerMinute": 0.133333333333333, }, Object { + "agentName": "dotnet", "averageResponseTime": 4943, "impact": 0.0596266425920813, "key": Object { @@ -363,6 +393,7 @@ Array [ "transactionsPerMinute": 0.466666666666667, }, Object { + "agentName": "nodejs", "averageResponseTime": 7892.33333333333, "impact": 0.0612407972225879, "key": Object { @@ -375,6 +406,7 @@ Array [ "transactionsPerMinute": 0.3, }, Object { + "agentName": "dotnet", "averageResponseTime": 6346.42857142857, "impact": 0.0769666700279444, "key": Object { @@ -387,6 +419,7 @@ Array [ "transactionsPerMinute": 0.466666666666667, }, Object { + "agentName": "go", "averageResponseTime": 7052.84615384615, "impact": 0.0794704188998674, "key": Object { @@ -399,6 +432,7 @@ Array [ "transactionsPerMinute": 0.433333333333333, }, Object { + "agentName": "java", "averageResponseTime": 10484.3333333333, "impact": 0.0818285496667966, "key": Object { @@ -411,6 +445,7 @@ Array [ "transactionsPerMinute": 0.3, }, Object { + "agentName": "nodejs", "averageResponseTime": 23711, "impact": 0.0822565786420813, "key": Object { @@ -423,6 +458,7 @@ Array [ "transactionsPerMinute": 0.133333333333333, }, Object { + "agentName": "dotnet", "averageResponseTime": 4491.36363636364, "impact": 0.0857567083657495, "key": Object { @@ -435,6 +471,7 @@ Array [ "transactionsPerMinute": 0.733333333333333, }, Object { + "agentName": "python", "averageResponseTime": 20715.8, "impact": 0.089965512867054, "key": Object { @@ -447,6 +484,7 @@ Array [ "transactionsPerMinute": 0.166666666666667, }, Object { + "agentName": "nodejs", "averageResponseTime": 9036.33333333333, "impact": 0.0942519803576885, "key": Object { @@ -459,6 +497,7 @@ Array [ "transactionsPerMinute": 0.4, }, Object { + "agentName": "java", "averageResponseTime": 7504.06666666667, "impact": 0.0978924329825326, "key": Object { @@ -471,6 +510,7 @@ Array [ "transactionsPerMinute": 0.5, }, Object { + "agentName": "go", "averageResponseTime": 4250.55555555556, "impact": 0.0998375378516613, "key": Object { @@ -483,6 +523,7 @@ Array [ "transactionsPerMinute": 0.9, }, Object { + "agentName": "nodejs", "averageResponseTime": 21343, "impact": 0.11156906191034, "key": Object { @@ -495,6 +536,7 @@ Array [ "transactionsPerMinute": 0.2, }, Object { + "agentName": "ruby", "averageResponseTime": 16655, "impact": 0.116142352941114, "key": Object { @@ -507,6 +549,7 @@ Array [ "transactionsPerMinute": 0.266666666666667, }, Object { + "agentName": "go", "averageResponseTime": 5749, "impact": 0.12032203382142, "key": Object { @@ -519,6 +562,7 @@ Array [ "transactionsPerMinute": 0.8, }, Object { + "agentName": "ruby", "averageResponseTime": 9951, "impact": 0.121502864272824, "key": Object { @@ -531,6 +575,7 @@ Array [ "transactionsPerMinute": 0.466666666666667, }, Object { + "agentName": "go", "averageResponseTime": 14040.6, "impact": 0.122466591367692, "key": Object { @@ -543,6 +588,7 @@ Array [ "transactionsPerMinute": 0.333333333333333, }, Object { + "agentName": "ruby", "averageResponseTime": 20963.5714285714, "impact": 0.128060974201361, "key": Object { @@ -555,6 +601,7 @@ Array [ "transactionsPerMinute": 0.233333333333333, }, Object { + "agentName": "python", "averageResponseTime": 22874.4285714286, "impact": 0.139865748579522, "key": Object { @@ -567,6 +614,7 @@ Array [ "transactionsPerMinute": 0.233333333333333, }, Object { + "agentName": "python", "averageResponseTime": 32203.8, "impact": 0.140658264084276, "key": Object { @@ -579,6 +627,7 @@ Array [ "transactionsPerMinute": 0.166666666666667, }, Object { + "agentName": "go", "averageResponseTime": 4482.11111111111, "impact": 0.140955678032051, "key": Object { @@ -591,6 +640,7 @@ Array [ "transactionsPerMinute": 1.2, }, Object { + "agentName": "ruby", "averageResponseTime": 12582.3846153846, "impact": 0.142910490774846, "key": Object { @@ -603,6 +653,7 @@ Array [ "transactionsPerMinute": 0.433333333333333, }, Object { + "agentName": "ruby", "averageResponseTime": 10009.9473684211, "impact": 0.166401779979233, "key": Object { @@ -615,6 +666,7 @@ Array [ "transactionsPerMinute": 0.633333333333333, }, Object { + "agentName": "python", "averageResponseTime": 27825.2857142857, "impact": 0.170450845832029, "key": Object { @@ -627,6 +679,7 @@ Array [ "transactionsPerMinute": 0.233333333333333, }, Object { + "agentName": "python", "averageResponseTime": 20562.2, "impact": 0.180021926732983, "key": Object { @@ -639,6 +692,7 @@ Array [ "transactionsPerMinute": 0.333333333333333, }, Object { + "agentName": "dotnet", "averageResponseTime": 7106.76470588235, "impact": 0.21180020991247, "key": Object { @@ -651,6 +705,7 @@ Array [ "transactionsPerMinute": 1.13333333333333, }, Object { + "agentName": "go", "averageResponseTime": 8612.51724137931, "impact": 0.218977858687708, "key": Object { @@ -663,6 +718,7 @@ Array [ "transactionsPerMinute": 0.966666666666667, }, Object { + "agentName": "ruby", "averageResponseTime": 11295, "impact": 0.277663720068132, "key": Object { @@ -675,6 +731,7 @@ Array [ "transactionsPerMinute": 0.933333333333333, }, Object { + "agentName": "python", "averageResponseTime": 65035.8, "impact": 0.285535040543522, "key": Object { @@ -687,6 +744,7 @@ Array [ "transactionsPerMinute": 0.166666666666667, }, Object { + "agentName": "go", "averageResponseTime": 30999.4705882353, "impact": 0.463640986028375, "key": Object { @@ -699,6 +757,7 @@ Array [ "transactionsPerMinute": 0.566666666666667, }, Object { + "agentName": "go", "averageResponseTime": 20197.4, "impact": 0.622424732781511, "key": Object { @@ -711,6 +770,7 @@ Array [ "transactionsPerMinute": 1.16666666666667, }, Object { + "agentName": "python", "averageResponseTime": 64681.6666666667, "impact": 0.68355874339377, "key": Object { @@ -723,6 +783,7 @@ Array [ "transactionsPerMinute": 0.4, }, Object { + "agentName": "dotnet", "averageResponseTime": 41416.1428571429, "impact": 0.766127739061111, "key": Object { @@ -735,6 +796,7 @@ Array [ "transactionsPerMinute": 0.7, }, Object { + "agentName": "go", "averageResponseTime": 19429, "impact": 0.821597646656097, "key": Object { @@ -747,6 +809,7 @@ Array [ "transactionsPerMinute": 1.6, }, Object { + "agentName": "dotnet", "averageResponseTime": 62390.652173913, "impact": 1.26497653527507, "key": Object { @@ -759,6 +822,7 @@ Array [ "transactionsPerMinute": 0.766666666666667, }, Object { + "agentName": "python", "averageResponseTime": 33266.2, "impact": 1.76006661931225, "key": Object { @@ -771,6 +835,7 @@ Array [ "transactionsPerMinute": 2, }, Object { + "agentName": "nodejs", "averageResponseTime": 38491.4444444444, "impact": 1.83293391905112, "key": Object { @@ -783,6 +848,7 @@ Array [ "transactionsPerMinute": 1.8, }, Object { + "agentName": "dotnet", "averageResponseTime": 118488.6, "impact": 2.08995781717084, "key": Object { @@ -795,6 +861,7 @@ Array [ "transactionsPerMinute": 0.666666666666667, }, Object { + "agentName": "dotnet", "averageResponseTime": 250440.142857143, "impact": 4.64001412901584, "key": Object { @@ -807,6 +874,7 @@ Array [ "transactionsPerMinute": 0.7, }, Object { + "agentName": "java", "averageResponseTime": 312096.523809524, "impact": 5.782704992387, "key": Object { @@ -819,6 +887,7 @@ Array [ "transactionsPerMinute": 0.7, }, Object { + "agentName": "ruby", "averageResponseTime": 91519.7032967033, "impact": 7.34855500859826, "key": Object { @@ -831,6 +900,7 @@ Array [ "transactionsPerMinute": 3.03333333333333, }, Object { + "agentName": "rum-js", "averageResponseTime": 648269.769230769, "impact": 7.43611473386403, "key": Object { @@ -843,6 +913,7 @@ Array [ "transactionsPerMinute": 0.433333333333333, }, Object { + "agentName": "python", "averageResponseTime": 1398919.72727273, "impact": 13.5790895084132, "key": Object { @@ -855,6 +926,7 @@ Array [ "transactionsPerMinute": 0.366666666666667, }, Object { + "agentName": "rum-js", "averageResponseTime": 1199907.57142857, "impact": 14.8239822181408, "key": Object { @@ -867,6 +939,7 @@ Array [ "transactionsPerMinute": 0.466666666666667, }, Object { + "agentName": "rum-js", "averageResponseTime": 955876.052631579, "impact": 16.026822184214, "key": Object { @@ -879,6 +952,7 @@ Array [ "transactionsPerMinute": 0.633333333333333, }, Object { + "agentName": "go", "averageResponseTime": 965009.526315789, "impact": 16.1799735991728, "key": Object { @@ -891,6 +965,7 @@ Array [ "transactionsPerMinute": 0.633333333333333, }, Object { + "agentName": "rum-js", "averageResponseTime": 1213675.30769231, "impact": 27.8474053933734, "key": Object { @@ -903,6 +978,7 @@ Array [ "transactionsPerMinute": 0.866666666666667, }, Object { + "agentName": "nodejs", "averageResponseTime": 924019.363636364, "impact": 35.8796065162284, "key": Object { @@ -915,6 +991,7 @@ Array [ "transactionsPerMinute": 1.46666666666667, }, Object { + "agentName": "nodejs", "averageResponseTime": 1060469.15384615, "impact": 36.498655556576, "key": Object { @@ -927,6 +1004,7 @@ Array [ "transactionsPerMinute": 1.3, }, Object { + "agentName": "python", "averageResponseTime": 118686.822222222, "impact": 37.7068083771466, "key": Object { @@ -939,6 +1017,7 @@ Array [ "transactionsPerMinute": 12, }, Object { + "agentName": "nodejs", "averageResponseTime": 1039228.27659574, "impact": 43.1048035741496, "key": Object { @@ -951,6 +1030,7 @@ Array [ "transactionsPerMinute": 1.56666666666667, }, Object { + "agentName": "python", "averageResponseTime": 1949922.55555556, "impact": 61.9499776921889, "key": Object { @@ -963,6 +1043,7 @@ Array [ "transactionsPerMinute": 1.2, }, Object { + "agentName": "dotnet", "averageResponseTime": 5963775, "impact": 100, "key": Object { diff --git a/x-pack/test/apm_api_integration/tests/traces/top_traces.spec.ts b/x-pack/test/apm_api_integration/tests/traces/top_traces.spec.ts index 51b14809982d..06a24cbd34a4 100644 --- a/x-pack/test/apm_api_integration/tests/traces/top_traces.spec.ts +++ b/x-pack/test/apm_api_integration/tests/traces/top_traces.spec.ts @@ -63,6 +63,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(firstItem).toMatchInline(` Object { + "agentName": "java", "averageResponseTime": 1639, "impact": 0, "key": Object { @@ -78,6 +79,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(lastItem).toMatchInline(` Object { + "agentName": "dotnet", "averageResponseTime": 5963775, "impact": 100, "key": Object { From 9f47e386c98056a3eb07cb44b78b5822f3f1d2e0 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 29 Nov 2021 14:13:06 -0500 Subject: [PATCH 026/224] [Fleet] Fix preconfiguration variable values (#119749) --- .../server/services/package_policy.test.ts | 786 +++++++++++++++++- .../fleet/server/services/package_policy.ts | 157 +++- .../fleet/server/services/preconfiguration.ts | 4 +- 3 files changed, 909 insertions(+), 38 deletions(-) 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 36976bea4a97..ac88204f082b 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -39,7 +39,8 @@ import type { import { IngestManagerError } from '../errors'; import { - overridePackageInputs, + preconfigurePackageInputs, + updatePackageInputs, packagePolicyService, _applyIndexPrivileges, } from './package_policy'; @@ -1170,7 +1171,776 @@ describe('Package policy service', () => { }); }); - describe('overridePackageInputs', () => { + describe('preconfigurePackageInputs', () => { + describe('when variable is already defined', () => { + it('override original variable value', () => { + const basePackagePolicy: NewPackagePolicy = { + name: 'base-package-policy', + description: 'Base Package Policy', + namespace: 'default', + enabled: true, + policy_id: 'xxxx', + output_id: 'xxxx', + package: { + name: 'test-package', + title: 'Test Package', + version: '0.0.1', + }, + inputs: [ + { + type: 'logs', + policy_template: 'template_1', + enabled: true, + vars: { + path: { + type: 'text', + value: ['/var/log/logfile.log'], + }, + }, + streams: [], + }, + ], + }; + + const packageInfo: PackageInfo = { + name: 'test-package', + description: 'Test Package', + title: 'Test Package', + version: '0.0.1', + latestVersion: '0.0.1', + release: 'experimental', + format_version: '1.0.0', + owner: { github: 'elastic/fleet' }, + policy_templates: [ + { + name: 'template_1', + title: 'Template 1', + description: 'Template 1', + inputs: [ + { + type: 'logs', + title: 'Log', + description: 'Log Input', + vars: [ + { + name: 'path', + type: 'text', + }, + ], + }, + ], + }, + ], + // @ts-ignore + assets: {}, + }; + + const inputsOverride: NewPackagePolicyInput[] = [ + { + type: 'logs', + enabled: true, + streams: [], + vars: { + path: { + type: 'text', + value: '/var/log/new-logfile.log', + }, + }, + }, + ]; + + const result = preconfigurePackageInputs( + basePackagePolicy, + packageInfo, + // TODO: Update this type assertion when the `InputsOverride` type is updated such + // that it no longer causes unresolvable type errors when used directly + inputsOverride as InputsOverride[] + ); + expect(result.inputs[0]?.vars?.path.value).toEqual('/var/log/new-logfile.log'); + }); + }); + + describe('when variable is undefined in original object', () => { + it('adds the variable definition to the resulting object', () => { + const basePackagePolicy: NewPackagePolicy = { + name: 'base-package-policy', + description: 'Base Package Policy', + namespace: 'default', + enabled: true, + policy_id: 'xxxx', + output_id: 'xxxx', + package: { + name: 'test-package', + title: 'Test Package', + version: '0.0.1', + }, + inputs: [ + { + type: 'logs', + policy_template: 'template_1', + enabled: true, + vars: { + path: { + type: 'text', + value: ['/var/log/logfile.log'], + }, + }, + streams: [], + }, + ], + }; + + const packageInfo: PackageInfo = { + name: 'test-package', + description: 'Test Package', + title: 'Test Package', + version: '0.0.1', + latestVersion: '0.0.1', + release: 'experimental', + format_version: '1.0.0', + owner: { github: 'elastic/fleet' }, + policy_templates: [ + { + name: 'template_1', + title: 'Template 1', + description: 'Template 1', + inputs: [ + { + type: 'logs', + title: 'Log', + description: 'Log Input', + vars: [ + { + name: 'path', + type: 'text', + }, + { + name: 'path_2', + type: 'text', + }, + ], + }, + ], + }, + ], + // @ts-ignore + assets: {}, + }; + + const inputsOverride: NewPackagePolicyInput[] = [ + { + type: 'logs', + enabled: true, + streams: [], + policy_template: 'template_1', + vars: { + path: { + type: 'text', + value: '/var/log/new-logfile.log', + }, + path_2: { + type: 'text', + value: '/var/log/custom.log', + }, + }, + }, + ]; + + const result = preconfigurePackageInputs( + basePackagePolicy, + packageInfo, + // TODO: Update this type assertion when the `InputsOverride` type is updated such + // that it no longer causes unresolvable type errors when used directly + inputsOverride as InputsOverride[] + ); + + expect(result.inputs[0]?.vars?.path_2.value).toEqual('/var/log/custom.log'); + }); + }); + + describe('when variable is undefined in original object and policy_template is undefined', () => { + it('adds the variable definition to the resulting object', () => { + const basePackagePolicy: NewPackagePolicy = { + name: 'base-package-policy', + description: 'Base Package Policy', + namespace: 'default', + enabled: true, + policy_id: 'xxxx', + output_id: 'xxxx', + package: { + name: 'test-package', + title: 'Test Package', + version: '0.0.1', + }, + inputs: [ + { + type: 'logs', + policy_template: 'template_1', + enabled: true, + vars: { + path: { + type: 'text', + value: ['/var/log/logfile.log'], + }, + }, + streams: [], + }, + ], + }; + + const packageInfo: PackageInfo = { + name: 'test-package', + description: 'Test Package', + title: 'Test Package', + version: '0.0.1', + latestVersion: '0.0.1', + release: 'experimental', + format_version: '1.0.0', + owner: { github: 'elastic/fleet' }, + policy_templates: [ + { + name: 'template_1', + title: 'Template 1', + description: 'Template 1', + inputs: [ + { + type: 'logs', + title: 'Log', + description: 'Log Input', + vars: [ + { + name: 'path', + type: 'text', + }, + { + name: 'path_2', + type: 'text', + }, + ], + }, + ], + }, + ], + // @ts-ignore + assets: {}, + }; + + const inputsOverride: NewPackagePolicyInput[] = [ + { + type: 'logs', + enabled: true, + streams: [], + policy_template: undefined, // preconfigured input overrides don't have a policy_template + vars: { + path: { + type: 'text', + value: '/var/log/new-logfile.log', + }, + path_2: { + type: 'text', + value: '/var/log/custom.log', + }, + }, + }, + ]; + + const result = preconfigurePackageInputs( + basePackagePolicy, + packageInfo, + // TODO: Update this type assertion when the `InputsOverride` type is updated such + // that it no longer causes unresolvable type errors when used directly + inputsOverride as InputsOverride[] + ); + + expect(result.inputs[0]?.vars?.path_2.value).toEqual('/var/log/custom.log'); + }); + }); + + describe('when an input of the same type exists under multiple policy templates', () => { + it('adds variable definitions to the proper streams', () => { + const basePackagePolicy: NewPackagePolicy = { + name: 'base-package-policy', + description: 'Base Package Policy', + namespace: 'default', + enabled: true, + policy_id: 'xxxx', + output_id: 'xxxx', + package: { + name: 'test-package', + title: 'Test Package', + version: '0.0.1', + }, + inputs: [ + { + type: 'logs', + policy_template: 'template_1', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { + dataset: 'test.logs', + type: 'logfile', + }, + vars: { + log_file_path: { + type: 'text', + }, + }, + }, + ], + }, + { + type: 'logs', + policy_template: 'template_2', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { + dataset: 'test.logs', + type: 'logfile', + }, + vars: { + log_file_path: { + type: 'text', + }, + }, + }, + ], + }, + ], + }; + + const packageInfo: PackageInfo = { + name: 'test-package', + description: 'Test Package', + title: 'Test Package', + version: '0.0.1', + latestVersion: '0.0.1', + release: 'experimental', + format_version: '1.0.0', + owner: { github: 'elastic/fleet' }, + policy_templates: [ + { + name: 'template_1', + title: 'Template 1', + description: 'Template 1', + inputs: [ + { + type: 'logs', + title: 'Log', + description: 'Log Input', + vars: [], + }, + ], + }, + { + name: 'template_2', + title: 'Template 2', + description: 'Template 2', + inputs: [ + { + type: 'logs', + title: 'Log', + description: 'Log Input', + vars: [], + }, + ], + }, + ], + // @ts-ignore + assets: {}, + }; + + const inputsOverride: NewPackagePolicyInput[] = [ + { + type: 'logs', + enabled: true, + policy_template: 'template_1', + streams: [ + { + enabled: true, + data_stream: { + dataset: 'test.logs', + type: 'logfile', + }, + vars: { + log_file_path: { + type: 'text', + value: '/var/log/template1-logfile.log', + }, + }, + }, + ], + }, + { + type: 'logs', + enabled: true, + policy_template: 'template_2', + streams: [ + { + enabled: true, + data_stream: { + dataset: 'test.logs', + type: 'logfile', + }, + vars: { + log_file_path: { + type: 'text', + value: '/var/log/template2-logfile.log', + }, + }, + }, + ], + }, + ]; + + const result = preconfigurePackageInputs( + basePackagePolicy, + packageInfo, + // TODO: Update this type assertion when the `InputsOverride` type is updated such + // that it no longer causes unresolvable type errors when used directly + inputsOverride as InputsOverride[] + ); + + expect(result.inputs).toHaveLength(2); + + const template1Input = result.inputs.find( + (input) => input.policy_template === 'template_1' + ); + const template2Input = result.inputs.find( + (input) => input.policy_template === 'template_2' + ); + + expect(template1Input).toBeDefined(); + expect(template2Input).toBeDefined(); + + expect(template1Input?.streams[0].vars?.log_file_path.value).toBe( + '/var/log/template1-logfile.log' + ); + + expect(template2Input?.streams[0].vars?.log_file_path.value).toBe( + '/var/log/template2-logfile.log' + ); + }); + }); + + describe('when an input or stream is disabled on the original policy object', () => { + it('remains disabled on the resulting policy object', () => { + const basePackagePolicy: NewPackagePolicy = { + name: 'base-package-policy', + description: 'Base Package Policy', + namespace: 'default', + enabled: true, + policy_id: 'xxxx', + output_id: 'xxxx', + package: { + name: 'test-package', + title: 'Test Package', + version: '0.0.1', + }, + inputs: [ + { + type: 'logs', + policy_template: 'template_1', + enabled: false, + streams: [ + { + enabled: false, + data_stream: { + dataset: 'test.logs', + type: 'logfile', + }, + vars: { + log_file_path: { + type: 'text', + }, + }, + }, + { + enabled: true, + data_stream: { + dataset: 'test.logs', + type: 'logfile2', + }, + vars: { + log_file_path_2: { + type: 'text', + }, + }, + }, + ], + }, + { + type: 'logs_2', + policy_template: 'template_1', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { + dataset: 'test.logs', + type: 'logfile', + }, + vars: { + log_file_path: { + type: 'text', + }, + }, + }, + ], + }, + { + type: 'logs', + policy_template: 'template_2', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { + dataset: 'test.logs', + type: 'logfile', + }, + vars: { + log_file_path: { + type: 'text', + }, + }, + }, + ], + }, + ], + }; + + const packageInfo: PackageInfo = { + name: 'test-package', + description: 'Test Package', + title: 'Test Package', + version: '0.0.1', + latestVersion: '0.0.1', + release: 'experimental', + format_version: '1.0.0', + owner: { github: 'elastic/fleet' }, + policy_templates: [ + { + name: 'template_1', + title: 'Template 1', + description: 'Template 1', + inputs: [ + { + type: 'logs', + title: 'Log', + description: 'Log Input', + vars: [], + }, + { + type: 'logs_2', + title: 'Log 2', + description: 'Log Input 2', + vars: [], + }, + ], + }, + { + name: 'template_2', + title: 'Template 2', + description: 'Template 2', + inputs: [ + { + type: 'logs', + title: 'Log', + description: 'Log Input', + vars: [], + }, + ], + }, + ], + // @ts-ignore + assets: {}, + }; + + const inputsOverride: NewPackagePolicyInput[] = [ + { + type: 'logs', + enabled: true, + policy_template: 'template_1', + streams: [ + { + enabled: true, + data_stream: { + dataset: 'test.logs', + type: 'logfile', + }, + vars: { + log_file_path: { + type: 'text', + value: '/var/log/template1-logfile.log', + }, + }, + }, + { + enabled: true, + data_stream: { + dataset: 'test.logs', + type: 'logfile2', + }, + vars: { + log_file_path_2: { + type: 'text', + value: '/var/log/template1-logfile2.log', + }, + }, + }, + ], + }, + { + type: 'logs', + enabled: true, + policy_template: 'template_2', + streams: [ + { + enabled: true, + data_stream: { + dataset: 'test.logs', + type: 'logfile', + }, + vars: { + log_file_path: { + type: 'text', + value: '/var/log/template2-logfile.log', + }, + }, + }, + ], + }, + ]; + + const result = preconfigurePackageInputs( + basePackagePolicy, + packageInfo, + // TODO: Update this type assertion when the `InputsOverride` type is updated such + // that it no longer causes unresolvable type errors when used directly + inputsOverride as InputsOverride[] + ); + + const template1Inputs = result.inputs.filter( + (input) => input.policy_template === 'template_1' + ); + + const template2Inputs = result.inputs.filter( + (input) => input.policy_template === 'template_2' + ); + + expect(template1Inputs).toHaveLength(2); + expect(template2Inputs).toHaveLength(1); + + const logsInput = template1Inputs?.find((input) => input.type === 'logs'); + expect(logsInput?.enabled).toBe(false); + + const logfileStream = logsInput?.streams.find( + (stream) => stream.data_stream.type === 'logfile' + ); + + expect(logfileStream?.enabled).toBe(false); + }); + }); + + describe('when a datastream is deleted from an input', () => { + it('it remove the non existing datastream', () => { + const basePackagePolicy: NewPackagePolicy = { + name: 'base-package-policy', + description: 'Base Package Policy', + namespace: 'default', + enabled: true, + policy_id: 'xxxx', + output_id: 'xxxx', + package: { + name: 'test-package', + title: 'Test Package', + version: '0.0.1', + }, + inputs: [ + { + type: 'logs', + policy_template: 'template_1', + enabled: true, + vars: { + path: { + type: 'text', + value: ['/var/log/logfile.log'], + }, + }, + streams: [ + { + enabled: true, + data_stream: { dataset: 'dataset.test123', type: 'log' }, + }, + ], + }, + ], + }; + + const packageInfo: PackageInfo = { + name: 'test-package', + description: 'Test Package', + title: 'Test Package', + version: '0.0.1', + latestVersion: '0.0.1', + release: 'experimental', + format_version: '1.0.0', + owner: { github: 'elastic/fleet' }, + policy_templates: [ + { + name: 'template_1', + title: 'Template 1', + description: 'Template 1', + inputs: [ + { + type: 'logs', + title: 'Log', + description: 'Log Input', + vars: [ + { + name: 'path', + type: 'text', + }, + ], + }, + ], + }, + ], + // @ts-ignore + assets: {}, + }; + + const inputsOverride: NewPackagePolicyInput[] = [ + { + type: 'logs', + enabled: true, + streams: [], + vars: { + path: { + type: 'text', + value: '/var/log/new-logfile.log', + }, + }, + }, + ]; + + const result = preconfigurePackageInputs( + basePackagePolicy, + packageInfo, + // TODO: Update this type assertion when the `InputsOverride` type is updated such + // that it no longer causes unresolvable type errors when used directly + inputsOverride as InputsOverride[] + ); + expect(result.inputs[0]?.vars?.path.value).toEqual('/var/log/new-logfile.log'); + }); + }); + }); + + describe('updatePackageInputs', () => { describe('when variable is already defined', () => { it('preserves original variable value without overwriting', () => { const basePackagePolicy: NewPackagePolicy = { @@ -1248,7 +2018,7 @@ describe('Package policy service', () => { }, ]; - const result = overridePackageInputs( + const result = updatePackageInputs( basePackagePolicy, packageInfo, // TODO: Update this type assertion when the `InputsOverride` type is updated such @@ -1346,7 +2116,7 @@ describe('Package policy service', () => { }, ]; - const result = overridePackageInputs( + const result = updatePackageInputs( basePackagePolicy, packageInfo, // TODO: Update this type assertion when the `InputsOverride` type is updated such @@ -1445,7 +2215,7 @@ describe('Package policy service', () => { }, ]; - const result = overridePackageInputs( + const result = updatePackageInputs( basePackagePolicy, packageInfo, // TODO: Update this type assertion when the `InputsOverride` type is updated such @@ -1598,7 +2368,7 @@ describe('Package policy service', () => { }, ]; - const result = overridePackageInputs( + const result = updatePackageInputs( basePackagePolicy, packageInfo, // TODO: Update this type assertion when the `InputsOverride` type is updated such @@ -1819,7 +2589,7 @@ describe('Package policy service', () => { }, ]; - const result = overridePackageInputs( + const result = updatePackageInputs( basePackagePolicy, packageInfo, // TODO: Update this type assertion when the `InputsOverride` type is updated such @@ -1932,7 +2702,7 @@ describe('Package policy service', () => { }, ]; - const result = overridePackageInputs( + const result = updatePackageInputs( basePackagePolicy, packageInfo, // TODO: Update this type assertion when the `InputsOverride` type is updated such diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 535d93cc3ece..5ac348ad7c8a 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -590,7 +590,7 @@ class PackagePolicyService { try { const { packagePolicy, packageInfo } = await this.getUpgradePackagePolicyInfo(soClient, id); - const updatePackagePolicy = overridePackageInputs( + const updatePackagePolicy = updatePackageInputs( { ...omit(packagePolicy, 'id'), inputs: packagePolicy.inputs, @@ -648,7 +648,7 @@ class PackagePolicyService { packageVersion ); - const updatedPackagePolicy = overridePackageInputs( + const updatedPackagePolicy = updatePackageInputs( { ...omit(packagePolicy, 'id'), inputs: packagePolicy.inputs, @@ -1030,13 +1030,13 @@ export const packagePolicyService = new PackagePolicyService(); export type { PackagePolicyService }; -export function overridePackageInputs( +export function updatePackageInputs( basePackagePolicy: NewPackagePolicy, packageInfo: PackageInfo, - inputsOverride?: InputsOverride[], + inputsUpdated?: InputsOverride[], dryRun?: boolean ): DryRunPackagePolicy { - if (!inputsOverride) return basePackagePolicy; + if (!inputsUpdated) return basePackagePolicy; const availablePolicyTemplates = packageInfo.policy_templates ?? []; @@ -1065,42 +1065,40 @@ export function overridePackageInputs( }), ]; - for (const override of inputsOverride) { - // Preconfiguration does not currently support multiple policy templates, so overrides will have an undefined - // policy template, so we only match on `type` in that case. - let originalInput = override.policy_template - ? inputs.find( - (i) => i.type === override.type && i.policy_template === override.policy_template - ) - : inputs.find((i) => i.type === override.type); + for (const update of inputsUpdated) { + // If update have an undefined policy template + // we only match on `type` . + let originalInput = update.policy_template + ? inputs.find((i) => i.type === update.type && i.policy_template === update.policy_template) + : inputs.find((i) => i.type === update.type); // If there's no corresponding input on the original package policy, just // take the override value from the new package as-is. This case typically // occurs when inputs or package policy templates are added/removed between versions. if (originalInput === undefined) { - inputs.push(override as NewPackagePolicyInput); + inputs.push(update as NewPackagePolicyInput); continue; } // For flags like this, we only want to override the original value if it was set // as `undefined` in the original object. An explicit true/false value should be // persisted from the original object to the result after the override process is complete. - if (originalInput.enabled === undefined && override.enabled !== undefined) { - originalInput.enabled = override.enabled; + if (originalInput.enabled === undefined && update.enabled !== undefined) { + originalInput.enabled = update.enabled; } - if (originalInput.keep_enabled === undefined && override.keep_enabled !== undefined) { - originalInput.keep_enabled = override.keep_enabled; + if (originalInput.keep_enabled === undefined && update.keep_enabled !== undefined) { + originalInput.keep_enabled = update.keep_enabled; } - if (override.vars) { + if (update.vars) { const indexOfInput = inputs.indexOf(originalInput); - inputs[indexOfInput] = deepMergeVars(originalInput, override) as NewPackagePolicyInput; + inputs[indexOfInput] = deepMergeVars(originalInput, update, true) as NewPackagePolicyInput; originalInput = inputs[indexOfInput]; } - if (override.streams) { - for (const stream of override.streams) { + if (update.streams) { + for (const stream of update.streams) { let originalStream = originalInput?.streams.find( (s) => s.data_stream.dataset === stream.data_stream.dataset ); @@ -1118,7 +1116,8 @@ export function overridePackageInputs( const indexOfStream = originalInput.streams.indexOf(originalStream); originalInput.streams[indexOfStream] = deepMergeVars( originalStream, - stream as InputsOverride + stream as InputsOverride, + true ); originalStream = originalInput.streams[indexOfStream]; } @@ -1128,9 +1127,8 @@ export function overridePackageInputs( // Filter all stream that have been removed from the input originalInput.streams = originalInput.streams.filter((originalStream) => { return ( - override.streams?.some( - (s) => s.data_stream.dataset === originalStream.data_stream.dataset - ) ?? false + update.streams?.some((s) => s.data_stream.dataset === originalStream.data_stream.dataset) ?? + false ); }); } @@ -1171,7 +1169,110 @@ export function overridePackageInputs( return resultingPackagePolicy; } -function deepMergeVars(original: any, override: any): any { +export function preconfigurePackageInputs( + basePackagePolicy: NewPackagePolicy, + packageInfo: PackageInfo, + preconfiguredInputs?: InputsOverride[] +): NewPackagePolicy { + if (!preconfiguredInputs) return basePackagePolicy; + + const inputs = [...basePackagePolicy.inputs]; + + for (const preconfiguredInput of preconfiguredInputs) { + // Preconfiguration does not currently support multiple policy templates, so overrides will have an undefined + // policy template, so we only match on `type` in that case. + let originalInput = preconfiguredInput.policy_template + ? inputs.find( + (i) => + i.type === preconfiguredInput.type && + i.policy_template === preconfiguredInput.policy_template + ) + : inputs.find((i) => i.type === preconfiguredInput.type); + + // If the input do not exist skip + if (originalInput === undefined) { + continue; + } + + // For flags like this, we only want to override the original value if it was set + // as `undefined` in the original object. An explicit true/false value should be + // persisted from the original object to the result after the override process is complete. + if (originalInput.enabled === undefined && preconfiguredInput.enabled !== undefined) { + originalInput.enabled = preconfiguredInput.enabled; + } + + if (originalInput.keep_enabled === undefined && preconfiguredInput.keep_enabled !== undefined) { + originalInput.keep_enabled = preconfiguredInput.keep_enabled; + } + + if (preconfiguredInput.vars) { + const indexOfInput = inputs.indexOf(originalInput); + inputs[indexOfInput] = deepMergeVars( + originalInput, + preconfiguredInput + ) as NewPackagePolicyInput; + originalInput = inputs[indexOfInput]; + } + + if (preconfiguredInput.streams) { + for (const stream of preconfiguredInput.streams) { + let originalStream = originalInput?.streams.find( + (s) => s.data_stream.dataset === stream.data_stream.dataset + ); + + if (originalStream === undefined) { + continue; + } + + if (originalStream?.enabled === undefined) { + originalStream.enabled = stream.enabled; + } + + if (stream.vars) { + const indexOfStream = originalInput.streams.indexOf(originalStream); + originalInput.streams[indexOfStream] = deepMergeVars( + originalStream, + stream as InputsOverride + ); + originalStream = originalInput.streams[indexOfStream]; + } + } + } + } + + const resultingPackagePolicy: NewPackagePolicy = { + ...basePackagePolicy, + inputs, + }; + + const validationResults = validatePackagePolicy(resultingPackagePolicy, packageInfo, safeLoad); + + if (validationHasErrors(validationResults)) { + const responseFormattedValidationErrors = Object.entries(getFlattenedObject(validationResults)) + .map(([key, value]) => ({ + key, + message: value, + })) + .filter(({ message }) => !!message); + + if (responseFormattedValidationErrors.length) { + throw new PackagePolicyValidationError( + i18n.translate('xpack.fleet.packagePolicyInvalidError', { + defaultMessage: 'Package policy is invalid: {errors}', + values: { + errors: responseFormattedValidationErrors + .map(({ key, message }) => `${key}: ${message}`) + .join('\n'), + }, + }) + ); + } + } + + return resultingPackagePolicy; +} + +function deepMergeVars(original: any, override: any, keepOriginalValue = false): any { if (!original.vars) { original.vars = { ...override.vars }; } @@ -1192,7 +1293,7 @@ function deepMergeVars(original: any, override: any): any { // Ensure that any value from the original object is persisted on the newly merged resulting object, // even if we merge other data about the given variable - if (originalVar?.value) { + if (keepOriginalValue && originalVar?.value) { result.vars[name].value = originalVar.value; } } diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index 8b906b68556a..76fa7778eafa 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -34,7 +34,7 @@ import { ensurePackagesCompletedInstall } from './epm/packages/install'; import { bulkInstallPackages } from './epm/packages/bulk_install_packages'; import { agentPolicyService, addPackageToAgentPolicy } from './agent_policy'; import type { InputsOverride } from './package_policy'; -import { overridePackageInputs, packagePolicyService } from './package_policy'; +import { preconfigurePackageInputs, packagePolicyService } from './package_policy'; import { appContextService } from './app_context'; import type { UpgradeManagedPackagePoliciesResult } from './managed_package_policies'; import { upgradeManagedPackagePolicies } from './managed_package_policies'; @@ -428,7 +428,7 @@ async function addPreconfiguredPolicyPackages( defaultOutput, name, description, - (policy) => overridePackageInputs(policy, packageInfo, inputs), + (policy) => preconfigurePackageInputs(policy, packageInfo, inputs), bumpAgentPolicyRevison ); } From ab47ac64ad512329e59f99cbc1245582ead7c524 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Mon, 29 Nov 2021 12:27:42 -0700 Subject: [PATCH 027/224] [saved objects] Updates import docs to make it clearer which versions are supported. (#119879) --- docs/api/saved-objects/import.asciidoc | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/api/saved-objects/import.asciidoc b/docs/api/saved-objects/import.asciidoc index 923482954aa2..a214598af31a 100644 --- a/docs/api/saved-objects/import.asciidoc +++ b/docs/api/saved-objects/import.asciidoc @@ -11,11 +11,13 @@ Saved objects can only be imported into the same version, a newer minor on the s |======= | Exporting version | Importing version | Compatible? -| 6.7.0 | 6.8.1 | Yes -| 6.8.1 | 7.3.0 | Yes -| 7.3.0 | 7.11.1 | Yes -| 7.11.1 | 7.6.0 | No -| 6.8.1 | 8.0.0 | No +| 6.7.x | 6.8.x | Yes +| 6.x.x | 7.x.x | Yes +| 7.x.x | 8.x.x | Yes +| 7.1.x | 7.15.x | Yes +| 7.x.x | 6.x.x | No +| 7.15.x | 7.1.x | No +| 6.x.x | 8.x.x | No |======= [[saved-objects-api-import-request]] From db7423506c858354e593430e1832942fb1a13f6b Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Mon, 29 Nov 2021 11:46:33 -0800 Subject: [PATCH 028/224] Add workplace search links to doc link service (#118814) Co-authored-by: Scotty Bollinger --- ...-plugin-core-public.doclinksstart.links.md | 63 ++++++- ...kibana-plugin-core-public.doclinksstart.md | 2 +- .../public/doc_links/doc_links_service.ts | 129 ++++++++++++- src/core/public/public.api.md | 63 ++++++- .../api_logs/components/empty_state.test.tsx | 4 +- .../api_logs/components/empty_state.tsx | 10 +- .../crawler/components/crawl_rules_table.tsx | 8 +- .../deduplication_panel.tsx | 8 +- .../crawler/components/entry_points_table.tsx | 8 +- .../automatic_crawl_scheduler.tsx | 4 +- .../components/crawler/crawler_overview.tsx | 10 +- .../components/credentials/constants.ts | 4 +- .../credentials_list/credentials_list.tsx | 9 +- .../curations/components/empty_state.test.tsx | 4 +- .../curations/components/empty_state.tsx | 9 +- .../curations_settings.test.tsx | 4 +- .../curations_settings/curations_settings.tsx | 6 +- .../api_code_example.tsx | 6 +- .../document_creation_buttons.tsx | 4 +- .../documents/components/empty_state.test.tsx | 4 +- .../documents/components/empty_state.tsx | 9 +- .../engine_overview_empty.test.tsx | 2 +- .../engine_overview/engine_overview_empty.tsx | 4 +- .../empty_meta_engines_state.test.tsx | 4 +- .../components/empty_meta_engines_state.tsx | 9 +- .../components/engines/constants.tsx | 4 +- .../meta_engine_creation/constants.tsx | 4 +- .../components/empty_state.test.tsx | 4 +- .../components/empty_state.tsx | 9 +- .../precision_slider.test.tsx | 4 +- .../precision_slider/precision_slider.tsx | 8 +- .../relevance_tuning_callouts.tsx | 4 +- .../components/empty_state.test.tsx | 4 +- .../components/empty_state.tsx | 9 +- .../role_mappings/role_mappings.tsx | 8 +- .../schema/components/empty_state.test.tsx | 4 +- .../schema/components/empty_state.tsx | 9 +- .../search_ui/components/empty_state.test.tsx | 4 +- .../search_ui/components/empty_state.tsx | 9 +- .../components/search_ui/search_ui.tsx | 4 +- .../log_retention/log_retention_panel.tsx | 4 +- .../components/setup_guide/setup_guide.tsx | 6 +- .../synonyms/components/empty_state.test.tsx | 4 +- .../synonyms/components/empty_state.tsx | 9 +- .../public/applications/app_search/routes.ts | 24 ++- .../components/setup_guide/setup_guide.tsx | 6 +- .../shared/doc_links/doc_links.test.ts | 22 +-- .../shared/doc_links/doc_links.ts | 175 ++++++++++++++++-- .../licensing/manage_license_button.test.tsx | 4 +- .../licensing/manage_license_button.tsx | 7 +- .../role_mapping/role_mappings_table.tsx | 2 +- .../shared/role_mapping/user_selector.tsx | 2 +- .../role_mapping/users_empty_prompt.tsx | 2 +- .../shared/setup_guide/cloud/instructions.tsx | 10 +- .../applications/workplace_search/routes.ts | 61 +++--- .../components/oauth_application.test.tsx | 4 +- .../views/setup_guide/setup_guide.tsx | 12 +- 57 files changed, 585 insertions(+), 244 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 5c2c1d531754..7669b9b64491 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -24,6 +24,9 @@ readonly links: { readonly canvas: { readonly guide: string; }; + readonly cloud: { + readonly indexManagement: string; + }; readonly dashboard: { readonly guide: string; readonly drilldowns: string; @@ -55,10 +58,64 @@ readonly links: { readonly install: string; readonly start: string; }; + readonly appSearch: { + readonly apiRef: string; + readonly apiClients: string; + readonly apiKeys: string; + readonly authentication: string; + readonly crawlRules: string; + readonly curations: string; + readonly duplicateDocuments: string; + readonly entryPoints: string; + readonly guide: string; + readonly indexingDocuments: string; + readonly indexingDocumentsSchema: string; + readonly logSettings: string; + readonly metaEngines: string; + readonly nativeAuth: string; + readonly precisionTuning: string; + readonly relevanceTuning: string; + readonly resultSettings: string; + readonly searchUI: string; + readonly security: string; + readonly standardAuth: string; + readonly synonyms: string; + readonly webCrawler: string; + readonly webCrawlerEventLogs: string; + }; readonly enterpriseSearch: { - readonly base: string; - readonly appSearchBase: string; - readonly workplaceSearchBase: string; + readonly configuration: string; + readonly licenseManagement: string; + readonly mailService: string; + readonly usersAccess: string; + }; + readonly workplaceSearch: { + readonly box: string; + readonly confluenceCloud: string; + readonly confluenceServer: string; + readonly customSources: string; + readonly customSourcePermissions: string; + readonly documentPermissions: string; + readonly dropbox: string; + readonly externalIdentities: string; + readonly gitHub: string; + readonly gettingStarted: string; + readonly gmail: string; + readonly googleDrive: string; + readonly indexingSchedule: string; + readonly jiraCloud: string; + readonly jiraServer: string; + readonly nativeAuth: string; + readonly oneDrive: string; + readonly permissions: string; + readonly salesforce: string; + readonly security: string; + readonly serviceNow: string; + readonly sharePoint: string; + readonly slack: string; + readonly standardAuth: string; + readonly synch: string; + readonly zendesk: string; }; readonly heartbeat: { readonly base: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index cbfe53d3eaea..6aa528d4f04d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | { readonly settings: string; readonly elasticStackGetStarted: string; readonly upgrade: { readonly upgradingElasticStack: string; }; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; readonly customLinks: string; readonly droppedTransactionSpans: string; readonly upgrading: string; readonly metaData: string; }; readonly canvas: { readonly guide: string; }; readonly dashboard: { readonly guide: string; readonly drilldowns: string; readonly drilldownsTriggerPicker: string; readonly urlDrilldownTemplateSyntax: string; readonly urlDrilldownVariables: string; }; readonly discover: Record<string, string>; readonly filebeat: { readonly base: string; readonly installation: string; readonly configuration: string; readonly elasticsearchOutput: string; readonly elasticsearchModule: string; readonly startup: string; readonly exportedFields: string; readonly suricataModule: string; readonly zeekModule: string; }; readonly auditbeat: { readonly base: string; readonly auditdModule: string; readonly systemModule: string; }; readonly metricbeat: { readonly base: string; readonly configure: string; readonly httpEndpoint: string; readonly install: string; readonly start: string; }; readonly enterpriseSearch: { readonly base: string; readonly appSearchBase: string; readonly workplaceSearchBase: string; }; readonly heartbeat: { readonly base: string; }; readonly libbeat: { readonly getStarted: string; }; readonly logstash: { readonly base: string; }; readonly functionbeat: { readonly base: string; }; readonly winlogbeat: { readonly base: string; }; readonly aggs: { readonly composite: string; readonly composite\_missing\_bucket: string; readonly date\_histogram: string; readonly date\_range: string; readonly date\_format\_pattern: string; readonly filter: string; readonly filters: string; readonly geohash\_grid: string; readonly histogram: string; readonly ip\_range: string; readonly range: string; readonly significant\_terms: string; readonly terms: string; readonly terms\_doc\_count\_error: string; readonly avg: string; readonly avg\_bucket: string; readonly max\_bucket: string; readonly min\_bucket: string; readonly sum\_bucket: string; readonly cardinality: string; readonly count: string; readonly cumulative\_sum: string; readonly derivative: string; readonly geo\_bounds: string; readonly geo\_centroid: string; readonly max: string; readonly median: string; readonly min: string; readonly moving\_avg: string; readonly percentile\_ranks: string; readonly serial\_diff: string; readonly std\_dev: string; readonly sum: string; readonly top\_hits: string; }; readonly runtimeFields: { readonly overview: string; readonly mapping: string; }; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; readonly painless: string; readonly painlessApi: string; readonly painlessLangSpec: string; readonly painlessSyntax: string; readonly painlessWalkthrough: string; readonly luceneExpressions: string; }; readonly search: { readonly sessions: string; readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; readonly fieldFormattersNumber: string; readonly fieldFormattersString: string; readonly runtimeFields: string; }; readonly addData: string; readonly kibana: string; readonly upgradeAssistant: { readonly overview: string; readonly batchReindex: string; readonly remoteReindex: string; }; readonly rollupJobs: string; readonly elasticsearch: Record<string, string>; readonly siem: { readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; readonly ruleChangeLog: string; readonly detectionsReq: string; readonly networkMap: string; readonly troubleshootGaps: string; }; readonly securitySolution: { readonly trustedApps: string; }; readonly query: { readonly eql: string; readonly kueryQuerySyntax: string; readonly luceneQuerySyntax: string; readonly percolate: string; readonly queryDsl: string; }; readonly date: { readonly dateMath: string; readonly dateMathIndexNames: string; }; readonly management: Record<string, string>; readonly ml: Record<string, string>; readonly transforms: Record<string, string>; readonly visualize: Record<string, string>; readonly apis: Readonly<{ bulkIndexAlias: string; byteSizeUnits: string; createAutoFollowPattern: string; createFollower: string; createIndex: string; createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; createRollupJobsRequest: string; createApiKey: string; createPipeline: string; createTransformRequest: string; cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; putComponentTemplate: string; painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; putSnapshotLifecyclePolicy: string; putIndexTemplateV1: string; putWatch: string; simulatePipeline: string; timeUnits: string; updateTransform: string; }>; readonly observability: Readonly<{ guide: string; infrastructureThreshold: string; logsThreshold: string; metricsThreshold: string; monitorStatus: string; monitorUptime: string; tlsCertificate: string; uptimeDurationAnomaly: string; }>; readonly alerting: Record<string, string>; readonly maps: Readonly<{ guide: string; importGeospatialPrivileges: string; gdalTutorial: string; }>; readonly monitoring: Record<string, string>; readonly security: Readonly<{ apiKeyServiceSettings: string; clusterPrivileges: string; elasticsearchSettings: string; elasticsearchEnableSecurity: string; elasticsearchEnableApiKeys: string; indicesPrivileges: string; kibanaTLS: string; kibanaPrivileges: string; mappingRoles: string; mappingRolesFieldRules: string; runAsPrivilege: string; }>; readonly spaces: Readonly<{ kibanaLegacyUrlAliases: string; kibanaDisableLegacyUrlAliasesApi: string; }>; readonly watcher: Record<string, string>; readonly ccs: Record<string, string>; readonly plugins: Record<string, string>; readonly snapshotRestore: Record<string, string>; readonly ingest: Record<string, string>; readonly fleet: Readonly<{ beatsAgentComparison: string; guide: string; fleetServer: string; fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; settingsFleetServerProxySettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; datastreamsNamingScheme: string; installElasticAgent: string; installElasticAgentStandalone: string; upgradeElasticAgent: string; upgradeElasticAgent712lower: string; learnMoreBlog: string; apiKeysLearnMore: string; onPremRegistry: string; }>; readonly ecs: { readonly guide: string; }; readonly clients: { readonly guide: string; readonly goOverview: string; readonly javaIndex: string; readonly jsIntro: string; readonly netGuide: string; readonly perlGuide: string; readonly phpGuide: string; readonly pythonGuide: string; readonly rubyOverview: string; readonly rustGuide: string; }; readonly endpoints: { readonly troubleshooting: string; }; } | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | { readonly settings: string; readonly elasticStackGetStarted: string; readonly upgrade: { readonly upgradingElasticStack: string; }; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; readonly customLinks: string; readonly droppedTransactionSpans: string; readonly upgrading: string; readonly metaData: string; }; readonly canvas: { readonly guide: string; }; readonly cloud: { readonly indexManagement: string; }; readonly dashboard: { readonly guide: string; readonly drilldowns: string; readonly drilldownsTriggerPicker: string; readonly urlDrilldownTemplateSyntax: string; readonly urlDrilldownVariables: string; }; readonly discover: Record<string, string>; readonly filebeat: { readonly base: string; readonly installation: string; readonly configuration: string; readonly elasticsearchOutput: string; readonly elasticsearchModule: string; readonly startup: string; readonly exportedFields: string; readonly suricataModule: string; readonly zeekModule: string; }; readonly auditbeat: { readonly base: string; readonly auditdModule: string; readonly systemModule: string; }; readonly metricbeat: { readonly base: string; readonly configure: string; readonly httpEndpoint: string; readonly install: string; readonly start: string; }; readonly appSearch: { readonly apiRef: string; readonly apiClients: string; readonly apiKeys: string; readonly authentication: string; readonly crawlRules: string; readonly curations: string; readonly duplicateDocuments: string; readonly entryPoints: string; readonly guide: string; readonly indexingDocuments: string; readonly indexingDocumentsSchema: string; readonly logSettings: string; readonly metaEngines: string; readonly nativeAuth: string; readonly precisionTuning: string; readonly relevanceTuning: string; readonly resultSettings: string; readonly searchUI: string; readonly security: string; readonly standardAuth: string; readonly synonyms: string; readonly webCrawler: string; readonly webCrawlerEventLogs: string; }; readonly enterpriseSearch: { readonly configuration: string; readonly licenseManagement: string; readonly mailService: string; readonly usersAccess: string; }; readonly workplaceSearch: { readonly box: string; readonly confluenceCloud: string; readonly confluenceServer: string; readonly customSources: string; readonly customSourcePermissions: string; readonly documentPermissions: string; readonly dropbox: string; readonly externalIdentities: string; readonly gitHub: string; readonly gettingStarted: string; readonly gmail: string; readonly googleDrive: string; readonly indexingSchedule: string; readonly jiraCloud: string; readonly jiraServer: string; readonly nativeAuth: string; readonly oneDrive: string; readonly permissions: string; readonly salesforce: string; readonly security: string; readonly serviceNow: string; readonly sharePoint: string; readonly slack: string; readonly standardAuth: string; readonly synch: string; readonly zendesk: string; }; readonly heartbeat: { readonly base: string; }; readonly libbeat: { readonly getStarted: string; }; readonly logstash: { readonly base: string; }; readonly functionbeat: { readonly base: string; }; readonly winlogbeat: { readonly base: string; }; readonly aggs: { readonly composite: string; readonly composite\_missing\_bucket: string; readonly date\_histogram: string; readonly date\_range: string; readonly date\_format\_pattern: string; readonly filter: string; readonly filters: string; readonly geohash\_grid: string; readonly histogram: string; readonly ip\_range: string; readonly range: string; readonly significant\_terms: string; readonly terms: string; readonly terms\_doc\_count\_error: string; readonly avg: string; readonly avg\_bucket: string; readonly max\_bucket: string; readonly min\_bucket: string; readonly sum\_bucket: string; readonly cardinality: string; readonly count: string; readonly cumulative\_sum: string; readonly derivative: string; readonly geo\_bounds: string; readonly geo\_centroid: string; readonly max: string; readonly median: string; readonly min: string; readonly moving\_avg: string; readonly percentile\_ranks: string; readonly serial\_diff: string; readonly std\_dev: string; readonly sum: string; readonly top\_hits: string; }; readonly runtimeFields: { readonly overview: string; readonly mapping: string; }; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; readonly painless: string; readonly painlessApi: string; readonly painlessLangSpec: string; readonly painlessSyntax: string; readonly painlessWalkthrough: string; readonly luceneExpressions: string; }; readonly search: { readonly sessions: string; readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; readonly fieldFormattersNumber: string; readonly fieldFormattersString: string; readonly runtimeFields: string; }; readonly addData: string; readonly kibana: string; readonly upgradeAssistant: { readonly overview: string; readonly batchReindex: string; readonly remoteReindex: string; }; readonly rollupJobs: string; readonly elasticsearch: Record<string, string>; readonly siem: { readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; readonly ruleChangeLog: string; readonly detectionsReq: string; readonly networkMap: string; readonly troubleshootGaps: string; }; readonly securitySolution: { readonly trustedApps: string; }; readonly query: { readonly eql: string; readonly kueryQuerySyntax: string; readonly luceneQuerySyntax: string; readonly percolate: string; readonly queryDsl: string; }; readonly date: { readonly dateMath: string; readonly dateMathIndexNames: string; }; readonly management: Record<string, string>; readonly ml: Record<string, string>; readonly transforms: Record<string, string>; readonly visualize: Record<string, string>; readonly apis: Readonly<{ bulkIndexAlias: string; byteSizeUnits: string; createAutoFollowPattern: string; createFollower: string; createIndex: string; createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; createRollupJobsRequest: string; createApiKey: string; createPipeline: string; createTransformRequest: string; cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; putComponentTemplate: string; painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; putSnapshotLifecyclePolicy: string; putIndexTemplateV1: string; putWatch: string; simulatePipeline: string; timeUnits: string; updateTransform: string; }>; readonly observability: Readonly<{ guide: string; infrastructureThreshold: string; logsThreshold: string; metricsThreshold: string; monitorStatus: string; monitorUptime: string; tlsCertificate: string; uptimeDurationAnomaly: string; }>; readonly alerting: Record<string, string>; readonly maps: Readonly<{ guide: string; importGeospatialPrivileges: string; gdalTutorial: string; }>; readonly monitoring: Record<string, string>; readonly security: Readonly<{ apiKeyServiceSettings: string; clusterPrivileges: string; elasticsearchSettings: string; elasticsearchEnableSecurity: string; elasticsearchEnableApiKeys: string; indicesPrivileges: string; kibanaTLS: string; kibanaPrivileges: string; mappingRoles: string; mappingRolesFieldRules: string; runAsPrivilege: string; }>; readonly spaces: Readonly<{ kibanaLegacyUrlAliases: string; kibanaDisableLegacyUrlAliasesApi: string; }>; readonly watcher: Record<string, string>; readonly ccs: Record<string, string>; readonly plugins: Record<string, string>; readonly snapshotRestore: Record<string, string>; readonly ingest: Record<string, string>; readonly fleet: Readonly<{ beatsAgentComparison: string; guide: string; fleetServer: string; fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; settingsFleetServerProxySettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; datastreamsNamingScheme: string; installElasticAgent: string; installElasticAgentStandalone: string; upgradeElasticAgent: string; upgradeElasticAgent712lower: string; learnMoreBlog: string; apiKeysLearnMore: string; onPremRegistry: string; }>; readonly ecs: { readonly guide: string; }; readonly clients: { readonly guide: string; readonly goOverview: string; readonly javaIndex: string; readonly jsIntro: string; readonly netGuide: string; readonly perlGuide: string; readonly phpGuide: string; readonly pythonGuide: string; readonly rubyOverview: string; readonly rustGuide: string; }; readonly endpoints: { readonly troubleshooting: string; }; } | | diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 92b4c815f224..5bc7691d6a40 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -30,6 +30,9 @@ export class DocLinksService { const APM_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/apm/`; const SECURITY_SOLUTION_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/`; const STACK_GETTING_STARTED = `${ELASTIC_WEBSITE_URL}guide/en/elastic-stack-get-started/${DOC_LINK_VERSION}/`; + const APP_SEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/app-search/${DOC_LINK_VERSION}/`; + const ENTERPRISE_SEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/enterprise-search/${DOC_LINK_VERSION}/`; + const WORKPLACE_SEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/workplace-search/${DOC_LINK_VERSION}/`; return deepFreeze({ DOC_LINK_VERSION, @@ -51,6 +54,9 @@ export class DocLinksService { canvas: { guide: `${KIBANA_DOCS}canvas.html`, }, + cloud: { + indexManagement: `${ELASTIC_WEBSITE_URL}/guide/en/cloud/current/ec-configure-index-management.html`, + }, dashboard: { guide: `${KIBANA_DOCS}dashboard.html`, drilldowns: `${KIBANA_DOCS}drilldowns.html`, @@ -77,10 +83,64 @@ export class DocLinksService { auditdModule: `${ELASTIC_WEBSITE_URL}guide/en/beats/auditbeat/${DOC_LINK_VERSION}/auditbeat-module-auditd.html`, systemModule: `${ELASTIC_WEBSITE_URL}guide/en/beats/auditbeat/${DOC_LINK_VERSION}/auditbeat-module-system.html`, }, + appSearch: { + apiRef: `${APP_SEARCH_DOCS}api-reference.html`, + apiClients: `${APP_SEARCH_DOCS}api-clients.html`, + apiKeys: `${APP_SEARCH_DOCS}authentication.html#authentication-api-keys`, + authentication: `${APP_SEARCH_DOCS}authentication.html`, + crawlRules: `${APP_SEARCH_DOCS}crawl-web-content.html#crawl-web-content-manage-crawl-rules`, + curations: `${APP_SEARCH_DOCS}curations-guide.html`, + duplicateDocuments: `${APP_SEARCH_DOCS}web-crawler-reference.html#web-crawler-reference-content-deduplication`, + entryPoints: `${APP_SEARCH_DOCS}crawl-web-content.html#crawl-web-content-manage-entry-points`, + guide: `${APP_SEARCH_DOCS}index.html`, + indexingDocuments: `${APP_SEARCH_DOCS}indexing-documents-guide.html`, + indexingDocumentsSchema: `${APP_SEARCH_DOCS}indexing-documents-guide.html#indexing-documents-guide-schema`, + logSettings: `${APP_SEARCH_DOCS}logs.html`, + metaEngines: `${APP_SEARCH_DOCS}meta-engines-guide.html`, + nativeAuth: `${APP_SEARCH_DOCS}security-and-users.html#app-search-self-managed-security-and-user-management-elasticsearch-native-realm`, + precisionTuning: `${APP_SEARCH_DOCS}precision-tuning.html`, + relevanceTuning: `${APP_SEARCH_DOCS}relevance-tuning-guide.html`, + resultSettings: `${APP_SEARCH_DOCS}result-settings-guide.html`, + searchUI: `${APP_SEARCH_DOCS}reference-ui-guide.html`, + security: `${APP_SEARCH_DOCS}security-and-users.html`, + standardAuth: `${APP_SEARCH_DOCS}security-and-users.html#app-search-self-managed-security-and-user-management-standard`, + synonyms: `${APP_SEARCH_DOCS}synonyms-guide.html`, + webCrawler: `${APP_SEARCH_DOCS}web-crawler.html`, + webCrawlerEventLogs: `${APP_SEARCH_DOCS}view-web-crawler-events-logs.html`, + }, enterpriseSearch: { - base: `${ELASTIC_WEBSITE_URL}guide/en/enterprise-search/${DOC_LINK_VERSION}`, - appSearchBase: `${ELASTIC_WEBSITE_URL}guide/en/app-search/${DOC_LINK_VERSION}`, - workplaceSearchBase: `${ELASTIC_WEBSITE_URL}guide/en/workplace-search/${DOC_LINK_VERSION}`, + configuration: `${ENTERPRISE_SEARCH_DOCS}configuration.html`, + licenseManagement: `${ENTERPRISE_SEARCH_DOCS}license-management.html`, + mailService: `${ENTERPRISE_SEARCH_DOCS}mailer-configuration.html`, + usersAccess: `${ENTERPRISE_SEARCH_DOCS}users-access.html`, + }, + workplaceSearch: { + box: `${WORKPLACE_SEARCH_DOCS}workplace-search-box-connector.html`, + confluenceCloud: `${WORKPLACE_SEARCH_DOCS}workplace-search-confluence-cloud-connector.html`, + confluenceServer: `${WORKPLACE_SEARCH_DOCS}workplace-search-confluence-server-connector.html`, + customSources: `${WORKPLACE_SEARCH_DOCS}workplace-search-custom-api-sources.html`, + customSourcePermissions: `${WORKPLACE_SEARCH_DOCS}workplace-search-custom-api-sources.html#custom-api-source-document-level-access-control`, + documentPermissions: `${WORKPLACE_SEARCH_DOCS}workplace-search-sources-document-permissions.html`, + dropbox: `${WORKPLACE_SEARCH_DOCS}workplace-search-dropbox-connector.html`, + externalIdentities: `${WORKPLACE_SEARCH_DOCS}workplace-search-external-identities-api.html`, + gettingStarted: `${WORKPLACE_SEARCH_DOCS}workplace-search-getting-started.html`, + gitHub: `${WORKPLACE_SEARCH_DOCS}workplace-search-github-connector.html`, + gmail: `${WORKPLACE_SEARCH_DOCS}workplace-search-gmail-connector.html`, + googleDrive: `${WORKPLACE_SEARCH_DOCS}workplace-search-google-drive-connector.html`, + indexingSchedule: `${WORKPLACE_SEARCH_DOCS}workplace-search-customizing-indexing-rules.html#_indexing_schedule`, + jiraCloud: `${WORKPLACE_SEARCH_DOCS}workplace-search-jira-cloud-connector.html`, + jiraServer: `${WORKPLACE_SEARCH_DOCS}workplace-search-jira-server-connector.html`, + nativeAuth: `${WORKPLACE_SEARCH_DOCS}workplace-search-security.html#elasticsearch-native-realm`, + oneDrive: `${WORKPLACE_SEARCH_DOCS}workplace-search-onedrive-connector.html`, + permissions: `${WORKPLACE_SEARCH_DOCS}workplace-search-permissions.html#organizational-sources-private-sources`, + salesforce: `${WORKPLACE_SEARCH_DOCS}workplace-search-salesforce-connector.html`, + security: `${WORKPLACE_SEARCH_DOCS}workplace-search-security.html`, + serviceNow: `${WORKPLACE_SEARCH_DOCS}workplace-search-servicenow-connector.html`, + sharePoint: `${WORKPLACE_SEARCH_DOCS}workplace-search-sharepoint-online-connector.html`, + slack: `${WORKPLACE_SEARCH_DOCS}workplace-search-slack-connector.html`, + standardAuth: `${WORKPLACE_SEARCH_DOCS}workplace-search-security.html#standard`, + synch: `${WORKPLACE_SEARCH_DOCS}workplace-search-customizing-indexing-rules.html`, + zendesk: `${WORKPLACE_SEARCH_DOCS}workplace-search-zendesk-connector.html`, }, metricbeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}`, @@ -550,6 +610,9 @@ export interface DocLinksStart { readonly canvas: { readonly guide: string; }; + readonly cloud: { + readonly indexManagement: string; + }; readonly dashboard: { readonly guide: string; readonly drilldowns: string; @@ -581,10 +644,64 @@ export interface DocLinksStart { readonly install: string; readonly start: string; }; + readonly appSearch: { + readonly apiRef: string; + readonly apiClients: string; + readonly apiKeys: string; + readonly authentication: string; + readonly crawlRules: string; + readonly curations: string; + readonly duplicateDocuments: string; + readonly entryPoints: string; + readonly guide: string; + readonly indexingDocuments: string; + readonly indexingDocumentsSchema: string; + readonly logSettings: string; + readonly metaEngines: string; + readonly nativeAuth: string; + readonly precisionTuning: string; + readonly relevanceTuning: string; + readonly resultSettings: string; + readonly searchUI: string; + readonly security: string; + readonly standardAuth: string; + readonly synonyms: string; + readonly webCrawler: string; + readonly webCrawlerEventLogs: string; + }; readonly enterpriseSearch: { - readonly base: string; - readonly appSearchBase: string; - readonly workplaceSearchBase: string; + readonly configuration: string; + readonly licenseManagement: string; + readonly mailService: string; + readonly usersAccess: string; + }; + readonly workplaceSearch: { + readonly box: string; + readonly confluenceCloud: string; + readonly confluenceServer: string; + readonly customSources: string; + readonly customSourcePermissions: string; + readonly documentPermissions: string; + readonly dropbox: string; + readonly externalIdentities: string; + readonly gitHub: string; + readonly gettingStarted: string; + readonly gmail: string; + readonly googleDrive: string; + readonly indexingSchedule: string; + readonly jiraCloud: string; + readonly jiraServer: string; + readonly nativeAuth: string; + readonly oneDrive: string; + readonly permissions: string; + readonly salesforce: string; + readonly security: string; + readonly serviceNow: string; + readonly sharePoint: string; + readonly slack: string; + readonly standardAuth: string; + readonly synch: string; + readonly zendesk: string; }; readonly heartbeat: { readonly base: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 772faa5321d9..cec80af843c4 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -506,6 +506,9 @@ export interface DocLinksStart { readonly canvas: { readonly guide: string; }; + readonly cloud: { + readonly indexManagement: string; + }; readonly dashboard: { readonly guide: string; readonly drilldowns: string; @@ -537,10 +540,64 @@ export interface DocLinksStart { readonly install: string; readonly start: string; }; + readonly appSearch: { + readonly apiRef: string; + readonly apiClients: string; + readonly apiKeys: string; + readonly authentication: string; + readonly crawlRules: string; + readonly curations: string; + readonly duplicateDocuments: string; + readonly entryPoints: string; + readonly guide: string; + readonly indexingDocuments: string; + readonly indexingDocumentsSchema: string; + readonly logSettings: string; + readonly metaEngines: string; + readonly nativeAuth: string; + readonly precisionTuning: string; + readonly relevanceTuning: string; + readonly resultSettings: string; + readonly searchUI: string; + readonly security: string; + readonly standardAuth: string; + readonly synonyms: string; + readonly webCrawler: string; + readonly webCrawlerEventLogs: string; + }; readonly enterpriseSearch: { - readonly base: string; - readonly appSearchBase: string; - readonly workplaceSearchBase: string; + readonly configuration: string; + readonly licenseManagement: string; + readonly mailService: string; + readonly usersAccess: string; + }; + readonly workplaceSearch: { + readonly box: string; + readonly confluenceCloud: string; + readonly confluenceServer: string; + readonly customSources: string; + readonly customSourcePermissions: string; + readonly documentPermissions: string; + readonly dropbox: string; + readonly externalIdentities: string; + readonly gitHub: string; + readonly gettingStarted: string; + readonly gmail: string; + readonly googleDrive: string; + readonly indexingSchedule: string; + readonly jiraCloud: string; + readonly jiraServer: string; + readonly nativeAuth: string; + readonly oneDrive: string; + readonly permissions: string; + readonly salesforce: string; + readonly security: string; + readonly serviceNow: string; + readonly sharePoint: string; + readonly slack: string; + readonly standardAuth: string; + readonly synch: string; + readonly zendesk: string; }; readonly heartbeat: { readonly base: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx index 19f45ced5dc5..cb1c34a19c01 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx @@ -11,6 +11,8 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { docLinks } from '../../../../shared/doc_links'; + import { EmptyState } from './'; describe('EmptyState', () => { @@ -21,7 +23,7 @@ describe('EmptyState', () => { expect(wrapper.find('h2').text()).toEqual('No API events in the last 24 hours'); expect(wrapper.find(EuiButton).prop('href')).toEqual( - expect.stringContaining('/api-reference.html') + expect.stringContaining(docLinks.appSearchApis) ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx index 76bd0cba1731..c78bf3e91873 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx @@ -8,9 +8,10 @@ import React from 'react'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; -import { DOCS_PREFIX } from '../../../routes'; +import { API_DOCS_URL } from '../../../routes'; export const EmptyState: React.FC = () => ( (

} actions={ - + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.empty.buttonLabel', { defaultMessage: 'View the API reference', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_rules_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_rules_table.tsx index d447db60fb25..bef8ed4462fd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_rules_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_rules_table.tsx @@ -27,7 +27,7 @@ import { clearFlashMessages, flashSuccessToast } from '../../../../shared/flash_ import { GenericEndpointInlineEditableTable } from '../../../../shared/tables/generic_endpoint_inline_editable_table'; import { InlineEditableTableColumn } from '../../../../shared/tables/inline_editable_table/types'; import { ItemWithAnID } from '../../../../shared/tables/types'; -import { DOCS_PREFIX } from '../../../routes'; +import { CRAWL_RULES_DOCS_URL } from '../../../routes'; import { CrawlerSingleDomainLogic } from '../crawler_single_domain_logic'; import { CrawlerPolicies, @@ -53,11 +53,7 @@ const DEFAULT_DESCRIPTION = ( defaultMessage="Create a crawl rule to include or exclude pages whose URL matches the rule. Rules run in sequential order, and each URL is evaluated according to the first match. {link}" values={{ link: ( - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.crawler.crawlRulesTable.descriptionLinkText', { defaultMessage: 'Learn more about crawl rules' } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.tsx index ea894e2b00ac..26794d042135 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.tsx @@ -27,7 +27,7 @@ import { EuiSelectableLIOption } from '@elastic/eui/src/components/selectable/se import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { DOCS_PREFIX } from '../../../../routes'; +import { DUPLICATE_DOCS_URL } from '../../../../routes'; import { DataPanel } from '../../../data_panel'; import { CrawlerSingleDomainLogic } from '../../crawler_single_domain_logic'; @@ -84,11 +84,7 @@ export const DeduplicationPanel: React.FC = () => { documents on this domain. {documentationLink}." values={{ documentationLink: ( - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.learnMoreMessage', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.tsx index aaf3cc451606..4fc7a0569ba0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.tsx @@ -17,7 +17,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { GenericEndpointInlineEditableTable } from '../../../../shared/tables/generic_endpoint_inline_editable_table'; import { InlineEditableTableColumn } from '../../../../shared/tables/inline_editable_table/types'; import { ItemWithAnID } from '../../../../shared/tables/types'; -import { DOCS_PREFIX } from '../../../routes'; +import { ENTRY_POINTS_DOCS_URL } from '../../../routes'; import { CrawlerDomain, EntryPoint } from '../types'; import { EntryPointsTableLogic } from './entry_points_table_logic'; @@ -80,11 +80,7 @@ export const EntryPointsTable: React.FC = ({ defaultMessage: 'Include the most important URLs for your website here. Entry point URLs will be the first pages to be indexed and processed for links to other pages.', })}{' '} - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.learnMoreLinkText', { defaultMessage: 'Learn more about entry points.' } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.tsx index 5f7200cb826d..128dcdcb778c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.tsx @@ -37,7 +37,7 @@ import { } from '../../../../..//shared/constants/units'; import { CANCEL_BUTTON_LABEL, SAVE_BUTTON_LABEL } from '../../../../../shared/constants'; -import { DOCS_PREFIX } from '../../../../routes'; +import { WEB_CRAWLER_DOCS_URL } from '../../../../routes'; import { CrawlUnits } from '../../types'; import { AutomaticCrawlSchedulerLogic } from './automatic_crawl_scheduler_logic'; @@ -81,7 +81,7 @@ export const AutomaticCrawlScheduler: React.FC = () => { defaultMessage="Don't worry about it, we'll start a crawl for you. {readMoreMessage}." values={{ readMoreMessage: ( - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.crawler.automaticCrawlSchedule.readMoreLink', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx index 6c3cb51111ae..c84deb3cb0c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx @@ -13,7 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from import { i18n } from '@kbn/i18n'; -import { DOCS_PREFIX } from '../../routes'; +import { WEB_CRAWLER_DOCS_URL, WEB_CRAWLER_LOG_DOCS_URL } from '../../routes'; import { getEngineBreadcrumbs } from '../engine'; import { AppSearchPageTemplate } from '../layout'; @@ -77,7 +77,7 @@ export const CrawlerOverview: React.FC = () => { defaultMessage: "Easily index your website's content. To get started, enter your domain name, provide optional entry points and crawl rules, and we will handle the rest.", })}{' '} - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.crawler.empty.crawlerDocumentationLinkDescription', { @@ -114,11 +114,7 @@ export const CrawlerOverview: React.FC = () => { defaultMessage: "Recent crawl requests are logged here. Using the request ID of each crawl, you can track progress and examine crawl events in Kibana's Discover or Logs user interfaces.", })}{' '} - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.crawler.configurationDocumentationLinkDescription', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts index 6a5f3df0e86f..315b4d864b3f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; -import { DOCS_PREFIX } from '../../routes'; +import { AUTHENTICATION_DOCS_URL } from '../../routes'; export const CREDENTIALS_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.credentials.title', @@ -109,4 +109,4 @@ export const TOKEN_TYPE_INFO = [ export const FLYOUT_ARIA_LABEL_ID = 'credentialsFlyoutTitle'; -export const DOCS_HREF = `${DOCS_PREFIX}/authentication.html`; +export const DOCS_HREF = AUTHENTICATION_DOCS_URL; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx index 040f313b1220..3ea2c022ec48 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { EDIT_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../../../../shared/constants'; import { HiddenText } from '../../../../shared/hidden_text'; import { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination'; -import { DOCS_PREFIX } from '../../../routes'; +import { API_KEYS_DOCS_URL } from '../../../routes'; import { TOKEN_TYPE_DISPLAY_NAMES } from '../constants'; import { CredentialsLogic } from '../credentials_logic'; import { ApiToken } from '../types'; @@ -141,12 +141,7 @@ export const CredentialsList: React.FC = () => { defaultMessage: 'Allow applications to access Elastic App Search on your behalf.', })} actions={ - + {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.empty.buttonLabel', { defaultMessage: 'Learn about API keys', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/empty_state.test.tsx index 60ae386bea58..69c2cc4b987b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/empty_state.test.tsx @@ -11,6 +11,8 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { docLinks } from '../../../../shared/doc_links'; + import { EmptyState } from './'; describe('EmptyState', () => { @@ -21,7 +23,7 @@ describe('EmptyState', () => { expect(wrapper.find('h2').text()).toEqual('Create your first curation'); expect(wrapper.find(EuiButton).prop('href')).toEqual( - expect.stringContaining('/curations-guide.html') + expect.stringContaining(docLinks.appSearchCurations) ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/empty_state.tsx index 872a7282136e..10d81f162395 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/empty_state.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DOCS_PREFIX } from '../../../routes'; +import { CURATIONS_DOCS_URL } from '../../../routes'; export const EmptyState: React.FC = () => ( (

} actions={ - + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.empty.buttonLabel', { defaultMessage: 'Read the curations guide', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.test.tsx index 4b4e11c31d4b..b95ae0bca5bf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.test.tsx @@ -19,6 +19,8 @@ import { EuiButtonEmpty, EuiCallOut, EuiSwitch } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test/jest'; +import { docLinks } from '../../../../../shared/doc_links'; + import { Loading } from '../../../../../shared/loading'; import { EuiButtonTo } from '../../../../../shared/react_router_helpers'; import { DataPanel } from '../../../data_panel'; @@ -227,7 +229,7 @@ describe('CurationsSettings', () => { const wrapper = shallow(); expect(wrapper.is(DataPanel)).toBe(true); expect(wrapper.prop('action').props.to).toEqual('/app/management/stack/license_management'); - expect(wrapper.find(EuiButtonEmpty).prop('href')).toEqual('/license-management.html'); + expect(wrapper.find(EuiButtonEmpty).prop('href')).toEqual(docLinks.licenseManagement); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.tsx index d78ca852ee7d..ffefea96d3a2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.tsx @@ -110,11 +110,7 @@ export const CurationsSettings: React.FC = () => { } > - + {i18n.translate('xpack.enterpriseSearch.curations.settings.licenseUpgradeLink', { defaultMessage: 'Learn more about license upgrades', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx index 793c6250d859..e86b06b423a9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx @@ -30,7 +30,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { CANCEL_BUTTON_LABEL } from '../../../../shared/constants'; import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url'; -import { DOCS_PREFIX } from '../../../routes'; +import { API_CLIENTS_DOCS_URL, INDEXING_DOCS_URL } from '../../../routes'; import { EngineLogic } from '../../engine'; import { EngineDetails } from '../../engine/types'; @@ -74,12 +74,12 @@ export const FlyoutBody: React.FC = () => { defaultMessage="The {documentsApiLink} can be used to add new documents to your engine, update documents, retrieve documents by id, and delete documents. There are a variety of {clientLibrariesLink} to help you get started." values={{ documentsApiLink: ( - + documents API ), clientLibrariesLink: ( - + client libraries ), diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx index 5366c00c0e7f..a8179f297644 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx @@ -27,7 +27,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { parseQueryParams } from '../../../shared/query_params'; import { EuiCardTo } from '../../../shared/react_router_helpers'; -import { DOCS_PREFIX, ENGINE_CRAWLER_PATH } from '../../routes'; +import { INDEXING_DOCS_URL, ENGINE_CRAWLER_PATH } from '../../routes'; import { generateEnginePath } from '../engine'; import { DocumentCreationLogic } from './'; @@ -66,7 +66,7 @@ export const DocumentCreationButtons: React.FC = ({ disabled = false }) = jsonCode: .json, postCode: POST, documentsApiLink: ( - + documents API ), diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.test.tsx index 907dcf8c9c20..b8bb26fa9ad6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.test.tsx @@ -11,6 +11,8 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { docLinks } from '../../../../shared/doc_links'; + import { EmptyState } from './'; describe('EmptyState', () => { @@ -21,7 +23,7 @@ describe('EmptyState', () => { expect(wrapper.find('h2').text()).toEqual('Add your first documents'); expect(wrapper.find(EuiButton).prop('href')).toEqual( - expect.stringContaining('/indexing-documents-guide.html') + expect.stringContaining(docLinks.appSearchIndexingDocs) ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.tsx index 39fe02a84854..85e834b32075 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DOCS_PREFIX } from '../../../routes'; +import { INDEXING_DOCS_URL } from '../../../routes'; export const EmptyState = () => ( (

} actions={ - + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.documents.empty.buttonLabel', { defaultMessage: 'Read the documents guide', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx index 6750ebf1140e..54bc7fb26e9d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx @@ -33,7 +33,7 @@ describe('EmptyEngineOverview', () => { it('renders a documentation link', () => { expect(getPageHeaderActions(wrapper).find(EuiButton).prop('href')).toEqual( - `${docLinks.appSearchBase}/index.html` + docLinks.appSearchGuide ); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx index 6f8332e1e332..ada2df654d52 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DOCS_PREFIX } from '../../routes'; +import { DOCS_URL } from '../../routes'; import { DocumentCreationButtons, DocumentCreationFlyout } from '../document_creation'; import { getEngineBreadcrumbs } from '../engine'; @@ -26,7 +26,7 @@ export const EmptyEngineOverview: React.FC = () => { { defaultMessage: 'Engine setup' } ), rightSideItems: [ - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.overview.empty.headingAction', { defaultMessage: 'View documentation' } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx index 8b4f5a69b814..350412825b99 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx @@ -11,6 +11,8 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { docLinks } from '../../../../shared/doc_links'; + import { EmptyMetaEnginesState } from './'; describe('EmptyMetaEnginesState', () => { @@ -21,7 +23,7 @@ describe('EmptyMetaEnginesState', () => { expect(wrapper.find('h3').text()).toEqual('Create your first meta engine'); expect(wrapper.find(EuiButton).prop('href')).toEqual( - expect.stringContaining('/meta-engines-guide.html') + expect.stringContaining(docLinks.appSearchMetaEngines) ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx index ad96f21022f2..3cf461e3f7d4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DOCS_PREFIX } from '../../../routes'; +import { META_ENGINES_DOCS_URL } from '../../../routes'; export const EmptyMetaEnginesState: React.FC = () => ( (

} actions={ - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.engines.metaEngines.emptyPromptButtonLabel', { defaultMessage: 'Learn more about meta engines' } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx index 8fbbf406cf5d..bf2a122ead42 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx @@ -11,7 +11,7 @@ import { EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { DOCS_PREFIX } from '../../routes'; +import { META_ENGINES_DOCS_URL } from '../../routes'; import { META_ENGINE_CREATION_FORM_META_ENGINE_DESCRIPTION, META_ENGINE_CREATION_FORM_DOCUMENTATION_LINK, @@ -40,7 +40,7 @@ export const META_ENGINES_DESCRIPTION = ( defaultMessage="{readDocumentationLink} for more information or upgrade to a Platinum license to get started." values={{ readDocumentationLink: ( - + {META_ENGINE_CREATION_FORM_DOCUMENTATION_LINK} ), diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/constants.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/constants.tsx index af7b6f3201b3..e41809054e12 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/constants.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/constants.tsx @@ -11,7 +11,7 @@ import { EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { DOCS_PREFIX } from '../../routes'; +import { META_ENGINES_DOCS_URL } from '../../routes'; export const DEFAULT_LANGUAGE = 'Universal'; @@ -57,7 +57,7 @@ export const META_ENGINE_CREATION_FORM_DOCUMENTATION_DESCRIPTION = ( defaultMessage="{documentationLink} for information about how to get started." values={{ documentationLink: ( - + {META_ENGINE_CREATION_FORM_DOCUMENTATION_LINK} ), diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.test.tsx index a60f68c19f6d..454437a203bc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.test.tsx @@ -11,6 +11,8 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { docLinks } from '../../../../shared/doc_links'; + import { EmptyState } from './'; describe('EmptyState', () => { @@ -21,7 +23,7 @@ describe('EmptyState', () => { expect(wrapper.find('h2').text()).toEqual('Add documents to tune relevance'); expect(wrapper.find(EuiButton).prop('href')).toEqual( - expect.stringContaining('/relevance-tuning-guide.html') + expect.stringContaining(docLinks.appSearchRelevance) ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx index df29010bd682..f17f7a582efd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DOCS_PREFIX } from '../../../routes'; +import { RELEVANCE_DOCS_URL } from '../../../routes'; export const EmptyState: React.FC = () => ( ( } )} actions={ - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.buttonLabel', { defaultMessage: 'Read the relevance tuning guide' } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/precision_slider/precision_slider.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/precision_slider/precision_slider.test.tsx index 3be30b77bc2e..0554b31c8835 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/precision_slider/precision_slider.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/precision_slider/precision_slider.test.tsx @@ -11,6 +11,8 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; +import { docLinks } from '../../../../../shared/doc_links'; + import { rerender } from '../../../../../test_helpers'; import { STEP_DESCRIPTIONS } from './constants'; @@ -82,7 +84,7 @@ describe('PrecisionSlider', () => { it('contains a documentation link', () => { const documentationLink = wrapper.find('[data-test-subj="documentationLink"]'); - expect(documentationLink.prop('href')).toContain('/precision-tuning.html'); + expect(documentationLink.prop('href')).toContain(docLinks.appSearchPrecision); expect(documentationLink.prop('target')).toEqual('_blank'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/precision_slider/precision_slider.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/precision_slider/precision_slider.tsx index 8e7a59c290ce..e4b2027aa3d6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/precision_slider/precision_slider.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/precision_slider/precision_slider.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DOCS_PREFIX } from '../../../../routes'; +import { PRECISION_DOCS_URL } from '../../../../routes'; import { RelevanceTuningLogic } from '../../relevance_tuning_logic'; import { STEP_DESCRIPTIONS } from './constants'; @@ -57,11 +57,7 @@ export const PrecisionSlider: React.FC = () => { defaultMessage: 'Fine tune the precision vs. recall settings on your engine.', } )}{' '} - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.learnMore.link', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx index d8963b33b8ab..463c61fb60c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiLinkTo } from '../../../shared/react_router_helpers'; -import { DOCS_PREFIX, ENGINE_SCHEMA_PATH } from '../../routes'; +import { META_ENGINES_DOCS_URL, ENGINE_SCHEMA_PATH } from '../../routes'; import { EngineLogic, generateEnginePath } from '../engine'; import { RelevanceTuningLogic } from '.'; @@ -98,7 +98,7 @@ export const RelevanceTuningCallouts: React.FC = () => { values={{ schemaFieldsWithConflictsCount, link: ( - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.whatsThisLinkLabel', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.test.tsx index 537fd9ec6a0d..8798c1a4bc52 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.test.tsx @@ -11,6 +11,8 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { docLinks } from '../../../../shared/doc_links'; + import { EmptyState } from './'; describe('EmptyState', () => { @@ -21,7 +23,7 @@ describe('EmptyState', () => { expect(wrapper.find('h2').text()).toEqual('Add documents to adjust settings'); expect(wrapper.find(EuiButton).prop('href')).toEqual( - expect.stringContaining('/result-settings-guide.html') + expect.stringContaining(docLinks.appSearchResultSettings) ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.tsx index dae8390a35fd..7f91447b910b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DOCS_PREFIX } from '../../../routes'; +import { RESULT_SETTINGS_DOCS_URL } from '../../../routes'; export const EmptyState: React.FC = () => ( ( } )} actions={ - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.resultSettings.empty.buttonLabel', { defaultMessage: 'Read the result settings guide' } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx index 3e692aa48623..e2021ac582d0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx @@ -22,7 +22,7 @@ import { } from '../../../shared/role_mapping'; import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants'; -import { DOCS_PREFIX } from '../../routes'; +import { SECURITY_DOCS_URL } from '../../routes'; import { AppSearchPageTemplate } from '../layout'; import { ROLE_MAPPINGS_ENGINE_ACCESS_HEADING } from './constants'; @@ -30,8 +30,6 @@ import { RoleMapping } from './role_mapping'; import { RoleMappingsLogic } from './role_mappings_logic'; import { User } from './user'; -const ROLES_DOCS_LINK = `${DOCS_PREFIX}/security-and-users.html`; - export const RoleMappings: React.FC = () => { const { enableRoleBasedAccess, @@ -60,7 +58,7 @@ export const RoleMappings: React.FC = () => { const rolesEmptyState = ( ); @@ -69,7 +67,7 @@ export const RoleMappings: React.FC = () => {
initializeRoleMapping()} /> { expect(wrapper.find('h2').text()).toEqual('Create a schema'); expect(wrapper.find(EuiButton).prop('href')).toEqual( - expect.stringContaining('#indexing-documents-guide-schema') + expect.stringContaining(docLinks.appSearchIndexingDocsSchema) ); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx index ad9285c7b8fe..3c2d5fc4df66 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx @@ -13,7 +13,7 @@ import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SchemaAddFieldModal } from '../../../../shared/schema'; -import { DOCS_PREFIX } from '../../../routes'; +import { INDEXING_SCHEMA_DOCS_URL } from '../../../routes'; import { SchemaLogic } from '../schema_logic'; export const EmptyState: React.FC = () => { @@ -40,12 +40,7 @@ export const EmptyState: React.FC = () => {

} actions={ - + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.schema.empty.buttonLabel', { defaultMessage: 'Read the indexing schema guide', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.test.tsx index 39f0cb376b32..3466542c0973 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.test.tsx @@ -11,6 +11,8 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { docLinks } from '../../../../shared/doc_links'; + import { EmptyState } from './empty_state'; describe('EmptyState', () => { @@ -21,7 +23,7 @@ describe('EmptyState', () => { expect(wrapper.find('h2').text()).toEqual('Add documents to generate a Search UI'); expect(wrapper.find(EuiButton).prop('href')).toEqual( - expect.stringContaining('/reference-ui-guide.html') + expect.stringContaining(docLinks.appSearchSearchUI) ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.tsx index b7665a58de30..9a663e137221 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DOCS_PREFIX } from '../../../routes'; +import { SEARCH_UI_DOCS_URL } from '../../../routes'; export const EmptyState: React.FC = () => ( (

} actions={ - + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.searchUI.empty.buttonLabel', { defaultMessage: 'Read the Search UI guide', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx index 2b210bd07ab4..43ea60fa8461 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx @@ -12,7 +12,7 @@ import { useActions, useValues } from 'kea'; import { EuiText, EuiFlexItem, EuiFlexGroup, EuiSpacer, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { DOCS_PREFIX } from '../../routes'; +import { SEARCH_UI_DOCS_URL } from '../../routes'; import { EngineLogic, getEngineBreadcrumbs } from '../engine'; import { AppSearchPageTemplate } from '../layout'; @@ -62,7 +62,7 @@ export const SearchUI: React.FC = () => { defaultMessage="Use the fields below to generate a sample search experience built with Search UI. Use the sample to preview search results, or build upon it to create your own custom search experience. {link}." values={{ link: ( - + { defaultMessage: 'Log retention is determined by the ILM policies for your deployment.', })}
- + {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.learnMore', { defaultMessage: 'Learn more about log retention for Enterprise Search.', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx index d460132dddbb..f1d9beaca513 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx @@ -15,7 +15,7 @@ import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; -import { DOCS_PREFIX } from '../../routes'; +import { NATIVE_AUTH_DOCS_URL, STANDARD_AUTH_DOCS_URL } from '../../routes'; import GettingStarted from './assets/getting_started.png'; @@ -23,8 +23,8 @@ export const SetupGuide: React.FC = () => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx index a43f170e5822..cdfdbadf6759 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx @@ -11,6 +11,8 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { docLinks } from '../../../../shared/doc_links'; + import { EmptyState, SynonymModal } from './'; describe('EmptyState', () => { @@ -21,7 +23,7 @@ describe('EmptyState', () => { expect(wrapper.find('h2').text()).toEqual('Create your first synonym set'); expect(wrapper.find(EuiButton).prop('href')).toEqual( - expect.stringContaining('/synonyms-guide.html') + expect.stringContaining(docLinks.appSearchSynonyms) ); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx index f856a5c035f8..ac8383ccea9e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DOCS_PREFIX } from '../../../routes'; +import { SYNONYMS_DOCS_URL } from '../../../routes'; import { SynonymModal, SynonymIcon } from './'; @@ -35,12 +35,7 @@ export const EmptyState: React.FC = () => {

} actions={ - + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.synonyms.empty.buttonLabel', { defaultMessage: 'Read the synonyms guide', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts index 97a9b407b3cd..1f2e7c883e1c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts @@ -7,7 +7,29 @@ import { docLinks } from '../shared/doc_links'; -export const DOCS_PREFIX = docLinks.appSearchBase; +export const API_DOCS_URL = docLinks.appSearchApis; +export const API_CLIENTS_DOCS_URL = docLinks.appSearchApiClients; +export const API_KEYS_DOCS_URL = docLinks.appSearchApiKeys; +export const AUTHENTICATION_DOCS_URL = docLinks.appSearchAuthentication; +export const CRAWL_RULES_DOCS_URL = docLinks.appSearchCrawlRules; +export const CURATIONS_DOCS_URL = docLinks.appSearchCurations; +export const DOCS_URL = docLinks.appSearchGuide; +export const DUPLICATE_DOCS_URL = docLinks.appSearchDuplicateDocuments; +export const ENTRY_POINTS_DOCS_URL = docLinks.appSearchEntryPoints; +export const INDEXING_DOCS_URL = docLinks.appSearchIndexingDocs; +export const INDEXING_SCHEMA_DOCS_URL = docLinks.appSearchIndexingDocsSchema; +export const LOG_SETTINGS_DOCS_URL = docLinks.appSearchLogSettings; +export const META_ENGINES_DOCS_URL = docLinks.appSearchMetaEngines; +export const NATIVE_AUTH_DOCS_URL = docLinks.appSearchNativeAuth; +export const PRECISION_DOCS_URL = docLinks.appSearchPrecision; +export const RELEVANCE_DOCS_URL = docLinks.appSearchRelevance; +export const RESULT_SETTINGS_DOCS_URL = docLinks.appSearchResultSettings; +export const SEARCH_UI_DOCS_URL = docLinks.appSearchSearchUI; +export const SECURITY_DOCS_URL = docLinks.appSearchSecurity; +export const STANDARD_AUTH_DOCS_URL = docLinks.appSearchStandardAuth; +export const SYNONYMS_DOCS_URL = docLinks.appSearchSynonyms; +export const WEB_CRAWLER_DOCS_URL = docLinks.appSearchWebCrawler; +export const WEB_CRAWLER_LOG_DOCS_URL = docLinks.appSearchWebCrawlerEventLogs; export const ROOT_PATH = '/'; export const SETUP_GUIDE_PATH = '/setup_guide'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx index e82dbcaa4113..c7c85fdd4935 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { ENTERPRISE_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { DOCS_PREFIX } from '../../../app_search/routes'; +import { NATIVE_AUTH_DOCS_URL, STANDARD_AUTH_DOCS_URL } from '../../../app_search/routes'; import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; @@ -23,8 +23,8 @@ export const SetupGuide: React.FC = () => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.test.ts index cbd7a1c6107b..b14af1c69795 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.test.ts @@ -5,27 +5,23 @@ * 2.0. */ +import { docLinksServiceMock } from '../../../../../../../src/core/public/mocks'; + import { docLinks } from './'; describe('DocLinks', () => { it('setDocLinks', () => { const links = { - DOC_LINK_VERSION: '', - ELASTIC_WEBSITE_URL: 'https://elastic.co/', - links: { - enterpriseSearch: { - base: 'http://elastic.enterprise.search', - appSearchBase: 'http://elastic.app.search', - workplaceSearchBase: 'http://elastic.workplace.search', - }, - }, + DOC_LINK_VERSION: docLinksServiceMock.createStartContract().DOC_LINK_VERSION, + ELASTIC_WEBSITE_URL: docLinksServiceMock.createStartContract().ELASTIC_WEBSITE_URL, + links: docLinksServiceMock.createStartContract().links, }; docLinks.setDocLinks(links as any); - expect(docLinks.enterpriseSearchBase).toEqual('http://elastic.enterprise.search'); - expect(docLinks.appSearchBase).toEqual('http://elastic.app.search'); - expect(docLinks.workplaceSearchBase).toEqual('http://elastic.workplace.search'); - expect(docLinks.cloudBase).toEqual('https://elastic.co/guide/en/cloud/current'); + expect(docLinks.appSearchApis).toEqual(links.links.appSearch.apiRef); + expect(docLinks.cloudIndexManagement).toEqual(links.links.cloud.indexManagement); + expect(docLinks.enterpriseSearchConfig).toEqual(links.links.enterpriseSearch.configuration); + expect(docLinks.workplaceSearchZendesk).toEqual(links.links.workplaceSearch.zendesk); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts index 6034846fac4f..93bead4d31f4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts @@ -8,23 +8,174 @@ import { DocLinksStart } from 'kibana/public'; class DocLinks { - public enterpriseSearchBase: string; - public appSearchBase: string; - public workplaceSearchBase: string; - public cloudBase: string; + public appSearchApis: string; + public appSearchApiClients: string; + public appSearchApiKeys: string; + public appSearchAuthentication: string; + public appSearchCrawlRules: string; + public appSearchCurations: string; + public appSearchDuplicateDocuments: string; + public appSearchEntryPoints: string; + public appSearchGuide: string; + public appSearchIndexingDocs: string; + public appSearchIndexingDocsSchema: string; + public appSearchLogSettings: string; + public appSearchMetaEngines: string; + public appSearchNativeAuth: string; + public appSearchPrecision: string; + public appSearchRelevance: string; + public appSearchResultSettings: string; + public appSearchSearchUI: string; + public appSearchSecurity: string; + public appSearchStandardAuth: string; + public appSearchSynonyms: string; + public appSearchWebCrawler: string; + public appSearchWebCrawlerEventLogs: string; + public cloudIndexManagement: string; + public enterpriseSearchConfig: string; + public enterpriseSearchMailService: string; + public enterpriseSearchUsersAccess: string; + public licenseManagement: string; + public workplaceSearchBox: string; + public workplaceSearchConfluenceCloud: string; + public workplaceSearchConfluenceServer: string; + public workplaceSearchCustomSources: string; + public workplaceSearchCustomSourcePermissions: string; + public workplaceSearchDocumentPermissions: string; + public workplaceSearchDropbox: string; + public workplaceSearchExternalIdentities: string; + public workplaceSearchGettingStarted: string; + public workplaceSearchGitHub: string; + public workplaceSearchGmail: string; + public workplaceSearchGoogleDrive: string; + public workplaceSearchIndexingSchedule: string; + public workplaceSearchJiraCloud: string; + public workplaceSearchJiraServer: string; + public workplaceSearchNativeAuth: string; + public workplaceSearchOneDrive: string; + public workplaceSearchPermissions: string; + public workplaceSearchSalesforce: string; + public workplaceSearchSecurity: string; + public workplaceSearchServiceNow: string; + public workplaceSearchSharePoint: string; + public workplaceSearchSlack: string; + public workplaceSearchStandardAuth: string; + public workplaceSearchSynch: string; + public workplaceSearchZendesk: string; constructor() { - this.enterpriseSearchBase = ''; - this.appSearchBase = ''; - this.workplaceSearchBase = ''; - this.cloudBase = ''; + this.appSearchApis = ''; + this.appSearchApiClients = ''; + this.appSearchApiKeys = ''; + this.appSearchAuthentication = ''; + this.appSearchCrawlRules = ''; + this.appSearchCurations = ''; + this.appSearchDuplicateDocuments = ''; + this.appSearchEntryPoints = ''; + this.appSearchGuide = ''; + this.appSearchIndexingDocs = ''; + this.appSearchIndexingDocsSchema = ''; + this.appSearchLogSettings = ''; + this.appSearchMetaEngines = ''; + this.appSearchNativeAuth = ''; + this.appSearchPrecision = ''; + this.appSearchRelevance = ''; + this.appSearchResultSettings = ''; + this.appSearchSearchUI = ''; + this.appSearchSecurity = ''; + this.appSearchStandardAuth = ''; + this.appSearchSynonyms = ''; + this.appSearchWebCrawler = ''; + this.appSearchWebCrawlerEventLogs = ''; + this.cloudIndexManagement = ''; + this.enterpriseSearchConfig = ''; + this.enterpriseSearchMailService = ''; + this.enterpriseSearchUsersAccess = ''; + this.licenseManagement = ''; + this.workplaceSearchBox = ''; + this.workplaceSearchConfluenceCloud = ''; + this.workplaceSearchConfluenceServer = ''; + this.workplaceSearchCustomSources = ''; + this.workplaceSearchCustomSourcePermissions = ''; + this.workplaceSearchDocumentPermissions = ''; + this.workplaceSearchDropbox = ''; + this.workplaceSearchExternalIdentities = ''; + this.workplaceSearchGettingStarted = ''; + this.workplaceSearchGitHub = ''; + this.workplaceSearchGmail = ''; + this.workplaceSearchGoogleDrive = ''; + this.workplaceSearchIndexingSchedule = ''; + this.workplaceSearchJiraCloud = ''; + this.workplaceSearchJiraServer = ''; + this.workplaceSearchNativeAuth = ''; + this.workplaceSearchOneDrive = ''; + this.workplaceSearchPermissions = ''; + this.workplaceSearchSalesforce = ''; + this.workplaceSearchSecurity = ''; + this.workplaceSearchServiceNow = ''; + this.workplaceSearchSharePoint = ''; + this.workplaceSearchSlack = ''; + this.workplaceSearchStandardAuth = ''; + this.workplaceSearchSynch = ''; + this.workplaceSearchZendesk = ''; } public setDocLinks(docLinks: DocLinksStart): void { - this.enterpriseSearchBase = docLinks.links.enterpriseSearch.base; - this.appSearchBase = docLinks.links.enterpriseSearch.appSearchBase; - this.workplaceSearchBase = docLinks.links.enterpriseSearch.workplaceSearchBase; - this.cloudBase = `${docLinks.ELASTIC_WEBSITE_URL}guide/en/cloud/current`; + this.appSearchApis = docLinks.links.appSearch.apiRef; + this.appSearchApiClients = docLinks.links.appSearch.apiClients; + this.appSearchApiKeys = docLinks.links.appSearch.apiKeys; + this.appSearchAuthentication = docLinks.links.appSearch.authentication; + this.appSearchCrawlRules = docLinks.links.appSearch.crawlRules; + this.appSearchCurations = docLinks.links.appSearch.curations; + this.appSearchDuplicateDocuments = docLinks.links.appSearch.duplicateDocuments; + this.appSearchEntryPoints = docLinks.links.appSearch.entryPoints; + this.appSearchGuide = docLinks.links.appSearch.guide; + this.appSearchIndexingDocs = docLinks.links.appSearch.indexingDocuments; + this.appSearchIndexingDocsSchema = docLinks.links.appSearch.indexingDocumentsSchema; + this.appSearchLogSettings = docLinks.links.appSearch.logSettings; + this.appSearchMetaEngines = docLinks.links.appSearch.metaEngines; + this.appSearchNativeAuth = docLinks.links.appSearch.nativeAuth; + this.appSearchPrecision = docLinks.links.appSearch.precisionTuning; + this.appSearchRelevance = docLinks.links.appSearch.relevanceTuning; + this.appSearchResultSettings = docLinks.links.appSearch.resultSettings; + this.appSearchSearchUI = docLinks.links.appSearch.searchUI; + this.appSearchSecurity = docLinks.links.appSearch.security; + this.appSearchStandardAuth = docLinks.links.appSearch.standardAuth; + this.appSearchSynonyms = docLinks.links.appSearch.synonyms; + this.appSearchWebCrawler = docLinks.links.appSearch.webCrawler; + this.appSearchWebCrawlerEventLogs = docLinks.links.appSearch.webCrawlerEventLogs; + this.cloudIndexManagement = docLinks.links.cloud.indexManagement; + this.enterpriseSearchConfig = docLinks.links.enterpriseSearch.configuration; + this.enterpriseSearchMailService = docLinks.links.enterpriseSearch.mailService; + this.enterpriseSearchUsersAccess = docLinks.links.enterpriseSearch.usersAccess; + this.licenseManagement = docLinks.links.enterpriseSearch.licenseManagement; + this.workplaceSearchBox = docLinks.links.workplaceSearch.box; + this.workplaceSearchConfluenceCloud = docLinks.links.workplaceSearch.confluenceCloud; + this.workplaceSearchConfluenceServer = docLinks.links.workplaceSearch.confluenceServer; + this.workplaceSearchCustomSources = docLinks.links.workplaceSearch.customSources; + this.workplaceSearchCustomSourcePermissions = + docLinks.links.workplaceSearch.customSourcePermissions; + this.workplaceSearchDocumentPermissions = docLinks.links.workplaceSearch.documentPermissions; + this.workplaceSearchDropbox = docLinks.links.workplaceSearch.dropbox; + this.workplaceSearchExternalIdentities = docLinks.links.workplaceSearch.externalIdentities; + this.workplaceSearchGettingStarted = docLinks.links.workplaceSearch.gettingStarted; + this.workplaceSearchGitHub = docLinks.links.workplaceSearch.gitHub; + this.workplaceSearchGmail = docLinks.links.workplaceSearch.gmail; + this.workplaceSearchGoogleDrive = docLinks.links.workplaceSearch.googleDrive; + this.workplaceSearchIndexingSchedule = docLinks.links.workplaceSearch.indexingSchedule; + this.workplaceSearchJiraCloud = docLinks.links.workplaceSearch.jiraCloud; + this.workplaceSearchJiraServer = docLinks.links.workplaceSearch.jiraServer; + this.workplaceSearchNativeAuth = docLinks.links.workplaceSearch.nativeAuth; + this.workplaceSearchOneDrive = docLinks.links.workplaceSearch.oneDrive; + this.workplaceSearchPermissions = docLinks.links.workplaceSearch.permissions; + this.workplaceSearchSalesforce = docLinks.links.workplaceSearch.salesforce; + this.workplaceSearchSecurity = docLinks.links.workplaceSearch.security; + this.workplaceSearchServiceNow = docLinks.links.workplaceSearch.serviceNow; + this.workplaceSearchSharePoint = docLinks.links.workplaceSearch.sharePoint; + this.workplaceSearchSlack = docLinks.links.workplaceSearch.slack; + this.workplaceSearchStandardAuth = docLinks.links.workplaceSearch.standardAuth; + this.workplaceSearchSynch = docLinks.links.workplaceSearch.synch; + this.workplaceSearchZendesk = docLinks.links.workplaceSearch.zendesk; } } diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx index 1877a4cbd0e4..07c71def01be 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx @@ -13,6 +13,8 @@ import { shallow } from 'enzyme'; import { EuiButton } from '@elastic/eui'; +import { docLinks } from '../../shared/doc_links'; + import { EuiButtonTo } from '../react_router_helpers'; import { ManageLicenseButton } from './'; @@ -35,7 +37,7 @@ describe('ManageLicenseButton', () => { const wrapper = shallow(); expect(wrapper.find(EuiButton).prop('href')).toEqual( - expect.stringContaining('/license-management.html') + expect.stringContaining(docLinks.licenseManagement) ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx index af3b33e3d7a3..d0fe98a7c139 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx @@ -27,12 +27,7 @@ export const ManageLicenseButton: React.FC = (props) => { })} ) : ( - + {i18n.translate('xpack.enterpriseSearch.licenseDocumentationLink', { defaultMessage: 'Learn more about license features', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx index 6e213edf457b..667980d5f049 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx @@ -18,7 +18,7 @@ import { RoleRules } from '../types'; import './role_mappings_table.scss'; -const AUTH_PROVIDER_DOCUMENTATION_URL = `${docLinks.enterpriseSearchBase}/users-access.html`; +const AUTH_PROVIDER_DOCUMENTATION_URL = `${docLinks.enterpriseSearchUsersAccess}`; import { ANY_AUTH_PROVIDER, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx index 25aff5077c68..077ef44c66b2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx @@ -24,7 +24,7 @@ import { Role as WSRole } from '../../workplace_search/types'; import { USERNAME_LABEL, EMAIL_LABEL } from '../constants'; import { docLinks } from '../doc_links'; -const SMTP_URL = `${docLinks.enterpriseSearchBase}/mailer-configuration.html`; +const SMTP_URL = `${docLinks.enterpriseSearchMailService}`; import { NEW_USER_LABEL, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.tsx index 42bf690c388c..56e0a325aafd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.tsx @@ -20,7 +20,7 @@ import { docLinks } from '../doc_links'; import { NO_USERS_TITLE, NO_USERS_DESCRIPTION, ENABLE_USERS_LINK } from './constants'; -const USERS_DOCS_URL = `${docLinks.enterpriseSearchBase}/users-access.html`; +const USERS_DOCS_URL = `${docLinks.enterpriseSearchUsersAccess}`; export const UsersEmptyPrompt: React.FC = () => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx index 4845d682b877..8d41e221a2cc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx @@ -80,10 +80,7 @@ export const CloudSetupInstructions: React.FC = ({ productName, cloudDepl defaultMessage="After enabling Enterprise Search for your instance you can customize the instance, including fault tolerance, RAM, and other {optionsLink}." values={{ optionsLink: ( - + {i18n.translate( 'xpack.enterpriseSearch.setupGuide.cloud.step3.instruction1LinkText', { defaultMessage: 'configurable options' } @@ -125,10 +122,7 @@ export const CloudSetupInstructions: React.FC = ({ productName, cloudDepl values={{ productName, configurePolicyLink: ( - + {i18n.translate( 'xpack.enterpriseSearch.setupGuide.cloud.step5.instruction1LinkText', { defaultMessage: 'configure an index lifecycle policy' } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 1be152ad5ca0..b28343f37ea2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -17,37 +17,36 @@ export const LOGOUT_ROUTE = '/logout'; export const LEAVE_FEEDBACK_EMAIL = 'support@elastic.co'; export const LEAVE_FEEDBACK_URL = `mailto:${LEAVE_FEEDBACK_EMAIL}?Subject=Elastic%20Workplace%20Search%20Feedback`; -export const DOCS_PREFIX = docLinks.workplaceSearchBase; -export const PERMISSIONS_DOCS_URL = `${DOCS_PREFIX}/workplace-search-permissions.html`; -export const DOCUMENT_PERMISSIONS_DOCS_URL = `${DOCS_PREFIX}/workplace-search-sources-document-permissions.html`; -export const DOCUMENT_PERMISSIONS_SYNC_DOCS_URL = `${DOCUMENT_PERMISSIONS_DOCS_URL}#sources-permissions-synchronizing`; -export const PRIVATE_SOURCES_DOCS_URL = `${PERMISSIONS_DOCS_URL}#organizational-sources-private-sources`; -export const EXTERNAL_IDENTITIES_DOCS_URL = `${DOCS_PREFIX}/workplace-search-external-identities-api.html`; -export const SECURITY_DOCS_URL = `${DOCS_PREFIX}/workplace-search-security.html`; -export const SMTP_DOCS_URL = `${DOCS_PREFIX}/workplace-search-smtp-mailer.html`; -export const BOX_DOCS_URL = `${DOCS_PREFIX}/workplace-search-box-connector.html`; -export const CONFLUENCE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-confluence-cloud-connector.html`; -export const CONFLUENCE_SERVER_DOCS_URL = `${DOCS_PREFIX}/workplace-search-confluence-server-connector.html`; -export const DROPBOX_DOCS_URL = `${DOCS_PREFIX}/workplace-search-dropbox-connector.html`; -export const GITHUB_DOCS_URL = `${DOCS_PREFIX}/workplace-search-github-connector.html`; -export const GITHUB_ENTERPRISE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-github-connector.html`; -export const GMAIL_DOCS_URL = `${DOCS_PREFIX}/workplace-search-gmail-connector.html`; -export const GOOGLE_DRIVE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-google-drive-connector.html`; -export const JIRA_DOCS_URL = `${DOCS_PREFIX}/workplace-search-jira-cloud-connector.html`; -export const JIRA_SERVER_DOCS_URL = `${DOCS_PREFIX}/workplace-search-jira-server-connector.html`; -export const ONEDRIVE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-onedrive-connector.html`; -export const SALESFORCE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-salesforce-connector.html`; -export const SERVICENOW_DOCS_URL = `${DOCS_PREFIX}/workplace-search-servicenow-connector.html`; -export const SHAREPOINT_DOCS_URL = `${DOCS_PREFIX}/workplace-search-sharepoint-online-connector.html`; -export const SLACK_DOCS_URL = `${DOCS_PREFIX}/workplace-search-slack-connector.html`; -export const ZENDESK_DOCS_URL = `${DOCS_PREFIX}/workplace-search-zendesk-connector.html`; -export const CUSTOM_SOURCE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-custom-api-sources.html`; -export const CUSTOM_API_DOCS_URL = `${DOCS_PREFIX}/workplace-search-custom-sources-api.html`; -export const CUSTOM_API_DOCUMENT_PERMISSIONS_DOCS_URL = `${CUSTOM_SOURCE_DOCS_URL}#custom-api-source-document-level-access-control`; -export const ENT_SEARCH_LICENSE_MANAGEMENT = `${docLinks.enterpriseSearchBase}/license-management.html`; -export const SYNCHRONIZATION_DOCS_URL = `${DOCS_PREFIX}}/workplace-search-customizing-indexing-rules.html#workplace-search-customizing-indexing-rules`; -export const DIFFERENT_SYNC_TYPES_DOCS_URL = `${DOCS_PREFIX}}/workplace-search-customizing-indexing-rules.html#_indexing_schedule`; -export const OBJECTS_AND_ASSETS_DOCS_URL = `${DOCS_PREFIX}}/workplace-search-customizing-indexing-rules.html#workplace-search-customizing-indexing-rules`; +export const BOX_DOCS_URL = docLinks.workplaceSearchBox; +export const CONFLUENCE_DOCS_URL = docLinks.workplaceSearchConfluenceCloud; +export const CONFLUENCE_SERVER_DOCS_URL = docLinks.workplaceSearchConfluenceServer; +export const CUSTOM_SOURCE_DOCS_URL = docLinks.workplaceSearchCustomSources; +export const CUSTOM_API_DOCUMENT_PERMISSIONS_DOCS_URL = + docLinks.workplaceSearchCustomSourcePermissions; +export const DIFFERENT_SYNC_TYPES_DOCS_URL = docLinks.workplaceSearchIndexingSchedule; +export const DOCUMENT_PERMISSIONS_DOCS_URL = docLinks.workplaceSearchDocumentPermissions; +export const DROPBOX_DOCS_URL = docLinks.workplaceSearchDropbox; +export const ENT_SEARCH_LICENSE_MANAGEMENT = docLinks.licenseManagement; +export const EXTERNAL_IDENTITIES_DOCS_URL = docLinks.workplaceSearchExternalIdentities; +export const GETTING_STARTED_DOCS_URL = docLinks.workplaceSearchGettingStarted; +export const GITHUB_DOCS_URL = docLinks.workplaceSearchGitHub; +export const GITHUB_ENTERPRISE_DOCS_URL = docLinks.workplaceSearchGitHub; +export const GMAIL_DOCS_URL = docLinks.workplaceSearchGmail; +export const GOOGLE_DRIVE_DOCS_URL = docLinks.workplaceSearchGoogleDrive; +export const JIRA_DOCS_URL = docLinks.workplaceSearchJiraCloud; +export const JIRA_SERVER_DOCS_URL = docLinks.workplaceSearchJiraServer; +export const NATIVE_AUTH_DOCS_URL = docLinks.workplaceSearchNativeAuth; +export const OBJECTS_AND_ASSETS_DOCS_URL = docLinks.workplaceSearchSynch; +export const ONEDRIVE_DOCS_URL = docLinks.workplaceSearchOneDrive; +export const PRIVATE_SOURCES_DOCS_URL = docLinks.workplaceSearchPermissions; +export const SALESFORCE_DOCS_URL = docLinks.workplaceSearchSalesforce; +export const SECURITY_DOCS_URL = docLinks.workplaceSearchSecurity; +export const SERVICENOW_DOCS_URL = docLinks.workplaceSearchServiceNow; +export const SHAREPOINT_DOCS_URL = docLinks.workplaceSearchSharePoint; +export const SLACK_DOCS_URL = docLinks.workplaceSearchSlack; +export const STANDARD_AUTH_DOCS_URL = docLinks.workplaceSearchStandardAuth; +export const SYNCHRONIZATION_DOCS_URL = docLinks.workplaceSearchSynch; +export const ZENDESK_DOCS_URL = docLinks.workplaceSearchZendesk; export const PERSONAL_PATH = '/p'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx index 4d329ff357b8..a992cf49f75f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx @@ -124,9 +124,9 @@ describe('OauthApplication', () => { `); }); + /* This href test should ultimately use the docLinkServiceMock */ it('renders description', () => { const wrapper = shallow(); - expect(wrapper.prop('pageHeader').description).toMatchInlineSnapshot(` { Explore Platinum features diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx index 905ba20e4f66..e52a174850c4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx @@ -15,19 +15,23 @@ import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; -import { DOCS_PREFIX } from '../../routes'; +import { + GETTING_STARTED_DOCS_URL, + NATIVE_AUTH_DOCS_URL, + STANDARD_AUTH_DOCS_URL, +} from '../../routes'; import GettingStarted from './assets/getting_started.png'; -const GETTING_STARTED_LINK_URL = `${DOCS_PREFIX}/workplace-search-getting-started.html`; +const GETTING_STARTED_LINK_URL = GETTING_STARTED_DOCS_URL; export const SetupGuide: React.FC = () => { return ( From 50c02645cf32eded853f934e3cfe24d0d6daaaee Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Mon, 29 Nov 2021 11:46:58 -0800 Subject: [PATCH 029/224] [DOCS] Changes index pattern to data views in intro docs (#119403) * [DOCS] Changes index pattern to data views in intro docs * [DOCS] Updates image of data views UI * [DOCS] Removes faulty sentence * [DOCS] removes sentence about index patterns Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/concepts/data-views.asciidoc | 8 ++++---- docs/concepts/index.asciidoc | 5 +---- docs/concepts/save-query.asciidoc | 2 +- docs/concepts/set-time-filter.asciidoc | 2 +- .../getting-started/quick-start-guide.asciidoc | 8 ++++---- .../index-patterns/images/create-data-view.png | Bin 0 -> 190415 bytes .../images/create-index-pattern.png | Bin 161561 -> 0 bytes 7 files changed, 11 insertions(+), 14 deletions(-) create mode 100644 docs/management/index-patterns/images/create-data-view.png delete mode 100644 docs/management/index-patterns/images/create-index-pattern.png diff --git a/docs/concepts/data-views.asciidoc b/docs/concepts/data-views.asciidoc index 7eb95405db6b..954581faa246 100644 --- a/docs/concepts/data-views.asciidoc +++ b/docs/concepts/data-views.asciidoc @@ -1,7 +1,7 @@ [[data-views]] === Create a data view -{kib} requires a data view to access the {es} data that you want to explore. +{kib} requires a data view to access the {es} data that you want to explore. A data view selects the data to use and allows you to define properties of the fields. A data view can point to one or more indices, {ref}/data-streams.html[data stream], or {ref}/alias.html[index aliases]. @@ -37,7 +37,7 @@ If you loaded your own data, follow these steps to create a data view. . Click *Create data view*. [role="screenshot"] -image:management/index-patterns/images/create-index-pattern.png["Create data view"] +image:management/index-patterns/images/create-data-view.png["Create data view"] . Start typing in the *name* field, and {kib} looks for the names of indices, data streams, and aliases that match your input. @@ -87,11 +87,11 @@ For an example, refer to <: +: ``` To query {ls} indices across two {es} clusters diff --git a/docs/concepts/index.asciidoc b/docs/concepts/index.asciidoc index eac26beee1f9..457251e62ae8 100644 --- a/docs/concepts/index.asciidoc +++ b/docs/concepts/index.asciidoc @@ -40,8 +40,6 @@ image:concepts/images/global-search.png["Global search showing matches to apps a {kib} requires a data view to tell it which {es} data you want to access, and whether the data is time-based. A data view can point to one or more {es} data streams, indices, or index aliases by name. -For example, `logs-elasticsearch-prod-*` is an index pattern, -and it is time-based with a time field of `@timestamp`. The time field is not editable. Data views are typically created by an administrator when sending data to {es}. You can <> in *Stack Management*, or by using a script @@ -129,8 +127,7 @@ Previously, {kib} used the {ref}/search-aggregations-bucket-terms-aggregation.ht Structured filters are a more interactive way to create {es} queries, and are commonly used when building dashboards that are shared by multiple analysts. Each filter can be disabled, inverted, or pinned across all apps. -The structured filters are the only way to use the {es} Query DSL in JSON form, -or to target a specific index pattern for filtering. Each of the structured +Each of the structured filters is combined with AND logic on the rest of the query. [role="screenshot"] diff --git a/docs/concepts/save-query.asciidoc b/docs/concepts/save-query.asciidoc index 61113b5491c2..54137d1f9f2c 100644 --- a/docs/concepts/save-query.asciidoc +++ b/docs/concepts/save-query.asciidoc @@ -17,7 +17,7 @@ image:concepts/images/saved-query.png["Example of the saved query management pop Saved queries are different than <>, which include the *Discover* configuration—selected columns in the document table, sort order, and -index pattern—in addition to the query. +{data-source}—in addition to the query. Saved searches are primarily used for adding search results to a dashboard. [role="xpack"] diff --git a/docs/concepts/set-time-filter.asciidoc b/docs/concepts/set-time-filter.asciidoc index 116bcd6f91f7..b379c0ac279e 100644 --- a/docs/concepts/set-time-filter.asciidoc +++ b/docs/concepts/set-time-filter.asciidoc @@ -2,7 +2,7 @@ === Set the time range Display data within a specified time range when your index contains time-based events, and a time-field is configured for the -selected <>. +selected <>. The default time range is 15 minutes, but you can customize it in <>. diff --git a/docs/getting-started/quick-start-guide.asciidoc b/docs/getting-started/quick-start-guide.asciidoc index 03e40c7cc6ce..2667729f4b85 100644 --- a/docs/getting-started/quick-start-guide.asciidoc +++ b/docs/getting-started/quick-start-guide.asciidoc @@ -11,7 +11,7 @@ When you've finished, you'll know how to: [float] === Required privileges -You must have `read`, `write`, and `manage` privileges on the `kibana_sample_data_*` indices. +You must have `read`, `write`, and `manage` privileges on the `kibana_sample_data_*` indices. Learn how to <>, or refer to {ref}/security-privileges.html[Security privileges] for more information. [float] @@ -37,7 +37,7 @@ image::images/addData_sampleDataCards_7.15.0.png[Add data UI for the sample data [[explore-the-data]] == Explore the data -*Discover* displays the data in an interactive histogram that shows the distribution of data, or documents, over time, and a table that lists the fields for each document that matches the index pattern. To view a subset of the documents, you can apply filters to the data, and customize the table to display only the fields you want to explore. +*Discover* displays the data in an interactive histogram that shows the distribution of data, or documents, over time, and a table that lists the fields for each document that matches the {data-source}. To view a subset of the documents, you can apply filters to the data, and customize the table to display only the fields you want to explore. . Open the main menu, then click *Discover*. @@ -65,7 +65,7 @@ image::images/tutorial-discover-3.png[Discover table that displays only the prod A dashboard is a collection of panels that you can use to view and analyze the data. Panels contain visualizations, interactive controls, text, and more. -. Open the main menu, then click *Dashboard*. +. Open the main menu, then click *Dashboard*. . Click *[eCommerce] Revenue Dashboard*. + @@ -104,7 +104,7 @@ The treemap appears as the last visualization panel on the dashboard. [[interact-with-the-data]] === Interact with the data -You can interact with the dashboard data using controls that allow you to apply dashboard-level filters. Interact with the *[eCommerce] Controls* panel to view the women's clothing data from the Gnomehouse manufacturer. +You can interact with the dashboard data using controls that allow you to apply dashboard-level filters. Interact with the *[eCommerce] Controls* panel to view the women's clothing data from the Gnomehouse manufacturer. . From the *Manufacturer* dropdown, select *Gnomehouse*. diff --git a/docs/management/index-patterns/images/create-data-view.png b/docs/management/index-patterns/images/create-data-view.png new file mode 100644 index 0000000000000000000000000000000000000000..229ed0f490b41fe2db1e1d70d7b6f77f097f2cf0 GIT binary patch literal 190415 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%0D^9sd$$>dxb=F{DY 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�MQt+w;r5Yb|Ni)8Q40vtdUb_ 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ÄN4{Sy8q0 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 z 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%0DeL0P@Y-n?OYz$490` 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 z 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 From 25936e7ac3a41effc87c1f6bc4c9cf2e194cc24b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Mon, 29 Nov 2021 15:04:03 -0500 Subject: [PATCH 030/224] Fix e2e tests when running it locally (#119747) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../apis/fixtures/package_registry_config.yml | 4 +++ x-pack/plugins/apm/ftr_e2e/ftr_config.ts | 10 ++++++- x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts | 27 ++++++++++++++++++- 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/apm/ftr_e2e/apis/fixtures/package_registry_config.yml diff --git a/x-pack/plugins/apm/ftr_e2e/apis/fixtures/package_registry_config.yml b/x-pack/plugins/apm/ftr_e2e/apis/fixtures/package_registry_config.yml new file mode 100644 index 000000000000..9f2300dedc82 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/apis/fixtures/package_registry_config.yml @@ -0,0 +1,4 @@ +package_paths: + - /packages/production + - /packages/snapshot + - /packages/test-packages diff --git a/x-pack/plugins/apm/ftr_e2e/ftr_config.ts b/x-pack/plugins/apm/ftr_e2e/ftr_config.ts index 12cc8845264c..84d1c40930c7 100644 --- a/x-pack/plugins/apm/ftr_e2e/ftr_config.ts +++ b/x-pack/plugins/apm/ftr_e2e/ftr_config.ts @@ -6,8 +6,11 @@ */ import { FtrConfigProviderContext } from '@kbn/test'; - import { CA_CERT_PATH } from '@kbn/dev-utils'; + +// Used to spin up a docker container with package registry service that will be used by fleet +export const packageRegistryPort = 1234; + async function config({ readConfigFile }: FtrConfigProviderContext) { const kibanaCommonTestsConfig = await readConfigFile( require.resolve('../../../../test/common/config.js') @@ -38,6 +41,11 @@ async function config({ readConfigFile }: FtrConfigProviderContext) { '--csp.warnLegacyBrowsers=false', // define custom kibana server args here `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + + // Fleet config + `--xpack.fleet.packages.0.name=endpoint`, + `--xpack.fleet.packages.0.version=latest`, + `--xpack.fleet.registryUrl=http://localhost:${packageRegistryPort}`, ], }, }; diff --git a/x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts b/x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts index 51c859a8477f..a5a0b52e3fbe 100644 --- a/x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts +++ b/x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts @@ -5,16 +5,41 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; +import { defineDockerServersConfig, FtrConfigProviderContext } from '@kbn/test'; import cypress from 'cypress'; +import path from 'path'; import { cypressStart } from './cypress_start'; +import { packageRegistryPort } from './ftr_config'; import { FtrProviderContext } from './ftr_provider_context'; +export const dockerImage = + 'docker.elastic.co/package-registry/distribution@sha256:13d9996dd24161624784704e080f5f5b7f0ef34ff0d9259f8f05010ccae00058'; + async function ftrConfigRun({ readConfigFile }: FtrConfigProviderContext) { const kibanaConfig = await readConfigFile(require.resolve('./ftr_config.ts')); + + // mount the config file for the package registry + const dockerArgs: string[] = [ + '-v', + `${path.join( + path.dirname(__filename), + './apis/fixtures/package_registry_config.yml' + )}:/package-registry/config.yml`, + ]; + return { ...kibanaConfig.getAll(), testRunner, + dockerServers: defineDockerServersConfig({ + registry: { + enabled: true, + image: dockerImage, + portInContainer: 8080, + port: packageRegistryPort, + args: dockerArgs, + waitForLogLine: 'package manifests loaded', + }, + }), }; } From a468b9850bece38ad99d13207be77c0ccc3feba1 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Mon, 29 Nov 2021 14:06:18 -0600 Subject: [PATCH 031/224] [DOCS] Fixes Lens typo (#119886) --- docs/user/dashboard/lens.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 23a6d1fbcfd3..1b0bbf866b85 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -249,7 +249,7 @@ In the legend, click the field, then choose one of the following options: [[configure-the-visualization-components]] ==== Configure the visualization components -Each visualiztion type comes with a set of components that you access from the editor toolbar. +Each visualization type comes with a set of components that you access from the editor toolbar. The following component menus are available: From 2f73bd98929f3aea5000d0d2545fcf8e41ed356a Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Mon, 29 Nov 2021 15:22:53 -0500 Subject: [PATCH 032/224] Be more explicit with which events we want (#119875) --- .../spaces_only/tests/alerting/event_log.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index 9bf7baf95d8d..806c1fa3a4ea 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -490,8 +490,11 @@ export default function eventLogTests({ getService }: FtrProviderContext) { }); }); - const startEvent = events[0]; - const executeEvent = events[1]; + const executeEvents = getEventsByAction(events, 'execute'); + const executeStartEvents = getEventsByAction(events, 'execute-start'); + + const startEvent = executeStartEvents[0]; + const executeEvent = executeEvents[0]; expect(startEvent).to.be.ok(); expect(executeEvent).to.be.ok(); From b2cc0b347cd77c3ccba7509a54e601b1cc649341 Mon Sep 17 00:00:00 2001 From: liza-mae Date: Mon, 29 Nov 2021 13:46:03 -0700 Subject: [PATCH 033/224] Fix dashboard and maps test (#119880) --- x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts | 2 +- x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts b/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts index c98336107117..2f35f0e1e12d 100644 --- a/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts +++ b/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts @@ -25,7 +25,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ]; const dashboardTests = [ - { name: 'flights', numPanels: 17 }, + { name: 'flights', numPanels: 16 }, { name: 'logs', numPanels: 10 }, { name: 'ecommerce', numPanels: 11 }, ]; diff --git a/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts b/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts index 53acb8b01631..22e081e88bfc 100644 --- a/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts +++ b/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts @@ -167,7 +167,7 @@ export default function ({ ); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.maps.waitForLayersToLoad(); - await PageObjects.maps.toggleLayerVisibility('Road map - desaturated'); + await PageObjects.maps.toggleLayerVisibility('Road map'); await PageObjects.maps.toggleLayerVisibility('Total Requests by Country'); await PageObjects.timePicker.setCommonlyUsedTime('sample_data range'); await PageObjects.maps.enterFullScreen(); From b5a30fc3fe59feaeb9c78e28763714d7d0e89511 Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Mon, 29 Nov 2021 21:55:31 +0100 Subject: [PATCH 034/224] Add possibility to update threat_indicator_path for prebuilt rule (#116583) * Add possibility to update threat_indicator_path for prebuiltt rule * Fix types * adds update_prepacked_rules test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Ece Ozalp --- .../routes/rules/import_rules_route.ts | 1 + .../routes/rules/patch_rules_bulk_route.ts | 2 + .../routes/rules/patch_rules_route.ts | 2 + .../rules/patch_rules.mock.ts | 2 + .../lib/detection_engine/rules/patch_rules.ts | 3 ++ .../lib/detection_engine/rules/types.ts | 1 + .../rules/update_prepacked_rules.test.ts | 38 +++++++++++++++++++ .../rules/update_prepacked_rules.ts | 2 + .../lib/detection_engine/rules/utils.test.ts | 3 ++ .../lib/detection_engine/rules/utils.ts | 2 + 10 files changed, 56 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index 187de40d33df..b056fd3ed80f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -326,6 +326,7 @@ export const importRulesRoute = ( threshold, threatFilters, threatIndex, + threatIndicatorPath, threatQuery, threatMapping, threatLanguage, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 838bfe63782c..9e821c8f686f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -98,6 +98,7 @@ export const patchRulesBulkRoute = ( threshold, threat_filters: threatFilters, threat_index: threatIndex, + threat_indicator_path: threatIndicatorPath, threat_query: threatQuery, threat_mapping: threatMapping, threat_language: threatLanguage, @@ -178,6 +179,7 @@ export const patchRulesBulkRoute = ( threshold, threatFilters, threatIndex, + threatIndicatorPath, threatQuery, threatMapping, threatLanguage, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts index bb9f7e147524..da3e4ccc99b9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -86,6 +86,7 @@ export const patchRulesRoute = ( threshold, threat_filters: threatFilters, threat_index: threatIndex, + threat_indicator_path: threatIndicatorPath, threat_query: threatQuery, threat_mapping: threatMapping, threat_language: threatLanguage, @@ -179,6 +180,7 @@ export const patchRulesRoute = ( threshold, threatFilters, threatIndex, + threatIndicatorPath, threatQuery, threatMapping, threatLanguage, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts index 3626bcd5f127..3a602a54ca09 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts @@ -49,6 +49,7 @@ export const getPatchRulesOptionsMock = (isRuleRegistryEnabled: boolean): PatchR threshold: undefined, threatFilters: undefined, threatIndex: undefined, + threatIndicatorPath: undefined, threatQuery: undefined, threatMapping: undefined, threatLanguage: undefined, @@ -103,6 +104,7 @@ export const getPatchMlRulesOptionsMock = (isRuleRegistryEnabled: boolean): Patc threshold: undefined, threatFilters: undefined, threatIndex: undefined, + threatIndicatorPath: undefined, threatQuery: undefined, threatMapping: undefined, threatLanguage: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts index fd48cd4eebc2..ee3098b8577d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts @@ -71,6 +71,7 @@ export const patchRules = async ({ threshold, threatFilters, threatIndex, + threatIndicatorPath, threatQuery, threatMapping, threatLanguage, @@ -123,6 +124,7 @@ export const patchRules = async ({ threshold, threatFilters, threatIndex, + threatIndicatorPath, threatQuery, threatMapping, threatLanguage, @@ -170,6 +172,7 @@ export const patchRules = async ({ threshold: threshold ? normalizeThresholdObject(threshold) : undefined, threatFilters, threatIndex, + threatIndicatorPath, threatQuery, threatMapping, threatLanguage, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index f847e385e647..06328137973c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -310,6 +310,7 @@ export interface PatchRulesOptions { threshold: ThresholdOrUndefined; threatFilters: ThreatFiltersOrUndefined; threatIndex: ThreatIndexOrUndefined; + threatIndicatorPath: ThreatIndicatorPathOrUndefined; threatQuery: ThreatQueryOrUndefined; threatMapping: ThreatMappingOrUndefined; threatLanguage: ThreatLanguageOrUndefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts index 9bd0fe3cef59..c4cd5d4211c9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts @@ -63,4 +63,42 @@ describe.each([ }) ); }); + + it('should update threat match rules', async () => { + const updatedThreatParams = { + threat_index: ['test-index'], + threat_indicator_path: 'test.path', + threat_query: 'threat:*', + }; + const prepackagedRule = getAddPrepackagedRulesSchemaDecodedMock(); + rulesClient.find.mockResolvedValue(getFindResultWithSingleHit(isRuleRegistryEnabled)); + + await updatePrepackagedRules( + rulesClient, + savedObjectsClient, + 'default', + ruleStatusClient, + [{ ...prepackagedRule, ...updatedThreatParams }], + 'output-index', + isRuleRegistryEnabled + ); + + expect(patchRules).toHaveBeenCalledWith( + expect.objectContaining({ + threatIndicatorPath: 'test.path', + }) + ); + + expect(patchRules).toHaveBeenCalledWith( + expect.objectContaining({ + threatIndex: ['test-index'], + }) + ); + + expect(patchRules).toHaveBeenCalledWith( + expect.objectContaining({ + threatQuery: 'threat:*', + }) + ); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts index dcf43d41e8d7..e24a6a883b6d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -124,6 +124,7 @@ export const createPromises = ( threshold, threat_filters: threatFilters, threat_index: threatIndex, + threat_indicator_path: threatIndicatorPath, threat_query: threatQuery, threat_mapping: threatMapping, threat_language: threatLanguage, @@ -195,6 +196,7 @@ export const createPromises = ( threshold, threatFilters, threatIndex, + threatIndicatorPath, threatQuery, threatMapping, threatLanguage, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts index 2cf7e95f3c62..448d1b1a1db6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts @@ -75,6 +75,7 @@ describe('utils', () => { threshold: undefined, threatFilters: undefined, threatIndex: undefined, + threatIndicatorPath: undefined, threatQuery: undefined, threatMapping: undefined, threatLanguage: undefined, @@ -126,6 +127,7 @@ describe('utils', () => { threshold: undefined, threatFilters: undefined, threatIndex: undefined, + threatIndicatorPath: undefined, threatQuery: undefined, threatMapping: undefined, threatLanguage: undefined, @@ -177,6 +179,7 @@ describe('utils', () => { threshold: undefined, threatFilters: undefined, threatIndex: undefined, + threatIndicatorPath: undefined, threatQuery: undefined, threatMapping: undefined, threatLanguage: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts index ec25b45dd159..4ab8afd796f6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts @@ -15,6 +15,7 @@ import type { ItemsPerSearchOrUndefined, ThreatFiltersOrUndefined, ThreatIndexOrUndefined, + ThreatIndicatorPathOrUndefined, ThreatLanguageOrUndefined, ThreatMappingOrUndefined, ThreatQueryOrUndefined, @@ -113,6 +114,7 @@ export interface UpdateProperties { threshold: ThresholdOrUndefined; threatFilters: ThreatFiltersOrUndefined; threatIndex: ThreatIndexOrUndefined; + threatIndicatorPath: ThreatIndicatorPathOrUndefined; threatQuery: ThreatQueryOrUndefined; threatMapping: ThreatMappingOrUndefined; threatLanguage: ThreatLanguageOrUndefined; From 14894262af6327bb160935f0b65c3ad040671306 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Mon, 29 Nov 2021 16:47:51 -0500 Subject: [PATCH 035/224] [Alerting] Skip running disabled rules (#119239) * Skip running disabled rules * Move to separate file * Add status that reflects a rule unable to run due to not enabled * Add better message * PR feedback --- x-pack/plugins/alerting/common/alert.ts | 1 + .../server/task_runner/task_runner.test.ts | 115 +++++++++++++++++- .../server/task_runner/task_runner.ts | 24 +++- .../task_runner/task_runner_cancel.test.ts | 1 + .../sections/alerts_list/translations.ts | 8 ++ 5 files changed, 143 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/alerting/common/alert.ts b/x-pack/plugins/alerting/common/alert.ts index 4431f185ac9c..8db51e223056 100644 --- a/x-pack/plugins/alerting/common/alert.ts +++ b/x-pack/plugins/alerting/common/alert.ts @@ -31,6 +31,7 @@ export enum AlertExecutionStatusErrorReasons { Unknown = 'unknown', License = 'license', Timeout = 'timeout', + Disabled = 'disabled', } export interface AlertExecutionStatus { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index f70cbaa13f7d..d370a278e0a5 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -212,6 +212,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -399,6 +400,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -642,6 +644,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -842,6 +845,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -913,6 +917,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -1090,6 +1095,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -1158,6 +1164,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -1204,6 +1211,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -1514,6 +1522,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -1872,6 +1881,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -1994,6 +2004,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -2097,6 +2108,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -2300,6 +2312,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -2328,6 +2341,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -2359,7 +2373,9 @@ describe('Task Runner', () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: '1', type: 'alert', - attributes: {}, + attributes: { + enabled: true, + }, references: [], }); @@ -2396,6 +2412,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -2441,6 +2458,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -2667,6 +2685,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -2784,6 +2803,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -2900,6 +2920,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -3020,6 +3041,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -3070,6 +3092,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -3103,6 +3126,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -3144,6 +3168,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -3200,6 +3225,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -3486,6 +3512,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -3692,6 +3719,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -3889,6 +3917,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -4092,6 +4121,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -4266,6 +4296,7 @@ describe('Task Runner', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); @@ -4401,4 +4432,86 @@ describe('Task Runner', () => { { refresh: false, namespace: undefined } ); }); + + test('successfully bails on execution if the rule is disabled', async () => { + const state = { + ...mockedTaskInstance.state, + previousStartedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + }; + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state, + }, + taskRunnerFactoryInitializerParams + ); + rulesClient.get.mockResolvedValue(mockedAlertTypeSavedObject); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + enabled: false, + }, + references: [], + }); + const runnerResult = await taskRunner.run(); + expect(runnerResult.state.previousStartedAt?.toISOString()).toBe(state.previousStartedAt); + expect(runnerResult.schedule).toStrictEqual(mockedTaskInstance.schedule); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(2); + expect(eventLogger.logEvent.mock.calls[0][0]).toStrictEqual({ + '@timestamp': '1970-01-01T00:00:00.000Z', + event: { + action: 'execute-start', + kind: 'alert', + category: ['alerts'], + }, + kibana: { + saved_objects: [ + { rel: 'primary', type: 'alert', id: '1', namespace: undefined, type_id: 'test' }, + ], + task: { scheduled: '1970-01-01T00:00:00.000Z', schedule_delay: 0 }, + }, + rule: { + id: '1', + license: 'basic', + category: 'test', + ruleset: 'alerts', + }, + message: 'alert execution start: "1"', + }); + expect(eventLogger.logEvent.mock.calls[1][0]).toStrictEqual({ + '@timestamp': '1970-01-01T00:00:00.000Z', + event: { + action: 'execute', + kind: 'alert', + category: ['alerts'], + reason: 'disabled', + outcome: 'failure', + }, + kibana: { + saved_objects: [ + { rel: 'primary', type: 'alert', id: '1', namespace: undefined, type_id: 'test' }, + ], + task: { + scheduled: '1970-01-01T00:00:00.000Z', + schedule_delay: 0, + }, + alerting: { status: 'error' }, + }, + rule: { + id: '1', + license: 'basic', + category: 'test', + ruleset: 'alerts', + }, + error: { + message: 'Rule failed to execute because rule ran after it was disabled.', + }, + message: 'test:1: execution failed', + }); + }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index fe95ec646387..fb7268ef529d 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -117,19 +117,22 @@ export class TaskRunner< this.cancelled = false; } - async getApiKeyForAlertPermissions(alertId: string, spaceId: string) { + async getDecryptedAttributes( + ruleId: string, + spaceId: string + ): Promise<{ apiKey: string | null; enabled: boolean }> { const namespace = this.context.spaceIdToNamespace(spaceId); // Only fetch encrypted attributes here, we'll create a saved objects client // scoped with the API key to fetch the remaining data. const { - attributes: { apiKey }, + attributes: { apiKey, enabled }, } = await this.context.encryptedSavedObjectsClient.getDecryptedAsInternalUser( 'alert', - alertId, + ruleId, { namespace } ); - return apiKey; + return { apiKey, enabled }; } private getFakeKibanaRequest(spaceId: string, apiKey: RawAlert['apiKey']) { @@ -516,12 +519,23 @@ export class TaskRunner< const { params: { alertId, spaceId }, } = this.taskInstance; + let enabled: boolean; let apiKey: string | null; try { - apiKey = await this.getApiKeyForAlertPermissions(alertId, spaceId); + const decryptedAttributes = await this.getDecryptedAttributes(alertId, spaceId); + apiKey = decryptedAttributes.apiKey; + enabled = decryptedAttributes.enabled; } catch (err) { throw new ErrorWithReason(AlertExecutionStatusErrorReasons.Decrypt, err); } + + if (!enabled) { + throw new ErrorWithReason( + AlertExecutionStatusErrorReasons.Disabled, + new Error(`Rule failed to execute because rule ran after it was disabled.`) + ); + } + const [services, rulesClient] = this.getServicesWithSpaceLevelPermissions(spaceId, apiKey); let alert: SanitizedAlert; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index 95cb356af3c1..c82cc0a7f21e 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -171,6 +171,7 @@ describe('Task Runner Cancel', () => { type: 'alert', attributes: { apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, }, references: [], }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/translations.ts index 8181a5171d19..293a5e79e29b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/translations.ts @@ -99,6 +99,13 @@ export const ALERT_ERROR_TIMEOUT_REASON = i18n.translate( } ); +export const ALERT_ERROR_DISABLED_REASON = i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.alertErrorReasonDisabled', + { + defaultMessage: 'Rule failed to execute because rule ran after it was disabled.', + } +); + export const alertsErrorReasonTranslationsMapping = { read: ALERT_ERROR_READING_REASON, decrypt: ALERT_ERROR_DECRYPTING_REASON, @@ -106,4 +113,5 @@ export const alertsErrorReasonTranslationsMapping = { unknown: ALERT_ERROR_UNKNOWN_REASON, license: ALERT_ERROR_LICENSE_REASON, timeout: ALERT_ERROR_TIMEOUT_REASON, + disabled: ALERT_ERROR_DISABLED_REASON, }; From 78d957dfc90d3cd436a222d3ce46b9169be3ee59 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Mon, 29 Nov 2021 16:59:01 -0500 Subject: [PATCH 036/224] [Security Solution] Fix broken responsive design for specific cases on endpoint flyout (#119893) --- .../endpoint_hosts/view/details/endpoint_details_content.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx index d9fbbcb5ec25..5f51436daff6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx @@ -31,7 +31,10 @@ import { EndpointAgentStatus } from '../components/endpoint_agent_status'; const EndpointDetailsContentStyled = styled.div` dl dt { - max-width: 220px; + max-width: 27%; + } + dl dd { + max-width: 73%; } .policyLineText { padding-right: 5px; From 3c11fa003721092af181aecb11546c10b475f961 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Mon, 29 Nov 2021 23:04:40 +0100 Subject: [PATCH 037/224] Make fixture app wait until network is idle before running authentication tests. (#119715) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/test_endpoints/public/plugin.tsx | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/x-pack/test/security_functional/fixtures/common/test_endpoints/public/plugin.tsx b/x-pack/test/security_functional/fixtures/common/test_endpoints/public/plugin.tsx index b0264998db17..745a8852968c 100644 --- a/x-pack/test/security_functional/fixtures/common/test_endpoints/public/plugin.tsx +++ b/x-pack/test/security_functional/fixtures/common/test_endpoints/public/plugin.tsx @@ -8,21 +8,34 @@ import type { CoreSetup, Plugin } from 'src/core/public'; import ReactDOM from 'react-dom'; import React from 'react'; +import { debounce, filter, first } from 'rxjs/operators'; +import { timer } from 'rxjs'; export class TestEndpointsPlugin implements Plugin { public setup(core: CoreSetup) { // Prevent auto-logout on server `401` errors. core.http.anonymousPaths.register('/authentication/app'); + + const networkIdle$ = core.http.getLoadingCount$().pipe( + debounce(() => timer(3000)), + filter((count) => count === 0), + first() + ); + core.application.register({ id: 'authentication_app', title: 'Authentication app', appRoute: '/authentication/app', chromeless: true, async mount({ element }) { - ReactDOM.render( -
Authenticated!
, - element - ); + // Promise is resolved as soon there are no requests has been made in the last 3 seconds. We need this to make + // sure none of the unrelated requests interferes with the test logic. + networkIdle$.toPromise().then(() => { + ReactDOM.render( +
Authenticated!
, + element + ); + }); return () => ReactDOM.unmountComponentAtNode(element); }, }); From d0eace257f19a763af5db6dc142c48da652f8311 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 29 Nov 2021 17:15:03 -0500 Subject: [PATCH 038/224] Make spacesWithConflictingAliases assertion resilient (#119858) --- .../common/suites/bulk_create.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts index 45a96f8ebd8b..d53afdbddd6e 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts @@ -109,17 +109,30 @@ export function bulkCreateTestSuiteFactory(esArchiver: any, supertest: SuperTest const { type, id } = testCase; expect(object.type).to.eql(type); expect(object.id).to.eql(id); - let metadata; + let expectedMetadata; if (testCase.fail409Param === 'unresolvableConflict') { - metadata = { isNotOverwritable: true }; + expectedMetadata = { isNotOverwritable: true }; } else if (testCase.fail409Param === 'aliasConflictSpace1') { - metadata = { spacesWithConflictingAliases: ['space_1'] }; + expectedMetadata = { spacesWithConflictingAliases: ['space_1'] }; } else if (testCase.fail409Param === 'aliasConflictAllSpaces') { - metadata = { spacesWithConflictingAliases: ['space_1', 'space_x'] }; + expectedMetadata = { spacesWithConflictingAliases: ['space_1', 'space_x'] }; + } + const expectedError = SavedObjectsErrorHelpers.createConflictError(type, id).output + .payload; + expect(object.error).be.an('object'); + expect(object.error.statusCode).to.eql(expectedError.statusCode); + expect(object.error.error).to.eql(expectedError.error); + expect(object.error.message).to.eql(expectedError.message); + if (expectedMetadata) { + const actualMetadata = object.error.metadata ?? {}; + if (actualMetadata.spacesWithConflictingAliases) { + actualMetadata.spacesWithConflictingAliases = + actualMetadata.spacesWithConflictingAliases.sort(); + } + expect(actualMetadata).to.eql(expectedMetadata); + } else { + expect(object.error.metadata).to.be(undefined); } - const error = SavedObjectsErrorHelpers.createConflictError(type, id); - const payload = { ...error.output.payload, ...(metadata && { metadata }) }; - expect(object.error).to.eql(payload); continue; } await expectResponses.permitted(object, testCase); From 14ad72efe5481a027e70245687a864677a698be6 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 29 Nov 2021 17:44:53 -0500 Subject: [PATCH 039/224] [Uptime] add monitor edit and add pages (#118947) * add monitor edit and add pages * remove unused imports * remove unexpected console log * remove unused values Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../enrollment_instructions/manual/index.tsx | 2 + .../fleet_package/contexts/index.ts | 30 ++++++ .../contexts/policy_config_context.tsx | 40 +++++++- .../contexts/synthetics_context_providers.tsx | 60 ++++++++++++ .../fleet_package/custom_fields.tsx | 4 +- .../fleet_package/hooks/use_policy.ts | 24 ++--- ...hetics_policy_create_extension_wrapper.tsx | 25 +---- .../monitor_management/formatters/browser.ts | 35 +++++++ .../monitor_management/formatters/common.ts | 31 ++++++ .../monitor_management/formatters/http.ts | 37 +++++++ .../monitor_management/formatters/icmp.ts | 17 ++++ .../monitor_management/formatters/index.ts | 38 ++++++++ .../monitor_management/formatters/tcp.ts | 23 +++++ .../monitor_management/formatters/tls.ts | 20 ++++ .../hooks/use_format_monitor.ts | 62 ++++++++++++ .../monitor_management/monitor_config.tsx | 37 +++++++ .../monitor_management/monitor_fields.tsx | 29 ++++++ .../monitor_name_location.tsx | 37 +++++++ .../uptime/public/pages/add_monitor.tsx | 12 ++- .../uptime/public/pages/edit_monitor.tsx | 97 ++++++++++++++++++- 20 files changed, 620 insertions(+), 40 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/contexts/synthetics_context_providers.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/formatters/browser.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/formatters/common.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/formatters/http.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/formatters/icmp.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/formatters/index.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/formatters/tcp.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/formatters/tls.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/hooks/use_format_monitor.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_config.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_fields.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_name_location.tsx diff --git a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx index 32846620221a..16ed53020a31 100644 --- a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx @@ -6,8 +6,10 @@ */ import React from 'react'; + import { useStartServices } from '../../../hooks'; import type { EnrollmentAPIKey } from '../../../types'; + import { PlatformSelector } from './platform_selector'; interface Props { diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts b/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts index df2e9cfa6d4e..37fdad9b195d 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts @@ -4,6 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { DataStream, PolicyConfig } from '../types'; +import { initialValues as defaultHTTPSimpleFields } from './http_context'; +import { initialValues as defaultHTTPAdvancedFields } from './http_context_advanced'; +import { initialValues as defaultTCPSimpleFields } from './tcp_context'; +import { initialValues as defaultICMPSimpleFields } from './icmp_context'; +import { initialValues as defaultTCPAdvancedFields } from './tcp_context_advanced'; +import { initialValues as defaultBrowserSimpleFields } from './browser_context'; +import { initialValues as defaultBrowserAdvancedFields } from './browser_context_advanced'; +import { initialValues as defaultTLSFields } from './tls_fields_context'; + export { PolicyConfigContext, PolicyConfigContextProvider, @@ -61,3 +71,23 @@ export { export { HTTPContextProvider } from './http_provider'; export { TCPContextProvider } from './tcp_provider'; export { BrowserContextProvider } from './browser_provider'; +export { SyntheticsProviders } from './synthetics_context_providers'; + +export const defaultConfig: PolicyConfig = { + [DataStream.HTTP]: { + ...defaultHTTPSimpleFields, + ...defaultHTTPAdvancedFields, + ...defaultTLSFields, + }, + [DataStream.TCP]: { + ...defaultTCPSimpleFields, + ...defaultTCPAdvancedFields, + ...defaultTLSFields, + }, + [DataStream.ICMP]: defaultICMPSimpleFields, + [DataStream.BROWSER]: { + ...defaultBrowserSimpleFields, + ...defaultBrowserAdvancedFields, + ...defaultTLSFields, + }, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/policy_config_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/policy_config_context.tsx index 69c0e1d7ba4f..ed183e1f8135 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/policy_config_context.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/policy_config_context.tsx @@ -10,6 +10,8 @@ import { DataStream } from '../types'; interface IPolicyConfigContext { setMonitorType: React.Dispatch>; + setName: React.Dispatch>; + setLocations: React.Dispatch>; setIsTLSEnabled: React.Dispatch>; setIsZipUrlTLSEnabled: React.Dispatch>; monitorType: DataStream; @@ -19,6 +21,10 @@ interface IPolicyConfigContext { defaultIsTLSEnabled?: boolean; defaultIsZipUrlTLSEnabled?: boolean; isEditable?: boolean; + defaultName?: string; + name?: string; + defaultLocations?: string[]; + locations?: string[]; } interface IPolicyConfigContextProvider { @@ -26,6 +32,8 @@ interface IPolicyConfigContextProvider { defaultMonitorType?: DataStream; defaultIsTLSEnabled?: boolean; defaultIsZipUrlTLSEnabled?: boolean; + defaultName?: string; + defaultLocations?: string[]; isEditable?: boolean; } @@ -35,6 +43,12 @@ const defaultContext: IPolicyConfigContext = { setMonitorType: (_monitorType: React.SetStateAction) => { throw new Error('setMonitorType was not initialized, set it when you invoke the context'); }, + setName: (_name: React.SetStateAction) => { + throw new Error('setName was not initialized, set it when you invoke the context'); + }, + setLocations: (_locations: React.SetStateAction) => { + throw new Error('setLocations was not initialized, set it when you invoke the context'); + }, setIsTLSEnabled: (_isTLSEnabled: React.SetStateAction) => { throw new Error('setIsTLSEnabled was not initialized, set it when you invoke the context'); }, @@ -47,19 +61,25 @@ const defaultContext: IPolicyConfigContext = { defaultMonitorType: initialValue, // immutable, defaultIsTLSEnabled: false, defaultIsZipUrlTLSEnabled: false, + defaultName: '', + defaultLocations: [], isEditable: false, }; export const PolicyConfigContext = createContext(defaultContext); -export const PolicyConfigContextProvider = ({ +export function PolicyConfigContextProvider({ children, defaultMonitorType = initialValue, defaultIsTLSEnabled = false, defaultIsZipUrlTLSEnabled = false, + defaultName = '', + defaultLocations = [], isEditable = false, -}: IPolicyConfigContextProvider) => { +}: IPolicyConfigContextProvider) { const [monitorType, setMonitorType] = useState(defaultMonitorType); + const [name, setName] = useState(defaultName); + const [locations, setLocations] = useState(defaultLocations); const [isTLSEnabled, setIsTLSEnabled] = useState(defaultIsTLSEnabled); const [isZipUrlTLSEnabled, setIsZipUrlTLSEnabled] = useState(defaultIsZipUrlTLSEnabled); @@ -75,6 +95,12 @@ export const PolicyConfigContextProvider = ({ defaultIsTLSEnabled, defaultIsZipUrlTLSEnabled, isEditable, + defaultName, + name, + setName, + defaultLocations, + locations, + setLocations, }; }, [ monitorType, @@ -84,9 +110,15 @@ export const PolicyConfigContextProvider = ({ defaultIsTLSEnabled, defaultIsZipUrlTLSEnabled, isEditable, + name, + defaultName, + locations, + defaultLocations, ]); return ; -}; +} -export const usePolicyConfigContext = () => useContext(PolicyConfigContext); +export function usePolicyConfigContext() { + return useContext(PolicyConfigContext); +} diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/synthetics_context_providers.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/synthetics_context_providers.tsx new file mode 100644 index 000000000000..0d730c5f96e9 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/synthetics_context_providers.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { HTTPFields, TCPFields, ICMPFields, BrowserFields, ITLSFields, DataStream } from '../types'; +import { + PolicyConfigContextProvider, + TCPContextProvider, + ICMPSimpleFieldsContextProvider, + HTTPContextProvider, + BrowserContextProvider, + TLSFieldsContextProvider, +} from '.'; + +interface Props { + children: React.ReactNode; + httpDefaultValues?: HTTPFields; + tcpDefaultValues?: TCPFields; + icmpDefaultValues?: ICMPFields; + browserDefaultValues?: BrowserFields; + tlsDefaultValues?: ITLSFields; + policyDefaultValues?: { + defaultMonitorType: DataStream; + defaultIsTLSEnabled: boolean; + defaultIsZipUrlTLSEnabled: boolean; + defaultName?: string; + defaultLocations?: string[]; + isEditable: boolean; + }; +} + +export const SyntheticsProviders = ({ + children, + httpDefaultValues, + tcpDefaultValues, + icmpDefaultValues, + browserDefaultValues, + tlsDefaultValues, + policyDefaultValues, +}: Props) => { + return ( + + + + + + + {children} + + + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx index f8754307e082..1952d50cdd92 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx @@ -33,6 +33,7 @@ import { BrowserAdvancedFields } from './browser/advanced_fields'; interface Props { validate: Validation; dataStreams?: DataStream[]; + children?: React.ReactNode; } const dataStreamToString = [ @@ -50,7 +51,7 @@ const dataStreamToString = [ }, ]; -export const CustomFields = memo(({ validate, dataStreams = [] }) => { +export const CustomFields = memo(({ validate, dataStreams = [], children }) => { const { monitorType, setMonitorType, isTLSEnabled, setIsTLSEnabled, isEditable } = usePolicyConfigContext(); @@ -98,6 +99,7 @@ export const CustomFields = memo(({ validate, dataStreams = [] }) => { > + {children} {!isEditable && ( { - const { isTLSEnabled, isZipUrlTLSEnabled } = usePolicyConfigContext(); +export const usePolicy = (fleetPolicyName: string = '') => { + const { + isTLSEnabled, + isZipUrlTLSEnabled, + name: monitorName, // the monitor name can come from two different places, either from fleet or from uptime + } = usePolicyConfigContext(); const { fields: httpSimpleFields } = useHTTPSimpleFieldsContext(); const { fields: tcpSimpleFields } = useTCPSimpleFieldsContext(); const { fields: icmpSimpleFields } = useICMPSimpleFieldsContext(); @@ -77,6 +77,7 @@ export const usePolicy = (name: string) => { [isTLSEnabled, isZipUrlTLSEnabled] ); + /* TODO add locations to policy config for synthetics service */ const policyConfig: PolicyConfig = useMemo( () => ({ [DataStream.HTTP]: { @@ -87,7 +88,7 @@ export const usePolicy = (name: string) => { ...httpSimpleFields[ConfigKeys.METADATA], ...metadata, }, - [ConfigKeys.NAME]: name, + [ConfigKeys.NAME]: fleetPolicyName || monitorName, } as HTTPFields, [DataStream.TCP]: { ...tcpSimpleFields, @@ -97,11 +98,11 @@ export const usePolicy = (name: string) => { ...tcpSimpleFields[ConfigKeys.METADATA], ...metadata, }, - [ConfigKeys.NAME]: name, + [ConfigKeys.NAME]: fleetPolicyName || monitorName, } as TCPFields, [DataStream.ICMP]: { ...icmpSimpleFields, - [ConfigKeys.NAME]: name, + [ConfigKeys.NAME]: fleetPolicyName || monitorName, } as ICMPFields, [DataStream.BROWSER]: { ...browserSimpleFields, @@ -110,7 +111,7 @@ export const usePolicy = (name: string) => { ...browserSimpleFields[ConfigKeys.METADATA], ...metadata, }, - [ConfigKeys.NAME]: name, + [ConfigKeys.NAME]: fleetPolicyName || monitorName, } as BrowserFields, }), [ @@ -123,7 +124,8 @@ export const usePolicy = (name: string) => { browserSimpleFields, browserAdvancedFields, tlsFields, - name, + fleetPolicyName, + monitorName, ] ); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.tsx index c3b2b632850b..e3e13b16ebf2 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.tsx @@ -8,14 +8,7 @@ import React, { memo } from 'react'; import { PackagePolicyCreateExtensionComponentProps } from '../../../../fleet/public'; import { SyntheticsPolicyCreateExtension } from './synthetics_policy_create_extension'; -import { - PolicyConfigContextProvider, - TCPContextProvider, - ICMPSimpleFieldsContextProvider, - HTTPContextProvider, - BrowserContextProvider, - TLSFieldsContextProvider, -} from './contexts'; +import { SyntheticsProviders } from './contexts'; /** * Exports Synthetics-specific package policy instructions @@ -24,19 +17,9 @@ import { export const SyntheticsPolicyCreateExtensionWrapper = memo(({ newPolicy, onChange }) => { return ( - - - - - - - - - - - - - + + + ); }); SyntheticsPolicyCreateExtensionWrapper.displayName = 'SyntheticsPolicyCreateExtensionWrapper'; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/formatters/browser.ts b/x-pack/plugins/uptime/public/components/monitor_management/formatters/browser.ts new file mode 100644 index 000000000000..e4a84e2bb85b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/formatters/browser.ts @@ -0,0 +1,35 @@ +/* + * 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 { BrowserFields, ConfigKeys } from '../../fleet_package/types'; +import { Formatter, commonFormatters, objectFormatter, arrayFormatter } from './common'; + +export type BrowserFormatMap = Record; + +export const browserFormatters: BrowserFormatMap = { + [ConfigKeys.METADATA]: (fields) => objectFormatter(fields[ConfigKeys.METADATA]), + [ConfigKeys.SOURCE_ZIP_URL]: null, + [ConfigKeys.SOURCE_ZIP_USERNAME]: null, + [ConfigKeys.SOURCE_ZIP_PASSWORD]: null, + [ConfigKeys.SOURCE_ZIP_FOLDER]: null, + [ConfigKeys.SOURCE_ZIP_PROXY_URL]: null, + [ConfigKeys.SOURCE_INLINE]: null, + [ConfigKeys.PARAMS]: null, + [ConfigKeys.SCREENSHOTS]: null, + [ConfigKeys.SYNTHETICS_ARGS]: (fields) => null, + [ConfigKeys.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]: null, + [ConfigKeys.ZIP_URL_TLS_CERTIFICATE]: null, + [ConfigKeys.ZIP_URL_TLS_KEY]: null, + [ConfigKeys.ZIP_URL_TLS_KEY_PASSPHRASE]: null, + [ConfigKeys.ZIP_URL_TLS_VERIFICATION_MODE]: null, + [ConfigKeys.ZIP_URL_TLS_VERSION]: (fields) => + arrayFormatter(fields[ConfigKeys.ZIP_URL_TLS_VERSION]), + [ConfigKeys.JOURNEY_FILTERS_MATCH]: null, + [ConfigKeys.JOURNEY_FILTERS_TAGS]: null, + [ConfigKeys.IGNORE_HTTPS_ERRORS]: null, + ...commonFormatters, +}; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/formatters/common.ts b/x-pack/plugins/uptime/public/components/monitor_management/formatters/common.ts new file mode 100644 index 000000000000..92d2b28f1283 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/formatters/common.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ICommonFields, ICustomFields, ConfigKeys } from '../../fleet_package/types'; + +export type Formatter = + | null + | ((fields: Partial) => string | string[] | Record | null); + +export type CommonFormatMap = Record; + +export const commonFormatters: CommonFormatMap = { + [ConfigKeys.NAME]: null, + [ConfigKeys.MONITOR_TYPE]: null, + [ConfigKeys.SCHEDULE]: (fields) => + `@every ${fields[ConfigKeys.SCHEDULE]?.number}${fields[ConfigKeys.SCHEDULE]?.unit}`, + [ConfigKeys.APM_SERVICE_NAME]: null, + [ConfigKeys.TAGS]: null, + [ConfigKeys.TIMEOUT]: (fields) => secondsToCronFormatter(fields[ConfigKeys.TIMEOUT]), +}; + +export const arrayFormatter = (value: string[] = []) => (value.length ? value : null); + +export const secondsToCronFormatter = (value: string = '') => (value ? `${value}s` : null); + +export const objectFormatter = (value: Record = {}) => + Object.keys(value).length ? value : null; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/formatters/http.ts b/x-pack/plugins/uptime/public/components/monitor_management/formatters/http.ts new file mode 100644 index 000000000000..41e162cff2d0 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/formatters/http.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HTTPFields, ConfigKeys } from '../../fleet_package/types'; +import { Formatter, commonFormatters, objectFormatter, arrayFormatter } from './common'; +import { tlsFormatters } from './tls'; + +export type HTTPFormatMap = Record; + +export const httpFormatters: HTTPFormatMap = { + [ConfigKeys.METADATA]: (fields) => objectFormatter(fields[ConfigKeys.METADATA]), + [ConfigKeys.URLS]: null, + [ConfigKeys.MAX_REDIRECTS]: null, + [ConfigKeys.USERNAME]: null, + [ConfigKeys.PASSWORD]: null, + [ConfigKeys.PROXY_URL]: null, + [ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]: (fields) => + arrayFormatter(fields[ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]), + [ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE]: (fields) => + arrayFormatter(fields[ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE]), + [ConfigKeys.RESPONSE_BODY_INDEX]: null, + [ConfigKeys.RESPONSE_HEADERS_CHECK]: (fields) => + objectFormatter(fields[ConfigKeys.RESPONSE_HEADERS_CHECK]), + [ConfigKeys.RESPONSE_HEADERS_INDEX]: null, + [ConfigKeys.RESPONSE_STATUS_CHECK]: (fields) => + arrayFormatter(fields[ConfigKeys.RESPONSE_STATUS_CHECK]), + [ConfigKeys.REQUEST_BODY_CHECK]: (fields) => fields[ConfigKeys.REQUEST_BODY_CHECK]?.value || null, + [ConfigKeys.REQUEST_HEADERS_CHECK]: (fields) => + objectFormatter(fields[ConfigKeys.REQUEST_HEADERS_CHECK]), + [ConfigKeys.REQUEST_METHOD_CHECK]: null, + ...tlsFormatters, + ...commonFormatters, +}; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/formatters/icmp.ts b/x-pack/plugins/uptime/public/components/monitor_management/formatters/icmp.ts new file mode 100644 index 000000000000..841f98630948 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/formatters/icmp.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 { ICMPFields, ConfigKeys } from '../../fleet_package/types'; +import { Formatter, commonFormatters, secondsToCronFormatter } from './common'; + +export type ICMPFormatMap = Record; + +export const icmpFormatters: ICMPFormatMap = { + [ConfigKeys.HOSTS]: null, + [ConfigKeys.WAIT]: (fields) => secondsToCronFormatter(fields[ConfigKeys.WAIT]), + ...commonFormatters, +}; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/formatters/index.ts b/x-pack/plugins/uptime/public/components/monitor_management/formatters/index.ts new file mode 100644 index 000000000000..56dac35596d4 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/formatters/index.ts @@ -0,0 +1,38 @@ +/* + * 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 { DataStream } from '../../fleet_package/types'; + +import { httpFormatters, HTTPFormatMap } from './http'; +import { tcpFormatters, TCPFormatMap } from './tcp'; +import { icmpFormatters, ICMPFormatMap } from './icmp'; +import { browserFormatters, BrowserFormatMap } from './browser'; +import { commonFormatters, CommonFormatMap } from './common'; + +type Formatters = HTTPFormatMap & TCPFormatMap & ICMPFormatMap & BrowserFormatMap & CommonFormatMap; + +interface FormatterMap { + [DataStream.HTTP]: HTTPFormatMap; + [DataStream.ICMP]: ICMPFormatMap; + [DataStream.TCP]: TCPFormatMap; + [DataStream.BROWSER]: BrowserFormatMap; +} + +export const formattersMap: FormatterMap = { + [DataStream.HTTP]: httpFormatters, + [DataStream.ICMP]: icmpFormatters, + [DataStream.TCP]: tcpFormatters, + [DataStream.BROWSER]: browserFormatters, +}; + +export const formatters: Formatters = { + ...httpFormatters, + ...icmpFormatters, + ...tcpFormatters, + ...browserFormatters, + ...commonFormatters, +}; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/formatters/tcp.ts b/x-pack/plugins/uptime/public/components/monitor_management/formatters/tcp.ts new file mode 100644 index 000000000000..cdf77307a693 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/formatters/tcp.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TCPFields, ConfigKeys } from '../../fleet_package/types'; +import { Formatter, commonFormatters } from './common'; +import { tlsFormatters } from './tls'; + +export type TCPFormatMap = Record; + +export const tcpFormatters: TCPFormatMap = { + [ConfigKeys.METADATA]: null, + [ConfigKeys.HOSTS]: null, + [ConfigKeys.PROXY_URL]: null, + [ConfigKeys.PROXY_USE_LOCAL_RESOLVER]: null, + [ConfigKeys.RESPONSE_RECEIVE_CHECK]: null, + [ConfigKeys.REQUEST_SEND_CHECK]: null, + ...tlsFormatters, + ...commonFormatters, +}; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/formatters/tls.ts b/x-pack/plugins/uptime/public/components/monitor_management/formatters/tls.ts new file mode 100644 index 000000000000..5f7ce27637f3 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/formatters/tls.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ITLSFields, ConfigKeys } from '../../fleet_package/types'; +import { arrayFormatter, Formatter } from './common'; + +type TLSFormatMap = Record; + +export const tlsFormatters: TLSFormatMap = { + [ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]: null, + [ConfigKeys.TLS_CERTIFICATE]: null, + [ConfigKeys.TLS_KEY]: null, + [ConfigKeys.TLS_KEY_PASSPHRASE]: null, + [ConfigKeys.TLS_VERIFICATION_MODE]: null, + [ConfigKeys.TLS_VERSION]: (fields) => arrayFormatter(fields[ConfigKeys.TLS_VERSION]), +}; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_format_monitor.ts b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_format_monitor.ts new file mode 100644 index 000000000000..9027aa452041 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_format_monitor.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useEffect, useRef, useState } from 'react'; +import { omitBy, isNil } from 'lodash'; +import { ConfigKeys, DataStream, Validation, ICustomFields } from '../../fleet_package/types'; +import { formatters } from '../formatters'; + +interface Props { + monitorType: DataStream; + defaultConfig: Partial; + config: Partial; + validate: Record; +} + +const formatMonitorConfig = (configKeys: ConfigKeys[], config: Partial) => { + const formattedMonitor = {} as Record; + + configKeys.forEach((key) => { + const value = config[key] ?? null; + if (value && formatters[key]) { + formattedMonitor[key] = formatters[key]?.(config); + } else if (value) { + formattedMonitor[key] = value; + } + }); + + return omitBy(formattedMonitor, isNil) as Partial; +}; + +export const useFormatMonitor = ({ monitorType, defaultConfig, config, validate }: Props) => { + const [formattedMonitor, setFormattedMonitor] = useState>( + formatMonitorConfig(Object.keys(config) as ConfigKeys[], config) + ); + const [isValid, setIsValid] = useState(false); + const currentConfig = useRef>(defaultConfig); + + useEffect(() => { + const configKeys = Object.keys(config) as ConfigKeys[]; + const validationKeys = Object.keys(validate[monitorType]) as ConfigKeys[]; + const configDidUpdate = configKeys.some((key) => config[key] !== currentConfig.current[key]); + const isValidT = + !!config.name && !validationKeys.find((key) => validate[monitorType]?.[key]?.(config)); + + // prevent an infinite loop of updating the policy + if (configDidUpdate) { + const formattedMonitorT = formatMonitorConfig(configKeys, config); + currentConfig.current = config; + setFormattedMonitor(formattedMonitorT); + setIsValid(isValidT); + } + }, [config, currentConfig, validate, monitorType]); + + return { + config, + isValid, + formattedMonitor, + }; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_config.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_config.tsx new file mode 100644 index 000000000000..081ceee1fe63 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_config.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { defaultConfig, usePolicyConfigContext } from '../fleet_package/contexts'; + +import { usePolicy } from '../fleet_package/hooks/use_policy'; +import { validate } from '../fleet_package/validation'; +import { MonitorFields } from './monitor_fields'; +import { useFormatMonitor } from './hooks/use_format_monitor'; + +export const MonitorConfig = () => { + const { monitorType } = usePolicyConfigContext(); + /* TODO - Use Effect to make sure the package/index templates are loaded. Wait for it to load before showing view + * then show error message if it fails */ + + /* raw policy config compatible with the UI. Save this to saved objects */ + const policyConfig = usePolicy(); + + /* Policy config that heartbeat can read. Send this to the service. + This type of helper should ideally be moved to task manager where we are syncing the config. + We can process validation (isValid) and formatting for heartbeat (formattedMonitor) separately + We don't need to save the heartbeat compatible version in saved objects */ + useFormatMonitor({ + monitorType, + validate, + config: policyConfig[monitorType], + defaultConfig: defaultConfig[monitorType], + }); + + return ; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_fields.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_fields.tsx new file mode 100644 index 000000000000..30659e6c2cf4 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_fields.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiForm } from '@elastic/eui'; +import { DataStream } from '../fleet_package/types'; +import { usePolicyConfigContext } from '../fleet_package/contexts'; + +import { CustomFields } from '../fleet_package/custom_fields'; +import { validate } from '../fleet_package/validation'; +import { MonitorNameAndLocation } from './monitor_name_location'; + +export const MonitorFields = () => { + const { monitorType } = usePolicyConfigContext(); + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_name_location.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_name_location.tsx new file mode 100644 index 000000000000..6ee3eacd81cd --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_name_location.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { usePolicyConfigContext } from '../fleet_package/contexts'; + +export const MonitorNameAndLocation = () => { + const { name, setName } = usePolicyConfigContext(); + return ( + <> + + setName(event.target.value)} + /> + + {/* TODO: connect locations */} + {/* + setLocations(selOptions.map(({ value }) => value as string))} + /> + */} + + ); +}; diff --git a/x-pack/plugins/uptime/public/pages/add_monitor.tsx b/x-pack/plugins/uptime/public/pages/add_monitor.tsx index 10e3d9d6ce29..a5f925e4712a 100644 --- a/x-pack/plugins/uptime/public/pages/add_monitor.tsx +++ b/x-pack/plugins/uptime/public/pages/add_monitor.tsx @@ -6,7 +6,17 @@ */ import React from 'react'; +import { useTrackPageview } from '../../../observability/public'; +import { SyntheticsProviders } from '../components/fleet_package/contexts'; +import { MonitorConfig } from '../components/monitor_management/monitor_config'; export const AddMonitorPage: React.FC = () => { - return null; + useTrackPageview({ app: 'uptime', path: 'add-monitor' }); + useTrackPageview({ app: 'uptime', path: 'add-monitor', delay: 15000 }); + + return ( + + + + ); }; diff --git a/x-pack/plugins/uptime/public/pages/edit_monitor.tsx b/x-pack/plugins/uptime/public/pages/edit_monitor.tsx index 3be1bc7b35d8..965639bd1214 100644 --- a/x-pack/plugins/uptime/public/pages/edit_monitor.tsx +++ b/x-pack/plugins/uptime/public/pages/edit_monitor.tsx @@ -5,8 +5,101 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; +import { + ConfigKeys, + ICustomFields, + ITLSFields, + PolicyConfig, + DataStream, +} from '../components/fleet_package/types'; +import { useTrackPageview } from '../../../observability/public'; +import { SyntheticsProviders } from '../components/fleet_package/contexts'; +import { MonitorConfig } from '../components/monitor_management/monitor_config'; export const EditMonitorPage: React.FC = () => { - return null; + useTrackPageview({ app: 'uptime', path: 'edit-monitor' }); + useTrackPageview({ app: 'uptime', path: 'edit-monitor', delay: 15000 }); + + const { + enableTLS: isTLSEnabled, + enableZipUrlTLS: isZipUrlTLSEnabled, + fullConfig: fullDefaultConfig, + monitorTypeConfig: defaultConfig, + monitorType, + tlsConfig: defaultTLSConfig, + } = useMemo(() => { + /* TODO: fetch current monitor to be edited from saved objects based on url param */ + const monitor = {} as Record; // fetch + + let enableTLS = false; + let enableZipUrlTLS = false; + const getDefaultConfig = () => { + const type: DataStream = monitor[ConfigKeys.MONITOR_TYPE] as DataStream; + + const configKeys: ConfigKeys[] = Object.values(ConfigKeys) || ([] as ConfigKeys[]); + const formattedDefaultConfigForMonitorType: ICustomFields = configKeys.reduce( + (acc: ICustomFields, key: ConfigKeys) => { + return { + ...acc, + key, + }; + }, + {} as ICustomFields + ); + + const tlsConfig: ITLSFields = { + [ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]: + formattedDefaultConfigForMonitorType[ConfigKeys.TLS_CERTIFICATE_AUTHORITIES], + [ConfigKeys.TLS_CERTIFICATE]: + formattedDefaultConfigForMonitorType[ConfigKeys.TLS_CERTIFICATE], + [ConfigKeys.TLS_KEY]: formattedDefaultConfigForMonitorType[ConfigKeys.TLS_KEY], + [ConfigKeys.TLS_KEY_PASSPHRASE]: + formattedDefaultConfigForMonitorType[ConfigKeys.TLS_KEY_PASSPHRASE], + [ConfigKeys.TLS_VERIFICATION_MODE]: + formattedDefaultConfigForMonitorType[ConfigKeys.TLS_VERIFICATION_MODE], + [ConfigKeys.TLS_VERSION]: formattedDefaultConfigForMonitorType[ConfigKeys.TLS_VERSION], + }; + + enableTLS = Boolean(formattedDefaultConfigForMonitorType[ConfigKeys.TLS_VERIFICATION_MODE]); + enableZipUrlTLS = Boolean( + formattedDefaultConfigForMonitorType[ConfigKeys.ZIP_URL_TLS_VERIFICATION_MODE] + ); + + const formattedDefaultConfig: Partial = { + [type]: formattedDefaultConfigForMonitorType, + }; + + return { + fullConfig: formattedDefaultConfig, + monitorTypeConfig: formattedDefaultConfigForMonitorType, + tlsConfig, + monitorType: type, + enableTLS, + enableZipUrlTLS, + }; + }; + + return getDefaultConfig(); + }, []); + + return ( + + + + ); }; From 0e6af5e74cbac3ec2b2648e84c4065c318f1805f Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 29 Nov 2021 16:02:16 -0700 Subject: [PATCH 040/224] [Maps] clean up geojson_vector_layer utils (#119395) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../layers/heatmap_layer/heatmap_layer.ts | 4 +- .../layers/vector_layer/bounds_data.ts | 54 ++++++++++++++ .../{utils.tsx => geojson_source_data.tsx} | 71 +------------------ .../geojson_vector_layer.tsx | 24 ++++++- .../classes/layers/vector_layer/index.ts | 6 +- .../layers/vector_layer/vector_layer.tsx | 4 +- 6 files changed, 81 insertions(+), 82 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/layers/vector_layer/bounds_data.ts rename x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/{utils.tsx => geojson_source_data.tsx} (60%) diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts index 2bc79bfaea74..9b49d2b597e4 100644 --- a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts @@ -11,7 +11,7 @@ import { HeatmapStyle } from '../../styles/heatmap/heatmap_style'; import { LAYER_TYPE } from '../../../../common/constants'; import { HeatmapLayerDescriptor } from '../../../../common/descriptor_types'; import { ESGeoGridSource } from '../../sources/es_geo_grid_source'; -import { getVectorSourceBounds, MvtSourceData, syncMvtSourceData } from '../vector_layer'; +import { syncBoundsData, MvtSourceData, syncMvtSourceData } from '../vector_layer'; import { DataRequestContext } from '../../../actions'; import { buildVectorRequestMeta } from '../build_vector_request_meta'; import { ITiledSingleLayerVectorSource } from '../../sources/tiled_single_layer_vector_source'; @@ -193,7 +193,7 @@ export class HeatmapLayer extends AbstractLayer { } async getBounds(syncContext: DataRequestContext) { - return await getVectorSourceBounds({ + return await syncBoundsData({ layerId: this.getId(), syncContext, source: this.getSource(), diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/bounds_data.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/bounds_data.ts new file mode 100644 index 000000000000..34edf8cd0960 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/bounds_data.ts @@ -0,0 +1,54 @@ +/* + * 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 { Query } from 'src/plugins/data/common'; +import { SOURCE_BOUNDS_DATA_REQUEST_ID } from '../../../../common/constants'; +import { MapExtent } from '../../../../common/descriptor_types'; +import { DataRequestContext } from '../../../actions'; +import { IVectorSource } from '../../sources/vector_source'; + +export async function syncBoundsData({ + layerId, + syncContext, + source, + sourceQuery, +}: { + layerId: string; + syncContext: DataRequestContext; + source: IVectorSource; + sourceQuery: Query | null; +}): Promise { + const { startLoading, stopLoading, registerCancelCallback, dataFilters } = syncContext; + + const requestToken = Symbol(`${SOURCE_BOUNDS_DATA_REQUEST_ID}-${layerId}`); + + // Do not pass all searchFilters to source.getBoundsForFilters(). + // For example, do not want to filter bounds request by extent and buffer. + const boundsFilters = { + sourceQuery: sourceQuery ? sourceQuery : undefined, + query: dataFilters.query, + timeFilters: dataFilters.timeFilters, + timeslice: dataFilters.timeslice, + filters: dataFilters.filters, + applyGlobalQuery: source.getApplyGlobalQuery(), + applyGlobalTime: source.getApplyGlobalTime(), + }; + + let bounds = null; + try { + startLoading(SOURCE_BOUNDS_DATA_REQUEST_ID, requestToken, boundsFilters); + bounds = await source.getBoundsForFilters( + boundsFilters, + registerCancelCallback.bind(null, requestToken) + ); + } finally { + // Use stopLoading callback instead of onLoadError callback. + // Function is loading bounds and not feature data. + stopLoading(SOURCE_BOUNDS_DATA_REQUEST_ID, requestToken, bounds ? bounds : {}); + } + return bounds; +} diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/utils.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_source_data.tsx similarity index 60% rename from x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/utils.tsx rename to x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_source_data.tsx index 4385adbd4de6..1f484b7ecfc5 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/utils.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_source_data.tsx @@ -6,17 +6,13 @@ */ import { FeatureCollection } from 'geojson'; -import type { Map as MbMap } from '@kbn/mapbox-gl'; -import type { Query } from 'src/plugins/data/common'; import { EMPTY_FEATURE_COLLECTION, - SOURCE_BOUNDS_DATA_REQUEST_ID, SOURCE_DATA_REQUEST_ID, VECTOR_SHAPE_TYPE, } from '../../../../../common/constants'; import { DataRequestMeta, - MapExtent, Timeslice, VectorSourceRequestMeta, } from '../../../../../common/descriptor_types'; @@ -28,30 +24,7 @@ import { getCentroidFeatures } from './get_centroid_features'; import { canSkipSourceUpdate } from '../../../util/can_skip_fetch'; import { assignFeatureIds } from './assign_feature_ids'; -export function addGeoJsonMbSource(mbSourceId: string, mbLayerIds: string[], mbMap: MbMap) { - const mbSource = mbMap.getSource(mbSourceId); - if (!mbSource) { - mbMap.addSource(mbSourceId, { - type: 'geojson', - data: EMPTY_FEATURE_COLLECTION, - }); - } else if (mbSource.type !== 'geojson') { - // Recreate source when existing source is not geojson. This can occur when layer changes from tile layer to vector layer. - mbLayerIds.forEach((mbLayerId) => { - if (mbMap.getLayer(mbLayerId)) { - mbMap.removeLayer(mbLayerId); - } - }); - - mbMap.removeSource(mbSourceId); - mbMap.addSource(mbSourceId, { - type: 'geojson', - data: EMPTY_FEATURE_COLLECTION, - }); - } -} - -export async function syncVectorSource({ +export async function syncGeojsonSourceData({ layerId, layerName, prevDataRequest, @@ -129,45 +102,3 @@ export async function syncVectorSource({ throw error; } } - -export async function getVectorSourceBounds({ - layerId, - syncContext, - source, - sourceQuery, -}: { - layerId: string; - syncContext: DataRequestContext; - source: IVectorSource; - sourceQuery: Query | null; -}): Promise { - const { startLoading, stopLoading, registerCancelCallback, dataFilters } = syncContext; - - const requestToken = Symbol(`${SOURCE_BOUNDS_DATA_REQUEST_ID}-${layerId}`); - - // Do not pass all searchFilters to source.getBoundsForFilters(). - // For example, do not want to filter bounds request by extent and buffer. - const boundsFilters = { - sourceQuery: sourceQuery ? sourceQuery : undefined, - query: dataFilters.query, - timeFilters: dataFilters.timeFilters, - timeslice: dataFilters.timeslice, - filters: dataFilters.filters, - applyGlobalQuery: source.getApplyGlobalQuery(), - applyGlobalTime: source.getApplyGlobalTime(), - }; - - let bounds = null; - try { - startLoading(SOURCE_BOUNDS_DATA_REQUEST_ID, requestToken, boundsFilters); - bounds = await source.getBoundsForFilters( - boundsFilters, - registerCancelCallback.bind(null, requestToken) - ); - } finally { - // Use stopLoading callback instead of onLoadError callback. - // Function is loading bounds and not feature data. - stopLoading(SOURCE_BOUNDS_DATA_REQUEST_ID, requestToken, bounds ? bounds : {}); - } - return bounds; -} diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx index 3f7c782ca469..3152ac27189b 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx @@ -39,7 +39,7 @@ import { DataRequestAbortError } from '../../../util/data_request'; import { canSkipSourceUpdate } from '../../../util/can_skip_fetch'; import { getFeatureCollectionBounds } from '../../../util/get_feature_collection_bounds'; import { GEOJSON_FEATURE_ID_PROPERTY_NAME } from './assign_feature_ids'; -import { addGeoJsonMbSource, syncVectorSource } from './utils'; +import { syncGeojsonSourceData } from './geojson_source_data'; import { JoinState, performInnerJoins } from './perform_inner_joins'; import { buildVectorRequestMeta } from '../../build_vector_request_meta'; @@ -138,8 +138,26 @@ export class GeoJsonVectorLayer extends AbstractVectorLayer { return await style.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); } + _requiresPrevSourceCleanup(mbMap: MbMap) { + const mbSource = mbMap.getSource(this.getMbSourceId()); + if (!mbSource) { + return false; + } + + return mbSource.type !== 'geojson'; + } + syncLayerWithMB(mbMap: MbMap, timeslice?: Timeslice) { - addGeoJsonMbSource(this.getMbSourceId(), this.getMbLayerIds(), mbMap); + this._removeStaleMbSourcesAndLayers(mbMap); + + const mbSourceId = this.getMbSourceId(); + const mbSource = mbMap.getSource(mbSourceId); + if (!mbSource) { + mbMap.addSource(mbSourceId, { + type: 'geojson', + data: EMPTY_FEATURE_COLLECTION, + }); + } this._syncFeatureCollectionWithMb(mbMap); @@ -211,7 +229,7 @@ export class GeoJsonVectorLayer extends AbstractVectorLayer { try { await this._syncSourceStyleMeta(syncContext, source, style); await this._syncSourceFormatters(syncContext, source, style); - const sourceResult = await syncVectorSource({ + const sourceResult = await syncGeojsonSourceData({ layerId: this.getId(), layerName: await this.getDisplayName(source), prevDataRequest: this.getSourceDataRequest(), diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts index 2f46bfa87193..5780c2c723dd 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts @@ -5,11 +5,7 @@ * 2.0. */ -export { - addGeoJsonMbSource, - getVectorSourceBounds, - syncVectorSource, -} from './geojson_vector_layer/utils'; +export { syncBoundsData } from './bounds_data'; export type { IVectorLayer, VectorLayerArguments } from './vector_layer'; export { isVectorLayer, NO_RESULTS_ICON_AND_TOOLTIPCONTENT } from './vector_layer'; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 2c6bd64e1bc6..71a960fc1919 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -58,7 +58,7 @@ import { IESSource } from '../../sources/es_source'; import { ITermJoinSource } from '../../sources/term_join_source'; import { buildVectorRequestMeta } from '../build_vector_request_meta'; import { getJoinAggKey } from '../../../../common/get_agg_key'; -import { getVectorSourceBounds } from './geojson_vector_layer/utils'; +import { syncBoundsData } from './bounds_data'; export function isVectorLayer(layer: ILayer) { return (layer as IVectorLayer).canShowTooltip !== undefined; @@ -287,7 +287,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { } async getBounds(syncContext: DataRequestContext) { - return getVectorSourceBounds({ + return syncBoundsData({ layerId: this.getId(), syncContext, source: this.getSource(), From 7028c97109d236f693a7d6adc68bad0d59e863f2 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Mon, 29 Nov 2021 19:04:57 -0500 Subject: [PATCH 041/224] [Maps] Use minimum symbol size if meta is not loaded yet. (#119119) --- .../vector/properties/dynamic_size_property.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx index 3dc44c171d41..e76e9e936fae 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import _ from 'lodash'; import React from 'react'; import type { Map as MbMap } from '@kbn/mapbox-gl'; import { DynamicStyleProperty } from './dynamic_style_property'; @@ -111,7 +110,11 @@ export class DynamicSizeProperty extends DynamicStyleProperty= 0 ? this._options.minSize : null; } return this._getMbDataDrivenSize({ @@ -156,8 +159,8 @@ export class DynamicSizeProperty extends DynamicStyleProperty= 0 && + this._options.maxSize >= 0 ); } From e52610fec818f352e43d681f750d7e05dd2db584 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 29 Nov 2021 19:20:46 -0700 Subject: [PATCH 042/224] [Maps] Refresh zoom when applying auto zoom in Maps (#119665) * [Maps] Refresh zoom when applying auto zoom in Maps * review feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../maps/public/actions/map_actions.ts | 14 +++++++---- .../map_settings_panel/index.ts | 5 ++-- .../fit_to_data/fit_to_data.tsx | 23 +++++++++++++------ .../toolbar_overlay/fit_to_data/index.ts | 6 ++++- .../translations/translations/ja-JP.json | 2 -- .../translations/translations/zh-CN.json | 2 -- 6 files changed, 34 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index 9ec9a42986fb..ddfc441471c1 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -92,10 +92,16 @@ export function updateMapSetting( settingKey: string, settingValue: string | boolean | number | object ) { - return { - type: UPDATE_MAP_SETTING, - settingKey, - settingValue, + return (dispatch: ThunkDispatch) => { + dispatch({ + type: UPDATE_MAP_SETTING, + settingKey, + settingValue, + }); + + if (settingKey === 'autoFitToDataBounds' && settingValue === true) { + dispatch(autoFitToBounds()); + } }; } diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts b/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts index 015aad84cde5..c858c74c819d 100644 --- a/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts @@ -5,8 +5,9 @@ * 2.0. */ -import { AnyAction, Dispatch } from 'redux'; +import { AnyAction } from 'redux'; import { connect } from 'react-redux'; +import { ThunkDispatch } from 'redux-thunk'; import { FLYOUT_STATE } from '../../reducers/ui'; import { MapStoreState } from '../../reducers/store'; import { MapSettingsPanel } from './map_settings_panel'; @@ -27,7 +28,7 @@ function mapStateToProps(state: MapStoreState) { }; } -function mapDispatchToProps(dispatch: Dispatch) { +function mapDispatchToProps(dispatch: ThunkDispatch) { return { cancelChanges: () => { dispatch(rollbackMapSettings()); diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx index f975bc293d82..54f99c37b6d8 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx @@ -11,10 +11,23 @@ import { EuiButtonIcon, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; export interface Props { + autoFitToDataBounds: boolean; fitToBounds: () => void; } export function FitToData(props: Props) { + const label = i18n.translate('xpack.maps.fitToData.label', { + defaultMessage: 'Fit to data bounds', + }); + let title = label; + if (props.autoFitToDataBounds) { + title = + `${title}. ` + + i18n.translate('xpack.maps.fitToData.autoFitToDataBounds', { + defaultMessage: + 'Map setting "auto fit map to data bounds" enabled, map will automatically pan and zoom to show the data bounds.', + }); + } return ( ); diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts index b4322c93097f..2979bec06b95 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts @@ -10,10 +10,14 @@ import { ThunkDispatch } from 'redux-thunk'; import { connect } from 'react-redux'; import { MapStoreState } from '../../../reducers/store'; import { fitToDataBounds } from '../../../actions'; +import { getMapSettings } from '../../../selectors/map_selectors'; import { FitToData } from './fit_to_data'; function mapStateToProps(state: MapStoreState) { - return {}; + const mapSettings = getMapSettings(state); + return { + autoFitToDataBounds: mapSettings.autoFitToDataBounds, + }; } function mapDispatchToProps(dispatch: ThunkDispatch) { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6d54be0664e0..5948e16a5907 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15031,8 +15031,6 @@ "xpack.maps.filterEditor.applyGlobalQueryCheckboxLabel": "ă‚°ăƒ­ăƒŒăƒăƒ«æ€œçŽąă‚’ăƒŹă‚€ăƒ€ăƒŒăƒ‡ăƒŒă‚żă«é©ç”š", "xpack.maps.filterEditor.applyGlobalTimeCheckboxLabel": "ă‚°ăƒ­ăƒŒăƒăƒ«æ™‚ćˆ»ă‚’ăƒŹă‚€ăƒ€ăƒŒăƒ‡ăƒŒă‚żă«é©ç”š", "xpack.maps.filterEditor.applyGlobalTimeHelp": "有ćŠčă«ă™ă‚‹ăšă€ç”æžœăŒă‚°ăƒ­ăƒŒăƒăƒ«æ™‚ćˆ»ă§ç”žă‚ŠèŸŒăŸă‚ŒăŸă™", - "xpack.maps.fitToData.fitAriaLabel": "ăƒ‡ăƒŒă‚żăƒă‚Šăƒłăƒ‰ă«ćˆă‚ă›ă‚‹", - "xpack.maps.fitToData.fitButtonLabel": "ăƒ‡ăƒŒă‚żăƒă‚Šăƒłăƒ‰ă«ćˆă‚ă›ă‚‹", "xpack.maps.geoGrid.resolutionLabel": "グăƒȘăƒƒăƒ‰è§ŁćƒćșŠ", "xpack.maps.geometryFilterForm.geometryLabelLabel": "ゾă‚ȘュトăƒȘăƒ©ăƒ™ăƒ«", "xpack.maps.geometryFilterForm.relationLabel": "ç©ș間閱係", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f04696d7dbe3..0da3642eefa3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15228,8 +15228,6 @@ "xpack.maps.filterEditor.applyGlobalQueryCheckboxLabel": "ć°†ć…šć±€æœçŽąćș”甚äșŽć›Ÿć±‚æ•°æź", "xpack.maps.filterEditor.applyGlobalTimeCheckboxLabel": "ć°†ć…šć±€æ—¶é—Žćș”甚äșŽć›Ÿć±‚æ•°æź", "xpack.maps.filterEditor.applyGlobalTimeHelp": "ćŻç”šćŽïŒŒć…šć±€æ—¶é—ŽäŒšçŒ©ć‡ç»“æžœ", - "xpack.maps.fitToData.fitAriaLabel": "适ćș”æ•°æźèŸč界", - "xpack.maps.fitToData.fitButtonLabel": "适ćș”æ•°æźèŸč界", "xpack.maps.geoGrid.resolutionLabel": "çœ‘æ Œćˆ†èŸšçŽ‡", "xpack.maps.geometryFilterForm.geometryLabelLabel": "ć‡ äœ•æ ‡ç­Ÿ", "xpack.maps.geometryFilterForm.relationLabel": "ç©șé—Žć…łçł»", From affd10d418cd960a8d8098a90748afb77bb9993e Mon Sep 17 00:00:00 2001 From: sphilipse <94373878+sphilipse@users.noreply.github.com> Date: Tue, 30 Nov 2021 10:13:59 +0100 Subject: [PATCH 043/224] [Workplace Search] Increase source overview recent activity text size (#2177) (#119803) * Increase source overview recent activity text size (#2177) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../views/content_sources/components/overview.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx index d506ed9c32ba..6abfd8ed2444 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx @@ -222,11 +222,11 @@ export const Overview: React.FC = () => { {activities.map(({ details: activityDetails, event, time, status }, i) => ( - {event} + {event} {!custom && ( - + {status} {activityDetails && } @@ -234,7 +234,7 @@ export const Overview: React.FC = () => { )} - + {time} From 2014fb1173a2276a2e02b24dc3a12ddc9c5fcf9a Mon Sep 17 00:00:00 2001 From: sphilipse <94373878+sphilipse@users.noreply.github.com> Date: Tue, 30 Nov 2021 10:14:40 +0100 Subject: [PATCH 044/224] [Workplace Search] Promote the Synchronize CTA (#2178) (#119857) --- .../views/content_sources/components/overview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx index 6abfd8ed2444..a33f5ec90e3a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx @@ -544,6 +544,7 @@ export const Overview: React.FC = () => { + {showSyncTriggerCallout && syncTriggerCallout} {groups.length > 0 && groupsSummary} {details.length > 0 && {detailsSummary}} {!custom && serviceTypeSupportsPermissions && ( @@ -587,7 +588,6 @@ export const Overview: React.FC = () => { )} )} - {showSyncTriggerCallout && syncTriggerCallout} From 3bde2ec0efa489b379df0cb60ccd76a317106855 Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Tue, 30 Nov 2021 12:38:14 +0300 Subject: [PATCH 045/224] [plugin_functional] remove doc_views test (#119424) * [plugin_functional] remove data views test * [plugin_functional] fix tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/plugin_functional/config.ts | 1 - .../test_suites/doc_views/doc_views.ts | 45 ------------------- .../test_suites/doc_views/index.ts | 22 --------- 3 files changed, 68 deletions(-) delete mode 100644 test/plugin_functional/test_suites/doc_views/doc_views.ts delete mode 100644 test/plugin_functional/test_suites/doc_views/index.ts diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index 8ac1633e61e4..5b7446370379 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -29,7 +29,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./test_suites/panel_actions'), require.resolve('./test_suites/core_plugins'), require.resolve('./test_suites/management'), - require.resolve('./test_suites/doc_views'), require.resolve('./test_suites/application_links'), require.resolve('./test_suites/data_plugin'), require.resolve('./test_suites/saved_objects_management'), diff --git a/test/plugin_functional/test_suites/doc_views/doc_views.ts b/test/plugin_functional/test_suites/doc_views/doc_views.ts deleted file mode 100644 index e9a6434e8f33..000000000000 --- a/test/plugin_functional/test_suites/doc_views/doc_views.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import expect from '@kbn/expect'; -import { PluginFunctionalProviderContext } from '../../services'; - -export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { - const testSubjects = getService('testSubjects'); - const find = getService('find'); - const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); - - describe('custom doc views', function () { - before(async () => { - await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - }); - - it('should show custom doc views', async () => { - await testSubjects.click('docTableExpandToggleColumn'); - const angularTab = await find.byButtonText('Angular doc view'); - const reactTab = await find.byButtonText('React doc view'); - expect(await angularTab.isDisplayed()).to.be(true); - expect(await reactTab.isDisplayed()).to.be(true); - }); - - it('should render angular doc view', async () => { - const angularTab = await find.byButtonText('Angular doc view'); - await angularTab.click(); - const angularContent = await testSubjects.find('angular-docview'); - expect(await angularContent.getVisibleText()).to.be('logstash-2015.09.22'); - }); - - it('should render react doc view', async () => { - const reactTab = await find.byButtonText('React doc view'); - await reactTab.click(); - const reactContent = await testSubjects.find('react-docview'); - expect(await reactContent.getVisibleText()).to.be('logstash-2015.09.22'); - }); - }); -} diff --git a/test/plugin_functional/test_suites/doc_views/index.ts b/test/plugin_functional/test_suites/doc_views/index.ts deleted file mode 100644 index a790f062e32d..000000000000 --- a/test/plugin_functional/test_suites/doc_views/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { PluginFunctionalProviderContext } from '../../services'; - -export default function ({ getService, loadTestFile }: PluginFunctionalProviderContext) { - const esArchiver = getService('esArchiver'); - - // SKIPPED: https://github.com/elastic/kibana/issues/100060 - describe.skip('doc views', function () { - before(async () => { - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/discover'); - }); - - loadTestFile(require.resolve('./doc_views')); - }); -} From d1bc2641a6eff20e8e4e5b0bb09992d4a0a8bfc8 Mon Sep 17 00:00:00 2001 From: Giorgos Bamparopoulos Date: Tue, 30 Nov 2021 10:02:31 +0000 Subject: [PATCH 046/224] [APM] Add Agent Keys in APM settings - Agent key table (#119543) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add agent keys table * Add support for invalidating keys Co-authored-by: SĂžren Louv-Jansen Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../Settings/agent_keys/agent_keys_table.tsx | 168 ++++++++++++++++ .../agent_keys/confirm_delete_modal.tsx | 84 ++++++++ .../app/Settings/agent_keys/index.tsx | 186 ++++++++++++++++++ .../prompts/api_keys_not_enabled.tsx | 56 ++++++ .../agent_keys/prompts/permission_denied.tsx | 38 ++++ .../components/routing/settings/index.tsx | 9 + .../routing/templates/settings_template.tsx | 14 +- .../routes/agent_keys/get_agent_keys.ts | 45 +++++ .../agent_keys/get_agent_keys_privileges.ts | 55 ++++++ .../routes/agent_keys/invalidate_agent_key.ts | 29 +++ .../apm/server/routes/agent_keys/route.ts | 85 ++++++++ .../get_global_apm_server_route_repository.ts | 5 +- 12 files changed, 772 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/Settings/agent_keys/agent_keys_table.tsx create mode 100644 x-pack/plugins/apm/public/components/app/Settings/agent_keys/confirm_delete_modal.tsx create mode 100644 x-pack/plugins/apm/public/components/app/Settings/agent_keys/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/Settings/agent_keys/prompts/api_keys_not_enabled.tsx create mode 100644 x-pack/plugins/apm/public/components/app/Settings/agent_keys/prompts/permission_denied.tsx create mode 100644 x-pack/plugins/apm/server/routes/agent_keys/get_agent_keys.ts create mode 100644 x-pack/plugins/apm/server/routes/agent_keys/get_agent_keys_privileges.ts create mode 100644 x-pack/plugins/apm/server/routes/agent_keys/invalidate_agent_key.ts create mode 100644 x-pack/plugins/apm/server/routes/agent_keys/route.ts diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/agent_keys_table.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/agent_keys_table.tsx new file mode 100644 index 000000000000..4a05f38d8e50 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/agent_keys_table.tsx @@ -0,0 +1,168 @@ +/* + * 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, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiInMemoryTable, + EuiBasicTableColumn, + EuiInMemoryTableProps, +} from '@elastic/eui'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { ApiKey } from '../../../../../../security/common/model'; +import { ConfirmDeleteModal } from './confirm_delete_modal'; + +interface Props { + agentKeys: ApiKey[]; + refetchAgentKeys: () => void; +} + +export function AgentKeysTable({ agentKeys, refetchAgentKeys }: Props) { + const [agentKeyToBeDeleted, setAgentKeyToBeDeleted] = useState(); + + const columns: Array> = [ + { + field: 'name', + name: i18n.translate( + 'xpack.apm.settings.agentKeys.table.nameColumnName', + { + defaultMessage: 'Name', + } + ), + sortable: true, + }, + { + field: 'username', + name: i18n.translate( + 'xpack.apm.settings.agentKeys.table.userNameColumnName', + { + defaultMessage: 'User', + } + ), + sortable: true, + }, + { + field: 'realm', + name: i18n.translate( + 'xpack.apm.settings.agentKeys.table.realmColumnName', + { + defaultMessage: 'Realm', + } + ), + sortable: true, + }, + { + field: 'creation', + name: i18n.translate( + 'xpack.apm.settings.agentKeys.table.creationColumnName', + { + defaultMessage: 'Created', + } + ), + dataType: 'date', + sortable: true, + mobileOptions: { + show: false, + }, + render: (date: number) => , + }, + { + actions: [ + { + name: i18n.translate( + 'xpack.apm.settings.agentKeys.table.deleteActionTitle', + { + defaultMessage: 'Delete', + } + ), + description: i18n.translate( + 'xpack.apm.settings.agentKeys.table.deleteActionDescription', + { + defaultMessage: 'Delete this agent key', + } + ), + icon: 'trash', + color: 'danger', + type: 'icon', + onClick: (agentKey: ApiKey) => setAgentKeyToBeDeleted(agentKey), + }, + ], + }, + ]; + + const search: EuiInMemoryTableProps['search'] = { + box: { + incremental: true, + }, + filters: [ + { + type: 'field_value_selection', + field: 'username', + name: i18n.translate( + 'xpack.apm.settings.agentKeys.table.userFilterLabel', + { + defaultMessage: 'User', + } + ), + multiSelect: 'or', + operator: 'exact', + options: Object.keys( + agentKeys.reduce((acc: Record, { username }) => { + acc[username] = true; + return acc; + }, {}) + ).map((value) => ({ value })), + }, + { + type: 'field_value_selection', + field: 'realm', + name: i18n.translate( + 'xpack.apm.settings.agentKeys.table.realmFilterLabel', + { + defaultMessage: 'Realm', + } + ), + multiSelect: 'or', + operator: 'exact', + options: Object.keys( + agentKeys.reduce((acc: Record, { realm }) => { + acc[realm] = true; + return acc; + }, {}) + ).map((value) => ({ value })), + }, + ], + }; + + return ( + + + {agentKeyToBeDeleted && ( + setAgentKeyToBeDeleted(undefined)} + agentKey={agentKeyToBeDeleted} + onConfirm={() => { + setAgentKeyToBeDeleted(undefined); + refetchAgentKeys(); + }} + /> + )} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/confirm_delete_modal.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/confirm_delete_modal.tsx new file mode 100644 index 000000000000..6125a238f11a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/confirm_delete_modal.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiConfirmModal } from '@elastic/eui'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { callApmApi } from '../../../../services/rest/createCallApmApi'; +import { ApiKey } from '../../../../../../security/common/model'; + +interface Props { + agentKey: ApiKey; + onCancel: () => void; + onConfirm: () => void; +} + +export function ConfirmDeleteModal({ agentKey, onCancel, onConfirm }: Props) { + const [isDeleting, setIsDeleting] = useState(false); + const { toasts } = useApmPluginContext().core.notifications; + const { id, name } = agentKey; + + const deleteAgentKey = async () => { + try { + await callApmApi({ + endpoint: 'POST /internal/apm/api_key/invalidate', + signal: null, + params: { + body: { id }, + }, + }); + toasts.addSuccess( + i18n.translate('xpack.apm.settings.agentKeys.invalidate.succeeded', { + defaultMessage: 'Deleted agent key "{name}"', + values: { name }, + }) + ); + } catch (error) { + toasts.addDanger( + i18n.translate('xpack.apm.settings.agentKeys.invalidate.failed', { + defaultMessage: 'Error deleting agent key "{name}"', + values: { name }, + }) + ); + } + }; + + return ( + { + setIsDeleting(true); + await deleteAgentKey(); + setIsDeleting(false); + onConfirm(); + }} + cancelButtonText={i18n.translate( + 'xpack.apm.settings.agentKeys.deleteConfirmModal.cancel', + { + defaultMessage: 'Cancel', + } + )} + confirmButtonText={i18n.translate( + 'xpack.apm.settings.agentKeys.deleteConfirmModal.delete', + { + defaultMessage: 'Delete', + } + )} + confirmButtonDisabled={isDeleting} + buttonColor="danger" + defaultFocusedButton="confirm" + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/index.tsx new file mode 100644 index 000000000000..23acc2e98dd7 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/index.tsx @@ -0,0 +1,186 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { + EuiText, + EuiSpacer, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiEmptyPrompt, + EuiButton, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { PermissionDenied } from './prompts/permission_denied'; +import { ApiKeysNotEnabled } from './prompts/api_keys_not_enabled'; +import { AgentKeysTable } from './agent_keys_table'; + +const INITIAL_DATA = { + areApiKeysEnabled: false, + canManage: false, +}; + +export function AgentKeys() { + return ( + + + {i18n.translate('xpack.apm.settings.agentKeys.descriptionText', { + defaultMessage: + 'View and delete agent keys. An agent key sends requests on behalf of a user.', + })} + + + + + +

+ {i18n.translate('xpack.apm.settings.agentKeys.title', { + defaultMessage: 'Agent keys', + })} +

+
+
+
+ + +
+ ); +} + +function AgentKeysContent() { + const { + data: { areApiKeysEnabled, canManage } = INITIAL_DATA, + status: privilegesStatus, + } = useFetcher( + (callApmApi) => { + return callApmApi({ + endpoint: 'GET /internal/apm/agent_keys/privileges', + }); + }, + [], + { showToastOnError: false } + ); + + const { + data, + status, + refetch: refetchAgentKeys, + } = useFetcher( + (callApmApi) => { + if (areApiKeysEnabled && canManage) { + return callApmApi({ + endpoint: 'GET /internal/apm/agent_keys', + }); + } + }, + [areApiKeysEnabled, canManage], + { showToastOnError: false } + ); + + const agentKeys = data?.agentKeys; + const isLoading = + privilegesStatus === FETCH_STATUS.LOADING || + status === FETCH_STATUS.LOADING; + + const requestFailed = + privilegesStatus === FETCH_STATUS.FAILURE || + status === FETCH_STATUS.FAILURE; + + if (!agentKeys) { + if (isLoading) { + return ( + } + titleSize="xs" + title={ +

+ {i18n.translate( + 'xpack.apm.settings.agentKeys.agentKeysLoadingPromptTitle', + { + defaultMessage: 'Loading Agent keys...', + } + )} +

+ } + /> + ); + } + + if (requestFailed) { + return ( + + {i18n.translate( + 'xpack.apm.settings.agentKeys.agentKeysErrorPromptTitle', + { + defaultMessage: 'Could not load agent keys.', + } + )} + + } + /> + ); + } + + if (!canManage) { + return ; + } + + if (!areApiKeysEnabled) { + return ; + } + } + + if (agentKeys && isEmpty(agentKeys)) { + return ( + + {i18n.translate('xpack.apm.settings.agentKeys.emptyPromptTitle', { + defaultMessage: 'Create your first agent key', + })} + + } + body={ +

+ {i18n.translate('xpack.apm.settings.agentKeys.emptyPromptBody', { + defaultMessage: + 'Create agent keys to authorize requests to the APM Server.', + })} +

+ } + actions={ + + {i18n.translate( + 'xpack.apm.settings.agentKeys.createAgentKeyButton', + { + defaultMessage: 'Create agent key', + } + )} + + } + /> + ); + } + + if (agentKeys && !isEmpty(agentKeys)) { + return ( + + ); + } + + return null; +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/prompts/api_keys_not_enabled.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/prompts/api_keys_not_enabled.tsx new file mode 100644 index 000000000000..5d667b9f3e1a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/prompts/api_keys_not_enabled.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiEmptyPrompt, EuiLink } from '@elastic/eui'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; + +export function ApiKeysNotEnabled() { + const { + services: { docLinks }, + } = useKibana(); + + return ( + + {i18n.translate( + 'xpack.apm.settings.agentKeys.apiKeysDisabledErrorTitle', + { + defaultMessage: 'API keys not enabled in Elasticsearch', + } + )} + + } + iconType="alert" + body={ +

+ + {i18n.translate( + 'xpack.apm.settings.agentKeys.apiKeysDisabledErrorLinkText', + { + defaultMessage: 'docs', + } + )} + + ), + }} + /> +

+ } + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/prompts/permission_denied.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/prompts/permission_denied.tsx new file mode 100644 index 000000000000..bcac32bbaa3b --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/prompts/permission_denied.tsx @@ -0,0 +1,38 @@ +/* + * 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 { EuiEmptyPrompt } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +export function PermissionDenied() { + return ( + + {i18n.translate( + 'xpack.apm.settings.agentKeys.noPermissionToManagelApiKeysTitle', + { + defaultMessage: 'You need permission to manage API keys', + } + )} + + } + body={ +

+ {i18n.translate( + 'xpack.apm.settings.agentKeys.noPermissionToManagelApiKeysDescription', + { + defaultMessage: 'Contact your system administrator', + } + )} +

+ } + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/routing/settings/index.tsx b/x-pack/plugins/apm/public/components/routing/settings/index.tsx index e33f60e5593b..45af7e62260b 100644 --- a/x-pack/plugins/apm/public/components/routing/settings/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/settings/index.tsx @@ -19,6 +19,7 @@ import { ApmIndices } from '../../app/Settings/ApmIndices'; import { CustomizeUI } from '../../app/Settings/customize_ui'; import { Schema } from '../../app/Settings/schema'; import { AnomalyDetection } from '../../app/Settings/anomaly_detection'; +import { AgentKeys } from '../../app/Settings/agent_keys'; function page({ path, @@ -132,6 +133,14 @@ export const settings = { element: , tab: 'anomaly-detection', }), + page({ + path: '/settings/agent-keys', + title: i18n.translate('xpack.apm.views.settings.agentKeys.title', { + defaultMessage: 'Agent keys', + }), + element: , + tab: 'agent-keys', + }), { path: '/settings', element: , diff --git a/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx index ecca2ddb07ec..dabe9043495b 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx @@ -21,7 +21,8 @@ type Tab = NonNullable[0] & { | 'anomaly-detection' | 'apm-indices' | 'customize-ui' - | 'schema'; + | 'schema' + | 'agent-keys'; hidden?: boolean; }; @@ -116,6 +117,17 @@ function getTabs({ }), href: getLegacyApmHref({ basePath, path: `/settings/schema`, search }), }, + { + key: 'agent-keys', + label: i18n.translate('xpack.apm.settings.agentKeys', { + defaultMessage: 'Agent Keys', + }), + href: getLegacyApmHref({ + basePath, + path: `/settings/agent-keys`, + search, + }), + }, ]; return tabs diff --git a/x-pack/plugins/apm/server/routes/agent_keys/get_agent_keys.ts b/x-pack/plugins/apm/server/routes/agent_keys/get_agent_keys.ts new file mode 100644 index 000000000000..9c5b3e04c94f --- /dev/null +++ b/x-pack/plugins/apm/server/routes/agent_keys/get_agent_keys.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ApmPluginRequestHandlerContext } from '../typings'; +import { ApiKey } from '../../../../security/common/model'; + +export async function getAgentKeys({ + context, +}: { + context: ApmPluginRequestHandlerContext; +}) { + const body = { + size: 1000, + query: { + bool: { + filter: [ + { + term: { + 'metadata.application': 'apm', + }, + }, + ], + }, + }, + }; + + const esClient = context.core.elasticsearch.client; + const apiResponse = await esClient.asCurrentUser.transport.request<{ + api_keys: ApiKey[]; + }>({ + method: 'GET', + path: '_security/_query/api_key', + body, + }); + + const agentKeys = apiResponse.body.api_keys.filter( + ({ invalidated }) => !invalidated + ); + return { + agentKeys, + }; +} diff --git a/x-pack/plugins/apm/server/routes/agent_keys/get_agent_keys_privileges.ts b/x-pack/plugins/apm/server/routes/agent_keys/get_agent_keys_privileges.ts new file mode 100644 index 000000000000..4aed9314f433 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/agent_keys/get_agent_keys_privileges.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ApmPluginRequestHandlerContext } from '../typings'; +import { APMPluginStartDependencies } from '../../types'; + +interface SecurityHasPrivilegesResponse { + cluster: { + manage_security: boolean; + manage_api_key: boolean; + manage_own_api_key: boolean; + }; +} + +export async function getAgentKeysPrivileges({ + context, + securityPluginStart, +}: { + context: ApmPluginRequestHandlerContext; + securityPluginStart: NonNullable; +}) { + const [securityHasPrivilegesResponse, areApiKeysEnabled] = await Promise.all([ + context.core.elasticsearch.client.asCurrentUser.security.hasPrivileges( + { + body: { + cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'], + }, + } + ), + securityPluginStart.authc.apiKeys.areAPIKeysEnabled(), + ]); + + const { + body: { + cluster: { + manage_security: manageSecurity, + manage_api_key: manageApiKey, + manage_own_api_key: manageOwnApiKey, + }, + }, + } = securityHasPrivilegesResponse; + + const isAdmin = manageSecurity || manageApiKey; + const canManage = manageSecurity || manageApiKey || manageOwnApiKey; + + return { + areApiKeysEnabled, + isAdmin, + canManage, + }; +} diff --git a/x-pack/plugins/apm/server/routes/agent_keys/invalidate_agent_key.ts b/x-pack/plugins/apm/server/routes/agent_keys/invalidate_agent_key.ts new file mode 100644 index 000000000000..e2f86298efdc --- /dev/null +++ b/x-pack/plugins/apm/server/routes/agent_keys/invalidate_agent_key.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 { ApmPluginRequestHandlerContext } from '../typings'; + +export async function invalidateAgentKey({ + context, + id, +}: { + context: ApmPluginRequestHandlerContext; + id: string; +}) { + const { + body: { invalidated_api_keys: invalidatedAgentKeys }, + } = await context.core.elasticsearch.client.asCurrentUser.security.invalidateApiKey( + { + body: { + ids: [id], + }, + } + ); + + return { + invalidatedAgentKeys, + }; +} diff --git a/x-pack/plugins/apm/server/routes/agent_keys/route.ts b/x-pack/plugins/apm/server/routes/agent_keys/route.ts new file mode 100644 index 000000000000..e5f40205b291 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/agent_keys/route.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import { i18n } from '@kbn/i18n'; +import * as t from 'io-ts'; +import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; +import { createApmServerRouteRepository } from '../apm_routes/create_apm_server_route_repository'; +import { getAgentKeys } from './get_agent_keys'; +import { getAgentKeysPrivileges } from './get_agent_keys_privileges'; +import { invalidateAgentKey } from './invalidate_agent_key'; + +const agentKeysRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/agent_keys', + options: { tags: ['access:apm'] }, + + handler: async (resources) => { + const { context } = resources; + const agentKeys = await getAgentKeys({ + context, + }); + + return agentKeys; + }, +}); + +const agentKeysPrivilegesRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/agent_keys/privileges', + options: { tags: ['access:apm'] }, + + handler: async (resources) => { + const { + plugins: { security }, + context, + } = resources; + + if (!security) { + throw Boom.internal(SECURITY_REQUIRED_MESSAGE); + } + + const securityPluginStart = await security.start(); + const agentKeysPrivileges = await getAgentKeysPrivileges({ + context, + securityPluginStart, + }); + + return agentKeysPrivileges; + }, +}); + +const invalidateAgentKeyRoute = createApmServerRoute({ + endpoint: 'POST /internal/apm/api_key/invalidate', + options: { tags: ['access:apm', 'access:apm_write'] }, + params: t.type({ + body: t.type({ id: t.string }), + }), + handler: async (resources) => { + const { context, params } = resources; + + const { + body: { id }, + } = params; + + const invalidatedKeys = await invalidateAgentKey({ + context, + id, + }); + + return invalidatedKeys; + }, +}); + +export const agentKeysRouteRepository = createApmServerRouteRepository() + .add(agentKeysRoute) + .add(agentKeysPrivilegesRoute) + .add(invalidateAgentKeyRoute); + +const SECURITY_REQUIRED_MESSAGE = i18n.translate( + 'xpack.apm.api.apiKeys.securityRequired', + { defaultMessage: 'Security plugin is required' } +); diff --git a/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts index ee4c9d1c8cfa..1462e7540650 100644 --- a/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts @@ -37,6 +37,7 @@ import { APMRouteHandlerResources } from '../typings'; import { historicalDataRouteRepository } from '../historical_data'; import { eventMetadataRouteRepository } from '../event_metadata/route'; import { suggestionsRouteRepository } from '../suggestions/route'; +import { agentKeysRouteRepository } from '../agent_keys/route'; const getTypedGlobalApmServerRouteRepository = () => { const repository = createApmServerRouteRepository() @@ -64,7 +65,9 @@ const getTypedGlobalApmServerRouteRepository = () => { .merge(correlationsRouteRepository) .merge(fallbackToTransactionsRouteRepository) .merge(historicalDataRouteRepository) - .merge(eventMetadataRouteRepository); + .merge(eventMetadataRouteRepository) + .merge(eventMetadataRouteRepository) + .merge(agentKeysRouteRepository); return repository; }; From a8d199c8df31b51f2199ac0814338a955ca45881 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 30 Nov 2021 11:04:57 +0100 Subject: [PATCH 047/224] specify date format for time split filters (#119504) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/data/common/search/aggs/agg_configs.test.ts | 2 ++ src/plugins/data/common/search/aggs/agg_configs.ts | 1 + src/plugins/data/common/search/aggs/utils/time_splits.ts | 1 + 3 files changed, 4 insertions(+) diff --git a/src/plugins/data/common/search/aggs/agg_configs.test.ts b/src/plugins/data/common/search/aggs/agg_configs.test.ts index 104cd3b2815b..80e5a079cfd5 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.test.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.test.ts @@ -360,6 +360,7 @@ describe('AggConfigs', () => { "0": Object { "range": Object { "@timestamp": Object { + "format": "strict_date_optional_time", "gte": "2021-05-05T00:00:00.000Z", "lte": "2021-05-10T00:00:00.000Z", }, @@ -368,6 +369,7 @@ describe('AggConfigs', () => { "86400000": Object { "range": Object { "@timestamp": Object { + "format": "strict_date_optional_time", "gte": "2021-05-04T00:00:00.000Z", "lte": "2021-05-09T00:00:00.000Z", }, diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts index 9a362466c0fd..a022abba7fb4 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.ts @@ -406,6 +406,7 @@ export class AggConfigs { .map(([filter, field]) => ({ range: { [field]: { + format: 'strict_date_optional_time', gte: moment(filter?.query.range[field].gte).subtract(shift).toISOString(), lte: moment(filter?.query.range[field].lte).subtract(shift).toISOString(), }, diff --git a/src/plugins/data/common/search/aggs/utils/time_splits.ts b/src/plugins/data/common/search/aggs/utils/time_splits.ts index c4a603a383e3..2eb49d76c608 100644 --- a/src/plugins/data/common/search/aggs/utils/time_splits.ts +++ b/src/plugins/data/common/search/aggs/utils/time_splits.ts @@ -430,6 +430,7 @@ export function insertTimeShiftSplit( filters[key] = { range: { [timeField]: { + format: 'strict_date_optional_time', gte: moment(timeFilter.query.range[timeField].gte).subtract(shift).toISOString(), lte: moment(timeFilter.query.range[timeField].lte).subtract(shift).toISOString(), }, From 6d20bf39fdade73ff9ab8ca08ab69c06ea9a4c41 Mon Sep 17 00:00:00 2001 From: Ignacio Rivas Date: Tue, 30 Nov 2021 11:16:04 +0100 Subject: [PATCH 048/224] [Index Management] Improve error messages when editing index settings (#119144) * Improve error message and start writting tests * Refactor tests * Fix tests * commit using @elastic.co * Refactor test * Add documentation * Fix ts types * Address CR changes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../helpers/http_requests.ts | 18 ++++++ .../helpers/test_subjects.ts | 2 + .../home/indices_tab.test.ts | 64 +++++++++++++++++++ .../edit_settings_json/edit_settings_json.js | 25 +++++--- .../public/application/services/api.ts | 17 +++-- .../store/actions/update_index_settings.js | 8 +-- 6 files changed, 113 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts index b9f1191da8af..e26eeadd4edc 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts @@ -10,6 +10,12 @@ import { API_BASE_PATH } from '../../../common/constants'; type HttpResponse = Record | any[]; +export interface ResponseError { + statusCode: number; + message: string | Error; + attributes?: Record; +} + // Register helpers to mock HTTP Requests const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { const setLoadTemplatesResponse = (response: HttpResponse = []) => { @@ -101,6 +107,17 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setUpdateIndexSettingsResponse = (response?: HttpResponse, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ?? response; + + server.respondWith('PUT', `${API_BASE_PATH}/settings/:name`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + const setSimulateTemplateResponse = (response?: HttpResponse, error?: any) => { const status = error ? error.status || 400 : 200; const body = error ? JSON.stringify(error.body) : JSON.stringify(response); @@ -134,6 +151,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setLoadTemplateResponse, setCreateTemplateResponse, setUpdateTemplateResponse, + setUpdateIndexSettingsResponse, setSimulateTemplateResponse, setLoadComponentTemplatesResponse, }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts index 96775484e073..ac4b4c46ad4d 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts @@ -60,4 +60,6 @@ export type TestSubjects = | 'templateTable' | 'title' | 'unfreezeIndexMenuButton' + | 'updateEditIndexSettingsButton' + | 'updateIndexSettingsErrorCallout' | 'viewButton'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts index 79fe885820fa..ec80bf5d712c 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts @@ -12,6 +12,31 @@ import { setupEnvironment, nextTick } from '../helpers'; import { IndicesTestBed, setup } from './indices_tab.helpers'; import { createDataStreamPayload, createNonDataStreamIndex } from './data_streams_tab.helpers'; +// Since the editor component being used for editing index settings is not a React +// component but an editor being instantiated on a div reference, we cannot mock +// the component and replace it with something else. In this particular case we're +// mocking the returned instance of the editor to always have the same values. +const mockGetAceEditorValue = jest.fn().mockReturnValue(`{}`); + +jest.mock('../../../public/application/lib/ace.js', () => { + const createAceEditor = () => { + return { + getValue: mockGetAceEditorValue, + getSession: () => { + return { + on: () => null, + getValue: () => null, + }; + }, + destroy: () => null, + }; + }; + + return { + createAceEditor, + }; +}); + /** * The below import is required to avoid a console error warn from the "brace" package * console.warn ../node_modules/brace/index.js:3999 @@ -212,4 +237,43 @@ describe('', () => { expect(exists('unfreezeIndexMenuButton')).toBe(false); }); }); + + describe('Edit index settings', () => { + const indexName = 'testIndex'; + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadIndicesResponse([createNonDataStreamIndex(indexName)]); + + testBed = await setup(); + const { find, component } = testBed; + component.update(); + + find('indexTableIndexNameLink').at(0).simulate('click'); + }); + + test('shows error callout when request fails', async () => { + const { actions, find, component, exists } = testBed; + + mockGetAceEditorValue.mockReturnValue(`{ + "index.routing.allocation.include._tier_preference": "non_existent_tier" + }`); + + const error = { + statusCode: 400, + error: 'Bad Request', + message: 'invalid tier names found in ...', + }; + httpRequestsMockHelpers.setUpdateIndexSettingsResponse(undefined, error); + + await actions.selectIndexDetailsTab('edit_settings'); + + await act(async () => { + find('updateEditIndexSettingsButton').simulate('click'); + }); + + component.update(); + + expect(exists('updateIndexSettingsErrorCallout')).toBe(true); + }); + }); }); diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.js index 2337485e6c82..55581190b89b 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.js @@ -6,6 +6,7 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { documentationService } from '../../../../../services/documentation'; @@ -13,10 +14,9 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, - EuiIcon, + EuiCallOut, EuiLink, EuiSpacer, - EuiTextColor, EuiTitle, } from '@elastic/eui'; import { TAB_SETTINGS } from '../../../../../constants'; @@ -90,16 +90,25 @@ export class EditSettingsJson extends React.PureComponent { }; errorMessage() { const { error } = this.props; + if (!error) { return null; } + return ( -
- - - {error} + <> -
+ +

{error}

+
+ ); } render() { @@ -135,6 +144,7 @@ export class EditSettingsJson extends React.PureComponent {
+ {this.errorMessage()} - {this.errorMessage()}
); diff --git a/x-pack/plugins/index_management/public/application/services/api.ts b/x-pack/plugins/index_management/public/application/services/api.ts index 5cfb881cb22c..972d7c6c87f3 100644 --- a/x-pack/plugins/index_management/public/application/services/api.ts +++ b/x-pack/plugins/index_management/public/application/services/api.ts @@ -194,14 +194,17 @@ export async function loadIndexSettings(indexName: string) { } export async function updateIndexSettings(indexName: string, body: object) { - const response = await httpService.httpClient.put( - `${API_BASE_PATH}/settings/${encodeURIComponent(indexName)}`, - { - body: JSON.stringify(body), - } - ); + const response = await sendRequest({ + path: `${API_BASE_PATH}/settings/${encodeURIComponent(indexName)}`, + method: 'put', + body: JSON.stringify(body), + }); + // Only track successful requests. - uiMetricService.trackMetric(METRIC_TYPE.COUNT, UIM_UPDATE_SETTINGS); + if (!response.error) { + uiMetricService.trackMetric(METRIC_TYPE.COUNT, UIM_UPDATE_SETTINGS); + } + return response; } diff --git a/x-pack/plugins/index_management/public/application/store/actions/update_index_settings.js b/x-pack/plugins/index_management/public/application/store/actions/update_index_settings.js index bbb251634976..22ebf78fc5fe 100644 --- a/x-pack/plugins/index_management/public/application/store/actions/update_index_settings.js +++ b/x-pack/plugins/index_management/public/application/store/actions/update_index_settings.js @@ -22,13 +22,9 @@ export const updateIndexSettings = ({ indexName, settings }) => async (dispatch) => { if (Object.keys(settings).length !== 0) { - try { - const { error, message } = await request(indexName, settings); + const { error } = await request(indexName, settings); - if (error) { - return dispatch(updateIndexSettingsError({ error: message })); - } - } catch (error) { + if (error) { return dispatch(updateIndexSettingsError({ error: error.message })); } } From b8a7370156d51128e75815fde8c328d878d712ab Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 30 Nov 2021 12:35:07 +0100 Subject: [PATCH 049/224] [Search] Add cancelation logic to search example (#118176) --- dev_docs/tutorials/data/search.mdx | 6 + .../search_examples/public/search/app.tsx | 106 +++++++++++++++--- .../server/routes/server_search_route.ts | 66 ++++++----- 3 files changed, 136 insertions(+), 42 deletions(-) diff --git a/dev_docs/tutorials/data/search.mdx b/dev_docs/tutorials/data/search.mdx index 425736ddb03b..0787c44b632e 100644 --- a/dev_docs/tutorials/data/search.mdx +++ b/dev_docs/tutorials/data/search.mdx @@ -129,6 +129,12 @@ setTimeout(() => { }, 1000); ``` + + Users might no longer be interested in search results. For example, they might start a new search + or leave your app without waiting for the results. You should handle such cases by using + `AbortController` with search API. + + #### Search strategies By default, the search service uses the DSL query and aggregation syntax and returns the response from Elasticsearch as is. It also provides several additional basic strategies, such as Async DSL (`x-pack` default) and EQL. diff --git a/examples/search_examples/public/search/app.tsx b/examples/search_examples/public/search/app.tsx index 1f8cda9443fa..eeceab569d3b 100644 --- a/examples/search_examples/public/search/app.tsx +++ b/examples/search_examples/public/search/app.tsx @@ -47,6 +47,7 @@ import { isErrorResponse, } from '../../../../src/plugins/data/public'; import { IMyStrategyResponse } from '../../common/types'; +import { AbortError } from '../../../../src/plugins/kibana_utils/common'; interface SearchExamplesAppDeps { notifications: CoreStart['notifications']; @@ -102,6 +103,8 @@ export const SearchExamplesApp = ({ IndexPatternField | null | undefined >(); const [request, setRequest] = useState>({}); + const [isLoading, setIsLoading] = useState(false); + const [currentAbortController, setAbortController] = useState(); const [rawResponse, setRawResponse] = useState>({}); const [selectedTab, setSelectedTab] = useState(0); @@ -187,16 +190,23 @@ export const SearchExamplesApp = ({ ...(strategy ? { get_cool: getCool } : {}), }; + const abortController = new AbortController(); + setAbortController(abortController); + // Submit the search request using the `data.search` service. setRequest(req.params.body); - const searchSubscription$ = data.search + setIsLoading(true); + + data.search .search(req, { strategy, sessionId, + abortSignal: abortController.signal, }) .subscribe({ next: (res) => { if (isCompleteResponse(res)) { + setIsLoading(false); setResponse(res); const avgResult: number | undefined = res.rawResponse.aggregations ? // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response @@ -226,7 +236,6 @@ export const SearchExamplesApp = ({ toastLifeTimeMs: 300000, } ); - searchSubscription$.unsubscribe(); if (res.warning) { notifications.toasts.addWarning({ title: 'Warning', @@ -236,14 +245,20 @@ export const SearchExamplesApp = ({ } else if (isErrorResponse(res)) { // TODO: Make response error status clearer notifications.toasts.addDanger('An error has occurred'); - searchSubscription$.unsubscribe(); } }, error: (e) => { - notifications.toasts.addDanger({ - title: 'Failed to run search', - text: e.message, - }); + setIsLoading(false); + if (e instanceof AbortError) { + notifications.toasts.addWarning({ + title: e.message, + }); + } else { + notifications.toasts.addDanger({ + title: 'Failed to run search', + text: e.message, + }); + } }, }); }; @@ -286,7 +301,12 @@ export const SearchExamplesApp = ({ } setRequest(searchSource.getSearchRequestBody()); - const { rawResponse: res } = await searchSource.fetch$().toPromise(); + const abortController = new AbortController(); + setAbortController(abortController); + setIsLoading(true); + const { rawResponse: res } = await searchSource + .fetch$({ abortSignal: abortController.signal }) + .toPromise(); setRawResponse(res); const message = Searched {res.hits.total} documents.; @@ -301,7 +321,18 @@ export const SearchExamplesApp = ({ ); } catch (e) { setRawResponse(e.body); - notifications.toasts.addWarning(`An error has occurred: ${e.message}`); + if (e instanceof AbortError) { + notifications.toasts.addWarning({ + title: e.message, + }); + } else { + notifications.toasts.addDanger({ + title: 'Failed to run search', + text: e.message, + }); + } + } finally { + setIsLoading(false); } }; @@ -329,32 +360,44 @@ export const SearchExamplesApp = ({ }, }; + const abortController = new AbortController(); + setAbortController(abortController); + // Submit the search request using the `data.search` service. setRequest(req.params); - const searchSubscription$ = data.search + setIsLoading(true); + data.search .search(req, { strategy: 'fibonacciStrategy', + abortSignal: abortController.signal, }) .subscribe({ next: (res) => { setResponse(res); if (isCompleteResponse(res)) { + setIsLoading(false); notifications.toasts.addSuccess({ title: 'Query result', text: 'Query finished', }); - searchSubscription$.unsubscribe(); } else if (isErrorResponse(res)) { + setIsLoading(false); // TODO: Make response error status clearer notifications.toasts.addWarning('An error has occurred'); - searchSubscription$.unsubscribe(); } }, error: (e) => { - notifications.toasts.addDanger({ - title: 'Failed to run search', - text: e.message, - }); + setIsLoading(false); + if (e instanceof AbortError) { + notifications.toasts.addWarning({ + title: e.message, + }); + } else { + notifications.toasts.addDanger({ + title: 'Failed to run search', + text: e.message, + }); + } }, }); }; @@ -365,17 +408,32 @@ export const SearchExamplesApp = ({ const onServerClickHandler = async () => { if (!indexPattern || !selectedNumericField) return; + const abortController = new AbortController(); + setAbortController(abortController); + setIsLoading(true); try { const res = await http.get(SERVER_SEARCH_ROUTE_PATH, { query: { index: indexPattern.title, field: selectedNumericField!.name, }, + signal: abortController.signal, }); notifications.toasts.addSuccess(`Server returned ${JSON.stringify(res)}`); } catch (e) { - notifications.toasts.addDanger('Failed to run search'); + if (e?.name === 'AbortError') { + notifications.toasts.addWarning({ + title: e.message, + }); + } else { + notifications.toasts.addDanger({ + title: 'Failed to run search', + text: e.message, + }); + } + } finally { + setIsLoading(false); } }; @@ -721,6 +779,11 @@ export const SearchExamplesApp = ({ strategy. This request does not take the configuration of{' '} TopNavMenu into account, but you could pass those down to the server as well. +
+ When executing search on the server, make sure to cancel the search in case user + cancels corresponding network request. This could happen in case user re-runs a + query or leaves the page without waiting for the result. Cancellation API is similar + on client and server and use `AbortController`. setSelectedTab(reqTabs.indexOf(tab))} /> + + {currentAbortController && isLoading && ( + currentAbortController?.abort()}> + + + )}
diff --git a/examples/search_examples/server/routes/server_search_route.ts b/examples/search_examples/server/routes/server_search_route.ts index 258587610a20..0d1302233a39 100644 --- a/examples/search_examples/server/routes/server_search_route.ts +++ b/examples/search_examples/server/routes/server_search_route.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { Observable } from 'rxjs'; import { IEsSearchRequest } from 'src/plugins/data/server'; import { schema } from '@kbn/config-schema'; import { IEsSearchResponse } from 'src/plugins/data/common'; @@ -26,36 +27,51 @@ export function registerServerSearchRoute(router: IRouter { const { index, field } = request.query; - // Run a synchronous search server side, by enforcing a high keepalive and waiting for completion. - // If you wish to run the search with polling (in basic+), you'd have to poll on the search API. - // Please reach out to the @app-arch-team if you need this to be implemented. - const res = await context - .search!.search( - { - params: { - index, - body: { - aggs: { - '1': { - avg: { - field, + + // User may abort the request without waiting for the results + // we need to handle this scenario by aborting underlying server requests + const abortSignal = getRequestAbortedSignal(request.events.aborted$); + + try { + const res = await context + .search!.search( + { + params: { + index, + body: { + aggs: { + '1': { + avg: { + field, + }, }, }, }, }, - waitForCompletionTimeout: '5m', - keepAlive: '5m', - }, - } as IEsSearchRequest, - {} - ) - .toPromise(); + } as IEsSearchRequest, + { abortSignal } + ) + .toPromise(); - return response.ok({ - body: { - aggs: (res as IEsSearchResponse).rawResponse.aggregations, - }, - }); + return response.ok({ + body: { + aggs: (res as IEsSearchResponse).rawResponse.aggregations, + }, + }); + } catch (e) { + return response.customError({ + statusCode: e.statusCode ?? 500, + body: { + message: e.message, + }, + }); + } } ); } + +function getRequestAbortedSignal(aborted$: Observable): AbortSignal { + const controller = new AbortController(); + aborted$.subscribe(() => controller.abort()); + return controller.signal; +} From 2ffe8d1c03364944c3014ff77e436f9fa53727bb Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Tue, 30 Nov 2021 13:14:10 +0100 Subject: [PATCH 050/224] Handle transient Elasticsearch errors during package installation (#118587) --- .../elasticsearch/datastream_ilm/install.ts | 24 ++-- .../services/epm/elasticsearch/ilm/install.ts | 20 +-- .../elasticsearch/ingest_pipeline/install.ts | 26 +++- .../epm/elasticsearch/ml_model/install.ts | 29 +++-- .../services/epm/elasticsearch/retry.test.ts | 88 +++++++++++++ .../services/epm/elasticsearch/retry.ts | 53 ++++++++ .../elasticsearch/template/install.test.ts | 5 + .../epm/elasticsearch/template/install.ts | 116 +++++++++++++----- .../epm/elasticsearch/template/template.ts | 40 ++++-- .../epm/elasticsearch/transform/install.ts | 29 +++-- .../elasticsearch/transform/transform.test.ts | 13 +- .../epm/packages/_install_package.test.ts | 2 + .../services/epm/packages/_install_package.ts | 31 +++-- .../server/services/epm/packages/install.ts | 3 + x-pack/plugins/fleet/server/services/setup.ts | 4 +- 15 files changed, 383 insertions(+), 100 deletions(-) create mode 100644 x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.test.ts create mode 100644 x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.ts diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/install.ts index 2e43fe44527b..a1075b15a646 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/install.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; +import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from 'kibana/server'; import { ElasticsearchAssetType } from '../../../../../common/types/models'; import type { @@ -18,6 +18,7 @@ import { saveInstalledEsRefs } from '../../packages/install'; import { getAsset } from '../transform/common'; import { getESAssetMetadata } from '../meta'; +import { retryTransientEsErrors } from '../retry'; import { deleteIlmRefs, deleteIlms } from './remove'; @@ -35,7 +36,8 @@ export const installIlmForDataStream = async ( registryPackage: InstallablePackage, paths: string[], esClient: ElasticsearchClient, - savedObjectsClient: SavedObjectsClientContract + savedObjectsClient: SavedObjectsClientContract, + logger: Logger ) => { const installation = await getInstallation({ savedObjectsClient, pkgName: registryPackage.name }); let previousInstalledIlmEsAssets: EsAssetReference[] = []; @@ -90,7 +92,7 @@ export const installIlmForDataStream = async ( ); const installationPromises = ilmInstallations.map(async (ilmInstallation) => { - return handleIlmInstall({ esClient, ilmInstallation }); + return handleIlmInstall({ esClient, ilmInstallation, logger }); }); installedIlms = await Promise.all(installationPromises).then((results) => results.flat()); @@ -117,15 +119,21 @@ export const installIlmForDataStream = async ( async function handleIlmInstall({ esClient, ilmInstallation, + logger, }: { esClient: ElasticsearchClient; ilmInstallation: IlmInstallation; + logger: Logger; }): Promise { - await esClient.transport.request({ - method: 'PUT', - path: `/_ilm/policy/${ilmInstallation.installationName}`, - body: ilmInstallation.content, - }); + await retryTransientEsErrors( + () => + esClient.transport.request({ + method: 'PUT', + path: `/_ilm/policy/${ilmInstallation.installationName}`, + body: ilmInstallation.content, + }), + { logger } + ); return { id: ilmInstallation.installationName, type: ElasticsearchAssetType.dataStreamIlmPolicy }; } diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ilm/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ilm/install.ts index 380bd0e913d6..b77a787090ed 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ilm/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ilm/install.ts @@ -5,18 +5,20 @@ * 2.0. */ -import type { ElasticsearchClient } from 'kibana/server'; +import type { ElasticsearchClient, Logger } from 'kibana/server'; import type { InstallablePackage } from '../../../../types'; import { ElasticsearchAssetType } from '../../../../types'; import { getAsset, getPathParts } from '../../archive'; import { getESAssetMetadata } from '../meta'; +import { retryTransientEsErrors } from '../retry'; export async function installILMPolicy( packageInfo: InstallablePackage, paths: string[], - esClient: ElasticsearchClient + esClient: ElasticsearchClient, + logger: Logger ) { const ilmPaths = paths.filter((path) => isILMPolicy(path)); if (!ilmPaths.length) return; @@ -29,11 +31,15 @@ export async function installILMPolicy( const { file } = getPathParts(path); const name = file.substr(0, file.lastIndexOf('.')); try { - await esClient.transport.request({ - method: 'PUT', - path: '/_ilm/policy/' + name, - body, - }); + await retryTransientEsErrors( + () => + esClient.transport.request({ + method: 'PUT', + path: '/_ilm/policy/' + name, + body, + }), + { logger } + ); } catch (err) { throw new Error(err.message); } diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts index 560ff0833104..d857d7c6bc2f 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -6,7 +6,7 @@ */ import type { TransportRequestOptions } from '@elastic/elasticsearch'; -import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from 'src/core/server'; import { ElasticsearchAssetType } from '../../../../types'; import type { EsAssetReference, RegistryDataStream, InstallablePackage } from '../../../../types'; @@ -22,6 +22,8 @@ import { import { appendMetadataToIngestPipeline } from '../meta'; +import { retryTransientEsErrors } from '../retry'; + import { deletePipelineRefs } from './remove'; interface RewriteSubstitution { @@ -41,7 +43,8 @@ export const installPipelines = async ( installablePackage: InstallablePackage, paths: string[], esClient: ElasticsearchClient, - savedObjectsClient: SavedObjectsClientContract + savedObjectsClient: SavedObjectsClientContract, + logger: Logger ) => { // unlike other ES assets, pipeline names are versioned so after a template is updated // it can be created pointing to the new template, without removing the old one and effecting data @@ -105,6 +108,7 @@ export const installPipelines = async ( installAllPipelines({ dataStream, esClient, + logger, paths: pipelinePaths, installablePackage, }) @@ -119,6 +123,7 @@ export const installPipelines = async ( installAllPipelines({ dataStream: undefined, esClient, + logger, paths: topLevelPipelinePaths, installablePackage, }) @@ -151,11 +156,13 @@ export function rewriteIngestPipeline( export async function installAllPipelines({ esClient, + logger, paths, dataStream, installablePackage, }: { esClient: ElasticsearchClient; + logger: Logger; paths: string[]; dataStream?: RegistryDataStream; installablePackage: InstallablePackage; @@ -195,7 +202,7 @@ export async function installAllPipelines({ }); const installationPromises = pipelines.map(async (pipeline) => { - return installPipeline({ esClient, pipeline, installablePackage }); + return installPipeline({ esClient, pipeline, installablePackage, logger }); }); return Promise.all(installationPromises); @@ -203,10 +210,12 @@ export async function installAllPipelines({ async function installPipeline({ esClient, + logger, pipeline, installablePackage, }: { esClient: ElasticsearchClient; + logger: Logger; pipeline: any; installablePackage?: InstallablePackage; }): Promise { @@ -233,7 +242,10 @@ async function installPipeline({ }; } - await esClient.ingest.putPipeline(esClientParams, esClientRequestOptions); + await retryTransientEsErrors( + () => esClient.ingest.putPipeline(esClientParams, esClientRequestOptions), + { logger } + ); return { id: pipelineWithMetadata.nameForInstallation, @@ -241,7 +253,10 @@ async function installPipeline({ }; } -export async function ensureFleetFinalPipelineIsInstalled(esClient: ElasticsearchClient) { +export async function ensureFleetFinalPipelineIsInstalled( + esClient: ElasticsearchClient, + logger: Logger +) { const esClientRequestOptions: TransportRequestOptions = { ignore: [404], }; @@ -258,6 +273,7 @@ export async function ensureFleetFinalPipelineIsInstalled(esClient: Elasticsearc ) { await installPipeline({ esClient, + logger, pipeline: { nameForInstallation: FLEET_FINAL_PIPELINE_ID, contentForInstallation: FLEET_FINAL_PIPELINE_CONTENT, diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ml_model/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ml_model/install.ts index d97081f15aca..e5c96bea8718 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ml_model/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ml_model/install.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; +import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from 'kibana/server'; import { errors } from '@elastic/elasticsearch'; import { saveInstalledEsRefs } from '../../packages/install'; @@ -13,6 +13,8 @@ import { getPathParts } from '../../archive'; import { ElasticsearchAssetType } from '../../../../../common/types/models'; import type { EsAssetReference, InstallablePackage } from '../../../../../common/types/models'; +import { retryTransientEsErrors } from '../retry'; + import { getAsset } from './common'; interface MlModelInstallation { @@ -24,7 +26,8 @@ export const installMlModel = async ( installablePackage: InstallablePackage, paths: string[], esClient: ElasticsearchClient, - savedObjectsClient: SavedObjectsClientContract + savedObjectsClient: SavedObjectsClientContract, + logger: Logger ) => { const mlModelPath = paths.find((path) => isMlModel(path)); @@ -47,7 +50,7 @@ export const installMlModel = async ( content, }; - const result = await handleMlModelInstall({ esClient, mlModel }); + const result = await handleMlModelInstall({ esClient, logger, mlModel }); installedMlModels.push(result); } return installedMlModels; @@ -61,19 +64,25 @@ const isMlModel = (path: string) => { async function handleMlModelInstall({ esClient, + logger, mlModel, }: { esClient: ElasticsearchClient; + logger: Logger; mlModel: MlModelInstallation; }): Promise { try { - await esClient.ml.putTrainedModel({ - model_id: mlModel.installationName, - defer_definition_decompression: true, - timeout: '45s', - // @ts-expect-error expects an object not a string - body: mlModel.content, - }); + await retryTransientEsErrors( + () => + esClient.ml.putTrainedModel({ + model_id: mlModel.installationName, + defer_definition_decompression: true, + timeout: '45s', + // @ts-expect-error expects an object not a string + body: mlModel.content, + }), + { logger } + ); } catch (err) { // swallow the error if the ml model already exists. const isAlreadyExistError = diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.test.ts new file mode 100644 index 000000000000..5b9a1bf1539f --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +jest.mock('timers/promises'); +import { setTimeout } from 'timers/promises'; + +import { loggerMock } from '@kbn/logging/mocks'; +import { errors as EsErrors } from '@elastic/elasticsearch'; + +import { retryTransientEsErrors } from './retry'; + +const setTimeoutMock = setTimeout as jest.Mock< + ReturnType, + Parameters +>; + +describe('retryTransientErrors', () => { + beforeEach(() => { + setTimeoutMock.mockClear(); + }); + + it("doesn't retry if operation is successful", async () => { + const esCallMock = jest.fn().mockResolvedValue('success'); + expect(await retryTransientEsErrors(esCallMock)).toEqual('success'); + expect(esCallMock).toHaveBeenCalledTimes(1); + }); + + it('logs an warning message on retry', async () => { + const logger = loggerMock.create(); + const esCallMock = jest + .fn() + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockResolvedValue('success'); + + await retryTransientEsErrors(esCallMock, { logger }); + expect(logger.warn).toHaveBeenCalledTimes(1); + expect(logger.warn.mock.calls[0][0]).toMatch( + `Retrying Elasticsearch operation after [2s] due to error: ConnectionError: foo ConnectionError: foo` + ); + }); + + it('retries with an exponential backoff', async () => { + let attempt = 0; + const esCallMock = jest.fn(async () => { + attempt++; + if (attempt < 5) { + throw new EsErrors.ConnectionError('foo'); + } else { + return 'success'; + } + }); + + expect(await retryTransientEsErrors(esCallMock)).toEqual('success'); + expect(setTimeoutMock.mock.calls).toEqual([[2000], [4000], [8000], [16000]]); + expect(esCallMock).toHaveBeenCalledTimes(5); + }); + + it('retries each supported error type', async () => { + const errors = [ + new EsErrors.NoLivingConnectionsError('no living connection', { + warnings: [], + meta: {} as any, + }), + new EsErrors.ConnectionError('no connection'), + new EsErrors.TimeoutError('timeout'), + new EsErrors.ResponseError({ statusCode: 503, meta: {} as any, warnings: [] }), + new EsErrors.ResponseError({ statusCode: 408, meta: {} as any, warnings: [] }), + new EsErrors.ResponseError({ statusCode: 410, meta: {} as any, warnings: [] }), + ]; + + for (const error of errors) { + const esCallMock = jest.fn().mockRejectedValueOnce(error).mockResolvedValue('success'); + expect(await retryTransientEsErrors(esCallMock)).toEqual('success'); + expect(esCallMock).toHaveBeenCalledTimes(2); + } + }); + + it('does not retry unsupported errors', async () => { + const error = new Error('foo!'); + const esCallMock = jest.fn().mockRejectedValueOnce(error).mockResolvedValue('success'); + await expect(retryTransientEsErrors(esCallMock)).rejects.toThrow(error); + expect(esCallMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.ts new file mode 100644 index 000000000000..c8ea36a4adde --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { setTimeout } from 'timers/promises'; + +import { errors as EsErrors } from '@elastic/elasticsearch'; +import type { Logger } from '@kbn/logging'; + +const MAX_ATTEMPTS = 5; + +const retryResponseStatuses = [ + 503, // ServiceUnavailable + 408, // RequestTimeout + 410, // Gone +]; + +const isRetryableError = (e: any) => + e instanceof EsErrors.NoLivingConnectionsError || + e instanceof EsErrors.ConnectionError || + e instanceof EsErrors.TimeoutError || + (e instanceof EsErrors.ResponseError && retryResponseStatuses.includes(e?.statusCode!)); + +/** + * Retries any transient network or configuration issues encountered from Elasticsearch with an exponential backoff. + * Should only be used to wrap operations that are idempotent and can be safely executed more than once. + */ +export const retryTransientEsErrors = async ( + esCall: () => Promise, + { logger, attempt = 0 }: { logger?: Logger; attempt?: number } = {} +): Promise => { + try { + return await esCall(); + } catch (e) { + if (attempt < MAX_ATTEMPTS && isRetryableError(e)) { + const retryCount = attempt + 1; + const retryDelaySec = Math.min(Math.pow(2, retryCount), 64); // 2s, 4s, 8s, 16s, 32s, 64s, 64s, 64s ... + + logger?.warn( + `Retrying Elasticsearch operation after [${retryDelaySec}s] due to error: ${e.toString()} ${ + e.stack + }` + ); + + await setTimeout(retryDelaySec * 1000); + return retryTransientEsErrors(esCall, { logger, attempt: retryCount }); + } + + throw e; + } +}; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts index 2e6365a9913e..eba645ae1aae 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts @@ -6,6 +6,7 @@ */ import { elasticsearchServiceMock } from 'src/core/server/mocks'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { loggerMock } from '@kbn/logging/mocks'; import { createAppContextStartContractMock } from '../../../../mocks'; import { appContextService } from '../../../../services'; @@ -44,6 +45,7 @@ describe('EPM install', () => { const templatePriorityDatasetIsPrefixUnset = 200; await installTemplate({ esClient, + logger: loggerMock.create(), fields, dataStream: dataStreamDatasetIsPrefixUnset, packageVersion: pkg.version, @@ -84,6 +86,7 @@ describe('EPM install', () => { const templatePriorityDatasetIsPrefixFalse = 200; await installTemplate({ esClient, + logger: loggerMock.create(), fields, dataStream: dataStreamDatasetIsPrefixFalse, packageVersion: pkg.version, @@ -124,6 +127,7 @@ describe('EPM install', () => { const templatePriorityDatasetIsPrefixTrue = 150; await installTemplate({ esClient, + logger: loggerMock.create(), fields, dataStream: dataStreamDatasetIsPrefixTrue, packageVersion: pkg.version, @@ -174,6 +178,7 @@ describe('EPM install', () => { const templatePriorityDatasetIsPrefixUnset = 200; await installTemplate({ esClient, + logger: loggerMock.create(), fields, dataStream: dataStreamDatasetIsPrefixUnset, packageVersion: pkg.version, diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index de64b99c787a..eb5b43650dad 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -7,7 +7,7 @@ import { merge } from 'lodash'; import Boom from '@hapi/boom'; -import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from 'src/core/server'; import { ElasticsearchAssetType } from '../../../../types'; import type { @@ -29,6 +29,7 @@ import { import type { ESAssetMetadata } from '../meta'; import { getESAssetMetadata } from '../meta'; +import { retryTransientEsErrors } from '../retry'; import { generateMappings, @@ -42,14 +43,15 @@ import { buildDefaultSettings } from './default_settings'; export const installTemplates = async ( installablePackage: InstallablePackage, esClient: ElasticsearchClient, + logger: Logger, paths: string[], savedObjectsClient: SavedObjectsClientContract ): Promise => { // install any pre-built index template assets, // atm, this is only the base package's global index templates // Install component templates first, as they are used by the index templates - await installPreBuiltComponentTemplates(paths, esClient); - await installPreBuiltTemplates(paths, esClient); + await installPreBuiltComponentTemplates(paths, esClient, logger); + await installPreBuiltTemplates(paths, esClient, logger); // remove package installation's references to index templates await removeAssetTypesFromInstalledEs(savedObjectsClient, installablePackage.name, [ @@ -65,6 +67,7 @@ export const installTemplates = async ( installTemplateForDataStream({ pkg: installablePackage, esClient, + logger, dataStream, }) ) @@ -84,7 +87,11 @@ export const installTemplates = async ( return installedTemplates; }; -const installPreBuiltTemplates = async (paths: string[], esClient: ElasticsearchClient) => { +const installPreBuiltTemplates = async ( + paths: string[], + esClient: ElasticsearchClient, + logger: Logger +) => { const templatePaths = paths.filter((path) => isTemplate(path)); const templateInstallPromises = templatePaths.map(async (path) => { const { file } = getPathParts(path); @@ -96,10 +103,16 @@ const installPreBuiltTemplates = async (paths: string[], esClient: Elasticsearch if (content.hasOwnProperty('template') || content.hasOwnProperty('composed_of')) { // Template is v2 - return esClient.indices.putIndexTemplate(esClientParams, esClientRequestOptions); + return retryTransientEsErrors( + () => esClient.indices.putIndexTemplate(esClientParams, esClientRequestOptions), + { logger } + ); } else { // template is V1 - return esClient.indices.putTemplate(esClientParams, esClientRequestOptions); + return retryTransientEsErrors( + () => esClient.indices.putTemplate(esClientParams, esClientRequestOptions), + { logger } + ); } }); try { @@ -113,7 +126,8 @@ const installPreBuiltTemplates = async (paths: string[], esClient: Elasticsearch const installPreBuiltComponentTemplates = async ( paths: string[], - esClient: ElasticsearchClient + esClient: ElasticsearchClient, + logger: Logger ) => { const templatePaths = paths.filter((path) => isComponentTemplate(path)); const templateInstallPromises = templatePaths.map(async (path) => { @@ -126,7 +140,10 @@ const installPreBuiltComponentTemplates = async ( body: content, }; - return esClient.cluster.putComponentTemplate(esClientParams, { ignore: [404] }); + return retryTransientEsErrors( + () => esClient.cluster.putComponentTemplate(esClientParams, { ignore: [404] }), + { logger } + ); }); try { @@ -157,15 +174,18 @@ const isComponentTemplate = (path: string) => { export async function installTemplateForDataStream({ pkg, esClient, + logger, dataStream, }: { pkg: InstallablePackage; esClient: ElasticsearchClient; + logger: Logger; dataStream: RegistryDataStream; }): Promise { const fields = await loadFieldsFromYaml(pkg, dataStream.path); return installTemplate({ esClient, + logger, fields, dataStream, packageVersion: pkg.version, @@ -186,6 +206,7 @@ interface TemplateMapEntry { type TemplateMap = Record; function putComponentTemplate( esClient: ElasticsearchClient, + logger: Logger, params: { body: TemplateMapEntry; name: string; @@ -194,9 +215,9 @@ function putComponentTemplate( ): { clusterPromise: Promise; name: string } { const { name, body, create = false } = params; return { - clusterPromise: esClient.cluster.putComponentTemplate( - { name, body, create }, - { ignore: [404] } + clusterPromise: retryTransientEsErrors( + () => esClient.cluster.putComponentTemplate({ name, body, create }, { ignore: [404] }), + { logger } ), name, }; @@ -256,10 +277,12 @@ async function installDataStreamComponentTemplates(params: { templateName: string; registryElasticsearch: RegistryElasticsearch | undefined; esClient: ElasticsearchClient; + logger: Logger; packageName: string; defaultSettings: IndexTemplate['template']['settings']; }) { - const { templateName, registryElasticsearch, esClient, packageName, defaultSettings } = params; + const { templateName, registryElasticsearch, esClient, packageName, defaultSettings, logger } = + params; const templates = buildComponentTemplates({ templateName, registryElasticsearch, @@ -274,15 +297,22 @@ async function installDataStreamComponentTemplates(params: { templateEntries.map(async ([name, body]) => { if (isUserSettingsTemplate(name)) { // look for existing user_settings template - const result = await esClient.cluster.getComponentTemplate({ name }, { ignore: [404] }); + const result = await retryTransientEsErrors( + () => esClient.cluster.getComponentTemplate({ name }, { ignore: [404] }), + { logger } + ); const hasUserSettingsTemplate = result.body.component_templates?.length === 1; if (!hasUserSettingsTemplate) { // only add if one isn't already present - const { clusterPromise } = putComponentTemplate(esClient, { body, name, create: true }); + const { clusterPromise } = putComponentTemplate(esClient, logger, { + body, + name, + create: true, + }); return clusterPromise; } } else { - const { clusterPromise } = putComponentTemplate(esClient, { body, name }); + const { clusterPromise } = putComponentTemplate(esClient, logger, { body, name }); return clusterPromise; } }) @@ -291,19 +321,26 @@ async function installDataStreamComponentTemplates(params: { return templateNames; } -export async function ensureDefaultComponentTemplate(esClient: ElasticsearchClient) { - const { body: getTemplateRes } = await esClient.cluster.getComponentTemplate( - { - name: FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME, - }, - { - ignore: [404], - } +export async function ensureDefaultComponentTemplate( + esClient: ElasticsearchClient, + logger: Logger +) { + const { body: getTemplateRes } = await retryTransientEsErrors( + () => + esClient.cluster.getComponentTemplate( + { + name: FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME, + }, + { + ignore: [404], + } + ), + { logger } ); const existingTemplate = getTemplateRes?.component_templates?.[0]; if (!existingTemplate) { - await putComponentTemplate(esClient, { + await putComponentTemplate(esClient, logger, { name: FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME, body: FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT, create: true, @@ -315,12 +352,14 @@ export async function ensureDefaultComponentTemplate(esClient: ElasticsearchClie export async function installTemplate({ esClient, + logger, fields, dataStream, packageVersion, packageName, }: { esClient: ElasticsearchClient; + logger: Logger; fields: Field[]; dataStream: RegistryDataStream; packageVersion: string; @@ -342,13 +381,17 @@ export async function installTemplate({ } // Datastream now throw an error if the aliases field is present so ensure that we remove that field. - const { body: getTemplateRes } = await esClient.indices.getIndexTemplate( - { - name: templateName, - }, - { - ignore: [404], - } + const { body: getTemplateRes } = await retryTransientEsErrors( + () => + esClient.indices.getIndexTemplate( + { + name: templateName, + }, + { + ignore: [404], + } + ), + { logger } ); const existingIndexTemplate = getTemplateRes?.index_templates?.[0]; @@ -369,7 +412,10 @@ export async function installTemplate({ }, }; - await esClient.indices.putIndexTemplate(updateIndexTemplateParams, { ignore: [404] }); + await retryTransientEsErrors( + () => esClient.indices.putIndexTemplate(updateIndexTemplateParams, { ignore: [404] }), + { logger } + ); } const defaultSettings = buildDefaultSettings({ @@ -384,6 +430,7 @@ export async function installTemplate({ templateName, registryElasticsearch: dataStream.elasticsearch, esClient, + logger, packageName, defaultSettings, }); @@ -406,7 +453,10 @@ export async function installTemplate({ body: template, }; - await esClient.indices.putIndexTemplate(esClientParams, { ignore: [404] }); + await retryTransientEsErrors( + () => esClient.indices.putIndexTemplate(esClientParams, { ignore: [404] }), + { logger } + ); return { templateName, diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts index 5bad33defc57..05f7b744f4db 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ElasticsearchClient } from 'kibana/server'; +import type { ElasticsearchClient, Logger } from 'kibana/server'; import type { Field, Fields } from '../../fields/field'; import type { @@ -18,6 +18,7 @@ import { appContextService } from '../../../'; import { getRegistryDataStreamAssetBaseName } from '../index'; import { FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME } from '../../../../constants'; import { getESAssetMetadata } from '../meta'; +import { retryTransientEsErrors } from '../retry'; interface Properties { [key: string]: any; @@ -408,13 +409,14 @@ function getBaseTemplate( export const updateCurrentWriteIndices = async ( esClient: ElasticsearchClient, + logger: Logger, templates: IndexTemplateEntry[] ): Promise => { if (!templates.length) return; const allIndices = await queryDataStreamsFromTemplates(esClient, templates); if (!allIndices.length) return; - return updateAllDataStreams(allIndices, esClient); + return updateAllDataStreams(allIndices, esClient, logger); }; function isCurrentDataStream(item: CurrentDataStream[] | undefined): item is CurrentDataStream[] { @@ -448,11 +450,12 @@ const getDataStreams = async ( const updateAllDataStreams = async ( indexNameWithTemplates: CurrentDataStream[], - esClient: ElasticsearchClient + esClient: ElasticsearchClient, + logger: Logger ): Promise => { const updatedataStreamPromises = indexNameWithTemplates.map( ({ dataStreamName, indexTemplate }) => { - return updateExistingDataStream({ dataStreamName, esClient, indexTemplate }); + return updateExistingDataStream({ dataStreamName, esClient, logger, indexTemplate }); } ); await Promise.all(updatedataStreamPromises); @@ -460,10 +463,12 @@ const updateAllDataStreams = async ( const updateExistingDataStream = async ({ dataStreamName, esClient, + logger, indexTemplate, }: { dataStreamName: string; esClient: ElasticsearchClient; + logger: Logger; indexTemplate: IndexTemplate; }) => { const { settings, mappings } = indexTemplate.template; @@ -476,14 +481,19 @@ const updateExistingDataStream = async ({ // try to update the mappings first try { - await esClient.indices.putMapping({ - index: dataStreamName, - body: mappings, - write_index_only: true, - }); + await retryTransientEsErrors( + () => + esClient.indices.putMapping({ + index: dataStreamName, + body: mappings, + write_index_only: true, + }), + { logger } + ); // if update fails, rollover data stream } catch (err) { try { + // Do no wrap rollovers in retryTransientEsErrors since it is not idempotent const path = `/${dataStreamName}/_rollover`; await esClient.transport.request({ method: 'POST', @@ -498,10 +508,14 @@ const updateExistingDataStream = async ({ // for now, only update the pipeline if (!settings.index.default_pipeline) return; try { - await esClient.indices.putSettings({ - index: dataStreamName, - body: { default_pipeline: settings.index.default_pipeline }, - }); + await retryTransientEsErrors( + () => + esClient.indices.putSettings({ + index: dataStreamName, + body: { default_pipeline: settings.index.default_pipeline }, + }), + { logger } + ); } catch (err) { throw new Error(`could not update index template settings for ${dataStreamName}`); } diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts index 8b76b5a026fc..197d463797ca 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; +import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from 'kibana/server'; import { errors } from '@elastic/elasticsearch'; import { saveInstalledEsRefs } from '../../packages/install'; @@ -13,10 +13,11 @@ import { getPathParts } from '../../archive'; import { ElasticsearchAssetType } from '../../../../../common/types/models'; import type { EsAssetReference, InstallablePackage } from '../../../../../common/types/models'; import { getInstallation } from '../../packages'; -import { appContextService } from '../../../app_context'; import { getESAssetMetadata } from '../meta'; +import { retryTransientEsErrors } from '../retry'; + import { deleteTransforms, deleteTransformRefs } from './remove'; import { getAsset } from './common'; @@ -29,9 +30,9 @@ export const installTransform = async ( installablePackage: InstallablePackage, paths: string[], esClient: ElasticsearchClient, - savedObjectsClient: SavedObjectsClientContract + savedObjectsClient: SavedObjectsClientContract, + logger: Logger ) => { - const logger = appContextService.getLogger(); const installation = await getInstallation({ savedObjectsClient, pkgName: installablePackage.name, @@ -87,7 +88,7 @@ export const installTransform = async ( }); const installationPromises = transforms.map(async (transform) => { - return handleTransformInstall({ esClient, transform }); + return handleTransformInstall({ esClient, logger, transform }); }); installedTransforms = await Promise.all(installationPromises).then((results) => results.flat()); @@ -118,18 +119,24 @@ const isTransform = (path: string) => { async function handleTransformInstall({ esClient, + logger, transform, }: { esClient: ElasticsearchClient; + logger: Logger; transform: TransformInstallation; }): Promise { try { - // defer validation on put if the source index is not available - await esClient.transform.putTransform({ - transform_id: transform.installationName, - defer_validation: true, - body: transform.content, - }); + await retryTransientEsErrors( + () => + // defer validation on put if the source index is not available + esClient.transform.putTransform({ + transform_id: transform.installationName, + defer_validation: true, + body: transform.content, + }), + { logger } + ); } catch (err) { // swallow the error if the transform already exists. const isAlreadyExistError = diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts index 1aef95a49fdc..94e2e3f6d73a 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts @@ -21,6 +21,7 @@ jest.mock('./common', () => { import { errors } from '@elastic/elasticsearch'; import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import type { ElasticsearchClient, SavedObject, SavedObjectsClientContract } from 'kibana/server'; +import { loggerMock } from '@kbn/logging/mocks'; import { ElasticsearchAssetType } from '../../../../types'; import type { Installation, RegistryPackage } from '../../../../types'; @@ -157,7 +158,8 @@ describe('test transform install', () => { 'endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/default.json', ], esClient, - savedObjectsClient + savedObjectsClient, + loggerMock.create() ); expect(esClient.transform.getTransform.mock.calls).toEqual([ @@ -329,7 +331,8 @@ describe('test transform install', () => { } as unknown as RegistryPackage, ['endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/default.json'], esClient, - savedObjectsClient + savedObjectsClient, + loggerMock.create() ); const meta = getESAssetMetadata({ packageName: 'endpoint' }); @@ -441,7 +444,8 @@ describe('test transform install', () => { } as unknown as RegistryPackage, [], esClient, - savedObjectsClient + savedObjectsClient, + loggerMock.create() ); expect(esClient.transform.getTransform.mock.calls).toEqual([ @@ -556,7 +560,8 @@ describe('test transform install', () => { } as unknown as RegistryPackage, ['endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/default.json'], esClient, - savedObjectsClient + savedObjectsClient, + loggerMock.create() ); const meta = getESAssetMetadata({ packageName: 'endpoint' }); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts index 7996cbfb79ef..5ee0f57b6e03 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts @@ -7,6 +7,7 @@ import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; import { savedObjectsClientMock, elasticsearchServiceMock } from 'src/core/server/mocks'; +import { loggerMock } from '@kbn/logging/mocks'; import { appContextService } from '../../app_context'; import { createAppContextStartContractMock } from '../../../mocks'; @@ -66,6 +67,7 @@ describe('_installPackage', () => { const installationPromise = _installPackage({ savedObjectsClient: soClient, esClient, + logger: loggerMock.create(), paths: [], packageInfo: { title: 'title', diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 776a3d3cd6bc..e2027a99463f 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -5,7 +5,12 @@ * 2.0. */ -import type { ElasticsearchClient, SavedObject, SavedObjectsClientContract } from 'src/core/server'; +import type { + ElasticsearchClient, + Logger, + SavedObject, + SavedObjectsClientContract, +} from 'src/core/server'; import { MAX_TIME_COMPLETE_INSTALL, @@ -44,6 +49,7 @@ import { deleteKibanaSavedObjectsAssets } from './remove'; export async function _installPackage({ savedObjectsClient, esClient, + logger, installedPkg, paths, packageInfo, @@ -52,6 +58,7 @@ export async function _installPackage({ }: { savedObjectsClient: SavedObjectsClientContract; esClient: ElasticsearchClient; + logger: Logger; installedPkg?: SavedObject; paths: string[]; packageInfo: InstallablePackage; @@ -131,41 +138,51 @@ export async function _installPackage({ // currently only the base package has an ILM policy // at some point ILM policies can be installed/modified // per data stream and we should then save them - await installILMPolicy(packageInfo, paths, esClient); + await installILMPolicy(packageInfo, paths, esClient, logger); const installedDataStreamIlm = await installIlmForDataStream( packageInfo, paths, esClient, - savedObjectsClient + savedObjectsClient, + logger ); // installs ml models - const installedMlModel = await installMlModel(packageInfo, paths, esClient, savedObjectsClient); + const installedMlModel = await installMlModel( + packageInfo, + paths, + esClient, + savedObjectsClient, + logger + ); // installs versionized pipelines without removing currently installed ones const installedPipelines = await installPipelines( packageInfo, paths, esClient, - savedObjectsClient + savedObjectsClient, + logger ); // install or update the templates referencing the newly installed pipelines const installedTemplates = await installTemplates( packageInfo, esClient, + logger, paths, savedObjectsClient ); // update current backing indices of each data stream - await updateCurrentWriteIndices(esClient, installedTemplates); + await updateCurrentWriteIndices(esClient, logger, installedTemplates); const installedTransforms = await installTransform( packageInfo, paths, esClient, - savedObjectsClient + savedObjectsClient, + logger ); // If this is an update or retrying an update, delete the previous version's pipelines diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index db26dc3a20a8..330fd84e789b 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -308,6 +308,7 @@ async function installPackageFromRegistry({ return _installPackage({ savedObjectsClient, esClient, + logger, installedPkg, paths, packageInfo, @@ -367,6 +368,7 @@ async function installPackageByUpload({ archiveBuffer, contentType, }: InstallUploadedArchiveParams): Promise { + const logger = appContextService.getLogger(); // if an error happens during getInstallType, report that we don't know let installType: InstallType = 'unknown'; const telemetryEvent: PackageUpdateEvent = getTelemetryEvent('', ''); @@ -409,6 +411,7 @@ async function installPackageByUpload({ return _installPackage({ savedObjectsClient, esClient, + logger, installedPkg, paths, packageInfo, diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index d39a5f447319..18c66e826746 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -139,8 +139,8 @@ export async function ensureFleetGlobalEsAssets( // Ensure Global Fleet ES assets are installed logger.debug('Creating Fleet component template and ingest pipeline'); const globalAssetsRes = await Promise.all([ - ensureDefaultComponentTemplate(esClient), - ensureFleetFinalPipelineIsInstalled(esClient), + ensureDefaultComponentTemplate(esClient, logger), + ensureFleetFinalPipelineIsInstalled(esClient, logger), ]); if (globalAssetsRes.some((asset) => asset.isCreated)) { From 9ec41f70a0975decca6812d98f353c77f5534c16 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Tue, 30 Nov 2021 13:39:06 +0100 Subject: [PATCH 051/224] Fix elasticsearch.queries -> elasticsearch.query (#119941) * Fix elasticsearch.queries -> elasticsearch.query * Fix another elasticsearch.queries -> elasticsearch.query --- config/kibana.yml | 2 +- src/core/server/elasticsearch/elasticsearch_config.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/kibana.yml b/config/kibana.yml index f6f85f057172..aedea8ce83bf 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -99,7 +99,7 @@ # Logs queries sent to Elasticsearch. #logging.loggers: -# - name: elasticsearch.queries +# - name: elasticsearch.query # level: debug # Logs http responses. diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 298144ca95a0..67d7d702c8df 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -250,11 +250,11 @@ const deprecations: ConfigDeprecationProvider = () => [ if (es.logQueries === true) { addDeprecation({ configPath: `${fromPath}.logQueries`, - message: `Setting [${fromPath}.logQueries] is deprecated and no longer used. You should set the log level to "debug" for the "elasticsearch.queries" context in "logging.loggers".`, + message: `Setting [${fromPath}.logQueries] is deprecated and no longer used. You should set the log level to "debug" for the "elasticsearch.query" context in "logging.loggers".`, correctiveActions: { manualSteps: [ `Remove Setting [${fromPath}.logQueries] from your kibana configs`, - `Set the log level to "debug" for the "elasticsearch.queries" context in "logging.loggers".`, + `Set the log level to "debug" for the "elasticsearch.query" context in "logging.loggers".`, ], }, }); From de1ed188321ddbc6f6a29ae40f70fd035cd6d759 Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Tue, 30 Nov 2021 13:46:03 +0100 Subject: [PATCH 052/224] unskips and add more fields to the 'Displays enrichment matched.* fields on the timeline' cypress test (#119938) --- .../integration/detection_alerts/cti_enrichments.spec.ts | 6 ++++-- x-pack/plugins/security_solution/cypress/objects/rule.ts | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts index ec3d5a867630..c9c2ff215933 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts @@ -55,11 +55,13 @@ describe('CTI Enrichment', () => { goToRuleDetails(); }); - it.skip('Displays enrichment matched.* fields on the timeline', () => { + it('Displays enrichment matched.* fields on the timeline', () => { const expectedFields = { 'threat.enrichments.matched.atomic': getNewThreatIndicatorRule().atomic, - 'threat.enrichments.matched.type': 'indicator_match_rule', + 'threat.enrichments.matched.type': getNewThreatIndicatorRule().matchedType, 'threat.enrichments.matched.field': getNewThreatIndicatorRule().indicatorMappingField, + 'threat.enrichments.matched.id': getNewThreatIndicatorRule().matchedId, + 'threat.enrichments.matched.index': getNewThreatIndicatorRule().matchedIndex, }; const fields = Object.keys(expectedFields) as Array; diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 1c81099d43dd..2c2a743eb96d 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -80,6 +80,9 @@ export interface ThreatIndicatorRule extends CustomRule { threatIndicatorPath: string; type?: string; atomic?: string; + matchedType?: string; + matchedId?: string; + matchedIndex?: string; } export interface MachineLearningRule { @@ -407,6 +410,9 @@ export const getNewThreatIndicatorRule = (): ThreatIndicatorRule => ({ timeline: getIndicatorMatchTimelineTemplate(), maxSignals: 100, threatIndicatorPath: 'threat.indicator', + matchedType: 'indicator_match_rule', + matchedId: '84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f', + matchedIndex: 'logs-ti_abusech.malware', }); export const duplicatedRuleName = `${getNewThreatIndicatorRule().name} [Duplicate]`; From d69aa8b2002e812dd332cbf861523f5b02eac247 Mon Sep 17 00:00:00 2001 From: Faisal Kanout Date: Tue, 30 Nov 2021 16:21:20 +0300 Subject: [PATCH 053/224] [8.0][RAC] 19482 t grid fix always show checkboxes (#119608) * Add showCheckboxes prop * Hide leading checkboxes from Alerts page and updates the tests. * Skip tests * Skip test * Testing FLAKY tls alert * Remove exclusive test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../containers/alerts_table_t_grid/alerts_table_t_grid.tsx | 1 + .../timelines/public/components/t_grid/body/index.tsx | 1 + .../timelines/public/components/t_grid/standalone/index.tsx | 5 ++++- .../apps/observability/alerts/add_to_case.ts | 3 ++- .../apps/observability/alerts/bulk_actions.ts | 3 ++- .../apps/observability/alerts/index.ts | 6 +++--- 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx index bf99bcedc16b..cc455567d007 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx @@ -439,6 +439,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { runtimeMappings: {}, start: rangeFrom, setRefetch, + showCheckboxes: false, sort: tGridState?.sort ?? [ { columnId: '@timestamp', diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index be9e30a62c50..8ed848a44c25 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -122,6 +122,7 @@ interface OwnProps { ruleProducer?: string; }) => boolean; totalSelectAllAlerts?: number; + showCheckboxes?: boolean; } const defaultUnit = (n: number) => i18n.ALERTS_UNIT(n); diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx index f08cb4db9b11..a95683e7de4a 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx @@ -118,6 +118,7 @@ export interface TGridStandaloneProps { bulkActions?: BulkActionsProp; data?: DataPublicPluginStart; unit?: (total: number) => React.ReactNode; + showCheckboxes?: boolean; } const TGridStandaloneComponent: React.FC = ({ @@ -151,6 +152,7 @@ const TGridStandaloneComponent: React.FC = ({ trailingControlColumns, data, unit, + showCheckboxes = true, }) => { const dispatch = useDispatch(); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; @@ -320,7 +322,7 @@ const TGridStandaloneComponent: React.FC = ({ indexNames, itemsPerPage: itemsPerPageStore, itemsPerPageOptions, - showCheckboxes: true, + showCheckboxes, }) ); dispatch( @@ -406,6 +408,7 @@ const TGridStandaloneComponent: React.FC = ({ unit={unit} filterStatus={filterStatus} trailingControlColumns={trailingControlColumns} + showCheckboxes={showCheckboxes} /> 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 67dbf2368c04..276fab1f1cb4 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 @@ -68,7 +68,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { }); }); - describe('When user has read permissions for cases', () => { + describe.skip('When user has read permissions for cases', () => { before(async () => { await observability.users.setTestUserRole( observability.users.defineBasicObservabilityRole({ @@ -83,6 +83,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { await observability.users.restoreDefaultTestUserRole(); }); + // Hide the WorkFlow filter, but keep its code as required in https://github.com/elastic/kibana/issues/117686 it('does not render case options in the overflow menu', async () => { await observability.alerts.common.openActionsMenuForRow(0); await retry.try(async () => { diff --git a/x-pack/test/observability_functional/apps/observability/alerts/bulk_actions.ts b/x-pack/test/observability_functional/apps/observability/alerts/bulk_actions.ts index 749324a39ba2..3784890e5a80 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/bulk_actions.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/bulk_actions.ts @@ -25,7 +25,8 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { const retry = getService('retry'); - describe('Observability alerts / Bulk actions', function () { + // Hide the WorkFlow filter, but keep its code as required in https://github.com/elastic/kibana/issues/117686 + describe.skip('Observability alerts / Bulk actions', function () { this.tags('includeFirefox'); before(async () => { await esArchiver.load('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 4d2f4b971f08..048c007dcb6a 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/index.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/index.ts @@ -15,8 +15,8 @@ async function asyncForEach(array: T[], callback: (item: T, index: number) => } const ACTIVE_ALERTS_CELL_COUNT = 78; -const RECOVERED_ALERTS_CELL_COUNT = 120; -const TOTAL_ALERTS_CELL_COUNT = 198; +const RECOVERED_ALERTS_CELL_COUNT = 100; +const TOTAL_ALERTS_CELL_COUNT = 165; export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); @@ -205,7 +205,7 @@ export default ({ getService }: FtrProviderContext) => { await observability.alerts.common.submitQuery(''); }); - it('Filter for value works', async () => { + it.skip('Filter for value works', async () => { await (await observability.alerts.common.getFilterForValueButton()).click(); const queryBarValue = await ( await observability.alerts.common.getQueryBar() From cd57578e08fe06dc2191b0912f29cf06d31f9bb5 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 30 Nov 2021 14:25:54 +0100 Subject: [PATCH 054/224] [Lens] Fix custom breakdown palette compatibility with reference lines (#119950) * :bug: Use totalSeries only for data layers * :white_check_mark: Add test for custom breakdown palette --- .../xy_visualization/color_assignment.ts | 26 ++++++++++--------- .../xy_visualization/visualization.test.ts | 15 +++++++++++ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts index 5ed6ec052a0d..be7f6f1d1d22 100644 --- a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts +++ b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts @@ -108,7 +108,7 @@ export function getAccessorColorConfig( ): AccessorConfig[] { const layerContainsSplits = Boolean(layer.splitAccessor); const currentPalette: PaletteOutput = layer.palette || { type: 'palette', name: 'default' }; - const totalSeriesCount = colorAssignments[currentPalette.name].totalSeriesCount; + const totalSeriesCount = colorAssignments[currentPalette.name]?.totalSeriesCount; return layer.accessors.map((accessor) => { const currentYConfig = layer.yConfig?.find((yConfig) => yConfig.forAccessor === accessor); if (layerContainsSplits) { @@ -132,17 +132,19 @@ export function getAccessorColorConfig( ); const customColor = currentYConfig?.color || - paletteService.get(currentPalette.name).getCategoricalColor( - [ - { - name: columnToLabel[accessor] || accessor, - rankAtDepth: rank, - totalSeriesAtDepth: totalSeriesCount, - }, - ], - { maxDepth: 1, totalSeries: totalSeriesCount }, - currentPalette.params - ); + (totalSeriesCount != null + ? paletteService.get(currentPalette.name).getCategoricalColor( + [ + { + name: columnToLabel[accessor] || accessor, + rankAtDepth: rank, + totalSeriesAtDepth: totalSeriesCount, + }, + ], + { maxDepth: 1, totalSeries: totalSeriesCount }, + currentPalette.params + ) + : undefined); return { columnId: accessor as string, triggerIcon: customColor ? 'color' : 'disabled', 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 086e05b3e462..ff7ad2c0f2d8 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -1040,6 +1040,21 @@ describe('xy_visualization', () => { ]) ); }); + + it('should be excluded and not crash when a custom palette is used for data layer', () => { + const state = getStateWithBaseReferenceLine(); + // now add a breakdown on the data layer with a custom palette + state.layers[0].palette = { type: 'palette', name: 'custom', params: {} }; + state.layers[0].splitAccessor = 'd'; + + const options = xyVisualization.getConfiguration({ + state, + frame, + layerId: 'referenceLine', + }).groups; + // it should not crash basically + expect(options).toHaveLength(1); + }); }); describe('color assignment', () => { From 43729d9efe734916559ed15870d69ff2f8233f4f Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 30 Nov 2021 14:38:40 +0100 Subject: [PATCH 055/224] [Uptime] Added a synthetics service handler in uptime (#119843) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/uptime/server/kibana.index.ts | 4 +- .../lib/adapters/framework/adapter_types.ts | 11 +- .../framework/kibana_framework_adapter.ts | 4 +- .../telemetry/kibana_telemetry_adapter.ts | 8 +- .../server/lib/alerts/test_utils/index.ts | 4 +- .../plugins/uptime/server/lib/alerts/types.ts | 4 +- .../uptime/server/lib/compose/kibana.ts | 4 +- x-pack/plugins/uptime/server/lib/lib.ts | 9 +- .../synthetics_service/get_api_key.test.ts | 23 +-- .../lib/synthetics_service/get_api_key.ts | 82 +++++----- .../synthetics_service/synthetics_service.ts | 153 ++++++++++++++++++ x-pack/plugins/uptime/server/plugin.ts | 42 ++--- .../install_index_templates.ts | 4 +- .../plugins/uptime/server/rest_api/types.ts | 6 +- x-pack/plugins/uptime/server/uptime_server.ts | 4 +- 15 files changed, 250 insertions(+), 112 deletions(-) create mode 100644 x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index 945a4295148a..3afdd7729c71 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -11,7 +11,7 @@ import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { PLUGIN } from '../common/constants/plugin'; import { compose } from './lib/compose/kibana'; import { initUptimeServer } from './uptime_server'; -import { UptimeCorePluginsSetup, UptimeCoreSetup } from './lib/adapters/framework'; +import { UptimeCorePluginsSetup, UptimeServerSetup } from './lib/adapters/framework'; import { umDynamicSettings } from './lib/saved_objects/uptime_settings'; import { UptimeRuleRegistry } from './plugin'; @@ -28,7 +28,7 @@ export interface KibanaServer extends Server { } export const initServerWithKibana = ( - server: UptimeCoreSetup, + server: UptimeServerSetup, plugins: UptimeCorePluginsSetup, ruleRegistry: UptimeRuleRegistry, logger: Logger diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index 029c6164c048..64063633d12e 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -6,11 +6,7 @@ */ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import type { - SavedObjectsClientContract, - ISavedObjectsRepository, - IScopedClusterClient, -} from 'src/core/server'; +import type { SavedObjectsClientContract, IScopedClusterClient } from 'src/core/server'; import { ObservabilityPluginSetup } from '../../../../../observability/server'; import { EncryptedSavedObjectsPluginSetup, @@ -35,16 +31,17 @@ export type UMElasticsearchQueryFn = ( ) => Promise; export type UMSavedObjectsQueryFn = ( - client: SavedObjectsClientContract | ISavedObjectsRepository, + client: SavedObjectsClientContract, params?: P ) => Promise | T; -export interface UptimeCoreSetup { +export interface UptimeServerSetup { router: UptimeRouter; config: UptimeConfig; cloud?: CloudSetup; fleet: FleetStartContract; security: SecurityPluginStart; + savedObjectsClient: SavedObjectsClientContract; encryptedSavedObjects: EncryptedSavedObjectsPluginStart; } diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts index d51496d6efaf..ae83c8365a92 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -5,12 +5,12 @@ * 2.0. */ -import { UptimeCoreSetup } from './adapter_types'; +import { UptimeServerSetup } from './adapter_types'; import { UMBackendFrameworkAdapter } from './adapter_types'; import { UMKibanaRoute } from '../../../rest_api'; export class UMKibanaBackendFrameworkAdapter implements UMBackendFrameworkAdapter { - constructor(private readonly server: UptimeCoreSetup) { + constructor(private readonly server: UptimeServerSetup) { this.server = server; } diff --git a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts index d829044da504..f72af3311aff 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts @@ -6,7 +6,7 @@ */ import moment from 'moment'; -import { ISavedObjectsRepository, SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract } from 'kibana/server'; import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { PageViewParams, UptimeTelemetry, Usage } from './types'; import { savedObjectsAdapter } from '../../saved_objects/saved_objects'; @@ -23,7 +23,7 @@ const BUCKET_NUMBER = 24; export class KibanaTelemetryAdapter { public static registerUsageCollector = ( usageCollector: UsageCollectionSetup, - getSavedObjectsClient: () => ISavedObjectsRepository | undefined + getSavedObjectsClient: () => SavedObjectsClientContract | undefined ) => { if (!usageCollector) { return; @@ -37,7 +37,7 @@ export class KibanaTelemetryAdapter { public static initUsageCollector( usageCollector: UsageCollectionSetup, - getSavedObjectsClient: () => ISavedObjectsRepository | undefined + getSavedObjectsClient: () => SavedObjectsClientContract | undefined ) { return usageCollector.makeUsageCollector({ type: 'uptime', @@ -212,7 +212,7 @@ export class KibanaTelemetryAdapter { public static async countNoOfUniqueMonitorAndLocations( callCluster: UptimeESClient, - savedObjectsClient: ISavedObjectsRepository | SavedObjectsClientContract + savedObjectsClient: SavedObjectsClientContract ) { const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient); const params = { diff --git a/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts b/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts index 6481a1e2ebdc..826259cfa140 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts @@ -7,7 +7,7 @@ import { Logger } from 'kibana/server'; import { UMServerLibs } from '../../lib'; -import { UptimeCorePluginsSetup, UptimeCoreSetup } from '../../adapters'; +import { UptimeCorePluginsSetup, UptimeServerSetup } from '../../adapters'; import type { UptimeRouter } from '../../../types'; import type { IRuleDataClient } from '../../../../../rule_registry/server'; import { ruleRegistryMocks } from '../../../../../rule_registry/server/mocks'; @@ -27,7 +27,7 @@ export const bootstrapDependencies = (customRequests?: any, customPlugins: any = const router = {} as UptimeRouter; // these server/libs parameters don't have any functionality, which is fine // because we aren't testing them here - const server = { router, config: {} } as UptimeCoreSetup; + const server = { router, config: {} } as UptimeServerSetup; const plugins: UptimeCorePluginsSetup = customPlugins as any; const libs: UMServerLibs = { requests: {} } as UMServerLibs; libs.requests = { ...libs.requests, ...customRequests }; diff --git a/x-pack/plugins/uptime/server/lib/alerts/types.ts b/x-pack/plugins/uptime/server/lib/alerts/types.ts index f734628e61b9..e8e496cba997 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/types.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/types.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { UptimeCorePluginsSetup, UptimeCoreSetup } from '../adapters'; +import { UptimeCorePluginsSetup, UptimeServerSetup } from '../adapters'; import { UMServerLibs } from '../lib'; import { AlertTypeWithExecutor } from '../../../../rule_registry/server'; import { AlertInstanceContext, AlertTypeState } from '../../../../alerting/common'; @@ -30,7 +30,7 @@ export type DefaultUptimeAlertInstance = AlertTy >; export type UptimeAlertTypeFactory = ( - server: UptimeCoreSetup, + server: UptimeServerSetup, libs: UMServerLibs, plugins: UptimeCorePluginsSetup ) => DefaultUptimeAlertInstance; diff --git a/x-pack/plugins/uptime/server/lib/compose/kibana.ts b/x-pack/plugins/uptime/server/lib/compose/kibana.ts index 6cdee92c40db..42c88a69bf84 100644 --- a/x-pack/plugins/uptime/server/lib/compose/kibana.ts +++ b/x-pack/plugins/uptime/server/lib/compose/kibana.ts @@ -9,9 +9,9 @@ import { UMKibanaBackendFrameworkAdapter } from '../adapters/framework'; import { requests } from '../requests'; import { licenseCheck } from '../domains'; import { UMServerLibs } from '../lib'; -import { UptimeCoreSetup } from '../adapters/framework'; +import { UptimeServerSetup } from '../adapters/framework'; -export function compose(server: UptimeCoreSetup): UMServerLibs { +export function compose(server: UptimeServerSetup): UMServerLibs { const framework = new UMKibanaBackendFrameworkAdapter(server); return { diff --git a/x-pack/plugins/uptime/server/lib/lib.ts b/x-pack/plugins/uptime/server/lib/lib.ts index 151f7b25adc1..577013e9cc5e 100644 --- a/x-pack/plugins/uptime/server/lib/lib.ts +++ b/x-pack/plugins/uptime/server/lib/lib.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { - ElasticsearchClient, - SavedObjectsClientContract, - KibanaRequest, - ISavedObjectsRepository, -} from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract, KibanaRequest } from 'kibana/server'; import chalk from 'chalk'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { UMBackendFrameworkAdapter } from './adapters'; @@ -57,7 +52,7 @@ export function createUptimeESClient({ }: { esClient: ElasticsearchClient; request?: KibanaRequest; - savedObjectsClient: SavedObjectsClientContract | ISavedObjectsRepository; + savedObjectsClient: SavedObjectsClientContract; }) { return { baseESClient: esClient, diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.test.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.test.ts index 1d164f5dd5b6..f9ba0ce545ba 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.test.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.test.ts @@ -11,13 +11,20 @@ import { securityMock } from '../../../../security/server/mocks'; import { coreMock } from '../../../../../../src/core/server/mocks'; import { syntheticsServiceApiKey } from '../saved_objects/service_api_key'; import { KibanaRequest } from 'kibana/server'; +import { UptimeServerSetup } from '../adapters'; describe('getAPIKeyTest', function () { const core = coreMock.createStart(); const security = securityMock.createStart(); - const encryptedSavedObject = encryptedSavedObjectsMock.createStart(); + const encryptedSavedObjects = encryptedSavedObjectsMock.createStart(); const request = {} as KibanaRequest; + const server = { + security, + encryptedSavedObjects, + savedObjectsClient: core.savedObjects.getScopedClient(request), + } as unknown as UptimeServerSetup; + security.authc.apiKeys.areAPIKeysEnabled = jest.fn().mockReturnValue(true); security.authc.apiKeys.create = jest.fn().mockReturnValue({ id: 'test', @@ -29,9 +36,7 @@ describe('getAPIKeyTest', function () { it('should generate an api key and return it', async () => { const apiKey = await getAPIKeyForSyntheticsService({ request, - security, - encryptedSavedObject, - savedObjectsClient: core.savedObjects.getScopedClient(request), + server, }); expect(security.authc.apiKeys.areAPIKeysEnabled).toHaveBeenCalledTimes(1); @@ -65,21 +70,19 @@ describe('getAPIKeyTest', function () { .fn() .mockReturnValue({ attributes: { apiKey: 'qwerty', id: 'test', name: 'service-api-key' } }); - encryptedSavedObject.getClient = jest.fn().mockReturnValue({ + encryptedSavedObjects.getClient = jest.fn().mockReturnValue({ getDecryptedAsInternalUser: getObject, }); const apiKey = await getAPIKeyForSyntheticsService({ request, - security, - encryptedSavedObject, - savedObjectsClient: core.savedObjects.getScopedClient(request), + server, }); expect(apiKey).toEqual({ apiKey: 'qwerty', id: 'test', name: 'service-api-key' }); - expect(encryptedSavedObject.getClient).toHaveBeenCalledTimes(1); + expect(encryptedSavedObjects.getClient).toHaveBeenCalledTimes(1); expect(getObject).toHaveBeenCalledTimes(1); - expect(encryptedSavedObject.getClient).toHaveBeenCalledWith({ + expect(encryptedSavedObjects.getClient).toHaveBeenCalledWith({ includedHiddenTypes: [syntheticsServiceApiKey.name], }); expect(getObject).toHaveBeenCalledWith( diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.ts index 2a291c64ca2b..015442dd3c4f 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.ts @@ -6,7 +6,6 @@ */ import { KibanaRequest, SavedObjectsClientContract } from '../../../../../../src/core/server'; -import { EncryptedSavedObjectsPluginStart } from '../../../../encrypted_saved_objects/server'; import { SecurityPluginStart } from '../../../../security/server'; import { getSyntheticsServiceAPIKey, @@ -14,19 +13,18 @@ import { syntheticsServiceApiKey, } from '../saved_objects/service_api_key'; import { SyntheticsServiceApiKey } from '../../../common/runtime_types/synthetics_service_api_key'; +import { UptimeServerSetup } from '../adapters'; export const getAPIKeyForSyntheticsService = async ({ - encryptedSavedObject, - savedObjectsClient, request, - security, + server, }: { - encryptedSavedObject: EncryptedSavedObjectsPluginStart; - request: KibanaRequest; - security: SecurityPluginStart; - savedObjectsClient: SavedObjectsClientContract; -}): Promise => { - const encryptedClient = encryptedSavedObject.getClient({ + server: UptimeServerSetup; + request?: KibanaRequest; +}): Promise => { + const { security, encryptedSavedObjects, savedObjectsClient } = server; + + const encryptedClient = encryptedSavedObjects.getClient({ includedHiddenTypes: [syntheticsServiceApiKey.name], }); @@ -42,44 +40,44 @@ export const generateAndSaveAPIKey = async ({ request, savedObjectsClient, }: { + request?: KibanaRequest; security: SecurityPluginStart; - request: KibanaRequest; savedObjectsClient: SavedObjectsClientContract; }) => { - try { - const isApiKeysEnabled = await security.authc.apiKeys?.areAPIKeysEnabled(); + const isApiKeysEnabled = await security.authc.apiKeys?.areAPIKeysEnabled(); - if (!isApiKeysEnabled) { - return new Error('Please enable API keys in kibana to use synthetics service.'); - } + if (!isApiKeysEnabled) { + throw new Error('Please enable API keys in kibana to use synthetics service.'); + } - const apiKeyResult = await security.authc.apiKeys?.create(request, { - name: 'synthetics-api-key', - role_descriptors: { - synthetics_writer: { - cluster: ['monitor', 'read_ilm', 'read_pipeline'], - index: [ - { - names: ['synthetics-*'], - privileges: ['view_index_metadata', 'create_doc', 'auto_configure'], - }, - ], - }, - }, - metadata: { - description: - 'Created for synthetics service to be passed to the heartbeat to communicate with ES', + if (!request) { + throw new Error('User authorization is needed for api key generation'); + } + + const apiKeyResult = await security.authc.apiKeys?.create(request, { + name: 'synthetics-api-key', + role_descriptors: { + synthetics_writer: { + cluster: ['monitor', 'read_ilm', 'read_pipeline'], + index: [ + { + names: ['synthetics-*'], + privileges: ['view_index_metadata', 'create_doc', 'auto_configure'], + }, + ], }, - }); + }, + metadata: { + description: + 'Created for synthetics service to be passed to the heartbeat to communicate with ES', + }, + }); - if (apiKeyResult) { - const { id, name, api_key: apiKey } = apiKeyResult; - const apiKeyObject = { id, name, apiKey }; - // discard decoded key and rest of the keys - await setSyntheticsServiceApiKey(savedObjectsClient, apiKeyObject); - return apiKeyObject; - } - } catch (e) { - throw e; + if (apiKeyResult) { + const { id, name, api_key: apiKey } = apiKeyResult; + const apiKeyObject = { id, name, apiKey }; + // discard decoded key and rest of the keys + await setSyntheticsServiceApiKey(savedObjectsClient, apiKeyObject); + return apiKeyObject; } }; diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts new file mode 100644 index 000000000000..75c72ef82990 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts @@ -0,0 +1,153 @@ +/* + * 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. + */ + +/* eslint-disable max-classes-per-file */ + +import axios from 'axios'; +import { + CoreStart, + KibanaRequest, + Logger, + SavedObjectsClient, +} from '../../../../../../src/core/server'; +import { UptimeServerSetup } from '../adapters'; +import { installSyntheticsIndexTemplates } from '../../rest_api/synthetics_service/install_index_templates'; +import { SyntheticsServiceApiKey } from '../../../common/runtime_types/synthetics_service_api_key'; +import { getAPIKeyForSyntheticsService } from './get_api_key'; +import { SyntheticsMonitorSavedObject } from '../../../common/types'; +import { syntheticsMonitorType } from '../saved_objects/synthetics_monitor'; +import { getEsHosts } from './get_es_hosts'; +import { UptimeConfig } from '../../../common/config'; + +export class SyntheticsService { + private logger: Logger; + private readonly server: UptimeServerSetup; + + private readonly config: UptimeConfig; + private readonly esHosts: string[]; + + private apiKey: SyntheticsServiceApiKey | undefined; + + constructor(logger: Logger, server: UptimeServerSetup) { + this.logger = logger; + this.server = server; + this.config = server.config; + + this.esHosts = getEsHosts({ config: this.config, cloud: server.cloud }); + } + + public init(coreStart: CoreStart) { + getAPIKeyForSyntheticsService({ server: this.server }).then((apiKey) => { + if (apiKey) { + this.apiKey = apiKey; + } + }); + + this.setupIndexTemplates(coreStart); + } + + private setupIndexTemplates(coreStart: CoreStart) { + const esClient = coreStart.elasticsearch.client.asInternalUser; + const savedObjectsClient = new SavedObjectsClient( + coreStart.savedObjects.createInternalRepository() + ); + + installSyntheticsIndexTemplates({ + esClient, + server: this.server, + savedObjectsClient, + }).then( + (result) => { + if (result.name === 'synthetics' && result.install_status === 'installed') { + this.logger.info('Installed synthetics index templates'); + } else if (result.name === 'synthetics' && result.install_status === 'install_failed') { + this.logger.warn(new IndexTemplateInstallationError()); + } + }, + () => { + this.logger.warn(new IndexTemplateInstallationError()); + } + ); + } + + public registerSyncTask() { + // handler for registering kibana task manager task + } + + public scheduleSyncTask() { + // handler for scheduling task + } + + async pushConfigs(request: KibanaRequest) { + if (!this.apiKey) { + try { + this.apiKey = await getAPIKeyForSyntheticsService({ server: this.server, request }); + } catch (err) { + throw err; + } + } + + if (!this.apiKey) { + const error = new APIKeyMissingError(); + this.logger.error(error); + throw error; + } + + const monitors = await this.getMonitorConfigs(); + const data = { + monitors, + output: { + hosts: this.esHosts, + api_key: `${this.apiKey.id}:${this.apiKey.apiKey}`, + }, + }; + + const { url, username, password } = this.config.unsafe.service; + + try { + await axios({ + method: 'POST', + url: url + '/monitors', + data, + headers: { + Authorization: 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'), + }, + }); + } catch (e) { + this.logger.error(e); + } + } + + async getMonitorConfigs() { + const savedObjectsClient = this.server.savedObjectsClient; + const monitorsSavedObjects = await savedObjectsClient.find({ + type: syntheticsMonitorType, + }); + + const savedObjectsList = monitorsSavedObjects.saved_objects; + return savedObjectsList.map(({ attributes, id }) => ({ + ...attributes, + id, + })); + } +} + +class APIKeyMissingError extends Error { + constructor() { + super(); + this.message = 'API key is needed for synthetics service.'; + this.name = 'APIKeyMissingError'; + } +} + +class IndexTemplateInstallationError extends Error { + constructor() { + super(); + this.message = 'Failed to install synthetics index templates.'; + this.name = 'IndexTemplateInstallationError'; + } +} diff --git a/x-pack/plugins/uptime/server/plugin.ts b/x-pack/plugins/uptime/server/plugin.ts index 427649725711..e7aa57da06ff 100644 --- a/x-pack/plugins/uptime/server/plugin.ts +++ b/x-pack/plugins/uptime/server/plugin.ts @@ -10,9 +10,9 @@ import { CoreStart, CoreSetup, Plugin as PluginType, - ISavedObjectsRepository, Logger, SavedObjectsClient, + SavedObjectsClientContract, } from '../../../../src/core/server'; import { uptimeRuleFieldMap } from '../common/rules/uptime_rule_field_map'; import { initServerWithKibana } from './kibana.index'; @@ -20,21 +20,22 @@ import { KibanaTelemetryAdapter, UptimeCorePluginsSetup, UptimeCorePluginsStart, - UptimeCoreSetup, + UptimeServerSetup, } from './lib/adapters'; import { registerUptimeSavedObjects, savedObjectsAdapter } from './lib/saved_objects/saved_objects'; import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map'; import { Dataset } from '../../rule_registry/server'; import { UptimeConfig } from '../common/config'; -import { installSyntheticsIndexTemplates } from './rest_api/synthetics_service/install_index_templates'; +import { SyntheticsService } from './lib/synthetics_service/synthetics_service'; export type UptimeRuleRegistry = ReturnType['ruleRegistry']; export class Plugin implements PluginType { - private savedObjectsClient?: ISavedObjectsRepository; + private savedObjectsClient?: SavedObjectsClientContract; private initContext: PluginInitializerContext; private logger: Logger; - private server?: UptimeCoreSetup; + private server?: UptimeServerSetup; + private syntheticService?: SyntheticsService; constructor(initializerContext: PluginInitializerContext) { this.initContext = initializerContext; @@ -66,7 +67,11 @@ export class Plugin implements PluginType { config, router: core.http.createRouter(), cloud: plugins.cloud, - } as UptimeCoreSetup; + } as UptimeServerSetup; + + if (this.server?.config?.unsafe?.service.enabled) { + this.syntheticService = new SyntheticsService(this.logger, this.server); + } initServerWithKibana(this.server, plugins, ruleDataClient, this.logger); @@ -82,32 +87,19 @@ export class Plugin implements PluginType { }; } - public start(core: CoreStart, plugins: UptimeCorePluginsStart) { - this.savedObjectsClient = core.savedObjects.createInternalRepository(); + public start(coreStart: CoreStart, plugins: UptimeCorePluginsStart) { + this.savedObjectsClient = new SavedObjectsClient( + coreStart.savedObjects.createInternalRepository() + ); if (this.server) { this.server.security = plugins.security; this.server.fleet = plugins.fleet; this.server.encryptedSavedObjects = plugins.encryptedSavedObjects; + this.server.savedObjectsClient = this.savedObjectsClient; } if (this.server?.config?.unsafe?.service.enabled) { - const esClient = core.elasticsearch.client.asInternalUser; - installSyntheticsIndexTemplates({ - esClient, - server: this.server, - savedObjectsClient: new SavedObjectsClient(core.savedObjects.createInternalRepository()), - }).then( - (result) => { - if (result.name === 'synthetics' && result.install_status === 'installed') { - this.logger.info('Installed synthetics index templates'); - } else if (result.name === 'synthetics' && result.install_status === 'install_failed') { - this.logger.warn('Failed to install synthetics index templates'); - } - }, - () => { - this.logger.warn('Failed to install synthetics index templates'); - } - ); + this.syntheticService?.init(coreStart); } } diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/install_index_templates.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/install_index_templates.ts index b40c6018f966..185e526d148f 100644 --- a/x-pack/plugins/uptime/server/rest_api/synthetics_service/install_index_templates.ts +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/install_index_templates.ts @@ -7,7 +7,7 @@ import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; import { UMRestApiRouteFactory } from '../types'; import { API_URLS } from '../../../common/constants'; -import { UptimeCoreSetup } from '../../lib/adapters'; +import { UptimeServerSetup } from '../../lib/adapters'; export const installIndexTemplatesRoute: UMRestApiRouteFactory = () => ({ method: 'GET', @@ -27,7 +27,7 @@ export async function installSyntheticsIndexTemplates({ server, savedObjectsClient, }: { - server: UptimeCoreSetup; + server: UptimeServerSetup; esClient: ElasticsearchClient; savedObjectsClient: SavedObjectsClientContract; }) { diff --git a/x-pack/plugins/uptime/server/rest_api/types.ts b/x-pack/plugins/uptime/server/rest_api/types.ts index f8027cefd3f5..48e3f58ae0ad 100644 --- a/x-pack/plugins/uptime/server/rest_api/types.ts +++ b/x-pack/plugins/uptime/server/rest_api/types.ts @@ -17,7 +17,7 @@ import { } from 'kibana/server'; import { UMServerLibs, UptimeESClient } from '../lib/lib'; import type { UptimeRequestHandlerContext } from '../types'; -import { UptimeCoreSetup } from '../lib/adapters'; +import { UptimeServerSetup } from '../lib/adapters'; /** * Defines the basic properties employed by Uptime routes. @@ -61,7 +61,7 @@ export type UMRestApiRouteFactory = (libs: UMServerLibs) => UptimeRoute; */ export type UMKibanaRouteWrapper = ( uptimeRoute: UptimeRoute, - server: UptimeCoreSetup + server: UptimeServerSetup ) => UMKibanaRoute; /** @@ -80,5 +80,5 @@ export type UMRouteHandler = ({ request: KibanaRequest, Record, Record>; response: KibanaResponseFactory; savedObjectsClient: SavedObjectsClientContract; - server: UptimeCoreSetup; + server: UptimeServerSetup; }) => IKibanaResponse | Promise>; diff --git a/x-pack/plugins/uptime/server/uptime_server.ts b/x-pack/plugins/uptime/server/uptime_server.ts index ae606d7d4c3b..fd16e8243c2c 100644 --- a/x-pack/plugins/uptime/server/uptime_server.ts +++ b/x-pack/plugins/uptime/server/uptime_server.ts @@ -9,7 +9,7 @@ import { Logger } from 'kibana/server'; import { createLifecycleRuleTypeFactory, IRuleDataClient } from '../../rule_registry/server'; import { UMServerLibs } from './lib/lib'; import { createRouteWithAuth, restApiRoutes, uptimeRouteWrapper } from './rest_api'; -import { UptimeCoreSetup, UptimeCorePluginsSetup } from './lib/adapters'; +import { UptimeServerSetup, UptimeCorePluginsSetup } from './lib/adapters'; import { statusCheckAlertFactory } from './lib/alerts/status_check'; import { tlsAlertFactory } from './lib/alerts/tls'; @@ -17,7 +17,7 @@ import { tlsLegacyAlertFactory } from './lib/alerts/tls_legacy'; import { durationAnomalyAlertFactory } from './lib/alerts/duration_anomaly'; export const initUptimeServer = ( - server: UptimeCoreSetup, + server: UptimeServerSetup, libs: UMServerLibs, plugins: UptimeCorePluginsSetup, ruleDataClient: IRuleDataClient, From 13e03105c47e240291f6173c30afb53c0e27e686 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 30 Nov 2021 08:47:30 -0500 Subject: [PATCH 056/224] [Fleet] Improve error message for bad handlebar template (#119905) --- .../fleet/server/services/epm/agent/agent.test.ts | 15 +++++++++++++++ .../fleet/server/services/epm/agent/agent.ts | 10 ++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts index ed5d6473760f..df4800b241ab 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts @@ -258,4 +258,19 @@ my-package: const output = compileTemplate(vars, stringTemplate); expect(output).toEqual(targetOutput); }); + + it('should throw on invalid handlebar template', () => { + const streamTemplate = ` +input: log +paths: +{{ if test}} + - {{ test}} +{{ end }} +`; + const vars = {}; + + expect(() => compileTemplate(vars, streamTemplate)).toThrowError( + 'Error while compiling agent template: options.inverse is not a function' + ); + }); }); diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts index a01643b22cf9..762bc1ea624e 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts @@ -14,8 +14,14 @@ const handlebars = Handlebars.create(); export function compileTemplate(variables: PackagePolicyConfigRecord, templateStr: string) { const { vars, yamlValues } = buildTemplateVariables(variables, templateStr); - const template = handlebars.compile(templateStr, { noEscape: true }); - let compiledTemplate = template(vars); + let compiledTemplate: string; + try { + const template = handlebars.compile(templateStr, { noEscape: true }); + compiledTemplate = template(vars); + } catch (err) { + throw new Error(`Error while compiling agent template: ${err.message}`); + } + compiledTemplate = replaceRootLevelYamlVariables(yamlValues, compiledTemplate); const yamlFromCompiledTemplate = safeLoad(compiledTemplate, {}); From 57134d40c86fbee8102555916768072b97cef20c Mon Sep 17 00:00:00 2001 From: Andrew Tate Date: Tue, 30 Nov 2021 07:55:31 -0600 Subject: [PATCH 057/224] match visualization type to first series type when available (#119377) --- .../vis_types/xy/public/vis_types/area.ts | 2 + .../get_vis_type_from_params.test.ts | 105 ++++++++++++++++++ .../vis_types/get_vis_type_from_params.ts | 17 +++ .../xy/public/vis_types/histogram.ts | 2 + .../xy/public/vis_types/horizontal_bar.ts | 2 + .../vis_types/xy/public/vis_types/line.ts | 2 + .../utils/saved_visualize_utils.test.ts | 5 +- src/plugins/visualizations/public/vis.ts | 14 ++- .../public/vis_types/base_vis_type.ts | 2 + .../visualizations/public/vis_types/types.ts | 6 + 10 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 src/plugins/vis_types/xy/public/vis_types/get_vis_type_from_params.test.ts create mode 100644 src/plugins/vis_types/xy/public/vis_types/get_vis_type_from_params.ts diff --git a/src/plugins/vis_types/xy/public/vis_types/area.ts b/src/plugins/vis_types/xy/public/vis_types/area.ts index 45296fa99cdf..3b8f78db25d3 100644 --- a/src/plugins/vis_types/xy/public/vis_types/area.ts +++ b/src/plugins/vis_types/xy/public/vis_types/area.ts @@ -26,6 +26,7 @@ import { import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; import { optionTabs } from '../editor/common_config'; +import { getVisTypeFromParams } from './get_vis_type_from_params'; export const areaVisTypeDefinition = { name: 'area', @@ -36,6 +37,7 @@ export const areaVisTypeDefinition = { }), toExpressionAst, getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], + updateVisTypeOnParamsChange: getVisTypeFromParams, visConfig: { defaults: { type: ChartType.Area, diff --git a/src/plugins/vis_types/xy/public/vis_types/get_vis_type_from_params.test.ts b/src/plugins/vis_types/xy/public/vis_types/get_vis_type_from_params.test.ts new file mode 100644 index 000000000000..d1ca6bf3455f --- /dev/null +++ b/src/plugins/vis_types/xy/public/vis_types/get_vis_type_from_params.test.ts @@ -0,0 +1,105 @@ +/* + * 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 { VisParams } from 'src/plugins/visualizations/common'; +import { getVisTypeFromParams } from './get_vis_type_from_params'; + +describe('extracting visualization type from vis params', () => { + [ + { + message: 'return undefined when no params', + params: undefined, + expectedType: undefined, + }, + { + message: 'extract a line type', + params: { + seriesParams: [ + { + type: 'line', + }, + ], + } as VisParams, + expectedType: 'line', + }, + { + message: 'extract an area type', + params: { + seriesParams: [ + { + type: 'area', + }, + ], + } as VisParams, + expectedType: 'area', + }, + { + message: 'extract a histogram type when axes not defined', + params: { + seriesParams: [ + { + type: 'histogram', + }, + ], + } as VisParams, + expectedType: 'histogram', + }, + { + message: 'extract a histogram type when first axis on bottom', + params: { + seriesParams: [ + { + type: 'histogram', + }, + ], + categoryAxes: [{ position: 'bottom' }], + } as VisParams, + expectedType: 'histogram', + }, + { + message: 'extract a histogram type when first axis on top', + params: { + seriesParams: [ + { + type: 'histogram', + }, + ], + categoryAxes: [{ position: 'top' }], + } as VisParams, + expectedType: 'histogram', + }, + { + message: 'extract a horizontal_bar type when first axis to left', + params: { + seriesParams: [ + { + type: 'histogram', + }, + ], + categoryAxes: [{ position: 'left' }], + } as VisParams, + expectedType: 'horizontal_bar', + }, + { + message: 'extract a horizontal_bar type when first axis to right', + params: { + seriesParams: [ + { + type: 'histogram', + }, + ], + categoryAxes: [{ position: 'right' }], + } as VisParams, + expectedType: 'horizontal_bar', + }, + ].forEach(({ message, params, expectedType }) => + it(message, () => { + expect(getVisTypeFromParams(params)).toBe(expectedType); + }) + ); +}); diff --git a/src/plugins/vis_types/xy/public/vis_types/get_vis_type_from_params.ts b/src/plugins/vis_types/xy/public/vis_types/get_vis_type_from_params.ts new file mode 100644 index 000000000000..abe67052cd97 --- /dev/null +++ b/src/plugins/vis_types/xy/public/vis_types/get_vis_type_from_params.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { VisParams } from 'src/plugins/visualizations/common'; + +export const getVisTypeFromParams = (params?: VisParams) => { + let type = params?.seriesParams?.[0]?.type; + if (type === 'histogram' && ['left', 'right'].includes(params?.categoryAxes?.[0]?.position)) { + type = 'horizontal_bar'; + } + return type; +}; diff --git a/src/plugins/vis_types/xy/public/vis_types/histogram.ts b/src/plugins/vis_types/xy/public/vis_types/histogram.ts index 32b72e753af7..79b3fd72de45 100644 --- a/src/plugins/vis_types/xy/public/vis_types/histogram.ts +++ b/src/plugins/vis_types/xy/public/vis_types/histogram.ts @@ -26,6 +26,7 @@ import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; import { optionTabs } from '../editor/common_config'; import { defaultCountLabel, LabelRotation } from '../../../../charts/public'; +import { getVisTypeFromParams } from './get_vis_type_from_params'; export const histogramVisTypeDefinition = { name: 'histogram', @@ -38,6 +39,7 @@ export const histogramVisTypeDefinition = { }), toExpressionAst, getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], + updateVisTypeOnParamsChange: getVisTypeFromParams, visConfig: { defaults: { type: ChartType.Histogram, diff --git a/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts b/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts index ca24f06e6d1c..5ac833190dd3 100644 --- a/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts +++ b/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts @@ -26,6 +26,7 @@ import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; import { optionTabs } from '../editor/common_config'; import { defaultCountLabel, LabelRotation } from '../../../../charts/public'; +import { getVisTypeFromParams } from './get_vis_type_from_params'; export const horizontalBarVisTypeDefinition = { name: 'horizontal_bar', @@ -38,6 +39,7 @@ export const horizontalBarVisTypeDefinition = { }), toExpressionAst, getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], + updateVisTypeOnParamsChange: getVisTypeFromParams, visConfig: { defaults: { type: ChartType.Histogram, diff --git a/src/plugins/vis_types/xy/public/vis_types/line.ts b/src/plugins/vis_types/xy/public/vis_types/line.ts index dd3196b1a7cb..f7467ca53fa0 100644 --- a/src/plugins/vis_types/xy/public/vis_types/line.ts +++ b/src/plugins/vis_types/xy/public/vis_types/line.ts @@ -26,6 +26,7 @@ import { import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; import { optionTabs } from '../editor/common_config'; +import { getVisTypeFromParams } from './get_vis_type_from_params'; export const lineVisTypeDefinition = { name: 'line', @@ -36,6 +37,7 @@ export const lineVisTypeDefinition = { }), toExpressionAst, getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], + updateVisTypeOnParamsChange: getVisTypeFromParams, visConfig: { defaults: { type: ChartType.Line, diff --git a/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts b/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts index 83b16026de39..5c8c0594d356 100644 --- a/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts +++ b/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts @@ -43,10 +43,10 @@ jest.mock('../../../../plugins/data/public', () => ({ })); const mockInjectReferences = jest.fn(); -const mockExtractReferences = jest.fn(() => ({ references: [], attributes: {} })); +const mockExtractReferences = jest.fn((arg) => arg); jest.mock('./saved_visualization_references', () => ({ injectReferences: jest.fn((...args) => mockInjectReferences(...args)), - extractReferences: jest.fn(() => mockExtractReferences()), + extractReferences: jest.fn((arg) => mockExtractReferences(arg)), })); let isTitleDuplicateConfirmed = true; @@ -184,6 +184,7 @@ describe('saved_visualize_utils', () => { vis = { visState: { type: 'area', + params: {}, }, title: 'test', uiStateJSON: '{}', diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index 2a1e7f2c8c67..8499bb1428c3 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -113,7 +113,19 @@ export class Vis { return defaults({}, cloneDeep(params ?? {}), cloneDeep(this.type.visConfig?.defaults ?? {})); } - async setState(state: PartialVisState) { + async setState(inState: PartialVisState) { + let state = inState; + + const { updateVisTypeOnParamsChange } = this.type; + const newType = updateVisTypeOnParamsChange && updateVisTypeOnParamsChange(state.params); + if (newType) { + state = { + ...inState, + type: newType, + params: { ...inState.params, type: newType }, + }; + } + let typeChanged = false; if (state.type && this.type.name !== state.type) { // @ts-ignore diff --git a/src/plugins/visualizations/public/vis_types/base_vis_type.ts b/src/plugins/visualizations/public/vis_types/base_vis_type.ts index 669bfb7f3652..675a1783274a 100644 --- a/src/plugins/visualizations/public/vis_types/base_vis_type.ts +++ b/src/plugins/visualizations/public/vis_types/base_vis_type.ts @@ -43,6 +43,7 @@ export class BaseVisType { public readonly inspectorAdapters; public readonly toExpressionAst; public readonly getInfoMessage; + public readonly updateVisTypeOnParamsChange; public readonly schemas; constructor(opts: VisTypeDefinition) { @@ -71,6 +72,7 @@ export class BaseVisType { this.inspectorAdapters = opts.inspectorAdapters; this.toExpressionAst = opts.toExpressionAst; this.getInfoMessage = opts.getInfoMessage; + this.updateVisTypeOnParamsChange = opts.updateVisTypeOnParamsChange; this.schemas = new Schemas(this.editorConfig?.schemas ?? []); } diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts index 77654c8a157e..724f9d6ccc66 100644 --- a/src/plugins/visualizations/public/vis_types/types.ts +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -147,6 +147,12 @@ export interface VisTypeDefinition { */ readonly toExpressionAst: VisToExpressionAst; + /** + * Should be defined when the visualization type should change + * when certain params are changed + */ + readonly updateVisTypeOnParamsChange?: (params: VisParams) => string | undefined; + readonly setup?: (vis: Vis) => Promise>; hidden?: boolean; From 5001000bcefc39454fcf33953d086c0edb59b171 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 30 Nov 2021 14:24:12 +0000 Subject: [PATCH 058/224] chore(NA): splits types from code on @kbn/analytics (#119869) --- package.json | 1 + packages/BUILD.bazel | 1 + packages/kbn-analytics/BUILD.bazel | 26 +++++++++++++++++---- packages/kbn-analytics/package.json | 1 - packages/kbn-ui-shared-deps-src/BUILD.bazel | 2 +- yarn.lock | 4 ++++ 6 files changed, 29 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b0c7e4659a55..1c9b4a842d16 100644 --- a/package.json +++ b/package.json @@ -557,6 +557,7 @@ "@types/json5": "^0.0.30", "@types/kbn__ace": "link:bazel-bin/packages/kbn-ace/npm_module_types", "@types/kbn__alerts": "link:bazel-bin/packages/kbn-alerts/npm_module_types", + "@types/kbn__analytics": "link:bazel-bin/packages/kbn-analytics/npm_module_types", "@types/kbn__i18n": "link:bazel-bin/packages/kbn-i18n/npm_module_types", "@types/kbn__i18n-react": "link:bazel-bin/packages/kbn-i18n-react/npm_module_types", "@types/license-checker": "15.0.0", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index c9a0f6a759b2..dff91679a2d9 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -79,6 +79,7 @@ filegroup( "//packages/elastic-datemath:build_types", "//packages/kbn-ace:build_types", "//packages/kbn-alerts:build_types", + "//packages/kbn-analytics:build_types", "//packages/kbn-i18n:build_types", "//packages/kbn-i18n-react:build_types", ], diff --git a/packages/kbn-analytics/BUILD.bazel b/packages/kbn-analytics/BUILD.bazel index cc65746e890c..94e65b2e35ba 100644 --- a/packages/kbn-analytics/BUILD.bazel +++ b/packages/kbn-analytics/BUILD.bazel @@ -1,9 +1,10 @@ -load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -load("//src/dev/bazel:index.bzl", "jsts_transpiler") +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") PKG_BASE_NAME = "kbn-analytics" PKG_REQUIRE_NAME = "@kbn/analytics" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__analytics" SOURCE_FILES = glob( [ @@ -81,7 +82,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) @@ -100,3 +101,20 @@ filegroup( ], visibility = ["//visibility:public"], ) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = TYPES_PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-analytics/package.json b/packages/kbn-analytics/package.json index 177c0eb81576..c3b30dcf7943 100644 --- a/packages/kbn-analytics/package.json +++ b/packages/kbn-analytics/package.json @@ -4,7 +4,6 @@ "version": "1.0.0", "description": "Kibana Analytics tool", "main": "target_node/index.js", - "types": "target_types/index.d.ts", "browser": "target_web/index.js", "author": "Ahmad Bamieh ", "license": "SSPL-1.0 OR Elastic License 2.0" diff --git a/packages/kbn-ui-shared-deps-src/BUILD.bazel b/packages/kbn-ui-shared-deps-src/BUILD.bazel index b135ae402140..3da5e0ed9a6f 100644 --- a/packages/kbn-ui-shared-deps-src/BUILD.bazel +++ b/packages/kbn-ui-shared-deps-src/BUILD.bazel @@ -44,7 +44,7 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "//packages/elastic-datemath:npm_module_types", "//packages/elastic-safer-lodash-set", - "//packages/kbn-analytics", + "//packages/kbn-analytics:npm_module_types", "//packages/kbn-babel-preset", "//packages/kbn-i18n:npm_module_types", "//packages/kbn-i18n-react:npm_module_types", diff --git a/yarn.lock b/yarn.lock index 71b82ffdf5af..c0f9d70e39e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5813,6 +5813,10 @@ version "0.0.0" uid "" +"@types/kbn__analytics@link:bazel-bin/packages/kbn-analytics/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__i18n-react@link:bazel-bin/packages/kbn-i18n-react/npm_module_types": version "0.0.0" uid "" From 37d75889fcfbc9f57b4a55faffb58916a91a218d Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 30 Nov 2021 15:26:00 +0100 Subject: [PATCH 059/224] [Kibana react] Added flag to Monaco to not always capture scroll (#119577) * added flag to Monaco to not always capture scroll * updated jest snapshot Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../code_editor/__snapshots__/code_editor.test.tsx.snap | 1 + src/plugins/kibana_react/public/code_editor/code_editor.tsx | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap b/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap index d85f96382e80..1cf6a3409539 100644 --- a/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap +++ b/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap @@ -199,6 +199,7 @@ exports[` is rendered 1`] = ` "renderLineHighlight": "none", "scrollBeyondLastLine": false, "scrollbar": Object { + "alwaysConsumeMouseWheel": false, "useShadows": false, }, "wordBasedSuggestions": false, diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.tsx index 93cee7c0477e..754cc4743447 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.tsx @@ -397,6 +397,10 @@ export const CodeEditor: React.FC = ({ }, scrollbar: { useShadows: false, + // Scroll events are handled only when there is scrollable content. When there is scrollable content, the + // editor should scroll to the bottom then break out of that scroll context and continue scrolling on any + // outer scrollbars. + alwaysConsumeMouseWheel: false, }, wordBasedSuggestions: false, wordWrap: 'on', From d5e199b8da21a0946fb2c3aaea8e88c47ae5a539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Tue, 30 Nov 2021 15:45:14 +0100 Subject: [PATCH 060/224] [Security Solution] [Endpoint] Expand/collapse all collapsible cards from list in one action (#119592) * Expand/collapse all collapsible cards from list in one action * Adds unit test * Fix ts errors when using FormatedMessage component from kibana i18n wrong import * Fix non truncated text with tooltip Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../artifact_card_grid.test.tsx | 53 +- .../artifact_card_grid/artifact_card_grid.tsx | 29 +- .../components/grid_header.tsx | 122 +++-- .../components/text_value_display.tsx | 4 +- .../__snapshots__/index.test.tsx.snap | 480 +++++++++--------- 5 files changed, 394 insertions(+), 294 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.test.tsx index d360ca8fa168..802bfbf7a9ef 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.test.tsx @@ -72,7 +72,7 @@ describe.each([ it.each([ ['header', 'testGrid-header'], - ['expand/collapse placeholder', 'testGrid-header-expandCollapsePlaceHolder'], + ['expand/collapse button', 'testGrid-header-expandCollapseAllButton'], ['name column', 'testGrid-header-layout-titleHolder'], ['description column', 'testGrid-header-layout-descriptionHolder'], ['description column', 'testGrid-header-layout-cardActionsPlaceholder'], @@ -128,4 +128,55 @@ describe.each([ expect(renderResult.getByTestId('card-1-criteriaConditions')).not.toBeNull(); }); }); + + describe('and when cards are expanded/collapsed all together', () => { + it('should call onExpandCollapse callback when expand all', () => { + render(); + act(() => { + fireEvent.click(renderResult.getByTestId('testGrid-header-expandCollapseAllButton')); + }); + + expect(expandCollapseHandler).toHaveBeenCalledWith({ + expanded: items, + collapsed: [], + }); + }); + + it('should call onExpandCollapse callback when collapse all', () => { + cardComponentPropsProvider = jest.fn((item) => { + return { + 'data-test-subj': `card-${items.indexOf(item as AnyArtifact)}`, + expanded: true, + }; + }); + render(); + act(() => { + fireEvent.click(renderResult.getByTestId('testGrid-header-expandCollapseAllButton')); + }); + + expect(expandCollapseHandler).toHaveBeenCalledWith({ + expanded: [], + collapsed: items, + }); + }); + + it('should call onExpandCollapse callback when expand all if not all items are expanded', () => { + cardComponentPropsProvider = jest.fn((item) => { + const index = items.indexOf(item as AnyArtifact); + return { + 'data-test-subj': `card-${index}`, + expanded: index === 0 ? false : true, + }; + }); + render(); + act(() => { + fireEvent.click(renderResult.getByTestId('testGrid-header-expandCollapseAllButton')); + }); + + expect(expandCollapseHandler).toHaveBeenLastCalledWith({ + expanded: items, + collapsed: [], + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.tsx index 0218b83288d8..588310c8a73a 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.tsx @@ -100,6 +100,29 @@ export const ArtifactCardGrid = memo( [callerDefinedCardProps, onExpandCollapse] ); + const isEverythingExpanded = useMemo(() => { + for (const [_, currentCardProps] of callerDefinedCardProps) { + const currentExpandedState = Boolean(currentCardProps.expanded); + if (!currentExpandedState) { + return false; + } + } + return true; + }, [callerDefinedCardProps]); + + const handleCardExpandCollapseAll = useCallback(() => { + let expanded: AnyArtifact[] = []; + let collapsed: AnyArtifact[] = []; + + if (!isEverythingExpanded) { + expanded = Array.from(callerDefinedCardProps.keys()); + } else { + collapsed = Array.from(callerDefinedCardProps.keys()); + } + + onExpandCollapse({ expanded, collapsed }); + }, [callerDefinedCardProps, onExpandCollapse, isEverythingExpanded]); + // Full list of card props that includes the actual artifact and the callbacks type FullCardProps = Map; const fullCardProps = useMemo(() => { @@ -127,7 +150,11 @@ export const ArtifactCardGrid = memo( return ( <> - + theme.eui.paddingSizes.s}; `; -export type GridHeaderProps = Pick; -export const GridHeader = memo(({ 'data-test-subj': dataTestSubj }) => { - const getTestId = useTestIdGenerator(dataTestSubj); +export type GridHeaderProps = Pick & { + expandAllIconType: 'fold' | 'unfold'; + onExpandCollapseAll(): void; +}; +export const GridHeader = memo( + ({ 'data-test-subj': dataTestSubj, expandAllIconType, onExpandCollapseAll }) => { + const getTestId = useTestIdGenerator(dataTestSubj); - const expandToggleElement = useMemo( - () =>
, - [getTestId] - ); + const expandToggleElement = useMemo( + () => ( + onExpandCollapseAll()} + style={{ marginLeft: '-5px' }} + /> + ), + [getTestId, expandAllIconType, onExpandCollapseAll] + ); - return ( - - - - - - - } - description={ - - - - - - } - effectScope={ - - - - - - } - actionMenu={true} - /> - - ); -}); + return ( + + + + + + + } + description={ + + + + + + } + effectScope={ + + + + + + } + actionMenu={true} + /> + + ); + } +); GridHeader.displayName = 'GridHeader'; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/text_value_display.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/text_value_display.tsx index aaef120fc566..dedb2c0ada87 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/text_value_display.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/text_value_display.tsx @@ -43,12 +43,12 @@ export const TextValueDisplay = memo( }, [bold, children]); return ( - + {withTooltip && 'string' === typeof children && children.length > 0 && children !== getEmptyValue() ? ( - + <>{textContent} ) : ( diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap index 64eca93be8bc..7dc91f62fc75 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap @@ -492,7 +492,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -502,7 +502,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -538,7 +538,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -548,7 +548,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -656,7 +656,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -720,7 +720,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -750,7 +750,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -762,7 +762,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiSpacer euiSpacer--l" />
Trusted App 0 @@ -872,7 +872,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -882,7 +882,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -918,7 +918,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -928,7 +928,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -1036,7 +1036,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -1100,7 +1100,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -1130,7 +1130,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -1142,7 +1142,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiSpacer euiSpacer--l" />
Trusted App 1 @@ -1252,7 +1252,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -1262,7 +1262,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -1298,7 +1298,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -1308,7 +1308,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -1416,7 +1416,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -1480,7 +1480,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -1510,7 +1510,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -1522,7 +1522,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiSpacer euiSpacer--l" />
Trusted App 2 @@ -1632,7 +1632,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -1642,7 +1642,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -1678,7 +1678,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -1688,7 +1688,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -1796,7 +1796,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -1860,7 +1860,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -1890,7 +1890,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -1902,7 +1902,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiSpacer euiSpacer--l" />
Trusted App 3 @@ -2012,7 +2012,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -2022,7 +2022,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -2058,7 +2058,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -2068,7 +2068,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -2176,7 +2176,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -2240,7 +2240,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -2270,7 +2270,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -2282,7 +2282,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiSpacer euiSpacer--l" />
Trusted App 4 @@ -2392,7 +2392,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -2402,7 +2402,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -2438,7 +2438,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -2448,7 +2448,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -2556,7 +2556,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -2620,7 +2620,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -2650,7 +2650,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -2662,7 +2662,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiSpacer euiSpacer--l" />
Trusted App 5 @@ -2772,7 +2772,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -2782,7 +2782,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -2818,7 +2818,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -2828,7 +2828,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -2936,7 +2936,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -3000,7 +3000,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -3030,7 +3030,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -3042,7 +3042,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiSpacer euiSpacer--l" />
Trusted App 6 @@ -3152,7 +3152,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -3162,7 +3162,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -3198,7 +3198,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -3208,7 +3208,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -3316,7 +3316,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -3380,7 +3380,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -3410,7 +3410,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -3422,7 +3422,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiSpacer euiSpacer--l" />
Trusted App 7 @@ -3532,7 +3532,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -3542,7 +3542,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -3578,7 +3578,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -3588,7 +3588,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -3696,7 +3696,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -3760,7 +3760,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -3790,7 +3790,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -3802,7 +3802,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiSpacer euiSpacer--l" />
Trusted App 8 @@ -3912,7 +3912,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -3922,7 +3922,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -3958,7 +3958,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -3968,7 +3968,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -4076,7 +4076,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -4140,7 +4140,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -4170,7 +4170,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -4182,7 +4182,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiSpacer euiSpacer--l" />
Trusted App 9 @@ -4615,7 +4615,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -4625,7 +4625,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -4661,7 +4661,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -4671,7 +4671,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -4779,7 +4779,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -4843,7 +4843,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -4873,7 +4873,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -4885,7 +4885,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiSpacer euiSpacer--l" />
Trusted App 0 @@ -4995,7 +4995,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -5005,7 +5005,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -5041,7 +5041,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -5051,7 +5051,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -5159,7 +5159,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -5223,7 +5223,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -5253,7 +5253,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -5265,7 +5265,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiSpacer euiSpacer--l" />
Trusted App 1 @@ -5375,7 +5375,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -5385,7 +5385,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -5421,7 +5421,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -5431,7 +5431,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -5539,7 +5539,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -5603,7 +5603,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -5633,7 +5633,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -5645,7 +5645,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiSpacer euiSpacer--l" />
Trusted App 2 @@ -5755,7 +5755,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -5765,7 +5765,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -5801,7 +5801,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -5811,7 +5811,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -5919,7 +5919,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -5983,7 +5983,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -6013,7 +6013,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -6025,7 +6025,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiSpacer euiSpacer--l" />
Trusted App 3 @@ -6135,7 +6135,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -6145,7 +6145,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -6181,7 +6181,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -6191,7 +6191,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -6299,7 +6299,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -6363,7 +6363,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -6393,7 +6393,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -6405,7 +6405,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiSpacer euiSpacer--l" />
Trusted App 4 @@ -6515,7 +6515,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -6525,7 +6525,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -6561,7 +6561,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -6571,7 +6571,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -6679,7 +6679,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -6743,7 +6743,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -6773,7 +6773,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -6785,7 +6785,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiSpacer euiSpacer--l" />
Trusted App 5 @@ -6895,7 +6895,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -6905,7 +6905,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -6941,7 +6941,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -6951,7 +6951,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -7059,7 +7059,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -7123,7 +7123,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -7153,7 +7153,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -7165,7 +7165,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiSpacer euiSpacer--l" />
Trusted App 6 @@ -7275,7 +7275,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -7285,7 +7285,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -7321,7 +7321,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -7331,7 +7331,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -7439,7 +7439,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -7503,7 +7503,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -7533,7 +7533,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -7545,7 +7545,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiSpacer euiSpacer--l" />
Trusted App 7 @@ -7655,7 +7655,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -7665,7 +7665,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -7701,7 +7701,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -7711,7 +7711,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -7819,7 +7819,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -7883,7 +7883,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -7913,7 +7913,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -7925,7 +7925,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiSpacer euiSpacer--l" />
Trusted App 8 @@ -8035,7 +8035,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -8045,7 +8045,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -8081,7 +8081,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -8091,7 +8091,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -8199,7 +8199,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -8263,7 +8263,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -8293,7 +8293,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -8305,7 +8305,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiSpacer euiSpacer--l" />
Trusted App 9 @@ -8695,7 +8695,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -8705,7 +8705,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -8741,7 +8741,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -8751,7 +8751,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -8859,7 +8859,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -8923,7 +8923,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -8953,7 +8953,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -8965,7 +8965,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiSpacer euiSpacer--l" />
Trusted App 0 @@ -9075,7 +9075,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -9085,7 +9085,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -9121,7 +9121,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -9131,7 +9131,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -9239,7 +9239,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -9303,7 +9303,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -9333,7 +9333,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -9345,7 +9345,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiSpacer euiSpacer--l" />
Trusted App 1 @@ -9455,7 +9455,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -9465,7 +9465,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -9501,7 +9501,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -9511,7 +9511,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -9619,7 +9619,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -9683,7 +9683,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -9713,7 +9713,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -9725,7 +9725,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiSpacer euiSpacer--l" />
Trusted App 2 @@ -9835,7 +9835,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -9845,7 +9845,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -9881,7 +9881,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -9891,7 +9891,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -9999,7 +9999,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -10063,7 +10063,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -10093,7 +10093,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -10105,7 +10105,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiSpacer euiSpacer--l" />
Trusted App 3 @@ -10215,7 +10215,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -10225,7 +10225,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -10261,7 +10261,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -10271,7 +10271,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -10379,7 +10379,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -10443,7 +10443,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -10473,7 +10473,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -10485,7 +10485,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiSpacer euiSpacer--l" />
Trusted App 4 @@ -10595,7 +10595,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -10605,7 +10605,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -10641,7 +10641,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -10651,7 +10651,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -10759,7 +10759,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -10823,7 +10823,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -10853,7 +10853,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -10865,7 +10865,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiSpacer euiSpacer--l" />
Trusted App 5 @@ -10975,7 +10975,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -10985,7 +10985,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -11021,7 +11021,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -11031,7 +11031,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -11139,7 +11139,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -11203,7 +11203,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -11233,7 +11233,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -11245,7 +11245,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiSpacer euiSpacer--l" />
Trusted App 6 @@ -11355,7 +11355,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -11365,7 +11365,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -11401,7 +11401,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -11411,7 +11411,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -11519,7 +11519,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -11583,7 +11583,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -11613,7 +11613,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -11625,7 +11625,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiSpacer euiSpacer--l" />
Trusted App 7 @@ -11735,7 +11735,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -11745,7 +11745,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -11781,7 +11781,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -11791,7 +11791,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -11899,7 +11899,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -11963,7 +11963,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -11993,7 +11993,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -12005,7 +12005,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiSpacer euiSpacer--l" />
Trusted App 8 @@ -12115,7 +12115,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-label" >
Last updated
@@ -12125,7 +12125,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-updated-value" >
1 minute ago @@ -12161,7 +12161,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-label" >
Created
@@ -12171,7 +12171,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-header-created-value" >
1 minute ago @@ -12279,7 +12279,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-createdBy-value" >
someone
@@ -12343,7 +12343,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-touchedBy-updatedBy-value" >
someone
@@ -12373,7 +12373,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard-subHeader-effectScope-value" >
Applied globally
@@ -12385,7 +12385,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiSpacer euiSpacer--l" />
Trusted App 9 From 433c378e1f02db673e4fa7d13daafd640efc09c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Tue, 30 Nov 2021 16:29:48 +0100 Subject: [PATCH 061/224] [Security Solution] [Endpoint] User is able to filter TA in policy details view (#119290) * Initial commit to add search bar for trusted apps in policy view page * Retrieve all assigned trusted apps to ensure if there are something assigned without the search filters or not. Also fixes unit tests * remove useless if condition * Adds more unit tests and fixes some pr suggestions * Fix weird bug when loading empty state * Fix ts errors due changes in api mocks * Fixes unit test * Remove grid loader to use paginated results one. Fix selectors and tests * Remove unused imports due ts errors * remove unused import Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../action/policy_trusted_apps_action.ts | 8 +- .../policy_trusted_apps_middleware.ts | 80 ++++++++++++++++--- .../reducer/initial_policy_details_state.ts | 1 + .../reducer/trusted_apps_reducer.ts | 10 +++ .../selectors/trusted_apps_selectors.ts | 25 +++++- .../public/management/pages/policy/types.ts | 4 +- .../flyout/policy_trusted_apps_flyout.tsx | 3 - .../policy_trusted_apps_layout.test.tsx | 45 ++++++++++- .../layout/policy_trusted_apps_layout.tsx | 45 ++++++----- .../list/policy_trusted_apps_list.test.tsx | 42 ++++++---- .../list/policy_trusted_apps_list.tsx | 38 ++++----- 11 files changed, 224 insertions(+), 77 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_trusted_apps_action.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_trusted_apps_action.ts index b3bdfe32ef09..3b27c7cd1b27 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_trusted_apps_action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_trusted_apps_action.ts @@ -45,7 +45,12 @@ export interface PolicyArtifactsAssignableListPageDataFilter { export interface PolicyArtifactsDeosAnyTrustedAppExists { type: 'policyArtifactsDeosAnyTrustedAppExists'; - payload: AsyncResourceState; + payload: AsyncResourceState; +} + +export interface PolicyArtifactsHasTrustedApps { + type: 'policyArtifactsHasTrustedApps'; + payload: AsyncResourceState; } export interface AssignedTrustedAppsListStateChanged @@ -78,6 +83,7 @@ export type PolicyTrustedAppsAction = | PolicyArtifactsAssignableListExistDataChanged | PolicyArtifactsAssignableListPageDataFilter | PolicyArtifactsDeosAnyTrustedAppExists + | PolicyArtifactsHasTrustedApps | AssignedTrustedAppsListStateChanged | PolicyDetailsListOfAllPoliciesStateChanged | PolicyDetailsTrustedAppsForceListDataRefresh diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts index 8bb13d6fcd3b..e9cbda1f487c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts @@ -46,6 +46,7 @@ import { createLoadingResourceState, isLoadingResourceState, isUninitialisedResourceState, + isLoadedResourceState, } from '../../../../../state'; import { parseQueryFilterToKQL } from '../../../../../common/utils'; import { SEARCHABLE_FIELDS } from '../../../../trusted_apps/constants'; @@ -135,6 +136,53 @@ const checkIfThereAreAssignableTrustedApps = async ( } }; +const checkIfPolicyHasTrustedAppsAssigned = async ( + store: ImmutableMiddlewareAPI, + trustedAppsService: TrustedAppsService +) => { + const state = store.getState(); + if (isLoadingResourceState(state.artifacts.hasTrustedApps)) { + return; + } + if (isLoadedResourceState(state.artifacts.hasTrustedApps)) { + store.dispatch({ + type: 'policyArtifactsHasTrustedApps', + payload: createLoadingResourceState(state.artifacts.hasTrustedApps), + }); + } else { + store.dispatch({ + type: 'policyArtifactsHasTrustedApps', + payload: createLoadingResourceState(), + }); + } + try { + const policyId = policyIdFromParams(state); + const kuery = `exception-list-agnostic.attributes.tags:"policy:${policyId}" OR exception-list-agnostic.attributes.tags:"policy:all"`; + const trustedApps = await trustedAppsService.getTrustedAppsList({ + page: 1, + per_page: 100, + kuery, + }); + + if ( + !trustedApps.total && + isUninitialisedResourceState(state.artifacts.doesAnyTrustedAppExists) + ) { + await checkIfAnyTrustedApp(store, trustedAppsService); + } + + store.dispatch({ + type: 'policyArtifactsHasTrustedApps', + payload: createLoadedResourceState(trustedApps), + }); + } catch (err) { + store.dispatch({ + type: 'policyArtifactsHasTrustedApps', + payload: createFailedResourceState(err.body ?? err), + }); + } +}; + const checkIfAnyTrustedApp = async ( store: ImmutableMiddlewareAPI, trustedAppsService: TrustedAppsService @@ -145,7 +193,7 @@ const checkIfAnyTrustedApp = async ( } store.dispatch({ type: 'policyArtifactsDeosAnyTrustedAppExists', - payload: createLoadingResourceState(), + payload: createLoadingResourceState(), }); try { const trustedApps = await trustedAppsService.getTrustedAppsList({ @@ -155,12 +203,12 @@ const checkIfAnyTrustedApp = async ( store.dispatch({ type: 'policyArtifactsDeosAnyTrustedAppExists', - payload: createLoadedResourceState(!isEmpty(trustedApps.data)), + payload: createLoadedResourceState(trustedApps), }); } catch (err) { store.dispatch({ type: 'policyArtifactsDeosAnyTrustedAppExists', - payload: createFailedResourceState(err.body ?? err), + payload: createFailedResourceState(err.body ?? err), }); } }; @@ -185,7 +233,9 @@ const searchTrustedApps = async ( if (filter) { const filterKuery = parseQueryFilterToKQL(filter, SEARCHABLE_FIELDS) || undefined; - if (filterKuery) kuery.push(filterKuery); + if (filterKuery) { + kuery.push(filterKuery); + } } const trustedApps = await trustedAppsService.getTrustedAppsList({ @@ -228,6 +278,7 @@ const updateTrustedApps = async ( policyId, trustedApps ); + await checkIfPolicyHasTrustedAppsAssigned(store, trustedAppsService); store.dispatch({ type: 'policyArtifactsUpdateTrustedAppsChanged', @@ -265,12 +316,21 @@ const fetchPolicyTrustedAppsIfNeeded = async ( try { const urlLocationData = getCurrentUrlLocationPaginationParams(state); const policyId = policyIdFromParams(state); + const kuery = [ + `((exception-list-agnostic.attributes.tags:"policy:${policyId}") OR (exception-list-agnostic.attributes.tags:"policy:all"))`, + ]; + + if (urlLocationData.filter) { + const filterKuery = + parseQueryFilterToKQL(urlLocationData.filter, SEARCHABLE_FIELDS) || undefined; + if (filterKuery) { + kuery.push(filterKuery); + } + } const fetchResponse = await trustedAppsService.getTrustedAppsList({ page: urlLocationData.page_index + 1, per_page: urlLocationData.page_size, - kuery: `((exception-list-agnostic.attributes.tags:"policy:${policyId}") OR (exception-list-agnostic.attributes.tags:"policy:all"))${ - urlLocationData.filter ? ` AND (${urlLocationData.filter})` : '' - }`, + kuery: kuery.join(' AND '), }); dispatch({ @@ -280,8 +340,9 @@ const fetchPolicyTrustedAppsIfNeeded = async ( artifacts: fetchResponse, }), }); - if (!fetchResponse.total) { - await checkIfAnyTrustedApp({ getState, dispatch }, trustedAppsService); + + if (isUninitialisedResourceState(state.artifacts.hasTrustedApps)) { + await checkIfPolicyHasTrustedAppsAssigned({ getState, dispatch }, trustedAppsService); } } catch (error) { dispatch({ @@ -362,6 +423,7 @@ const removeTrustedAppsFromPolicy = async ( currentPolicyId, trustedApps ); + await checkIfPolicyHasTrustedAppsAssigned({ getState, dispatch }, trustedAppsService); dispatch({ type: 'policyDetailsTrustedAppsRemoveListStateChanged', diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/initial_policy_details_state.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/initial_policy_details_state.ts index 008bcd262cef..a1e63bc889dd 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/initial_policy_details_state.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/initial_policy_details_state.ts @@ -38,6 +38,7 @@ export const initialPolicyDetailsState: () => Immutable = () trustedAppsToUpdate: createUninitialisedResourceState(), assignableListEntriesExist: createUninitialisedResourceState(), doesAnyTrustedAppExists: createUninitialisedResourceState(), + hasTrustedApps: createUninitialisedResourceState(), assignedList: createUninitialisedResourceState(), policies: createUninitialisedResourceState(), removeList: createUninitialisedResourceState(), diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.ts index f9d090647b1b..f601e3ef0afb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.ts @@ -70,6 +70,16 @@ export const policyTrustedAppsReducer: ImmutableReducer { return { @@ -114,7 +114,7 @@ export const getUpdateArtifacts = ( export const getDoesTrustedAppExists = (state: Immutable): boolean => { return ( isLoadedResourceState(state.artifacts.doesAnyTrustedAppExists) && - state.artifacts.doesAnyTrustedAppExists.data + !!state.artifacts.doesAnyTrustedAppExists.data.total ); }; @@ -132,6 +132,11 @@ export const getCurrentPolicyAssignedTrustedAppsState: PolicyDetailsSelector< return state.artifacts.assignedList; }; +/** Returns current filter value */ +export const getCurrentPolicyArtifactsFilter: PolicyDetailsSelector = (state) => { + return state.artifacts.location.filter; +}; + export const getLatestLoadedPolicyAssignedTrustedAppsState: PolicyDetailsSelector< undefined | LoadedResourceState > = createSelector(getCurrentPolicyAssignedTrustedAppsState, (currentAssignedTrustedAppsState) => { @@ -183,6 +188,12 @@ export const getPolicyTrustedAppsListPagination: PolicyDetailsSelector +): number => { + return getLastLoadedResourceState(state.artifacts.hasTrustedApps)?.data.total || 0; +}; + export const getTrustedAppsPolicyListState: PolicyDetailsSelector< PolicyDetailsState['artifacts']['policies'] > = (state) => state.artifacts.policies; @@ -203,6 +214,16 @@ export const getTrustedAppsAllPoliciesById: PolicyDetailsSelector< }, {}) as Immutable>>; }); +export const getHasTrustedApps: PolicyDetailsSelector = (state) => { + return !!getLastLoadedResourceState(state.artifacts.hasTrustedApps)?.data.total; +}; + +export const getIsLoadedHasTrustedApps: PolicyDetailsSelector = (state) => + !!getLastLoadedResourceState(state.artifacts.hasTrustedApps); + +export const getHasTrustedAppsIsLoading: PolicyDetailsSelector = (state) => + isLoadingResourceState(state.artifacts.hasTrustedApps); + export const getDoesAnyTrustedAppExists: PolicyDetailsSelector< PolicyDetailsState['artifacts']['doesAnyTrustedAppExists'] > = (state) => state.artifacts.doesAnyTrustedAppExists; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts index ad06f027542d..e59a339b662b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts @@ -98,7 +98,9 @@ export interface PolicyArtifactsState { /** A list of trusted apps going to be updated */ trustedAppsToUpdate: AsyncResourceState; /** Represents if there is any trusted app existing */ - doesAnyTrustedAppExists: AsyncResourceState; + doesAnyTrustedAppExists: AsyncResourceState; + /** Represents if there is any trusted app existing assigned to the policy (without filters) */ + hasTrustedApps: AsyncResourceState; /** List of artifacts currently assigned to the policy (body specific and global) */ assignedList: AsyncResourceState; /** A list of all available polices */ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.tsx index c8aa18cf2086..a5bbff4a644b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.tsx @@ -28,7 +28,6 @@ import { import { Dispatch } from 'redux'; import { policyDetails, - getCurrentArtifactsLocation, getAssignableArtifactsList, getAssignableArtifactsListIsLoading, getUpdateArtifactsIsLoading, @@ -50,7 +49,6 @@ export const PolicyTrustedAppsFlyout = React.memo(() => { usePolicyTrustedAppsNotification(); const dispatch = useDispatch>(); const [selectedArtifactIds, setSelectedArtifactIds] = useState([]); - const location = usePolicyDetailsSelector(getCurrentArtifactsLocation); const policyItem = usePolicyDetailsSelector(policyDetails); const assignableArtifactsList = usePolicyDetailsSelector(getAssignableArtifactsList); const isAssignableArtifactsListLoading = usePolicyDetailsSelector( @@ -175,7 +173,6 @@ export const PolicyTrustedAppsFlyout = React.memo(() => { {(assignableArtifactsList?.total || 0) > 100 ? searchWarningMessage : null} { mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234')); const component = render(); - await waitForAction('policyArtifactsDeosAnyTrustedAppExists', { + await waitForAction('policyArtifactsHasTrustedApps', { validate: (action) => isLoadedResourceState(action.payload), }); @@ -107,11 +109,13 @@ describe('Policy trusted apps layout', () => { mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234')); const component = render(); - await waitForAction('assignedTrustedAppsListStateChanged'); + await waitForAction('policyArtifactsHasTrustedApps', { + validate: (action) => isLoadedResourceState(action.payload), + }); mockedContext.store.dispatch({ type: 'policyArtifactsDeosAnyTrustedAppExists', - payload: createLoadedResourceState(true), + payload: createLoadedResourceState({ data: [], total: 1 }), }); expect(component.getByTestId('policy-trusted-apps-empty-unassigned')).not.toBeNull(); @@ -121,11 +125,44 @@ describe('Policy trusted apps layout', () => { mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234')); const component = render(); - await waitForAction('assignedTrustedAppsListStateChanged'); + await waitForAction('policyArtifactsHasTrustedApps', { + validate: (action) => isLoadedResourceState(action.payload), + }); expect(component.getAllByTestId('policyTrustedAppsGrid-card')).toHaveLength(10); }); + it('should renders layout with data but no results', async () => { + mockedApis.responseProvider.trustedAppsList.mockImplementation( + (options: HttpFetchOptionsWithPath) => { + const hasAnyQuery = + 'exception-list-agnostic.attributes.tags:"policy:1234" OR exception-list-agnostic.attributes.tags:"policy:all"'; + if (options.query?.filter === hasAnyQuery) { + const exceptionsGenerator = new ExceptionsListItemGenerator('seed'); + return { + data: Array.from({ length: 10 }, () => exceptionsGenerator.generate()), + total: 10, + page: 0, + per_page: 10, + }; + } else { + return { data: [], total: 0, page: 0, per_page: 10 }; + } + } + ); + + const component = render(); + mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234', { filter: 'search' })); + + await waitForAction('policyArtifactsHasTrustedApps', { + validate: (action) => isLoadedResourceState(action.payload), + }); + + expect(component.queryAllByTestId('policyTrustedAppsGrid-card')).toHaveLength(0); + expect(component.queryByTestId('policy-trusted-apps-empty-unassigned')).toBeNull(); + expect(component.queryByTestId('policy-trusted-apps-empty-unexisting')).toBeNull(); + }); + it('should hide assign button on empty state with unassigned policies when downgraded to a gold or below license', async () => { mockUseEndpointPrivileges.mockReturnValue( getEndpointPrivilegesInitialStateMock({ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx index 1d00c09393d5..f39b080e56e3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx @@ -17,15 +17,17 @@ import { EuiText, EuiSpacer, EuiLink, + EuiProgress, } from '@elastic/eui'; import { PolicyTrustedAppsEmptyUnassigned, PolicyTrustedAppsEmptyUnexisting } from '../empty'; import { getCurrentArtifactsLocation, getDoesTrustedAppExists, policyDetails, - doesPolicyHaveTrustedApps, doesTrustedAppExistsLoading, - getPolicyTrustedAppsListPagination, + getTotalPolicyTrustedAppsListPagination, + getHasTrustedApps, + getIsLoadedHasTrustedApps, } from '../../../store/policy_details/selectors'; import { usePolicyDetailsNavigateCallback, usePolicyDetailsSelector } from '../../policy_hooks'; import { PolicyTrustedAppsFlyout } from '../flyout'; @@ -42,11 +44,10 @@ export const PolicyTrustedAppsLayout = React.memo(() => { const isDoesTrustedAppExistsLoading = usePolicyDetailsSelector(doesTrustedAppExistsLoading); const policyItem = usePolicyDetailsSelector(policyDetails); const navigateCallback = usePolicyDetailsNavigateCallback(); - const hasAssignedTrustedApps = usePolicyDetailsSelector(doesPolicyHaveTrustedApps); const { isPlatinumPlus } = useEndpointPrivileges(); - const totalAssignedCount = usePolicyDetailsSelector( - getPolicyTrustedAppsListPagination - ).totalItemCount; + const totalAssignedCount = usePolicyDetailsSelector(getTotalPolicyTrustedAppsListPagination); + const hasTrustedApps = usePolicyDetailsSelector(getHasTrustedApps); + const isLoadedHasTrustedApps = usePolicyDetailsSelector(getIsLoadedHasTrustedApps); const showListFlyout = location.show === 'list'; @@ -73,21 +74,19 @@ export const PolicyTrustedAppsLayout = React.memo(() => { [navigateCallback] ); + const isDisplaysEmptyStateLoading = useMemo( + () => !isLoadedHasTrustedApps || isDoesTrustedAppExistsLoading, + [isLoadedHasTrustedApps, isDoesTrustedAppExistsLoading] + ); + const displaysEmptyState = useMemo( - () => - !isDoesTrustedAppExistsLoading && - !hasAssignedTrustedApps.loading && - !hasAssignedTrustedApps.hasTrustedApps, - [ - hasAssignedTrustedApps.hasTrustedApps, - hasAssignedTrustedApps.loading, - isDoesTrustedAppExistsLoading, - ] + () => !isDisplaysEmptyStateLoading && !hasTrustedApps, + [isDisplaysEmptyStateLoading, hasTrustedApps] ); - const displaysEmptyStateIsLoading = useMemo( - () => isDoesTrustedAppExistsLoading || hasAssignedTrustedApps.loading, - [hasAssignedTrustedApps.loading, isDoesTrustedAppExistsLoading] + const displayHeaderAndContent = useMemo( + () => !isDisplaysEmptyStateLoading && !displaysEmptyState && isLoadedHasTrustedApps, + [displaysEmptyState, isDisplaysEmptyStateLoading, isLoadedHasTrustedApps] ); const aboutInfo = useMemo(() => { @@ -117,7 +116,7 @@ export const PolicyTrustedAppsLayout = React.memo(() => { return policyItem ? (
- {!displaysEmptyStateIsLoading && !displaysEmptyState ? ( + {displayHeaderAndContent ? ( <> @@ -142,7 +141,7 @@ export const PolicyTrustedAppsLayout = React.memo(() => { {isPlatinumPlus && assignTrustedAppButton} - + ) : null} { color="transparent" borderRadius="none" > - {displaysEmptyState ? ( + {displaysEmptyState && !isDoesTrustedAppExistsLoading ? ( doesTrustedAppExists ? ( { policyName={policyItem.name} /> ) + ) : displayHeaderAndContent ? ( + ) : ( - + )} {isPlatinumPlus && showListFlyout ? : null} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx index 549b829f44a2..7410dd20d928 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx @@ -13,11 +13,7 @@ import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing import { PolicyTrustedAppsList, PolicyTrustedAppsListProps } from './policy_trusted_apps_list'; import React from 'react'; import { policyDetailsPageAllApiHttpMocks } from '../../../test_utils'; -import { - createLoadingResourceState, - isFailedResourceState, - isLoadedResourceState, -} from '../../../../../state'; +import { isFailedResourceState, isLoadedResourceState } from '../../../../../state'; import { fireEvent, within, act, waitFor } from '@testing-library/react'; import { APP_UI_ID } from '../../../../../../../common/constants'; import { @@ -105,25 +101,22 @@ describe('when rendering the PolicyTrustedAppsList', () => { }) : Promise.resolve(); + const checkTrustedAppDataAssignedReceived = waitForLoadedState + ? waitForAction('policyArtifactsHasTrustedApps', { + validate({ payload }) { + return isLoadedResourceState(payload); + }, + }) + : Promise.resolve(); + renderResult = appTestContext.render(); + await checkTrustedAppDataAssignedReceived; await trustedAppDataReceived; return renderResult; }; }); - it('should show loading spinner if checking to see if trusted apps exist', async () => { - await render(); - act(() => { - appTestContext.store.dispatch({ - type: 'policyArtifactsDeosAnyTrustedAppExists', - payload: createLoadingResourceState(), - }); - }); - - expect(renderResult.getByTestId('policyTrustedAppsGrid-loading')).not.toBeNull(); - }); - it('should show total number of of items being displayed', async () => { await render(); @@ -334,4 +327,19 @@ describe('when rendering the PolicyTrustedAppsList', () => { expect(renderResult.queryByTestId('policyTrustedAppsGrid-removeAction')).toBeNull(); }); + + it('should handle search changes', async () => { + await render(); + + expect(appTestContext.history.location.search).not.toBeTruthy(); + + act(() => { + fireEvent.change(renderResult.getByTestId('searchField'), { + target: { value: 'search' }, + }); + fireEvent.submit(renderResult.getByTestId('searchField')); + }); + + expect(appTestContext.history.location.search).toMatch('?filter=search'); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx index 48f66806b46b..3453bc529b27 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx @@ -6,7 +6,7 @@ */ import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { EuiLoadingSpinner, EuiSpacer, EuiText, Pagination, EuiPageTemplate } from '@elastic/eui'; +import { EuiSpacer, EuiText, Pagination } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { @@ -14,9 +14,8 @@ import { ArtifactCardGridCardComponentProps, ArtifactCardGridProps, } from '../../../../../components/artifact_card_grid'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { usePolicyDetailsSelector, usePolicyDetailsNavigateCallback } from '../../policy_hooks'; import { - doesPolicyHaveTrustedApps, getCurrentArtifactsLocation, getPolicyTrustedAppList, getPolicyTrustedAppListError, @@ -24,7 +23,7 @@ import { getTrustedAppsAllPoliciesById, isPolicyTrustedAppListLoading, policyIdFromParams, - doesTrustedAppExistsLoading, + getCurrentPolicyArtifactsFilter, } from '../../../store/policy_details/selectors'; import { getPolicyDetailPath, @@ -34,6 +33,7 @@ import { import { Immutable, TrustedApp } from '../../../../../../../common/endpoint/types'; import { useAppUrl, useToasts } from '../../../../../../common/lib/kibana'; import { APP_UI_ID } from '../../../../../../../common/constants'; +import { SearchExceptions } from '../../../../../components/search_exceptions'; import { ContextMenuItemNavByRouterProps } from '../../../../../components/context_menu_with_router_support/context_menu_item_nav_by_router'; import { ArtifactEntryCollapsibleCardProps } from '../../../../../components/artifact_entry_card'; import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator'; @@ -54,14 +54,14 @@ export const PolicyTrustedAppsList = memo( const { getAppUrl } = useAppUrl(); const { isPlatinumPlus } = useEndpointPrivileges(); const policyId = usePolicyDetailsSelector(policyIdFromParams); - const hasTrustedApps = usePolicyDetailsSelector(doesPolicyHaveTrustedApps); const isLoading = usePolicyDetailsSelector(isPolicyTrustedAppListLoading); - const isTrustedAppExistsCheckLoading = usePolicyDetailsSelector(doesTrustedAppExistsLoading); + const defaultFilter = usePolicyDetailsSelector(getCurrentPolicyArtifactsFilter); const trustedAppItems = usePolicyDetailsSelector(getPolicyTrustedAppList); const pagination = usePolicyDetailsSelector(getPolicyTrustedAppsListPagination); const urlParams = usePolicyDetailsSelector(getCurrentArtifactsLocation); const allPoliciesById = usePolicyDetailsSelector(getTrustedAppsAllPoliciesById); const trustedAppsApiError = usePolicyDetailsSelector(getPolicyTrustedAppListError); + const navigateCallback = usePolicyDetailsNavigateCallback(); const [isCardExpanded, setCardExpanded] = useState>({}); const [trustedAppsForRemoval, setTrustedAppsForRemoval] = useState([]); @@ -227,20 +227,22 @@ export const PolicyTrustedAppsList = memo( } }, [toasts, trustedAppsApiError]); - if (hasTrustedApps.loading || isTrustedAppExistsCheckLoading) { - return ( - - - - ); - } - return ( <> + { + navigateCallback({ filter }); + }} + /> + {!hideTotalShowingLabel && ( {totalItemsCountLabel} From 40eb1a370e6c1949a701b01b18b17fded20e3b40 Mon Sep 17 00:00:00 2001 From: Claudio Procida Date: Tue, 30 Nov 2021 16:37:12 +0100 Subject: [PATCH 062/224] chore: Refactors alerts search bar (#119104) * Refactors alerts search bar * Fixes bad merge --- .../observability/public/config/translations.ts | 5 +++++ .../pages/alerts/components/alerts_search_bar.tsx | 12 ++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/observability/public/config/translations.ts b/x-pack/plugins/observability/public/config/translations.ts index 265787ede447..db1378d5b5a5 100644 --- a/x-pack/plugins/observability/public/config/translations.ts +++ b/x-pack/plugins/observability/public/config/translations.ts @@ -101,4 +101,9 @@ export const translations = { defaultMessage: 'View in app', }), }, + alertsSearchBar: { + placeholder: i18n.translate('xpack.observability.alerts.searchBarPlaceholder', { + defaultMessage: 'Search alerts (e.g. kibana.alert.evaluation.threshold > 75)', + }), + }, }; diff --git a/x-pack/plugins/observability/public/pages/alerts/components/alerts_search_bar.tsx b/x-pack/plugins/observability/public/pages/alerts/components/alerts_search_bar.tsx index 14d47d1e7e9d..230574fba94d 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/alerts_search_bar.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/alerts_search_bar.tsx @@ -6,10 +6,12 @@ */ import { IndexPatternBase } from '@kbn/es-query'; -import { i18n } from '@kbn/i18n'; import React, { useMemo, useState } from 'react'; import { SearchBar, TimeHistory } from '../../../../../../../src/plugins/data/public'; import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; +import { translations } from '../../../config'; + +type QueryLanguageType = 'lucene' | 'kuery'; export function AlertsSearchBar({ dynamicIndexPatterns, @@ -30,7 +32,7 @@ export function AlertsSearchBar({ const timeHistory = useMemo(() => { return new TimeHistory(new Storage(localStorage)); }, []); - const [queryLanguage, setQueryLanguage] = useState<'lucene' | 'kuery'>('kuery'); + const [queryLanguage, setQueryLanguage] = useState('kuery'); const compatibleIndexPatterns = useMemo( () => @@ -45,9 +47,7 @@ export function AlertsSearchBar({ return ( 75)', - })} + placeholder={translations.alertsSearchBar.placeholder} query={{ query: query ?? '', language: queryLanguage }} timeHistory={timeHistory} dateRangeFrom={rangeFrom} @@ -60,7 +60,7 @@ export function AlertsSearchBar({ dateRange, query: typeof nextQuery?.query === 'string' ? nextQuery.query : '', }); - setQueryLanguage((nextQuery?.language || 'kuery') as 'kuery' | 'lucene'); + setQueryLanguage((nextQuery?.language ?? 'kuery') as QueryLanguageType); }} displayStyle="inPage" /> From d3d61d3482f410fec9ad42dbc9eb84c1a3ca33a0 Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Tue, 30 Nov 2021 17:14:07 +0100 Subject: [PATCH 063/224] [Security Solution] Fix attach to case test (#119589) * updates the detections script in order to take into consideration the new alerts index * refactors and unskips attach to case test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../detection_alerts/attach_to_case.spec.ts | 15 +++++++++------ .../security_solution/cypress/tasks/alerts.ts | 4 ++++ .../detections_admin/detections_role.json | 1 + .../roles_users/hunter/detections_role.json | 2 +- .../platform_engineer/detections_role.json | 2 +- .../roles_users/reader/detections_role.json | 3 ++- .../roles_users/rule_author/detections_role.json | 2 +- .../roles_users/soc_manager/detections_role.json | 2 +- .../roles_users/t1_analyst/detections_role.json | 2 +- .../roles_users/t2_analyst/detections_role.json | 2 +- 10 files changed, 22 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts index e5b2c4eed3b0..d7a5ce679923 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts @@ -8,7 +8,11 @@ import { getNewRule } from '../../objects/rule'; import { ROLES } from '../../../common/test'; -import { waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded } from '../../tasks/alerts'; +import { + expandFirstAlertActions, + waitForAlertsIndexToBeCreated, + waitForAlertsPanelToBeLoaded, +} from '../../tasks/alerts'; import { createCustomRuleActivated } from '../../tasks/api_calls/rules'; import { cleanKibana } from '../../tasks/common'; import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; @@ -16,7 +20,7 @@ import { login, loginAndWaitForPage, waitForPageWithoutDateRange } from '../../t import { refreshPage } from '../../tasks/security_header'; import { ALERTS_URL } from '../../urls/navigation'; -import { ATTACH_ALERT_TO_CASE_BUTTON, TIMELINE_CONTEXT_MENU_BTN } from '../../screens/alerts'; +import { ATTACH_ALERT_TO_CASE_BUTTON } from '../../screens/alerts'; const loadDetectionsPage = (role: ROLES) => { waitForPageWithoutDateRange(ALERTS_URL, role); @@ -44,7 +48,7 @@ describe('Alerts timeline', () => { }); it('should not allow user with read only privileges to attach alerts to cases', () => { - cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click({ force: true }); + expandFirstAlertActions(); cy.get(ATTACH_ALERT_TO_CASE_BUTTON).should('not.exist'); }); }); @@ -54,9 +58,8 @@ describe('Alerts timeline', () => { loadDetectionsPage(ROLES.platform_engineer); }); - // Skipping due to alerts not refreshing for platform_engineer despite being returned from API? - it.skip('should allow a user with crud privileges to attach alerts to cases', () => { - cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click({ force: true }); + it('should allow a user with crud privileges to attach alerts to cases', () => { + expandFirstAlertActions(); cy.get(ATTACH_ALERT_TO_CASE_BUTTON).first().should('not.be.disabled'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index 56f3e6821f5f..5cb39ea3e1b4 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -60,6 +60,10 @@ export const closeAlerts = () => { .should('not.be.visible'); }; +export const expandFirstAlertActions = () => { + cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click({ force: true }); +}; + export const expandFirstAlert = () => { cy.get(EXPAND_ALERT_BTN).should('exist'); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json index e6fbef08d25e..e0219dbc941a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json @@ -5,6 +5,7 @@ { "names": [ ".siem-signals-*", + ".alerts-security*", ".lists*", ".items*", "apm-*-transaction*", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json index af12a2cb674d..5f7d1091cdb3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json @@ -16,7 +16,7 @@ "privileges": ["read", "write"] }, { - "names": [".siem-signals-*"], + "names": [".alerts-security*", ".siem-signals-*"], "privileges": ["read", "write"] }, { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json index 18effae645c4..bb26dec6decb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json @@ -23,7 +23,7 @@ "privileges": ["all"] }, { - "names": [".siem-signals-*"], + "names": [".alerts-security*", ".siem-signals-*"], "privileges": ["all"] } ] diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json index 8f9434d9a362..e351227fb173 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json @@ -4,7 +4,8 @@ "indices": [ { "names" : [ - ".siem-signals*", + ".siem-signals-*", + ".alerts-security*", ".lists*", ".items*", "metrics-endpoint.metadata_current_*", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json index d6bee8ce9dc1..bf2d94851956 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json @@ -18,7 +18,7 @@ "privileges": ["read", "write"] }, { - "names": [".siem-signals-*"], + "names": [".alerts-security*", ".siem-signals-*"], "privileges": ["read", "write", "maintenance", "view_index_metadata"] }, { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json index 46f7ca1d0067..36e811c5a7ac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json @@ -18,7 +18,7 @@ "privileges": ["read", "write"] }, { - "names": [".siem-signals-*"], + "names": [".alerts-security*", ".siem-signals-*"], "privileges": ["read", "write", "manage"] }, { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json index ea3bd7b97e3c..bd7f211f16d9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json @@ -2,7 +2,7 @@ "elasticsearch": { "cluster": [], "indices": [ - { "names": [".siem-signals-*"], "privileges": ["read", "write", "maintenance"] }, + { "names": [".alerts-security*", ".siem-signals-*"], "privileges": ["read", "write", "maintenance"] }, { "names": [ "apm-*-transaction*", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json index 209e57eba2cf..d97cd39a1142 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json @@ -2,7 +2,7 @@ "elasticsearch": { "cluster": [], "indices": [ - { "names": [".siem-signals-*"], "privileges": ["read", "write", "maintenance"] }, + { "names": [".alerts-security*", ".siem-signals-*"], "privileges": ["read", "write", "maintenance"] }, { "names": [ ".lists*", From badc730264d7927947eb0d2b6cc4953dd43621f3 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Tue, 30 Nov 2021 08:25:59 -0800 Subject: [PATCH 064/224] [DOCS] Fixes index pattern page (#119904) --- docs/concepts/data-views.asciidoc | 7 ++++--- docs/index-extra-title-page.html | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/concepts/data-views.asciidoc b/docs/concepts/data-views.asciidoc index 954581faa246..870b923f20cf 100644 --- a/docs/concepts/data-views.asciidoc +++ b/docs/concepts/data-views.asciidoc @@ -87,11 +87,12 @@ For an example, refer to <: +: ``` To query {ls} indices across two {es} clusters diff --git a/docs/index-extra-title-page.html b/docs/index-extra-title-page.html index ff1c879c0f40..b70e3a985f22 100644 --- a/docs/index-extra-title-page.html +++ b/docs/index-extra-title-page.html @@ -63,7 +63,7 @@ >
  • - Create a data view
  • From b71f78a19d9ef556a06821685f22f9c7d68f6fe3 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 30 Nov 2021 17:49:55 +0100 Subject: [PATCH 065/224] [Uptime] Route to get service locations and a handler (#119964) --- .../uptime/common/constants/rest_api.ts | 1 + x-pack/plugins/uptime/common/types/index.ts | 16 ++++++ .../get_service_locations.test.ts | 50 +++++++++++++++++++ .../get_service_locations.ts | 30 +++++++++++ .../plugins/uptime/server/rest_api/index.ts | 2 + .../get_service_locations.ts | 17 +++++++ 6 files changed, 116 insertions(+) create mode 100644 x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.test.ts create mode 100644 x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.ts create mode 100644 x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts diff --git a/x-pack/plugins/uptime/common/constants/rest_api.ts b/x-pack/plugins/uptime/common/constants/rest_api.ts index 9c8098390d12..fe9712e17827 100644 --- a/x-pack/plugins/uptime/common/constants/rest_api.ts +++ b/x-pack/plugins/uptime/common/constants/rest_api.ts @@ -38,5 +38,6 @@ export enum API_URLS { // Service end points INDEX_TEMPLATES = '/internal/uptime/service/index_templates', + SERVICE_LOCATIONS = '/internal/uptime/service/locations', SYNTHETICS_MONITORS = '/internal/uptime/service/monitors', } diff --git a/x-pack/plugins/uptime/common/types/index.ts b/x-pack/plugins/uptime/common/types/index.ts index 734cfcc5f42d..e013fb11c2d6 100644 --- a/x-pack/plugins/uptime/common/types/index.ts +++ b/x-pack/plugins/uptime/common/types/index.ts @@ -44,3 +44,19 @@ export type SyntheticsMonitorSavedObject = SimpleSavedObject<{ }; }; }>; + +interface LocationGeo { + lat: number; + lon: number; +} + +export interface ManifestLocation { + url: string; + geo: { + name: string; + location: LocationGeo; + }; + status: string; +} + +export type ServiceLocations = Array<{ id: string; label: string; geo: LocationGeo }>; diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.test.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.test.ts new file mode 100644 index 000000000000..375ceffe492d --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import axios from 'axios'; +import { getServiceLocations } from './get_service_locations'; +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +describe('getServiceLocations', function () { + mockedAxios.get.mockRejectedValue('Network error: Something went wrong'); + mockedAxios.get.mockResolvedValue({ + data: { + locations: { + us_central: { + url: 'https://local.dev', + geo: { + name: 'US Central', + location: { lat: 41.25, lon: -95.86 }, + }, + status: 'beta', + }, + }, + }, + }); + it('should return parsed locations', async () => { + const locations = await getServiceLocations({ + config: { + unsafe: { + service: { + manifestUrl: 'http://local.dev', + }, + }, + }, + }); + + expect(locations).toEqual([ + { + geo: { + lat: 41.25, + lon: -95.86, + }, + id: 'us_central', + label: 'US Central', + }, + ]); + }); +}); diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.ts new file mode 100644 index 000000000000..fdd24ed2394b --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import axios from 'axios'; +import { UptimeConfig } from '../../../common/config'; +import { ManifestLocation, ServiceLocations } from '../../../common/types'; + +export async function getServiceLocations({ config }: { config: UptimeConfig }) { + const manifestURL = config.unsafe.service.manifestUrl; + const locations: ServiceLocations = []; + try { + const { data } = await axios.get>(manifestURL); + + Object.entries(data.locations).forEach(([locationId, location]) => { + locations.push({ + id: locationId, + label: location.geo.name, + geo: location.geo.location, + }); + }); + + return locations; + } catch (e) { + return []; + } +} diff --git a/x-pack/plugins/uptime/server/rest_api/index.ts b/x-pack/plugins/uptime/server/rest_api/index.ts index 4eb6ae307125..b4dac68a78e0 100644 --- a/x-pack/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/plugins/uptime/server/rest_api/index.ts @@ -28,6 +28,7 @@ import { createNetworkEventsRoute } from './network_events'; import { createJourneyFailedStepsRoute } from './pings/journeys'; import { createLastSuccessfulStepRoute } from './synthetics/last_successful_step'; import { installIndexTemplatesRoute } from './synthetics_service/install_index_templates'; +import { getServiceLocationsRoute } from './synthetics_service/get_service_locations'; import { getAllSyntheticsMonitorRoute, getSyntheticsMonitorRoute, @@ -60,6 +61,7 @@ export const restApiRoutes: UMRestApiRouteFactory[] = [ createLastSuccessfulStepRoute, createJourneyScreenshotBlocksRoute, installIndexTemplatesRoute, + getServiceLocationsRoute, getSyntheticsMonitorRoute, getAllSyntheticsMonitorRoute, addSyntheticsMonitorRoute, diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts new file mode 100644 index 000000000000..b96b98870e38 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.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 { UMRestApiRouteFactory } from '../types'; +import { API_URLS } from '../../../common/constants'; +import { getServiceLocations } from '../../lib/synthetics_service/get_service_locations'; + +export const getServiceLocationsRoute: UMRestApiRouteFactory = () => ({ + method: 'GET', + path: API_URLS.SERVICE_LOCATIONS, + validate: {}, + handler: async ({ server }): Promise => getServiceLocations({ config: server.config }), +}); From 84ab37720ede46eeb16718dbaf2337f33def01cf Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 30 Nov 2021 11:59:11 -0500 Subject: [PATCH 066/224] Make console log outputs multiline. (#119675) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/synthetics/executed_step.test.tsx | 15 +++++++++++++++ .../components/synthetics/executed_step.tsx | 9 +-------- 2 files changed, 16 insertions(+), 8 deletions(-) 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 f9876593a03d..e14b32fc8da9 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 @@ -82,4 +82,19 @@ describe('ExecutedStep', () => { expect(getByText('Console output')); expect(getByText(browserConsole[0])); }); + + it('renders multi-line console output', () => { + const browserConsole = ['line1', 'line2', 'line3']; + + const { getByText } = render( + + ); + + expect(getByText('Console output')); + + const codeBlock = getByText('line1 line2', { exact: false }); + expect(codeBlock.innerHTML).toEqual(`line1 +line2 +line3`); + }); }); 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 57b94544e598..80786730505a 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx @@ -88,14 +88,7 @@ export const ExecutedStep: FC = ({ loading, step, index, brow language="javascript" initialIsOpen={!isSucceeded} > - <> - {browserConsoles?.map((browserConsole) => ( - <> - {browserConsole} - - - ))} - + {browserConsoles?.join('\n')} From 608258622e338730647acad8b101ea4f0e44a40e Mon Sep 17 00:00:00 2001 From: Trevor Pierce <1Copenut@users.noreply.github.com> Date: Tue, 30 Nov 2021 11:19:42 -0600 Subject: [PATCH 067/224] Adding HTML tag and impact level to axe-core CI violation reporter (#119903) * Changed the axe-report information to include HTML tag and impact level. * One further addition to the ASCIIDOC description of elements. --- .../contributing/development-accessibility-tests.asciidoc | 4 ++-- test/accessibility/services/a11y/axe_report.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/developer/contributing/development-accessibility-tests.asciidoc b/docs/developer/contributing/development-accessibility-tests.asciidoc index 584d779bc7de..2fe2682a3e36 100644 --- a/docs/developer/contributing/development-accessibility-tests.asciidoc +++ b/docs/developer/contributing/development-accessibility-tests.asciidoc @@ -90,7 +90,7 @@ Failures can seem confusing if you've never seen one before. Here is a breakdown [aria-hidden-focus]: Ensures aria-hidden elements do not contain focusable elements Help: https://dequeuniversity.com/rules/axe/3.5/aria-hidden-focus?application=axeAPI Elements: - - #example + - at Accessibility.testAxeReport (test/accessibility/services/a11y/a11y.ts:90:15) at Accessibility.testAppSnapshot (test/accessibility/services/a11y/a11y.ts:58:18) at process._tickCallback (internal/process/next_tick.js:68:7) @@ -100,5 +100,5 @@ Failures can seem confusing if you've never seen one before. Here is a breakdown * "Dashboard" and "create dashboard button" are the names of the test suite and specific test that failed. * Always in brackets, "[aria-hidden-focus]" is the name of the axe rule that failed, followed by a short description. * "Help: " links to the axe documentation for that rule, including severity, remediation tips, and good and bad code examples. -* "Elements:" points to where in the DOM the failure originated (using CSS selector syntax). In this example, the problem came from an element with the ID `example`. If the selector is too complicated to find the source of the problem, use the browser plugins mentioned earlier to locate it. If you have a general idea where the issue is coming from, you can also try adding unique IDs to the page to narrow down the location. +* "Elements:" points to where in the DOM the failure originated (using HTML syntax). In this example, the problem came from a span with the `aria-hidden="true"` attribute and a nested `