diff --git a/.buildkite/pipelines/performance/nightly.yml b/.buildkite/pipelines/performance/nightly.yml new file mode 100644 index 00000000000000..aa52fb7a9335c1 --- /dev/null +++ b/.buildkite/pipelines/performance/nightly.yml @@ -0,0 +1,35 @@ +steps: + - block: ":gear: Performance Tests Configuration" + prompt: "Fill out the details for performance test" + fields: + - text: ":arrows_counterclockwise: Iterations" + key: "performance-test-iteration-count" + hint: "How many times you want to run tests? " + required: true + if: build.env('ITERATION_COUNT_ENV') == null + + - label: ":male-mechanic::skin-tone-2: Pre-Build" + command: .buildkite/scripts/lifecycle/pre_build.sh + + - wait + + - label: ":factory_worker: Build Kibana Distribution and Plugins" + command: .buildkite/scripts/steps/build_kibana.sh + agents: + queue: c2-16 + key: build + + - label: ":muscle: Performance Tests" + command: .buildkite/scripts/steps/functional/performance.sh + agents: + queue: ci-group-6 + depends_on: build + concurrency: 50 + concurrency_group: 'performance-test-group' + + - wait: ~ + continue_on_failure: true + + - label: ":male_superhero::skin-tone-2: Post-Build" + command: .buildkite/scripts/lifecycle/post_build.sh + diff --git a/.buildkite/scripts/steps/functional/performance.sh b/.buildkite/scripts/steps/functional/performance.sh new file mode 100644 index 00000000000000..2f1a77690d1538 --- /dev/null +++ b/.buildkite/scripts/steps/functional/performance.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -uo pipefail + +if [ -z "${ITERATION_COUNT_ENV+x}" ]; then + ITERATION_COUNT="$(buildkite-agent meta-data get performance-test-iteration-count)" +else + ITERATION_COUNT=$ITERATION_COUNT_ENV +fi + +tput setab 2; tput setaf 0; echo "Performance test will be run at ${BUILDKITE_BRANCH} ${ITERATION_COUNT} times" + +cat << EOF | buildkite-agent pipeline upload +steps: + - command: .buildkite/scripts/steps/functional/performance_sub.sh + parallelism: "$ITERATION_COUNT" +EOF + + + diff --git a/.buildkite/scripts/steps/functional/performance_sub.sh b/.buildkite/scripts/steps/functional/performance_sub.sh new file mode 100644 index 00000000000000..d3e6c0ba7304e0 --- /dev/null +++ b/.buildkite/scripts/steps/functional/performance_sub.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/common/util.sh + +.buildkite/scripts/bootstrap.sh +.buildkite/scripts/download_build_artifacts.sh + +cd "$XPACK_DIR" + +echo --- Run Performance Tests +checks-reporter-with-killswitch "Run Performance Tests" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ + --config test/performance/config.ts; diff --git a/.eslintrc.js b/.eslintrc.js index 98ce9bb4bad967..60f3ae1528fbcc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -156,6 +156,78 @@ const DEV_PATTERNS = [ 'x-pack/plugins/*/server/scripts/**/*', ]; +/** Restricted imports with suggested alternatives */ +const RESTRICTED_IMPORTS = [ + { + name: 'lodash', + importNames: ['set', 'setWith'], + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash.set', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash.setwith', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash/set', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash/setWith', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash/fp', + importNames: ['set', 'setWith', 'assoc', 'assocPath'], + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash/fp/set', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash/fp/setWith', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash/fp/assoc', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash/fp/assocPath', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash', + importNames: ['template'], + message: 'lodash.template is unsafe, and not compatible with our content security policy.', + }, + { + name: 'lodash.template', + message: 'lodash.template is unsafe, and not compatible with our content security policy.', + }, + { + name: 'lodash/template', + message: 'lodash.template is unsafe, and not compatible with our content security policy.', + }, + { + name: 'lodash/fp', + importNames: ['template'], + message: 'lodash.template is unsafe, and not compatible with our content security policy.', + }, + { + name: 'lodash/fp/template', + message: 'lodash.template is unsafe, and not compatible with our content security policy.', + }, + { + name: 'react-use', + message: 'Please use react-use/lib/{method} instead.', + }, +]; + module.exports = { root: true, @@ -668,81 +740,7 @@ module.exports = { 'no-restricted-imports': [ 2, { - paths: [ - { - name: 'lodash', - importNames: ['set', 'setWith'], - message: 'Please use @elastic/safer-lodash-set instead', - }, - { - name: 'lodash.set', - message: 'Please use @elastic/safer-lodash-set instead', - }, - { - name: 'lodash.setwith', - message: 'Please use @elastic/safer-lodash-set instead', - }, - { - name: 'lodash/set', - message: 'Please use @elastic/safer-lodash-set instead', - }, - { - name: 'lodash/setWith', - message: 'Please use @elastic/safer-lodash-set instead', - }, - { - name: 'lodash/fp', - importNames: ['set', 'setWith', 'assoc', 'assocPath'], - message: 'Please use @elastic/safer-lodash-set instead', - }, - { - name: 'lodash/fp/set', - message: 'Please use @elastic/safer-lodash-set instead', - }, - { - name: 'lodash/fp/setWith', - message: 'Please use @elastic/safer-lodash-set instead', - }, - { - name: 'lodash/fp/assoc', - message: 'Please use @elastic/safer-lodash-set instead', - }, - { - name: 'lodash/fp/assocPath', - message: 'Please use @elastic/safer-lodash-set instead', - }, - { - name: 'lodash', - importNames: ['template'], - message: - 'lodash.template is unsafe, and not compatible with our content security policy.', - }, - { - name: 'lodash.template', - message: - 'lodash.template is unsafe, and not compatible with our content security policy.', - }, - { - name: 'lodash/template', - message: - 'lodash.template is unsafe, and not compatible with our content security policy.', - }, - { - name: 'lodash/fp', - importNames: ['template'], - message: - 'lodash.template is unsafe, and not compatible with our content security policy.', - }, - { - name: 'lodash/fp/template', - message: - 'lodash.template is unsafe, and not compatible with our content security policy.', - }, - { - name: 'react-use', - message: 'Please use react-use/lib/{method} instead.', - }, - ], + paths: RESTRICTED_IMPORTS, }, ], 'no-restricted-modules': [ @@ -835,6 +833,23 @@ module.exports = { ], }, }, + { + files: ['**/common/**/*.{js,mjs,ts,tsx}', '**/public/**/*.{js,mjs,ts,tsx}'], + rules: { + 'no-restricted-imports': [ + 2, + { + paths: [ + ...RESTRICTED_IMPORTS, + { + name: 'semver', + message: 'Please use "semver/*/{function}" instead', + }, + ], + }, + ], + }, + }, /** * APM and Observability overrides diff --git a/docs/management/connectors/action-types/pagerduty.asciidoc b/docs/management/connectors/action-types/pagerduty.asciidoc index db1c4e3932d148..5e12eddaa5c774 100644 --- a/docs/management/connectors/action-types/pagerduty.asciidoc +++ b/docs/management/connectors/action-types/pagerduty.asciidoc @@ -68,7 +68,7 @@ PagerDuty actions have the following properties. Severity:: The perceived severity of on the affected system. This can be one of `Critical`, `Error`, `Warning` or `Info`(default). Event action:: One of `Trigger` (default), `Resolve`, or `Acknowledge`. See https://v2.developer.pagerduty.com/docs/events-api-v2#event-action[event action] for more details. -Dedup Key:: All actions sharing this key will be associated with the same PagerDuty alert. This value is used to correlate trigger and resolution. This value is *optional*, and if not set, defaults to `:`. The maximum length is *255* characters. See https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication[alert deduplication] for details. +Dedup Key:: All actions sharing this key will be associated with the same PagerDuty alert. This value is used to correlate trigger and resolution. This value is *optional*, and if not set, defaults to `:`. The maximum length is *255* characters. See https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication[alert deduplication] for details. Timestamp:: An *optional* https://v2.developer.pagerduty.com/v2/docs/types#datetime[ISO-8601 format date-time], indicating the time the event was detected or generated. Component:: An *optional* value indicating the component of the source machine that is responsible for the event, for example `mysql` or `eth0`. Group:: An *optional* value indicating the logical grouping of components of a service, for example `app-stack`. diff --git a/docs/management/connectors/action-types/servicenow-sir.asciidoc b/docs/management/connectors/action-types/servicenow-sir.asciidoc index 4556746284d5bd..2fa49fe552c2eb 100644 --- a/docs/management/connectors/action-types/servicenow-sir.asciidoc +++ b/docs/management/connectors/action-types/servicenow-sir.asciidoc @@ -72,13 +72,11 @@ image::management/connectors/images/servicenow-sir-params-test.png[ServiceNow Se ServiceNow SecOps actions have the following configuration properties. Short description:: A short description for the incident, used for searching the contents of the knowledge base. -Source Ips:: A list of source IPs related to the incident. The IPs will be added as observables to the security incident. -Destination Ips:: A list of destination IPs related to the incident. The IPs will be added as observables to the security incident. -Malware URLs:: A list of malware URLs related to the incident. The URLs will be added as observables to the security incident. -Malware Hashes:: A list of malware hashes related to the incident. The hashes will be added as observables to the security incident. Priority:: The priority of the incident. Category:: The category of the incident. Subcategory:: The subcategory of the incident. +Correlation ID:: All actions sharing this ID will be associated with the same ServiceNow security incident. If an incident exists in ServiceNow with the same correlation ID the security incident will be updated. Default value: `:`. +Correlation Display:: A descriptive label of the alert for correlation purposes in ServiceNow. Description:: The details about the incident. Additional comments:: Additional information for the client, such as how to troubleshoot the issue. diff --git a/docs/management/connectors/action-types/servicenow.asciidoc b/docs/management/connectors/action-types/servicenow.asciidoc index cf5244a9e3f9e1..f7c3187f3f024c 100644 --- a/docs/management/connectors/action-types/servicenow.asciidoc +++ b/docs/management/connectors/action-types/servicenow.asciidoc @@ -76,6 +76,8 @@ Severity:: The severity of the incident. Impact:: The effect an incident has on business. Can be measured by the number of affected users or by how critical it is to the business in question. Category:: The category of the incident. Subcategory:: The category of the incident. +Correlation ID:: All actions sharing this ID will be associated with the same ServiceNow incident. If an incident exists in ServiceNow with the same correlation ID the incident will be updated. Default value: `:`. +Correlation Display:: A descriptive label of the alert for correlation purposes in ServiceNow. Short description:: A short description for the incident, used for searching the contents of the knowledge base. Description:: The details about the incident. Additional comments:: Additional information for the client, such as how to troubleshoot the issue. diff --git a/docs/management/connectors/images/servicenow-sir-params-test.png b/docs/management/connectors/images/servicenow-sir-params-test.png index 80103a4272bfac..a2bf8761a88240 100644 Binary files a/docs/management/connectors/images/servicenow-sir-params-test.png and b/docs/management/connectors/images/servicenow-sir-params-test.png differ diff --git a/docs/settings/spaces-settings.asciidoc b/docs/settings/spaces-settings.asciidoc index 969adb93185d06..5483912387ceca 100644 --- a/docs/settings/spaces-settings.asciidoc +++ b/docs/settings/spaces-settings.asciidoc @@ -7,11 +7,6 @@ By default, spaces is enabled in {kib}. To secure spaces, <>. -`xpack.spaces.enabled`:: -deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported and it will not be possible to disable this plugin."] -To enable spaces, set to `true`. -The default is `true`. - `xpack.spaces.maxSpaces`:: The maximum number of spaces that you can use with the {kib} instance. Some {kib} operations return all spaces using a single `_search` from {es}, so you must diff --git a/docs/spaces/index.asciidoc b/docs/spaces/index.asciidoc index 28d29e4822f83f..00e50a41b6ce3d 100644 --- a/docs/spaces/index.asciidoc +++ b/docs/spaces/index.asciidoc @@ -109,11 +109,6 @@ image::images/spaces-configure-landing-page.png["Configure space-level landing p [float] [[spaces-delete-started]] -=== Disable and version updates - -Spaces are automatically enabled in {kib}. If you don't want use this feature, -you can disable it. For more information, refer to <>. - -When you upgrade {kib}, the default space contains all of your existing saved objects. - +=== Disabling spaces +Starting in {kib} 8.0, the Spaces feature cannot be disabled. diff --git a/packages/elastic-apm-generator/src/lib/apm_error.ts b/packages/elastic-apm-generator/src/lib/apm_error.ts new file mode 100644 index 00000000000000..5a48093a26db23 --- /dev/null +++ b/packages/elastic-apm-generator/src/lib/apm_error.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 { Fields } from './entity'; +import { Serializable } from './serializable'; +import { generateLongId, generateShortId } from './utils/generate_id'; + +export class ApmError extends Serializable { + constructor(fields: Fields) { + super({ + ...fields, + 'processor.event': 'error', + 'processor.name': 'error', + 'error.id': generateShortId(), + }); + } + + serialize() { + const [data] = super.serialize(); + data['error.grouping_key'] = generateLongId( + this.fields['error.grouping_name'] || this.fields['error.exception']?.[0]?.message + ); + return [data]; + } +} diff --git a/packages/elastic-apm-generator/src/lib/base_span.ts b/packages/elastic-apm-generator/src/lib/base_span.ts index 6288c16d339b62..f762bf730a7177 100644 --- a/packages/elastic-apm-generator/src/lib/base_span.ts +++ b/packages/elastic-apm-generator/src/lib/base_span.ts @@ -10,7 +10,7 @@ import { Fields } from './entity'; import { Serializable } from './serializable'; import { Span } from './span'; import { Transaction } from './transaction'; -import { generateTraceId } from './utils/generate_id'; +import { generateLongId } from './utils/generate_id'; export class BaseSpan extends Serializable { private readonly _children: BaseSpan[] = []; @@ -19,7 +19,7 @@ export class BaseSpan extends Serializable { super({ ...fields, 'event.outcome': 'unknown', - 'trace.id': generateTraceId(), + 'trace.id': generateLongId(), 'processor.name': 'transaction', }); } diff --git a/packages/elastic-apm-generator/src/lib/entity.ts b/packages/elastic-apm-generator/src/lib/entity.ts index 2a4beee652cf74..bf8fc10efd3a7e 100644 --- a/packages/elastic-apm-generator/src/lib/entity.ts +++ b/packages/elastic-apm-generator/src/lib/entity.ts @@ -6,6 +6,19 @@ * 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 interface Exception { + message: string; +} + export type Fields = Partial<{ '@timestamp': number; 'agent.name': string; @@ -14,6 +27,10 @@ export type Fields = Partial<{ '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; 'metricset.name': string; 'observer.version': string; @@ -46,7 +63,8 @@ export type Fields = Partial<{ '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) { diff --git a/packages/elastic-apm-generator/src/lib/instance.ts b/packages/elastic-apm-generator/src/lib/instance.ts index 4218a9e23f4b4f..3570f497c90555 100644 --- a/packages/elastic-apm-generator/src/lib/instance.ts +++ b/packages/elastic-apm-generator/src/lib/instance.ts @@ -6,7 +6,9 @@ * Side Public License, v 1. */ -import { Entity } from './entity'; +import { ApmError } from './apm_error'; +import { ApplicationMetricFields, Entity } from './entity'; +import { Metricset } from './metricset'; import { Span } from './span'; import { Transaction } from './transaction'; @@ -27,4 +29,20 @@ export class Instance extends Entity { 'span.subtype': spanSubtype, }); } + + error(message: string, type?: string, groupingName?: string) { + return new ApmError({ + ...this.fields, + 'error.exception': [{ message, ...(type ? { type } : {}) }], + 'error.grouping_name': groupingName || message, + }); + } + + appMetrics(metrics: ApplicationMetricFields) { + return new Metricset({ + ...this.fields, + 'metricset.name': 'app', + ...metrics, + }); + } } diff --git a/packages/elastic-apm-generator/src/lib/metricset.ts b/packages/elastic-apm-generator/src/lib/metricset.ts index f7abec6fde9585..c1ebbea3131237 100644 --- a/packages/elastic-apm-generator/src/lib/metricset.ts +++ b/packages/elastic-apm-generator/src/lib/metricset.ts @@ -6,12 +6,15 @@ * Side Public License, v 1. */ +import { Fields } from './entity'; import { Serializable } from './serializable'; -export class Metricset extends Serializable {} - -export function metricset(name: string) { - return new Metricset({ - 'metricset.name': name, - }); +export class Metricset extends Serializable { + constructor(fields: Fields) { + super({ + 'processor.event': 'metric', + 'processor.name': 'metric', + ...fields, + }); + } } diff --git a/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts b/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts index b4cae1b41b9a65..d90ce8e01f83d1 100644 --- a/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts +++ b/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts @@ -25,7 +25,8 @@ export function toElasticsearchOutput(events: Fields[], versionOverride?: string const document = {}; // eslint-disable-next-line guard-for-in for (const key in values) { - set(document, key, values[key as keyof typeof values]); + const val = values[key as keyof typeof values]; + set(document, key, val); } return { _index: `apm-${versionOverride || values['observer.version']}-${values['processor.event']}`, diff --git a/packages/elastic-apm-generator/src/lib/span.ts b/packages/elastic-apm-generator/src/lib/span.ts index 36f7f44816d01d..3c8d90f56b78e6 100644 --- a/packages/elastic-apm-generator/src/lib/span.ts +++ b/packages/elastic-apm-generator/src/lib/span.ts @@ -8,14 +8,14 @@ import { BaseSpan } from './base_span'; import { Fields } from './entity'; -import { generateEventId } from './utils/generate_id'; +import { generateShortId } from './utils/generate_id'; export class Span extends BaseSpan { constructor(fields: Fields) { super({ ...fields, 'processor.event': 'span', - 'span.id': generateEventId(), + 'span.id': generateShortId(), }); } diff --git a/packages/elastic-apm-generator/src/lib/transaction.ts b/packages/elastic-apm-generator/src/lib/transaction.ts index f615f467109969..3a8d32e1843f84 100644 --- a/packages/elastic-apm-generator/src/lib/transaction.ts +++ b/packages/elastic-apm-generator/src/lib/transaction.ts @@ -6,22 +6,48 @@ * Side Public License, v 1. */ +import { ApmError } from './apm_error'; import { BaseSpan } from './base_span'; import { Fields } from './entity'; -import { generateEventId } from './utils/generate_id'; +import { generateShortId } from './utils/generate_id'; export class Transaction extends BaseSpan { private _sampled: boolean = true; + private readonly _errors: ApmError[] = []; constructor(fields: Fields) { super({ ...fields, 'processor.event': 'transaction', - 'transaction.id': generateEventId(), + 'transaction.id': generateShortId(), 'transaction.sampled': true, }); } + parent(span: BaseSpan) { + super.parent(span); + + this._errors.forEach((error) => { + error.fields['trace.id'] = this.fields['trace.id']; + error.fields['transaction.id'] = this.fields['transaction.id']; + error.fields['transaction.type'] = this.fields['transaction.type']; + }); + + return this; + } + + errors(...errors: ApmError[]) { + errors.forEach((error) => { + error.fields['trace.id'] = this.fields['trace.id']; + error.fields['transaction.id'] = this.fields['transaction.id']; + error.fields['transaction.type'] = this.fields['transaction.type']; + }); + + this._errors.push(...errors); + + return this; + } + duration(duration: number) { this.fields['transaction.duration.us'] = duration * 1000; return this; @@ -35,11 +61,13 @@ export class Transaction extends BaseSpan { serialize() { const [transaction, ...spans] = super.serialize(); + const errors = this._errors.flatMap((error) => error.serialize()); + const events = [transaction]; if (this._sampled) { events.push(...spans); } - return events; + return events.concat(errors); } } diff --git a/packages/elastic-apm-generator/src/lib/utils/generate_id.ts b/packages/elastic-apm-generator/src/lib/utils/generate_id.ts index 6c8b33fc190773..cc372a56209aac 100644 --- a/packages/elastic-apm-generator/src/lib/utils/generate_id.ts +++ b/packages/elastic-apm-generator/src/lib/utils/generate_id.ts @@ -12,14 +12,14 @@ let seq = 0; const namespace = 'f38d5b83-8eee-4f5b-9aa6-2107e15a71e3'; -function generateId() { - return uuidv5(String(seq++), namespace).replace(/-/g, ''); +function generateId(seed?: string) { + return uuidv5(seed ?? String(seq++), namespace).replace(/-/g, ''); } -export function generateEventId() { - return generateId().substr(0, 16); +export function generateShortId(seed?: string) { + return generateId(seed).substr(0, 16); } -export function generateTraceId() { - return generateId().substr(0, 32); +export function generateLongId(seed?: string) { + return generateId(seed).substr(0, 32); } diff --git a/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts b/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts index 7aae2986919c87..f6aad154532c28 100644 --- a/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts +++ b/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts @@ -14,11 +14,11 @@ export function simpleTrace(from: number, to: number) { const range = timerange(from, to); - const transactionName = '100rpm (80% success) failed 1000ms'; + const transactionName = '240rpm/60% 1000ms'; const successfulTraceEvents = range - .interval('30s') - .rate(40) + .interval('1s') + .rate(3) .flatMap((timestamp) => instance .transaction(transactionName) @@ -38,21 +38,39 @@ export function simpleTrace(from: number, to: number) { ); const failedTraceEvents = range - .interval('30s') - .rate(10) + .interval('1s') + .rate(1) .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() + ); const events = successfulTraceEvents.concat(failedTraceEvents); return [ ...events, + ...metricsets, ...getTransactionMetrics(events), ...getSpanDestinationMetrics(events), ...getBreakdownMetrics(events), diff --git a/packages/elastic-apm-generator/src/test/scenarios/05_transactions_with_errors.test.ts b/packages/elastic-apm-generator/src/test/scenarios/05_transactions_with_errors.test.ts new file mode 100644 index 00000000000000..289fdfa6cf5658 --- /dev/null +++ b/packages/elastic-apm-generator/src/test/scenarios/05_transactions_with_errors.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { pick } from 'lodash'; +import { service } from '../../index'; +import { Instance } from '../../lib/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'); + }); + it('generates error events', () => { + const events = instance + .transaction('GET /api') + .timestamp(timestamp) + .errors(instance.error('test error').timestamp(timestamp)) + .serialize(); + + const errorEvents = events.filter((event) => event['processor.event'] === 'error'); + + expect(errorEvents.length).toEqual(1); + + expect( + pick(errorEvents[0], 'processor.event', 'processor.name', 'error.exception', '@timestamp') + ).toEqual({ + 'processor.event': 'error', + 'processor.name': 'error', + '@timestamp': timestamp, + 'error.exception': [{ message: 'test error' }], + }); + }); + + it('sets the transaction and trace id', () => { + const [transaction, error] = instance + .transaction('GET /api') + .timestamp(timestamp) + .errors(instance.error('test error').timestamp(timestamp)) + .serialize(); + + const keys = ['transaction.id', 'trace.id', 'transaction.type']; + + expect(pick(error, keys)).toEqual({ + 'transaction.id': transaction['transaction.id'], + 'trace.id': transaction['trace.id'], + 'transaction.type': 'request', + }); + }); + + it('sets the error grouping key', () => { + const [, error] = instance + .transaction('GET /api') + .timestamp(timestamp) + .errors(instance.error('test error').timestamp(timestamp)) + .serialize(); + + expect(error['error.grouping_name']).toEqual('test error'); + expect(error['error.grouping_key']).toMatchInlineSnapshot(`"8b96fa10a7f85a5d960198627bf50840"`); + }); +}); diff --git a/packages/elastic-apm-generator/src/test/scenarios/06_application_metrics.test.ts b/packages/elastic-apm-generator/src/test/scenarios/06_application_metrics.test.ts new file mode 100644 index 00000000000000..59ca8f0edbe88b --- /dev/null +++ b/packages/elastic-apm-generator/src/test/scenarios/06_application_metrics.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { pick } from 'lodash'; +import { service } from '../../index'; +import { Instance } from '../../lib/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'); + }); + it('generates application metricsets', () => { + const events = instance + .appMetrics({ + 'system.memory.actual.free': 80, + 'system.memory.total': 100, + }) + .timestamp(timestamp) + .serialize(); + + const appMetrics = events.filter((event) => event['processor.event'] === 'metric'); + + expect(appMetrics.length).toEqual(1); + + expect( + pick( + appMetrics[0], + '@timestamp', + 'agent.name', + 'container.id', + 'metricset.name', + 'processor.event', + 'processor.name', + 'service.environment', + 'service.name', + 'service.node.name', + 'system.memory.actual.free', + 'system.memory.total' + ) + ).toEqual({ + '@timestamp': timestamp, + 'metricset.name': 'app', + 'processor.event': 'metric', + 'processor.name': 'metric', + 'system.memory.actual.free': 80, + 'system.memory.total': 100, + ...pick( + instance.fields, + 'agent.name', + 'container.id', + 'service.environment', + 'service.name', + 'service.node.name' + ), + }); + }); +}); diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index acce34d2673e16..d1491ba63e6e60 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -8,7 +8,7 @@ pageLoadAssetSize: console: 46091 core: 435325 crossClusterReplication: 65408 - dashboard: 374194 + dashboard: 186763 dashboardEnhanced: 65646 devTools: 38637 discover: 99999 @@ -99,7 +99,7 @@ pageLoadAssetSize: expressionMetricVis: 23121 visTypeMetric: 23332 bfetch: 22837 - kibanaUtils: 97808 + kibanaUtils: 79713 data: 491273 dataViews: 41532 expressions: 140958 diff --git a/packages/kbn-securitysolution-list-constants/src/index.ts b/packages/kbn-securitysolution-list-constants/src/index.ts index 8f5ea4668e00a3..f0e09ff7bb4618 100644 --- a/packages/kbn-securitysolution-list-constants/src/index.ts +++ b/packages/kbn-securitysolution-list-constants/src/index.ts @@ -73,6 +73,6 @@ export const ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION = 'Endpoint Security Event export const ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID = 'endpoint_host_isolation_exceptions'; export const ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_NAME = - 'Endpoint Security Host Isolation Exceptions List'; + 'Endpoint Security Host isolation exceptions List'; export const ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_DESCRIPTION = - 'Endpoint Security Host Isolation Exceptions List'; + 'Endpoint Security Host isolation exceptions List'; diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index ac0aac3466f5f5..a07e12eae8d711 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -327,6 +327,7 @@ export class DocLinksService { preconfiguredConnectors: `${KIBANA_DOCS}pre-configured-connectors.html`, preconfiguredAlertHistoryConnector: `${KIBANA_DOCS}index-action-type.html#preconfigured-connector-alert-history`, serviceNowAction: `${KIBANA_DOCS}servicenow-action-type.html#configuring-servicenow`, + serviceNowSIRAction: `${KIBANA_DOCS}servicenow-sir-action-type.html`, setupPrerequisites: `${KIBANA_DOCS}alerting-setup.html#alerting-prerequisites`, slackAction: `${KIBANA_DOCS}slack-action-type.html#configuring-slack`, teamsAction: `${KIBANA_DOCS}teams-action-type.html#configuring-teams`, diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index 1d3b70348bec19..855ec75995be77 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -322,7 +322,7 @@ describe('deprecations', () => { const { messages } = applyElasticsearchDeprecations({ username: 'elastic' }); expect(messages).toMatchInlineSnapshot(` Array [ - "Setting [${CONFIG_PATH}.username] to \\"elastic\\" is deprecated. You should use the \\"kibana_system\\" user instead.", + "Kibana is configured to authenticate to Elasticsearch with the \\"elastic\\" user. Use a service account token instead.", ] `); }); @@ -331,7 +331,7 @@ describe('deprecations', () => { const { messages } = applyElasticsearchDeprecations({ username: 'kibana' }); expect(messages).toMatchInlineSnapshot(` Array [ - "Setting [${CONFIG_PATH}.username] to \\"kibana\\" is deprecated. You should use the \\"kibana_system\\" user instead.", + "Kibana is configured to authenticate to Elasticsearch with the \\"kibana\\" user. Use a service account token instead.", ] `); }); @@ -350,7 +350,7 @@ describe('deprecations', () => { const { messages } = applyElasticsearchDeprecations({ ssl: { key: '' } }); expect(messages).toMatchInlineSnapshot(` Array [ - "Setting [${CONFIG_PATH}.ssl.key] without [${CONFIG_PATH}.ssl.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.", + "Use both \\"elasticsearch.ssl.key\\" and \\"elasticsearch.ssl.certificate\\" to enable Kibana to use Mutual TLS authentication with Elasticsearch.", ] `); }); @@ -359,7 +359,7 @@ describe('deprecations', () => { const { messages } = applyElasticsearchDeprecations({ ssl: { certificate: '' } }); expect(messages).toMatchInlineSnapshot(` Array [ - "Setting [${CONFIG_PATH}.ssl.certificate] without [${CONFIG_PATH}.ssl.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.", + "Use both \\"elasticsearch.ssl.certificate\\" and \\"elasticsearch.ssl.key\\" to enable Kibana to use Mutual TLS authentication with Elasticsearch.", ] `); }); diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index f130504e3293af..298144ca95a02d 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -8,6 +8,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { readPkcs12Keystore, readPkcs12Truststore } from '@kbn/crypto'; +import { i18n } from '@kbn/i18n'; import { Duration } from 'moment'; import { readFileSync } from 'fs'; import { ConfigDeprecationProvider } from 'src/core/server'; @@ -171,49 +172,82 @@ export const configSchema = schema.object({ }); const deprecations: ConfigDeprecationProvider = () => [ - (settings, fromPath, addDeprecation) => { + (settings, fromPath, addDeprecation, { branch }) => { const es = settings[fromPath]; if (!es) { return; } - if (es.username === 'elastic') { - addDeprecation({ - configPath: `${fromPath}.username`, - message: `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana_system" user instead.`, - correctiveActions: { - manualSteps: [`Replace [${fromPath}.username] from "elastic" to "kibana_system".`], - }, - }); - } else if (es.username === 'kibana') { + + if (es.username === 'elastic' || es.username === 'kibana') { + const username = es.username; addDeprecation({ configPath: `${fromPath}.username`, - message: `Setting [${fromPath}.username] to "kibana" is deprecated. You should use the "kibana_system" user instead.`, - correctiveActions: { - manualSteps: [`Replace [${fromPath}.username] from "kibana" to "kibana_system".`], - }, - }); - } - if (es.ssl?.key !== undefined && es.ssl?.certificate === undefined) { - addDeprecation({ - configPath: `${fromPath}.ssl.key`, - message: `Setting [${fromPath}.ssl.key] without [${fromPath}.ssl.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, + title: i18n.translate('core.deprecations.elasticsearchUsername.title', { + defaultMessage: 'Using "elasticsearch.username: {username}" is deprecated', + values: { username }, + }), + message: i18n.translate('core.deprecations.elasticsearchUsername.message', { + defaultMessage: + 'Kibana is configured to authenticate to Elasticsearch with the "{username}" user. Use a service account token instead.', + values: { username }, + }), + level: 'warning', + documentationUrl: `https://www.elastic.co/guide/en/elasticsearch/reference/${branch}/service-accounts.html`, correctiveActions: { manualSteps: [ - `Set [${fromPath}.ssl.certificate] in your kibana configs to enable TLS client authentication to Elasticsearch.`, + i18n.translate('core.deprecations.elasticsearchUsername.manualSteps1', { + defaultMessage: + 'Use the elasticsearch-service-tokens CLI tool to create a new service account token for the "elastic/kibana" service account.', + }), + i18n.translate('core.deprecations.elasticsearchUsername.manualSteps2', { + defaultMessage: 'Add the "elasticsearch.serviceAccountToken" setting to kibana.yml.', + }), + i18n.translate('core.deprecations.elasticsearchUsername.manualSteps3', { + defaultMessage: + 'Remove "elasticsearch.username" and "elasticsearch.password" from kibana.yml.', + }), ], }, }); - } else if (es.ssl?.certificate !== undefined && es.ssl?.key === undefined) { + } + + const addSslDeprecation = (existingSetting: string, missingSetting: string) => { addDeprecation({ - configPath: `${fromPath}.ssl.certificate`, - message: `Setting [${fromPath}.ssl.certificate] without [${fromPath}.ssl.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, + configPath: existingSetting, + title: i18n.translate('core.deprecations.elasticsearchSSL.title', { + defaultMessage: 'Using "{existingSetting}" without "{missingSetting}" has no effect', + values: { existingSetting, missingSetting }, + }), + message: i18n.translate('core.deprecations.elasticsearchSSL.message', { + defaultMessage: + 'Use both "{existingSetting}" and "{missingSetting}" to enable Kibana to use Mutual TLS authentication with Elasticsearch.', + values: { existingSetting, missingSetting }, + }), + level: 'warning', + documentationUrl: `https://www.elastic.co/guide/en/kibana/${branch}/elasticsearch-mutual-tls.html`, correctiveActions: { manualSteps: [ - `Set [${fromPath}.ssl.key] in your kibana configs to enable TLS client authentication to Elasticsearch.`, + i18n.translate('core.deprecations.elasticsearchSSL.manualSteps1', { + defaultMessage: 'Add the "{missingSetting}" setting to kibana.yml.', + values: { missingSetting }, + }), + i18n.translate('core.deprecations.elasticsearchSSL.manualSteps2', { + defaultMessage: + 'Alternatively, if you don\'t want to use Mutual TLS authentication, remove "{existingSetting}" from kibana.yml.', + values: { existingSetting }, + }), ], }, }); - } else if (es.logQueries === true) { + }; + + if (es.ssl?.key !== undefined && es.ssl?.certificate === undefined) { + addSslDeprecation(`${fromPath}.ssl.key`, `${fromPath}.ssl.certificate`); + } else if (es.ssl?.certificate !== undefined && es.ssl?.key === undefined) { + addSslDeprecation(`${fromPath}.ssl.certificate`, `${fromPath}.ssl.key`); + } + + 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".`, diff --git a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts index cf27505e8f073c..f85576aa64451f 100644 --- a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts +++ b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts @@ -16,6 +16,7 @@ import { sortOrderSchema } from './common_schemas'; * - filter * - histogram * - nested + * - reverse_nested * - terms * * Not implemented: @@ -37,7 +38,6 @@ import { sortOrderSchema } from './common_schemas'; * - parent * - range * - rare_terms - * - reverse_nested * - sampler * - significant_terms * - significant_text @@ -76,6 +76,9 @@ export const bucketAggsSchemas: Record = { nested: s.object({ path: s.string(), }), + reverse_nested: s.object({ + path: s.maybe(s.string()), + }), terms: s.object({ field: s.maybe(s.string()), collect_mode: s.maybe(s.string()), diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 1827f9b9e8e796..4b5c2e25084edc 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -366,7 +366,6 @@ kibana_vars=( xpack.securitySolution.packagerTaskInterval xpack.securitySolution.prebuiltRulesFromFileSystem xpack.securitySolution.prebuiltRulesFromSavedObjects - xpack.spaces.enabled xpack.spaces.maxSpaces xpack.task_manager.index xpack.task_manager.max_attempts diff --git a/src/plugins/custom_integrations/server/language_clients/index.ts b/src/plugins/custom_integrations/server/language_clients/index.ts index da61f804b42427..0ce45dbcfcd878 100644 --- a/src/plugins/custom_integrations/server/language_clients/index.ts +++ b/src/plugins/custom_integrations/server/language_clients/index.ts @@ -23,18 +23,6 @@ interface LanguageIntegration { const ELASTIC_WEBSITE_URL = 'https://www.elastic.co'; const ELASTICSEARCH_CLIENT_URL = `${ELASTIC_WEBSITE_URL}/guide/en/elasticsearch/client`; export const integrations: LanguageIntegration[] = [ - { - id: 'all', - title: i18n.translate('customIntegrations.languageclients.AllTitle', { - defaultMessage: 'Elasticsearch Clients', - }), - euiIconName: 'logoElasticsearch', - description: i18n.translate('customIntegrations.languageclients.AllDescription', { - defaultMessage: - 'Start building your custom application on top of Elasticsearch with the official language clients.', - }), - docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/index.html`, - }, { id: 'javascript', title: i18n.translate('customIntegrations.languageclients.JavascriptTitle', { @@ -42,8 +30,7 @@ export const integrations: LanguageIntegration[] = [ }), icon: 'nodejs.svg', description: i18n.translate('customIntegrations.languageclients.JavascriptDescription', { - defaultMessage: - 'Start building your custom application on top of Elasticsearch with the official Node.js client.', + defaultMessage: 'Index data to Elasticsearch with the JavaScript client.', }), docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/javascript-api/{branch}/introduction.html`, }, @@ -54,8 +41,7 @@ export const integrations: LanguageIntegration[] = [ }), icon: 'ruby.svg', description: i18n.translate('customIntegrations.languageclients.RubyDescription', { - defaultMessage: - 'Start building your custom application on top of Elasticsearch with the official Ruby client.', + defaultMessage: 'Index data to Elasticsearch with the Ruby client.', }), docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/ruby-api/{branch}/ruby_client.html`, }, @@ -66,8 +52,7 @@ export const integrations: LanguageIntegration[] = [ }), icon: 'go.svg', description: i18n.translate('customIntegrations.languageclients.GoDescription', { - defaultMessage: - 'Start building your custom application on top of Elasticsearch with the official Go client.', + defaultMessage: 'Index data to Elasticsearch with the Go client.', }), docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/go-api/{branch}/overview.html`, }, @@ -78,8 +63,7 @@ export const integrations: LanguageIntegration[] = [ }), icon: 'dotnet.svg', description: i18n.translate('customIntegrations.languageclients.DotNetDescription', { - defaultMessage: - 'Start building your custom application on top of Elasticsearch with the official .NET client.', + defaultMessage: 'Index data to Elasticsearch with the .NET client.', }), docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/net-api/{branch}/index.html`, }, @@ -90,8 +74,7 @@ export const integrations: LanguageIntegration[] = [ }), icon: 'php.svg', description: i18n.translate('customIntegrations.languageclients.PhpDescription', { - defaultMessage: - 'Start building your custom application on top of Elasticsearch with the official .PHP client.', + defaultMessage: 'Index data to Elasticsearch with the PHP client.', }), docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/php-api/{branch}/index.html`, }, @@ -102,8 +85,7 @@ export const integrations: LanguageIntegration[] = [ }), icon: 'perl.svg', description: i18n.translate('customIntegrations.languageclients.PerlDescription', { - defaultMessage: - 'Start building your custom application on top of Elasticsearch with the official Perl client.', + defaultMessage: 'Index data to Elasticsearch with the Perl client.', }), docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/perl-api/{branch}/index.html`, }, @@ -114,8 +96,7 @@ export const integrations: LanguageIntegration[] = [ }), icon: 'python.svg', description: i18n.translate('customIntegrations.languageclients.PythonDescription', { - defaultMessage: - 'Start building your custom application on top of Elasticsearch with the official Python client.', + defaultMessage: 'Index data to Elasticsearch with the Python client.', }), docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/python-api/{branch}/index.html`, }, @@ -126,8 +107,7 @@ export const integrations: LanguageIntegration[] = [ }), icon: 'rust.svg', description: i18n.translate('customIntegrations.languageclients.RustDescription', { - defaultMessage: - 'Start building your custom application on top of Elasticsearch with the official Rust client.', + defaultMessage: 'Index data to Elasticsearch with the Rust client.', }), docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/rust-api/{branch}/index.html`, }, @@ -138,8 +118,7 @@ export const integrations: LanguageIntegration[] = [ }), icon: 'java.svg', description: i18n.translate('customIntegrations.languageclients.JavaDescription', { - defaultMessage: - 'Start building your custom application on top of Elasticsearch with the official Java client.', + defaultMessage: 'Index data to Elasticsearch with the Java client.', }), docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/java-api-client/{branch}/index.html`, }, diff --git a/src/plugins/custom_integrations/server/plugin.test.ts b/src/plugins/custom_integrations/server/plugin.test.ts index 8dee81ba6cba39..3b18d2e960c2aa 100644 --- a/src/plugins/custom_integrations/server/plugin.test.ts +++ b/src/plugins/custom_integrations/server/plugin.test.ts @@ -31,23 +31,10 @@ describe('CustomIntegrationsPlugin', () => { test('should register language clients', () => { const setup = new CustomIntegrationsPlugin(initContext).setup(mockCoreSetup); expect(setup.getAppendCustomIntegrations()).toEqual([ - { - id: 'language_client.all', - title: 'Elasticsearch Clients', - description: - 'Start building your custom application on top of Elasticsearch with the official language clients.', - type: 'ui_link', - shipper: 'language_clients', - uiInternalPath: 'https://www.elastic.co/guide/en/elasticsearch/client/index.html', - isBeta: false, - icons: [{ type: 'eui', src: 'logoElasticsearch' }], - categories: ['elastic_stack', 'custom', 'language_client'], - }, { id: 'language_client.javascript', title: 'Elasticsearch JavaScript Client', - description: - 'Start building your custom application on top of Elasticsearch with the official Node.js client.', + description: 'Index data to Elasticsearch with the JavaScript client.', type: 'ui_link', shipper: 'language_clients', uiInternalPath: @@ -59,8 +46,7 @@ describe('CustomIntegrationsPlugin', () => { { id: 'language_client.ruby', title: 'Elasticsearch Ruby Client', - description: - 'Start building your custom application on top of Elasticsearch with the official Ruby client.', + description: 'Index data to Elasticsearch with the Ruby client.', type: 'ui_link', shipper: 'language_clients', uiInternalPath: @@ -72,8 +58,7 @@ describe('CustomIntegrationsPlugin', () => { { id: 'language_client.go', title: 'Elasticsearch Go Client', - description: - 'Start building your custom application on top of Elasticsearch with the official Go client.', + description: 'Index data to Elasticsearch with the Go client.', type: 'ui_link', shipper: 'language_clients', uiInternalPath: @@ -85,8 +70,7 @@ describe('CustomIntegrationsPlugin', () => { { id: 'language_client.dotnet', title: 'Elasticsearch .NET Client', - description: - 'Start building your custom application on top of Elasticsearch with the official .NET client.', + description: 'Index data to Elasticsearch with the .NET client.', type: 'ui_link', shipper: 'language_clients', uiInternalPath: @@ -98,8 +82,7 @@ describe('CustomIntegrationsPlugin', () => { { id: 'language_client.php', title: 'Elasticsearch PHP Client', - description: - 'Start building your custom application on top of Elasticsearch with the official .PHP client.', + description: 'Index data to Elasticsearch with the PHP client.', type: 'ui_link', shipper: 'language_clients', uiInternalPath: @@ -111,8 +94,7 @@ describe('CustomIntegrationsPlugin', () => { { id: 'language_client.perl', title: 'Elasticsearch Perl Client', - description: - 'Start building your custom application on top of Elasticsearch with the official Perl client.', + description: 'Index data to Elasticsearch with the Perl client.', type: 'ui_link', shipper: 'language_clients', uiInternalPath: @@ -124,8 +106,7 @@ describe('CustomIntegrationsPlugin', () => { { id: 'language_client.python', title: 'Elasticsearch Python Client', - description: - 'Start building your custom application on top of Elasticsearch with the official Python client.', + description: 'Index data to Elasticsearch with the Python client.', type: 'ui_link', shipper: 'language_clients', uiInternalPath: @@ -137,8 +118,7 @@ describe('CustomIntegrationsPlugin', () => { { id: 'language_client.rust', title: 'Elasticsearch Rust Client', - description: - 'Start building your custom application on top of Elasticsearch with the official Rust client.', + description: 'Index data to Elasticsearch with the Rust client.', type: 'ui_link', shipper: 'language_clients', uiInternalPath: @@ -150,8 +130,7 @@ describe('CustomIntegrationsPlugin', () => { { id: 'language_client.java', title: 'Elasticsearch Java Client', - description: - 'Start building your custom application on top of Elasticsearch with the official Java client.', + description: 'Index data to Elasticsearch with the Java client.', type: 'ui_link', shipper: 'language_clients', uiInternalPath: diff --git a/src/plugins/dashboard/common/saved_dashboard_references.ts b/src/plugins/dashboard/common/saved_dashboard_references.ts index 95c141b5d4e7b8..4b3a379068c487 100644 --- a/src/plugins/dashboard/common/saved_dashboard_references.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import Semver from 'semver'; +import semverGt from 'semver/functions/gt'; import { SavedObjectAttributes, SavedObjectReference } from '../../../core/types'; import { DashboardContainerStateWithType, DashboardPanelState } from './types'; import { EmbeddablePersistableStateService } from '../../embeddable/common/types'; @@ -23,7 +23,7 @@ export interface SavedObjectAttributesAndReferences { } const isPre730Panel = (panel: Record): boolean => { - return 'version' in panel ? Semver.gt('7.3.0', panel.version) : true; + return 'version' in panel ? semverGt('7.3.0', panel.version) : true; }; function dashboardAttributesToState(attributes: SavedObjectAttributes): { diff --git a/src/plugins/home/server/services/sample_data/data_sets/logs/index.ts b/src/plugins/home/server/services/sample_data/data_sets/logs/index.ts index ac783c1a2aba68..43d42c25574311 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/logs/index.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/logs/index.ts @@ -20,6 +20,7 @@ const logsDescription = i18n.translate('home.sampleData.logsSpecDescription', { }); const initialAppLinks = [] as AppLinkSchema[]; +export const GLOBE_ICON_PATH = '/plugins/home/assets/sample_data_resources/logs/icon.svg'; export const logsSpecProvider = function (): SampleDatasetSchema { return { id: 'logs', @@ -42,6 +43,6 @@ export const logsSpecProvider = function (): SampleDatasetSchema { }, ], status: 'not_installed', - iconPath: '/plugins/home/assets/sample_data_resources/logs/icon.svg', + iconPath: GLOBE_ICON_PATH, }; }; diff --git a/src/plugins/home/server/services/sample_data/lib/register_with_integrations.ts b/src/plugins/home/server/services/sample_data/lib/register_with_integrations.ts index 96c62b040926c2..e33cd58910fd69 100644 --- a/src/plugins/home/server/services/sample_data/lib/register_with_integrations.ts +++ b/src/plugins/home/server/services/sample_data/lib/register_with_integrations.ts @@ -7,29 +7,26 @@ */ import { CoreSetup } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; import { CustomIntegrationsPluginSetup } from '../../../../../custom_integrations/server'; -import { SampleDatasetSchema } from './sample_dataset_schema'; import { HOME_APP_BASE_PATH } from '../../../../common/constants'; +import { GLOBE_ICON_PATH } from '../data_sets/logs'; export function registerSampleDatasetWithIntegration( customIntegrations: CustomIntegrationsPluginSetup, - core: CoreSetup, - sampleDataset: SampleDatasetSchema + core: CoreSetup ) { customIntegrations.registerCustomIntegration({ - id: sampleDataset.id, - title: sampleDataset.name, - description: sampleDataset.description, + id: 'sample_data_all', + title: i18n.translate('home.sampleData.customIntegrationsTitle', { + defaultMessage: 'Sample Data', + }), + description: i18n.translate('home.sampleData.customIntegrationsDescription', { + defaultMessage: 'Add sample data and assets to Elasticsearch and Kibana.', + }), uiInternalPath: `${HOME_APP_BASE_PATH}#/tutorial_directory/sampleData`, isBeta: false, - icons: sampleDataset.iconPath - ? [ - { - type: 'svg', - src: core.http.basePath.prepend(sampleDataset.iconPath), - }, - ] - : [], + icons: [{ type: 'svg', src: core.http.basePath.prepend(GLOBE_ICON_PATH) }], categories: ['sample_data'], shipper: 'sample_data', }); diff --git a/src/plugins/home/server/services/sample_data/sample_data_registry.test.ts b/src/plugins/home/server/services/sample_data/sample_data_registry.test.ts index 74c4d66c4fb021..3d836d233d72cf 100644 --- a/src/plugins/home/server/services/sample_data/sample_data_registry.test.ts +++ b/src/plugins/home/server/services/sample_data/sample_data_registry.test.ts @@ -28,20 +28,36 @@ describe('SampleDataRegistry', () => { }); describe('setup', () => { - test('should register the three sample datasets', () => { + let sampleDataRegistry: SampleDataRegistry; + beforeEach(() => { const initContext = coreMock.createPluginInitializerContext(); - const plugin = new SampleDataRegistry(initContext); - plugin.setup( + sampleDataRegistry = new SampleDataRegistry(initContext); + }); + + test('should register the three sample datasets', () => { + const setup = sampleDataRegistry.setup( mockCoreSetup, mockUsageCollectionPluginSetup, mockCustomIntegrationsPluginSetup ); + const datasets = setup.getSampleDatasets(); + expect(datasets[0].id).toEqual('flights'); + expect(datasets[2].id).toEqual('ecommerce'); + expect(datasets[1].id).toEqual('logs'); + }); + + test('should register the three sample datasets as single card', () => { + sampleDataRegistry.setup( + mockCoreSetup, + mockUsageCollectionPluginSetup, + mockCustomIntegrationsPluginSetup + ); const ids: string[] = mockCustomIntegrationsPluginSetup.registerCustomIntegration.mock.calls.map((args) => { return args[0].id; }); - expect(ids).toEqual(['flights', 'logs', 'ecommerce']); + expect(ids).toEqual(['sample_data_all']); }); }); }); diff --git a/src/plugins/home/server/services/sample_data/sample_data_registry.ts b/src/plugins/home/server/services/sample_data/sample_data_registry.ts index f966a05c123978..b88f42ca970af8 100644 --- a/src/plugins/home/server/services/sample_data/sample_data_registry.ts +++ b/src/plugins/home/server/services/sample_data/sample_data_registry.ts @@ -28,22 +28,13 @@ export class SampleDataRegistry { constructor(private readonly initContext: PluginInitializerContext) {} private readonly sampleDatasets: SampleDatasetSchema[] = []; - private registerSampleDataSet( - specProvider: SampleDatasetProvider, - core: CoreSetup, - customIntegrations?: CustomIntegrationsPluginSetup - ) { + private registerSampleDataSet(specProvider: SampleDatasetProvider) { let value: SampleDatasetSchema; try { value = sampleDataSchema.validate(specProvider()); } catch (error) { throw new Error(`Unable to register sample dataset spec because it's invalid. ${error}`); } - - if (customIntegrations && core) { - registerSampleDatasetWithIntegration(customIntegrations, core, value); - } - const defaultIndexSavedObjectJson = value.savedObjects.find((savedObjectJson: any) => { return savedObjectJson.type === 'index-pattern' && savedObjectJson.id === value.defaultIndex; }); @@ -86,9 +77,12 @@ export class SampleDataRegistry { ); createUninstallRoute(router, this.sampleDatasets, usageTracker); - this.registerSampleDataSet(flightsSpecProvider, core, customIntegrations); - this.registerSampleDataSet(logsSpecProvider, core, customIntegrations); - this.registerSampleDataSet(ecommerceSpecProvider, core, customIntegrations); + this.registerSampleDataSet(flightsSpecProvider); + this.registerSampleDataSet(logsSpecProvider); + this.registerSampleDataSet(ecommerceSpecProvider); + if (customIntegrations && core) { + registerSampleDatasetWithIntegration(customIntegrations, core); + } return { getSampleDatasets: () => this.sampleDatasets, diff --git a/src/plugins/inspector/public/views/requests/components/request_selector.tsx b/src/plugins/inspector/public/views/requests/components/request_selector.tsx index 2d94c7ff5bb184..04fac0bd93b7e5 100644 --- a/src/plugins/inspector/public/views/requests/components/request_selector.tsx +++ b/src/plugins/inspector/public/views/requests/components/request_selector.tsx @@ -13,118 +13,73 @@ import { i18n } from '@kbn/i18n'; import { EuiBadge, - EuiButtonEmpty, - EuiContextMenuPanel, - EuiContextMenuItem, + EuiComboBox, + EuiComboBoxOptionOption, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, - EuiPopover, - EuiTextColor, EuiToolTip, } from '@elastic/eui'; import { RequestStatus } from '../../../../common/adapters'; import { Request } from '../../../../common/adapters/request/types'; -interface RequestSelectorState { - isPopoverOpen: boolean; -} - interface RequestSelectorProps { requests: Request[]; selectedRequest: Request; - onRequestChanged: Function; + onRequestChanged: (request: Request) => void; } -export class RequestSelector extends Component { +export class RequestSelector extends Component { static propTypes = { requests: PropTypes.array.isRequired, selectedRequest: PropTypes.object.isRequired, onRequestChanged: PropTypes.func, }; - state = { - isPopoverOpen: false, - }; + handleSelected = (selectedOptions: Array>) => { + const selectedOption = this.props.requests.find( + (request) => request.id === selectedOptions[0].value + ); - togglePopover = () => { - this.setState((prevState: RequestSelectorState) => ({ - isPopoverOpen: !prevState.isPopoverOpen, - })); + if (selectedOption) { + this.props.onRequestChanged(selectedOption); + } }; - closePopover = () => { - this.setState({ - isPopoverOpen: false, + renderRequestCombobox() { + const options = this.props.requests.map((item) => { + const hasFailed = item.status === RequestStatus.ERROR; + const testLabel = item.name.replace(/\s+/, '_'); + + return { + 'data-test-subj': `inspectorRequestChooser${testLabel}`, + label: hasFailed + ? `${item.name} ${i18n.translate('inspector.requests.failedLabel', { + defaultMessage: ' (failed)', + })}` + : item.name, + value: item.id, + }; }); - }; - - renderRequestDropdownItem = (request: Request, index: number) => { - const hasFailed = request.status === RequestStatus.ERROR; - const inProgress = request.status === RequestStatus.PENDING; return ( - { - this.props.onRequestChanged(request); - this.closePopover(); - }} - toolTipContent={request.description} - toolTipPosition="left" - data-test-subj={`inspectorRequestChooser${request.name}`} - > - - {request.name} - - {hasFailed && ( - - )} - - {inProgress && ( - - )} - - - ); - }; - - renderRequestDropdown() { - const button = ( - - {this.props.selectedRequest.name} - - ); - - return ( - - - + isClearable={false} + onChange={this.handleSelected} + options={options} + prepend="Request" + selectedOptions={[ + { + label: this.props.selectedRequest.name, + value: this.props.selectedRequest.id, + }, + ]} + singleSelection={{ asPlainText: true }} + /> ); } @@ -132,23 +87,8 @@ export class RequestSelector extends Component - - - - - - - {requests.length <= 1 && ( -
- {selectedRequest.name} -
- )} - {requests.length > 1 && this.renderRequestDropdown()} -
+ + {requests.length && this.renderRequestCombobox()} {selectedRequest.status !== RequestStatus.PENDING && ( { - eventEmitter.emit('embeddableRendered'); + embeddableHandler.render(visRef.current).then(() => { + setTimeout(async () => { + eventEmitter.emit('embeddableRendered'); + }); }); return () => embeddableHandler.destroy(); diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 0c7d58453db69c..5868489934dc51 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -358,7 +358,7 @@ export class VisualizeEmbeddable this.subscriptions.push(this.handler.loading$.subscribe(this.onContainerLoading)); this.subscriptions.push(this.handler.render$.subscribe(this.onContainerRender)); - this.updateHandler(); + await this.updateHandler(); } public destroy() { diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 9b2d6bfe25b325..48f850539c20c5 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -33,9 +33,6 @@ const createStartContract = (): VisualizationsStart => ({ getAliases: jest.fn(), getByGroup: jest.fn(), unRegisterAlias: jest.fn(), - savedVisualizationsLoader: { - get: jest.fn(), - } as any, getSavedVisualization: jest.fn(), saveVisualization: jest.fn(), findListItems: jest.fn(), diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 87095f5c389edf..60c50d018252b5 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -18,7 +18,6 @@ import { setUsageCollector, setExpressions, setUiActions, - setSavedVisualizationsLoader, setTimeFilter, setAggs, setChrome, @@ -39,7 +38,6 @@ import { visDimension as visDimensionExpressionFunction } from '../common/expres import { xyDimension as xyDimensionExpressionFunction } from '../common/expression_functions/xy_dimension'; import { createStartServicesGetter, StartServicesGetter } from '../../kibana_utils/public'; -import { createSavedVisLoader, SavedVisualizationsLoader } from './saved_visualizations'; import type { SerializedVis, Vis } from './vis'; import { showNewVisModal } from './wizard'; @@ -83,7 +81,6 @@ import type { VisSavedObject, SaveVisOptions, GetVisOptions } from './types'; export type VisualizationsSetup = TypesSetup; export interface VisualizationsStart extends TypesStart { - savedVisualizationsLoader: SavedVisualizationsLoader; createVis: (visType: string, visState: SerializedVis) => Promise; convertToSerializedVis: typeof convertToSerializedVis; convertFromSerializedVis: typeof convertFromSerializedVis; @@ -194,14 +191,6 @@ export class VisualizationsPlugin setSpaces(spaces); } - const savedVisualizationsLoader = createSavedVisLoader({ - savedObjectsClient: core.savedObjects.client, - indexPatterns: data.indexPatterns, - savedObjects, - visualizationTypes: types, - }); - setSavedVisualizationsLoader(savedVisualizationsLoader); - return { ...types, showNewVisModal, @@ -236,7 +225,6 @@ export class VisualizationsPlugin await createVisAsync(visType, visState), convertToSerializedVis, convertFromSerializedVis, - savedVisualizationsLoader, __LEGACY: { createVisEmbeddableFromObject: createVisEmbeddableFromObject({ start: this.getStartServicesOrDie!, diff --git a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts deleted file mode 100644 index 9107805185fe3d..00000000000000 --- a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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. - */ - -/** - * @name SavedVis - * - * @extends SavedObject. - * - * NOTE: It's a type of SavedObject, but specific to visualizations. - */ -import type { SavedObjectsStart, SavedObject } from '../../../../plugins/saved_objects/public'; -// @ts-ignore -import { updateOldState } from '../legacy/vis_update_state'; -import { extractReferences, injectReferences } from '../utils/saved_visualization_references'; -import type { SavedObjectsClientContract } from '../../../../core/public'; -import type { IndexPatternsContract } from '../../../../plugins/data/public'; -import type { ISavedVis } from '../types'; - -export interface SavedVisServices { - savedObjectsClient: SavedObjectsClientContract; - savedObjects: SavedObjectsStart; - indexPatterns: IndexPatternsContract; -} - -/** @deprecated **/ -export function createSavedVisClass(services: SavedVisServices) { - class SavedVis extends services.savedObjects.SavedObjectClass { - public static type: string = 'visualization'; - public static mapping: Record = { - title: 'text', - visState: 'json', - uiStateJSON: 'text', - description: 'text', - savedSearchId: 'keyword', - version: 'integer', - }; - // Order these fields to the top, the rest are alphabetical - public static fieldOrder = ['title', 'description']; - - constructor(opts: Record | string = {}) { - if (typeof opts !== 'object') { - opts = { id: opts }; - } - const visState = !opts.type ? null : { type: opts.type }; - // Gives our SavedWorkspace the properties of a SavedObject - super({ - type: SavedVis.type, - mapping: SavedVis.mapping, - extractReferences, - injectReferences, - id: (opts.id as string) || '', - indexPattern: opts.indexPattern, - defaults: { - title: '', - visState, - uiStateJSON: '{}', - description: '', - savedSearchId: opts.savedSearchId, - version: 1, - }, - afterESResp: async (savedObject: SavedObject) => { - const savedVis = savedObject as any as ISavedVis; - savedVis.visState = await updateOldState(savedVis.visState); - if (savedVis.searchSourceFields?.index) { - await services.indexPatterns.get(savedVis.searchSourceFields.index as any); - } - return savedVis as any as SavedObject; - }, - }); - this.showInRecentlyAccessed = true; - this.getFullPath = () => { - return `/app/visualize#/edit/${this.id}`; - }; - } - } - - return SavedVis as unknown as new (opts: Record | string) => SavedObject; -} diff --git a/src/plugins/visualizations/public/saved_visualizations/find_list_items.test.ts b/src/plugins/visualizations/public/saved_visualizations/find_list_items.test.ts deleted file mode 100644 index 229f5a4ffd05c1..00000000000000 --- a/src/plugins/visualizations/public/saved_visualizations/find_list_items.test.ts +++ /dev/null @@ -1,204 +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 { findListItems } from './find_list_items'; -import { coreMock } from '../../../../core/public/mocks'; -import { SavedObjectsClientContract } from '../../../../core/public'; -import { VisTypeAlias } from '../vis_types'; - -describe('saved_visualizations', () => { - function testProps() { - const savedObjects = coreMock.createStart().savedObjects - .client as jest.Mocked; - (savedObjects.find as jest.Mock).mockImplementation(() => ({ - total: 0, - savedObjects: [], - })); - return { - visTypes: [], - search: '', - size: 10, - savedObjectsClient: savedObjects, - mapSavedObjectApiHits: jest.fn(), - }; - } - - it('searches visualization title and description', async () => { - const props = testProps(); - const { find } = props.savedObjectsClient; - await findListItems(props); - expect(find.mock.calls).toMatchObject([ - [ - { - type: ['visualization'], - searchFields: ['title^3', 'description'], - }, - ], - ]); - }); - - it('searches searchFields and types specified by app extensions', async () => { - const props = { - ...testProps(), - visTypes: [ - { - appExtensions: { - visualizations: { - docTypes: ['bazdoc', 'etc'], - searchFields: ['baz', 'bing'], - }, - }, - } as VisTypeAlias, - ], - }; - const { find } = props.savedObjectsClient; - await findListItems(props); - expect(find.mock.calls).toMatchObject([ - [ - { - type: ['bazdoc', 'etc', 'visualization'], - searchFields: ['baz', 'bing', 'title^3', 'description'], - }, - ], - ]); - }); - - it('deduplicates types and search fields', async () => { - const props = { - ...testProps(), - visTypes: [ - { - appExtensions: { - visualizations: { - docTypes: ['bazdoc', 'bar'], - searchFields: ['baz', 'bing', 'barfield'], - }, - }, - } as VisTypeAlias, - { - appExtensions: { - visualizations: { - docTypes: ['visualization', 'foo', 'bazdoc'], - searchFields: ['baz', 'bing', 'foofield'], - }, - }, - } as VisTypeAlias, - ], - }; - const { find } = props.savedObjectsClient; - await findListItems(props); - expect(find.mock.calls).toMatchObject([ - [ - { - type: ['bazdoc', 'bar', 'visualization', 'foo'], - searchFields: ['baz', 'bing', 'barfield', 'foofield', 'title^3', 'description'], - }, - ], - ]); - }); - - it('searches the search term prefix', async () => { - const props = { - ...testProps(), - search: 'ahoythere', - }; - const { find } = props.savedObjectsClient; - await findListItems(props); - expect(find.mock.calls).toMatchObject([ - [ - { - search: 'ahoythere*', - }, - ], - ]); - }); - - it('searches with references', async () => { - const props = { - ...testProps(), - references: [ - { type: 'foo', id: 'hello' }, - { type: 'bar', id: 'dolly' }, - ], - }; - const { find } = props.savedObjectsClient; - await findListItems(props); - expect(find.mock.calls).toMatchObject([ - [ - { - hasReference: [ - { type: 'foo', id: 'hello' }, - { type: 'bar', id: 'dolly' }, - ], - }, - ], - ]); - }); - - it('uses type-specific toListItem function, if available', async () => { - const props = { - ...testProps(), - mapSavedObjectApiHits(savedObject: { - id: string; - type: string; - attributes: { title: string }; - }) { - return { - id: savedObject.id, - title: `DEFAULT ${savedObject.attributes.title}`, - }; - }, - visTypes: [ - { - appExtensions: { - visualizations: { - docTypes: ['wizard'], - toListItem(savedObject) { - return { - id: savedObject.id, - title: `${(savedObject.attributes as { label: string }).label} THE GRAY`, - }; - }, - }, - }, - } as VisTypeAlias, - ], - }; - - (props.savedObjectsClient.find as jest.Mock).mockImplementationOnce(async () => ({ - total: 2, - savedObjects: [ - { - id: 'lotr', - type: 'wizard', - attributes: { label: 'Gandalf' }, - }, - { - id: 'wat', - type: 'visualization', - attributes: { title: 'WATEVER' }, - }, - ], - })); - - const items = await findListItems(props); - expect(items).toEqual({ - total: 2, - hits: [ - { - id: 'lotr', - title: 'Gandalf THE GRAY', - }, - { - id: 'wat', - title: 'DEFAULT WATEVER', - }, - ], - }); - }); -}); diff --git a/src/plugins/visualizations/public/saved_visualizations/find_list_items.ts b/src/plugins/visualizations/public/saved_visualizations/find_list_items.ts deleted file mode 100644 index f000b18413ce32..00000000000000 --- a/src/plugins/visualizations/public/saved_visualizations/find_list_items.ts +++ /dev/null @@ -1,78 +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 _ from 'lodash'; -import { - SavedObjectAttributes, - SavedObjectsClientContract, - SavedObjectsFindOptionsReference, - SavedObjectsFindOptions, -} from '../../../../core/public'; -import { SavedObjectLoader } from '../../../../plugins/saved_objects/public'; -import type { VisTypeAlias } from '../vis_types'; -import { VisualizationsAppExtension } from '../vis_types/vis_type_alias_registry'; - -/** - * Search for visualizations and convert them into a list display-friendly format. - */ -export async function findListItems({ - visTypes, - search, - size, - savedObjectsClient, - mapSavedObjectApiHits, - references, -}: { - search: string; - size: number; - visTypes: VisTypeAlias[]; - savedObjectsClient: SavedObjectsClientContract; - mapSavedObjectApiHits: SavedObjectLoader['mapSavedObjectApiHits']; - references?: SavedObjectsFindOptionsReference[]; -}) { - const extensions = visTypes - .map((v) => v.appExtensions?.visualizations) - .filter(Boolean) as VisualizationsAppExtension[]; - const extensionByType = extensions.reduce((acc, m) => { - return m!.docTypes.reduce((_acc, type) => { - acc[type] = m; - return acc; - }, acc); - }, {} as { [visType: string]: VisualizationsAppExtension }); - const searchOption = (field: string, ...defaults: string[]) => - _(extensions).map(field).concat(defaults).compact().flatten().uniq().value() as string[]; - const searchOptions: SavedObjectsFindOptions = { - type: searchOption('docTypes', 'visualization'), - searchFields: searchOption('searchFields', 'title^3', 'description'), - search: search ? `${search}*` : undefined, - perPage: size, - page: 1, - defaultSearchOperator: 'AND' as 'AND', - hasReference: references, - }; - - const { total, savedObjects } = await savedObjectsClient.find( - searchOptions - ); - - return { - total, - hits: savedObjects.map((savedObject) => { - const config = extensionByType[savedObject.type]; - - if (config) { - return { - ...config.toListItem(savedObject), - references: savedObject.references, - }; - } else { - return mapSavedObjectApiHits(savedObject); - } - }), - }; -} diff --git a/src/plugins/visualizations/public/saved_visualizations/index.ts b/src/plugins/visualizations/public/saved_visualizations/index.ts deleted file mode 100644 index e42348bc0b4349..00000000000000 --- a/src/plugins/visualizations/public/saved_visualizations/index.ts +++ /dev/null @@ -1,9 +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. - */ - -export * from './saved_visualizations'; diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts deleted file mode 100644 index cec65b8f988b3a..00000000000000 --- a/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts +++ /dev/null @@ -1,89 +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 type { SavedObjectReference, SavedObjectsFindOptionsReference } from 'kibana/public'; -import { SavedObjectLoader } from '../../../../plugins/saved_objects/public'; -import { findListItems } from './find_list_items'; -import { createSavedVisClass, SavedVisServices } from './_saved_vis'; -import type { TypesStart } from '../vis_types'; - -export interface SavedVisServicesWithVisualizations extends SavedVisServices { - visualizationTypes: TypesStart; -} -export type SavedVisualizationsLoader = ReturnType; - -export interface FindListItemsOptions { - size?: number; - references?: SavedObjectsFindOptionsReference[]; -} - -/** @deprecated **/ -export function createSavedVisLoader(services: SavedVisServicesWithVisualizations) { - const { savedObjectsClient, visualizationTypes } = services; - - class SavedObjectLoaderVisualize extends SavedObjectLoader { - mapHitSource = ( - source: Record, - id: string, - references: SavedObjectReference[] = [] - ) => { - const visTypes = visualizationTypes; - source.id = id; - source.references = references; - source.url = this.urlFor(id); - - let typeName = source.typeName; - if (source.visState) { - try { - typeName = JSON.parse(String(source.visState)).type; - } catch (e) { - /* missing typename handled below */ - } - } - - if (!typeName || !visTypes.get(typeName)) { - source.error = 'Unknown visualization type'; - return source; - } - - source.type = visTypes.get(typeName); - source.savedObjectType = 'visualization'; - source.icon = source.type.icon; - source.image = source.type.image; - source.typeTitle = source.type.title; - source.editUrl = `/edit/${id}`; - - return source; - }; - urlFor(id: string) { - return `#/edit/${encodeURIComponent(id)}`; - } - // This behaves similarly to find, except it returns visualizations that are - // defined as appExtensions and which may not conform to type: visualization - findListItems(search: string = '', sizeOrOptions: number | FindListItemsOptions = 100) { - const { size = 100, references = undefined } = - typeof sizeOrOptions === 'number' - ? { - size: sizeOrOptions, - } - : sizeOrOptions; - return findListItems({ - search, - size, - references, - mapSavedObjectApiHits: this.mapSavedObjectApiHits.bind(this), - savedObjectsClient, - visTypes: visualizationTypes.getAliases(), - }); - } - } - const SavedVis = createSavedVisClass(services); - return new SavedObjectLoaderVisualize(SavedVis, savedObjectsClient) as SavedObjectLoader & { - findListItems: (search: string, sizeOrOptions?: number | FindListItemsOptions) => any; - }; -} diff --git a/src/plugins/visualizations/public/services.ts b/src/plugins/visualizations/public/services.ts index ed18884d9dc831..95f5fa02c09a85 100644 --- a/src/plugins/visualizations/public/services.ts +++ b/src/plugins/visualizations/public/services.ts @@ -18,13 +18,11 @@ import type { } from '../../../core/public'; import type { TypesStart } from './vis_types'; import { createGetterSetter } from '../../../plugins/kibana_utils/public'; -import type { DataPublicPluginStart, TimefilterContract } from '../../../plugins/data/public'; -import type { UsageCollectionSetup } from '../../../plugins/usage_collection/public'; -import type { ExpressionsStart } from '../../../plugins/expressions/public'; -import type { UiActionsStart } from '../../../plugins/ui_actions/public'; -import type { SavedVisualizationsLoader } from './saved_visualizations'; -import type { EmbeddableStart } from '../../embeddable/public'; - +import { DataPublicPluginStart, TimefilterContract } from '../../../plugins/data/public'; +import { UsageCollectionSetup } from '../../../plugins/usage_collection/public'; +import { ExpressionsStart } from '../../../plugins/expressions/public'; +import { UiActionsStart } from '../../../plugins/ui_actions/public'; +import { EmbeddableStart } from '../../embeddable/public'; import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public'; export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); @@ -57,9 +55,6 @@ export const [getExpressions, setExpressions] = createGetterSetter('UiActions'); -export const [getSavedVisualizationsLoader, setSavedVisualizationsLoader] = - createGetterSetter('SavedVisualisationsLoader'); - export const [getAggs, setAggs] = createGetterSetter('AggConfigs'); diff --git a/src/plugins/visualizations/server/saved_objects/visualization.ts b/src/plugins/visualizations/server/saved_objects/visualization.ts index 53027d5d5046cd..0793893f1d3d50 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization.ts @@ -12,7 +12,8 @@ import { visualizationSavedObjectTypeMigrations } from '../migrations/visualizat export const visualizationSavedObjectType: SavedObjectsType = { name: 'visualization', hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', management: { icon: 'visualizeApp', defaultSearchField: 'title', diff --git a/test/api_integration/apis/custom_integration/integrations.ts b/test/api_integration/apis/custom_integration/integrations.ts index 816e360c5a30bc..e4797b334a8662 100644 --- a/test/api_integration/apis/custom_integration/integrations.ts +++ b/test/api_integration/apis/custom_integration/integrations.ts @@ -22,12 +22,12 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.be.an('array'); - // sample data - expect(resp.body.length).to.be.above(14); // at least the language clients + sample data + add data + expect(resp.body.length).to.be(12); - ['flights', 'logs', 'ecommerce'].forEach((sampleData) => { - expect(resp.body.findIndex((c: { id: string }) => c.id === sampleData)).to.be.above(-1); - }); + // Test for sample data card + expect(resp.body.findIndex((c: { id: string }) => c.id === 'sample_data_all')).to.be.above( + -1 + ); }); }); diff --git a/test/api_integration/apis/saved_objects/find.ts b/test/api_integration/apis/saved_objects/find.ts index 9b2b3a96cba5b7..c8623f08e6f972 100644 --- a/test/api_integration/apis/saved_objects/find.ts +++ b/test/api_integration/apis/saved_objects/find.ts @@ -14,6 +14,9 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); const SPACE_ID = 'ftr-so-find'; + const UUID_PATTERN = new RegExp( + /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i + ); describe('find', () => { before(async () => { @@ -25,7 +28,7 @@ export default function ({ getService }: FtrProviderContext) { await kibanaServer.spaces.create({ id: `${SPACE_ID}-foo`, name: `${SPACE_ID}-foo` }); await kibanaServer.importExport.load( - 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic/foo-ns.json', + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json', { space: `${SPACE_ID}-foo`, } @@ -128,22 +131,25 @@ export default function ({ getService }: FtrProviderContext) { describe('wildcard namespace', () => { it('should return 200 with individual responses from the all namespaces', async () => await supertest - .get(`/api/saved_objects/_find?type=visualization&fields=title&namespaces=*`) + .get( + `/api/saved_objects/_find?type=visualization&fields=title&fields=originId&namespaces=*` + ) .expect(200) .then((resp) => { const knownDocuments = resp.body.saved_objects.filter((so: { namespaces: string[] }) => so.namespaces.some((ns) => [SPACE_ID, `${SPACE_ID}-foo`].includes(ns)) ); + const [obj1, obj2] = knownDocuments.map( + ({ id, originId, namespaces }: SavedObject) => ({ id, originId, namespaces }) + ); - expect( - knownDocuments.map((so: { id: string; namespaces: string[] }) => ({ - id: so.id, - namespaces: so.namespaces, - })) - ).to.eql([ - { id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', namespaces: [SPACE_ID] }, - { id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', namespaces: [`${SPACE_ID}-foo`] }, - ]); + expect(obj1.id).to.equal('dd7caf20-9efd-11e7-acb3-3dab96693fab'); + expect(obj1.originId).to.equal(undefined); + expect(obj1.namespaces).to.eql([SPACE_ID]); + + expect(obj2.id).to.match(UUID_PATTERN); // this was imported to the second space and hit an unresolvable conflict, so the object ID was regenerated silently + expect(obj2.originId).to.equal('dd7caf20-9efd-11e7-acb3-3dab96693fab'); + expect(obj2.namespaces).to.eql([`${SPACE_ID}-foo`]); })); }); diff --git a/test/api_integration/apis/saved_objects_management/find.ts b/test/api_integration/apis/saved_objects_management/find.ts index 79d8a645d3ba73..bb1840d6d4e876 100644 --- a/test/api_integration/apis/saved_objects_management/find.ts +++ b/test/api_integration/apis/saved_objects_management/find.ts @@ -220,7 +220,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, - namespaceType: 'single', + namespaceType: 'multiple-isolated', }); expect(resp.body.saved_objects[1].meta).to.eql({ icon: 'visualizeApp', @@ -230,7 +230,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, - namespaceType: 'single', + namespaceType: 'multiple-isolated', }); })); diff --git a/test/api_integration/apis/saved_objects_management/relationships.ts b/test/api_integration/apis/saved_objects_management/relationships.ts index 47a0bedd7d77b6..cab323ca028aee 100644 --- a/test/api_integration/apis/saved_objects_management/relationships.ts +++ b/test/api_integration/apis/saved_objects_management/relationships.ts @@ -107,7 +107,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, - namespaceType: 'single', + namespaceType: 'multiple-isolated', hiddenType: false, }, }, @@ -149,7 +149,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, - namespaceType: 'single', + namespaceType: 'multiple-isolated', hiddenType: false, }, relationship: 'parent', @@ -192,7 +192,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, - namespaceType: 'single', + namespaceType: 'multiple-isolated', hiddenType: false, }, }, @@ -207,7 +207,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, - namespaceType: 'single', + namespaceType: 'multiple-isolated', hiddenType: false, }, }, @@ -230,7 +230,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, - namespaceType: 'single', + namespaceType: 'multiple-isolated', hiddenType: false, }, relationship: 'child', @@ -245,7 +245,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, - namespaceType: 'single', + namespaceType: 'multiple-isolated', hiddenType: false, }, relationship: 'child', @@ -386,7 +386,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, - namespaceType: 'single', + namespaceType: 'multiple-isolated', hiddenType: false, }, }, @@ -456,7 +456,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, - namespaceType: 'single', + namespaceType: 'multiple-isolated', hiddenType: false, title: 'Visualization', }, diff --git a/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json b/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json index 4f343b81cd4021..09651172e56a33 100644 --- a/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json +++ b/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json @@ -94,4 +94,4 @@ "type": "dashboard", "updated_at": "2017-09-21T18:57:40.826Z", "version": "WzExLDJd" -} \ No newline at end of file +} diff --git a/test/api_integration/fixtures/kbn_archiver/saved_objects/basic/foo-ns.json b/test/api_integration/fixtures/kbn_archiver/saved_objects/basic/foo-ns.json deleted file mode 100644 index 736abf331d3146..00000000000000 --- a/test/api_integration/fixtures/kbn_archiver/saved_objects/basic/foo-ns.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "attributes": { - "buildNum": 8467, - "defaultIndex": "91200a00-9efd-11e7-acb3-3dab96693fab" - }, - "coreMigrationVersion": "7.14.0", - "id": "7.0.0-alpha1", - "migrationVersion": { - "config": "7.13.0" - }, - "references": [], - "type": "config", - "updated_at": "2017-09-21T18:49:16.302Z", - "version": "WzEzLDJd" -} - -{ - "attributes": { - "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", - "timeFieldName": "@timestamp", - "title": "logstash-*" - }, - "coreMigrationVersion": "7.14.0", - "id": "91200a00-9efd-11e7-acb3-3dab96693fab", - "migrationVersion": { - "index-pattern": "7.11.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2017-09-21T18:49:16.270Z", - "version": "WzEyLDJd" -} - -{ - "attributes": { - "description": "", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" - }, - "title": "Count of requests", - "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", - "version": 1, - "visState": "{\"title\":\"Count of requests\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}]}" - }, - "coreMigrationVersion": "7.14.0", - "id": "dd7caf20-9efd-11e7-acb3-3dab96693fab", - "migrationVersion": { - "visualization": "7.13.0" - }, - "references": [ - { - "id": "91200a00-9efd-11e7-acb3-3dab96693fab", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - } - ], - "type": "visualization", - "updated_at": "2017-09-21T18:51:23.794Z", - "version": "WzE0LDJd" -} - -{ - "attributes": { - "description": "", - "hits": 0, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" - }, - "optionsJSON": "{\"darkTheme\":false}", - "panelsJSON": "[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":12,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"}]", - "refreshInterval": { - "display": "Off", - "pause": false, - "value": 0 - }, - "timeFrom": "Wed Sep 16 2015 22:52:17 GMT-0700", - "timeRestore": true, - "timeTo": "Fri Sep 18 2015 12:24:38 GMT-0700", - "title": "Requests", - "version": 1 - }, - "coreMigrationVersion": "7.14.0", - "id": "be3733a0-9efe-11e7-acb3-3dab96693fab", - "migrationVersion": { - "dashboard": "7.11.0" - }, - "references": [ - { - "id": "dd7caf20-9efd-11e7-acb3-3dab96693fab", - "name": "1:panel_1", - "type": "visualization" - } - ], - "type": "dashboard", - "updated_at": "2017-09-21T18:57:40.826Z", - "version": "WzE1LDJd" -} \ No newline at end of file diff --git a/test/functional/apps/discover/_inspector.ts b/test/functional/apps/discover/_inspector.ts index 9ff425be2739b7..10402703875d64 100644 --- a/test/functional/apps/discover/_inspector.ts +++ b/test/functional/apps/discover/_inspector.ts @@ -53,9 +53,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await inspector.open(); await testSubjects.click('inspectorRequestChooser'); let foundZero = false; - for (const subj of ['Documents', 'Total hits', 'Charts']) { + for (const subj of ['Documents', 'Chart_data']) { await testSubjects.click(`inspectorRequestChooser${subj}`); - if (testSubjects.exists('inspectorRequestDetailStatistics', { timeout: 500 })) { + if (await testSubjects.exists('inspectorRequestDetailStatistics', { timeout: 500 })) { await testSubjects.click(`inspectorRequestDetailStatistics`); const requestStatsTotalHits = getHitCount(await inspector.getTableData()); if (requestStatsTotalHits === '0') { diff --git a/test/functional/apps/visualize/_vega_chart.ts b/test/functional/apps/visualize/_vega_chart.ts index b2692c2a00d781..6640b37b4a28a6 100644 --- a/test/functional/apps/visualize/_vega_chart.ts +++ b/test/functional/apps/visualize/_vega_chart.ts @@ -131,9 +131,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should set the default query name if not given in the schema', async () => { - const requests = await inspector.getRequestNames(); + const singleExampleRequest = await inspector.hasSingleRequest(); + const selectedExampleRequest = await inspector.getSelectedOption(); - expect(requests).to.be('Unnamed request #0'); + expect(singleExampleRequest).to.be(true); + expect(selectedExampleRequest).to.equal('Unnamed request #0'); }); it('should log the request statistic', async () => { diff --git a/test/functional/services/inspector.ts b/test/functional/services/inspector.ts index 5364dbebe904c0..753d9b7b0b85ea 100644 --- a/test/functional/services/inspector.ts +++ b/test/functional/services/inspector.ts @@ -16,6 +16,7 @@ export class InspectorService extends FtrService { private readonly flyout = this.ctx.getService('flyout'); private readonly testSubjects = this.ctx.getService('testSubjects'); private readonly find = this.ctx.getService('find'); + private readonly comboBox = this.ctx.getService('comboBox'); private async getIsEnabled(): Promise { const ariaDisabled = await this.testSubjects.getAttribute('openInspectorButton', 'disabled'); @@ -206,20 +207,29 @@ export class InspectorService extends FtrService { } /** - * Returns request name as the comma-separated string + * Returns the selected option value from combobox */ - public async getRequestNames(): Promise { + public async getSelectedOption(): Promise { await this.openInspectorRequestsView(); - const requestChooserExists = await this.testSubjects.exists('inspectorRequestChooser'); - if (requestChooserExists) { - await this.testSubjects.click('inspectorRequestChooser'); - const menu = await this.testSubjects.find('inspectorRequestChooserMenuPanel'); - const requestNames = await menu.getVisibleText(); - return requestNames.trim().split('\n').join(','); + const selectedOption = await this.comboBox.getComboBoxSelectedOptions( + 'inspectorRequestChooser' + ); + + if (selectedOption.length !== 1) { + return 'Combobox has multiple options'; } - const singleRequest = await this.testSubjects.find('inspectorRequestName'); - return await singleRequest.getVisibleText(); + return selectedOption[0]; + } + + /** + * Returns request name as the comma-separated string from combobox + */ + public async getRequestNames(): Promise { + await this.openInspectorRequestsView(); + + const comboBoxOptions = await this.comboBox.getOptionsList('inspectorRequestChooser'); + return comboBoxOptions.trim().split('\n').join(','); } public getOpenRequestStatisticButton() { @@ -233,4 +243,17 @@ export class InspectorService extends FtrService { public getOpenRequestDetailResponseButton() { return this.testSubjects.find('inspectorRequestDetailResponse'); } + + /** + * Returns true if the value equals the combobox options list + * @param value default combobox single option text + */ + public async hasSingleRequest( + value: string = "You've selected all available options" + ): Promise { + await this.openInspectorRequestsView(); + const comboBoxOptions = await this.comboBox.getOptionsList('inspectorRequestChooser'); + + return value === comboBoxOptions; + } } diff --git a/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts index 857e1e9dbe95db..266d7246c35d4f 100644 --- a/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts +++ b/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts @@ -13,6 +13,7 @@ import { } from '../types'; import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from './constants'; +import { FieldStats } from '../field_stats_types'; export interface FailedTransactionsCorrelation extends FieldValuePair { doc_count: number; @@ -42,4 +43,5 @@ export interface FailedTransactionsCorrelationsRawResponse percentileThresholdValue?: number; overallHistogram?: HistogramItem[]; errorHistogram?: HistogramItem[]; + fieldStats?: FieldStats[]; } diff --git a/x-pack/plugins/apm/common/search_strategies/field_stats_types.ts b/x-pack/plugins/apm/common/search_strategies/field_stats_types.ts new file mode 100644 index 00000000000000..d96bb4408f0e89 --- /dev/null +++ b/x-pack/plugins/apm/common/search_strategies/field_stats_types.ts @@ -0,0 +1,57 @@ +/* + * 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 { estypes } from '@elastic/elasticsearch'; +import { SearchStrategyParams } from './types'; + +export interface FieldStatsCommonRequestParams extends SearchStrategyParams { + samplerShardSize: number; +} + +export interface Field { + fieldName: string; + type: string; + cardinality: number; +} + +export interface Aggs { + [key: string]: estypes.AggregationsAggregationContainer; +} + +export interface TopValueBucket { + key: string | number; + doc_count: number; +} + +export interface TopValuesStats { + count?: number; + fieldName: string; + topValues: TopValueBucket[]; + topValuesSampleSize: number; + isTopValuesSampled?: boolean; + topValuesSamplerShardSize?: number; +} + +export interface NumericFieldStats extends TopValuesStats { + min: number; + max: number; + avg: number; + median?: number; +} + +export type KeywordFieldStats = TopValuesStats; + +export interface BooleanFieldStats { + fieldName: string; + count: number; + [key: string]: number | string; +} + +export type FieldStats = + | NumericFieldStats + | KeywordFieldStats + | BooleanFieldStats; diff --git a/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts index 75d526202bb082..2eb2b371594596 100644 --- a/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts +++ b/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts @@ -11,6 +11,7 @@ import { RawResponseBase, SearchStrategyClientParams, } from '../types'; +import { FieldStats } from '../field_stats_types'; export interface LatencyCorrelation extends FieldValuePair { correlation: number; @@ -40,4 +41,5 @@ export interface LatencyCorrelationsRawResponse extends RawResponseBase { overallHistogram?: HistogramItem[]; percentileThresholdValue?: number; latencyCorrelations?: LatencyCorrelation[]; + fieldStats?: FieldStats[]; } diff --git a/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx b/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx new file mode 100644 index 00000000000000..4a0f7d81e24dc3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiPopoverTitle, + EuiSpacer, + EuiText, + EuiTitle, + EuiToolTip, +} from '@elastic/eui'; +import React, { Fragment, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { FieldStats } from '../../../../../common/search_strategies/field_stats_types'; +import { OnAddFilter, TopValues } from './top_values'; +import { useTheme } from '../../../../hooks/use_theme'; + +export function CorrelationsContextPopover({ + fieldName, + fieldValue, + topValueStats, + onAddFilter, +}: { + fieldName: string; + fieldValue: string | number; + topValueStats?: FieldStats; + onAddFilter: OnAddFilter; +}) { + const [infoIsOpen, setInfoOpen] = useState(false); + const theme = useTheme(); + + if (!topValueStats) return null; + + const popoverTitle = ( + + + +
{fieldName}
+
+
+
+ ); + + return ( + + ) => { + setInfoOpen(!infoIsOpen); + }} + aria-label={i18n.translate( + 'xpack.apm.correlations.fieldContextPopover.topFieldValuesAriaLabel', + { + defaultMessage: 'Show top 10 field values', + } + )} + data-test-subj={'apmCorrelationsContextPopoverButton'} + style={{ marginLeft: theme.eui.paddingSizes.xs }} + /> +
+ } + isOpen={infoIsOpen} + closePopover={() => setInfoOpen(false)} + anchorPosition="rightCenter" + data-test-subj={'apmCorrelationsContextPopover'} + > + {popoverTitle} + +
+ {i18n.translate( + 'xpack.apm.correlations.fieldContextPopover.fieldTopValuesLabel', + { + defaultMessage: 'Top 10 values', + } + )} +
+
+ {infoIsOpen ? ( + <> + + {topValueStats.topValuesSampleSize !== undefined && ( + + + + + + + )} + + ) : null} + + ); +} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/index.ts b/x-pack/plugins/apm/public/components/app/correlations/context_popover/index.ts similarity index 78% rename from x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/index.ts rename to x-pack/plugins/apm/public/components/app/correlations/context_popover/index.ts index bea5a3d2d592f1..5588328da44527 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/index.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/context_popover/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { SimplePrivilegeSection } from './simple_privilege_section'; +export { CorrelationsContextPopover } from './context_popover'; diff --git a/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx b/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx new file mode 100644 index 00000000000000..803b474fe77542 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx @@ -0,0 +1,169 @@ +/* + * 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 { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiSpacer, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FieldStats } from '../../../../../common/search_strategies/field_stats_types'; +import { asPercent } from '../../../../../common/utils/formatters'; +import { useTheme } from '../../../../hooks/use_theme'; + +export type OnAddFilter = ({ + fieldName, + fieldValue, + include, +}: { + fieldName: string; + fieldValue: string | number; + include: boolean; +}) => void; + +interface Props { + topValueStats: FieldStats; + compressed?: boolean; + onAddFilter?: OnAddFilter; + fieldValue?: string | number; +} + +export function TopValues({ topValueStats, onAddFilter, fieldValue }: Props) { + const { topValues, topValuesSampleSize, count, fieldName } = topValueStats; + const theme = useTheme(); + + if (!Array.isArray(topValues) || topValues.length === 0) return null; + + const sampledSize = + typeof topValuesSampleSize === 'string' + ? parseInt(topValuesSampleSize, 10) + : topValuesSampleSize; + const progressBarMax = sampledSize ?? count; + return ( +
+ {Array.isArray(topValues) && + topValues.map((value) => { + const isHighlighted = + fieldValue !== undefined && value.key === fieldValue; + const barColor = isHighlighted ? 'accent' : 'primary'; + const valueText = + progressBarMax !== undefined + ? asPercent(value.doc_count, progressBarMax) + : undefined; + + return ( + <> + + + + + {value.key} + + } + className="eui-textTruncate" + aria-label={value.key.toString()} + valueText={valueText} + labelProps={ + isHighlighted + ? { + style: { fontWeight: 'bold' }, + } + : undefined + } + /> + + {fieldName !== undefined && + value.key !== undefined && + onAddFilter !== undefined ? ( + <> + { + onAddFilter({ + fieldName, + fieldValue: + typeof value.key === 'number' + ? value.key.toString() + : value.key, + include: true, + }); + }} + aria-label={i18n.translate( + 'xpack.apm.correlations.fieldContextPopover.addFilterAriaLabel', + { + defaultMessage: 'Filter for {fieldName}: "{value}"', + values: { fieldName, value: value.key }, + } + )} + data-test-subj={`apmFieldContextTopValuesAddFilterButton-${value.key}-${value.key}`} + style={{ + minHeight: 'auto', + width: theme.eui.euiSizeL, + paddingRight: 2, + paddingLeft: 2, + paddingTop: 0, + paddingBottom: 0, + }} + /> + { + onAddFilter({ + fieldName, + fieldValue: + typeof value.key === 'number' + ? value.key.toString() + : value.key, + include: false, + }); + }} + aria-label={i18n.translate( + 'xpack.apm.correlations.fieldContextPopover.removeFilterAriaLabel', + { + defaultMessage: 'Filter out {fieldName}: "{value}"', + values: { fieldName, value: value.key }, + } + )} + data-test-subj={`apmFieldContextTopValuesExcludeFilterButton-${value.key}-${value.key}`} + style={{ + minHeight: 'auto', + width: theme.eui.euiSizeL, + paddingTop: 0, + paddingBottom: 0, + paddingRight: 2, + paddingLeft: 2, + }} + /> + + ) : null} + + + ); + })} +
+ ); +} 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 d73ed9d58e526e..a177733b3ecaf0 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 @@ -15,12 +15,10 @@ import { EuiFlexItem, EuiSpacer, EuiIcon, - EuiLink, EuiTitle, EuiBetaBadge, EuiBadge, EuiToolTip, - RIGHT_ALIGNMENT, EuiSwitch, EuiIconTip, } from '@elastic/eui'; @@ -45,7 +43,7 @@ import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { useSearchStrategy } from '../../../hooks/use_search_strategy'; import { ImpactBar } from '../../shared/ImpactBar'; -import { createHref, push } from '../../shared/Links/url_helpers'; +import { push } from '../../shared/Links/url_helpers'; import { CorrelationsTable } from './correlations_table'; import { FailedTransactionsCorrelationsHelpPopover } from './failed_transactions_correlations_help_popover'; @@ -62,6 +60,9 @@ import { CrossClusterSearchCompatibilityWarning } from './cross_cluster_search_w import { CorrelationsProgressControls } from './progress_controls'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { useTheme } from '../../../hooks/use_theme'; +import { CorrelationsContextPopover } from './context_popover'; +import { FieldStats } from '../../../../common/search_strategies/field_stats_types'; +import { OnAddFilter } from './context_popover/top_values'; export function FailedTransactionsCorrelations({ onFilter, @@ -81,6 +82,14 @@ export function FailedTransactionsCorrelations({ percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, } ); + + const fieldStats: Record | undefined = useMemo(() => { + return response.fieldStats?.reduce((obj, field) => { + obj[field.fieldName] = field; + return obj; + }, {} as Record); + }, [response?.fieldStats]); + const progressNormalized = progress.loaded / progress.total; const { overallHistogram, hasData, status } = getOverallHistogram( response, @@ -104,6 +113,28 @@ export function FailedTransactionsCorrelations({ setShowStats(!showStats); }, [setShowStats, showStats]); + const onAddFilter = useCallback( + ({ fieldName, fieldValue, include }) => { + if (include) { + push(history, { + query: { + kuery: `${fieldName}:"${fieldValue}"`, + }, + }); + trackApmEvent({ metric: 'correlations_term_include_filter' }); + } else { + push(history, { + query: { + kuery: `not ${fieldName}:"${fieldValue}"`, + }, + }); + trackApmEvent({ metric: 'correlations_term_exclude_filter' }); + } + onFilter(); + }, + [onFilter, history, trackApmEvent] + ); + const failedTransactionsCorrelationsColumns: Array< EuiBasicTableColumn > = useMemo(() => { @@ -227,7 +258,6 @@ export function FailedTransactionsCorrelations({ )} ), - align: RIGHT_ALIGNMENT, render: (_, { normalizedScore }) => { return ( <> @@ -264,6 +294,17 @@ export function FailedTransactionsCorrelations({ 'xpack.apm.correlations.failedTransactions.correlationsTable.fieldNameLabel', { defaultMessage: 'Field name' } ), + render: (_, { fieldName, fieldValue }) => ( + <> + {fieldName} + + + ), sortable: true, }, { @@ -290,15 +331,15 @@ export function FailedTransactionsCorrelations({ ), icon: 'plusInCircle', type: 'icon', - onClick: (term: FailedTransactionsCorrelation) => { - push(history, { - query: { - kuery: `${term.fieldName}:"${term.fieldValue}"`, - }, - }); - onFilter(); - trackApmEvent({ metric: 'correlations_term_include_filter' }); - }, + onClick: ({ + fieldName, + fieldValue, + }: FailedTransactionsCorrelation) => + onAddFilter({ + fieldName, + fieldValue, + include: true, + }), }, { name: i18n.translate( @@ -311,49 +352,20 @@ export function FailedTransactionsCorrelations({ ), icon: 'minusInCircle', type: 'icon', - onClick: (term: FailedTransactionsCorrelation) => { - push(history, { - query: { - kuery: `not ${term.fieldName}:"${term.fieldValue}"`, - }, - }); - onFilter(); - trackApmEvent({ metric: 'correlations_term_exclude_filter' }); - }, + onClick: ({ + fieldName, + fieldValue, + }: FailedTransactionsCorrelation) => + onAddFilter({ + fieldName, + fieldValue, + include: false, + }), }, ], - name: i18n.translate( - 'xpack.apm.correlations.correlationsTable.actionsLabel', - { defaultMessage: 'Filter' } - ), - render: (_, { fieldName, fieldValue }) => { - return ( - <> - - - -  /  - - - - - ); - }, }, ] as Array>; - }, [history, onFilter, trackApmEvent, showStats]); + }, [fieldStats, onAddFilter, showStats]); useEffect(() => { if (isErrorMessage(progress.error)) { 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 167df0fd10b40c..75af7fae4ce120 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 @@ -53,6 +53,9 @@ import { CorrelationsLog } from './correlations_log'; import { CorrelationsEmptyStatePrompt } from './empty_state_prompt'; import { CrossClusterSearchCompatibilityWarning } from './cross_cluster_search_warning'; import { CorrelationsProgressControls } from './progress_controls'; +import { FieldStats } from '../../../../common/search_strategies/field_stats_types'; +import { CorrelationsContextPopover } from './context_popover'; +import { OnAddFilter } from './context_popover/top_values'; export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { const { @@ -74,6 +77,13 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { progress.isRunning ); + const fieldStats: Record | undefined = useMemo(() => { + return response.fieldStats?.reduce((obj, field) => { + obj[field.fieldName] = field; + return obj; + }, {} as Record); + }, [response?.fieldStats]); + useEffect(() => { if (isErrorMessage(progress.error)) { notifications.toasts.addDanger({ @@ -104,6 +114,28 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { const history = useHistory(); const trackApmEvent = useUiTracker({ app: 'apm' }); + const onAddFilter = useCallback( + ({ fieldName, fieldValue, include }) => { + if (include) { + push(history, { + query: { + kuery: `${fieldName}:"${fieldValue}"`, + }, + }); + trackApmEvent({ metric: 'correlations_term_include_filter' }); + } else { + push(history, { + query: { + kuery: `not ${fieldName}:"${fieldValue}"`, + }, + }); + trackApmEvent({ metric: 'correlations_term_exclude_filter' }); + } + onFilter(); + }, + [onFilter, history, trackApmEvent] + ); + const mlCorrelationColumns: Array> = useMemo( () => [ @@ -147,6 +179,17 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { 'xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldNameLabel', { defaultMessage: 'Field name' } ), + render: (_, { fieldName, fieldValue }) => ( + <> + {fieldName} + + + ), sortable: true, }, { @@ -172,15 +215,12 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { ), icon: 'plusInCircle', type: 'icon', - onClick: (term: LatencyCorrelation) => { - push(history, { - query: { - kuery: `${term.fieldName}:"${term.fieldValue}"`, - }, - }); - onFilter(); - trackApmEvent({ metric: 'correlations_term_include_filter' }); - }, + onClick: ({ fieldName, fieldValue }: LatencyCorrelation) => + onAddFilter({ + fieldName, + fieldValue, + include: true, + }), }, { name: i18n.translate( @@ -193,15 +233,12 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { ), icon: 'minusInCircle', type: 'icon', - onClick: (term: LatencyCorrelation) => { - push(history, { - query: { - kuery: `not ${term.fieldName}:"${term.fieldValue}"`, - }, - }); - onFilter(); - trackApmEvent({ metric: 'correlations_term_exclude_filter' }); - }, + onClick: ({ fieldName, fieldValue }: LatencyCorrelation) => + onAddFilter({ + fieldName, + fieldValue, + include: false, + }), }, ], name: i18n.translate( @@ -210,7 +247,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { ), }, ], - [history, onFilter, trackApmEvent] + [fieldStats, onAddFilter] ); const [sortField, setSortField] = diff --git a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts index c900123c6cee97..6397c79ce4ffbf 100644 --- a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts +++ b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts @@ -17,8 +17,7 @@ import { argv } from 'yargs'; import { Logger } from 'kibana/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { CollectTelemetryParams } from '../../server/lib/apm_telemetry/collect_data_telemetry'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { unwrapEsResponse } from '../../../observability/server/utils/unwrap_es_response'; +import { unwrapEsResponse } from '../../../observability/common/utils/unwrap_es_response'; import { downloadTelemetryTemplate } from '../shared/download-telemetry-template'; import { mergeApmTelemetryMapping } from '../../common/apm_telemetry'; import { generateSampleDocuments } from './generate-sample-documents'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/constants.ts b/x-pack/plugins/apm/server/lib/search_strategies/constants.ts index 5500e336c35425..5af1b216307205 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/constants.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/constants.ts @@ -81,3 +81,10 @@ export const CORRELATION_THRESHOLD = 0.3; export const KS_TEST_THRESHOLD = 0.1; export const ERROR_CORRELATION_THRESHOLD = 0.02; + +/** + * Field stats/top values sampling constants + */ + +export const SAMPLER_TOP_TERMS_THRESHOLD = 100000; +export const SAMPLER_TOP_TERMS_SHARD_SIZE = 5000; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts index 239cf39f15ffe9..af5e535abdc3f3 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts @@ -36,6 +36,7 @@ import type { SearchServiceProvider } from '../search_strategy_provider'; import { failedTransactionsCorrelationsSearchServiceStateProvider } from './failed_transactions_correlations_search_service_state'; import { ERROR_CORRELATION_THRESHOLD } from '../constants'; +import { fetchFieldsStats } from '../queries/field_stats/get_fields_stats'; export type FailedTransactionsCorrelationsSearchServiceProvider = SearchServiceProvider< @@ -133,6 +134,7 @@ export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransact state.setProgress({ loadedFieldCandidates: 1 }); let fieldCandidatesFetchedCount = 0; + const fieldsToSample = new Set(); if (params !== undefined && fieldCandidates.length > 0) { const batches = chunk(fieldCandidates, 10); for (let i = 0; i < batches.length; i++) { @@ -150,13 +152,19 @@ export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransact results.forEach((result, idx) => { if (result.status === 'fulfilled') { + const significantCorrelations = result.value.filter( + (record) => + record && + record.pValue !== undefined && + record.pValue < ERROR_CORRELATION_THRESHOLD + ); + + significantCorrelations.forEach((r) => { + fieldsToSample.add(r.fieldName); + }); + state.addFailedTransactionsCorrelations( - result.value.filter( - (record) => - record && - typeof record.pValue === 'number' && - record.pValue < ERROR_CORRELATION_THRESHOLD - ) + significantCorrelations ); } else { // If one of the fields in the batch had an error @@ -184,6 +192,23 @@ export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransact `Identified correlations for ${fieldCandidatesFetchedCount} fields out of ${fieldCandidates.length} candidates.` ); } + + addLogMessage( + `Identified ${fieldsToSample.size} fields to sample for field statistics.` + ); + + const { stats: fieldStats } = await fetchFieldsStats( + esClient, + params, + [...fieldsToSample], + [{ fieldName: EVENT_OUTCOME, fieldValue: EventOutcome.failure }] + ); + + addLogMessage( + `Retrieved field statistics for ${fieldStats.length} fields out of ${fieldsToSample.size} fields.` + ); + + state.addFieldStats(fieldStats); } catch (e) { state.setError(e); } @@ -208,6 +233,7 @@ export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransact errorHistogram, percentileThresholdValue, progress, + fieldStats, } = state.getState(); return { @@ -231,6 +257,7 @@ export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransact overallHistogram, errorHistogram, percentileThresholdValue, + fieldStats, }, }; }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service_state.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service_state.ts index a2530ea8a400ce..ed0fe5d6e178bc 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service_state.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service_state.ts @@ -8,6 +8,7 @@ import { FailedTransactionsCorrelation } from '../../../../common/search_strategies/failed_transactions_correlations/types'; import type { HistogramItem } from '../../../../common/search_strategies/types'; +import { FieldStats } from '../../../../common/search_strategies/field_stats_types'; interface Progress { started: number; @@ -73,6 +74,11 @@ export const failedTransactionsCorrelationsSearchServiceStateProvider = () => { }; } + const fieldStats: FieldStats[] = []; + function addFieldStats(stats: FieldStats[]) { + fieldStats.push(...stats); + } + const failedTransactionsCorrelations: FailedTransactionsCorrelation[] = []; function addFailedTransactionsCorrelation(d: FailedTransactionsCorrelation) { failedTransactionsCorrelations.push(d); @@ -98,6 +104,7 @@ export const failedTransactionsCorrelationsSearchServiceStateProvider = () => { percentileThresholdValue, progress, failedTransactionsCorrelations, + fieldStats, }; } @@ -115,6 +122,7 @@ export const failedTransactionsCorrelationsSearchServiceStateProvider = () => { setErrorHistogram, setPercentileThresholdValue, setProgress, + addFieldStats, }; }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts index 91f4a0d3349a4a..4862f7dd1de1a0 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts @@ -36,6 +36,7 @@ import { searchServiceLogProvider } from '../search_service_log'; import type { SearchServiceProvider } from '../search_strategy_provider'; import { latencyCorrelationsSearchServiceStateProvider } from './latency_correlations_search_service_state'; +import { fetchFieldsStats } from '../queries/field_stats/get_fields_stats'; export type LatencyCorrelationsSearchServiceProvider = SearchServiceProvider< LatencyCorrelationsRequestParams, @@ -196,6 +197,7 @@ export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearch `Loaded fractions and totalDocCount of ${totalDocCount}.` ); + const fieldsToSample = new Set(); let loadedHistograms = 0; for await (const item of fetchTransactionDurationHistograms( esClient, @@ -211,6 +213,7 @@ export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearch )) { if (item !== undefined) { state.addLatencyCorrelation(item); + fieldsToSample.add(item.fieldName); } loadedHistograms++; state.setProgress({ @@ -225,6 +228,19 @@ export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearch fieldValuePairs.length } field/value pairs.` ); + + addLogMessage( + `Identified ${fieldsToSample.size} fields to sample for field statistics.` + ); + + const { stats: fieldStats } = await fetchFieldsStats(esClient, params, [ + ...fieldsToSample, + ]); + + addLogMessage( + `Retrieved field statistics for ${fieldStats.length} fields out of ${fieldsToSample.size} fields.` + ); + state.addFieldStats(fieldStats); } catch (e) { state.setError(e); } @@ -251,6 +267,7 @@ export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearch overallHistogram, percentileThresholdValue, progress, + fieldStats, } = state.getState(); return { @@ -270,6 +287,7 @@ export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearch state.getLatencyCorrelationsSortedByCorrelation(), percentileThresholdValue, overallHistogram, + fieldStats, }, }; }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.ts index 53f357ed1135fd..186099e4c307ab 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.ts @@ -10,6 +10,7 @@ import type { LatencyCorrelationSearchServiceProgress, LatencyCorrelation, } from '../../../../common/search_strategies/latency_correlations/types'; +import { FieldStats } from '../../../../common/search_strategies/field_stats_types'; export const latencyCorrelationsSearchServiceStateProvider = () => { let ccsWarning = false; @@ -79,6 +80,10 @@ export const latencyCorrelationsSearchServiceStateProvider = () => { function getLatencyCorrelationsSortedByCorrelation() { return latencyCorrelations.sort((a, b) => b.correlation - a.correlation); } + const fieldStats: FieldStats[] = []; + function addFieldStats(stats: FieldStats[]) { + fieldStats.push(...stats); + } function getState() { return { @@ -90,6 +95,7 @@ export const latencyCorrelationsSearchServiceStateProvider = () => { percentileThresholdValue, progress, latencyCorrelations, + fieldStats, }; } @@ -106,6 +112,7 @@ export const latencyCorrelationsSearchServiceStateProvider = () => { setOverallHistogram, setPercentileThresholdValue, setProgress, + addFieldStats, }; }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_boolean_field_stats.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_boolean_field_stats.ts new file mode 100644 index 00000000000000..551ecfe3cd4ead --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_boolean_field_stats.ts @@ -0,0 +1,87 @@ +/* + * 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 { ElasticsearchClient } from 'kibana/server'; +import { SearchRequest } from '@elastic/elasticsearch/api/types'; +import { estypes } from '@elastic/elasticsearch'; +import { buildSamplerAggregation } from '../../utils/field_stats_utils'; +import { FieldValuePair } from '../../../../../common/search_strategies/types'; +import { + FieldStatsCommonRequestParams, + BooleanFieldStats, + Aggs, + TopValueBucket, +} from '../../../../../common/search_strategies/field_stats_types'; +import { getQueryWithParams } from '../get_query_with_params'; + +export const getBooleanFieldStatsRequest = ( + params: FieldStatsCommonRequestParams, + fieldName: string, + termFilters?: FieldValuePair[] +): SearchRequest => { + const query = getQueryWithParams({ params, termFilters }); + + const { index, samplerShardSize } = params; + + const size = 0; + const aggs: Aggs = { + sampled_value_count: { + filter: { exists: { field: fieldName } }, + }, + sampled_values: { + terms: { + field: fieldName, + size: 2, + }, + }, + }; + + const searchBody = { + query, + aggs: { + sample: buildSamplerAggregation(aggs, samplerShardSize), + }, + }; + + return { + index, + size, + body: searchBody, + }; +}; + +export const fetchBooleanFieldStats = async ( + esClient: ElasticsearchClient, + params: FieldStatsCommonRequestParams, + field: FieldValuePair, + termFilters?: FieldValuePair[] +): Promise => { + const request = getBooleanFieldStatsRequest( + params, + field.fieldName, + termFilters + ); + const { body } = await esClient.search(request); + const aggregations = body.aggregations as { + sample: { + sampled_value_count: estypes.AggregationsFiltersBucketItemKeys; + sampled_values: estypes.AggregationsTermsAggregate; + }; + }; + + const stats: BooleanFieldStats = { + fieldName: field.fieldName, + count: aggregations?.sample.sampled_value_count.doc_count ?? 0, + }; + + const valueBuckets: TopValueBucket[] = + aggregations?.sample.sampled_values?.buckets ?? []; + valueBuckets.forEach((bucket) => { + stats[`${bucket.key.toString()}Count`] = bucket.doc_count; + }); + return stats; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts new file mode 100644 index 00000000000000..deb89ace47c5db --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts @@ -0,0 +1,170 @@ +/* + * 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 { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; +import { getNumericFieldStatsRequest } from './get_numeric_field_stats'; +import { getKeywordFieldStatsRequest } from './get_keyword_field_stats'; +import { getBooleanFieldStatsRequest } from './get_boolean_field_stats'; +import { estypes } from '@elastic/elasticsearch'; +import { ElasticsearchClient } from 'kibana/server'; +import { fetchFieldsStats } from './get_fields_stats'; + +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, + environment: ENVIRONMENT_ALL.value, + kuery: '', + samplerShardSize: 5000, +}; + +export const getExpectedQuery = (aggs: any) => { + return { + body: { + aggs, + query: { + bool: { + filter: [ + { term: { 'processor.event': 'transaction' } }, + { + range: { + '@timestamp': { + format: 'epoch_millis', + gte: 1577836800000, + lte: 1609459200000, + }, + }, + }, + ], + }, + }, + }, + index: 'apm-*', + size: 0, + }; +}; + +describe('field_stats', () => { + describe('getNumericFieldStatsRequest', () => { + it('returns request with filter, percentiles, and top terms aggregations ', () => { + const req = getNumericFieldStatsRequest(params, 'url.path'); + + const expectedAggs = { + sample: { + aggs: { + sampled_field_stats: { + aggs: { actual_stats: { stats: { field: 'url.path' } } }, + filter: { exists: { field: 'url.path' } }, + }, + sampled_percentiles: { + percentiles: { + field: 'url.path', + keyed: false, + percents: [50], + }, + }, + sampled_top: { + terms: { + field: 'url.path', + order: { _count: 'desc' }, + size: 10, + }, + }, + }, + sampler: { shard_size: 5000 }, + }, + }; + expect(req).toEqual(getExpectedQuery(expectedAggs)); + }); + }); + describe('getKeywordFieldStatsRequest', () => { + it('returns request with top terms sampler aggregation ', () => { + const req = getKeywordFieldStatsRequest(params, 'url.path'); + + const expectedAggs = { + sample: { + sampler: { shard_size: 5000 }, + aggs: { + sampled_top: { + terms: { field: 'url.path', size: 10, order: { _count: 'desc' } }, + }, + }, + }, + }; + expect(req).toEqual(getExpectedQuery(expectedAggs)); + }); + }); + describe('getBooleanFieldStatsRequest', () => { + it('returns request with top terms sampler aggregation ', () => { + const req = getBooleanFieldStatsRequest(params, 'url.path'); + + const expectedAggs = { + sample: { + sampler: { shard_size: 5000 }, + aggs: { + sampled_value_count: { + filter: { exists: { field: 'url.path' } }, + }, + sampled_values: { terms: { field: 'url.path', size: 2 } }, + }, + }, + }; + expect(req).toEqual(getExpectedQuery(expectedAggs)); + }); + }); + + describe('fetchFieldsStats', () => { + it('returns field candidates and total hits', async () => { + const fieldsCaps = { + body: { + fields: { + myIpFieldName: { ip: {} }, + myKeywordFieldName: { keyword: {} }, + myMultiFieldName: { keyword: {}, text: {} }, + myHistogramFieldName: { histogram: {} }, + myNumericFieldName: { number: {} }, + }, + }, + }; + const esClientFieldCapsMock = jest.fn(() => fieldsCaps); + + const fieldsToSample = Object.keys(fieldsCaps.body.fields); + + const esClientSearchMock = jest.fn( + ( + req: estypes.SearchRequest + ): { + body: estypes.SearchResponse; + } => { + return { + body: { + aggregations: { sample: {} }, + } as unknown as estypes.SearchResponse, + }; + } + ); + + const esClientMock = { + fieldCaps: esClientFieldCapsMock, + search: esClientSearchMock, + } as unknown as ElasticsearchClient; + + const resp = await fetchFieldsStats(esClientMock, params, fieldsToSample); + // Should not return stats for unsupported field types like histogram + const expectedFields = [ + 'myIpFieldName', + 'myKeywordFieldName', + 'myMultiFieldName', + 'myNumericFieldName', + ]; + expect(resp.stats.map((s) => s.fieldName)).toEqual(expectedFields); + expect(esClientFieldCapsMock).toHaveBeenCalledTimes(1); + expect(esClientSearchMock).toHaveBeenCalledTimes(4); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_fields_stats.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_fields_stats.ts new file mode 100644 index 00000000000000..2e1441ccbd6a12 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_fields_stats.ts @@ -0,0 +1,110 @@ +/* + * 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 { ElasticsearchClient } from 'kibana/server'; +import { chunk } from 'lodash'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; +import { + FieldValuePair, + SearchStrategyParams, +} from '../../../../../common/search_strategies/types'; +import { getRequestBase } from '../get_request_base'; +import { fetchKeywordFieldStats } from './get_keyword_field_stats'; +import { fetchNumericFieldStats } from './get_numeric_field_stats'; +import { + FieldStats, + FieldStatsCommonRequestParams, +} from '../../../../../common/search_strategies/field_stats_types'; +import { fetchBooleanFieldStats } from './get_boolean_field_stats'; + +export const fetchFieldsStats = async ( + esClient: ElasticsearchClient, + params: SearchStrategyParams, + fieldsToSample: string[], + termFilters?: FieldValuePair[] +): Promise<{ stats: FieldStats[]; errors: any[] }> => { + const stats: FieldStats[] = []; + const errors: any[] = []; + + if (fieldsToSample.length === 0) return { stats, errors }; + + const respMapping = await esClient.fieldCaps({ + ...getRequestBase(params), + fields: fieldsToSample, + }); + + const fieldStatsParams: FieldStatsCommonRequestParams = { + ...params, + samplerShardSize: 5000, + }; + const fieldStatsPromises = Object.entries(respMapping.body.fields) + .map(([key, value], idx) => { + const field: FieldValuePair = { fieldName: key, fieldValue: '' }; + const fieldTypes = Object.keys(value); + + for (const ft of fieldTypes) { + switch (ft) { + case ES_FIELD_TYPES.KEYWORD: + case ES_FIELD_TYPES.IP: + return fetchKeywordFieldStats( + esClient, + fieldStatsParams, + field, + termFilters + ); + break; + + case 'numeric': + case 'number': + case ES_FIELD_TYPES.FLOAT: + case ES_FIELD_TYPES.HALF_FLOAT: + case ES_FIELD_TYPES.SCALED_FLOAT: + case ES_FIELD_TYPES.DOUBLE: + case ES_FIELD_TYPES.INTEGER: + case ES_FIELD_TYPES.LONG: + case ES_FIELD_TYPES.SHORT: + case ES_FIELD_TYPES.UNSIGNED_LONG: + case ES_FIELD_TYPES.BYTE: + return fetchNumericFieldStats( + esClient, + fieldStatsParams, + field, + termFilters + ); + + break; + case ES_FIELD_TYPES.BOOLEAN: + return fetchBooleanFieldStats( + esClient, + fieldStatsParams, + field, + termFilters + ); + + default: + return; + } + } + }) + .filter((f) => f !== undefined) as Array>; + + const batches = chunk(fieldStatsPromises, 10); + for (let i = 0; i < batches.length; i++) { + try { + const results = await Promise.allSettled(batches[i]); + results.forEach((r) => { + if (r.status === 'fulfilled' && r.value !== undefined) { + stats.push(r.value); + } + }); + } catch (e) { + errors.push(e); + } + } + + return { stats, errors }; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_keyword_field_stats.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_keyword_field_stats.ts new file mode 100644 index 00000000000000..b15449657cba54 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_keyword_field_stats.ts @@ -0,0 +1,87 @@ +/* + * 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 { ElasticsearchClient } from 'kibana/server'; +import { SearchRequest } from '@elastic/elasticsearch/api/types'; +import { estypes } from '@elastic/elasticsearch'; +import { FieldValuePair } from '../../../../../common/search_strategies/types'; +import { getQueryWithParams } from '../get_query_with_params'; +import { buildSamplerAggregation } from '../../utils/field_stats_utils'; +import { + FieldStatsCommonRequestParams, + KeywordFieldStats, + Aggs, + TopValueBucket, +} from '../../../../../common/search_strategies/field_stats_types'; + +export const getKeywordFieldStatsRequest = ( + params: FieldStatsCommonRequestParams, + fieldName: string, + termFilters?: FieldValuePair[] +): SearchRequest => { + const query = getQueryWithParams({ params, termFilters }); + + const { index, samplerShardSize } = params; + + const size = 0; + const aggs: Aggs = { + sampled_top: { + terms: { + field: fieldName, + size: 10, + order: { + _count: 'desc', + }, + }, + }, + }; + + const searchBody = { + query, + aggs: { + sample: buildSamplerAggregation(aggs, samplerShardSize), + }, + }; + + return { + index, + size, + body: searchBody, + }; +}; + +export const fetchKeywordFieldStats = async ( + esClient: ElasticsearchClient, + params: FieldStatsCommonRequestParams, + field: FieldValuePair, + termFilters?: FieldValuePair[] +): Promise => { + const request = getKeywordFieldStatsRequest( + params, + field.fieldName, + termFilters + ); + const { body } = await esClient.search(request); + const aggregations = body.aggregations as { + sample: { + sampled_top: estypes.AggregationsTermsAggregate; + }; + }; + const topValues: TopValueBucket[] = + aggregations?.sample.sampled_top?.buckets ?? []; + + const stats = { + fieldName: field.fieldName, + topValues, + topValuesSampleSize: topValues.reduce( + (acc, curr) => acc + curr.doc_count, + aggregations.sample.sampled_top?.sum_other_doc_count ?? 0 + ), + }; + + return stats; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_numeric_field_stats.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_numeric_field_stats.ts new file mode 100644 index 00000000000000..bab4a1af29b65e --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_numeric_field_stats.ts @@ -0,0 +1,130 @@ +/* + * 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 { ElasticsearchClient } from 'kibana/server'; +import { SearchRequest } from '@elastic/elasticsearch/api/types'; +import { find, get } from 'lodash'; +import { estypes } from '@elastic/elasticsearch/index'; +import { + NumericFieldStats, + FieldStatsCommonRequestParams, + TopValueBucket, + Aggs, +} from '../../../../../common/search_strategies/field_stats_types'; +import { FieldValuePair } from '../../../../../common/search_strategies/types'; +import { getQueryWithParams } from '../get_query_with_params'; +import { buildSamplerAggregation } from '../../utils/field_stats_utils'; + +// Only need 50th percentile for the median +const PERCENTILES = [50]; + +export const getNumericFieldStatsRequest = ( + params: FieldStatsCommonRequestParams, + fieldName: string, + termFilters?: FieldValuePair[] +) => { + const query = getQueryWithParams({ params, termFilters }); + const size = 0; + + const { index, samplerShardSize } = params; + + const percents = PERCENTILES; + const aggs: Aggs = { + sampled_field_stats: { + filter: { exists: { field: fieldName } }, + aggs: { + actual_stats: { + stats: { field: fieldName }, + }, + }, + }, + sampled_percentiles: { + percentiles: { + field: fieldName, + percents, + keyed: false, + }, + }, + sampled_top: { + terms: { + field: fieldName, + size: 10, + order: { + _count: 'desc', + }, + }, + }, + }; + + const searchBody = { + query, + aggs: { + sample: buildSamplerAggregation(aggs, samplerShardSize), + }, + }; + + return { + index, + size, + body: searchBody, + }; +}; + +export const fetchNumericFieldStats = async ( + esClient: ElasticsearchClient, + params: FieldStatsCommonRequestParams, + field: FieldValuePair, + termFilters?: FieldValuePair[] +): Promise => { + const request: SearchRequest = getNumericFieldStatsRequest( + params, + field.fieldName, + termFilters + ); + const { body } = await esClient.search(request); + + const aggregations = body.aggregations as { + sample: { + sampled_top: estypes.AggregationsTermsAggregate; + sampled_percentiles: estypes.AggregationsHdrPercentilesAggregate; + sampled_field_stats: { + doc_count: number; + actual_stats: estypes.AggregationsStatsAggregate; + }; + }; + }; + const docCount = aggregations?.sample.sampled_field_stats?.doc_count ?? 0; + const fieldStatsResp = + aggregations?.sample.sampled_field_stats?.actual_stats ?? {}; + const topValues = aggregations?.sample.sampled_top?.buckets ?? []; + + const stats: NumericFieldStats = { + fieldName: field.fieldName, + count: docCount, + min: get(fieldStatsResp, 'min', 0), + max: get(fieldStatsResp, 'max', 0), + avg: get(fieldStatsResp, 'avg', 0), + topValues, + topValuesSampleSize: topValues.reduce( + (acc: number, curr: TopValueBucket) => acc + curr.doc_count, + aggregations.sample.sampled_top?.sum_other_doc_count ?? 0 + ), + }; + + if (stats.count !== undefined && stats.count > 0) { + const percentiles = aggregations?.sample.sampled_percentiles.values ?? []; + const medianPercentile: { value: number; key: number } | undefined = find( + percentiles, + { + key: 50, + } + ); + stats.median = medianPercentile !== undefined ? medianPercentile!.value : 0; + } + + return stats; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/utils/field_stats_utils.ts b/x-pack/plugins/apm/server/lib/search_strategies/utils/field_stats_utils.ts new file mode 100644 index 00000000000000..2eb67ec501babd --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/utils/field_stats_utils.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { estypes } from '@elastic/elasticsearch'; +/* + * Contains utility functions for building and processing queries. + */ + +// Builds the base filter criteria used in queries, +// adding criteria for the time range and an optional query. +export function buildBaseFilterCriteria( + timeFieldName?: string, + earliestMs?: number, + latestMs?: number, + query?: object +) { + const filterCriteria = []; + if (timeFieldName && earliestMs && latestMs) { + filterCriteria.push({ + range: { + [timeFieldName]: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }); + } + + if (query) { + filterCriteria.push(query); + } + + return filterCriteria; +} + +// Wraps the supplied aggregations in a sampler aggregation. +// A supplied samplerShardSize (the shard_size parameter of the sampler aggregation) +// of less than 1 indicates no sampling, and the aggs are returned as-is. +export function buildSamplerAggregation( + aggs: any, + samplerShardSize: number +): estypes.AggregationsAggregationContainer { + if (samplerShardSize < 1) { + return aggs; + } + + return { + sampler: { + shard_size: samplerShardSize, + }, + aggs, + }; +} diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx index 38923784d862c8..4e0f1689bd4d1d 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx @@ -135,12 +135,8 @@ describe('Connectors', () => { } ); - expect(screen.getByText('Deprecated connector type')).toBeInTheDocument(); - expect( - screen.getByText( - 'This connector type is deprecated. Create a new connector or update this connector' - ) - ).toBeInTheDocument(); + expect(screen.getByText('This connector type is deprecated')).toBeInTheDocument(); + expect(screen.getByText('Update this connector, or create a new one.')).toBeInTheDocument(); }); test('it does not shows the deprecated callout when the connector is none', async () => { diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx index 34422392b7efa0..6f05f9f940d25c 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx @@ -190,17 +190,17 @@ describe('ConnectorsDropdown', () => { > My Connector + (deprecated)
- @@ -293,7 +293,9 @@ describe('ConnectorsDropdown', () => { wrapper: ({ children }) => {children}, }); - const tooltips = screen.getAllByLabelText('Deprecated connector'); + const tooltips = screen.getAllByLabelText( + 'This connector is deprecated. Update it, or create a new one.' + ); expect(tooltips[0]).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx index f21b3ab3d544f3..c5fe9c7470745e 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx @@ -14,6 +14,7 @@ import { ActionConnector } from '../../containers/configure/types'; import * as i18n from './translations'; import { useKibana } from '../../common/lib/kibana'; import { getConnectorIcon, isLegacyConnector } from '../utils'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; export interface Props { connectors: ActionConnector[]; @@ -57,6 +58,11 @@ const addNewConnector = { 'data-test-subj': 'dropdown-connector-add-connector', }; +const StyledEuiIconTip = euiStyled(EuiIconTip)` + margin-left: ${({ theme }) => theme.eui.euiSizeS} + margin-bottom: 0 !important; +`; + const ConnectorsDropdownComponent: React.FC = ({ connectors, disabled, @@ -87,16 +93,18 @@ const ConnectorsDropdownComponent: React.FC = ({ /> - {connector.name} + + {connector.name} + {isLegacyConnector(connector) && ` (${i18n.DEPRECATED_TOOLTIP_TEXT})`} + {isLegacyConnector(connector) && ( - diff --git a/x-pack/plugins/cases/public/components/configure_cases/translations.ts b/x-pack/plugins/cases/public/components/configure_cases/translations.ts index 4a775c78d4ab87..26b45a8c3a2506 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/configure_cases/translations.ts @@ -163,16 +163,16 @@ export const UPDATE_SELECTED_CONNECTOR = (connectorName: string): string => defaultMessage: 'Update { connectorName }', }); -export const DEPRECATED_TOOLTIP_TITLE = i18n.translate( - 'xpack.cases.configureCases.deprecatedTooltipTitle', +export const DEPRECATED_TOOLTIP_TEXT = i18n.translate( + 'xpack.cases.configureCases.deprecatedTooltipText', { - defaultMessage: 'Deprecated connector', + defaultMessage: 'deprecated', } ); export const DEPRECATED_TOOLTIP_CONTENT = i18n.translate( 'xpack.cases.configureCases.deprecatedTooltipContent', { - defaultMessage: 'Please update your connector', + defaultMessage: 'This connector is deprecated. Update it, or create a new one.', } ); diff --git a/x-pack/plugins/cases/public/components/connectors/card.tsx b/x-pack/plugins/cases/public/components/connectors/card.tsx index 86cd90dafb3763..ec4b52c54f707d 100644 --- a/x-pack/plugins/cases/public/components/connectors/card.tsx +++ b/x-pack/plugins/cases/public/components/connectors/card.tsx @@ -6,7 +6,7 @@ */ import React, { memo, useMemo } from 'react'; -import { EuiCard, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; import styled from 'styled-components'; import { ConnectorTypes } from '../../../common'; @@ -59,16 +59,20 @@ const ConnectorCardDisplay: React.FC = ({ <> {isLoading && } {!isLoading && ( - + + + + + {icon} + )} ); diff --git a/x-pack/plugins/cases/public/components/connectors/deprecated_callout.test.tsx b/x-pack/plugins/cases/public/components/connectors/deprecated_callout.test.tsx index 6b1475e3c4bd01..367609df3c887c 100644 --- a/x-pack/plugins/cases/public/components/connectors/deprecated_callout.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/deprecated_callout.test.tsx @@ -12,12 +12,8 @@ import { DeprecatedCallout } from './deprecated_callout'; describe('DeprecatedCallout', () => { test('it renders correctly', () => { render(); - expect(screen.getByText('Deprecated connector type')).toBeInTheDocument(); - expect( - screen.getByText( - 'This connector type is deprecated. Create a new connector or update this connector' - ) - ).toBeInTheDocument(); + expect(screen.getByText('This connector type is deprecated')).toBeInTheDocument(); + expect(screen.getByText('Update this connector, or create a new one.')).toBeInTheDocument(); expect(screen.getByTestId('legacy-connector-warning-callout')).toHaveClass( 'euiCallOut euiCallOut--warning' ); diff --git a/x-pack/plugins/cases/public/components/connectors/deprecated_callout.tsx b/x-pack/plugins/cases/public/components/connectors/deprecated_callout.tsx index 937f8406e218ad..9337f2843506b2 100644 --- a/x-pack/plugins/cases/public/components/connectors/deprecated_callout.tsx +++ b/x-pack/plugins/cases/public/components/connectors/deprecated_callout.tsx @@ -12,15 +12,14 @@ import { i18n } from '@kbn/i18n'; const LEGACY_CONNECTOR_WARNING_TITLE = i18n.translate( 'xpack.cases.connectors.serviceNow.legacyConnectorWarningTitle', { - defaultMessage: 'Deprecated connector type', + defaultMessage: 'This connector type is deprecated', } ); const LEGACY_CONNECTOR_WARNING_DESC = i18n.translate( 'xpack.cases.connectors.serviceNow.legacyConnectorWarningDesc', { - defaultMessage: - 'This connector type is deprecated. Create a new connector or update this connector', + defaultMessage: 'Update this connector, or create a new one.', } ); diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx index 096e450c736c18..e24b25065a1c8e 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx @@ -157,7 +157,7 @@ const ServiceNowITSMFieldsComponent: React.FunctionComponent< {showConnectorWarning && ( - + )} diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx index a7b8aa7b27df51..d502b7382664b6 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx @@ -173,7 +173,7 @@ const ServiceNowSIRFieldsComponent: React.FunctionComponent< {showConnectorWarning && ( - + )} diff --git a/x-pack/plugins/data_visualizer/common/constants.ts b/x-pack/plugins/data_visualizer/common/constants.ts index 5a3a1d8f2e5bf1..cc661ca6ffeffe 100644 --- a/x-pack/plugins/data_visualizer/common/constants.ts +++ b/x-pack/plugins/data_visualizer/common/constants.ts @@ -46,7 +46,4 @@ export const applicationPath = `/app/home#/tutorial_directory/${FILE_DATA_VIS_TA export const featureTitle = i18n.translate('xpack.dataVisualizer.title', { defaultMessage: 'Upload a file', }); -export const featureDescription = i18n.translate('xpack.dataVisualizer.description', { - defaultMessage: 'Import your own CSV, NDJSON, or log file.', -}); export const featureId = `file_data_visualizer`; diff --git a/x-pack/plugins/data_visualizer/public/register_home.ts b/x-pack/plugins/data_visualizer/public/register_home.ts index 4f4601ae769777..9338c93000ec96 100644 --- a/x-pack/plugins/data_visualizer/public/register_home.ts +++ b/x-pack/plugins/data_visualizer/public/register_home.ts @@ -9,13 +9,7 @@ import { i18n } from '@kbn/i18n'; import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; import { FileDataVisualizerWrapper } from './lazy_load_bundle/component_wrapper'; -import { - featureDescription, - featureTitle, - FILE_DATA_VIS_TAB_ID, - applicationPath, - featureId, -} from '../common'; +import { featureTitle, FILE_DATA_VIS_TAB_ID, applicationPath, featureId } from '../common'; export function registerHomeAddData(home: HomePublicPluginSetup) { home.addData.registerAddDataTab({ @@ -31,7 +25,9 @@ export function registerHomeFeatureCatalogue(home: HomePublicPluginSetup) { home.featureCatalogue.register({ id: featureId, title: featureTitle, - description: featureDescription, + description: i18n.translate('xpack.dataVisualizer.description', { + defaultMessage: 'Import your own CSV, NDJSON, or log file.', + }), icon: 'document', path: applicationPath, showOnHomePage: true, diff --git a/x-pack/plugins/data_visualizer/server/register_custom_integration.ts b/x-pack/plugins/data_visualizer/server/register_custom_integration.ts index 86aa3cd96d6130..67be78277189b6 100644 --- a/x-pack/plugins/data_visualizer/server/register_custom_integration.ts +++ b/x-pack/plugins/data_visualizer/server/register_custom_integration.ts @@ -5,14 +5,18 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; import { CustomIntegrationsPluginSetup } from '../../../../src/plugins/custom_integrations/server'; -import { applicationPath, featureDescription, featureId, featureTitle } from '../common'; +import { applicationPath, featureId, featureTitle } from '../common'; export function registerWithCustomIntegrations(customIntegrations: CustomIntegrationsPluginSetup) { customIntegrations.registerCustomIntegration({ id: featureId, title: featureTitle, - description: featureDescription, + description: i18n.translate('xpack.dataVisualizer.customIntegrationsDescription', { + defaultMessage: + 'Upload data from a CSV, TSV, JSON or other log file to Elasticsearch for analysis.', + }), uiInternalPath: applicationPath, isBeta: false, icons: [ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx index 2cee5bbbec80b0..944d8315452b0b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx @@ -8,6 +8,7 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../../__mocks__/kea_logic'; import { mockUseParams } from '../../../../__mocks__/react_router'; + import '../../../__mocks__/engine_logic.mock'; import React from 'react'; @@ -27,6 +28,7 @@ import { CurationLogic } from './curation_logic'; import { DeleteCurationButton } from './delete_curation_button'; import { PromotedDocuments, OrganicDocuments } from './documents'; +import { History } from './history'; describe('AutomatedCuration', () => { const values = { @@ -39,6 +41,7 @@ describe('AutomatedCuration', () => { suggestion: { status: 'applied', }, + queries: ['foo'], }, activeQuery: 'query A', isAutomated: true, @@ -61,20 +64,46 @@ describe('AutomatedCuration', () => { expect(wrapper.is(AppSearchPageTemplate)); expect(wrapper.find(PromotedDocuments)).toHaveLength(1); expect(wrapper.find(OrganicDocuments)).toHaveLength(1); + expect(wrapper.find(History)).toHaveLength(0); }); - it('includes a static tab group', () => { + it('includes tabs', () => { const wrapper = shallow(); - const tabs = getPageHeaderTabs(wrapper).find(EuiTab); + let tabs = getPageHeaderTabs(wrapper).find(EuiTab); - expect(tabs).toHaveLength(2); + expect(tabs).toHaveLength(3); - expect(tabs.at(0).prop('onClick')).toBeUndefined(); expect(tabs.at(0).prop('isSelected')).toBe(true); expect(tabs.at(1).prop('onClick')).toBeUndefined(); expect(tabs.at(1).prop('isSelected')).toBe(false); expect(tabs.at(1).prop('disabled')).toBe(true); + + expect(tabs.at(2).prop('isSelected')).toBe(false); + + // Clicking on the History tab shows the history view + tabs.at(2).simulate('click'); + + tabs = getPageHeaderTabs(wrapper).find(EuiTab); + + expect(tabs.at(0).prop('isSelected')).toBe(false); + expect(tabs.at(2).prop('isSelected')).toBe(true); + + expect(wrapper.find(PromotedDocuments)).toHaveLength(0); + expect(wrapper.find(OrganicDocuments)).toHaveLength(0); + expect(wrapper.find(History)).toHaveLength(1); + + // Clicking back to the Promoted tab shows promoted documents + tabs.at(0).simulate('click'); + + tabs = getPageHeaderTabs(wrapper).find(EuiTab); + + expect(tabs.at(0).prop('isSelected')).toBe(true); + expect(tabs.at(2).prop('isSelected')).toBe(false); + + expect(wrapper.find(PromotedDocuments)).toHaveLength(1); + expect(wrapper.find(OrganicDocuments)).toHaveLength(1); + expect(wrapper.find(History)).toHaveLength(0); }); it('initializes CurationLogic with a curationId prop from URL param', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx index fa34fa071b8552..276b40ba886776 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx @@ -5,15 +5,18 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; import { useParams } from 'react-router-dom'; import { useValues, useActions } from 'kea'; import { EuiButton, EuiBadge, EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EngineLogic } from '../../engine'; import { AppSearchPageTemplate } from '../../layout'; import { AutomatedIcon } from '../components/automated_icon'; + import { AUTOMATED_LABEL, COVERT_TO_MANUAL_BUTTON_LABEL, @@ -26,19 +29,25 @@ import { HIDDEN_DOCUMENTS_TITLE, PROMOTED_DOCUMENTS_TITLE } from './constants'; import { CurationLogic } from './curation_logic'; import { DeleteCurationButton } from './delete_curation_button'; import { PromotedDocuments, OrganicDocuments } from './documents'; +import { History } from './history'; + +const PROMOTED = 'promoted'; +const HISTORY = 'history'; export const AutomatedCuration: React.FC = () => { const { curationId } = useParams<{ curationId: string }>(); const logic = CurationLogic({ curationId }); const { convertToManual } = useActions(logic); const { activeQuery, dataLoading, queries, curation } = useValues(logic); + const { engineName } = useValues(EngineLogic); + const [selectedPageTab, setSelectedPageTab] = useState(PROMOTED); - // This tab group is meant to visually mirror the dynamic group of tags in the ManualCuration component const pageTabs = [ { label: PROMOTED_DOCUMENTS_TITLE, append: {curation.promoted.length}, - isSelected: true, + isSelected: selectedPageTab === PROMOTED, + onClick: () => setSelectedPageTab(PROMOTED), }, { label: HIDDEN_DOCUMENTS_TITLE, @@ -46,6 +55,16 @@ export const AutomatedCuration: React.FC = () => { isSelected: false, disabled: true, }, + { + label: i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curation.detail.historyButtonLabel', + { + defaultMessage: 'History', + } + ), + isSelected: selectedPageTab === HISTORY, + onClick: () => setSelectedPageTab(HISTORY), + }, ]; return ( @@ -83,8 +102,11 @@ export const AutomatedCuration: React.FC = () => { }} isLoading={dataLoading} > - - + {selectedPageTab === PROMOTED && } + {selectedPageTab === PROMOTED && } + {selectedPageTab === HISTORY && ( + + )} ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.test.tsx new file mode 100644 index 00000000000000..a7f83fb0c61d94 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.test.tsx @@ -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 React from 'react'; + +import { shallow } from 'enzyme'; + +import { EntSearchLogStream } from '../../../../shared/log_stream'; + +import { History } from './history'; + +describe('History', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(EntSearchLogStream).prop('query')).toEqual( + 'appsearch.search_relevance_suggestions.query: some text and event.kind: event and event.dataset: search-relevance-suggestions and appsearch.search_relevance_suggestions.engine: foo and event.action: curation_suggestion' + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.tsx new file mode 100644 index 00000000000000..744141372469c3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { EntSearchLogStream } from '../../../../shared/log_stream'; +import { DataPanel } from '../../data_panel'; + +interface Props { + query: string; + engineName: string; +} + +export const History: React.FC = ({ query, engineName }) => { + const filters = [ + `appsearch.search_relevance_suggestions.query: ${query}`, + 'event.kind: event', + 'event.dataset: search-relevance-suggestions', + `appsearch.search_relevance_suggestions.engine: ${engineName}`, + 'event.action: curation_suggestion', + ]; + + return ( + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curation.detail.historyTableTitle', + { + defaultMessage: 'Automated curation changes', + } + )} + + } + subtitle={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curation.detail.historyTableDescription', + { + defaultMessage: 'A detailed log of recent changes to your automated curation.', + } + )} + hasBorder + > + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_logic.test.ts new file mode 100644 index 00000000000000..83a200943256b2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_logic.test.ts @@ -0,0 +1,215 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + LogicMounter, + mockFlashMessageHelpers, + mockHttpValues, +} from '../../../../../../../__mocks__/kea_logic'; +import '../../../../../../__mocks__/engine_logic.mock'; + +// I don't know why eslint is saying this line is out of order +// eslint-disable-next-line import/order +import { nextTick } from '@kbn/test/jest'; + +import { DEFAULT_META } from '../../../../../../../shared/constants'; + +import { IgnoredQueriesLogic } from './ignored_queries_logic'; + +const DEFAULT_VALUES = { + dataLoading: true, + ignoredQueries: [], + meta: { + ...DEFAULT_META, + page: { + ...DEFAULT_META.page, + size: 10, + }, + }, +}; + +describe('IgnoredQueriesLogic', () => { + const { mount } = new LogicMounter(IgnoredQueriesLogic); + const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; + const { http } = mockHttpValues; + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has expected default values', () => { + expect(IgnoredQueriesLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('onIgnoredQueriesLoad', () => { + it('should set queries, meta state, & dataLoading to false', () => { + IgnoredQueriesLogic.actions.onIgnoredQueriesLoad(['first query', 'second query'], { + page: { + current: 1, + size: 10, + total_results: 1, + total_pages: 1, + }, + }); + + expect(IgnoredQueriesLogic.values).toEqual({ + ...DEFAULT_VALUES, + ignoredQueries: ['first query', 'second query'], + meta: { + page: { + current: 1, + size: 10, + total_results: 1, + total_pages: 1, + }, + }, + dataLoading: false, + }); + }); + }); + + describe('onPaginate', () => { + it('should update meta', () => { + IgnoredQueriesLogic.actions.onPaginate(2); + + expect(IgnoredQueriesLogic.values).toEqual({ + ...DEFAULT_VALUES, + meta: { + ...DEFAULT_META, + page: { + ...DEFAULT_META.page, + current: 2, + }, + }, + }); + }); + }); + }); + + describe('listeners', () => { + describe('loadIgnoredQueries', () => { + it('should make an API call and set suggestions & meta state', async () => { + http.post.mockReturnValueOnce( + Promise.resolve({ + results: [{ query: 'first query' }, { query: 'second query' }], + meta: { + page: { + current: 1, + size: 10, + total_results: 1, + total_pages: 1, + }, + }, + }) + ); + jest.spyOn(IgnoredQueriesLogic.actions, 'onIgnoredQueriesLoad'); + + IgnoredQueriesLogic.actions.loadIgnoredQueries(); + await nextTick(); + + expect(http.post).toHaveBeenCalledWith( + '/internal/app_search/engines/some-engine/search_relevance_suggestions', + { + body: JSON.stringify({ + page: { + current: 1, + size: 10, + }, + filters: { + status: ['disabled'], + type: 'curation', + }, + }), + } + ); + + expect(IgnoredQueriesLogic.actions.onIgnoredQueriesLoad).toHaveBeenCalledWith( + ['first query', 'second query'], + { + page: { + current: 1, + size: 10, + total_results: 1, + total_pages: 1, + }, + } + ); + }); + + it('handles errors', async () => { + http.post.mockReturnValueOnce(Promise.reject('error')); + + IgnoredQueriesLogic.actions.loadIgnoredQueries(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + + describe('allowIgnoredQuery', () => { + it('will make an http call to reject the suggestion for the query', async () => { + http.put.mockReturnValueOnce( + Promise.resolve({ + results: [ + { + query: 'test query', + type: 'curation', + status: 'rejected', + }, + ], + }) + ); + + IgnoredQueriesLogic.actions.allowIgnoredQuery('test query'); + await nextTick(); + + expect(http.put).toHaveBeenCalledWith( + '/internal/app_search/engines/some-engine/search_relevance_suggestions', + { + body: JSON.stringify([ + { + query: 'test query', + type: 'curation', + status: 'rejected', + }, + ]), + } + ); + + expect(flashSuccessToast).toHaveBeenCalledWith(expect.any(String)); + }); + + it('handles errors', async () => { + http.put.mockReturnValueOnce(Promise.reject('error')); + + IgnoredQueriesLogic.actions.allowIgnoredQuery('test query'); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + + it('handles inline errors', async () => { + http.put.mockReturnValueOnce( + Promise.resolve({ + results: [ + { + error: 'error', + }, + ], + }) + ); + + IgnoredQueriesLogic.actions.allowIgnoredQuery('test query'); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_logic.ts new file mode 100644 index 00000000000000..e36b5bc156b468 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_logic.ts @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { i18n } from '@kbn/i18n'; + +import { Meta } from '../../../../../../../../../common/types'; +import { DEFAULT_META } from '../../../../../../../shared/constants'; +import { flashAPIErrors, flashSuccessToast } from '../../../../../../../shared/flash_messages'; +import { HttpLogic } from '../../../../../../../shared/http'; +import { updateMetaPageIndex } from '../../../../../../../shared/table_pagination'; +import { EngineLogic } from '../../../../../engine'; +import { CurationSuggestion } from '../../../../types'; + +interface IgnoredQueriesValues { + dataLoading: boolean; + ignoredQueries: string[]; + meta: Meta; +} + +interface IgnoredQueriesActions { + allowIgnoredQuery(ignoredQuery: string): { + ignoredQuery: string; + }; + loadIgnoredQueries(): void; + onIgnoredQueriesLoad( + ignoredQueries: string[], + meta: Meta + ): { ignoredQueries: string[]; meta: Meta }; + onPaginate(newPageIndex: number): { newPageIndex: number }; +} + +interface SuggestionUpdateError { + error: string; +} + +const ALLOW_SUCCESS_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.curations.ignoredSuggestionsPanel.allowQuerySuccessMessage', + { + defaultMessage: 'You’ll be notified about future suggestions for this query', + } +); + +export const IgnoredQueriesLogic = kea>({ + path: ['enterprise_search', 'app_search', 'curations', 'ignored_queries_panel_logic'], + actions: () => ({ + allowIgnoredQuery: (ignoredQuery) => ({ ignoredQuery }), + loadIgnoredQueries: true, + onIgnoredQueriesLoad: (ignoredQueries, meta) => ({ ignoredQueries, meta }), + onPaginate: (newPageIndex) => ({ newPageIndex }), + }), + reducers: () => ({ + dataLoading: [ + true, + { + onIgnoredQueriesLoad: () => false, + }, + ], + ignoredQueries: [ + [], + { + onIgnoredQueriesLoad: (_, { ignoredQueries }) => ignoredQueries, + }, + ], + meta: [ + { + ...DEFAULT_META, + page: { + ...DEFAULT_META.page, + size: 10, + }, + }, + { + onIgnoredQueriesLoad: (_, { meta }) => meta, + onPaginate: (state, { newPageIndex }) => updateMetaPageIndex(state, newPageIndex), + }, + ], + }), + listeners: ({ actions, values }) => ({ + loadIgnoredQueries: async () => { + const { meta } = values; + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + const response: { results: CurationSuggestion[]; meta: Meta } = await http.post( + `/internal/app_search/engines/${engineName}/search_relevance_suggestions`, + { + body: JSON.stringify({ + page: { + current: meta.page.current, + size: meta.page.size, + }, + filters: { + status: ['disabled'], + type: 'curation', + }, + }), + } + ); + + const queries = response.results.map((suggestion) => suggestion.query); + actions.onIgnoredQueriesLoad(queries, response.meta); + } catch (e) { + flashAPIErrors(e); + } + }, + allowIgnoredQuery: async ({ ignoredQuery }) => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + try { + const response = await http.put<{ + results: Array; + }>(`/internal/app_search/engines/${engineName}/search_relevance_suggestions`, { + body: JSON.stringify([ + { + query: ignoredQuery, + type: 'curation', + status: 'rejected', + }, + ]), + }); + + if (response.results[0].hasOwnProperty('error')) { + throw (response.results[0] as SuggestionUpdateError).error; + } + + flashSuccessToast(ALLOW_SUCCESS_MESSAGE); + // re-loading to update the current page rather than manually remove the query + actions.loadIgnoredQueries(); + } catch (e) { + flashAPIErrors(e); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_panel.test.tsx new file mode 100644 index 00000000000000..919e1e8706c945 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_panel.test.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../../../../../__mocks__/shallow_useeffect.mock'; +// I don't know why eslint is saying this line is out of order +// eslint-disable-next-line import/order +import { setMockActions, setMockValues } from '../../../../../../../__mocks__/kea_logic'; +import '../../../../../../__mocks__/engine_logic.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiBasicTable } from '@elastic/eui'; + +import { IgnoredQueriesPanel } from './ignored_queries_panel'; + +describe('IgnoredQueriesPanel', () => { + const values = { + dataLoading: false, + suggestions: [ + { + query: 'foo', + updated_at: '2021-07-08T14:35:50Z', + promoted: ['1', '2'], + }, + ], + meta: { + page: { + current: 1, + size: 10, + total_results: 2, + }, + }, + }; + + const mockActions = { + allowIgnoredQuery: jest.fn(), + loadIgnoredQueries: jest.fn(), + onPaginate: jest.fn(), + }; + + beforeAll(() => { + setMockValues(values); + setMockActions(mockActions); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const getColumn = (index: number) => { + const wrapper = shallow(); + const table = wrapper.find(EuiBasicTable); + const columns = table.prop('columns'); + return columns[index]; + }; + + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiBasicTable).exists()).toBe(true); + }); + + it('show a query', () => { + const column = getColumn(0).render('test query'); + expect(column).toEqual('test query'); + }); + + it('has an allow action', () => { + const column = getColumn(1); + // @ts-ignore + const actions = column.actions; + actions[0].onClick('test query'); + expect(mockActions.allowIgnoredQuery).toHaveBeenCalledWith('test query'); + }); + + it('fetches data on load', () => { + shallow(); + + expect(mockActions.loadIgnoredQueries).toHaveBeenCalled(); + }); + + it('supports pagination', () => { + const wrapper = shallow(); + wrapper.find(EuiBasicTable).simulate('change', { page: { index: 0 } }); + + expect(mockActions.onPaginate).toHaveBeenCalledWith(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_panel.tsx new file mode 100644 index 00000000000000..f7cc192932332b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_panel.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { + convertMetaToPagination, + handlePageChange, +} from '../../../../../../../shared/table_pagination'; + +import { DataPanel } from '../../../../../data_panel'; + +import { IgnoredQueriesLogic } from './ignored_queries_logic'; + +export const IgnoredQueriesPanel: React.FC = () => { + const { dataLoading, ignoredQueries, meta } = useValues(IgnoredQueriesLogic); + const { allowIgnoredQuery, loadIgnoredQueries, onPaginate } = useActions(IgnoredQueriesLogic); + + useEffect(() => { + loadIgnoredQueries(); + }, [meta.page.current]); + + const columns: Array> = [ + { + render: (query: string) => query, + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.curations.ignoredSuggestionsPanel.queryColumnName', + { + defaultMessage: 'Query', + } + ), + }, + { + actions: [ + { + type: 'button', + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.curations.ignoredSuggestions.allowButtonLabel', + { + defaultMessage: 'Allow', + } + ), + description: i18n.translate( + 'xpack.enterpriseSearch.appSearch.curations.ignoredSuggestions.allowButtonDescription', + { + defaultMessage: 'Enable suggestions for this query', + } + ), + onClick: (query) => allowIgnoredQuery(query), + color: 'primary', + }, + ], + }, + ]; + + return ( + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.curations.ignoredSuggestionsPanel.title', + { + defaultMessage: 'Ignored queries', + } + )} + + } + subtitle={ + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.curations.ignoredSuggestionsPanel.description', + { + defaultMessage: 'You won’t be notified about suggestions for these queries', + } + )} + + } + iconType="eyeClosed" + hasBorder + > + + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/index.ts similarity index 66% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/index.ts index 9d5fafbf5a0ea2..f4cb73919f42f0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export const UPDATE_INCIDENT_VARIABLE = '{{rule.id}}'; -export const NOT_UPDATE_INCIDENT_VARIABLE = '{{rule.id}}:{{alert.id}}'; +export { IgnoredQueriesPanel } from './ignored_queries_panel'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_suggestions_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_suggestions_panel.test.tsx deleted file mode 100644 index b09981748f19cb..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_suggestions_panel.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { EuiBasicTable } from '@elastic/eui'; - -import { DataPanel } from '../../../../data_panel'; - -import { IgnoredSuggestionsPanel } from './ignored_suggestions_panel'; - -describe('IgnoredSuggestionsPanel', () => { - it('renders', () => { - const wrapper = shallow(); - - expect(wrapper.is(DataPanel)).toBe(true); - expect(wrapper.find(EuiBasicTable)).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_suggestions_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_suggestions_panel.tsx deleted file mode 100644 index f2fdfd55a7e5a1..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_suggestions_panel.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { CustomItemAction, EuiBasicTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; - -import { DataPanel } from '../../../../data_panel'; -import { CurationSuggestion } from '../../../types'; - -export const IgnoredSuggestionsPanel: React.FC = () => { - const ignoredSuggestions: CurationSuggestion[] = []; - - const allowSuggestion = (query: string) => alert(query); - - const actions: Array> = [ - { - render: (item: CurationSuggestion) => { - return ( - allowSuggestion(item.query)} color="primary"> - Allow - - ); - }, - }, - ]; - - const columns: Array> = [ - { - field: 'query', - name: 'Query', - sortable: true, - }, - { - actions, - }, - ]; - - return ( - Ignored queries} - subtitle={You won’t be notified about suggestions for these queries} - iconType="eyeClosed" - hasBorder - > - - - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/index.ts index 2e16d9bde8550b..43651e613364eb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/index.ts @@ -6,5 +6,5 @@ */ export { CurationChangesPanel } from './curation_changes_panel'; -export { IgnoredSuggestionsPanel } from './ignored_suggestions_panel'; +export { IgnoredQueriesPanel } from './ignored_queries_panel'; export { RejectedCurationsPanel } from './rejected_curations_panel'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/curations_history.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/curations_history.test.tsx index 1ebd4da694d54d..407454922ef053 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/curations_history.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/curations_history.test.tsx @@ -9,11 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { - CurationChangesPanel, - IgnoredSuggestionsPanel, - RejectedCurationsPanel, -} from './components'; +import { CurationChangesPanel, IgnoredQueriesPanel, RejectedCurationsPanel } from './components'; import { CurationsHistory } from './curations_history'; describe('CurationsHistory', () => { @@ -22,6 +18,6 @@ describe('CurationsHistory', () => { expect(wrapper.find(CurationChangesPanel)).toHaveLength(1); expect(wrapper.find(RejectedCurationsPanel)).toHaveLength(1); - expect(wrapper.find(IgnoredSuggestionsPanel)).toHaveLength(1); + expect(wrapper.find(IgnoredQueriesPanel)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/curations_history.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/curations_history.tsx index 6db62820b1cdb5..5f857087e05ef1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/curations_history.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/curations_history.tsx @@ -9,11 +9,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { - CurationChangesPanel, - IgnoredSuggestionsPanel, - RejectedCurationsPanel, -} from './components'; +import { CurationChangesPanel, IgnoredQueriesPanel, RejectedCurationsPanel } from './components'; export const CurationsHistory: React.FC = () => { return ( @@ -29,7 +25,7 @@ export const CurationsHistory: React.FC = () => {
- + ); diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index 131cc276fc0733..734d578687bcd4 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -15,6 +15,10 @@ export const FLEET_SERVER_PACKAGE = 'fleet_server'; export const FLEET_ENDPOINT_PACKAGE = 'endpoint'; export const FLEET_APM_PACKAGE = 'apm'; export const FLEET_SYNTHETICS_PACKAGE = 'synthetics'; +export const FLEET_KUBERNETES_PACKAGE = 'kubernetes'; +export const KUBERNETES_RUN_INSTRUCTIONS = + 'kubectl apply -f elastic-agent-standalone-kubernetes.yaml'; +export const STANDALONE_RUN_INSTRUCTIONS = './elastic-agent install'; /* Package rules: diff --git a/x-pack/plugins/fleet/common/services/agent_cm_to_yaml.ts b/x-pack/plugins/fleet/common/services/agent_cm_to_yaml.ts new file mode 100644 index 00000000000000..5987110d7752fb --- /dev/null +++ b/x-pack/plugins/fleet/common/services/agent_cm_to_yaml.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { safeDump } from 'js-yaml'; + +import type { FullAgentConfigMap } from '../types/models/agent_cm'; + +const CM_KEYS_ORDER = ['apiVersion', 'kind', 'metadata', 'data']; + +export const fullAgentConfigMapToYaml = ( + policy: FullAgentConfigMap, + toYaml: typeof safeDump +): string => { + return toYaml(policy, { + skipInvalid: true, + sortKeys: (keyA: string, keyB: string) => { + const indexA = CM_KEYS_ORDER.indexOf(keyA); + const indexB = CM_KEYS_ORDER.indexOf(keyB); + if (indexA >= 0 && indexB < 0) { + return -1; + } + + if (indexA < 0 && indexB >= 0) { + return 1; + } + + return indexA - indexB; + }, + }); +}; diff --git a/x-pack/plugins/fleet/common/types/models/agent_cm.ts b/x-pack/plugins/fleet/common/types/models/agent_cm.ts new file mode 100644 index 00000000000000..bd8200c96ad88b --- /dev/null +++ b/x-pack/plugins/fleet/common/types/models/agent_cm.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FullAgentPolicy } from './agent_policy'; + +export interface FullAgentConfigMap { + apiVersion: string; + kind: string; + metadata: Metadata; + data: AgentYML; +} + +interface Metadata { + name: string; + namespace: string; + labels: Labels; +} + +interface Labels { + 'k8s-app': string; +} + +interface AgentYML { + 'agent.yml': FullAgentPolicy; +} diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts index 927368694693a6..0975b1e28fb8bf 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts @@ -78,3 +78,7 @@ export interface GetFullAgentPolicyRequest { export interface GetFullAgentPolicyResponse { item: FullAgentPolicy; } + +export interface GetFullAgentConfigMapResponse { + item: string; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/index.ts index 14eca1406f6238..c8b3a21f46ebd6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/index.ts @@ -8,3 +8,4 @@ export { CreatePackagePolicyPageLayout } from './layout'; export { PackagePolicyInputPanel } from './package_policy_input_panel'; export { PackagePolicyInputVarField } from './package_policy_input_var_field'; +export { PostInstallAddAgentModal } from './post_install_add_agent_modal'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/post_install_add_agent_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/post_install_add_agent_modal.tsx new file mode 100644 index 00000000000000..c91b6d348180d2 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/post_install_add_agent_modal.tsx @@ -0,0 +1,59 @@ +/* + * 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 { EuiConfirmModal } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import type { AgentPolicy, PackageInfo } from '../../../../types'; + +const toTitleCase = (str: string) => str.charAt(0).toUpperCase() + str.substr(1); + +export const PostInstallAddAgentModal: React.FunctionComponent<{ + onConfirm: () => void; + onCancel: () => void; + packageInfo: PackageInfo; + agentPolicy: AgentPolicy; +}> = ({ onConfirm, onCancel, packageInfo, agentPolicy }) => { + return ( + + } + onCancel={onCancel} + onConfirm={onConfirm} + cancelButtonText={ + + } + confirmButtonText={ + + } + buttonColor="primary" + data-test-subj="postInstallAddAgentModal" + > + Elastic Agent, + }} + /> + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index ffc9cba90efeaa..f6ad41f69e99ee 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -19,15 +19,19 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, - EuiLink, EuiErrorBoundary, } from '@elastic/eui'; import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; import type { ApplicationStart } from 'kibana/public'; import { safeLoad } from 'js-yaml'; -import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; -import type { AgentPolicy, NewPackagePolicy, CreatePackagePolicyRouteState } from '../../../types'; +import type { + AgentPolicy, + NewPackagePolicy, + PackagePolicy, + CreatePackagePolicyRouteState, + OnSaveQueryParamKeys, +} from '../../../types'; import { useLink, useBreadcrumbs, @@ -45,10 +49,11 @@ import type { PackagePolicyEditExtensionComponentProps } from '../../../types'; import { PLUGIN_ID } from '../../../../../../common/constants'; import { pkgKeyFromPackageInfo } from '../../../services'; -import { CreatePackagePolicyPageLayout } from './components'; +import { CreatePackagePolicyPageLayout, PostInstallAddAgentModal } from './components'; import type { EditPackagePolicyFrom, PackagePolicyFormState } from './types'; import type { PackagePolicyValidationResults } from './services'; import { validatePackagePolicy, validationHasErrors } from './services'; +import { appendOnSaveQueryParamsToPath } from './utils'; import { StepSelectAgentPolicy } from './step_select_agent_policy'; import { StepConfigurePackagePolicy } from './step_configure_package'; import { StepDefinePackagePolicy } from './step_define_package_policy'; @@ -105,6 +110,9 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { // Agent policy state const [agentPolicy, setAgentPolicy] = useState(); + // only used to store the resulting package policy once saved + const [savedPackagePolicy, setSavedPackagePolicy] = useState(); + // Retrieve agent count const agentPolicyId = agentPolicy?.id; useEffect(() => { @@ -256,9 +264,9 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { const savePackagePolicy = useCallback(async () => { setFormState('LOADING'); const result = await sendCreatePackagePolicy(packagePolicy); - setFormState('SUBMITTED'); + setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_NO_AGENTS'); return result; - }, [packagePolicy]); + }, [packagePolicy, agentCount]); const doOnSaveNavigation = useRef(true); // Detect if user left page @@ -268,6 +276,39 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { }; }, []); + const navigateAddAgent = (policy?: PackagePolicy) => + onSaveNavigate(policy, ['openEnrollmentFlyout']); + + const navigateAddAgentHelp = (policy?: PackagePolicy) => + onSaveNavigate(policy, ['showAddAgentHelp']); + + const onSaveNavigate = useCallback( + (policy?: PackagePolicy, paramsToApply: OnSaveQueryParamKeys[] = []) => { + if (!doOnSaveNavigation.current) { + return; + } + + if (routeState?.onSaveNavigateTo && policy) { + const [appId, options] = routeState.onSaveNavigateTo; + + if (options?.path) { + const pathWithQueryString = appendOnSaveQueryParamsToPath({ + path: options.path, + policy, + mappingOptions: routeState.onSaveQueryParams, + paramsToApply, + }); + handleNavigateTo([appId, { ...options, path: pathWithQueryString }]); + } else { + handleNavigateTo(routeState.onSaveNavigateTo); + } + } else { + history.push(getPath('policy_details', { policyId: agentPolicy!.id })); + } + }, + [agentPolicy, getPath, handleNavigateTo, history, routeState] + ); + const onSubmit = useCallback(async () => { if (formState === 'VALID' && hasErrors) { setFormState('INVALID'); @@ -279,27 +320,14 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { } const { error, data } = await savePackagePolicy(); if (!error) { - if (doOnSaveNavigation.current) { - if (routeState && routeState.onSaveNavigateTo) { - handleNavigateTo( - typeof routeState.onSaveNavigateTo === 'function' - ? routeState.onSaveNavigateTo(data!.item) - : routeState.onSaveNavigateTo - ); - } else { - history.push( - getPath('policy_details', { - policyId: agentPolicy!.id, - }) - ); - } - } - - const fromPolicyWithoutAgentsAssigned = from === 'policy' && agentPolicy && agentCount === 0; - - const fromPackageWithoutAgentsAssigned = packageInfo && agentPolicy && agentCount === 0; + setSavedPackagePolicy(data!.item); const hasAgentsAssigned = agentCount && agentPolicy; + if (!hasAgentsAssigned) { + setFormState('SUBMITTED_NO_AGENTS'); + return; + } + onSaveNavigate(data!.item); notifications.toasts.addSuccess({ title: i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationTitle', { @@ -308,40 +336,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { packagePolicyName: packagePolicy.name, }, }), - text: fromPolicyWithoutAgentsAssigned - ? i18n.translate( - 'xpack.fleet.createPackagePolicy.policyContextAddAgentNextNotificationMessage', - { - defaultMessage: `The policy has been updated. Add an agent to the '{agentPolicyName}' policy to deploy this policy.`, - values: { - agentPolicyName: agentPolicy!.name, - }, - } - ) - : fromPackageWithoutAgentsAssigned - ? toMountPoint( - // To render the link below we need to mount this JSX in the success toast - - {i18n.translate( - 'xpack.fleet.createPackagePolicy.integrationsContextAddAgentLinkMessage', - { defaultMessage: 'add an agent' } - )} - - ), - }} - /> - ) - : hasAgentsAssigned + text: hasAgentsAssigned ? i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationMessage', { defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`, values: { @@ -362,16 +357,10 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { hasErrors, agentCount, savePackagePolicy, - from, + onSaveNavigate, agentPolicy, - packageInfo, notifications.toasts, packagePolicy.name, - getHref, - routeState, - handleNavigateTo, - history, - getPath, ]); const integrationInfo = useMemo( @@ -508,6 +497,14 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { onCancel={() => setFormState('VALID')} /> )} + {formState === 'SUBMITTED_NO_AGENTS' && agentPolicy && packageInfo && ( + navigateAddAgent(savedPackagePolicy)} + onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} + /> + )} {packageInfo && ( { + it('should do nothing if no paramsToApply provided', () => { + expect( + appendOnSaveQueryParamsToPath({ path: '/hello', policy: mockPolicy, paramsToApply: [] }) + ).toEqual('/hello'); + }); + it('should do nothing if all params set to false', () => { + const options = { + path: '/hello', + policy: mockPolicy, + mappingOptions: { + showAddAgentHelp: false, + openEnrollmentFlyout: false, + }, + paramsToApply: ['showAddAgentHelp', 'openEnrollmentFlyout'] as OnSaveQueryParamKeys[], + }; + expect(appendOnSaveQueryParamsToPath(options)).toEqual('/hello'); + }); + + it('should append query params if set to true', () => { + const options = { + path: '/hello', + policy: mockPolicy, + mappingOptions: { + showAddAgentHelp: true, + openEnrollmentFlyout: true, + }, + paramsToApply: ['showAddAgentHelp', 'openEnrollmentFlyout'] as OnSaveQueryParamKeys[], + }; + + const hrefOut = appendOnSaveQueryParamsToPath(options); + const [basePath, qs] = parseHref(hrefOut); + expect(basePath).toEqual('/hello'); + expect(qs).toEqual({ showAddAgentHelp: 'true', openEnrollmentFlyout: 'true' }); + }); + it('should append query params if set to true (existing query string)', () => { + const options = { + path: '/hello?world=1', + policy: mockPolicy, + mappingOptions: { + showAddAgentHelp: true, + openEnrollmentFlyout: true, + }, + paramsToApply: ['showAddAgentHelp', 'openEnrollmentFlyout'] as OnSaveQueryParamKeys[], + }; + + const hrefOut = appendOnSaveQueryParamsToPath(options); + const [basePath, qs] = parseHref(hrefOut); + expect(basePath).toEqual('/hello'); + expect(qs).toEqual({ showAddAgentHelp: 'true', openEnrollmentFlyout: 'true', world: '1' }); + }); + + it('should append renamed param', () => { + const options = { + path: '/hello', + policy: mockPolicy, + mappingOptions: { + showAddAgentHelp: { renameKey: 'renamedKey' }, + }, + paramsToApply: ['showAddAgentHelp'] as OnSaveQueryParamKeys[], + }; + + const hrefOut = appendOnSaveQueryParamsToPath(options); + const [basePath, qs] = parseHref(hrefOut); + expect(basePath).toEqual('/hello'); + expect(qs).toEqual({ renamedKey: 'true' }); + }); + + it('should append renamed param (existing param)', () => { + const options = { + path: '/hello?world=1', + policy: mockPolicy, + mappingOptions: { + showAddAgentHelp: { renameKey: 'renamedKey' }, + }, + paramsToApply: ['showAddAgentHelp'] as OnSaveQueryParamKeys[], + }; + + const hrefOut = appendOnSaveQueryParamsToPath(options); + const [basePath, qs] = parseHref(hrefOut); + expect(basePath).toEqual('/hello'); + expect(qs).toEqual({ renamedKey: 'true', world: '1' }); + }); + + it('should append renamed param and policyId', () => { + const options = { + path: '/hello', + policy: mockPolicy, + mappingOptions: { + showAddAgentHelp: { renameKey: 'renamedKey', policyIdAsValue: true }, + }, + paramsToApply: ['showAddAgentHelp'] as OnSaveQueryParamKeys[], + }; + + const hrefOut = appendOnSaveQueryParamsToPath(options); + const [basePath, qs] = parseHref(hrefOut); + expect(basePath).toEqual('/hello'); + expect(qs).toEqual({ renamedKey: mockPolicy.policy_id }); + }); + + it('should append renamed param and policyId (existing param)', () => { + const options = { + path: '/hello?world=1', + policy: mockPolicy, + mappingOptions: { + showAddAgentHelp: { renameKey: 'renamedKey', policyIdAsValue: true }, + }, + paramsToApply: ['showAddAgentHelp'] as OnSaveQueryParamKeys[], + }; + + const hrefOut = appendOnSaveQueryParamsToPath(options); + const [basePath, qs] = parseHref(hrefOut); + expect(basePath).toEqual('/hello'); + expect(qs).toEqual({ renamedKey: mockPolicy.policy_id, world: '1' }); + }); + + it('should append renamed params and policyIds (existing param)', () => { + const options = { + path: '/hello?world=1', + policy: mockPolicy, + mappingOptions: { + showAddAgentHelp: { renameKey: 'renamedKey', policyIdAsValue: true }, + openEnrollmentFlyout: { renameKey: 'renamedKey2', policyIdAsValue: true }, + }, + paramsToApply: ['showAddAgentHelp', 'openEnrollmentFlyout'] as OnSaveQueryParamKeys[], + }; + + const hrefOut = appendOnSaveQueryParamsToPath(options); + const [basePath, qs] = parseHref(hrefOut); + expect(basePath).toEqual('/hello'); + expect(qs).toEqual({ + renamedKey: mockPolicy.policy_id, + renamedKey2: mockPolicy.policy_id, + world: '1', + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/utils/append_on_save_query_params.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/utils/append_on_save_query_params.ts new file mode 100644 index 00000000000000..4b7e3c61806cef --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/utils/append_on_save_query_params.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 { parse, stringify } from 'query-string'; + +import type { + CreatePackagePolicyRouteState, + OnSaveQueryParamOpts, + PackagePolicy, + OnSaveQueryParamKeys, +} from '../../../../types'; + +export function appendOnSaveQueryParamsToPath({ + path, + policy, + paramsToApply, + mappingOptions = {}, +}: { + path: string; + policy: PackagePolicy; + paramsToApply: OnSaveQueryParamKeys[]; + mappingOptions?: CreatePackagePolicyRouteState['onSaveQueryParams']; +}) { + const [basePath, queryStringIn] = path.split('?'); + const queryParams = parse(queryStringIn); + + paramsToApply.forEach((paramName) => { + const paramOptions = mappingOptions[paramName]; + if (paramOptions) { + const [paramKey, paramValue] = createQueryParam(paramName, paramOptions, policy.policy_id); + if (paramKey && paramValue) { + queryParams[paramKey] = paramValue; + } + } + }); + + const queryString = stringify(queryParams); + + return basePath + (queryString ? `?${queryString}` : ''); +} + +function createQueryParam( + name: string, + opts: OnSaveQueryParamOpts, + policyId: string +): [string?, string?] { + if (!opts) { + return []; + } + if (typeof opts === 'boolean' && opts) { + return [name, 'true']; + } + + const paramKey = opts.renameKey ? opts.renameKey : name; + const paramValue = opts.policyIdAsValue ? policyId : 'true'; + + return [paramKey, paramValue]; +} diff --git a/x-pack/test/saved_object_api_integration/security_only/config_basic.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/utils/index.ts similarity index 54% rename from x-pack/test/saved_object_api_integration/security_only/config_basic.ts rename to x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/utils/index.ts index 5c26b8be16dd0d..15de46e1dc5886 100644 --- a/x-pack/test/saved_object_api_integration/security_only/config_basic.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/utils/index.ts @@ -5,7 +5,4 @@ * 2.0. */ -import { createTestConfig } from '../common/config'; - -// eslint-disable-next-line import/no-default-export -export default createTestConfig('security_only', { disabledPlugins: ['spaces'], license: 'basic' }); +export { appendOnSaveQueryParamsToPath } from './append_on_save_query_params'; diff --git a/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx b/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx index 0c46e1af301cf6..d6d6dedf753efb 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx @@ -41,7 +41,7 @@ export const DefaultLayout: React.FunctionComponent = memo(({ section, ch

diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.tsx index ecc5c22c8d8ce5..4634996d6bc730 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.tsx @@ -54,7 +54,7 @@ const title = ( const recommendedTooltip = ( ); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index ade290aab4e5e7..881fc566c932dc 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -250,7 +250,7 @@ export function Detail() { let redirectToPath: CreatePackagePolicyRouteState['onSaveNavigateTo'] & CreatePackagePolicyRouteState['onCancelNavigateTo']; - + let onSaveQueryParams: CreatePackagePolicyRouteState['onSaveQueryParams']; if (agentPolicyIdFromContext) { redirectToPath = [ PLUGIN_ID, @@ -260,6 +260,11 @@ export function Detail() { })[1], }, ]; + + onSaveQueryParams = { + showAddAgentHelp: true, + openEnrollmentFlyout: true, + }; } else { redirectToPath = [ INTEGRATIONS_PLUGIN_ID, @@ -269,10 +274,16 @@ export function Detail() { })[1], }, ]; + + onSaveQueryParams = { + showAddAgentHelp: { renameKey: 'showAddAgentHelpForPolicyId', policyIdAsValue: true }, + openEnrollmentFlyout: { renameKey: 'addAgentToPolicyId', policyIdAsValue: true }, + }; } const redirectBackRouteState: CreatePackagePolicyRouteState = { onSaveNavigateTo: redirectToPath, + onSaveQueryParams, onCancelNavigateTo: [ INTEGRATIONS_PLUGIN_ID, { diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.tsx index e70d10e735571e..0ecab3290051e4 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.tsx @@ -13,7 +13,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { LinkedAgentCount, AddAgentHelpPopover } from '../../../../../../components'; const AddAgentButton = ({ onAddAgent }: { onAddAgent: () => void }) => ( - + agentPolicy.id === showAddAgentHelpForPolicyId + )?.packagePolicy?.id; // Handle the "add agent" link displayed in post-installation toast notifications in the case // where a user is clicking the link while on the package policies listing page useEffect(() => { @@ -292,13 +295,13 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentCount', { defaultMessage: 'Agents', }), - render({ agentPolicy }: InMemoryPackagePolicyAndAgentPolicy) { + render({ agentPolicy, packagePolicy }: InMemoryPackagePolicyAndAgentPolicy) { return ( setFlyoutOpenForPolicyId(agentPolicy.id)} - hasHelpPopover={showAddAgentHelpForPolicyId === agentPolicy.id} + hasHelpPopover={showAddAgentHelpForPackagePolicyId === packagePolicy.id} /> ); }, @@ -326,7 +329,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps }, }, ], - [getHref, showAddAgentHelpForPolicyId, viewDataStep] + [getHref, showAddAgentHelpForPackagePolicyId, viewDataStep] ); const noItemsMessage = useMemo(() => { diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/update_button.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/update_button.tsx index 48569d782a70ba..2acd5634b1e5f4 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/update_button.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/update_button.tsx @@ -114,18 +114,20 @@ export const UpdateButton: React.FunctionComponent = ({ return Array.isArray(arr) && arr.every((p) => typeof p === 'string'); } - const agentCount = useMemo( - () => - agentPolicyData?.items.reduce((acc, item) => { - const existingPolicies = isStringArray(item?.package_policies) - ? (item?.package_policies as string[]).filter((p) => packagePolicyIds.includes(p)) - : (item?.package_policies as PackagePolicy[]).filter((p) => + const agentCount = useMemo(() => { + if (!agentPolicyData?.items) return 0; + + return agentPolicyData.items.reduce((acc, item) => { + const existingPolicies = item?.package_policies + ? isStringArray(item.package_policies) + ? (item.package_policies as string[]).filter((p) => packagePolicyIds.includes(p)) + : (item.package_policies as PackagePolicy[]).filter((p) => packagePolicyIds.includes(p.id) - ); - return (acc += existingPolicies.length > 0 && item?.agents ? item?.agents : 0); - }, 0), - [agentPolicyData, packagePolicyIds] - ); + ) + : []; + return (acc += existingPolicies.length > 0 && item?.agents ? item?.agents : 0); + }, 0); + }, [agentPolicyData, packagePolicyIds]); const conflictCount = useMemo( () => dryRunData?.filter((item) => item.hasErrors).length, diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx index 91b557d0db5b67..f5c521ebacf16c 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx @@ -181,7 +181,7 @@ export const AvailablePackages: React.FC = memo(() => { let controls = [ - , + , ]; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx index d7b9ae2aef08a8..99e8809923140b 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect } from 'react'; import { EuiSteps, EuiText, @@ -23,16 +23,27 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { safeDump } from 'js-yaml'; -import { useStartServices, useLink, sendGetOneAgentPolicyFull } from '../../hooks'; +import { + useStartServices, + useLink, + sendGetOneAgentPolicyFull, + sendGetOneAgentPolicy, +} from '../../hooks'; import { fullAgentPolicyToYaml, agentPolicyRouteService } from '../../services'; +import type { PackagePolicy } from '../../../common'; + +import { + FLEET_KUBERNETES_PACKAGE, + KUBERNETES_RUN_INSTRUCTIONS, + STANDALONE_RUN_INSTRUCTIONS, +} from '../../../common'; + import { DownloadStep, AgentPolicySelectionStep } from './steps'; import type { BaseProps } from './types'; type Props = BaseProps; -const RUN_INSTRUCTIONS = './elastic-agent install'; - export const StandaloneInstructions = React.memo(({ agentPolicy, agentPolicies }) => { const { getHref } = useLink(); const core = useStartServices(); @@ -40,12 +51,34 @@ export const StandaloneInstructions = React.memo(({ agentPolicy, agentPol const [selectedPolicyId, setSelectedPolicyId] = useState(agentPolicy?.id); const [fullAgentPolicy, setFullAgentPolicy] = useState(); + const [isK8s, setIsK8s] = useState<'IS_LOADING' | 'IS_KUBERNETES' | 'IS_NOT_KUBERNETES'>( + 'IS_LOADING' + ); + const [yaml, setYaml] = useState(''); + const runInstructions = + isK8s === 'IS_KUBERNETES' ? KUBERNETES_RUN_INSTRUCTIONS : STANDALONE_RUN_INSTRUCTIONS; - const downloadLink = selectedPolicyId - ? core.http.basePath.prepend( - `${agentPolicyRouteService.getInfoFullDownloadPath(selectedPolicyId)}?standalone=true` - ) - : undefined; + useEffect(() => { + async function checkifK8s() { + if (!selectedPolicyId) { + return; + } + const agentPolicyRequest = await sendGetOneAgentPolicy(selectedPolicyId); + const agentPol = agentPolicyRequest.data ? agentPolicyRequest.data.item : null; + + if (!agentPol) { + setIsK8s('IS_NOT_KUBERNETES'); + return; + } + const k8s = (pkg: PackagePolicy) => pkg.package?.name === FLEET_KUBERNETES_PACKAGE; + setIsK8s( + (agentPol.package_policies as PackagePolicy[]).some(k8s) + ? 'IS_KUBERNETES' + : 'IS_NOT_KUBERNETES' + ); + } + checkifK8s(); + }, [selectedPolicyId, notifications.toasts]); useEffect(() => { async function fetchFullPolicy() { @@ -53,7 +86,11 @@ export const StandaloneInstructions = React.memo(({ agentPolicy, agentPol if (!selectedPolicyId) { return; } - const res = await sendGetOneAgentPolicyFull(selectedPolicyId, { standalone: true }); + let query = { standalone: true, kubernetes: false }; + if (isK8s === 'IS_KUBERNETES') { + query = { standalone: true, kubernetes: true }; + } + const res = await sendGetOneAgentPolicyFull(selectedPolicyId, query); if (res.error) { throw res.error; } @@ -61,7 +98,6 @@ export const StandaloneInstructions = React.memo(({ agentPolicy, agentPol if (!res.data) { throw new Error('No data while fetching full agent policy'); } - setFullAgentPolicy(res.data.item); } catch (error) { notifications.toasts.addError(error, { @@ -69,10 +105,86 @@ export const StandaloneInstructions = React.memo(({ agentPolicy, agentPol }); } } - fetchFullPolicy(); - }, [selectedPolicyId, notifications.toasts]); + if (isK8s !== 'IS_LOADING') { + fetchFullPolicy(); + } + }, [selectedPolicyId, notifications.toasts, isK8s, core.http.basePath]); + + useEffect(() => { + if (isK8s === 'IS_KUBERNETES') { + if (typeof fullAgentPolicy === 'object') { + return; + } + setYaml(fullAgentPolicy); + } else { + if (typeof fullAgentPolicy === 'string') { + return; + } + setYaml(fullAgentPolicyToYaml(fullAgentPolicy, safeDump)); + } + }, [fullAgentPolicy, isK8s]); + + const policyMsg = + isK8s === 'IS_KUBERNETES' ? ( + ES_USERNAME, + ESPasswordVariable: ES_PASSWORD, + }} + /> + ) : ( + elastic-agent.yml, + ESUsernameVariable: ES_USERNAME, + ESPasswordVariable: ES_PASSWORD, + outputSection: outputs, + }} + /> + ); + + let downloadLink = ''; + if (selectedPolicyId) { + downloadLink = + isK8s === 'IS_KUBERNETES' + ? core.http.basePath.prepend( + `${agentPolicyRouteService.getInfoFullDownloadPath(selectedPolicyId)}?kubernetes=true` + ) + : core.http.basePath.prepend( + `${agentPolicyRouteService.getInfoFullDownloadPath(selectedPolicyId)}?standalone=true` + ); + } + + const downloadMsg = + isK8s === 'IS_KUBERNETES' ? ( + + ) : ( + + ); + + const applyMsg = + isK8s === 'IS_KUBERNETES' ? ( + + ) : ( + + ); - const yaml = useMemo(() => fullAgentPolicyToYaml(fullAgentPolicy, safeDump), [fullAgentPolicy]); const steps = [ DownloadStep(), !agentPolicy @@ -85,16 +197,7 @@ export const StandaloneInstructions = React.memo(({ agentPolicy, agentPol children: ( <> - elastic-agent.yml, - ESUsernameVariable: ES_USERNAME, - ESPasswordVariable: ES_PASSWORD, - outputSection: outputs, - }} - /> + <>{policyMsg} @@ -111,10 +214,7 @@ export const StandaloneInstructions = React.memo(({ agentPolicy, agentPol - + <>{downloadMsg} @@ -133,14 +233,11 @@ export const StandaloneInstructions = React.memo(({ agentPolicy, agentPol children: ( <> - + <>{applyMsg} - {RUN_INSTRUCTIONS} + {runInstructions} - + {(copy) => ( { const kibanaVersion = useKibanaVersion(); const kibanaVersionURLString = useMemo( () => - `${semver.major(kibanaVersion)}-${semver.minor(kibanaVersion)}-${semver.patch( - kibanaVersion - )}`, + `${semverMajor(kibanaVersion)}-${semverMinor(kibanaVersion)}-${semverPatch(kibanaVersion)}`, [kibanaVersion] ); return { diff --git a/x-pack/plugins/fleet/public/hooks/use_request/agent_policy.ts b/x-pack/plugins/fleet/public/hooks/use_request/agent_policy.ts index 33ef9afd3d8fd9..777a74b079d7bf 100644 --- a/x-pack/plugins/fleet/public/hooks/use_request/agent_policy.ts +++ b/x-pack/plugins/fleet/public/hooks/use_request/agent_policy.ts @@ -58,7 +58,7 @@ export const useGetOneAgentPolicyFull = (agentPolicyId: string) => { export const sendGetOneAgentPolicyFull = ( agentPolicyId: string, - query: { standalone?: boolean } = {} + query: { standalone?: boolean; kubernetes?: boolean } = {} ) => { return sendRequest({ path: agentPolicyRouteService.getInfoFullPath(agentPolicyId), diff --git a/x-pack/plugins/fleet/public/types/intra_app_route_state.ts b/x-pack/plugins/fleet/public/types/intra_app_route_state.ts index 36fd32c2a6584f..0ea40e6fe56954 100644 --- a/x-pack/plugins/fleet/public/types/intra_app_route_state.ts +++ b/x-pack/plugins/fleet/public/types/intra_app_route_state.ts @@ -7,20 +7,34 @@ import type { ApplicationStart } from 'kibana/public'; -import type { PackagePolicy } from './'; +/** + * Supported query parameters for CreatePackagePolicyRouteState + */ +export type OnSaveQueryParamKeys = 'showAddAgentHelp' | 'openEnrollmentFlyout'; +/** + * Query string parameter options for CreatePackagePolicyRouteState + */ +export type OnSaveQueryParamOpts = + | { + renameKey?: string; // override param name + policyIdAsValue?: boolean; // use policyId as param value instead of true + } + | boolean; /** * Supported routing state for the create package policy page routes */ export interface CreatePackagePolicyRouteState { /** On a successful save of the package policy, use navigate to the given app */ - onSaveNavigateTo?: - | Parameters - | ((newPackagePolicy: PackagePolicy) => Parameters); + onSaveNavigateTo?: Parameters; /** On cancel, navigate to the given app */ onCancelNavigateTo?: Parameters; /** Url to be used on cancel links */ onCancelUrl?: string; + /** supported query params for onSaveNavigateTo path */ + onSaveQueryParams?: { + [key in OnSaveQueryParamKeys]?: OnSaveQueryParamOpts; + }; } /** diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index b3197d918d2312..c3da75183f5812 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -34,6 +34,7 @@ import type { CopyAgentPolicyResponse, DeleteAgentPolicyResponse, GetFullAgentPolicyResponse, + GetFullAgentConfigMapResponse, } from '../../../common'; import { defaultIngestErrorHandler } from '../../errors'; @@ -232,27 +233,52 @@ export const getFullAgentPolicy: RequestHandler< > = async (context, request, response) => { const soClient = context.core.savedObjects.client; - try { - const fullAgentPolicy = await agentPolicyService.getFullAgentPolicy( - soClient, - request.params.agentPolicyId, - { standalone: request.query.standalone === true } - ); - if (fullAgentPolicy) { - const body: GetFullAgentPolicyResponse = { - item: fullAgentPolicy, - }; - return response.ok({ - body, - }); - } else { - return response.customError({ - statusCode: 404, - body: { message: 'Agent policy not found' }, - }); + if (request.query.kubernetes === true) { + try { + const fullAgentConfigMap = await agentPolicyService.getFullAgentConfigMap( + soClient, + request.params.agentPolicyId, + { standalone: request.query.standalone === true } + ); + if (fullAgentConfigMap) { + const body: GetFullAgentConfigMapResponse = { + item: fullAgentConfigMap, + }; + return response.ok({ + body, + }); + } else { + return response.customError({ + statusCode: 404, + body: { message: 'Agent config map not found' }, + }); + } + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } + } else { + try { + const fullAgentPolicy = await agentPolicyService.getFullAgentPolicy( + soClient, + request.params.agentPolicyId, + { standalone: request.query.standalone === true } + ); + if (fullAgentPolicy) { + const body: GetFullAgentPolicyResponse = { + item: fullAgentPolicy, + }; + return response.ok({ + body, + }); + } else { + return response.customError({ + statusCode: 404, + body: { message: 'Agent policy not found' }, + }); + } + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } - } catch (error) { - return defaultIngestErrorHandler({ error, response }); } }; @@ -265,27 +291,55 @@ export const downloadFullAgentPolicy: RequestHandler< params: { agentPolicyId }, } = request; - try { - const fullAgentPolicy = await agentPolicyService.getFullAgentPolicy(soClient, agentPolicyId, { - standalone: request.query.standalone === true, - }); - if (fullAgentPolicy) { - const body = fullAgentPolicyToYaml(fullAgentPolicy, safeDump); - const headers: ResponseHeaders = { - 'content-type': 'text/x-yaml', - 'content-disposition': `attachment; filename="elastic-agent.yml"`, - }; - return response.ok({ - body, - headers, - }); - } else { - return response.customError({ - statusCode: 404, - body: { message: 'Agent policy not found' }, + if (request.query.kubernetes === true) { + try { + const fullAgentConfigMap = await agentPolicyService.getFullAgentConfigMap( + soClient, + request.params.agentPolicyId, + { standalone: request.query.standalone === true } + ); + if (fullAgentConfigMap) { + const body = fullAgentConfigMap; + const headers: ResponseHeaders = { + 'content-type': 'text/x-yaml', + 'content-disposition': `attachment; filename="elastic-agent-standalone-kubernetes.yaml"`, + }; + return response.ok({ + body, + headers, + }); + } else { + return response.customError({ + statusCode: 404, + body: { message: 'Agent config map not found' }, + }); + } + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } + } else { + try { + const fullAgentPolicy = await agentPolicyService.getFullAgentPolicy(soClient, agentPolicyId, { + standalone: request.query.standalone === true, }); + if (fullAgentPolicy) { + const body = fullAgentPolicyToYaml(fullAgentPolicy, safeDump); + const headers: ResponseHeaders = { + 'content-type': 'text/x-yaml', + 'content-disposition': `attachment; filename="elastic-agent.yml"`, + }; + return response.ok({ + body, + headers, + }); + } else { + return response.customError({ + statusCode: 404, + body: { message: 'Agent policy not found' }, + }); + } + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } - } catch (error) { - return defaultIngestErrorHandler({ error, response }); } }; diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts index 561c463b998d42..60cf9c8d96257b 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts @@ -70,12 +70,14 @@ export async function getFullAgentPolicy( if (!monitoringOutput) { throw new Error(`Monitoring output not found ${monitoringOutputId}`); } - const fullAgentPolicy: FullAgentPolicy = { id: agentPolicy.id, outputs: { ...outputs.reduce((acc, output) => { - acc[getOutputIdForAgentPolicy(output)] = transformOutputToFullPolicyOutput(output); + acc[getOutputIdForAgentPolicy(output)] = transformOutputToFullPolicyOutput( + output, + standalone + ); return acc; }, {}), @@ -179,8 +181,8 @@ function transformOutputToFullPolicyOutput( if (standalone) { delete newOutput.api_key; - newOutput.username = 'ES_USERNAME'; - newOutput.password = 'ES_PASSWORD'; + newOutput.username = '{ES_USERNAME}'; + newOutput.password = '{ES_PASSWORD}'; } return newOutput; diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 6ebe890aeaef20..321bc7f289594d 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -13,6 +13,8 @@ import type { SavedObjectsBulkUpdateResponse, } from 'src/core/server'; +import { safeDump } from 'js-yaml'; + import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import type { AuthenticatedUser } from '../../../security/server'; @@ -41,6 +43,12 @@ import type { } from '../../common'; import { AgentPolicyNameExistsError, HostedAgentPolicyRestrictionRelatedError } from '../errors'; +import type { FullAgentConfigMap } from '../../common/types/models/agent_cm'; + +import { fullAgentConfigMapToYaml } from '../../common/services/agent_cm_to_yaml'; + +import { elasticAgentManifest } from './elastic_agent_manifest'; + import { getPackageInfo } from './epm/packages'; import { getAgentsByKuery } from './agents'; import { packagePolicyService } from './package_policy'; @@ -49,7 +57,6 @@ import { agentPolicyUpdateEventHandler } from './agent_policy_update'; import { normalizeKuery, escapeSearchQueryPhrase } from './saved_object'; import { appContextService } from './app_context'; import { getFullAgentPolicy } from './agent_policies'; - const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE; class AgentPolicyService { @@ -717,6 +724,40 @@ class AgentPolicyService { return res.body.hits.hits[0]._source; } + public async getFullAgentConfigMap( + soClient: SavedObjectsClientContract, + id: string, + options?: { standalone: boolean } + ): Promise { + const fullAgentPolicy = await getFullAgentPolicy(soClient, id, options); + if (fullAgentPolicy) { + const fullAgentConfigMap: FullAgentConfigMap = { + apiVersion: 'v1', + kind: 'ConfigMap', + metadata: { + name: 'agent-node-datastreams', + namespace: 'kube-system', + labels: { + 'k8s-app': 'elastic-agent', + }, + }, + data: { + 'agent.yml': fullAgentPolicy, + }, + }; + + const configMapYaml = fullAgentConfigMapToYaml(fullAgentConfigMap, safeDump); + const updateManifestVersion = elasticAgentManifest.replace( + 'VERSION', + appContextService.getKibanaVersion() + ); + const fixedAgentYML = configMapYaml.replace('agent.yml:', 'agent.yml: |-'); + return [fixedAgentYML, updateManifestVersion].join('\n'); + } else { + return ''; + } + } + public async getFullAgentPolicy( soClient: SavedObjectsClientContract, id: string, diff --git a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts new file mode 100644 index 00000000000000..392ee170d02ad9 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts @@ -0,0 +1,222 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const elasticAgentManifest = ` +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: elastic-agent + namespace: kube-system + labels: + app: elastic-agent +spec: + selector: + matchLabels: + app: elastic-agent + template: + metadata: + labels: + app: elastic-agent + spec: + tolerations: + - key: node-role.kubernetes.io/master + effect: NoSchedule + serviceAccountName: elastic-agent + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + containers: + - name: elastic-agent + image: docker.elastic.co/beats/elastic-agent:VERSION + args: [ + "-c", "/etc/agent.yml", + "-e", + "-d", "'*'", + ] + env: + - name: ES_USERNAME + value: "elastic" + - name: ES_PASSWORD + value: "changeme" + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + securityContext: + runAsUser: 0 + resources: + limits: + memory: 500Mi + requests: + cpu: 100m + memory: 200Mi + volumeMounts: + - name: datastreams + mountPath: /etc/agent.yml + readOnly: true + subPath: agent.yml + - name: proc + mountPath: /hostfs/proc + readOnly: true + - name: cgroup + mountPath: /hostfs/sys/fs/cgroup + readOnly: true + - name: varlibdockercontainers + mountPath: /var/lib/docker/containers + readOnly: true + - name: varlog + mountPath: /var/log + readOnly: true + volumes: + - name: datastreams + configMap: + defaultMode: 0640 + name: agent-node-datastreams + - name: proc + hostPath: + path: /proc + - name: cgroup + hostPath: + path: /sys/fs/cgroup + - name: varlibdockercontainers + hostPath: + path: /var/lib/docker/containers + - name: varlog + hostPath: + path: /var/log +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: elastic-agent +subjects: + - kind: ServiceAccount + name: elastic-agent + namespace: kube-system +roleRef: + kind: ClusterRole + name: elastic-agent + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + namespace: kube-system + name: elastic-agent +subjects: + - kind: ServiceAccount + name: elastic-agent + namespace: kube-system +roleRef: + kind: Role + name: elastic-agent + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: elastic-agent-kubeadm-config + namespace: kube-system +subjects: + - kind: ServiceAccount + name: elastic-agent + namespace: kube-system +roleRef: + kind: Role + name: elastic-agent-kubeadm-config + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: elastic-agent + labels: + k8s-app: elastic-agent +rules: + - apiGroups: [""] + resources: + - nodes + - namespaces + - events + - pods + - services + - configmaps + verbs: ["get", "list", "watch"] + # Enable this rule only if planing to use kubernetes_secrets provider + #- apiGroups: [""] + # resources: + # - secrets + # verbs: ["get"] + - apiGroups: ["extensions"] + resources: + - replicasets + verbs: ["get", "list", "watch"] + - apiGroups: ["apps"] + resources: + - statefulsets + - deployments + - replicasets + verbs: ["get", "list", "watch"] + - apiGroups: ["batch"] + resources: + - jobs + verbs: ["get", "list", "watch"] + - apiGroups: + - "" + resources: + - nodes/stats + verbs: + - get + # required for apiserver + - nonResourceURLs: + - "/metrics" + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: elastic-agent + # should be the namespace where elastic-agent is running + namespace: kube-system + labels: + k8s-app: elastic-agent +rules: + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: ["get", "create", "update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: elastic-agent-kubeadm-config + namespace: kube-system + labels: + k8s-app: elastic-agent +rules: + - apiGroups: [""] + resources: + - configmaps + resourceNames: + - kubeadm-config + verbs: ["get"] +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: elastic-agent + namespace: kube-system + labels: + k8s-app: elastic-agent +--- +`; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts index 714ffab922dd98..64d142f150bfd0 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts @@ -56,5 +56,6 @@ export const GetFullAgentPolicyRequestSchema = { query: schema.object({ download: schema.maybe(schema.boolean()), standalone: schema.maybe(schema.boolean()), + kubernetes: schema.maybe(schema.boolean()), }), }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx index 9f8199215df5b9..1682431900a84f 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx @@ -9,7 +9,7 @@ import React from 'react'; import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { merge } from 'lodash'; -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { notificationServiceMock, diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index f562ab9d15a8b9..5cd0864a4df210 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -7,7 +7,7 @@ import React, { createContext, useContext } from 'react'; import { ScopedHistory } from 'kibana/public'; -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { ManagementAppMountParams } from 'src/plugins/management/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx index 5e3ae3c1544aef..d80712dfa0fea5 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; /* eslint-disable-next-line @kbn/eslint/no-restricted-paths */ import '../../../../../../../../../../src/plugins/es_ui_shared/public/components/code_editor/jest_mock'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index a751dc3fa72a50..aa6a5e7981cbc8 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -19,7 +19,7 @@ import { EuiSpacer, EuiCallOut, } from '@elastic/eui'; -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { documentationService } from '../../../../../../services/documentation'; import { Form, FormHook, FormDataProvider } from '../../../../shared_imports'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx index ad7f7e6d93c412..0ec89de1daf19c 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx index ea71e7fcce5d27..db68b14e62ee85 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { i18n } from '@kbn/i18n'; import { NormalizedField, Field as FieldType } from '../../../../types'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx index 0f58c75ca9cb7e..aadc64392db514 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { NormalizedField, Field as FieldType } from '../../../../types'; import { UseField, Field } from '../../../../shared_imports'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts index f62a19e55a8352..9f1bb05a5b6467 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts @@ -6,7 +6,7 @@ */ import { ComponentType } from 'react'; -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { MainType, SubType, DataType, NormalizedField, NormalizedFields } from '../../../../types'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/ip_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/ip_type.tsx index 3ea56805829d51..82ca1cd02d2e17 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/ip_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/ip_type.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { NormalizedField, Field as FieldType } from '../../../../types'; import { getFieldConfig } from '../../../../lib'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/keyword_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/keyword_type.tsx index 9d820c1b07636e..543a2d3520b72a 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/keyword_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/keyword_type.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { i18n } from '@kbn/i18n'; import { documentationService } from '../../../../../../services/documentation'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx index 7035a730f15f4b..764db2744f43f2 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { NormalizedField, Field as FieldType, ComboBoxOption } from '../../../../types'; import { getFieldConfig } from '../../../../lib'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx index 9b8dae490d8192..b9fb4950c9a19c 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { NormalizedField, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx index 6857e20dc1ec40..329c896e525288 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiSpacer, EuiDualRange, EuiFormRow, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { documentationService } from '../../../../../../services/documentation'; import { NormalizedField, Field as FieldType } from '../../../../types'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx index 3c0e8a28f30904..cc7816d55cec93 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { i18n } from '@kbn/i18n'; import { documentationService } from '../../../../../../services/documentation'; diff --git a/x-pack/plugins/index_management/public/application/index.tsx b/x-pack/plugins/index_management/public/application/index.tsx index fc64dad0ae7baa..b7a4bd21351473 100644 --- a/x-pack/plugins/index_management/public/application/index.tsx +++ b/x-pack/plugins/index_management/public/application/index.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { render, unmountComponentAtNode } from 'react-dom'; -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { CoreStart, CoreSetup } from '../../../../../src/core/public'; diff --git a/x-pack/plugins/index_management/public/application/lib/indices.ts b/x-pack/plugins/index_management/public/application/lib/indices.ts index fc93aa6f544489..6d4bbc992a21c5 100644 --- a/x-pack/plugins/index_management/public/application/lib/indices.ts +++ b/x-pack/plugins/index_management/public/application/lib/indices.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { MAJOR_VERSION } from '../../../common'; import { Index } from '../../../common'; diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index 71e0f803654303..48508695bfc98c 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { CoreSetup } from 'src/core/public'; import { ManagementAppMountParams } from 'src/plugins/management/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; diff --git a/x-pack/plugins/index_management/public/application/store/selectors/indices_filter.test.ts b/x-pack/plugins/index_management/public/application/store/selectors/indices_filter.test.ts index b32f2736a96840..bdb531e41abb22 100644 --- a/x-pack/plugins/index_management/public/application/store/selectors/indices_filter.test.ts +++ b/x-pack/plugins/index_management/public/application/store/selectors/indices_filter.test.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { MAJOR_VERSION } from '../../../../common'; import { ExtensionsService } from '../../../services'; diff --git a/x-pack/plugins/index_management/public/plugin.ts b/x-pack/plugins/index_management/public/plugin.ts index b7e810b15dbf93..4e123b6f474f81 100644 --- a/x-pack/plugins/index_management/public/plugin.ts +++ b/x-pack/plugins/index_management/public/plugin.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { SemVer } from 'semver'; +import SemVer from 'semver/classes/semver'; import { CoreSetup, PluginInitializerContext } from '../../../../src/core/public'; import { setExtensionsService } from './application/store/selectors/extension_service'; diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts index 2e8ad1de3413cf..9e8935ddb99688 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/types.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts @@ -5,7 +5,10 @@ * 2.0. */ import * as rt from 'io-ts'; +import { Unit } from '@elastic/datemath'; import { ANOMALY_THRESHOLD } from '../../infra_ml'; +import { InventoryItemType, SnapshotMetricType } from '../../inventory_models/types'; +import { SnapshotCustomMetricInput } from '../../http_api'; // TODO: Have threshold and inventory alerts import these types from this file instead of from their // local directories @@ -54,3 +57,25 @@ export interface MetricAnomalyParams { threshold: Exclude; influencerFilter: rt.TypeOf | undefined; } + +// Types for the executor + +export interface InventoryMetricConditions { + metric: SnapshotMetricType; + timeSize: number; + timeUnit: Unit; + sourceId?: string; + threshold: number[]; + comparator: Comparator; + customMetric?: SnapshotCustomMetricInput; + warningThreshold?: number[]; + warningComparator?: Comparator; +} + +export interface InventoryMetricThresholdParams { + criteria: InventoryMetricConditions[]; + filterQuery?: string; + nodeType: InventoryItemType; + sourceId?: string; + alertOnNoData?: boolean; +} diff --git a/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts b/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts index 30e0de94021910..ee27f1ff099253 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts +++ b/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts @@ -5,15 +5,48 @@ * 2.0. */ -import { ALERT_REASON } from '@kbn/rule-data-utils'; +import { ALERT_REASON, ALERT_RULE_PARAMS, TIMESTAMP } from '@kbn/rule-data-utils'; +import { encode } from 'rison-node'; +import { stringify } from 'query-string'; import { ObservabilityRuleTypeFormatter } from '../../../../observability/public'; +import { InventoryMetricThresholdParams } from '../../../common/alerting/metrics'; export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => { const reason = fields[ALERT_REASON] ?? '-'; - const link = '/app/metrics/inventory'; // TODO https://github.com/elastic/kibana/issues/106497 + const ruleParams = parseRuleParams(fields[ALERT_RULE_PARAMS]); + + let link = '/app/metrics/link-to/inventory?'; + + if (ruleParams) { + const linkToParams: Record = { + nodeType: ruleParams.nodeType, + timestamp: Date.parse(fields[TIMESTAMP]), + customMetric: '', + }; + + // We always pick the first criteria metric for the URL + const criteria = ruleParams.criteria[0]; + if (criteria.customMetric && criteria.customMetric.id !== 'alert-custom-metric') { + const customMetric = encode(criteria.customMetric); + linkToParams.customMetric = customMetric; + linkToParams.metric = customMetric; + } else { + linkToParams.metric = encode({ type: criteria.metric }); + } + + link += stringify(linkToParams); + } return { reason, link, }; }; + +function parseRuleParams(params?: string): InventoryMetricThresholdParams | undefined { + try { + return typeof params === 'string' ? JSON.parse(params) : undefined; + } catch (_) { + return; + } +} diff --git a/x-pack/plugins/infra/public/pages/link_to/link_to_metrics.tsx b/x-pack/plugins/infra/public/pages/link_to/link_to_metrics.tsx index 8304e1cdb7262b..05a099441b699c 100644 --- a/x-pack/plugins/infra/public/pages/link_to/link_to_metrics.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/link_to_metrics.tsx @@ -10,6 +10,7 @@ import { match as RouteMatch, Redirect, Route, Switch } from 'react-router-dom'; import { RedirectToNodeDetail } from './redirect_to_node_detail'; import { RedirectToHostDetailViaIP } from './redirect_to_host_detail_via_ip'; +import { RedirectToInventory } from './redirect_to_inventory'; import { inventoryModels } from '../../../common/inventory_models'; interface LinkToPageProps { @@ -29,6 +30,7 @@ export const LinkToMetricsPage: React.FC = (props) => { path={`${props.match.url}/host-detail-via-ip/:hostIp`} component={RedirectToHostDetailViaIP} /> + ); diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_inventory.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_inventory.tsx new file mode 100644 index 00000000000000..37ddbacf72488d --- /dev/null +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_inventory.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { parse } from 'query-string'; +import { Redirect, RouteComponentProps } from 'react-router-dom'; + +// FIXME what would be the right way to build this query string? +const QUERY_STRING_TEMPLATE = + "?waffleFilter=(expression:'',kind:kuery)&waffleTime=(currentTime:{timestamp},isAutoReloading:!f)&waffleOptions=(accountId:'',autoBounds:!t,boundsOverride:(max:1,min:0),customMetrics:!({customMetric}),customOptions:!(),groupBy:!(),legend:(palette:cool,reverseColors:!f,steps:10),metric:{metric},nodeType:{nodeType},region:'',sort:(by:name,direction:desc),timelineOpen:!f,view:map)"; + +export const RedirectToInventory: React.FC = ({ location }) => { + const parsedQueryString = parseQueryString(location.search); + + const inventoryQueryString = QUERY_STRING_TEMPLATE.replace( + /{(\w+)}/g, + (_, key) => parsedQueryString[key] || '' + ); + + return ; +}; + +function parseQueryString(search: string): Record { + if (search.length === 0) { + return {}; + } + + const obj = parse(search.substring(1)); + + // Force all values into string. If they are empty don't create the keys + for (const key in obj) { + if (Object.hasOwnProperty.call(obj, key)) { + if (!obj[key]) { + delete obj[key]; + } + if (Array.isArray(obj.key)) { + obj[key] = obj[key]![0]; + } + } + } + + return obj as Record; +} diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 72d9ea9e39defb..5cd093c6f1472d 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -11,7 +11,7 @@ import { ALERT_REASON, ALERT_RULE_PARAMS } from '@kbn/rule-data-utils'; import moment from 'moment'; import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; -import { AlertStates, InventoryMetricConditions } from './types'; +import { AlertStates } from './types'; import { ActionGroupIdsOf, ActionGroup, @@ -20,10 +20,11 @@ import { RecoveredActionGroup, } from '../../../../../alerting/common'; import { AlertInstance, AlertTypeState } from '../../../../../alerting/server'; -import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; +import { SnapshotMetricType } from '../../../../common/inventory_models/types'; import { InfraBackendLibs } from '../../infra_types'; import { METRIC_FORMATTERS } from '../../../../common/formatters/snapshot_metric_formats'; import { createFormatter } from '../../../../common/formatters'; +import { InventoryMetricThresholdParams } from '../../../../common/alerting/metrics'; import { buildErrorAlertReason, buildFiredAlertReason, @@ -33,19 +34,10 @@ import { } from '../common/messages'; import { evaluateCondition } from './evaluate_condition'; -interface InventoryMetricThresholdParams { - criteria: InventoryMetricConditions[]; - filterQuery: string | undefined; - nodeType: InventoryItemType; - sourceId?: string; - alertOnNoData?: boolean; -} - type InventoryMetricThresholdAllowedActionGroups = ActionGroupIdsOf< typeof FIRED_ACTIONS | typeof WARNING_ACTIONS >; -export type InventoryMetricThresholdAlertTypeParams = Record; export type InventoryMetricThresholdAlertTypeState = AlertTypeState; // no specific state used export type InventoryMetricThresholdAlertInstanceState = AlertInstanceState; // no specific state used export type InventoryMetricThresholdAlertInstanceContext = AlertInstanceContext; // no specific instance context used @@ -64,14 +56,13 @@ type InventoryMetricThresholdAlertInstanceFactory = ( export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) => libs.metricsRules.createLifecycleRuleExecutor< - InventoryMetricThresholdAlertTypeParams, + InventoryMetricThresholdParams & Record, InventoryMetricThresholdAlertTypeState, InventoryMetricThresholdAlertInstanceState, InventoryMetricThresholdAlertInstanceContext, InventoryMetricThresholdAllowedActionGroups >(async ({ services, params }) => { - const { criteria, filterQuery, sourceId, nodeType, alertOnNoData } = - params as InventoryMetricThresholdParams; + const { criteria, filterQuery, sourceId, nodeType, alertOnNoData } = params; if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); const { alertWithLifecycle, savedObjectsClient } = services; const alertInstanceFactory: InventoryMetricThresholdAlertInstanceFactory = (id, reason) => diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts index 5d516f3591419a..77c85967e64f67 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; +import { schema, Type } from '@kbn/config-schema'; +import { Unit } from '@elastic/datemath'; import { i18n } from '@kbn/i18n'; import { PluginSetupContract } from '../../../../../alerting/server'; import { @@ -26,21 +27,32 @@ import { metricActionVariableDescription, thresholdActionVariableDescription, } from '../common/messages'; +import { + SnapshotMetricTypeKeys, + SnapshotMetricType, + InventoryItemType, +} from '../../../../common/inventory_models/types'; +import { + SNAPSHOT_CUSTOM_AGGREGATIONS, + SnapshotCustomAggregation, +} from '../../../../common/http_api/snapshot_api'; const condition = schema.object({ threshold: schema.arrayOf(schema.number()), - comparator: oneOfLiterals(Object.values(Comparator)), - timeUnit: schema.string(), + comparator: oneOfLiterals(Object.values(Comparator)) as Type, + timeUnit: schema.string() as Type, timeSize: schema.number(), - metric: schema.string(), + metric: oneOfLiterals(Object.keys(SnapshotMetricTypeKeys)) as Type, warningThreshold: schema.maybe(schema.arrayOf(schema.number())), - warningComparator: schema.maybe(oneOfLiterals(Object.values(Comparator))), + warningComparator: schema.maybe(oneOfLiterals(Object.values(Comparator))) as Type< + Comparator | undefined + >, customMetric: schema.maybe( schema.object({ type: schema.literal('custom'), id: schema.string(), field: schema.string(), - aggregation: schema.string(), + aggregation: oneOfLiterals(SNAPSHOT_CUSTOM_AGGREGATIONS) as Type, label: schema.maybe(schema.string()), }) ), @@ -59,7 +71,7 @@ export async function registerMetricInventoryThresholdAlertType( params: schema.object( { criteria: schema.arrayOf(condition), - nodeType: schema.string(), + nodeType: schema.string() as Type, filterQuery: schema.maybe( schema.string({ validate: validateIsStringElasticsearchJSONFilter }) ), diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts index 120fa47c079ab7..829f34d42ee039 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts @@ -8,9 +8,9 @@ import { Unit } from '@elastic/datemath'; import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api'; import { SnapshotMetricType } from '../../../../common/inventory_models/types'; -import { Comparator, AlertStates } from '../common/types'; +import { Comparator, AlertStates, Aggregators } from '../common/types'; -export { Comparator, AlertStates }; +export { Comparator, AlertStates, Aggregators }; export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold'; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index d4bc59a1e9e2ca..692fb0499176d3 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -171,13 +171,6 @@ export async function mountApp( ? historyLocationState.payload : undefined; - // Clear app-specific filters when navigating to Lens. Necessary because Lens - // can be loaded without a full page refresh. If the user navigates to Lens from Discover - // we keep the filters - if (!initialContext) { - data.query.filterManager.setAppFilters([]); - } - if (embeddableEditorIncomingState?.searchSessionId) { data.search.session.continue(embeddableEditorIncomingState.searchSessionId); } @@ -206,9 +199,14 @@ export async function mountApp( trackUiEvent('loaded'); const initialInput = getInitialInput(props.id, props.editByValue); - lensStore.dispatch( - loadInitial({ redirectCallback, initialInput, emptyState, history: props.history }) - ); + // Clear app-specific filters when navigating to Lens. Necessary because Lens + // can be loaded without a full page refresh. If the user navigates to Lens from Discover + // we keep the filters + if (!initialContext) { + data.query.filterManager.setAppFilters([]); + } + + lensStore.dispatch(loadInitial({ redirectCallback, initialInput, history: props.history })); return ( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/dimension_button.tsx index 5d9fd1f8b8f13c..508148be8b2a9c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/dimension_button.tsx @@ -25,6 +25,7 @@ export function DimensionButton({ onRemoveClick, accessorConfig, label, + invalid, }: { group: VisualizationDimensionGroupConfig; children: React.ReactElement; @@ -32,6 +33,7 @@ export function DimensionButton({ onRemoveClick: (id: string) => void; accessorConfig: AccessorConfig; label: string; + invalid?: boolean; }) { return ( <> @@ -41,6 +43,7 @@ export function DimensionButton({ onClick={() => onClick(accessorConfig.columnId)} aria-label={triggerLinkA11yText(label)} title={triggerLinkA11yText(label)} + color={invalid || group.invalid ? 'danger' : undefined} > {children} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 8d19620cebbdc7..bdd5d93c2c2c88 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -477,6 +477,13 @@ export function LayerPanel( ); removeButtonRef(id); }} + invalid={ + !layerDatasource.isValidColumn( + layerDatasourceState, + layerId, + columnId + ) + } > { - if (hasLoaded) { - dispatchLens(setSaveable(expressionExists)); - } - }, [hasLoaded, expressionExists, dispatchLens]); + dispatchLens(setSaveable(expressionExists)); + }, [expressionExists, dispatchLens]); const onEvent = useCallback( (event: ExpressionRendererEvent) => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 2138b06a4c3447..1407f7f7a25de2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -42,7 +42,7 @@ import { getDatasourceSuggestionsForVisualizeField, } from './indexpattern_suggestions'; -import { isDraggedField, normalizeOperationDataType } from './utils'; +import { isColumnInvalid, isDraggedField, normalizeOperationDataType } from './utils'; import { LayerPanel } from './layerpanel'; import { IndexPatternColumn, getErrorMessages, insertNewColumn } from './operations'; import { IndexPatternField, IndexPatternPrivateState, IndexPatternPersistedState } from './types'; @@ -268,6 +268,11 @@ export function getIndexPatternDatasource({ return columnLabelMap; }, + isValidColumn: (state: IndexPatternPrivateState, layerId: string, columnId: string) => { + const layer = state.layers[layerId]; + return !isColumnInvalid(layer, columnId, state.indexPatterns[layer.indexPatternId]); + }, + renderDimensionTrigger: ( domElement: Element, props: DatasourceDimensionTriggerProps diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx index 989c858b1f29d5..5c285f70b2ed9c 100644 --- a/x-pack/plugins/lens/public/mocks.tsx +++ b/x-pack/plugins/lens/public/mocks.tsx @@ -159,6 +159,7 @@ export function createMockDatasource(id: string): DatasourceMock { getErrorMessages: jest.fn((_state) => undefined), checkIntegrity: jest.fn((_state) => []), isTimeBased: jest.fn(), + isValidColumn: jest.fn(), }; } diff --git a/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap b/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap index 57da18d9dc92f3..0c92267382053c 100644 --- a/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap +++ b/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap @@ -18,7 +18,7 @@ Object { "isFullscreenDatasource": false, "isLinkedToOriginatingApp": false, "isLoading": false, - "isSaveable": false, + "isSaveable": true, "persistedDoc": Object { "exactMatchDoc": Object { "expression": "definitely a valid expression", diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index 5c571c750a4aa5..915c56d59dbb32 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -6,10 +6,10 @@ */ import { MiddlewareAPI } from '@reduxjs/toolkit'; -import { isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; import { History } from 'history'; -import { LensAppState, setState, initEmpty, LensStoreDeps } from '..'; +import { setState, initEmpty, LensStoreDeps } from '..'; +import { getPreloadedState } from '../lens_slice'; import { SharingSavedObjectProps } from '../../types'; import { LensEmbeddableInput, LensByReferenceInput } from '../../embeddable/embeddable'; import { getInitialDatasourceId } from '../../utils'; @@ -83,19 +83,20 @@ export const getPersisted = async ({ export function loadInitial( store: MiddlewareAPI, - { lensServices, datasourceMap, embeddableEditorIncomingState, initialContext }: LensStoreDeps, + storeDeps: LensStoreDeps, { redirectCallback, initialInput, - emptyState, history, }: { redirectCallback: (savedObjectId?: string) => void; initialInput?: LensEmbeddableInput; - emptyState?: LensAppState; history?: History; } ) { + const { lensServices, datasourceMap, embeddableEditorIncomingState, initialContext } = storeDeps; + const { resolvedDateRange, searchSessionId, isLinkedToOriginatingApp, ...emptyState } = + getPreloadedState(storeDeps); const { attributeService, notifications, data, dashboardFeatureFlag } = lensServices; const currentSessionId = data.search.session.getSessionId(); @@ -150,10 +151,6 @@ export function loadInitial( initialInput.savedObjectId ); } - // Don't overwrite any pinned filters - data.query.filterManager.setAppFilters( - injectFilterReferences(doc.state.filters, doc.references) - ); const docDatasourceStates = Object.entries(doc.state.datasourceStates).reduce( (stateMap, [datasourceId, datasourceState]) => ({ @@ -166,6 +163,10 @@ export function loadInitial( {} ); + const filters = injectFilterReferences(doc.state.filters, doc.references); + // Don't overwrite any pinned filters + data.query.filterManager.setAppFilters(filters); + initializeDatasources( datasourceMap, docDatasourceStates, @@ -178,7 +179,9 @@ export function loadInitial( .then((result) => { store.dispatch( setState({ + isSaveable: true, sharingSavedObjectProps, + filters, query: doc.state.query, searchSessionId: dashboardFeatureFlag.allowByValueEmbeddables && @@ -187,7 +190,7 @@ export function loadInitial( currentSessionId ? currentSessionId : data.search.session.start(), - ...(!isEqual(lens.persistedDoc, doc) ? { persistedDoc: doc } : null), + persistedDoc: doc, activeDatasourceId: getInitialDatasourceId(datasourceMap, doc), visualization: { activeId: doc.visualizationType, diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 0461070020055d..df178cadf6c300 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -55,9 +55,11 @@ export const getPreloadedState = ({ const state = { ...initialState, isLoading: true, - query: data.query.queryString.getQuery(), // Do not use app-specific filters from previous app, // only if Lens was opened with the intention to visualize a field (e.g. coming from Discover) + query: !initialContext + ? data.query.queryString.getDefaultQuery() + : data.query.queryString.getQuery(), filters: !initialContext ? data.query.filterManager.getGlobalFilters() : data.query.filterManager.getFilters(), @@ -117,7 +119,6 @@ export const navigateAway = createAction('lens/navigateAway'); export const loadInitial = createAction<{ initialInput?: LensEmbeddableInput; redirectCallback: (savedObjectId?: string) => void; - emptyState: LensAppState; history: History; }>('lens/loadInitial'); export const initEmpty = createAction( @@ -356,7 +357,6 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { payload: PayloadAction<{ initialInput?: LensEmbeddableInput; redirectCallback: (savedObjectId?: string) => void; - emptyState: LensAppState; history: History; }> ) => state, diff --git a/x-pack/plugins/lens/public/state_management/load_initial.test.tsx b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx index fe4c553ce4bd7a..ac27ca4398326c 100644 --- a/x-pack/plugins/lens/public/state_management/load_initial.test.tsx +++ b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx @@ -16,8 +16,7 @@ import { import { Location, History } from 'history'; import { act } from 'react-dom/test-utils'; import { LensEmbeddableInput } from '../embeddable'; -import { getPreloadedState, initialState, loadInitial } from './lens_slice'; -import { LensAppState } from '.'; +import { loadInitial } from './lens_slice'; const history = { location: { @@ -38,7 +37,6 @@ const defaultProps = { redirectCallback: jest.fn(), initialInput: { savedObjectId: defaultSavedObjectId } as unknown as LensEmbeddableInput, history, - emptyState: initialState, }; describe('Initializing the store', () => { @@ -52,9 +50,8 @@ describe('Initializing the store', () => { it('should have initialized the initial datasource and visualization', async () => { const { store, deps } = await makeLensStore({ preloadedState }); - const emptyState = getPreloadedState(deps) as LensAppState; await act(async () => { - await store.dispatch(loadInitial({ ...defaultProps, initialInput: undefined, emptyState })); + await store.dispatch(loadInitial({ ...defaultProps, initialInput: undefined })); }); expect(deps.datasourceMap.testDatasource.initialize).toHaveBeenCalled(); expect(deps.datasourceMap.testDatasource2.initialize).not.toHaveBeenCalled(); @@ -187,13 +184,10 @@ describe('Initializing the store', () => { }), }); - const emptyState = getPreloadedState(deps) as LensAppState; - await act(async () => { await store.dispatch( loadInitial({ ...defaultProps, - emptyState, initialInput: undefined, }) ); diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 87e2762149acd2..e207f2938dd3cf 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -281,6 +281,10 @@ export interface Datasource { * Checks if the visualization created is time based, for example date histogram */ isTimeBased: (state: T) => boolean; + /** + * Given the current state layer and a columnId will verify if the column configuration has errors + */ + isValidColumn: (state: T, layerId: string, columnId: string) => boolean; } export interface DatasourceFixAction { diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/__snapshots__/layer_template.test.tsx.snap b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/__snapshots__/layer_template.test.tsx.snap index 3a301a951ed571..47dadb1246b38a 100644 --- a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/__snapshots__/layer_template.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/__snapshots__/layer_template.test.tsx.snap @@ -32,7 +32,7 @@ exports[`should render EMS UI when left source is BOUNDARIES_SOURCE.EMS 1`] = ` Array [ Object { "id": "EMS", - "label": "Administrative boundaries from Elastic Maps Service", + "label": "Administrative boundaries from the Elastic Maps Service", }, Object { "id": "ELASTICSEARCH", @@ -85,7 +85,7 @@ exports[`should render elasticsearch UI when left source is BOUNDARIES_SOURCE.EL Array [ Object { "id": "EMS", - "label": "Administrative boundaries from Elastic Maps Service", + "label": "Administrative boundaries from the Elastic Maps Service", }, Object { "id": "ELASTICSEARCH", diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx index 5bd2b68e61bc4f..dfca19dbb964b4 100644 --- a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx +++ b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx @@ -40,7 +40,7 @@ const BOUNDARIES_OPTIONS = [ { id: BOUNDARIES_SOURCE.EMS, label: i18n.translate('xpack.maps.choropleth.boundaries.ems', { - defaultMessage: 'Administrative boundaries from Elastic Maps Service', + defaultMessage: 'Administrative boundaries from the Elastic Maps Service', }), }, { diff --git a/x-pack/plugins/maps/server/tutorials/ems/index.ts b/x-pack/plugins/maps/server/tutorials/ems/index.ts index 94da7c6258faa0..ba8720a7bc8eb3 100644 --- a/x-pack/plugins/maps/server/tutorials/ems/index.ts +++ b/x-pack/plugins/maps/server/tutorials/ems/index.ts @@ -61,11 +61,11 @@ export function emsBoundariesSpecProvider({ return () => ({ id: 'emsBoundaries', name: i18n.translate('xpack.maps.tutorials.ems.nameTitle', { - defaultMessage: 'EMS Boundaries', + defaultMessage: 'Elastic Maps Service', }), category: TutorialsCategory.OTHER, shortDescription: i18n.translate('xpack.maps.tutorials.ems.shortDescription', { - defaultMessage: 'Administrative boundaries from Elastic Maps Service.', + defaultMessage: 'Administrative boundaries from the Elastic Maps Service.', }), longDescription: i18n.translate('xpack.maps.tutorials.ems.longDescription', { defaultMessage: diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json index 4f8e1c0bdbae4f..bc0cf471815856 100644 --- a/x-pack/plugins/monitoring/kibana.json +++ b/x-pack/plugins/monitoring/kibana.json @@ -25,7 +25,6 @@ "home", "alerting", "kibanaReact", - "licenseManagement", "kibanaLegacy" ] } diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx index 6b1c8c50855659..22bffb5d62b195 100644 --- a/x-pack/plugins/monitoring/public/alerts/badge.tsx +++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx @@ -73,7 +73,7 @@ export const AlertsBadge: React.FC = (props: Props) => { const groupByType = GROUP_BY_NODE; const panels = showByNode ? getAlertPanelsByNode(PANEL_TITLE, alerts, stateFilter) - : getAlertPanelsByCategory(PANEL_TITLE, inSetupMode, alerts, stateFilter); + : getAlertPanelsByCategory(PANEL_TITLE, !!inSetupMode, alerts, stateFilter); if (panels.length && !inSetupMode && panels[0].items) { panels[0].items.push( ...[ diff --git a/x-pack/plugins/monitoring/public/angular/app_modules.ts b/x-pack/plugins/monitoring/public/angular/app_modules.ts deleted file mode 100644 index 6ded0bce51d4b8..00000000000000 --- a/x-pack/plugins/monitoring/public/angular/app_modules.ts +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import angular, { IWindowService } from 'angular'; -import '../views/all'; -// required for `ngSanitize` angular module -import 'angular-sanitize'; -import 'angular-route'; -import '../index.scss'; -import { upperFirst } from 'lodash'; -import { CoreStart } from 'kibana/public'; -import { i18nDirective, i18nFilter, I18nProvider } from './angular_i18n'; -import { Storage } from '../../../../../src/plugins/kibana_utils/public'; -import { createTopNavDirective, createTopNavHelper } from './top_nav'; -import { MonitoringStartPluginDependencies } from '../types'; -import { GlobalState } from '../url_state'; -import { getSafeForExternalLink } from '../lib/get_safe_for_external_link'; - -// @ts-ignore -import { formatMetric, formatNumber } from '../lib/format_number'; -// @ts-ignore -import { extractIp } from '../lib/extract_ip'; -// @ts-ignore -import { PrivateProvider } from './providers/private'; -// @ts-ignore -import { breadcrumbsProvider } from '../services/breadcrumbs'; -// @ts-ignore -import { monitoringClustersProvider } from '../services/clusters'; -// @ts-ignore -import { executorProvider } from '../services/executor'; -// @ts-ignore -import { featuresProvider } from '../services/features'; -// @ts-ignore -import { licenseProvider } from '../services/license'; -// @ts-ignore -import { titleProvider } from '../services/title'; -// @ts-ignore -import { enableAlertsModalProvider } from '../services/enable_alerts_modal'; -// @ts-ignore -import { monitoringMlListingProvider } from '../directives/elasticsearch/ml_job_listing'; -// @ts-ignore -import { monitoringMainProvider } from '../directives/main'; - -export const appModuleName = 'monitoring'; - -type IPrivate = (provider: (...injectable: unknown[]) => T) => T; - -const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react']; - -export const localAppModule = ({ - core, - data: { query }, - navigation, - externalConfig, -}: MonitoringStartPluginDependencies) => { - createLocalI18nModule(); - createLocalPrivateModule(); - createLocalStorage(); - createLocalConfigModule(core); - createLocalStateModule(query, core.notifications.toasts); - createLocalTopNavModule(navigation); - createHrefModule(core); - createMonitoringAppServices(); - createMonitoringAppDirectives(); - createMonitoringAppConfigConstants(externalConfig); - createMonitoringAppFilters(); - - const appModule = angular.module(appModuleName, [ - ...thirdPartyAngularDependencies, - 'monitoring/I18n', - 'monitoring/Private', - 'monitoring/Storage', - 'monitoring/Config', - 'monitoring/State', - 'monitoring/TopNav', - 'monitoring/href', - 'monitoring/constants', - 'monitoring/services', - 'monitoring/filters', - 'monitoring/directives', - ]); - return appModule; -}; - -function createMonitoringAppConfigConstants( - keys: MonitoringStartPluginDependencies['externalConfig'] -) { - let constantsModule = angular.module('monitoring/constants', []); - keys.map(([key, value]) => (constantsModule = constantsModule.constant(key as string, value))); -} - -function createLocalStateModule( - query: MonitoringStartPluginDependencies['data']['query'], - toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts'] -) { - angular - .module('monitoring/State', ['monitoring/Private']) - .service( - 'globalState', - function ( - Private: IPrivate, - $rootScope: ng.IRootScopeService, - $location: ng.ILocationService - ) { - function GlobalStateProvider(this: any) { - const state = new GlobalState(query, toasts, $rootScope, $location, this); - const initialState: any = state.getState(); - for (const key in initialState) { - if (!initialState.hasOwnProperty(key)) { - continue; - } - this[key] = initialState[key]; - } - this.save = () => { - const newState = { ...this }; - delete newState.save; - state.setState(newState); - }; - } - return Private(GlobalStateProvider); - } - ); -} - -function createMonitoringAppServices() { - angular - .module('monitoring/services', ['monitoring/Private']) - .service('breadcrumbs', function (Private: IPrivate) { - return Private(breadcrumbsProvider); - }) - .service('monitoringClusters', function (Private: IPrivate) { - return Private(monitoringClustersProvider); - }) - .service('$executor', function (Private: IPrivate) { - return Private(executorProvider); - }) - .service('features', function (Private: IPrivate) { - return Private(featuresProvider); - }) - .service('enableAlertsModal', function (Private: IPrivate) { - return Private(enableAlertsModalProvider); - }) - .service('license', function (Private: IPrivate) { - return Private(licenseProvider); - }) - .service('title', function (Private: IPrivate) { - return Private(titleProvider); - }); -} - -function createMonitoringAppDirectives() { - angular - .module('monitoring/directives', []) - .directive('monitoringMlListing', monitoringMlListingProvider) - .directive('monitoringMain', monitoringMainProvider); -} - -function createMonitoringAppFilters() { - angular - .module('monitoring/filters', []) - .filter('capitalize', function () { - return function (input: string) { - return upperFirst(input?.toLowerCase()); - }; - }) - .filter('formatNumber', function () { - return formatNumber; - }) - .filter('formatMetric', function () { - return formatMetric; - }) - .filter('extractIp', function () { - return extractIp; - }); -} - -function createLocalConfigModule(core: MonitoringStartPluginDependencies['core']) { - angular.module('monitoring/Config', []).provider('config', function () { - return { - $get: () => ({ - get: (key: string) => core.uiSettings?.get(key), - }), - }; - }); -} - -function createLocalStorage() { - angular - .module('monitoring/Storage', []) - .service('localStorage', function ($window: IWindowService) { - return new Storage($window.localStorage); - }) - .service('sessionStorage', function ($window: IWindowService) { - return new Storage($window.sessionStorage); - }) - .service('sessionTimeout', function () { - return {}; - }); -} - -function createLocalPrivateModule() { - angular.module('monitoring/Private', []).provider('Private', PrivateProvider); -} - -function createLocalTopNavModule({ ui }: MonitoringStartPluginDependencies['navigation']) { - angular - .module('monitoring/TopNav', ['react']) - .directive('kbnTopNav', createTopNavDirective) - .directive('kbnTopNavHelper', createTopNavHelper(ui)); -} - -function createLocalI18nModule() { - angular - .module('monitoring/I18n', []) - .provider('i18n', I18nProvider) - .filter('i18n', i18nFilter) - .directive('i18nId', i18nDirective); -} - -function createHrefModule(core: CoreStart) { - const name: string = 'kbnHref'; - angular.module('monitoring/href', []).directive(name, function () { - return { - restrict: 'A', - link: { - pre: (_$scope, _$el, $attr) => { - $attr.$observe(name, (val) => { - if (val) { - const url = getSafeForExternalLink(val as string); - $attr.$set('href', core.http.basePath.prepend(url)); - } - }); - - _$scope.$on('$locationChangeSuccess', () => { - const url = getSafeForExternalLink($attr.href as string); - $attr.$set('href', core.http.basePath.prepend(url)); - }); - }, - }, - }; - }); -} diff --git a/x-pack/plugins/monitoring/public/angular/helpers/routes.ts b/x-pack/plugins/monitoring/public/angular/helpers/routes.ts deleted file mode 100644 index 2579e522882a26..00000000000000 --- a/x-pack/plugins/monitoring/public/angular/helpers/routes.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -type RouteObject = [string, { reloadOnSearch: boolean }]; -interface Redirect { - redirectTo: string; -} - -class Routes { - private routes: RouteObject[] = []; - public redirect?: Redirect = { redirectTo: '/no-data' }; - - public when = (...args: RouteObject) => { - const [, routeOptions] = args; - routeOptions.reloadOnSearch = false; - this.routes.push(args); - return this; - }; - - public otherwise = (redirect: Redirect) => { - this.redirect = redirect; - return this; - }; - - public addToProvider = ($routeProvider: any) => { - this.routes.forEach((args) => { - $routeProvider.when.apply(this, args); - }); - - if (this.redirect) { - $routeProvider.otherwise(this.redirect); - } - }; -} -export const uiRoutes = new Routes(); diff --git a/x-pack/plugins/monitoring/public/angular/helpers/utils.ts b/x-pack/plugins/monitoring/public/angular/helpers/utils.ts deleted file mode 100644 index 32184ad71ed8d0..00000000000000 --- a/x-pack/plugins/monitoring/public/angular/helpers/utils.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { IScope } from 'angular'; -import * as Rx from 'rxjs'; - -/** - * Subscribe to an observable at a $scope, ensuring that the digest cycle - * is run for subscriber hooks and routing errors to fatalError if not handled. - */ -export const subscribeWithScope = ( - $scope: IScope, - observable: Rx.Observable, - observer?: Rx.PartialObserver -) => { - return observable.subscribe({ - next(value) { - if (observer && observer.next) { - $scope.$applyAsync(() => observer.next!(value)); - } - }, - error(error) { - $scope.$applyAsync(() => { - if (observer && observer.error) { - observer.error(error); - } else { - throw new Error( - `Uncaught error in subscribeWithScope(): ${ - error ? error.stack || error.message : error - }` - ); - } - }); - }, - complete() { - if (observer && observer.complete) { - $scope.$applyAsync(() => observer.complete!()); - } - }, - }); -}; diff --git a/x-pack/plugins/monitoring/public/angular/index.ts b/x-pack/plugins/monitoring/public/angular/index.ts deleted file mode 100644 index 1a655fc1ee256b..00000000000000 --- a/x-pack/plugins/monitoring/public/angular/index.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import angular, { IModule } from 'angular'; -import { uiRoutes } from './helpers/routes'; -import { Legacy } from '../legacy_shims'; -import { configureAppAngularModule } from '../angular/top_nav'; -import { localAppModule, appModuleName } from './app_modules'; -import { APP_WRAPPER_CLASS } from '../../../../../src/core/public'; - -import { MonitoringStartPluginDependencies } from '../types'; - -export class AngularApp { - private injector?: angular.auto.IInjectorService; - - constructor(deps: MonitoringStartPluginDependencies) { - const { - core, - element, - data, - navigation, - isCloud, - pluginInitializerContext, - externalConfig, - triggersActionsUi, - usageCollection, - appMountParameters, - } = deps; - const app: IModule = localAppModule(deps); - app.run(($injector: angular.auto.IInjectorService) => { - this.injector = $injector; - Legacy.init( - { - core, - element, - data, - navigation, - isCloud, - pluginInitializerContext, - externalConfig, - triggersActionsUi, - usageCollection, - appMountParameters, - }, - this.injector - ); - }); - - app.config(($routeProvider: unknown) => uiRoutes.addToProvider($routeProvider)); - - const np = { core, env: pluginInitializerContext.env }; - configureAppAngularModule(app, np, true); - const appElement = document.createElement('div'); - appElement.setAttribute('style', 'height: 100%'); - appElement.innerHTML = '
'; - - if (!element.classList.contains(APP_WRAPPER_CLASS)) { - element.classList.add(APP_WRAPPER_CLASS); - } - - angular.bootstrap(appElement, [appModuleName]); - angular.element(element).append(appElement); - } - - public destroy = () => { - if (this.injector) { - this.injector.get('$rootScope').$destroy(); - } - }; - - public applyScope = () => { - if (!this.injector) { - return; - } - - const rootScope = this.injector.get('$rootScope'); - rootScope.$applyAsync(); - }; -} diff --git a/x-pack/plugins/monitoring/public/angular/providers/private.js b/x-pack/plugins/monitoring/public/angular/providers/private.js deleted file mode 100644 index 018e2d7d418409..00000000000000 --- a/x-pack/plugins/monitoring/public/angular/providers/private.js +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** - * # `Private()` - * Private module loader, used to merge angular and require js dependency styles - * by allowing a require.js module to export a single provider function that will - * create a value used within an angular application. This provider can declare - * angular dependencies by listing them as arguments, and can be require additional - * Private modules. - * - * ## Define a private module provider: - * ```js - * export default function PingProvider($http) { - * this.ping = function () { - * return $http.head('/health-check'); - * }; - * }; - * ``` - * - * ## Require a private module: - * ```js - * export default function ServerHealthProvider(Private, Promise) { - * let ping = Private(require('ui/ping')); - * return { - * check: Promise.method(function () { - * let attempts = 0; - * return (function attempt() { - * attempts += 1; - * return ping.ping() - * .catch(function (err) { - * if (attempts < 3) return attempt(); - * }) - * }()) - * .then(function () { - * return true; - * }) - * .catch(function () { - * return false; - * }); - * }) - * } - * }; - * ``` - * - * # `Private.stub(provider, newInstance)` - * `Private.stub()` replaces the instance of a module with another value. This is all we have needed until now. - * - * ```js - * beforeEach(inject(function ($injector, Private) { - * Private.stub( - * // since this module just exports a function, we need to change - * // what Private returns in order to modify it's behavior - * require('ui/agg_response/hierarchical/_build_split'), - * sinon.stub().returns(fakeSplit) - * ); - * })); - * ``` - * - * # `Private.swap(oldProvider, newProvider)` - * This new method does an 1-for-1 swap of module providers, unlike `stub()` which replaces a modules instance. - * Pass the module you want to swap out, and the one it should be replaced with, then profit. - * - * Note: even though this example shows `swap()` being called in a config - * function, it can be called from anywhere. It is particularly useful - * in this scenario though. - * - * ```js - * beforeEach(module('kibana', function (PrivateProvider) { - * PrivateProvider.swap( - * function StubbedRedirectProvider($decorate) { - * // $decorate is a function that will instantiate the original module when called - * return sinon.spy($decorate()); - * } - * ); - * })); - * ``` - * - * @param {[type]} prov [description] - */ -import { partial, uniqueId, isObject } from 'lodash'; - -const nextId = partial(uniqueId, 'privateProvider#'); - -function name(fn) { - return fn.name || fn.toString().split('\n').shift(); -} - -export function PrivateProvider() { - const provider = this; - - // one cache/swaps per Provider - const cache = {}; - const swaps = {}; - - // return the uniq id for this function - function identify(fn) { - if (typeof fn !== 'function') { - throw new TypeError('Expected private module "' + fn + '" to be a function'); - } - - if (fn.$$id) return fn.$$id; - else return (fn.$$id = nextId()); - } - - provider.stub = function (fn, instance) { - cache[identify(fn)] = instance; - return instance; - }; - - provider.swap = function (fn, prov) { - const id = identify(fn); - swaps[id] = prov; - }; - - provider.$get = [ - '$injector', - function PrivateFactory($injector) { - // prevent circular deps by tracking where we came from - const privPath = []; - const pathToString = function () { - return privPath.map(name).join(' -> '); - }; - - // call a private provider and return the instance it creates - function instantiate(prov, locals) { - if (~privPath.indexOf(prov)) { - throw new Error( - 'Circular reference to "' + - name(prov) + - '"' + - ' found while resolving private deps: ' + - pathToString() - ); - } - - privPath.push(prov); - - const context = {}; - let instance = $injector.invoke(prov, context, locals); - if (!isObject(instance)) instance = context; - - privPath.pop(); - return instance; - } - - // retrieve an instance from cache or create and store on - function get(id, prov, $delegateId, $delegateProv) { - if (cache[id]) return cache[id]; - - let instance; - - if ($delegateId != null && $delegateProv != null) { - instance = instantiate(prov, { - $decorate: partial(get, $delegateId, $delegateProv), - }); - } else { - instance = instantiate(prov); - } - - return (cache[id] = instance); - } - - // main api, get the appropriate instance for a provider - function Private(prov) { - let id = identify(prov); - let $delegateId; - let $delegateProv; - - if (swaps[id]) { - $delegateId = id; - $delegateProv = prov; - - prov = swaps[$delegateId]; - id = identify(prov); - } - - return get(id, prov, $delegateId, $delegateProv); - } - - Private.stub = provider.stub; - Private.swap = provider.swap; - - return Private; - }, - ]; - - return provider; -} diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/pipelines.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/pipelines.tsx index c2dfe1c0dae7da..2a2de0a716cea8 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/pipelines.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/pipelines.tsx @@ -34,12 +34,12 @@ export const LogStashPipelinesPage: React.FC = ({ clusters }) => const { getPaginationTableProps, getPaginationRouteOptions, updateTotalItemCount } = useTable('logstash.pipelines'); - const title = i18n.translate('xpack.monitoring.logstash.overview.title', { - defaultMessage: 'Logstash', + const title = i18n.translate('xpack.monitoring.logstash.pipelines.routeTitle', { + defaultMessage: 'Logstash Pipelines', }); - const pageTitle = i18n.translate('xpack.monitoring.logstash.overview.pageTitle', { - defaultMessage: 'Logstash overview', + const pageTitle = i18n.translate('xpack.monitoring.logstash.pipelines.pageTitle', { + defaultMessage: 'Logstash pipelines', }); const getPageData = useCallback(async () => { diff --git a/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx b/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx index e798e7d74ad38e..e767074aea42b5 100644 --- a/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx @@ -17,7 +17,7 @@ import { CODE_PATH_LICENSE, STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../ import { Legacy } from '../../../legacy_shims'; import { Enabler } from './enabler'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; -import { initSetupModeState } from '../../setup_mode/setup_mode'; +import { initSetupModeState } from '../../../lib/setup_mode'; import { GlobalStateContext } from '../../contexts/global_state_context'; import { useRequestErrorHandler } from '../../hooks/use_request_error_handler'; diff --git a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx index 23eeb2c034a80f..c0030cfcfe55c2 100644 --- a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx @@ -17,7 +17,7 @@ import { getSetupModeState, isSetupModeFeatureEnabled, updateSetupModeData, -} from '../setup_mode/setup_mode'; +} from '../../lib/setup_mode'; import { SetupModeFeature } from '../../../common/enums'; import { AlertsDropdown } from '../../alerts/alerts_dropdown'; import { ActionMenu } from '../../components/action_menu'; diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/index.ts b/x-pack/plugins/monitoring/public/application/setup_mode/index.ts index 1bcdcdef09c281..57d734fc6d056d 100644 --- a/x-pack/plugins/monitoring/public/application/setup_mode/index.ts +++ b/x-pack/plugins/monitoring/public/application/setup_mode/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export * from './setup_mode'; +export * from '../../lib/setup_mode'; diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx deleted file mode 100644 index 828d5a2d20ae6b..00000000000000 --- a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render } from 'react-dom'; -import { get, includes } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { HttpStart, IHttpFetchError } from 'kibana/public'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { Legacy } from '../../legacy_shims'; -import { SetupModeEnterButton } from '../../components/setup_mode/enter_button'; -import { SetupModeFeature } from '../../../common/enums'; -import { ISetupModeContext } from '../../components/setup_mode/setup_mode_context'; -import { State as GlobalState } from '../contexts/global_state_context'; - -function isOnPage(hash: string) { - return includes(window.location.hash, hash); -} - -let globalState: GlobalState; -let httpService: HttpStart; -let errorHandler: (error: IHttpFetchError) => void; - -interface ISetupModeState { - enabled: boolean; - data: any; - callback?: (() => void) | null; - hideBottomBar: boolean; -} -const setupModeState: ISetupModeState = { - enabled: false, - data: null, - callback: null, - hideBottomBar: false, -}; - -export const getSetupModeState = () => setupModeState; - -export const setNewlyDiscoveredClusterUuid = (clusterUuid: string) => { - globalState.cluster_uuid = clusterUuid; - globalState.save?.(); -}; - -export const fetchCollectionData = async (uuid?: string, fetchWithoutClusterUuid = false) => { - const clusterUuid = globalState.cluster_uuid; - const ccs = globalState.ccs; - - let url = '../api/monitoring/v1/setup/collection'; - if (uuid) { - url += `/node/${uuid}`; - } else if (!fetchWithoutClusterUuid && clusterUuid) { - url += `/cluster/${clusterUuid}`; - } else { - url += '/cluster'; - } - - try { - const response = await httpService.post(url, { - body: JSON.stringify({ - ccs, - }), - }); - return response; - } catch (err) { - errorHandler(err); - throw err; - } -}; - -const notifySetupModeDataChange = () => setupModeState.callback && setupModeState.callback(); - -export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid = false) => { - const data = await fetchCollectionData(uuid, fetchWithoutClusterUuid); - setupModeState.data = data; - const hasPermissions = get(data, '_meta.hasPermissions', false); - if (!hasPermissions) { - let text: string = ''; - if (!hasPermissions) { - text = i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { - defaultMessage: 'You do not have the necessary permissions to do this.', - }); - } - - Legacy.shims.toastNotifications.addDanger({ - title: i18n.translate('xpack.monitoring.setupMode.notAvailableTitle', { - defaultMessage: 'Setup mode is not available', - }), - text, - }); - return toggleSetupMode(false); - } - notifySetupModeDataChange(); - - const clusterUuid = globalState.cluster_uuid; - if (!clusterUuid) { - const liveClusterUuid: string = get(data, '_meta.liveClusterUuid'); - const migratedEsNodes = Object.values(get(data, 'elasticsearch.byUuid', {})).filter( - (node: any) => node.isPartiallyMigrated || node.isFullyMigrated - ); - if (liveClusterUuid && migratedEsNodes.length > 0) { - setNewlyDiscoveredClusterUuid(liveClusterUuid); - } - } -}; - -export const hideBottomBar = () => { - setupModeState.hideBottomBar = true; - notifySetupModeDataChange(); -}; -export const showBottomBar = () => { - setupModeState.hideBottomBar = false; - notifySetupModeDataChange(); -}; - -export const disableElasticsearchInternalCollection = async () => { - const clusterUuid = globalState.cluster_uuid; - const url = `../api/monitoring/v1/setup/collection/${clusterUuid}/disable_internal_collection`; - try { - const response = await httpService.post(url); - return response; - } catch (err) { - errorHandler(err); - throw err; - } -}; - -export const toggleSetupMode = (inSetupMode: boolean) => { - setupModeState.enabled = inSetupMode; - globalState.inSetupMode = inSetupMode; - globalState.save?.(); - setSetupModeMenuItem(); - notifySetupModeDataChange(); - - if (inSetupMode) { - // Intentionally do not await this so we don't block UI operations - updateSetupModeData(); - } -}; - -export const setSetupModeMenuItem = () => { - if (isOnPage('no-data')) { - return; - } - - const enabled = !globalState.inSetupMode; - const I18nContext = Legacy.shims.I18nContext; - - render( - - - - - , - document.getElementById('setupModeNav') - ); -}; - -export const initSetupModeState = async ( - state: GlobalState, - http: HttpStart, - handleErrors: (error: IHttpFetchError) => void, - callback?: () => void -) => { - globalState = state; - httpService = http; - errorHandler = handleErrors; - if (callback) { - setupModeState.callback = callback; - } - - if (globalState.inSetupMode) { - toggleSetupMode(true); - } -}; - -export const isInSetupMode = (context?: ISetupModeContext, gState: GlobalState = globalState) => { - if (context?.setupModeSupported === false) { - return false; - } - if (setupModeState.enabled) { - return true; - } - - return gState.inSetupMode; -}; - -export const isSetupModeFeatureEnabled = (feature: SetupModeFeature) => { - if (!setupModeState.enabled) { - return false; - } - - if (feature === SetupModeFeature.MetricbeatMigration) { - if (Legacy.shims.isCloud) { - return false; - } - } - - return true; -}; diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js index a9ee2464cd4233..df524fa99ae536 100644 --- a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js +++ b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js @@ -13,7 +13,7 @@ import { disableElasticsearchInternalCollection, toggleSetupMode, setSetupModeMenuItem, -} from './setup_mode'; +} from '../../lib/setup_mode'; import { Flyout } from '../../components/metricbeat_migration/flyout'; import { EuiBottomBar, diff --git a/x-pack/plugins/monitoring/public/components/no_data/checking_settings.js b/x-pack/plugins/monitoring/public/components/no_data/checking_settings.js index 86a7537c2e6613..d55f2587950af6 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/checking_settings.js +++ b/x-pack/plugins/monitoring/public/components/no_data/checking_settings.js @@ -15,18 +15,18 @@ export function CheckingSettings({ checkMessage }) { const message = checkMessage || ( ); return ( - + - {message}... + {message} ); diff --git a/x-pack/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js b/x-pack/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js deleted file mode 100644 index 69579cb831c06f..00000000000000 --- a/x-pack/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { capitalize } from 'lodash'; -import numeral from '@elastic/numeral'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { EuiMonitoringTable } from '../../../components/table'; -import { MachineLearningJobStatusIcon } from '../../../components/elasticsearch/ml_job_listing/status_icon'; -import { LARGE_ABBREVIATED, LARGE_BYTES } from '../../../../common/formatting'; -import { EuiLink, EuiPage, EuiPageContent, EuiPageBody, EuiPanel, EuiSpacer } from '@elastic/eui'; -import { ClusterStatus } from '../../../components/elasticsearch/cluster_status'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; - -const getColumns = () => [ - { - name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.jobIdTitle', { - defaultMessage: 'Job ID', - }), - field: 'job_id', - sortable: true, - }, - { - name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.stateTitle', { - defaultMessage: 'State', - }), - field: 'state', - sortable: true, - render: (state) => ( -
- -   - {capitalize(state)} -
- ), - }, - { - name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.processedRecordsTitle', { - defaultMessage: 'Processed Records', - }), - field: 'data_counts.processed_record_count', - sortable: true, - render: (value) => {numeral(value).format(LARGE_ABBREVIATED)}, - }, - { - name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.modelSizeTitle', { - defaultMessage: 'Model Size', - }), - field: 'model_size_stats.model_bytes', - sortable: true, - render: (value) => {numeral(value).format(LARGE_BYTES)}, - }, - { - name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.forecastsTitle', { - defaultMessage: 'Forecasts', - }), - field: 'forecasts_stats.total', - sortable: true, - render: (value) => {numeral(value).format(LARGE_ABBREVIATED)}, - }, - { - name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.nodeTitle', { - defaultMessage: 'Node', - }), - field: 'node.name', - sortable: true, - render: (name, node) => { - if (node) { - return ( - - {name} - - ); - } - - return ( - - ); - }, - }, -]; - -//monitoringMlListing -export function monitoringMlListingProvider() { - return { - restrict: 'E', - scope: { - jobs: '=', - paginationSettings: '=', - sorting: '=', - onTableChange: '=', - status: '=', - }, - link(scope, $el) { - scope.$on('$destroy', () => $el && $el[0] && unmountComponentAtNode($el[0])); - const columns = getColumns(); - - const filterJobsPlaceholder = i18n.translate( - 'xpack.monitoring.elasticsearch.mlJobListing.filterJobsPlaceholder', - { - defaultMessage: 'Filter Jobs…', - } - ); - - scope.$watch('jobs', (_jobs = []) => { - const jobs = _jobs.map((job) => { - if (job.ml) { - return { - ...job.ml.job, - node: job.node, - job_id: job.ml.job.id, - }; - } - return job; - }); - const mlTable = ( - - - - - - - - - - - - ); - render(mlTable, $el[0]); - }); - }, - }; -} diff --git a/x-pack/plugins/monitoring/public/directives/main/index.html b/x-pack/plugins/monitoring/public/directives/main/index.html deleted file mode 100644 index fd14120e1db2fc..00000000000000 --- a/x-pack/plugins/monitoring/public/directives/main/index.html +++ /dev/null @@ -1,323 +0,0 @@ -
-
-
-
-
-
-
-

{{pageTitle || monitoringMain.instance}}

-
-
-
-
-
- - -
-
-
- - - - - - - - - - - - - - - -
-
-
diff --git a/x-pack/plugins/monitoring/public/directives/main/index.js b/x-pack/plugins/monitoring/public/directives/main/index.js deleted file mode 100644 index 0e464f0a356c43..00000000000000 --- a/x-pack/plugins/monitoring/public/directives/main/index.js +++ /dev/null @@ -1,275 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { EuiSelect, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; -import template from './index.html'; -import { Legacy } from '../../legacy_shims'; -import { shortenPipelineHash } from '../../../common/formatting'; -import { - getSetupModeState, - initSetupModeState, - isSetupModeFeatureEnabled, -} from '../../lib/setup_mode'; -import { Subscription } from 'rxjs'; -import { getSafeForExternalLink } from '../../lib/get_safe_for_external_link'; -import { SetupModeFeature } from '../../../common/enums'; -import './index.scss'; - -const setOptions = (controller) => { - if ( - !controller.pipelineVersions || - !controller.pipelineVersions.length || - !controller.pipelineDropdownElement - ) { - return; - } - - render( - - - { - return { - text: i18n.translate( - 'xpack.monitoring.logstashNavigation.pipelineVersionDescription', - { - defaultMessage: - 'Version active {relativeLastSeen} and first seen {relativeFirstSeen}', - values: { - relativeLastSeen: option.relativeLastSeen, - relativeFirstSeen: option.relativeFirstSeen, - }, - } - ), - value: option.hash, - }; - })} - onChange={controller.onChangePipelineHash} - /> - - , - controller.pipelineDropdownElement - ); -}; - -/* - * Manage data and provide helper methods for the "main" directive's template - */ -export class MonitoringMainController { - // called internally by Angular - constructor() { - this.inListing = false; - this.inAlerts = false; - this.inOverview = false; - this.inElasticsearch = false; - this.inKibana = false; - this.inLogstash = false; - this.inBeats = false; - this.inApm = false; - } - - addTimerangeObservers = () => { - const timefilter = Legacy.shims.timefilter; - this.subscriptions = new Subscription(); - - const refreshIntervalUpdated = () => { - const { value: refreshInterval, pause: isPaused } = timefilter.getRefreshInterval(); - this.datePicker.onRefreshChange({ refreshInterval, isPaused }, true); - }; - - const timeUpdated = () => { - this.datePicker.onTimeUpdate({ dateRange: timefilter.getTime() }, true); - }; - - this.subscriptions.add( - timefilter.getRefreshIntervalUpdate$().subscribe(refreshIntervalUpdated) - ); - this.subscriptions.add(timefilter.getTimeUpdate$().subscribe(timeUpdated)); - }; - - dropdownLoadedHandler() { - this.pipelineDropdownElement = document.querySelector('#dropdown-elm'); - setOptions(this); - } - - // kick things off from the directive link function - setup(options) { - const timefilter = Legacy.shims.timefilter; - this._licenseService = options.licenseService; - this._breadcrumbsService = options.breadcrumbsService; - this._executorService = options.executorService; - - Object.assign(this, options.attributes); - - this.navName = `${this.name}-nav`; - - // set the section we're navigated in - if (this.product) { - this.inElasticsearch = this.product === 'elasticsearch'; - this.inKibana = this.product === 'kibana'; - this.inLogstash = this.product === 'logstash'; - this.inBeats = this.product === 'beats'; - this.inApm = this.product === 'apm'; - } else { - this.inOverview = this.name === 'overview'; - this.inAlerts = this.name === 'alerts'; - this.inListing = this.name === 'listing'; // || this.name === 'no-data'; - } - - if (!this.inListing) { - // no breadcrumbs in cluster listing page - this.breadcrumbs = this._breadcrumbsService(options.clusterName, this); - } - - if (this.pipelineHash) { - this.pipelineHashShort = shortenPipelineHash(this.pipelineHash); - this.onChangePipelineHash = () => { - window.location.hash = getSafeForExternalLink( - `#/logstash/pipelines/${this.pipelineId}/${this.pipelineHash}` - ); - }; - } - - this.datePicker = { - enableTimeFilter: timefilter.isTimeRangeSelectorEnabled(), - timeRange: timefilter.getTime(), - refreshInterval: timefilter.getRefreshInterval(), - onRefreshChange: ({ isPaused, refreshInterval }, skipSet = false) => { - this.datePicker.refreshInterval = { - pause: isPaused, - value: refreshInterval, - }; - if (!skipSet) { - timefilter.setRefreshInterval({ - pause: isPaused, - value: refreshInterval ? refreshInterval : this.datePicker.refreshInterval.value, - }); - } - }, - onTimeUpdate: ({ dateRange }, skipSet = false) => { - this.datePicker.timeRange = { - ...dateRange, - }; - if (!skipSet) { - timefilter.setTime(dateRange); - } - this._executorService.cancel(); - this._executorService.run(); - }, - }; - } - - // check whether to "highlight" a tab - isActiveTab(testPath) { - return this.name === testPath; - } - - // check whether to show ML tab - isMlSupported() { - return this._licenseService.mlIsSupported(); - } - - isDisabledTab(product) { - const setupMode = getSetupModeState(); - if (!isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { - return false; - } - - if (!setupMode.data) { - return false; - } - - const data = setupMode.data[product] || {}; - if (data.totalUniqueInstanceCount === 0) { - return true; - } - if ( - data.totalUniqueInternallyCollectedCount === 0 && - data.totalUniqueFullyMigratedCount === 0 && - data.totalUniquePartiallyMigratedCount === 0 - ) { - return true; - } - return false; - } -} - -export function monitoringMainProvider(breadcrumbs, license, $injector) { - const $executor = $injector.get('$executor'); - const $parse = $injector.get('$parse'); - - return { - restrict: 'E', - transclude: true, - template, - controller: MonitoringMainController, - controllerAs: 'monitoringMain', - bindToController: true, - link(scope, _element, attributes, controller) { - scope.$applyAsync(() => { - controller.addTimerangeObservers(); - const setupObj = getSetupObj(); - controller.setup(setupObj); - Object.keys(setupObj.attributes).forEach((key) => { - attributes.$observe(key, () => controller.setup(getSetupObj())); - }); - if (attributes.onLoaded) { - const onLoaded = $parse(attributes.onLoaded)(scope); - onLoaded(); - } - }); - - initSetupModeState(scope, $injector, () => { - controller.setup(getSetupObj()); - }); - if (!scope.cluster) { - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - scope.cluster = ($route.current.locals.clusters || []).find( - (cluster) => cluster.cluster_uuid === globalState.cluster_uuid - ); - } - - function getSetupObj() { - return { - licenseService: license, - breadcrumbsService: breadcrumbs, - executorService: $executor, - attributes: { - name: attributes.name, - product: attributes.product, - instance: attributes.instance, - resolver: attributes.resolver, - page: attributes.page, - tabIconClass: attributes.tabIconClass, - tabIconLabel: attributes.tabIconLabel, - pipelineId: attributes.pipelineId, - pipelineHash: attributes.pipelineHash, - pipelineVersions: get(scope, 'pageData.versions'), - isCcrEnabled: attributes.isCcrEnabled === 'true' || attributes.isCcrEnabled === true, - }, - clusterName: get(scope, 'cluster.cluster_name'), - }; - } - - scope.$on('$destroy', () => { - controller.pipelineDropdownElement && - unmountComponentAtNode(controller.pipelineDropdownElement); - controller.subscriptions && controller.subscriptions.unsubscribe(); - }); - scope.$watch('pageData.versions', (versions) => { - controller.pipelineVersions = versions; - setOptions(controller); - }); - }, - }; -} diff --git a/x-pack/plugins/monitoring/public/directives/main/index.scss b/x-pack/plugins/monitoring/public/directives/main/index.scss deleted file mode 100644 index db5d2b72ab07b4..00000000000000 --- a/x-pack/plugins/monitoring/public/directives/main/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -.monTopNavSecondItem { - padding-left: $euiSizeM; -} diff --git a/x-pack/plugins/monitoring/public/directives/main/monitoring_main_controller.test.js b/x-pack/plugins/monitoring/public/directives/main/monitoring_main_controller.test.js deleted file mode 100644 index 195e11cee6112b..00000000000000 --- a/x-pack/plugins/monitoring/public/directives/main/monitoring_main_controller.test.js +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { noop } from 'lodash'; -import expect from '@kbn/expect'; -import { Legacy } from '../../legacy_shims'; -import { MonitoringMainController } from './'; - -const getMockLicenseService = (options) => ({ mlIsSupported: () => options.mlIsSupported }); -const getMockBreadcrumbsService = () => noop; // breadcrumb service has its own test - -describe('Monitoring Main Directive Controller', () => { - const core = { - notifications: {}, - application: {}, - i18n: {}, - chrome: {}, - }; - const data = { - query: { - timefilter: { - timefilter: { - isTimeRangeSelectorEnabled: () => true, - getTime: () => 1, - getRefreshInterval: () => 1, - }, - }, - }, - }; - const isCloud = false; - const triggersActionsUi = {}; - - beforeAll(() => { - Legacy.init({ - core, - data, - isCloud, - triggersActionsUi, - }); - }); - - /* - * Simulates calling the monitoringMain directive the way Cluster Listing - * does: - * - * ... - */ - it('in Cluster Listing', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: getMockLicenseService(), - breadcrumbsService: getMockBreadcrumbsService(), - attributes: { - name: 'listing', - }, - }); - - // derived properties - expect(controller.inListing).to.be(true); - expect(controller.inAlerts).to.be(false); - expect(controller.inOverview).to.be(false); - - // attributes - expect(controller.name).to.be('listing'); - expect(controller.product).to.be(undefined); - expect(controller.instance).to.be(undefined); - expect(controller.resolver).to.be(undefined); - expect(controller.page).to.be(undefined); - expect(controller.tabIconClass).to.be(undefined); - expect(controller.tabIconLabel).to.be(undefined); - }); - - /* - * Simulates calling the monitoringMain directive the way Cluster Alerts - * Listing does: - * - * ... - */ - it('in Cluster Alerts', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: getMockLicenseService(), - breadcrumbsService: getMockBreadcrumbsService(), - attributes: { - name: 'alerts', - }, - }); - - // derived properties - expect(controller.inListing).to.be(false); - expect(controller.inAlerts).to.be(true); - expect(controller.inOverview).to.be(false); - - // attributes - expect(controller.name).to.be('alerts'); - expect(controller.product).to.be(undefined); - expect(controller.instance).to.be(undefined); - expect(controller.resolver).to.be(undefined); - expect(controller.page).to.be(undefined); - expect(controller.tabIconClass).to.be(undefined); - expect(controller.tabIconLabel).to.be(undefined); - }); - - /* - * Simulates calling the monitoringMain directive the way Cluster Overview - * does: - * - * ... - */ - it('in Cluster Overview', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: getMockLicenseService(), - breadcrumbsService: getMockBreadcrumbsService(), - attributes: { - name: 'overview', - }, - }); - - // derived properties - expect(controller.inListing).to.be(false); - expect(controller.inAlerts).to.be(false); - expect(controller.inOverview).to.be(true); - - // attributes - expect(controller.name).to.be('overview'); - expect(controller.product).to.be(undefined); - expect(controller.instance).to.be(undefined); - expect(controller.resolver).to.be(undefined); - expect(controller.page).to.be(undefined); - expect(controller.tabIconClass).to.be(undefined); - expect(controller.tabIconLabel).to.be(undefined); - }); - - /* - * Simulates calling the monitoringMain directive the way that Elasticsearch - * Node / Advanced does: - * - * ... - */ - it('in ES Node - Advanced', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: getMockLicenseService(), - breadcrumbsService: getMockBreadcrumbsService(), - attributes: { - product: 'elasticsearch', - name: 'nodes', - instance: 'es-node-name-01', - resolver: 'es-node-resolver-01', - page: 'advanced', - tabIconClass: 'fa star', - tabIconLabel: 'Master Node', - }, - }); - - // derived properties - expect(controller.inListing).to.be(false); - expect(controller.inAlerts).to.be(false); - expect(controller.inOverview).to.be(false); - - // attributes - expect(controller.name).to.be('nodes'); - expect(controller.product).to.be('elasticsearch'); - expect(controller.instance).to.be('es-node-name-01'); - expect(controller.resolver).to.be('es-node-resolver-01'); - expect(controller.page).to.be('advanced'); - expect(controller.tabIconClass).to.be('fa star'); - expect(controller.tabIconLabel).to.be('Master Node'); - }); - - /** - * - */ - it('in Kibana Overview', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: getMockLicenseService(), - breadcrumbsService: getMockBreadcrumbsService(), - attributes: { - product: 'kibana', - name: 'overview', - }, - }); - - // derived properties - expect(controller.inListing).to.be(false); - expect(controller.inAlerts).to.be(false); - expect(controller.inOverview).to.be(false); - - // attributes - expect(controller.name).to.be('overview'); - expect(controller.product).to.be('kibana'); - expect(controller.instance).to.be(undefined); - expect(controller.resolver).to.be(undefined); - expect(controller.page).to.be(undefined); - expect(controller.tabIconClass).to.be(undefined); - expect(controller.tabIconLabel).to.be(undefined); - }); - - /** - * - */ - it('in Logstash Listing', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: getMockLicenseService(), - breadcrumbsService: getMockBreadcrumbsService(), - attributes: { - product: 'logstash', - name: 'listing', - }, - }); - - // derived properties - expect(controller.inListing).to.be(false); - expect(controller.inAlerts).to.be(false); - expect(controller.inOverview).to.be(false); - - // attributes - expect(controller.name).to.be('listing'); - expect(controller.product).to.be('logstash'); - expect(controller.instance).to.be(undefined); - expect(controller.resolver).to.be(undefined); - expect(controller.page).to.be(undefined); - expect(controller.tabIconClass).to.be(undefined); - expect(controller.tabIconLabel).to.be(undefined); - }); - - /* - * Test `controller.isMlSupported` function - */ - describe('Checking support for ML', () => { - it('license supports ML', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: getMockLicenseService({ mlIsSupported: true }), - breadcrumbsService: getMockBreadcrumbsService(), - attributes: { - name: 'listing', - }, - }); - - expect(controller.isMlSupported()).to.be(true); - }); - it('license does not support ML', () => { - getMockLicenseService({ mlIsSupported: false }); - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: getMockLicenseService({ mlIsSupported: false }), - breadcrumbsService: getMockBreadcrumbsService(), - attributes: { - name: 'listing', - }, - }); - - expect(controller.isMlSupported()).to.be(false); - }); - }); -}); diff --git a/x-pack/plugins/monitoring/public/lib/setup_mode.test.js b/x-pack/plugins/monitoring/public/lib/setup_mode.test.js index 6dad6effeecc1b..47cae9c4f08512 100644 --- a/x-pack/plugins/monitoring/public/lib/setup_mode.test.js +++ b/x-pack/plugins/monitoring/public/lib/setup_mode.test.js @@ -83,7 +83,7 @@ function waitForSetupModeData() { return new Promise((resolve) => process.nextTick(resolve)); } -describe('setup_mode', () => { +xdescribe('setup_mode', () => { beforeEach(async () => { setModulesAndMocks(); }); diff --git a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx index fca7f94731bc5a..e582f4aa408128 100644 --- a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx +++ b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx @@ -9,37 +9,21 @@ import React from 'react'; import { render } from 'react-dom'; import { get, includes } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { HttpStart, IHttpFetchError } from 'kibana/public'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { Legacy } from '../legacy_shims'; -import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; import { SetupModeFeature } from '../../common/enums'; import { ISetupModeContext } from '../components/setup_mode/setup_mode_context'; -import * as setupModeReact from '../application/setup_mode/setup_mode'; -import { isReactMigrationEnabled } from '../external_config'; +import { State as GlobalState } from '../application/contexts/global_state_context'; function isOnPage(hash: string) { return includes(window.location.hash, hash); } -interface IAngularState { - injector: any; - scope: any; -} - -const angularState: IAngularState = { - injector: null, - scope: null, -}; - -const checkAngularState = () => { - if (!angularState.injector || !angularState.scope) { - throw new Error( - 'Unable to interact with setup mode because the angular injector was not previously set.' + - ' This needs to be set by calling `initSetupModeState`.' - ); - } -}; +let globalState: GlobalState; +let httpService: HttpStart; +let errorHandler: (error: IHttpFetchError) => void; interface ISetupModeState { enabled: boolean; @@ -57,20 +41,11 @@ const setupModeState: ISetupModeState = { export const getSetupModeState = () => setupModeState; export const setNewlyDiscoveredClusterUuid = (clusterUuid: string) => { - const globalState = angularState.injector.get('globalState'); - const executor = angularState.injector.get('$executor'); - angularState.scope.$apply(() => { - globalState.cluster_uuid = clusterUuid; - globalState.save(); - }); - executor.run(); + globalState.cluster_uuid = clusterUuid; + globalState.save?.(); }; export const fetchCollectionData = async (uuid?: string, fetchWithoutClusterUuid = false) => { - checkAngularState(); - - const http = angularState.injector.get('$http'); - const globalState = angularState.injector.get('globalState'); const clusterUuid = globalState.cluster_uuid; const ccs = globalState.ccs; @@ -84,12 +59,15 @@ export const fetchCollectionData = async (uuid?: string, fetchWithoutClusterUuid } try { - const response = await http.post(url, { ccs }); - return response.data; + const response = await httpService.post(url, { + body: JSON.stringify({ + ccs, + }), + }); + return response; } catch (err) { - const Private = angularState.injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); + errorHandler(err); + throw err; } }; @@ -107,19 +85,16 @@ export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid }); } - angularState.scope.$evalAsync(() => { - Legacy.shims.toastNotifications.addDanger({ - title: i18n.translate('xpack.monitoring.setupMode.notAvailableTitle', { - defaultMessage: 'Setup mode is not available', - }), - text, - }); + Legacy.shims.toastNotifications.addDanger({ + title: i18n.translate('xpack.monitoring.setupMode.notAvailableTitle', { + defaultMessage: 'Setup mode is not available', + }), + text, }); return toggleSetupMode(false); } notifySetupModeDataChange(); - const globalState = angularState.injector.get('globalState'); const clusterUuid = globalState.cluster_uuid; if (!clusterUuid) { const liveClusterUuid: string = get(data, '_meta.liveClusterUuid'); @@ -142,31 +117,21 @@ export const showBottomBar = () => { }; export const disableElasticsearchInternalCollection = async () => { - checkAngularState(); - - const http = angularState.injector.get('$http'); - const globalState = angularState.injector.get('globalState'); const clusterUuid = globalState.cluster_uuid; const url = `../api/monitoring/v1/setup/collection/${clusterUuid}/disable_internal_collection`; try { - const response = await http.post(url); - return response.data; + const response = await httpService.post(url); + return response; } catch (err) { - const Private = angularState.injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); + errorHandler(err); + throw err; } }; export const toggleSetupMode = (inSetupMode: boolean) => { - if (isReactMigrationEnabled()) return setupModeReact.toggleSetupMode(inSetupMode); - - checkAngularState(); - - const globalState = angularState.injector.get('globalState'); setupModeState.enabled = inSetupMode; globalState.inSetupMode = inSetupMode; - globalState.save(); + globalState.save?.(); setSetupModeMenuItem(); notifySetupModeDataChange(); @@ -177,13 +142,10 @@ export const toggleSetupMode = (inSetupMode: boolean) => { }; export const setSetupModeMenuItem = () => { - checkAngularState(); - if (isOnPage('no-data')) { return; } - const globalState = angularState.injector.get('globalState'); const enabled = !globalState.inSetupMode; const I18nContext = Legacy.shims.I18nContext; @@ -197,23 +159,25 @@ export const setSetupModeMenuItem = () => { ); }; -export const addSetupModeCallback = (callback: () => void) => (setupModeState.callback = callback); - -export const initSetupModeState = async ($scope: any, $injector: any, callback?: () => void) => { - angularState.scope = $scope; - angularState.injector = $injector; +export const initSetupModeState = async ( + state: GlobalState, + http: HttpStart, + handleErrors: (error: IHttpFetchError) => void, + callback?: () => void +) => { + globalState = state; + httpService = http; + errorHandler = handleErrors; if (callback) { setupModeState.callback = callback; } - const globalState = $injector.get('globalState'); if (globalState.inSetupMode) { toggleSetupMode(true); } }; -export const isInSetupMode = (context?: ISetupModeContext) => { - if (isReactMigrationEnabled()) return setupModeReact.isInSetupMode(context); +export const isInSetupMode = (context?: ISetupModeContext, gState: GlobalState = globalState) => { if (context?.setupModeSupported === false) { return false; } @@ -221,20 +185,19 @@ export const isInSetupMode = (context?: ISetupModeContext) => { return true; } - const $injector = angularState.injector || Legacy.shims.getAngularInjector(); - const globalState = $injector.get('globalState'); - return globalState.inSetupMode; + return gState.inSetupMode; }; export const isSetupModeFeatureEnabled = (feature: SetupModeFeature) => { - if (isReactMigrationEnabled()) return setupModeReact.isSetupModeFeatureEnabled(feature); if (!setupModeState.enabled) { return false; } + if (feature === SetupModeFeature.MetricbeatMigration) { if (Legacy.shims.isCloud) { return false; } } + return true; }; diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 82e49fec5a8d47..f75b76871f58cb 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -36,9 +36,6 @@ interface MonitoringSetupPluginDependencies { triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; usageCollection: UsageCollectionSetup; } - -const HASH_CHANGE = 'hashchange'; - export class MonitoringPlugin implements Plugin @@ -88,7 +85,6 @@ export class MonitoringPlugin category: DEFAULT_APP_CATEGORIES.management, mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); - const { AngularApp } = await import('./angular'); const externalConfig = this.getExternalConfig(); const deps: MonitoringStartPluginDependencies = { navigation: pluginsStart.navigation, @@ -118,26 +114,8 @@ export class MonitoringPlugin const config = Object.fromEntries(externalConfig); setConfig(config); - if (config.renderReactApp) { - const { renderApp } = await import('./application'); - return renderApp(coreStart, pluginsStart, params, config); - } else { - const monitoringApp = new AngularApp(deps); - const removeHistoryListener = params.history.listen((location) => { - if (location.pathname === '' && location.hash === '') { - monitoringApp.applyScope(); - } - }); - - const removeHashChange = this.setInitialTimefilter(deps); - return () => { - if (removeHashChange) { - removeHashChange(); - } - removeHistoryListener(); - monitoringApp.destroy(); - }; - } + const { renderApp } = await import('./application'); + return renderApp(coreStart, pluginsStart, params, config); }, }; @@ -148,28 +126,6 @@ export class MonitoringPlugin public stop() {} - private setInitialTimefilter({ data }: MonitoringStartPluginDependencies) { - const { timefilter } = data.query.timefilter; - const { pause: pauseByDefault } = timefilter.getRefreshIntervalDefaults(); - if (pauseByDefault) { - return; - } - /** - * We can't use timefilter.getRefreshIntervalUpdate$ last value, - * since it's not a BehaviorSubject. This means we need to wait for - * hash change because of angular's applyAsync - */ - const onHashChange = () => { - const { value, pause } = timefilter.getRefreshInterval(); - if (!value && pause) { - window.removeEventListener(HASH_CHANGE, onHashChange); - timefilter.setRefreshInterval({ value: 10000, pause: false }); - } - }; - window.addEventListener(HASH_CHANGE, onHashChange, false); - return () => window.removeEventListener(HASH_CHANGE, onHashChange); - } - private getExternalConfig() { const monitoring = this.initializerContext.config.get(); return [ diff --git a/x-pack/plugins/monitoring/public/services/breadcrumbs.js b/x-pack/plugins/monitoring/public/services/breadcrumbs.js deleted file mode 100644 index 54ff46f4bf0ab3..00000000000000 --- a/x-pack/plugins/monitoring/public/services/breadcrumbs.js +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Legacy } from '../legacy_shims'; -import { i18n } from '@kbn/i18n'; - -// Helper for making objects to use in a link element -const createCrumb = (url, label, testSubj, ignoreGlobalState = false) => { - const crumb = { url, label, ignoreGlobalState }; - if (testSubj) { - crumb.testSubj = testSubj; - } - return crumb; -}; - -// generate Elasticsearch breadcrumbs -function getElasticsearchBreadcrumbs(mainInstance) { - const breadcrumbs = []; - if (mainInstance.instance) { - breadcrumbs.push(createCrumb('#/elasticsearch', 'Elasticsearch')); - if (mainInstance.name === 'indices') { - breadcrumbs.push( - createCrumb( - '#/elasticsearch/indices', - i18n.translate('xpack.monitoring.breadcrumbs.es.indicesLabel', { - defaultMessage: 'Indices', - }), - 'breadcrumbEsIndices' - ) - ); - } else if (mainInstance.name === 'nodes') { - breadcrumbs.push( - createCrumb( - '#/elasticsearch/nodes', - i18n.translate('xpack.monitoring.breadcrumbs.es.nodesLabel', { defaultMessage: 'Nodes' }), - 'breadcrumbEsNodes' - ) - ); - } else if (mainInstance.name === 'ml') { - // ML Instance (for user later) - breadcrumbs.push( - createCrumb( - '#/elasticsearch/ml_jobs', - i18n.translate('xpack.monitoring.breadcrumbs.es.jobsLabel', { - defaultMessage: 'Machine learning jobs', - }) - ) - ); - } else if (mainInstance.name === 'ccr_shard') { - breadcrumbs.push( - createCrumb( - '#/elasticsearch/ccr', - i18n.translate('xpack.monitoring.breadcrumbs.es.ccrLabel', { defaultMessage: 'CCR' }) - ) - ); - } - breadcrumbs.push(createCrumb(null, mainInstance.instance)); - } else { - // don't link to Overview when we're possibly on Overview or its sibling tabs - breadcrumbs.push(createCrumb(null, 'Elasticsearch')); - } - return breadcrumbs; -} - -// generate Kibana breadcrumbs -function getKibanaBreadcrumbs(mainInstance) { - const breadcrumbs = []; - if (mainInstance.instance) { - breadcrumbs.push(createCrumb('#/kibana', 'Kibana')); - breadcrumbs.push( - createCrumb( - '#/kibana/instances', - i18n.translate('xpack.monitoring.breadcrumbs.kibana.instancesLabel', { - defaultMessage: 'Instances', - }) - ) - ); - breadcrumbs.push(createCrumb(null, mainInstance.instance)); - } else { - // don't link to Overview when we're possibly on Overview or its sibling tabs - breadcrumbs.push(createCrumb(null, 'Kibana')); - } - return breadcrumbs; -} - -// generate Logstash breadcrumbs -function getLogstashBreadcrumbs(mainInstance) { - const logstashLabel = i18n.translate('xpack.monitoring.breadcrumbs.logstashLabel', { - defaultMessage: 'Logstash', - }); - const breadcrumbs = []; - if (mainInstance.instance) { - breadcrumbs.push(createCrumb('#/logstash', logstashLabel)); - if (mainInstance.name === 'nodes') { - breadcrumbs.push( - createCrumb( - '#/logstash/nodes', - i18n.translate('xpack.monitoring.breadcrumbs.logstash.nodesLabel', { - defaultMessage: 'Nodes', - }) - ) - ); - } - breadcrumbs.push(createCrumb(null, mainInstance.instance)); - } else if (mainInstance.page === 'pipeline') { - breadcrumbs.push(createCrumb('#/logstash', logstashLabel)); - breadcrumbs.push( - createCrumb( - '#/logstash/pipelines', - i18n.translate('xpack.monitoring.breadcrumbs.logstash.pipelinesLabel', { - defaultMessage: 'Pipelines', - }) - ) - ); - } else { - // don't link to Overview when we're possibly on Overview or its sibling tabs - breadcrumbs.push(createCrumb(null, logstashLabel)); - } - - return breadcrumbs; -} - -// generate Beats breadcrumbs -function getBeatsBreadcrumbs(mainInstance) { - const beatsLabel = i18n.translate('xpack.monitoring.breadcrumbs.beatsLabel', { - defaultMessage: 'Beats', - }); - const breadcrumbs = []; - if (mainInstance.instance) { - breadcrumbs.push(createCrumb('#/beats', beatsLabel)); - breadcrumbs.push( - createCrumb( - '#/beats/beats', - i18n.translate('xpack.monitoring.breadcrumbs.beats.instancesLabel', { - defaultMessage: 'Instances', - }) - ) - ); - breadcrumbs.push(createCrumb(null, mainInstance.instance)); - } else { - breadcrumbs.push(createCrumb(null, beatsLabel)); - } - - return breadcrumbs; -} - -// generate Apm breadcrumbs -function getApmBreadcrumbs(mainInstance) { - const apmLabel = i18n.translate('xpack.monitoring.breadcrumbs.apmLabel', { - defaultMessage: 'APM server', - }); - const breadcrumbs = []; - if (mainInstance.instance) { - breadcrumbs.push(createCrumb('#/apm', apmLabel)); - breadcrumbs.push( - createCrumb( - '#/apm/instances', - i18n.translate('xpack.monitoring.breadcrumbs.apm.instancesLabel', { - defaultMessage: 'Instances', - }) - ) - ); - breadcrumbs.push(createCrumb(null, mainInstance.instance)); - } else { - // don't link to Overview when we're possibly on Overview or its sibling tabs - breadcrumbs.push(createCrumb(null, apmLabel)); - } - return breadcrumbs; -} - -export function breadcrumbsProvider() { - return function createBreadcrumbs(clusterName, mainInstance) { - const homeCrumb = i18n.translate('xpack.monitoring.breadcrumbs.clustersLabel', { - defaultMessage: 'Clusters', - }); - - let breadcrumbs = [createCrumb('#/home', homeCrumb, 'breadcrumbClusters', true)]; - - if (!mainInstance.inOverview && clusterName) { - breadcrumbs.push(createCrumb('#/overview', clusterName)); - } - - if (mainInstance.inElasticsearch) { - breadcrumbs = breadcrumbs.concat(getElasticsearchBreadcrumbs(mainInstance)); - } - if (mainInstance.inKibana) { - breadcrumbs = breadcrumbs.concat(getKibanaBreadcrumbs(mainInstance)); - } - if (mainInstance.inLogstash) { - breadcrumbs = breadcrumbs.concat(getLogstashBreadcrumbs(mainInstance)); - } - if (mainInstance.inBeats) { - breadcrumbs = breadcrumbs.concat(getBeatsBreadcrumbs(mainInstance)); - } - if (mainInstance.inApm) { - breadcrumbs = breadcrumbs.concat(getApmBreadcrumbs(mainInstance)); - } - - Legacy.shims.breadcrumbs.set( - breadcrumbs.map((b) => ({ - text: b.label, - href: b.url, - 'data-test-subj': b.testSubj, - ignoreGlobalState: b.ignoreGlobalState, - })) - ); - - return breadcrumbs; - }; -} diff --git a/x-pack/plugins/monitoring/public/services/breadcrumbs.test.js b/x-pack/plugins/monitoring/public/services/breadcrumbs.test.js deleted file mode 100644 index 0af5d59e54555a..00000000000000 --- a/x-pack/plugins/monitoring/public/services/breadcrumbs.test.js +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { breadcrumbsProvider } from './breadcrumbs'; -import { MonitoringMainController } from '../directives/main'; -import { Legacy } from '../legacy_shims'; - -describe('Monitoring Breadcrumbs Service', () => { - const core = { - notifications: {}, - application: {}, - i18n: {}, - chrome: {}, - }; - const data = { - query: { - timefilter: { - timefilter: { - isTimeRangeSelectorEnabled: () => true, - getTime: () => 1, - getRefreshInterval: () => 1, - }, - }, - }, - }; - const isCloud = false; - const triggersActionsUi = {}; - - beforeAll(() => { - Legacy.init({ - core, - data, - isCloud, - triggersActionsUi, - }); - }); - - it('in Cluster Alerts', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: {}, - breadcrumbsService: breadcrumbsProvider(), - attributes: { - name: 'alerts', - }, - }); - expect(controller.breadcrumbs).to.eql([ - { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters', ignoreGlobalState: true }, - { url: '#/overview', label: 'test-cluster-foo', ignoreGlobalState: false }, - ]); - }); - - it('in Cluster Overview', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: {}, - breadcrumbsService: breadcrumbsProvider(), - attributes: { - name: 'overview', - }, - }); - expect(controller.breadcrumbs).to.eql([ - { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters', ignoreGlobalState: true }, - ]); - }); - - it('in ES Node - Advanced', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: {}, - breadcrumbsService: breadcrumbsProvider(), - attributes: { - product: 'elasticsearch', - name: 'nodes', - instance: 'es-node-name-01', - resolver: 'es-node-resolver-01', - page: 'advanced', - tabIconClass: 'fa star', - tabIconLabel: 'Master Node', - }, - }); - expect(controller.breadcrumbs).to.eql([ - { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters', ignoreGlobalState: true }, - { url: '#/overview', label: 'test-cluster-foo', ignoreGlobalState: false }, - { url: '#/elasticsearch', label: 'Elasticsearch', ignoreGlobalState: false }, - { - url: '#/elasticsearch/nodes', - label: 'Nodes', - testSubj: 'breadcrumbEsNodes', - ignoreGlobalState: false, - }, - { url: null, label: 'es-node-name-01', ignoreGlobalState: false }, - ]); - }); - - it('in Kibana Overview', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: {}, - breadcrumbsService: breadcrumbsProvider(), - attributes: { - product: 'kibana', - name: 'overview', - }, - }); - expect(controller.breadcrumbs).to.eql([ - { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters', ignoreGlobalState: true }, - { url: '#/overview', label: 'test-cluster-foo', ignoreGlobalState: false }, - { url: null, label: 'Kibana', ignoreGlobalState: false }, - ]); - }); - - /** - * - */ - it('in Logstash Listing', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: {}, - breadcrumbsService: breadcrumbsProvider(), - attributes: { - product: 'logstash', - name: 'listing', - }, - }); - expect(controller.breadcrumbs).to.eql([ - { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters', ignoreGlobalState: true }, - { url: '#/overview', label: 'test-cluster-foo', ignoreGlobalState: false }, - { url: null, label: 'Logstash', ignoreGlobalState: false }, - ]); - }); - - /** - * - */ - it('in Logstash Pipeline Viewer', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: {}, - breadcrumbsService: breadcrumbsProvider(), - attributes: { - product: 'logstash', - page: 'pipeline', - pipelineId: 'main', - pipelineHash: '42ee890af9...', - }, - }); - expect(controller.breadcrumbs).to.eql([ - { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters', ignoreGlobalState: true }, - { url: '#/overview', label: 'test-cluster-foo', ignoreGlobalState: false }, - { url: '#/logstash', label: 'Logstash', ignoreGlobalState: false }, - { url: '#/logstash/pipelines', label: 'Pipelines', ignoreGlobalState: false }, - ]); - }); -}); diff --git a/x-pack/plugins/monitoring/public/services/clusters.js b/x-pack/plugins/monitoring/public/services/clusters.js deleted file mode 100644 index b19d0ea56765f3..00000000000000 --- a/x-pack/plugins/monitoring/public/services/clusters.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ajaxErrorHandlersProvider } from '../lib/ajax_error_handler'; -import { Legacy } from '../legacy_shims'; -import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../common/constants'; - -function formatClusters(clusters) { - return clusters.map(formatCluster); -} - -function formatCluster(cluster) { - if (cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID) { - cluster.cluster_name = 'Standalone Cluster'; - } - return cluster; -} - -export function monitoringClustersProvider($injector) { - return async (clusterUuid, ccs, codePaths) => { - const { min, max } = Legacy.shims.timefilter.getBounds(); - - // append clusterUuid if the parameter is given - let url = '../api/monitoring/v1/clusters'; - if (clusterUuid) { - url += `/${clusterUuid}`; - } - - const $http = $injector.get('$http'); - - async function getClusters() { - try { - const response = await $http.post( - url, - { - ccs, - timeRange: { - min: min.toISOString(), - max: max.toISOString(), - }, - codePaths, - }, - { headers: { 'kbn-system-request': 'true' } } - ); - return formatClusters(response.data); - } catch (err) { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - } - } - - return await getClusters(); - }; -} diff --git a/x-pack/plugins/monitoring/public/services/enable_alerts_modal.js b/x-pack/plugins/monitoring/public/services/enable_alerts_modal.js deleted file mode 100644 index 438c5ab83f5e30..00000000000000 --- a/x-pack/plugins/monitoring/public/services/enable_alerts_modal.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { ajaxErrorHandlersProvider } from '../lib/ajax_error_handler'; -import { showAlertsToast } from '../alerts/lib/alerts_toast'; - -export function enableAlertsModalProvider($http, $window, $injector) { - function shouldShowAlertsModal(alerts) { - const modalHasBeenShown = $window.sessionStorage.getItem('ALERTS_MODAL_HAS_BEEN_SHOWN'); - const decisionMade = $window.localStorage.getItem('ALERTS_MODAL_DECISION_MADE'); - - if (Object.keys(alerts).length > 0) { - $window.localStorage.setItem('ALERTS_MODAL_DECISION_MADE', true); - return false; - } else if (!modalHasBeenShown && !decisionMade) { - return true; - } - - return false; - } - - async function enableAlerts() { - try { - const { data } = await $http.post('../api/monitoring/v1/alerts/enable', {}); - $window.localStorage.setItem('ALERTS_MODAL_DECISION_MADE', true); - showAlertsToast(data); - } catch (err) { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - } - } - - function notAskAgain() { - $window.localStorage.setItem('ALERTS_MODAL_DECISION_MADE', true); - } - - function hideModalForSession() { - $window.sessionStorage.setItem('ALERTS_MODAL_HAS_BEEN_SHOWN', true); - } - - return { - shouldShowAlertsModal, - enableAlerts, - notAskAgain, - hideModalForSession, - }; -} diff --git a/x-pack/plugins/monitoring/public/services/executor.js b/x-pack/plugins/monitoring/public/services/executor.js deleted file mode 100644 index 60b2c171eac320..00000000000000 --- a/x-pack/plugins/monitoring/public/services/executor.js +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Legacy } from '../legacy_shims'; -import { subscribeWithScope } from '../angular/helpers/utils'; -import { Subscription } from 'rxjs'; - -export function executorProvider($timeout, $q) { - const queue = []; - const subscriptions = new Subscription(); - let executionTimer; - let ignorePaused = false; - - /** - * Resets the timer to start again - * @returns {void} - */ - function reset() { - cancel(); - start(); - } - - function killTimer() { - if (executionTimer) { - $timeout.cancel(executionTimer); - } - } - - /** - * Cancels the execution timer - * @returns {void} - */ - function cancel() { - killTimer(); - } - - /** - * Registers a service with the executor - * @param {object} service The service to register - * @returns {void} - */ - function register(service) { - queue.push(service); - } - - /** - * Stops the executor and empties the service queue - * @returns {void} - */ - function destroy() { - subscriptions.unsubscribe(); - cancel(); - ignorePaused = false; - queue.splice(0, queue.length); - } - - /** - * Runs the queue (all at once) - * @returns {Promise} a promise of all the services - */ - function run() { - const noop = () => $q.resolve(); - return $q - .all( - queue.map((service) => { - return service - .execute() - .then(service.handleResponse || noop) - .catch(service.handleError || noop); - }) - ) - .finally(reset); - } - - function reFetch() { - cancel(); - run(); - } - - function killIfPaused() { - if (Legacy.shims.timefilter.getRefreshInterval().pause) { - killTimer(); - } - } - - /** - * Starts the executor service if the timefilter is not paused - * @returns {void} - */ - function start() { - const timefilter = Legacy.shims.timefilter; - if ( - (ignorePaused || timefilter.getRefreshInterval().pause === false) && - timefilter.getRefreshInterval().value > 0 - ) { - executionTimer = $timeout(run, timefilter.getRefreshInterval().value); - } - } - - /** - * Expose the methods - */ - return { - register, - start($scope) { - $scope.$applyAsync(() => { - const timefilter = Legacy.shims.timefilter; - subscriptions.add( - subscribeWithScope($scope, timefilter.getFetch$(), { - next: reFetch, - }) - ); - subscriptions.add( - subscribeWithScope($scope, timefilter.getRefreshIntervalUpdate$(), { - next: killIfPaused, - }) - ); - start(); - }); - }, - run, - destroy, - reset, - cancel, - }; -} diff --git a/x-pack/plugins/monitoring/public/services/features.js b/x-pack/plugins/monitoring/public/services/features.js deleted file mode 100644 index 34564f79c92471..00000000000000 --- a/x-pack/plugins/monitoring/public/services/features.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { has, isUndefined } from 'lodash'; - -export function featuresProvider($window) { - function getData() { - let returnData = {}; - const monitoringData = $window.localStorage.getItem('xpack.monitoring.data'); - - try { - returnData = (monitoringData && JSON.parse(monitoringData)) || {}; - } catch (e) { - console.error('Monitoring UI: error parsing locally stored monitoring data', e); - } - - return returnData; - } - - function update(featureName, value) { - const monitoringDataObj = getData(); - monitoringDataObj[featureName] = value; - $window.localStorage.setItem('xpack.monitoring.data', JSON.stringify(monitoringDataObj)); - } - - function isEnabled(featureName, defaultSetting) { - const monitoringDataObj = getData(); - if (has(monitoringDataObj, featureName)) { - return monitoringDataObj[featureName]; - } - - if (isUndefined(defaultSetting)) { - return false; - } - - return defaultSetting; - } - - return { - isEnabled, - update, - }; -} diff --git a/x-pack/plugins/monitoring/public/services/license.js b/x-pack/plugins/monitoring/public/services/license.js deleted file mode 100644 index cab5ad01cf58aa..00000000000000 --- a/x-pack/plugins/monitoring/public/services/license.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { includes } from 'lodash'; -import { ML_SUPPORTED_LICENSES } from '../../common/constants'; - -export function licenseProvider() { - return new (class LicenseService { - constructor() { - // do not initialize with usable state - this.license = { - type: null, - expiry_date_in_millis: -Infinity, - }; - } - - // we're required to call this initially - setLicense(license) { - this.license = license; - } - - isBasic() { - return this.license.type === 'basic'; - } - - mlIsSupported() { - return includes(ML_SUPPORTED_LICENSES, this.license.type); - } - - doesExpire() { - const { expiry_date_in_millis: expiryDateInMillis } = this.license; - return expiryDateInMillis !== undefined; - } - - isActive() { - const { expiry_date_in_millis: expiryDateInMillis } = this.license; - return new Date().getTime() < expiryDateInMillis; - } - - isExpired() { - if (this.doesExpire()) { - const { expiry_date_in_millis: expiryDateInMillis } = this.license; - return new Date().getTime() >= expiryDateInMillis; - } - return false; - } - })(); -} diff --git a/x-pack/plugins/monitoring/public/services/title.js b/x-pack/plugins/monitoring/public/services/title.js deleted file mode 100644 index e12d4936584fa2..00000000000000 --- a/x-pack/plugins/monitoring/public/services/title.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { get } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { Legacy } from '../legacy_shims'; - -export function titleProvider($rootScope) { - return function changeTitle(cluster, suffix) { - let clusterName = get(cluster, 'cluster_name'); - clusterName = clusterName ? `- ${clusterName}` : ''; - suffix = suffix ? `- ${suffix}` : ''; - $rootScope.$applyAsync(() => { - Legacy.shims.docTitle.change( - i18n.translate('xpack.monitoring.stackMonitoringDocTitle', { - defaultMessage: 'Stack Monitoring {clusterName} {suffix}', - values: { clusterName, suffix }, - }) - ); - }); - }; -} diff --git a/x-pack/plugins/monitoring/public/views/access_denied/index.html b/x-pack/plugins/monitoring/public/views/access_denied/index.html deleted file mode 100644 index 24863559212f78..00000000000000 --- a/x-pack/plugins/monitoring/public/views/access_denied/index.html +++ /dev/null @@ -1,44 +0,0 @@ -
-
-
- - -
- -
-
- -
- -
-
- - - -
-
-
-
-
diff --git a/x-pack/plugins/monitoring/public/views/access_denied/index.js b/x-pack/plugins/monitoring/public/views/access_denied/index.js deleted file mode 100644 index e52df61dd8966d..00000000000000 --- a/x-pack/plugins/monitoring/public/views/access_denied/index.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { uiRoutes } from '../../angular/helpers/routes'; -import template from './index.html'; - -const tryPrivilege = ($http) => { - return $http - .get('../api/monitoring/v1/check_access') - .then(() => window.history.replaceState(null, null, '#/home')) - .catch(() => true); -}; - -uiRoutes.when('/access-denied', { - template, - resolve: { - /* - * The user may have been granted privileges in between leaving Monitoring - * and before coming back to Monitoring. That means, they just be on this - * page because Kibana remembers the "last app URL". We check for the - * privilege one time up front (doing it in the resolve makes it happen - * before the template renders), and then keep retrying every 5 seconds. - */ - initialCheck($http) { - return tryPrivilege($http); - }, - }, - controllerAs: 'accessDenied', - controller: function ($scope, $injector) { - const $http = $injector.get('$http'); - const $interval = $injector.get('$interval'); - - // The template's "Back to Kibana" button click handler - this.goToKibanaURL = '/app/home'; - - // keep trying to load data in the background - const accessPoller = $interval(() => tryPrivilege($http), 5 * 1000); // every 5 seconds - $scope.$on('$destroy', () => $interval.cancel(accessPoller)); - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/all.js b/x-pack/plugins/monitoring/public/views/all.js deleted file mode 100644 index 3af0c85d956876..00000000000000 --- a/x-pack/plugins/monitoring/public/views/all.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import './no_data'; -import './access_denied'; -import './license'; -import './cluster/listing'; -import './cluster/overview'; -import './elasticsearch/overview'; -import './elasticsearch/indices'; -import './elasticsearch/index'; -import './elasticsearch/index/advanced'; -import './elasticsearch/nodes'; -import './elasticsearch/node'; -import './elasticsearch/node/advanced'; -import './elasticsearch/ccr'; -import './elasticsearch/ccr/shard'; -import './elasticsearch/ml_jobs'; -import './kibana/overview'; -import './kibana/instances'; -import './kibana/instance'; -import './logstash/overview'; -import './logstash/nodes'; -import './logstash/node'; -import './logstash/node/advanced'; -import './logstash/node/pipelines'; -import './logstash/pipelines'; -import './logstash/pipeline'; -import './beats/overview'; -import './beats/listing'; -import './beats/beat'; -import './apm/overview'; -import './apm/instances'; -import './apm/instance'; -import './loading'; diff --git a/x-pack/plugins/monitoring/public/views/apm/instance/index.html b/x-pack/plugins/monitoring/public/views/apm/instance/index.html deleted file mode 100644 index 79579990eb6495..00000000000000 --- a/x-pack/plugins/monitoring/public/views/apm/instance/index.html +++ /dev/null @@ -1,8 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/apm/instance/index.js b/x-pack/plugins/monitoring/public/views/apm/instance/index.js deleted file mode 100644 index 0d733036bb2662..00000000000000 --- a/x-pack/plugins/monitoring/public/views/apm/instance/index.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { find, get } from 'lodash'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { routeInitProvider } from '../../../lib/route_init'; -import template from './index.html'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { ApmServerInstance } from '../../../components/apm/instance'; -import { CODE_PATH_APM } from '../../../../common/constants'; - -uiRoutes.when('/apm/instances/:uuid', { - template, - resolve: { - clusters: function (Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_APM] }); - }, - }, - - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const title = $injector.get('title'); - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - super({ - title: i18n.translate('xpack.monitoring.apm.instance.routeTitle', { - defaultMessage: '{apm} - Instance', - values: { - apm: 'APM server', - }, - }), - telemetryPageViewTitle: 'apm_server_instance', - api: `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/apm/${$route.current.params.uuid}`, - defaultData: {}, - reactNodeId: 'apmInstanceReact', - $scope, - $injector, - }); - - $scope.$watch( - () => this.data, - (data) => { - this.setPageTitle( - i18n.translate('xpack.monitoring.apm.instance.pageTitle', { - defaultMessage: 'APM server instance: {instanceName}', - values: { - instanceName: get(data, 'apmSummary.name'), - }, - }) - ); - title($scope.cluster, `APM server - ${get(data, 'apmSummary.name')}`); - this.renderReact( - - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/apm/instances/index.html b/x-pack/plugins/monitoring/public/views/apm/instances/index.html deleted file mode 100644 index fd8029e277d786..00000000000000 --- a/x-pack/plugins/monitoring/public/views/apm/instances/index.html +++ /dev/null @@ -1,7 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/apm/instances/index.js b/x-pack/plugins/monitoring/public/views/apm/instances/index.js deleted file mode 100644 index f9747ec176e864..00000000000000 --- a/x-pack/plugins/monitoring/public/views/apm/instances/index.js +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { find } from 'lodash'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { routeInitProvider } from '../../../lib/route_init'; -import template from './index.html'; -import { ApmServerInstances } from '../../../components/apm/instances'; -import { MonitoringViewBaseEuiTableController } from '../..'; -import { SetupModeRenderer } from '../../../components/renderers'; -import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; -import { APM_SYSTEM_ID, CODE_PATH_APM } from '../../../../common/constants'; - -uiRoutes.when('/apm/instances', { - template, - resolve: { - clusters: function (Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_APM] }); - }, - }, - controller: class extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - super({ - title: i18n.translate('xpack.monitoring.apm.instances.routeTitle', { - defaultMessage: '{apm} - Instances', - values: { - apm: 'APM server', - }, - }), - pageTitle: i18n.translate('xpack.monitoring.apm.instances.pageTitle', { - defaultMessage: 'APM server instances', - }), - storageKey: 'apm.instances', - api: `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/apm/instances`, - defaultData: {}, - reactNodeId: 'apmInstancesReact', - $scope, - $injector, - }); - - this.scope = $scope; - this.injector = $injector; - this.onTableChangeRender = this.renderComponent; - - $scope.$watch( - () => this.data, - () => this.renderComponent() - ); - } - - renderComponent() { - const { pagination, sorting, onTableChange } = this; - - const component = ( - ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> - ); - this.renderReact(component); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/apm/overview/index.html b/x-pack/plugins/monitoring/public/views/apm/overview/index.html deleted file mode 100644 index 0cf804e377476a..00000000000000 --- a/x-pack/plugins/monitoring/public/views/apm/overview/index.html +++ /dev/null @@ -1,7 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/apm/overview/index.js b/x-pack/plugins/monitoring/public/views/apm/overview/index.js deleted file mode 100644 index bef17bf4a2fade..00000000000000 --- a/x-pack/plugins/monitoring/public/views/apm/overview/index.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { find } from 'lodash'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { routeInitProvider } from '../../../lib/route_init'; -import template from './index.html'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { ApmOverview } from '../../../components/apm/overview'; -import { CODE_PATH_APM } from '../../../../common/constants'; - -uiRoutes.when('/apm', { - template, - resolve: { - clusters: function (Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_APM] }); - }, - }, - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - super({ - title: i18n.translate('xpack.monitoring.apm.overview.routeTitle', { - defaultMessage: 'APM server', - }), - pageTitle: i18n.translate('xpack.monitoring.apm.overview.pageTitle', { - defaultMessage: 'APM server overview', - }), - api: `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/apm`, - defaultData: {}, - reactNodeId: 'apmOverviewReact', - $scope, - $injector, - }); - - $scope.$watch( - () => this.data, - (data) => { - this.renderReact( - - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/base_controller.js b/x-pack/plugins/monitoring/public/views/base_controller.js deleted file mode 100644 index dd9898a6e195c2..00000000000000 --- a/x-pack/plugins/monitoring/public/views/base_controller.js +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import moment from 'moment'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { getPageData } from '../lib/get_page_data'; -import { PageLoading } from '../components'; -import { Legacy } from '../legacy_shims'; -import { PromiseWithCancel } from '../../common/cancel_promise'; -import { SetupModeFeature } from '../../common/enums'; -import { updateSetupModeData, isSetupModeFeatureEnabled } from '../lib/setup_mode'; -import { AlertsContext } from '../alerts/context'; -import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; -import { AlertsDropdown } from '../alerts/alerts_dropdown'; -import { HeaderMenuPortal } from '../../../observability/public'; - -/** - * Given a timezone, this function will calculate the offset in milliseconds - * from UTC time. - * - * @param {string} timezone - */ -const getOffsetInMS = (timezone) => { - if (timezone === 'Browser') { - return 0; - } - const offsetInMinutes = moment.tz(timezone).utcOffset(); - const offsetInMS = offsetInMinutes * 1 * 60 * 1000; - return offsetInMS; -}; - -/** - * Class to manage common instantiation behaviors in a view controller - * - * This is expected to be extended, and behavior enabled using super(); - * - * Example: - * uiRoutes.when('/myRoute', { - * template: importedTemplate, - * controllerAs: 'myView', - * controller: class MyView extends MonitoringViewBaseController { - * constructor($injector, $scope) { - * super({ - * title: 'Hello World', - * api: '../api/v1/monitoring/foo/bar', - * defaultData, - * reactNodeId, - * $scope, - * $injector, - * options: { - * enableTimeFilter: false // this will have just the page auto-refresh control show - * } - * }); - * } - * } - * }); - */ -export class MonitoringViewBaseController { - /** - * Create a view controller - * @param {String} title - Title of the page - * @param {String} api - Back-end API endpoint to poll for getting the page - * data using POST and time range data in the body. Whenever possible, use - * this method for data polling rather than supply the getPageData param. - * @param {Function} apiUrlFn - Function that returns a string for the back-end - * API endpoint, in case the string has dynamic query parameters (e.g. - * show_system_indices) rather than supply the getPageData param. - * @param {Function} getPageData - (Optional) Function to fetch page data, if - * simply passing the API string isn't workable. - * @param {Object} defaultData - Initial model data to populate - * @param {String} reactNodeId - DOM element ID of the element for mounting - * the view's main React component - * @param {Service} $injector - Angular dependency injection service - * @param {Service} $scope - Angular view data binding service - * @param {Boolean} options.enableTimeFilter - Whether to show the time filter - * @param {Boolean} options.enableAutoRefresh - Whether to show the auto - * refresh control - */ - constructor({ - title = '', - pageTitle = '', - api = '', - apiUrlFn, - getPageData: _getPageData = getPageData, - defaultData, - reactNodeId = null, // WIP: https://github.com/elastic/x-pack-kibana/issues/5198 - $scope, - $injector, - options = {}, - alerts = { shouldFetch: false, options: {} }, - fetchDataImmediately = true, - telemetryPageViewTitle = '', - }) { - const titleService = $injector.get('title'); - const $executor = $injector.get('$executor'); - const $window = $injector.get('$window'); - const config = $injector.get('config'); - - titleService($scope.cluster, title); - - $scope.pageTitle = pageTitle; - this.setPageTitle = (title) => ($scope.pageTitle = title); - $scope.pageData = this.data = { ...defaultData }; - this._isDataInitialized = false; - this.reactNodeId = reactNodeId; - this.telemetryPageViewTitle = telemetryPageViewTitle || title; - - let deferTimer; - let zoomInLevel = 0; - - const popstateHandler = () => zoomInLevel > 0 && --zoomInLevel; - const removePopstateHandler = () => $window.removeEventListener('popstate', popstateHandler); - const addPopstateHandler = () => $window.addEventListener('popstate', popstateHandler); - - this.zoomInfo = { - zoomOutHandler: () => $window.history.back(), - showZoomOutBtn: () => zoomInLevel > 0, - }; - - const { enableTimeFilter = true, enableAutoRefresh = true } = options; - - async function fetchAlerts() { - const globalState = $injector.get('globalState'); - const bounds = Legacy.shims.timefilter.getBounds(); - const min = bounds.min?.valueOf(); - const max = bounds.max?.valueOf(); - const options = alerts.options || {}; - try { - return await Legacy.shims.http.post( - `/api/monitoring/v1/alert/${globalState.cluster_uuid}/status`, - { - body: JSON.stringify({ - alertTypeIds: options.alertTypeIds, - filters: options.filters, - timeRange: { - min, - max, - }, - }), - } - ); - } catch (err) { - Legacy.shims.toastNotifications.addDanger({ - title: 'Error fetching alert status', - text: err.message, - }); - } - } - - this.updateData = () => { - if (this.updateDataPromise) { - // Do not sent another request if one is inflight - // See https://github.com/elastic/kibana/issues/24082 - this.updateDataPromise.cancel(); - this.updateDataPromise = null; - } - const _api = apiUrlFn ? apiUrlFn() : api; - const promises = [_getPageData($injector, _api, this.getPaginationRouteOptions())]; - if (alerts.shouldFetch) { - promises.push(fetchAlerts()); - } - if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { - promises.push(updateSetupModeData()); - } - this.updateDataPromise = new PromiseWithCancel(Promise.allSettled(promises)); - return this.updateDataPromise.promise().then(([pageData, alerts]) => { - $scope.$apply(() => { - this._isDataInitialized = true; // render will replace loading screen with the react component - $scope.pageData = this.data = pageData.value; // update the view's data with the fetch result - $scope.alerts = this.alerts = alerts && alerts.value ? alerts.value : {}; - }); - }); - }; - - $scope.$applyAsync(() => { - const timefilter = Legacy.shims.timefilter; - - if (enableTimeFilter === false) { - timefilter.disableTimeRangeSelector(); - } else { - timefilter.enableTimeRangeSelector(); - } - - if (enableAutoRefresh === false) { - timefilter.disableAutoRefreshSelector(); - } else { - timefilter.enableAutoRefreshSelector(); - } - - // needed for chart pages - this.onBrush = ({ xaxis }) => { - removePopstateHandler(); - const { to, from } = xaxis; - const timezone = config.get('dateFormat:tz'); - const offset = getOffsetInMS(timezone); - timefilter.setTime({ - from: moment(from - offset), - to: moment(to - offset), - mode: 'absolute', - }); - $executor.cancel(); - $executor.run(); - ++zoomInLevel; - clearTimeout(deferTimer); - /* - Needed to defer 'popstate' event, so it does not fire immediately after it's added. - 10ms is to make sure the event is not added with the same code digest - */ - deferTimer = setTimeout(() => addPopstateHandler(), 10); - }; - - // Render loading state - this.renderReact(null, true); - fetchDataImmediately && this.updateData(); - }); - - $executor.register({ - execute: () => this.updateData(), - }); - $executor.start($scope); - $scope.$on('$destroy', () => { - clearTimeout(deferTimer); - removePopstateHandler(); - const targetElement = document.getElementById(this.reactNodeId); - if (targetElement) { - // WIP https://github.com/elastic/x-pack-kibana/issues/5198 - unmountComponentAtNode(targetElement); - } - $executor.destroy(); - }); - - this.setTitle = (title) => titleService($scope.cluster, title); - } - - renderReact(component, trackPageView = false) { - const renderElement = document.getElementById(this.reactNodeId); - if (!renderElement) { - console.warn(`"#${this.reactNodeId}" element has not been added to the DOM yet`); - return; - } - const I18nContext = Legacy.shims.I18nContext; - const wrappedComponent = ( - - - - - - - {!this._isDataInitialized ? ( - - ) : ( - component - )} - - - - ); - render(wrappedComponent, renderElement); - } - - getPaginationRouteOptions() { - return {}; - } -} diff --git a/x-pack/plugins/monitoring/public/views/base_eui_table_controller.js b/x-pack/plugins/monitoring/public/views/base_eui_table_controller.js deleted file mode 100644 index 0520ce3f10de55..00000000000000 --- a/x-pack/plugins/monitoring/public/views/base_eui_table_controller.js +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MonitoringViewBaseController } from './'; -import { euiTableStorageGetter, euiTableStorageSetter } from '../components/table'; -import { EUI_SORT_ASCENDING } from '../../common/constants'; - -const PAGE_SIZE_OPTIONS = [5, 10, 20, 50]; - -/** - * Class to manage common instantiation behaviors in a view controller - * And add persistent state to a table: - * - page index: in table pagination, which page are we looking at - * - filter text: what filter was entered in the table's filter bar - * - sortKey: which column field of table data is used for sorting - * - sortOrder: is sorting ordered ascending or descending - * - * This is expected to be extended, and behavior enabled using super(); - */ -export class MonitoringViewBaseEuiTableController extends MonitoringViewBaseController { - /** - * Create a table view controller - * - used by parent class: - * @param {String} title - Title of the page - * @param {Function} getPageData - Function to fetch page data - * @param {Service} $injector - Angular dependency injection service - * @param {Service} $scope - Angular view data binding service - * @param {Boolean} options.enableTimeFilter - Whether to show the time filter - * @param {Boolean} options.enableAutoRefresh - Whether to show the auto refresh control - * - specific to this class: - * @param {String} storageKey - the namespace that will be used to keep the state data in the Monitoring localStorage object - * - */ - constructor(args) { - super(args); - const { storageKey, $injector } = args; - const storage = $injector.get('localStorage'); - - const getLocalStorageData = euiTableStorageGetter(storageKey); - const setLocalStorageData = euiTableStorageSetter(storageKey); - const { page, sort } = getLocalStorageData(storage); - - this.pagination = { - pageSize: 20, - initialPageSize: 20, - pageIndex: 0, - initialPageIndex: 0, - pageSizeOptions: PAGE_SIZE_OPTIONS, - }; - - if (page) { - if (!PAGE_SIZE_OPTIONS.includes(page.size)) { - page.size = 20; - } - this.setPagination(page); - } - - this.setSorting(sort); - - this.onTableChange = ({ page, sort }) => { - this.setPagination(page); - this.setSorting({ sort }); - setLocalStorageData(storage, { - page, - sort: { - sort, - }, - }); - if (this.onTableChangeRender) { - this.onTableChangeRender(); - } - }; - - // For pages where we do not fetch immediately, we want to fetch after pagination is applied - args.fetchDataImmediately === false && this.updateData(); - } - - setPagination(page) { - this.pagination = { - initialPageSize: page.size, - pageSize: page.size, - initialPageIndex: page.index, - pageIndex: page.index, - pageSizeOptions: PAGE_SIZE_OPTIONS, - }; - } - - setSorting(sort) { - this.sorting = sort || { sort: {} }; - - if (!this.sorting.sort.field) { - this.sorting.sort.field = 'name'; - } - if (!this.sorting.sort.direction) { - this.sorting.sort.direction = EUI_SORT_ASCENDING; - } - } - - setQueryText(queryText) { - this.queryText = queryText; - } - - getPaginationRouteOptions() { - if (!this.pagination || !this.sorting) { - return {}; - } - - return { - pagination: { - size: this.pagination.pageSize, - index: this.pagination.pageIndex, - }, - ...this.sorting, - queryText: this.queryText, - }; - } - - getPaginationTableProps(pagination) { - return { - sorting: this.sorting, - pagination: pagination, - onTableChange: this.onTableChange, - fetchMoreData: async ({ page, sort, queryText }) => { - this.setPagination(page); - this.setSorting(sort); - this.setQueryText(queryText); - await this.updateData(); - }, - }; - } -} diff --git a/x-pack/plugins/monitoring/public/views/base_table_controller.js b/x-pack/plugins/monitoring/public/views/base_table_controller.js deleted file mode 100644 index a066a91e48c8ba..00000000000000 --- a/x-pack/plugins/monitoring/public/views/base_table_controller.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MonitoringViewBaseController } from './'; -import { tableStorageGetter, tableStorageSetter } from '../components/table'; - -/** - * Class to manage common instantiation behaviors in a view controller - * And add persistent state to a table: - * - page index: in table pagination, which page are we looking at - * - filter text: what filter was entered in the table's filter bar - * - sortKey: which column field of table data is used for sorting - * - sortOrder: is sorting ordered ascending or descending - * - * This is expected to be extended, and behavior enabled using super(); - */ -export class MonitoringViewBaseTableController extends MonitoringViewBaseController { - /** - * Create a table view controller - * - used by parent class: - * @param {String} title - Title of the page - * @param {Function} getPageData - Function to fetch page data - * @param {Service} $injector - Angular dependency injection service - * @param {Service} $scope - Angular view data binding service - * @param {Boolean} options.enableTimeFilter - Whether to show the time filter - * @param {Boolean} options.enableAutoRefresh - Whether to show the auto refresh control - * - specific to this class: - * @param {String} storageKey - the namespace that will be used to keep the state data in the Monitoring localStorage object - * - */ - constructor(args) { - super(args); - const { storageKey, $injector } = args; - const storage = $injector.get('localStorage'); - - const getLocalStorageData = tableStorageGetter(storageKey); - const setLocalStorageData = tableStorageSetter(storageKey); - const { pageIndex, filterText, sortKey, sortOrder } = getLocalStorageData(storage); - - this.pageIndex = pageIndex; - this.filterText = filterText; - this.sortKey = sortKey; - this.sortOrder = sortOrder; - - this.onNewState = (newState) => { - setLocalStorageData(storage, newState); - }; - } -} diff --git a/x-pack/plugins/monitoring/public/views/beats/beat/get_page_data.js b/x-pack/plugins/monitoring/public/views/beats/beat/get_page_data.js deleted file mode 100644 index 7f87fa413d8ca9..00000000000000 --- a/x-pack/plugins/monitoring/public/views/beats/beat/get_page_data.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; -import { Legacy } from '../../../legacy_shims'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/beats/beat/${$route.current.params.beatUuid}`; - const timeBounds = Legacy.shims.timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/plugins/monitoring/public/views/beats/beat/index.html b/x-pack/plugins/monitoring/public/views/beats/beat/index.html deleted file mode 100644 index 6ae727e31cbeb3..00000000000000 --- a/x-pack/plugins/monitoring/public/views/beats/beat/index.html +++ /dev/null @@ -1,11 +0,0 @@ - -
- -
diff --git a/x-pack/plugins/monitoring/public/views/beats/beat/index.js b/x-pack/plugins/monitoring/public/views/beats/beat/index.js deleted file mode 100644 index f1a171a19cd89a..00000000000000 --- a/x-pack/plugins/monitoring/public/views/beats/beat/index.js +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { find } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { routeInitProvider } from '../../../lib/route_init'; -import { MonitoringViewBaseController } from '../../'; -import { getPageData } from './get_page_data'; -import template from './index.html'; -import { CODE_PATH_BEATS } from '../../../../common/constants'; -import { Beat } from '../../../components/beats/beat'; - -uiRoutes.when('/beats/beat/:beatUuid', { - template, - resolve: { - clusters: function (Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_BEATS] }); - }, - pageData: getPageData, - }, - controllerAs: 'beat', - controller: class BeatDetail extends MonitoringViewBaseController { - constructor($injector, $scope) { - // breadcrumbs + page title - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - const pageData = $route.current.locals.pageData; - super({ - title: i18n.translate('xpack.monitoring.beats.instance.routeTitle', { - defaultMessage: 'Beats - {instanceName} - Overview', - values: { - instanceName: pageData.summary.name, - }, - }), - pageTitle: i18n.translate('xpack.monitoring.beats.instance.pageTitle', { - defaultMessage: 'Beat instance: {beatName}', - values: { - beatName: pageData.summary.name, - }, - }), - telemetryPageViewTitle: 'beats_instance', - getPageData, - $scope, - $injector, - reactNodeId: 'monitoringBeatsInstanceApp', - }); - - this.data = pageData; - $scope.$watch( - () => this.data, - (data) => { - this.renderReact( - - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/beats/listing/get_page_data.js b/x-pack/plugins/monitoring/public/views/beats/listing/get_page_data.js deleted file mode 100644 index 99366f05f3ad44..00000000000000 --- a/x-pack/plugins/monitoring/public/views/beats/listing/get_page_data.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; -import { Legacy } from '../../../legacy_shims'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const timeBounds = Legacy.shims.timefilter.getBounds(); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/beats/beats`; - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/plugins/monitoring/public/views/beats/listing/index.html b/x-pack/plugins/monitoring/public/views/beats/listing/index.html deleted file mode 100644 index 0ce66a6848dfd2..00000000000000 --- a/x-pack/plugins/monitoring/public/views/beats/listing/index.html +++ /dev/null @@ -1,7 +0,0 @@ - -
-
\ No newline at end of file diff --git a/x-pack/plugins/monitoring/public/views/beats/listing/index.js b/x-pack/plugins/monitoring/public/views/beats/listing/index.js deleted file mode 100644 index eae74d8a08b9e1..00000000000000 --- a/x-pack/plugins/monitoring/public/views/beats/listing/index.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { find } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { routeInitProvider } from '../../../lib/route_init'; -import { MonitoringViewBaseEuiTableController } from '../../'; -import { getPageData } from './get_page_data'; -import template from './index.html'; -import React from 'react'; -import { Listing } from '../../../components/beats/listing/listing'; -import { SetupModeRenderer } from '../../../components/renderers'; -import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; -import { CODE_PATH_BEATS, BEATS_SYSTEM_ID } from '../../../../common/constants'; - -uiRoutes.when('/beats/beats', { - template, - resolve: { - clusters: function (Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_BEATS] }); - }, - pageData: getPageData, - }, - controllerAs: 'beats', - controller: class BeatsListing extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - // breadcrumbs + page title - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - super({ - title: i18n.translate('xpack.monitoring.beats.routeTitle', { defaultMessage: 'Beats' }), - pageTitle: i18n.translate('xpack.monitoring.beats.listing.pageTitle', { - defaultMessage: 'Beats listing', - }), - telemetryPageViewTitle: 'beats_listing', - storageKey: 'beats.beats', - getPageData, - reactNodeId: 'monitoringBeatsInstancesApp', - $scope, - $injector, - }); - - this.data = $route.current.locals.pageData; - this.scope = $scope; - this.injector = $injector; - this.onTableChangeRender = this.renderComponent; - - $scope.$watch( - () => this.data, - () => this.renderComponent() - ); - } - - renderComponent() { - const { sorting, pagination, onTableChange } = this.scope.beats; - this.renderReact( - ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/beats/overview/get_page_data.js b/x-pack/plugins/monitoring/public/views/beats/overview/get_page_data.js deleted file mode 100644 index 497ed8cdb0e74b..00000000000000 --- a/x-pack/plugins/monitoring/public/views/beats/overview/get_page_data.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; -import { Legacy } from '../../../legacy_shims'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const timeBounds = Legacy.shims.timefilter.getBounds(); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/beats`; - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/plugins/monitoring/public/views/beats/overview/index.html b/x-pack/plugins/monitoring/public/views/beats/overview/index.html deleted file mode 100644 index 0b827c96f68fd3..00000000000000 --- a/x-pack/plugins/monitoring/public/views/beats/overview/index.html +++ /dev/null @@ -1,7 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/beats/overview/index.js b/x-pack/plugins/monitoring/public/views/beats/overview/index.js deleted file mode 100644 index 475a63d440c76c..00000000000000 --- a/x-pack/plugins/monitoring/public/views/beats/overview/index.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { find } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { routeInitProvider } from '../../../lib/route_init'; -import { MonitoringViewBaseController } from '../../'; -import { getPageData } from './get_page_data'; -import template from './index.html'; -import { CODE_PATH_BEATS } from '../../../../common/constants'; -import { BeatsOverview } from '../../../components/beats/overview'; - -uiRoutes.when('/beats', { - template, - resolve: { - clusters: function (Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_BEATS] }); - }, - pageData: getPageData, - }, - controllerAs: 'beats', - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - // breadcrumbs + page title - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - super({ - title: i18n.translate('xpack.monitoring.beats.overview.routeTitle', { - defaultMessage: 'Beats - Overview', - }), - pageTitle: i18n.translate('xpack.monitoring.beats.overview.pageTitle', { - defaultMessage: 'Beats overview', - }), - getPageData, - $scope, - $injector, - reactNodeId: 'monitoringBeatsOverviewApp', - }); - - this.data = $route.current.locals.pageData; - $scope.$watch( - () => this.data, - (data) => { - this.renderReact( - - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/cluster/listing/index.html b/x-pack/plugins/monitoring/public/views/cluster/listing/index.html deleted file mode 100644 index 713ca8fb1ffc9d..00000000000000 --- a/x-pack/plugins/monitoring/public/views/cluster/listing/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/cluster/listing/index.js b/x-pack/plugins/monitoring/public/views/cluster/listing/index.js deleted file mode 100644 index 8b365292aeb139..00000000000000 --- a/x-pack/plugins/monitoring/public/views/cluster/listing/index.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { routeInitProvider } from '../../../lib/route_init'; -import { MonitoringViewBaseEuiTableController } from '../../'; -import template from './index.html'; -import { Listing } from '../../../components/cluster/listing'; -import { CODE_PATH_ALL } from '../../../../common/constants'; -import { EnableAlertsModal } from '../../../alerts/enable_alerts_modal.tsx'; - -const CODE_PATHS = [CODE_PATH_ALL]; - -const getPageData = ($injector) => { - const monitoringClusters = $injector.get('monitoringClusters'); - return monitoringClusters(undefined, undefined, CODE_PATHS); -}; - -const getAlerts = (clusters) => { - return clusters.reduce((alerts, cluster) => ({ ...alerts, ...cluster.alerts.list }), {}); -}; - -uiRoutes - .when('/home', { - template, - resolve: { - clusters: (Private) => { - const routeInit = Private(routeInitProvider); - return routeInit({ - codePaths: CODE_PATHS, - fetchAllClusters: true, - unsetGlobalState: true, - }).then((clusters) => { - if (!clusters || !clusters.length) { - window.location.hash = '#/no-data'; - return Promise.reject(); - } - if (clusters.length === 1) { - // Bypass the cluster listing if there is just 1 cluster - window.history.replaceState(null, null, '#/overview'); - return Promise.reject(); - } - return clusters; - }); - }, - }, - controllerAs: 'clusters', - controller: class ClustersList extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - super({ - storageKey: 'clusters', - pageTitle: i18n.translate('xpack.monitoring.cluster.listing.pageTitle', { - defaultMessage: 'Cluster listing', - }), - getPageData, - $scope, - $injector, - reactNodeId: 'monitoringClusterListingApp', - telemetryPageViewTitle: 'cluster_listing', - }); - - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - const storage = $injector.get('localStorage'); - const showLicenseExpiration = $injector.get('showLicenseExpiration'); - - this.data = $route.current.locals.clusters; - - $scope.$watch( - () => this.data, - (data) => { - this.renderReact( - <> - - - - ); - } - ); - } - }, - }) - .otherwise({ redirectTo: '/loading' }); diff --git a/x-pack/plugins/monitoring/public/views/cluster/overview/index.html b/x-pack/plugins/monitoring/public/views/cluster/overview/index.html deleted file mode 100644 index 1762ee1c2a2823..00000000000000 --- a/x-pack/plugins/monitoring/public/views/cluster/overview/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/cluster/overview/index.js b/x-pack/plugins/monitoring/public/views/cluster/overview/index.js deleted file mode 100644 index 20e694ad8548f0..00000000000000 --- a/x-pack/plugins/monitoring/public/views/cluster/overview/index.js +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { isEmpty } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { routeInitProvider } from '../../../lib/route_init'; -import template from './index.html'; -import { MonitoringViewBaseController } from '../../'; -import { Overview } from '../../../components/cluster/overview'; -import { SetupModeRenderer } from '../../../components/renderers'; -import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; -import { CODE_PATH_ALL } from '../../../../common/constants'; -import { EnableAlertsModal } from '../../../alerts/enable_alerts_modal.tsx'; - -const CODE_PATHS = [CODE_PATH_ALL]; - -uiRoutes.when('/overview', { - template, - resolve: { - clusters(Private) { - // checks license info of all monitored clusters for multi-cluster monitoring usage and capability - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: CODE_PATHS }); - }, - }, - controllerAs: 'monitoringClusterOverview', - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - const monitoringClusters = $injector.get('monitoringClusters'); - const globalState = $injector.get('globalState'); - const showLicenseExpiration = $injector.get('showLicenseExpiration'); - - super({ - title: i18n.translate('xpack.monitoring.cluster.overviewTitle', { - defaultMessage: 'Overview', - }), - pageTitle: i18n.translate('xpack.monitoring.cluster.overview.pageTitle', { - defaultMessage: 'Cluster overview', - }), - defaultData: {}, - getPageData: async () => { - const clusters = await monitoringClusters( - globalState.cluster_uuid, - globalState.ccs, - CODE_PATHS - ); - return clusters[0]; - }, - reactNodeId: 'monitoringClusterOverviewApp', - $scope, - $injector, - alerts: { - shouldFetch: true, - }, - telemetryPageViewTitle: 'cluster_overview', - }); - - this.init = () => this.renderReact(null); - - $scope.$watch( - () => this.data, - async (data) => { - if (isEmpty(data)) { - return; - } - - this.renderReact( - ( - - {flyoutComponent} - - - {bottomBarComponent} - - )} - /> - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/get_page_data.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/get_page_data.js deleted file mode 100644 index 4f450389863329..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/get_page_data.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; -import { Legacy } from '../../../legacy_shims'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const timeBounds = Legacy.shims.timefilter.getBounds(); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/ccr`; - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.html deleted file mode 100644 index ca0b036ae39e12..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.html +++ /dev/null @@ -1,7 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js deleted file mode 100644 index 91cc9c8782b22d..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { getPageData } from './get_page_data'; -import { routeInitProvider } from '../../../lib/route_init'; -import template from './index.html'; -import { Ccr } from '../../../components/elasticsearch/ccr'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { - CODE_PATH_ELASTICSEARCH, - RULE_CCR_READ_EXCEPTIONS, - ELASTICSEARCH_SYSTEM_ID, -} from '../../../../common/constants'; -import { SetupModeRenderer } from '../../../components/renderers'; -import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; - -uiRoutes.when('/elasticsearch/ccr', { - template, - resolve: { - clusters: function (Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - pageData: getPageData, - }, - controllerAs: 'elasticsearchCcr', - controller: class ElasticsearchCcrController extends MonitoringViewBaseController { - constructor($injector, $scope) { - super({ - title: i18n.translate('xpack.monitoring.elasticsearch.ccr.routeTitle', { - defaultMessage: 'Elasticsearch - Ccr', - }), - pageTitle: i18n.translate('xpack.monitoring.elasticsearch.ccr.pageTitle', { - defaultMessage: 'Elasticsearch Ccr', - }), - reactNodeId: 'elasticsearchCcrReact', - getPageData, - $scope, - $injector, - alerts: { - shouldFetch: true, - options: { - alertTypeIds: [RULE_CCR_READ_EXCEPTIONS], - }, - }, - }); - - $scope.$watch( - () => this.data, - (data) => { - if (!data) { - return; - } - this.renderReact( - ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/get_page_data.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/get_page_data.js deleted file mode 100644 index ca1aad39e3610d..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/get_page_data.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ajaxErrorHandlersProvider } from '../../../../lib/ajax_error_handler'; -import { Legacy } from '../../../../legacy_shims'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - const timeBounds = Legacy.shims.timefilter.getBounds(); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/ccr/${$route.current.params.index}/shard/${$route.current.params.shardId}`; - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.html deleted file mode 100644 index 76469e5d9add50..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.html +++ /dev/null @@ -1,8 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js deleted file mode 100644 index 767fb18685633b..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; -import { uiRoutes } from '../../../../angular/helpers/routes'; -import { getPageData } from './get_page_data'; -import { routeInitProvider } from '../../../../lib/route_init'; -import template from './index.html'; -import { MonitoringViewBaseController } from '../../../base_controller'; -import { CcrShard } from '../../../../components/elasticsearch/ccr_shard'; -import { - CODE_PATH_ELASTICSEARCH, - RULE_CCR_READ_EXCEPTIONS, - ELASTICSEARCH_SYSTEM_ID, -} from '../../../../../common/constants'; -import { SetupModeRenderer } from '../../../../components/renderers'; -import { SetupModeContext } from '../../../../components/setup_mode/setup_mode_context'; - -uiRoutes.when('/elasticsearch/ccr/:index/shard/:shardId', { - template, - resolve: { - clusters: function (Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - pageData: getPageData, - }, - controllerAs: 'elasticsearchCcr', - controller: class ElasticsearchCcrController extends MonitoringViewBaseController { - constructor($injector, $scope, pageData) { - const $route = $injector.get('$route'); - super({ - title: i18n.translate('xpack.monitoring.elasticsearch.ccr.shard.routeTitle', { - defaultMessage: 'Elasticsearch - Ccr - Shard', - }), - reactNodeId: 'elasticsearchCcrShardReact', - getPageData, - $scope, - $injector, - alerts: { - shouldFetch: true, - options: { - alertTypeIds: [RULE_CCR_READ_EXCEPTIONS], - filters: [ - { - shardId: $route.current.pathParams.shardId, - }, - ], - }, - }, - }); - - $scope.instance = i18n.translate('xpack.monitoring.elasticsearch.ccr.shard.instanceTitle', { - defaultMessage: 'Index: {followerIndex} Shard: {shardId}', - values: { - followerIndex: get(pageData, 'stat.follower_index'), - shardId: get(pageData, 'stat.shard_id'), - }, - }); - - $scope.$watch( - () => this.data, - (data) => { - if (!data) { - return; - } - - this.setPageTitle( - i18n.translate('xpack.monitoring.elasticsearch.ccr.shard.pageTitle', { - defaultMessage: 'Elasticsearch Ccr Shard - Index: {followerIndex} Shard: {shardId}', - values: { - followerIndex: get( - pageData, - 'stat.follower.index', - get(pageData, 'stat.follower_index') - ), - shardId: get( - pageData, - 'stat.follower.shard.number', - get(pageData, 'stat.shard_id') - ), - }, - }) - ); - - this.renderReact( - ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.html deleted file mode 100644 index 159376148d1734..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.html +++ /dev/null @@ -1,8 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js deleted file mode 100644 index 92765279516126..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** - * Controller for Advanced Index Detail - */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { uiRoutes } from '../../../../angular/helpers/routes'; -import { ajaxErrorHandlersProvider } from '../../../../lib/ajax_error_handler'; -import { routeInitProvider } from '../../../../lib/route_init'; -import template from './index.html'; -import { Legacy } from '../../../../legacy_shims'; -import { AdvancedIndex } from '../../../../components/elasticsearch/index/advanced'; -import { MonitoringViewBaseController } from '../../../base_controller'; -import { - CODE_PATH_ELASTICSEARCH, - RULE_LARGE_SHARD_SIZE, - ELASTICSEARCH_SYSTEM_ID, -} from '../../../../../common/constants'; -import { SetupModeContext } from '../../../../components/setup_mode/setup_mode_context'; -import { SetupModeRenderer } from '../../../../components/renderers'; - -function getPageData($injector) { - const globalState = $injector.get('globalState'); - const $route = $injector.get('$route'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/indices/${$route.current.params.index}`; - const $http = $injector.get('$http'); - const timeBounds = Legacy.shims.timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - is_advanced: true, - }) - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/elasticsearch/indices/:index/advanced', { - template, - resolve: { - clusters: function (Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - pageData: getPageData, - }, - controllerAs: 'monitoringElasticsearchAdvancedIndexApp', - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const indexName = $route.current.params.index; - - super({ - title: i18n.translate('xpack.monitoring.elasticsearch.indices.advanced.routeTitle', { - defaultMessage: 'Elasticsearch - Indices - {indexName} - Advanced', - values: { - indexName, - }, - }), - telemetryPageViewTitle: 'elasticsearch_index_advanced', - defaultData: {}, - getPageData, - reactNodeId: 'monitoringElasticsearchAdvancedIndexApp', - $scope, - $injector, - alerts: { - shouldFetch: true, - options: { - alertTypeIds: [RULE_LARGE_SHARD_SIZE], - filters: [ - { - shardIndex: $route.current.pathParams.index, - }, - ], - }, - }, - }); - - this.indexName = indexName; - - $scope.$watch( - () => this.data, - (data) => { - this.renderReact( - ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.html deleted file mode 100644 index 84d90f184358d8..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.html +++ /dev/null @@ -1,9 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js deleted file mode 100644 index c9efb622ff9d1f..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** - * Controller for single index detail - */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { routeInitProvider } from '../../../lib/route_init'; -import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; -import template from './index.html'; -import { Legacy } from '../../../legacy_shims'; -import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels'; -import { indicesByNodes } from '../../../components/elasticsearch/shard_allocation/transformers/indices_by_nodes'; -import { Index } from '../../../components/elasticsearch/index/index'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { - CODE_PATH_ELASTICSEARCH, - RULE_LARGE_SHARD_SIZE, - ELASTICSEARCH_SYSTEM_ID, -} from '../../../../common/constants'; -import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; -import { SetupModeRenderer } from '../../../components/renderers'; - -function getPageData($injector) { - const $http = $injector.get('$http'); - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/indices/${$route.current.params.index}`; - const timeBounds = Legacy.shims.timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - is_advanced: false, - }) - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/elasticsearch/indices/:index', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - pageData: getPageData, - }, - controllerAs: 'monitoringElasticsearchIndexApp', - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const indexName = $route.current.params.index; - - super({ - title: i18n.translate('xpack.monitoring.elasticsearch.indices.overview.routeTitle', { - defaultMessage: 'Elasticsearch - Indices - {indexName} - Overview', - values: { - indexName, - }, - }), - telemetryPageViewTitle: 'elasticsearch_index', - pageTitle: i18n.translate('xpack.monitoring.elasticsearch.indices.overview.pageTitle', { - defaultMessage: 'Index: {indexName}', - values: { - indexName, - }, - }), - defaultData: {}, - getPageData, - reactNodeId: 'monitoringElasticsearchIndexApp', - $scope, - $injector, - alerts: { - shouldFetch: true, - options: { - alertTypeIds: [RULE_LARGE_SHARD_SIZE], - filters: [ - { - shardIndex: $route.current.pathParams.index, - }, - ], - }, - }, - }); - - this.indexName = indexName; - const transformer = indicesByNodes(); - - $scope.$watch( - () => this.data, - (data) => { - if (!data || !data.shards) { - return; - } - - const shards = data.shards; - $scope.totalCount = shards.length; - $scope.showing = transformer(shards, data.nodes); - $scope.labels = labels.node; - if (shards.some((shard) => shard.state === 'UNASSIGNED')) { - $scope.labels = labels.indexWithUnassigned; - } else { - $scope.labels = labels.index; - } - - this.renderReact( - ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.html deleted file mode 100644 index 84013078e0ef1a..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.html +++ /dev/null @@ -1,8 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js deleted file mode 100644 index 5acff8be20dcfc..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { find } from 'lodash'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { routeInitProvider } from '../../../lib/route_init'; -import { MonitoringViewBaseEuiTableController } from '../../'; -import { ElasticsearchIndices } from '../../../components'; -import template from './index.html'; -import { - CODE_PATH_ELASTICSEARCH, - ELASTICSEARCH_SYSTEM_ID, - RULE_LARGE_SHARD_SIZE, -} from '../../../../common/constants'; -import { SetupModeRenderer } from '../../../components/renderers'; -import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; - -uiRoutes.when('/elasticsearch/indices', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - }, - controllerAs: 'elasticsearchIndices', - controller: class ElasticsearchIndicesController extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - const features = $injector.get('features'); - - const { cluster_uuid: clusterUuid } = globalState; - $scope.cluster = find($route.current.locals.clusters, { cluster_uuid: clusterUuid }); - - let showSystemIndices = features.isEnabled('showSystemIndices', false); - - super({ - title: i18n.translate('xpack.monitoring.elasticsearch.indices.routeTitle', { - defaultMessage: 'Elasticsearch - Indices', - }), - pageTitle: i18n.translate('xpack.monitoring.elasticsearch.indices.pageTitle', { - defaultMessage: 'Elasticsearch indices', - }), - storageKey: 'elasticsearch.indices', - apiUrlFn: () => - `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/indices?show_system_indices=${showSystemIndices}`, - reactNodeId: 'elasticsearchIndicesReact', - defaultData: {}, - $scope, - $injector, - $scope, - $injector, - alerts: { - shouldFetch: true, - options: { - alertTypeIds: [RULE_LARGE_SHARD_SIZE], - }, - }, - }); - - this.isCcrEnabled = $scope.cluster.isCcrEnabled; - - // for binding - const toggleShowSystemIndices = (isChecked) => { - // flip the boolean - showSystemIndices = isChecked; - // preserve setting in localStorage - features.update('showSystemIndices', isChecked); - // update the page (resets pagination and sorting) - this.updateData(); - }; - - const renderComponent = () => { - const { clusterStatus, indices } = this.data; - this.renderReact( - ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> - ); - }; - - this.onTableChangeRender = renderComponent; - - $scope.$watch( - () => this.data, - (data) => { - if (!data) { - return; - } - renderComponent(); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/get_page_data.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/get_page_data.js deleted file mode 100644 index 39bd2686069de9..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/get_page_data.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; -import { Legacy } from '../../../legacy_shims'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/ml_jobs`; - const timeBounds = Legacy.shims.timefilter.getBounds(); - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html deleted file mode 100644 index 6fdae46b6b6ed9..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.js deleted file mode 100644 index d44b782f3994bc..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { find } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { routeInitProvider } from '../../../lib/route_init'; -import { MonitoringViewBaseEuiTableController } from '../../'; -import { getPageData } from './get_page_data'; -import template from './index.html'; -import { CODE_PATH_ELASTICSEARCH, CODE_PATH_ML } from '../../../../common/constants'; - -uiRoutes.when('/elasticsearch/ml_jobs', { - template, - resolve: { - clusters: function (Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH, CODE_PATH_ML] }); - }, - pageData: getPageData, - }, - controllerAs: 'mlJobs', - controller: class MlJobsList extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - super({ - title: i18n.translate('xpack.monitoring.elasticsearch.mlJobs.routeTitle', { - defaultMessage: 'Elasticsearch - Machine Learning Jobs', - }), - pageTitle: i18n.translate('xpack.monitoring.elasticsearch.mlJobs.pageTitle', { - defaultMessage: 'Elasticsearch machine learning jobs', - }), - storageKey: 'elasticsearch.mlJobs', - getPageData, - $scope, - $injector, - }); - - const $route = $injector.get('$route'); - this.data = $route.current.locals.pageData; - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - this.isCcrEnabled = Boolean($scope.cluster && $scope.cluster.isCcrEnabled); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.html deleted file mode 100644 index c79c4eed46bb73..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.html +++ /dev/null @@ -1,11 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js deleted file mode 100644 index dc0456178fbffb..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** - * Controller for Advanced Node Detail - */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; -import { uiRoutes } from '../../../../angular/helpers/routes'; -import { ajaxErrorHandlersProvider } from '../../../../lib/ajax_error_handler'; -import { routeInitProvider } from '../../../../lib/route_init'; -import template from './index.html'; -import { Legacy } from '../../../../legacy_shims'; -import { AdvancedNode } from '../../../../components/elasticsearch/node/advanced'; -import { MonitoringViewBaseController } from '../../../base_controller'; -import { - CODE_PATH_ELASTICSEARCH, - RULE_CPU_USAGE, - RULE_THREAD_POOL_SEARCH_REJECTIONS, - RULE_THREAD_POOL_WRITE_REJECTIONS, - RULE_MISSING_MONITORING_DATA, - RULE_DISK_USAGE, - RULE_MEMORY_USAGE, -} from '../../../../../common/constants'; - -function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const $route = $injector.get('$route'); - const timeBounds = Legacy.shims.timefilter.getBounds(); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/nodes/${$route.current.params.node}`; - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - is_advanced: true, - }) - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/elasticsearch/nodes/:node/advanced', { - template, - resolve: { - clusters: function (Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - pageData: getPageData, - }, - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const nodeName = $route.current.params.node; - - super({ - defaultData: {}, - getPageData, - reactNodeId: 'monitoringElasticsearchAdvancedNodeApp', - telemetryPageViewTitle: 'elasticsearch_node_advanced', - $scope, - $injector, - alerts: { - shouldFetch: true, - options: { - alertTypeIds: [ - RULE_CPU_USAGE, - RULE_DISK_USAGE, - RULE_THREAD_POOL_SEARCH_REJECTIONS, - RULE_THREAD_POOL_WRITE_REJECTIONS, - RULE_MEMORY_USAGE, - RULE_MISSING_MONITORING_DATA, - ], - filters: [ - { - nodeUuid: nodeName, - }, - ], - }, - }, - }); - - $scope.$watch( - () => this.data, - (data) => { - if (!data || !data.nodeSummary) { - return; - } - - this.setTitle( - i18n.translate('xpack.monitoring.elasticsearch.node.advanced.routeTitle', { - defaultMessage: 'Elasticsearch - Nodes - {nodeSummaryName} - Advanced', - values: { - nodeSummaryName: get(data, 'nodeSummary.name'), - }, - }) - ); - - this.setPageTitle( - i18n.translate('xpack.monitoring.elasticsearch.node.overview.pageTitle', { - defaultMessage: 'Elasticsearch node: {node}', - values: { - node: get(data, 'nodeSummary.name'), - }, - }) - ); - - this.renderReact( - - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/get_page_data.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/get_page_data.js deleted file mode 100644 index 1d8bc3f3efa328..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/get_page_data.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; -import { Legacy } from '../../../legacy_shims'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const $route = $injector.get('$route'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/nodes/${$route.current.params.node}`; - const features = $injector.get('features'); - const showSystemIndices = features.isEnabled('showSystemIndices', false); - const timeBounds = Legacy.shims.timefilter.getBounds(); - - return $http - .post(url, { - showSystemIndices, - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - is_advanced: false, - }) - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.html deleted file mode 100644 index 1c3b32728cecd6..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.html +++ /dev/null @@ -1,11 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js deleted file mode 100644 index 3ec10aa9d4a4ce..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** - * Controller for Node Detail - */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { get, partial } from 'lodash'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { routeInitProvider } from '../../../lib/route_init'; -import { getPageData } from './get_page_data'; -import template from './index.html'; -import { SetupModeRenderer } from '../../../components/renderers'; -import { Node } from '../../../components/elasticsearch/node/node'; -import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels'; -import { nodesByIndices } from '../../../components/elasticsearch/shard_allocation/transformers/nodes_by_indices'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { - CODE_PATH_ELASTICSEARCH, - RULE_CPU_USAGE, - RULE_THREAD_POOL_SEARCH_REJECTIONS, - RULE_THREAD_POOL_WRITE_REJECTIONS, - RULE_MISSING_MONITORING_DATA, - RULE_DISK_USAGE, - RULE_MEMORY_USAGE, - ELASTICSEARCH_SYSTEM_ID, -} from '../../../../common/constants'; -import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; - -uiRoutes.when('/elasticsearch/nodes/:node', { - template, - resolve: { - clusters: function (Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - pageData: getPageData, - }, - controllerAs: 'monitoringElasticsearchNodeApp', - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const nodeName = $route.current.params.node; - - super({ - title: i18n.translate('xpack.monitoring.elasticsearch.node.overview.routeTitle', { - defaultMessage: 'Elasticsearch - Nodes - {nodeName} - Overview', - values: { - nodeName, - }, - }), - telemetryPageViewTitle: 'elasticsearch_node', - defaultData: {}, - getPageData, - reactNodeId: 'monitoringElasticsearchNodeApp', - $scope, - $injector, - alerts: { - shouldFetch: true, - options: { - alertTypeIds: [ - RULE_CPU_USAGE, - RULE_DISK_USAGE, - RULE_THREAD_POOL_SEARCH_REJECTIONS, - RULE_THREAD_POOL_WRITE_REJECTIONS, - RULE_MEMORY_USAGE, - RULE_MISSING_MONITORING_DATA, - ], - filters: [ - { - nodeUuid: nodeName, - }, - ], - }, - }, - }); - - this.nodeName = nodeName; - - const features = $injector.get('features'); - const callPageData = partial(getPageData, $injector); - // show/hide system indices in shard allocation view - $scope.showSystemIndices = features.isEnabled('showSystemIndices', false); - $scope.toggleShowSystemIndices = (isChecked) => { - $scope.showSystemIndices = isChecked; - // preserve setting in localStorage - features.update('showSystemIndices', isChecked); - // update the page - callPageData().then((data) => (this.data = data)); - }; - - const transformer = nodesByIndices(); - $scope.$watch( - () => this.data, - (data) => { - if (!data || !data.shards) { - return; - } - - this.setTitle( - i18n.translate('xpack.monitoring.elasticsearch.node.overview.routeTitle', { - defaultMessage: 'Elasticsearch - Nodes - {nodeName} - Overview', - values: { - nodeName: get(data, 'nodeSummary.name'), - }, - }) - ); - - this.setPageTitle( - i18n.translate('xpack.monitoring.elasticsearch.node.overview.pageTitle', { - defaultMessage: 'Elasticsearch node: {node}', - values: { - node: get(data, 'nodeSummary.name'), - }, - }) - ); - - const shards = data.shards; - $scope.totalCount = shards.length; - $scope.showing = transformer(shards, data.nodes); - $scope.labels = labels.node; - - this.renderReact( - ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.html deleted file mode 100644 index 95a483a59f20ce..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.html +++ /dev/null @@ -1,8 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js deleted file mode 100644 index 5bc546e8590ad6..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { find } from 'lodash'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { Legacy } from '../../../legacy_shims'; -import template from './index.html'; -import { routeInitProvider } from '../../../lib/route_init'; -import { MonitoringViewBaseEuiTableController } from '../../'; -import { ElasticsearchNodes } from '../../../components'; -import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; -import { SetupModeRenderer } from '../../../components/renderers'; -import { - ELASTICSEARCH_SYSTEM_ID, - CODE_PATH_ELASTICSEARCH, - RULE_CPU_USAGE, - RULE_THREAD_POOL_SEARCH_REJECTIONS, - RULE_THREAD_POOL_WRITE_REJECTIONS, - RULE_MISSING_MONITORING_DATA, - RULE_DISK_USAGE, - RULE_MEMORY_USAGE, -} from '../../../../common/constants'; -import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; - -uiRoutes.when('/elasticsearch/nodes', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - }, - controllerAs: 'elasticsearchNodes', - controller: class ElasticsearchNodesController extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - const showCgroupMetricsElasticsearch = $injector.get('showCgroupMetricsElasticsearch'); - - $scope.cluster = - find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }) || {}; - - const getPageData = ($injector, _api = undefined, routeOptions = {}) => { - _api; // to fix eslint - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const timeBounds = Legacy.shims.timefilter.getBounds(); - - const getNodes = (clusterUuid = globalState.cluster_uuid) => - $http.post(`../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/nodes`, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - ...routeOptions, - }); - - const promise = globalState.cluster_uuid - ? getNodes() - : new Promise((resolve) => resolve({ data: {} })); - return promise - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); - }; - - super({ - title: i18n.translate('xpack.monitoring.elasticsearch.nodes.routeTitle', { - defaultMessage: 'Elasticsearch - Nodes', - }), - pageTitle: i18n.translate('xpack.monitoring.elasticsearch.nodes.pageTitle', { - defaultMessage: 'Elasticsearch nodes', - }), - storageKey: 'elasticsearch.nodes', - reactNodeId: 'elasticsearchNodesReact', - defaultData: {}, - getPageData, - $scope, - $injector, - fetchDataImmediately: false, // We want to apply pagination before sending the first request, - alerts: { - shouldFetch: true, - options: { - alertTypeIds: [ - RULE_CPU_USAGE, - RULE_DISK_USAGE, - RULE_THREAD_POOL_SEARCH_REJECTIONS, - RULE_THREAD_POOL_WRITE_REJECTIONS, - RULE_MEMORY_USAGE, - RULE_MISSING_MONITORING_DATA, - ], - }, - }, - }); - - this.isCcrEnabled = $scope.cluster.isCcrEnabled; - - $scope.$watch( - () => this.data, - (data) => { - if (!data) { - return; - } - - const { clusterStatus, nodes, totalNodeCount } = data; - const pagination = { - ...this.pagination, - totalItemCount: totalNodeCount, - }; - - this.renderReact( - ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/overview/controller.js b/x-pack/plugins/monitoring/public/views/elasticsearch/overview/controller.js deleted file mode 100644 index f39033fe7014db..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/overview/controller.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { find } from 'lodash'; -import { MonitoringViewBaseController } from '../../'; -import { ElasticsearchOverview } from '../../../components'; - -export class ElasticsearchOverviewController extends MonitoringViewBaseController { - constructor($injector, $scope) { - // breadcrumbs + page title - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - super({ - title: 'Elasticsearch', - pageTitle: i18n.translate('xpack.monitoring.elasticsearch.overview.pageTitle', { - defaultMessage: 'Elasticsearch overview', - }), - api: `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch`, - defaultData: { - clusterStatus: { status: '' }, - metrics: null, - shardActivity: null, - }, - reactNodeId: 'elasticsearchOverviewReact', - $scope, - $injector, - }); - - this.isCcrEnabled = $scope.cluster.isCcrEnabled; - this.showShardActivityHistory = false; - this.toggleShardActivityHistory = () => { - this.showShardActivityHistory = !this.showShardActivityHistory; - $scope.$evalAsync(() => { - this.renderReact(this.data, $scope.cluster); - }); - }; - - this.initScope($scope); - } - - initScope($scope) { - $scope.$watch( - () => this.data, - (data) => { - this.renderReact(data, $scope.cluster); - } - ); - - // HACK to force table to re-render even if data hasn't changed. This - // happens when the data remains empty after turning on showHistory. The - // button toggle needs to update the "no data" message based on the value of showHistory - $scope.$watch( - () => this.showShardActivityHistory, - () => { - const { data } = this; - const dataWithShardActivityLoading = { ...data, shardActivity: null }; - // force shard activity to rerender by manipulating and then re-setting its data prop - this.renderReact(dataWithShardActivityLoading, $scope.cluster); - this.renderReact(data, $scope.cluster); - } - ); - } - - filterShardActivityData(shardActivity) { - return shardActivity.filter((row) => { - return this.showShardActivityHistory || row.stage !== 'DONE'; - }); - } - - renderReact(data, cluster) { - // All data needs to originate in this view, and get passed as a prop to the components, for statelessness - const { clusterStatus, metrics, shardActivity, logs } = data || {}; - const shardActivityData = shardActivity && this.filterShardActivityData(shardActivity); // no filter on data = null - const component = ( - - ); - - super.renderReact(component); - } -} diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/overview/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/overview/index.html deleted file mode 100644 index 127c48add5e8df..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/overview/index.html +++ /dev/null @@ -1,8 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/overview/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/overview/index.js deleted file mode 100644 index cc507934dd767c..00000000000000 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/overview/index.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { uiRoutes } from '../../../angular/helpers/routes'; -import { routeInitProvider } from '../../../lib/route_init'; -import template from './index.html'; -import { ElasticsearchOverviewController } from './controller'; -import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; - -uiRoutes.when('/elasticsearch', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - }, - controllerAs: 'elasticsearchOverview', - controller: ElasticsearchOverviewController, -}); diff --git a/x-pack/plugins/monitoring/public/views/index.js b/x-pack/plugins/monitoring/public/views/index.js deleted file mode 100644 index 8cfb8f35e68baa..00000000000000 --- a/x-pack/plugins/monitoring/public/views/index.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { MonitoringViewBaseController } from './base_controller'; -export { MonitoringViewBaseTableController } from './base_table_controller'; -export { MonitoringViewBaseEuiTableController } from './base_eui_table_controller'; diff --git a/x-pack/plugins/monitoring/public/views/kibana/instance/index.html b/x-pack/plugins/monitoring/public/views/kibana/instance/index.html deleted file mode 100644 index 8bb17839683a89..00000000000000 --- a/x-pack/plugins/monitoring/public/views/kibana/instance/index.html +++ /dev/null @@ -1,8 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js deleted file mode 100644 index a71289b084516b..00000000000000 --- a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* - * Kibana Instance - */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; -import { routeInitProvider } from '../../../lib/route_init'; -import template from './index.html'; -import { Legacy } from '../../../legacy_shims'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiSpacer, - EuiFlexGrid, - EuiFlexItem, - EuiPanel, -} from '@elastic/eui'; -import { MonitoringTimeseriesContainer } from '../../../components/chart'; -import { DetailStatus } from '../../../components/kibana/detail_status'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_KIBANA, RULE_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; -import { AlertsCallout } from '../../../alerts/callout'; - -function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const $route = $injector.get('$route'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/kibana/${$route.current.params.uuid}`; - const timeBounds = Legacy.shims.timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/kibana/instances/:uuid', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_KIBANA] }); - }, - pageData: getPageData, - }, - controllerAs: 'monitoringKibanaInstanceApp', - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - super({ - title: `Kibana - ${get($scope.pageData, 'kibanaSummary.name')}`, - telemetryPageViewTitle: 'kibana_instance', - defaultData: {}, - getPageData, - reactNodeId: 'monitoringKibanaInstanceApp', - $scope, - $injector, - alerts: { - shouldFetch: true, - options: { - alertTypeIds: [RULE_KIBANA_VERSION_MISMATCH], - }, - }, - }); - - $scope.$watch( - () => this.data, - (data) => { - if (!data || !data.metrics) { - return; - } - this.setTitle(`Kibana - ${get(data, 'kibanaSummary.name')}`); - this.setPageTitle( - i18n.translate('xpack.monitoring.kibana.instance.pageTitle', { - defaultMessage: 'Kibana instance: {instance}', - values: { - instance: get($scope.pageData, 'kibanaSummary.name'), - }, - }) - ); - - this.renderReact( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/kibana/instances/get_page_data.js b/x-pack/plugins/monitoring/public/views/kibana/instances/get_page_data.js deleted file mode 100644 index 82c49ee0ebb138..00000000000000 --- a/x-pack/plugins/monitoring/public/views/kibana/instances/get_page_data.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; -import { Legacy } from '../../../legacy_shims'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/kibana/instances`; - const timeBounds = Legacy.shims.timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/plugins/monitoring/public/views/kibana/instances/index.html b/x-pack/plugins/monitoring/public/views/kibana/instances/index.html deleted file mode 100644 index 8e1639a2323a50..00000000000000 --- a/x-pack/plugins/monitoring/public/views/kibana/instances/index.html +++ /dev/null @@ -1,7 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/kibana/instances/index.js b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js deleted file mode 100644 index 2601a366e68433..00000000000000 --- a/x-pack/plugins/monitoring/public/views/kibana/instances/index.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { routeInitProvider } from '../../../lib/route_init'; -import { MonitoringViewBaseEuiTableController } from '../../'; -import { getPageData } from './get_page_data'; -import template from './index.html'; -import { KibanaInstances } from '../../../components/kibana/instances'; -import { SetupModeRenderer } from '../../../components/renderers'; -import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; -import { - KIBANA_SYSTEM_ID, - CODE_PATH_KIBANA, - RULE_KIBANA_VERSION_MISMATCH, -} from '../../../../common/constants'; - -uiRoutes.when('/kibana/instances', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_KIBANA] }); - }, - pageData: getPageData, - }, - controllerAs: 'kibanas', - controller: class KibanaInstancesList extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - super({ - title: i18n.translate('xpack.monitoring.kibana.instances.routeTitle', { - defaultMessage: 'Kibana - Instances', - }), - pageTitle: i18n.translate('xpack.monitoring.kibana.instances.pageTitle', { - defaultMessage: 'Kibana instances', - }), - storageKey: 'kibana.instances', - getPageData, - reactNodeId: 'monitoringKibanaInstancesApp', - $scope, - $injector, - alerts: { - shouldFetch: true, - options: { - alertTypeIds: [RULE_KIBANA_VERSION_MISMATCH], - }, - }, - }); - - const renderReact = () => { - this.renderReact( - ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> - ); - }; - - this.onTableChangeRender = renderReact; - - $scope.$watch( - () => this.data, - (data) => { - if (!data) { - return; - } - - renderReact(); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/kibana/overview/index.html b/x-pack/plugins/monitoring/public/views/kibana/overview/index.html deleted file mode 100644 index 5b131e113dfa49..00000000000000 --- a/x-pack/plugins/monitoring/public/views/kibana/overview/index.html +++ /dev/null @@ -1,7 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/kibana/overview/index.js b/x-pack/plugins/monitoring/public/views/kibana/overview/index.js deleted file mode 100644 index ad59265a985315..00000000000000 --- a/x-pack/plugins/monitoring/public/views/kibana/overview/index.js +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** - * Kibana Overview - */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { MonitoringTimeseriesContainer } from '../../../components/chart'; -import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; -import { routeInitProvider } from '../../../lib/route_init'; -import template from './index.html'; -import { Legacy } from '../../../legacy_shims'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPanel, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import { ClusterStatus } from '../../../components/kibana/cluster_status'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_KIBANA } from '../../../../common/constants'; - -function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/kibana`; - const timeBounds = Legacy.shims.timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/kibana', { - template, - resolve: { - clusters: function (Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_KIBANA] }); - }, - pageData: getPageData, - }, - controllerAs: 'monitoringKibanaOverviewApp', - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - super({ - title: `Kibana`, - pageTitle: i18n.translate('xpack.monitoring.kibana.overview.pageTitle', { - defaultMessage: 'Kibana overview', - }), - defaultData: {}, - getPageData, - reactNodeId: 'monitoringKibanaOverviewApp', - $scope, - $injector, - }); - - $scope.$watch( - () => this.data, - (data) => { - if (!data || !data.clusterStatus) { - return; - } - - this.renderReact( - - - - - - - - - - - - - - - - - - - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/license/controller.js b/x-pack/plugins/monitoring/public/views/license/controller.js deleted file mode 100644 index 297edf6481a555..00000000000000 --- a/x-pack/plugins/monitoring/public/views/license/controller.js +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { get, find } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { Legacy } from '../../legacy_shims'; -import { formatDateTimeLocal } from '../../../common/formatting'; -import { BASE_PATH as MANAGEMENT_BASE_PATH } from '../../../../../plugins/license_management/common/constants'; -import { License } from '../../components'; - -const REACT_NODE_ID = 'licenseReact'; - -export class LicenseViewController { - constructor($injector, $scope) { - Legacy.shims.timefilter.disableTimeRangeSelector(); - Legacy.shims.timefilter.disableAutoRefreshSelector(); - - $scope.$on('$destroy', () => { - unmountComponentAtNode(document.getElementById(REACT_NODE_ID)); - }); - - this.init($injector, $scope, i18n); - } - - init($injector, $scope) { - const globalState = $injector.get('globalState'); - const title = $injector.get('title'); - const $route = $injector.get('$route'); - - const cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - $scope.cluster = cluster; - const routeTitle = i18n.translate('xpack.monitoring.license.licenseRouteTitle', { - defaultMessage: 'License', - }); - title($scope.cluster, routeTitle); - - this.license = cluster.license; - this.isExpired = Date.now() > get(cluster, 'license.expiry_date_in_millis'); - this.isPrimaryCluster = cluster.isPrimary; - - const basePath = Legacy.shims.getBasePath(); - this.uploadLicensePath = basePath + '/app/kibana#' + MANAGEMENT_BASE_PATH + 'upload_license'; - - this.renderReact($scope); - } - - renderReact($scope) { - const injector = Legacy.shims.getAngularInjector(); - const timezone = injector.get('config').get('dateFormat:tz'); - $scope.$evalAsync(() => { - const { isPrimaryCluster, license, isExpired, uploadLicensePath } = this; - let expiryDate = license.expiry_date_in_millis; - if (license.expiry_date_in_millis !== undefined) { - expiryDate = formatDateTimeLocal(license.expiry_date_in_millis, timezone); - } - - // Mount the React component to the template - render( - , - document.getElementById(REACT_NODE_ID) - ); - }); - } -} diff --git a/x-pack/plugins/monitoring/public/views/license/index.html b/x-pack/plugins/monitoring/public/views/license/index.html deleted file mode 100644 index 7fb9c699410043..00000000000000 --- a/x-pack/plugins/monitoring/public/views/license/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/license/index.js b/x-pack/plugins/monitoring/public/views/license/index.js deleted file mode 100644 index 0ffb9532686906..00000000000000 --- a/x-pack/plugins/monitoring/public/views/license/index.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { uiRoutes } from '../../angular/helpers/routes'; -import { routeInitProvider } from '../../lib/route_init'; -import template from './index.html'; -import { LicenseViewController } from './controller'; -import { CODE_PATH_LICENSE } from '../../../common/constants'; - -uiRoutes.when('/license', { - template, - resolve: { - clusters: (Private) => { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_LICENSE] }); - }, - }, - controllerAs: 'licenseView', - controller: LicenseViewController, -}); diff --git a/x-pack/plugins/monitoring/public/views/loading/index.html b/x-pack/plugins/monitoring/public/views/loading/index.html deleted file mode 100644 index 9a5971a65bc393..00000000000000 --- a/x-pack/plugins/monitoring/public/views/loading/index.html +++ /dev/null @@ -1,5 +0,0 @@ - -
-
-
-
diff --git a/x-pack/plugins/monitoring/public/views/loading/index.js b/x-pack/plugins/monitoring/public/views/loading/index.js deleted file mode 100644 index 6406b9e6364f0b..00000000000000 --- a/x-pack/plugins/monitoring/public/views/loading/index.js +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** - * Controller for single index detail - */ -import React from 'react'; -import { render } from 'react-dom'; -import { i18n } from '@kbn/i18n'; -import { uiRoutes } from '../../angular/helpers/routes'; -import { routeInitProvider } from '../../lib/route_init'; -import template from './index.html'; -import { Legacy } from '../../legacy_shims'; -import { CODE_PATH_ELASTICSEARCH } from '../../../common/constants'; -import { PageLoading } from '../../components'; -import { ajaxErrorHandlersProvider } from '../../lib/ajax_error_handler'; - -const CODE_PATHS = [CODE_PATH_ELASTICSEARCH]; -uiRoutes.when('/loading', { - template, - controllerAs: 'monitoringLoading', - controller: class { - constructor($injector, $scope) { - const Private = $injector.get('Private'); - const titleService = $injector.get('title'); - titleService( - $scope.cluster, - i18n.translate('xpack.monitoring.loading.pageTitle', { - defaultMessage: 'Loading', - }) - ); - - this.init = () => { - const reactNodeId = 'monitoringLoadingReact'; - const renderElement = document.getElementById(reactNodeId); - if (!renderElement) { - console.warn(`"#${reactNodeId}" element has not been added to the DOM yet`); - return; - } - const I18nContext = Legacy.shims.I18nContext; - render( - - - , - renderElement - ); - }; - - const routeInit = Private(routeInitProvider); - routeInit({ codePaths: CODE_PATHS, fetchAllClusters: true, unsetGlobalState: true }) - .then((clusters) => { - if (!clusters || !clusters.length) { - window.location.hash = '#/no-data'; - $scope.$apply(); - return; - } - if (clusters.length === 1) { - // Bypass the cluster listing if there is just 1 cluster - window.history.replaceState(null, null, '#/overview'); - $scope.$apply(); - return; - } - - window.history.replaceState(null, null, '#/home'); - $scope.$apply(); - }) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return $scope.$apply(() => ajaxErrorHandlers(err)); - }); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.html b/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.html deleted file mode 100644 index 63f51809fd7e70..00000000000000 --- a/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.html +++ /dev/null @@ -1,9 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js deleted file mode 100644 index 9acfd81d186fdc..00000000000000 --- a/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* - * Logstash Node Advanced View - */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { uiRoutes } from '../../../../angular/helpers/routes'; -import { ajaxErrorHandlersProvider } from '../../../../lib/ajax_error_handler'; -import { routeInitProvider } from '../../../../lib/route_init'; -import template from './index.html'; -import { Legacy } from '../../../../legacy_shims'; -import { MonitoringViewBaseController } from '../../../base_controller'; -import { DetailStatus } from '../../../../components/logstash/detail_status'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPanel, - EuiSpacer, - EuiFlexGrid, - EuiFlexItem, -} from '@elastic/eui'; -import { MonitoringTimeseriesContainer } from '../../../../components/chart'; -import { - CODE_PATH_LOGSTASH, - RULE_LOGSTASH_VERSION_MISMATCH, -} from '../../../../../common/constants'; -import { AlertsCallout } from '../../../../alerts/callout'; - -function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const $route = $injector.get('$route'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/node/${$route.current.params.uuid}`; - const timeBounds = Legacy.shims.timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - is_advanced: true, - }) - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/logstash/node/:uuid/advanced', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); - }, - pageData: getPageData, - }, - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - super({ - defaultData: {}, - getPageData, - reactNodeId: 'monitoringLogstashNodeAdvancedApp', - $scope, - $injector, - alerts: { - shouldFetch: true, - options: { - alertTypeIds: [RULE_LOGSTASH_VERSION_MISMATCH], - }, - }, - telemetryPageViewTitle: 'logstash_node_advanced', - }); - - $scope.$watch( - () => this.data, - (data) => { - if (!data || !data.nodeSummary) { - return; - } - - this.setTitle( - i18n.translate('xpack.monitoring.logstash.node.advanced.routeTitle', { - defaultMessage: 'Logstash - {nodeName} - Advanced', - values: { - nodeName: data.nodeSummary.name, - }, - }) - ); - - this.setPageTitle( - i18n.translate('xpack.monitoring.logstash.node.advanced.pageTitle', { - defaultMessage: 'Logstash node: {nodeName}', - values: { - nodeName: data.nodeSummary.name, - }, - }) - ); - - const metricsToShow = [ - data.metrics.logstash_node_cpu_utilization, - data.metrics.logstash_queue_events_count, - data.metrics.logstash_node_cgroup_cpu, - data.metrics.logstash_pipeline_queue_size, - data.metrics.logstash_node_cgroup_stats, - ]; - - this.renderReact( - - - - - - - - - - {metricsToShow.map((metric, index) => ( - - - - - ))} - - - - - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/logstash/node/index.html b/x-pack/plugins/monitoring/public/views/logstash/node/index.html deleted file mode 100644 index 062c830dd8b7ae..00000000000000 --- a/x-pack/plugins/monitoring/public/views/logstash/node/index.html +++ /dev/null @@ -1,9 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/logstash/node/index.js b/x-pack/plugins/monitoring/public/views/logstash/node/index.js deleted file mode 100644 index b23875ba1a3bbe..00000000000000 --- a/x-pack/plugins/monitoring/public/views/logstash/node/index.js +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* - * Logstash Node - */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; -import { routeInitProvider } from '../../../lib/route_init'; -import template from './index.html'; -import { Legacy } from '../../../legacy_shims'; -import { DetailStatus } from '../../../components/logstash/detail_status'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPanel, - EuiSpacer, - EuiFlexGrid, - EuiFlexItem, -} from '@elastic/eui'; -import { MonitoringTimeseriesContainer } from '../../../components/chart'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_LOGSTASH, RULE_LOGSTASH_VERSION_MISMATCH } from '../../../../common/constants'; -import { AlertsCallout } from '../../../alerts/callout'; - -function getPageData($injector) { - const $http = $injector.get('$http'); - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/node/${$route.current.params.uuid}`; - const timeBounds = Legacy.shims.timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - is_advanced: false, - }) - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/logstash/node/:uuid', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); - }, - pageData: getPageData, - }, - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - super({ - defaultData: {}, - getPageData, - reactNodeId: 'monitoringLogstashNodeApp', - $scope, - $injector, - alerts: { - shouldFetch: true, - options: { - alertTypeIds: [RULE_LOGSTASH_VERSION_MISMATCH], - }, - }, - telemetryPageViewTitle: 'logstash_node', - }); - - $scope.$watch( - () => this.data, - (data) => { - if (!data || !data.nodeSummary) { - return; - } - - this.setTitle( - i18n.translate('xpack.monitoring.logstash.node.routeTitle', { - defaultMessage: 'Logstash - {nodeName}', - values: { - nodeName: data.nodeSummary.name, - }, - }) - ); - - this.setPageTitle( - i18n.translate('xpack.monitoring.logstash.node.pageTitle', { - defaultMessage: 'Logstash node: {nodeName}', - values: { - nodeName: data.nodeSummary.name, - }, - }) - ); - - const metricsToShow = [ - data.metrics.logstash_events_input_rate, - data.metrics.logstash_jvm_usage, - data.metrics.logstash_events_output_rate, - data.metrics.logstash_node_cpu_metric, - data.metrics.logstash_events_latency, - data.metrics.logstash_os_load, - ]; - - this.renderReact( - - - - - - - - - - {metricsToShow.map((metric, index) => ( - - - - - ))} - - - - - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/logstash/node/pipelines/index.html b/x-pack/plugins/monitoring/public/views/logstash/node/pipelines/index.html deleted file mode 100644 index cae3a169bfd5ac..00000000000000 --- a/x-pack/plugins/monitoring/public/views/logstash/node/pipelines/index.html +++ /dev/null @@ -1,8 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/logstash/node/pipelines/index.js b/x-pack/plugins/monitoring/public/views/logstash/node/pipelines/index.js deleted file mode 100644 index 0d5105696102a2..00000000000000 --- a/x-pack/plugins/monitoring/public/views/logstash/node/pipelines/index.js +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* - * Logstash Node Pipelines Listing - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { uiRoutes } from '../../../../angular/helpers/routes'; -import { ajaxErrorHandlersProvider } from '../../../../lib/ajax_error_handler'; -import { routeInitProvider } from '../../../../lib/route_init'; -import { isPipelineMonitoringSupportedInVersion } from '../../../../lib/logstash/pipelines'; -import template from './index.html'; -import { Legacy } from '../../../../legacy_shims'; -import { MonitoringViewBaseEuiTableController } from '../../../'; -import { PipelineListing } from '../../../../components/logstash/pipeline_listing/pipeline_listing'; -import { DetailStatus } from '../../../../components/logstash/detail_status'; -import { CODE_PATH_LOGSTASH } from '../../../../../common/constants'; - -const getPageData = ($injector, _api = undefined, routeOptions = {}) => { - _api; // fixing eslint - const $route = $injector.get('$route'); - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const Private = $injector.get('Private'); - - const logstashUuid = $route.current.params.uuid; - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/node/${logstashUuid}/pipelines`; - const timeBounds = Legacy.shims.timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - ...routeOptions, - }) - .then((response) => response.data) - .catch((err) => { - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -}; - -function makeUpgradeMessage(logstashVersion) { - if (isPipelineMonitoringSupportedInVersion(logstashVersion)) { - return null; - } - - return i18n.translate('xpack.monitoring.logstash.node.pipelines.notAvailableDescription', { - defaultMessage: - 'Pipeline monitoring is only available in Logstash version 6.0.0 or higher. This node is running version {logstashVersion}.', - values: { - logstashVersion, - }, - }); -} - -uiRoutes.when('/logstash/node/:uuid/pipelines', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); - }, - }, - controller: class extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - const config = $injector.get('config'); - - super({ - defaultData: {}, - getPageData, - reactNodeId: 'monitoringLogstashNodePipelinesApp', - $scope, - $injector, - fetchDataImmediately: false, // We want to apply pagination before sending the first request - telemetryPageViewTitle: 'logstash_node_pipelines', - }); - - $scope.$watch( - () => this.data, - (data) => { - if (!data || !data.nodeSummary) { - return; - } - - this.setTitle( - i18n.translate('xpack.monitoring.logstash.node.pipelines.routeTitle', { - defaultMessage: 'Logstash - {nodeName} - Pipelines', - values: { - nodeName: data.nodeSummary.name, - }, - }) - ); - - this.setPageTitle( - i18n.translate('xpack.monitoring.logstash.node.pipelines.pageTitle', { - defaultMessage: 'Logstash node pipelines: {nodeName}', - values: { - nodeName: data.nodeSummary.name, - }, - }) - ); - - const pagination = { - ...this.pagination, - totalItemCount: data.totalPipelineCount, - }; - - this.renderReact( - - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/logstash/nodes/get_page_data.js b/x-pack/plugins/monitoring/public/views/logstash/nodes/get_page_data.js deleted file mode 100644 index 4c9167a47b0d7e..00000000000000 --- a/x-pack/plugins/monitoring/public/views/logstash/nodes/get_page_data.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; -import { Legacy } from '../../../legacy_shims'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/nodes`; - const timeBounds = Legacy.shims.timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.html b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.html deleted file mode 100644 index 6da00b1c771b8e..00000000000000 --- a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js deleted file mode 100644 index 56b5d0ec6c82a0..00000000000000 --- a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { routeInitProvider } from '../../../lib/route_init'; -import { MonitoringViewBaseEuiTableController } from '../../'; -import { getPageData } from './get_page_data'; -import template from './index.html'; -import { Listing } from '../../../components/logstash/listing'; -import { SetupModeRenderer } from '../../../components/renderers'; -import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; -import { - CODE_PATH_LOGSTASH, - LOGSTASH_SYSTEM_ID, - RULE_LOGSTASH_VERSION_MISMATCH, -} from '../../../../common/constants'; - -uiRoutes.when('/logstash/nodes', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); - }, - pageData: getPageData, - }, - controllerAs: 'lsNodes', - controller: class LsNodesList extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - super({ - title: i18n.translate('xpack.monitoring.logstash.nodes.routeTitle', { - defaultMessage: 'Logstash - Nodes', - }), - pageTitle: i18n.translate('xpack.monitoring.logstash.nodes.pageTitle', { - defaultMessage: 'Logstash nodes', - }), - storageKey: 'logstash.nodes', - getPageData, - reactNodeId: 'monitoringLogstashNodesApp', - $scope, - $injector, - alerts: { - shouldFetch: true, - options: { - alertTypeIds: [RULE_LOGSTASH_VERSION_MISMATCH], - }, - }, - }); - - const renderComponent = () => { - this.renderReact( - ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> - ); - }; - - this.onTableChangeRender = renderComponent; - - $scope.$watch( - () => this.data, - () => renderComponent() - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/logstash/overview/index.html b/x-pack/plugins/monitoring/public/views/logstash/overview/index.html deleted file mode 100644 index 088aa35892bbee..00000000000000 --- a/x-pack/plugins/monitoring/public/views/logstash/overview/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/logstash/overview/index.js b/x-pack/plugins/monitoring/public/views/logstash/overview/index.js deleted file mode 100644 index b5e8ecbefc532d..00000000000000 --- a/x-pack/plugins/monitoring/public/views/logstash/overview/index.js +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** - * Logstash Overview - */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; -import { routeInitProvider } from '../../../lib/route_init'; -import template from './index.html'; -import { Legacy } from '../../../legacy_shims'; -import { Overview } from '../../../components/logstash/overview'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_LOGSTASH } from '../../../../common/constants'; - -function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash`; - const timeBounds = Legacy.shims.timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then((response) => response.data) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/logstash', { - template, - resolve: { - clusters: function (Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); - }, - pageData: getPageData, - }, - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - super({ - title: 'Logstash', - pageTitle: i18n.translate('xpack.monitoring.logstash.overview.pageTitle', { - defaultMessage: 'Logstash overview', - }), - getPageData, - reactNodeId: 'monitoringLogstashOverviewApp', - $scope, - $injector, - }); - - $scope.$watch( - () => this.data, - (data) => { - this.renderReact( - - ); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/logstash/pipeline/index.html b/x-pack/plugins/monitoring/public/views/logstash/pipeline/index.html deleted file mode 100644 index afd1d994f1e9cb..00000000000000 --- a/x-pack/plugins/monitoring/public/views/logstash/pipeline/index.html +++ /dev/null @@ -1,12 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/logstash/pipeline/index.js b/x-pack/plugins/monitoring/public/views/logstash/pipeline/index.js deleted file mode 100644 index dd7bcc8436358b..00000000000000 --- a/x-pack/plugins/monitoring/public/views/logstash/pipeline/index.js +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* - * Logstash Node Pipeline View - */ -import React from 'react'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import moment from 'moment'; -import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; -import { routeInitProvider } from '../../../lib/route_init'; -import { CALCULATE_DURATION_SINCE, CODE_PATH_LOGSTASH } from '../../../../common/constants'; -import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration'; -import template from './index.html'; -import { i18n } from '@kbn/i18n'; -import { List } from '../../../components/logstash/pipeline_viewer/models/list'; -import { PipelineState } from '../../../components/logstash/pipeline_viewer/models/pipeline_state'; -import { PipelineViewer } from '../../../components/logstash/pipeline_viewer'; -import { Pipeline } from '../../../components/logstash/pipeline_viewer/models/pipeline'; -import { vertexFactory } from '../../../components/logstash/pipeline_viewer/models/graph/vertex_factory'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { EuiPageBody, EuiPage, EuiPageContent } from '@elastic/eui'; - -let previousPipelineHash = undefined; -let detailVertexId = undefined; - -function getPageData($injector) { - const $route = $injector.get('$route'); - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const minIntervalSeconds = $injector.get('minIntervalSeconds'); - const Private = $injector.get('Private'); - - const { ccs, cluster_uuid: clusterUuid } = globalState; - const pipelineId = $route.current.params.id; - const pipelineHash = $route.current.params.hash || ''; - - // Pipeline version was changed, so clear out detailVertexId since that vertex won't - // exist in the updated pipeline version - if (pipelineHash !== previousPipelineHash) { - previousPipelineHash = pipelineHash; - detailVertexId = undefined; - } - - const url = pipelineHash - ? `../api/monitoring/v1/clusters/${clusterUuid}/logstash/pipeline/${pipelineId}/${pipelineHash}` - : `../api/monitoring/v1/clusters/${clusterUuid}/logstash/pipeline/${pipelineId}`; - return $http - .post(url, { - ccs, - detailVertexId, - }) - .then((response) => response.data) - .then((data) => { - data.versions = data.versions.map((version) => { - const relativeFirstSeen = formatTimestampToDuration( - version.firstSeen, - CALCULATE_DURATION_SINCE - ); - const relativeLastSeen = formatTimestampToDuration( - version.lastSeen, - CALCULATE_DURATION_SINCE - ); - - const fudgeFactorSeconds = 2 * minIntervalSeconds; - const isLastSeenCloseToNow = Date.now() - version.lastSeen <= fudgeFactorSeconds * 1000; - - return { - ...version, - relativeFirstSeen: i18n.translate( - 'xpack.monitoring.logstash.pipeline.relativeFirstSeenAgoLabel', - { - defaultMessage: '{relativeFirstSeen} ago', - values: { relativeFirstSeen }, - } - ), - relativeLastSeen: isLastSeenCloseToNow - ? i18n.translate('xpack.monitoring.logstash.pipeline.relativeLastSeenNowLabel', { - defaultMessage: 'now', - }) - : i18n.translate('xpack.monitoring.logstash.pipeline.relativeLastSeenAgoLabel', { - defaultMessage: 'until {relativeLastSeen} ago', - values: { relativeLastSeen }, - }), - }; - }); - - return data; - }) - .catch((err) => { - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/logstash/pipelines/:id/:hash?', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); - }, - pageData: getPageData, - }, - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - const config = $injector.get('config'); - const dateFormat = config.get('dateFormat'); - - super({ - title: i18n.translate('xpack.monitoring.logstash.pipeline.routeTitle', { - defaultMessage: 'Logstash - Pipeline', - }), - storageKey: 'logstash.pipelines', - getPageData, - reactNodeId: 'monitoringLogstashPipelineApp', - $scope, - options: { - enableTimeFilter: false, - }, - $injector, - }); - - const timeseriesTooltipXValueFormatter = (xValue) => moment(xValue).format(dateFormat); - - const setDetailVertexId = (vertex) => { - if (!vertex) { - detailVertexId = undefined; - } else { - detailVertexId = vertex.id; - } - - return this.updateData(); - }; - - $scope.$watch( - () => this.data, - (data) => { - if (!data || !data.pipeline) { - return; - } - this.setPageTitle( - i18n.translate('xpack.monitoring.logstash.pipeline.pageTitle', { - defaultMessage: 'Logstash pipeline: {pipeline}', - values: { - pipeline: data.pipeline.id, - }, - }) - ); - this.pipelineState = new PipelineState(data.pipeline); - this.detailVertex = data.vertex ? vertexFactory(null, data.vertex) : null; - this.renderReact( - - - - - - - - ); - } - ); - - $scope.$on('$destroy', () => { - previousPipelineHash = undefined; - detailVertexId = undefined; - }); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/logstash/pipelines/index.html b/x-pack/plugins/monitoring/public/views/logstash/pipelines/index.html deleted file mode 100644 index bef8a7a4737f3a..00000000000000 --- a/x-pack/plugins/monitoring/public/views/logstash/pipelines/index.html +++ /dev/null @@ -1,7 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/logstash/pipelines/index.js b/x-pack/plugins/monitoring/public/views/logstash/pipelines/index.js deleted file mode 100644 index f3121687f17db2..00000000000000 --- a/x-pack/plugins/monitoring/public/views/logstash/pipelines/index.js +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { find } from 'lodash'; -import { uiRoutes } from '../../../angular/helpers/routes'; -import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; -import { routeInitProvider } from '../../../lib/route_init'; -import { isPipelineMonitoringSupportedInVersion } from '../../../lib/logstash/pipelines'; -import template from './index.html'; -import { Legacy } from '../../../legacy_shims'; -import { PipelineListing } from '../../../components/logstash/pipeline_listing/pipeline_listing'; -import { MonitoringViewBaseEuiTableController } from '../..'; -import { CODE_PATH_LOGSTASH } from '../../../../common/constants'; - -/* - * Logstash Pipelines Listing page - */ - -const getPageData = ($injector, _api = undefined, routeOptions = {}) => { - _api; // to fix eslint - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const Private = $injector.get('Private'); - - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/pipelines`; - const timeBounds = Legacy.shims.timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - ...routeOptions, - }) - .then((response) => response.data) - .catch((err) => { - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -}; - -function makeUpgradeMessage(logstashVersions) { - if ( - !Array.isArray(logstashVersions) || - logstashVersions.length === 0 || - logstashVersions.some(isPipelineMonitoringSupportedInVersion) - ) { - return null; - } - - return 'Pipeline monitoring is only available in Logstash version 6.0.0 or higher.'; -} - -uiRoutes.when('/logstash/pipelines', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); - }, - }, - controller: class LogstashPipelinesList extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - super({ - title: i18n.translate('xpack.monitoring.logstash.pipelines.routeTitle', { - defaultMessage: 'Logstash Pipelines', - }), - pageTitle: i18n.translate('xpack.monitoring.logstash.pipelines.pageTitle', { - defaultMessage: 'Logstash pipelines', - }), - storageKey: 'logstash.pipelines', - getPageData, - reactNodeId: 'monitoringLogstashPipelinesApp', - $scope, - $injector, - fetchDataImmediately: false, // We want to apply pagination before sending the first request - }); - - const $route = $injector.get('$route'); - const config = $injector.get('config'); - this.data = $route.current.locals.pageData; - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - const renderReact = (pageData) => { - if (!pageData) { - return; - } - - const upgradeMessage = pageData - ? makeUpgradeMessage(pageData.clusterStatus.versions, i18n) - : null; - - const pagination = { - ...this.pagination, - totalItemCount: pageData.totalPipelineCount, - }; - - super.renderReact( - this.onBrush({ xaxis })} - stats={pageData.clusterStatus} - data={pageData.pipelines} - {...this.getPaginationTableProps(pagination)} - upgradeMessage={upgradeMessage} - dateFormat={config.get('dateFormat')} - /> - ); - }; - - $scope.$watch( - () => this.data, - (pageData) => { - renderReact(pageData); - } - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/no_data/controller.js b/x-pack/plugins/monitoring/public/views/no_data/controller.js deleted file mode 100644 index 4a6a73dfb20106..00000000000000 --- a/x-pack/plugins/monitoring/public/views/no_data/controller.js +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { - ClusterSettingsChecker, - NodeSettingsChecker, - Enabler, - startChecks, -} from '../../lib/elasticsearch_settings'; -import { ModelUpdater } from './model_updater'; -import { NoData } from '../../components'; -import { CODE_PATH_LICENSE } from '../../../common/constants'; -import { MonitoringViewBaseController } from '../base_controller'; -import { i18n } from '@kbn/i18n'; -import { Legacy } from '../../legacy_shims'; - -export class NoDataController extends MonitoringViewBaseController { - constructor($injector, $scope) { - window.injectorThree = $injector; - const monitoringClusters = $injector.get('monitoringClusters'); - const $http = $injector.get('$http'); - const checkers = [new ClusterSettingsChecker($http), new NodeSettingsChecker($http)]; - - const getData = async () => { - let catchReason; - try { - const monitoringClustersData = await monitoringClusters(undefined, undefined, [ - CODE_PATH_LICENSE, - ]); - if (monitoringClustersData && monitoringClustersData.length) { - window.history.replaceState(null, null, '#/home'); - return monitoringClustersData; - } - } catch (err) { - if (err && err.status === 503) { - catchReason = { - property: 'custom', - message: err.data.message, - }; - } - } - - this.errors.length = 0; - if (catchReason) { - this.reason = catchReason; - } else if (!this.isCollectionEnabledUpdating && !this.isCollectionIntervalUpdating) { - /** - * `no-use-before-define` is fine here, since getData is an async function. - * Needs to be done this way, since there is no `this` before super is executed - * */ - await startChecks(checkers, updateModel); // eslint-disable-line no-use-before-define - } - }; - - super({ - title: i18n.translate('xpack.monitoring.noData.routeTitle', { - defaultMessage: 'Setup Monitoring', - }), - getPageData: async () => await getData(), - reactNodeId: 'noDataReact', - $scope, - $injector, - }); - Object.assign(this, this.getDefaultModel()); - - //Need to set updateModel after super since there is no `this` otherwise - const { updateModel } = new ModelUpdater($scope, this); - const enabler = new Enabler($http, updateModel); - $scope.$watch( - () => this, - () => { - if (this.isCollectionEnabledUpdated && !this.reason) { - return; - } - this.render(enabler); - }, - true - ); - } - - getDefaultModel() { - return { - errors: [], // errors can happen from trying to check or set ES settings - checkMessage: null, // message to show while waiting for api response - isLoading: true, // flag for in-progress state of checking for no data reason - isCollectionEnabledUpdating: false, // flags to indicate whether to show a spinner while waiting for ajax - isCollectionEnabledUpdated: false, - isCollectionIntervalUpdating: false, - isCollectionIntervalUpdated: false, - }; - } - - render(enabler) { - const props = this; - this.renderReact(); - } -} diff --git a/x-pack/plugins/monitoring/public/views/no_data/index.html b/x-pack/plugins/monitoring/public/views/no_data/index.html deleted file mode 100644 index c6fc97b639f423..00000000000000 --- a/x-pack/plugins/monitoring/public/views/no_data/index.html +++ /dev/null @@ -1,5 +0,0 @@ - -
-
-
-
diff --git a/x-pack/plugins/monitoring/public/views/no_data/index.js b/x-pack/plugins/monitoring/public/views/no_data/index.js deleted file mode 100644 index 4bbc490ce29ede..00000000000000 --- a/x-pack/plugins/monitoring/public/views/no_data/index.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { uiRoutes } from '../../angular/helpers/routes'; -import template from './index.html'; -import { NoDataController } from './controller'; - -uiRoutes.when('/no-data', { - template, - controller: NoDataController, -}); diff --git a/x-pack/plugins/monitoring/public/views/no_data/model_updater.js b/x-pack/plugins/monitoring/public/views/no_data/model_updater.js deleted file mode 100644 index 115dc782162a7f..00000000000000 --- a/x-pack/plugins/monitoring/public/views/no_data/model_updater.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* - * Class for handling model updates of an Angular controller - * Some properties are simple primitives like strings or booleans, - * but sometimes we need a property in the model to be an Array. For example, - * there may be multiple errors that happen in a flow. - * - * I use 1 method to handling property values that are either primitives or - * arrays, because it allows the callers to be a little more dumb. All they - * have to know is the property name, rather than the type as well. - */ -export class ModelUpdater { - constructor($scope, model) { - this.$scope = $scope; - this.model = model; - this.updateModel = this.updateModel.bind(this); - } - - updateModel(properties) { - const { $scope, model } = this; - const keys = Object.keys(properties); - $scope.$evalAsync(() => { - keys.forEach((key) => { - if (Array.isArray(model[key])) { - model[key].push(properties[key]); - } else { - model[key] = properties[key]; - } - }); - }); - } -} diff --git a/x-pack/plugins/monitoring/public/views/no_data/model_updater.test.js b/x-pack/plugins/monitoring/public/views/no_data/model_updater.test.js deleted file mode 100644 index b286bfb10a9e4a..00000000000000 --- a/x-pack/plugins/monitoring/public/views/no_data/model_updater.test.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ModelUpdater } from './model_updater'; - -describe('Model Updater for Angular Controller with React Components', () => { - let $scope; - let model; - let updater; - - beforeEach(() => { - $scope = {}; - $scope.$evalAsync = (cb) => cb(); - - model = {}; - - updater = new ModelUpdater($scope, model); - jest.spyOn(updater, 'updateModel'); - }); - - test('should successfully construct an object', () => { - expect(typeof updater).toBe('object'); - expect(updater.updateModel).not.toHaveBeenCalled(); - }); - - test('updateModel method should add properties to the model', () => { - expect(typeof updater).toBe('object'); - updater.updateModel({ - foo: 'bar', - bar: 'baz', - error: 'monkeywrench', - }); - expect(model).toEqual({ - foo: 'bar', - bar: 'baz', - error: 'monkeywrench', - }); - }); - - test('updateModel method should push properties to the model if property is originally an array', () => { - model.errors = ['first']; - updater.updateModel({ - errors: 'second', - primitive: 'hello', - }); - expect(model).toEqual({ - errors: ['first', 'second'], - primitive: 'hello', - }); - }); -}); diff --git a/x-pack/plugins/monitoring/server/deprecations.test.js b/x-pack/plugins/monitoring/server/deprecations.test.js index 4c12979e97804f..9216132fd61196 100644 --- a/x-pack/plugins/monitoring/server/deprecations.test.js +++ b/x-pack/plugins/monitoring/server/deprecations.test.js @@ -67,64 +67,6 @@ describe('monitoring plugin deprecations', function () { }); }); - describe('elasticsearch.username', function () { - it('logs a warning if elasticsearch.username is set to "elastic"', () => { - const settings = { elasticsearch: { username: 'elastic' } }; - - const addDeprecation = jest.fn(); - transformDeprecations(settings, fromPath, addDeprecation); - expect(addDeprecation).toHaveBeenCalled(); - }); - - it('logs a warning if elasticsearch.username is set to "kibana"', () => { - const settings = { elasticsearch: { username: 'kibana' } }; - - const addDeprecation = jest.fn(); - transformDeprecations(settings, fromPath, addDeprecation); - expect(addDeprecation).toHaveBeenCalled(); - }); - - it('does not log a warning if elasticsearch.username is set to something besides "elastic" or "kibana"', () => { - const settings = { elasticsearch: { username: 'otheruser' } }; - - const addDeprecation = jest.fn(); - transformDeprecations(settings, fromPath, addDeprecation); - expect(addDeprecation).not.toHaveBeenCalled(); - }); - - it('does not log a warning if elasticsearch.username is unset', () => { - const settings = { elasticsearch: { username: undefined } }; - - const addDeprecation = jest.fn(); - transformDeprecations(settings, fromPath, addDeprecation); - expect(addDeprecation).not.toHaveBeenCalled(); - }); - - it('logs a warning if ssl.key is set and ssl.certificate is not', () => { - const settings = { elasticsearch: { ssl: { key: '' } } }; - - const addDeprecation = jest.fn(); - transformDeprecations(settings, fromPath, addDeprecation); - expect(addDeprecation).toHaveBeenCalled(); - }); - - it('logs a warning if ssl.certificate is set and ssl.key is not', () => { - const settings = { elasticsearch: { ssl: { certificate: '' } } }; - - const addDeprecation = jest.fn(); - transformDeprecations(settings, fromPath, addDeprecation); - expect(addDeprecation).toHaveBeenCalled(); - }); - - it('does not log a warning if both ssl.key and ssl.certificate are set', () => { - const settings = { elasticsearch: { ssl: { key: '', certificate: '' } } }; - - const addDeprecation = jest.fn(); - transformDeprecations(settings, fromPath, addDeprecation); - expect(addDeprecation).not.toHaveBeenCalled(); - }); - }); - describe('xpack_api_polling_frequency_millis', () => { it('should call rename for this renamed config key', () => { const settings = { xpack_api_polling_frequency_millis: 30000 }; diff --git a/x-pack/plugins/monitoring/server/deprecations.ts b/x-pack/plugins/monitoring/server/deprecations.ts index 7c3d3e3baf58a0..42868e3fa25847 100644 --- a/x-pack/plugins/monitoring/server/deprecations.ts +++ b/x-pack/plugins/monitoring/server/deprecations.ts @@ -59,56 +59,13 @@ export const deprecations = ({ } return config; }, - (config, fromPath, addDeprecation) => { - const es: Record = get(config, 'elasticsearch'); - if (es) { - if (es.username === 'elastic') { - addDeprecation({ - configPath: 'elasticsearch.username', - message: `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana_system" user instead.`, - correctiveActions: { - manualSteps: [`Replace [${fromPath}.username] from "elastic" to "kibana_system".`], - }, - }); - } else if (es.username === 'kibana') { - addDeprecation({ - configPath: 'elasticsearch.username', - message: `Setting [${fromPath}.username] to "kibana" is deprecated. You should use the "kibana_system" user instead.`, - correctiveActions: { - manualSteps: [`Replace [${fromPath}.username] from "kibana" to "kibana_system".`], - }, - }); - } - } - return config; - }, - (config, fromPath, addDeprecation) => { - const ssl: Record = get(config, 'elasticsearch.ssl'); - if (ssl) { - if (ssl.key !== undefined && ssl.certificate === undefined) { - addDeprecation({ - configPath: 'elasticsearch.ssl.key', - message: `Setting [${fromPath}.key] without [${fromPath}.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, - correctiveActions: { - manualSteps: [ - `Set [${fromPath}.ssl.certificate] in your kibana configs to enable TLS client authentication to Elasticsearch.`, - ], - }, - }); - } else if (ssl.certificate !== undefined && ssl.key === undefined) { - addDeprecation({ - configPath: 'elasticsearch.ssl.certificate', - message: `Setting [${fromPath}.certificate] without [${fromPath}.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, - correctiveActions: { - manualSteps: [ - `Set [${fromPath}.ssl.key] in your kibana configs to enable TLS client authentication to Elasticsearch.`, - ], - }, - }); - } - } - return config; - }, rename('xpack_api_polling_frequency_millis', 'licensing.api_polling_frequency'), + + // TODO: Add deprecations for "monitoring.ui.elasticsearch.username: elastic" and "monitoring.ui.elasticsearch.username: kibana". + // TODO: Add deprecations for using "monitoring.ui.elasticsearch.ssl.certificate" without "monitoring.ui.elasticsearch.ssl.key", and + // vice versa. + // ^ These deprecations should only be shown if they are explicitly configured for monitoring -- we should not show Monitoring + // deprecations for these settings if they are inherited from the Core elasticsearch settings. + // See the Core implementation: src/core/server/elasticsearch/elasticsearch_config.ts ]; }; diff --git a/x-pack/plugins/observability/server/utils/get_inspect_response.ts b/x-pack/plugins/observability/common/utils/get_inspect_response.ts similarity index 79% rename from x-pack/plugins/observability/server/utils/get_inspect_response.ts rename to x-pack/plugins/observability/common/utils/get_inspect_response.ts index a6792e0cac5fd0..55d84b622dc2c6 100644 --- a/x-pack/plugins/observability/server/utils/get_inspect_response.ts +++ b/x-pack/plugins/observability/common/utils/get_inspect_response.ts @@ -8,8 +8,8 @@ import { i18n } from '@kbn/i18n'; import type { KibanaRequest } from 'kibana/server'; import type { RequestStatistics, RequestStatus } from '../../../../../src/plugins/inspector'; -import { WrappedElasticsearchClientError } from '../index'; import { InspectResponse } from '../../typings/common'; +import { WrappedElasticsearchClientError } from './unwrap_es_response'; /** * Get statistics to show on inspector tab. @@ -29,19 +29,26 @@ function getStats({ kibanaRequest: KibanaRequest; }) { const stats: RequestStatistics = { - kibanaApiQueryParameters: { - label: i18n.translate('xpack.observability.inspector.stats.kibanaApiQueryParametersLabel', { - defaultMessage: 'Kibana API query parameters', - }), - description: i18n.translate( - 'xpack.observability.inspector.stats.kibanaApiQueryParametersDescription', - { - defaultMessage: - 'The query parameters used in the Kibana API request that initiated the Elasticsearch request.', + ...(kibanaRequest.query + ? { + kibanaApiQueryParameters: { + label: i18n.translate( + 'xpack.observability.inspector.stats.kibanaApiQueryParametersLabel', + { + defaultMessage: 'Kibana API query parameters', + } + ), + description: i18n.translate( + 'xpack.observability.inspector.stats.kibanaApiQueryParametersDescription', + { + defaultMessage: + 'The query parameters used in the Kibana API request that initiated the Elasticsearch request.', + } + ), + value: JSON.stringify(kibanaRequest.query, null, 2), + }, } - ), - value: JSON.stringify(kibanaRequest.query, null, 2), - }, + : {}), kibanaApiRoute: { label: i18n.translate('xpack.observability.inspector.stats.kibanaApiRouteLabel', { defaultMessage: 'Kibana API route', @@ -93,11 +100,17 @@ function getStats({ } if (esResponse?.hits?.total !== undefined) { - const total = esResponse.hits.total as { - relation: string; - value: number; - }; - const hitsTotalValue = total.relation === 'eq' ? `${total.value}` : `> ${total.value}`; + let hitsTotalValue; + + if (typeof esResponse.hits.total === 'number') { + hitsTotalValue = esResponse.hits.total; + } else { + const total = esResponse.hits.total as { + relation: string; + value: number; + }; + hitsTotalValue = total.relation === 'eq' ? `${total.value}` : `> ${total.value}`; + } stats.hitsTotal = { label: i18n.translate('xpack.observability.inspector.stats.hitsTotalLabel', { diff --git a/x-pack/plugins/observability/server/utils/unwrap_es_response.ts b/x-pack/plugins/observability/common/utils/unwrap_es_response.ts similarity index 100% rename from x-pack/plugins/observability/server/utils/unwrap_es_response.ts rename to x-pack/plugins/observability/common/utils/unwrap_es_response.ts diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/use_filter_values.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/use_filter_values.ts index 8c659db559d686..e84f79f88298c1 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/use_filter_values.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/use_filter_values.ts @@ -12,7 +12,10 @@ import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; import { ESFilter } from '../../../../../../../../src/core/types/elasticsearch'; import { PersistableFilter } from '../../../../../../lens/common'; -export function useFilterValues({ field, series, baseFilters }: FilterProps, query?: string) { +export function useFilterValues( + { field, series, baseFilters, label }: FilterProps, + query?: string +) { const { indexPatterns } = useAppIndexPatternContext(series.dataType); const queryFilters: ESFilter[] = []; @@ -28,6 +31,7 @@ export function useFilterValues({ field, series, baseFilters }: FilterProps, que return useValuesList({ query, + label, sourceField: field, time: series.time, keepHistory: true, diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx index 1c5da15dd33dfb..ccb2ea2932f5d3 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx @@ -33,6 +33,7 @@ export function FieldValueSuggestions({ required, allowExclusions = true, cardinalityField, + inspector, asCombobox = true, onChange: onSelectionChange, }: FieldValueSuggestionsProps) { @@ -44,7 +45,9 @@ export function FieldValueSuggestions({ sourceField, filters, time, + inspector, cardinalityField, + label, keepHistory: true, }); diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts index b6de2bafdd8520..6f6d520a831549 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts @@ -8,6 +8,7 @@ import { PopoverAnchorPosition } from '@elastic/eui'; import { Dispatch, SetStateAction } from 'react'; import { ESFilter } from 'src/core/types/elasticsearch'; +import { IInspectorInfo } from '../../../../../../../src/plugins/data/common'; interface CommonProps { selectedValue?: string[]; @@ -37,6 +38,7 @@ export type FieldValueSuggestionsProps = CommonProps & { onChange: (val?: string[], excludedValue?: string[]) => void; filters: ESFilter[]; time?: { from: string; to: string }; + inspector?: IInspectorInfo; }; export type FieldValueSelectionProps = CommonProps & { diff --git a/x-pack/plugins/observability/public/context/inspector/inspector_context.tsx b/x-pack/plugins/observability/public/context/inspector/inspector_context.tsx index 1d9bd95fa08fa6..56498fcaecd5c1 100644 --- a/x-pack/plugins/observability/public/context/inspector/inspector_context.tsx +++ b/x-pack/plugins/observability/public/context/inspector/inspector_context.tsx @@ -23,6 +23,13 @@ const value: InspectorContextValue = { export const InspectorContext = createContext(value); +export type AddInspectorRequest = ( + result: FetcherResult<{ + mainStatisticsData?: { _inspect?: InspectResponse }; + _inspect?: InspectResponse; + }> +) => void; + export function InspectorContextProvider({ children }: { children: ReactNode }) { const history = useHistory(); const { inspectorAdapters } = value; diff --git a/x-pack/plugins/observability/public/hooks/use_es_search.ts b/x-pack/plugins/observability/public/hooks/use_es_search.ts index 70ffdbba22c588..bf96cf2c1f2c5c 100644 --- a/x-pack/plugins/observability/public/hooks/use_es_search.ts +++ b/x-pack/plugins/observability/public/hooks/use_es_search.ts @@ -9,27 +9,61 @@ import { estypes } from '@elastic/elasticsearch'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { ESSearchResponse } from '../../../../../src/core/types/elasticsearch'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; -import { isCompleteResponse } from '../../../../../src/plugins/data/common'; -import { useFetcher } from './use_fetcher'; +import { IInspectorInfo, isCompleteResponse } from '../../../../../src/plugins/data/common'; +import { FETCH_STATUS, useFetcher } from './use_fetcher'; +import { useInspectorContext } from '../context/inspector/use_inspector_context'; +import { getInspectResponse } from '../../common/utils/get_inspect_response'; export const useEsSearch = ( params: TParams, - fnDeps: any[] + fnDeps: any[], + options: { inspector?: IInspectorInfo; name: string } ) => { const { services: { data }, } = useKibana<{ data: DataPublicPluginStart }>(); + const { name } = options ?? {}; + + const { addInspectorRequest } = useInspectorContext(); + const { data: response = {}, loading } = useFetcher(() => { if (params.index) { + const startTime = Date.now(); return new Promise((resolve) => { const search$ = data.search - .search({ - params, - }) + .search( + { + params, + }, + {} + ) .subscribe({ next: (result) => { if (isCompleteResponse(result)) { + if (addInspectorRequest) { + addInspectorRequest({ + data: { + _inspect: [ + getInspectResponse({ + startTime, + esRequestParams: params, + esResponse: result.rawResponse, + esError: null, + esRequestStatus: 1, + operationName: name, + kibanaRequest: { + route: { + path: '/internal/bsearch', + method: 'POST', + }, + } as any, + }), + ], + }, + status: FETCH_STATUS.SUCCESS, + }); + } // Final result resolve(result); search$.unsubscribe(); diff --git a/x-pack/plugins/observability/public/hooks/use_values_list.ts b/x-pack/plugins/observability/public/hooks/use_values_list.ts index 7f52fff55e7062..e2268f7b852442 100644 --- a/x-pack/plugins/observability/public/hooks/use_values_list.ts +++ b/x-pack/plugins/observability/public/hooks/use_values_list.ts @@ -10,16 +10,19 @@ import { useEffect, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import { ESFilter } from '../../../../../src/core/types/elasticsearch'; import { createEsParams, useEsSearch } from './use_es_search'; +import { IInspectorInfo } from '../../../../../src/plugins/data/common'; import { TRANSACTION_URL } from '../components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames'; export interface Props { sourceField: string; + label: string; query?: string; indexPatternTitle?: string; filters?: ESFilter[]; time?: { from: string; to: string }; keepHistory?: boolean; cardinalityField?: string; + inspector?: IInspectorInfo; } export interface ListItem { @@ -60,6 +63,7 @@ export const useValuesList = ({ query = '', filters, time, + label, keepHistory, cardinalityField, }: Props): { values: ListItem[]; loading?: boolean } => { @@ -131,7 +135,8 @@ export const useValuesList = ({ }, }, }), - [debouncedQuery, from, to, JSON.stringify(filters), indexPatternTitle, sourceField] + [debouncedQuery, from, to, JSON.stringify(filters), indexPatternTitle, sourceField], + { name: `get${label.replace(/\s/g, '')}ValuesList` } ); useEffect(() => { diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 22ad95b96f41fd..2dd380c3b76830 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -83,5 +83,8 @@ export type { export { createObservabilityRuleTypeRegistryMock } from './rules/observability_rule_type_registry_mock'; export type { ExploratoryEmbeddableProps } from './components/shared/exploratory_view/embeddable/embeddable'; -export { InspectorContextProvider } from './context/inspector/inspector_context'; +export { + InspectorContextProvider, + AddInspectorRequest, +} from './context/inspector/inspector_context'; export { useInspectorContext } from './context/inspector/use_inspector_context'; diff --git a/x-pack/plugins/observability/server/index.ts b/x-pack/plugins/observability/server/index.ts index 1e811e0a5278cb..77595d11870939 100644 --- a/x-pack/plugins/observability/server/index.ts +++ b/x-pack/plugins/observability/server/index.ts @@ -13,9 +13,12 @@ import { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/serve import { ObservabilityPlugin, ObservabilityPluginSetup } from './plugin'; import { createOrUpdateIndex, Mappings } from './utils/create_or_update_index'; import { ScopedAnnotationsClient } from './lib/annotations/bootstrap_annotations'; -import { unwrapEsResponse, WrappedElasticsearchClientError } from './utils/unwrap_es_response'; +import { + unwrapEsResponse, + WrappedElasticsearchClientError, +} from '../common/utils/unwrap_es_response'; export { rangeQuery, kqlQuery } from './utils/queries'; -export { getInspectResponse } from './utils/get_inspect_response'; +export { getInspectResponse } from '../common/utils/get_inspect_response'; export * from './types'; diff --git a/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts b/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts index 39a594dcc86ca9..98e8908cd60a53 100644 --- a/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts +++ b/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts @@ -17,7 +17,7 @@ import { } from '../../../common/annotations'; import { createOrUpdateIndex } from '../../utils/create_or_update_index'; import { mappings } from './mappings'; -import { unwrapEsResponse } from '../../utils/unwrap_es_response'; +import { unwrapEsResponse } from '../../../common/utils/unwrap_es_response'; type CreateParams = t.TypeOf; type DeleteParams = t.TypeOf; diff --git a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx index 2eddb5d6e932a4..752e95b70efacb 100644 --- a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx +++ b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx @@ -6,7 +6,7 @@ */ import { get, isEmpty, unset, set } from 'lodash'; -import { satisfies } from 'semver'; +import satisfies from 'semver/functions/satisfies'; import { EuiFlexGroup, EuiFlexItem, diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx index b8963ea5a76e30..f621ca0eddb0f8 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -22,7 +22,6 @@ import { userAPIClientMock } from '../../users/index.mock'; import { createRawKibanaPrivileges } from '../__fixtures__/kibana_privileges'; import { indicesAPIClientMock, privilegesAPIClientMock, rolesAPIClientMock } from '../index.mock'; import { EditRolePage } from './edit_role_page'; -import { SimplePrivilegeSection } from './privileges/kibana/simple_privilege_section'; import { SpaceAwarePrivilegeSection } from './privileges/kibana/space_aware_privilege_section'; import { TransformErrorSection } from './privileges/kibana/transform_error_section'; @@ -132,12 +131,10 @@ function getProps({ action, role, canManageSpaces = true, - spacesEnabled = true, }: { action: 'edit' | 'clone'; role?: Role; canManageSpaces?: boolean; - spacesEnabled?: boolean; }) { const rolesAPIClient = rolesAPIClientMock.create(); rolesAPIClient.getRole.mockResolvedValue(role); @@ -165,12 +162,7 @@ function getProps({ const { http, docLinks, notifications } = coreMock.createStart(); http.get.mockImplementation(async (path: any) => { if (path === '/api/spaces/space') { - if (spacesEnabled) { - return buildSpaces(); - } - - const notFoundError = { response: { status: 404 } }; - throw notFoundError; + return buildSpaces(); } }); @@ -335,152 +327,6 @@ describe('', () => { }); }); - describe('with spaces disabled', () => { - it('can render a reserved role', async () => { - const wrapper = mountWithIntl( - - ); - - await waitForRender(wrapper); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectReadOnlyFormButtons(wrapper); - }); - - it('can render a user defined role', async () => { - const wrapper = mountWithIntl( - - ); - - await waitForRender(wrapper); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectSaveFormButtons(wrapper); - }); - - it('can render when creating a new role', async () => { - const wrapper = mountWithIntl( - - ); - - await waitForRender(wrapper); - - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('can render when cloning an existing role', async () => { - const wrapper = mountWithIntl( - - ); - - await waitForRender(wrapper); - - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('does not care if user cannot manage spaces', async () => { - const wrapper = mountWithIntl( - - ); - - await waitForRender(wrapper); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - - expect( - wrapper.find('EuiCallOut[data-test-subj="userCannotManageSpacesCallout"]') - ).toHaveLength(0); - - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('renders a partial read-only view when there is a transform error', async () => { - const wrapper = mountWithIntl( - - ); - - await waitForRender(wrapper); - - expect(wrapper.find(TransformErrorSection)).toHaveLength(1); - expectReadOnlyFormButtons(wrapper); - }); - }); - it('registers fatal error if features endpoint fails unexpectedly', async () => { const error = { response: { status: 500 } }; const getFeatures = jest.fn().mockRejectedValue(error); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index 51aaa988da2a4d..4c71dcd935ff96 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -436,7 +436,6 @@ export const EditRolePage: FunctionComponent = ({ = ({ setFormError(null); try { - await rolesAPIClient.saveRole({ role, spacesEnabled: spaces.enabled }); + await rolesAPIClient.saveRole({ role }); } catch (error) { notifications.toasts.addDanger( error?.body?.message ?? @@ -566,24 +565,17 @@ export const EditRolePage: FunctionComponent = ({ backToRoleList(); }; - const description = spaces.enabled ? ( - - ) : ( - - ); - return (
{getFormTitle()} - {description} + + + {isRoleReserved && ( diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.test.tsx index e27c2eb7485609..5b1d06a741ad2b 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.test.tsx @@ -12,11 +12,10 @@ import type { Role } from '../../../../../../common/model'; import { KibanaPrivileges } from '../../../model'; import { RoleValidator } from '../../validate_role'; import { KibanaPrivilegesRegion } from './kibana_privileges_region'; -import { SimplePrivilegeSection } from './simple_privilege_section'; import { SpaceAwarePrivilegeSection } from './space_aware_privilege_section'; import { TransformErrorSection } from './transform_error_section'; -const buildProps = (customProps = {}) => { +const buildProps = () => { return { role: { name: '', @@ -27,7 +26,6 @@ const buildProps = (customProps = {}) => { }, kibana: [], }, - spacesEnabled: true, spaces: [ { id: 'default', @@ -64,7 +62,6 @@ const buildProps = (customProps = {}) => { onChange: jest.fn(), validator: new RoleValidator(), canCustomizeSubFeaturePrivileges: true, - ...customProps, }; }; @@ -73,26 +70,17 @@ describe('', () => { expect(shallow()).toMatchSnapshot(); }); - it('renders the simple privilege form when spaces is disabled', () => { - const props = buildProps({ spacesEnabled: false }); + it('renders the space-aware privilege form', () => { + const props = buildProps(); const wrapper = shallow(); - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(0); - }); - - it('renders the space-aware privilege form when spaces is enabled', () => { - const props = buildProps({ spacesEnabled: true }); - const wrapper = shallow(); - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(0); expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); }); it('renders the transform error section when the role has a transform error', () => { - const props = buildProps({ spacesEnabled: true }); + const props = buildProps(); (props.role as Role)._transform_error = ['kibana']; const wrapper = shallow(); - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(0); expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(0); expect(wrapper.find(TransformErrorSection)).toHaveLength(1); }); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.tsx index c9c7df222df296..0aba384ede90e4 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.tsx @@ -14,13 +14,11 @@ import type { Role } from '../../../../../../common/model'; import type { KibanaPrivileges } from '../../../model'; import { CollapsiblePanel } from '../../collapsible_panel'; import type { RoleValidator } from '../../validate_role'; -import { SimplePrivilegeSection } from './simple_privilege_section'; import { SpaceAwarePrivilegeSection } from './space_aware_privilege_section'; import { TransformErrorSection } from './transform_error_section'; interface Props { role: Role; - spacesEnabled: boolean; canCustomizeSubFeaturePrivileges: boolean; spaces?: Space[]; uiCapabilities: Capabilities; @@ -44,7 +42,6 @@ export class KibanaPrivilegesRegion extends Component { const { kibanaPrivileges, role, - spacesEnabled, canCustomizeSubFeaturePrivileges, spaces = [], uiCapabilities, @@ -58,30 +55,18 @@ export class KibanaPrivilegesRegion extends Component { return ; } - if (spacesEnabled) { - return ( - - ); - } else { - return ( - - ); - } + return ( + + ); }; } diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap deleted file mode 100644 index 7873e47d2e0ff3..00000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap +++ /dev/null @@ -1,160 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` renders without crashing 1`] = ` - - - - -

- -

-
-
- - - } - labelType="label" - > - - - - -

- -

- , - "inputDisplay": - - , - "value": "none", - }, - Object { - "dropdownDisplay": - - - -

- -

-
, - "inputDisplay": - - , - "value": "custom", - }, - Object { - "dropdownDisplay": - - - -

- -

-
, - "inputDisplay": - - , - "value": "read", - }, - Object { - "dropdownDisplay": - - - -

- -

-
, - "inputDisplay": - - , - "value": "all", - }, - ] - } - valueOfSelected="none" - /> -
-
-
-
-`; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx deleted file mode 100644 index 72061958ecc357..00000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiSelect } from '@elastic/eui'; -import type { ChangeEvent } from 'react'; -import React, { Component } from 'react'; - -import { NO_PRIVILEGE_VALUE } from '../constants'; - -interface Props { - ['data-test-subj']: string; - availablePrivileges: string[]; - onChange: (privilege: string) => void; - value: string | null; - allowNone?: boolean; - disabled?: boolean; - compressed?: boolean; -} - -export class PrivilegeSelector extends Component { - public state = {}; - - public render() { - const { availablePrivileges, value, disabled, allowNone, compressed } = this.props; - - const options = []; - - if (allowNone) { - options.push({ value: NO_PRIVILEGE_VALUE, text: 'none' }); - } - - options.push( - ...availablePrivileges.map((p) => ({ - value: p, - text: p, - })) - ); - - return ( - - ); - } - - public onChange = (e: ChangeEvent) => { - this.props.onChange(e.target.value); - }; -} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx deleted file mode 100644 index bb7124b6c8876e..00000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { EuiButtonGroupProps } from '@elastic/eui'; -import { EuiButtonGroup, EuiComboBox, EuiSuperSelect } from '@elastic/eui'; -import React from 'react'; - -import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest'; - -import type { Role } from '../../../../../../../common/model'; -import { KibanaPrivileges, SecuredFeature } from '../../../../model'; -import { SimplePrivilegeSection } from './simple_privilege_section'; -import { UnsupportedSpacePrivilegesWarning } from './unsupported_space_privileges_warning'; - -const buildProps = (customProps: any = {}) => { - const features = [ - new SecuredFeature({ - id: 'feature1', - name: 'Feature 1', - app: ['app'], - category: { id: 'foo', label: 'foo' }, - privileges: { - all: { - app: ['app'], - savedObject: { - all: ['foo'], - read: [], - }, - ui: ['app-ui'], - }, - read: { - app: ['app'], - savedObject: { - all: [], - read: [], - }, - ui: ['app-ui'], - }, - }, - }), - ] as SecuredFeature[]; - - const kibanaPrivileges = new KibanaPrivileges( - { - features: { - feature1: { - all: ['*'], - read: ['read'], - }, - }, - global: {}, - space: {}, - reserved: {}, - }, - features - ); - - const role = { - name: '', - elasticsearch: { - cluster: ['manage'], - indices: [], - run_as: [], - }, - kibana: [], - ...customProps.role, - }; - - return { - editable: true, - kibanaPrivileges, - features, - onChange: jest.fn(), - canCustomizeSubFeaturePrivileges: true, - ...customProps, - role, - }; -}; - -describe('', () => { - it('renders without crashing', () => { - expect(shallowWithIntl()).toMatchSnapshot(); - }); - - it('displays "none" when no privilege is selected', () => { - const props = buildProps(); - const wrapper = shallowWithIntl(); - const selector = wrapper.find(EuiSuperSelect); - expect(selector.props()).toMatchObject({ - valueOfSelected: 'none', - }); - expect(wrapper.find(UnsupportedSpacePrivilegesWarning)).toHaveLength(0); - }); - - it('displays "custom" when feature privileges are customized', () => { - const props = buildProps({ - role: { - elasticsearch: {}, - kibana: [ - { - spaces: ['*'], - base: [], - feature: { - feature1: ['foo'], - }, - }, - ], - }, - }); - const wrapper = shallowWithIntl(); - const selector = wrapper.find(EuiSuperSelect); - expect(selector.props()).toMatchObject({ - valueOfSelected: 'custom', - }); - expect(wrapper.find(UnsupportedSpacePrivilegesWarning)).toHaveLength(0); - }); - - it('displays the selected privilege', () => { - const props = buildProps({ - role: { - elasticsearch: {}, - kibana: [ - { - spaces: ['*'], - base: ['read'], - feature: {}, - }, - ], - }, - }); - const wrapper = shallowWithIntl(); - const selector = wrapper.find(EuiSuperSelect); - expect(selector.props()).toMatchObject({ - valueOfSelected: 'read', - }); - expect(wrapper.find(UnsupportedSpacePrivilegesWarning)).toHaveLength(0); - }); - - it('displays the reserved privilege', () => { - const props = buildProps({ - role: { - elasticsearch: {}, - kibana: [ - { - spaces: ['*'], - base: [], - feature: {}, - _reserved: ['foo'], - }, - ], - }, - }); - const wrapper = shallowWithIntl(); - const selector = wrapper.find(EuiComboBox); - expect(selector.props()).toMatchObject({ - isDisabled: true, - selectedOptions: [{ label: 'foo' }], - }); - expect(wrapper.find(UnsupportedSpacePrivilegesWarning)).toHaveLength(0); - }); - - it('fires its onChange callback when the privilege changes', () => { - const props = buildProps(); - const wrapper = mountWithIntl(); - const selector = wrapper.find(EuiSuperSelect); - (selector.props() as any).onChange('all'); - - expect(props.onChange).toHaveBeenCalledWith({ - name: '', - elasticsearch: { - cluster: ['manage'], - indices: [], - run_as: [], - }, - kibana: [{ feature: {}, base: ['all'], spaces: ['*'] }], - }); - expect(wrapper.find(UnsupportedSpacePrivilegesWarning)).toHaveLength(0); - }); - - it('allows feature privileges to be customized', () => { - const props = buildProps({ - onChange: (role: Role) => { - wrapper.setProps({ - role, - }); - }, - }); - const wrapper = mountWithIntl(); - const selector = wrapper.find(EuiSuperSelect); - (selector.props() as any).onChange('custom'); - - wrapper.update(); - - const featurePrivilegeToggles = wrapper.find(EuiButtonGroup); - expect(featurePrivilegeToggles).toHaveLength(1); - expect(featurePrivilegeToggles.find('input')).toHaveLength(3); - - (featurePrivilegeToggles.props() as EuiButtonGroupProps).onChange('feature1_all', null); - - wrapper.update(); - - expect(wrapper.props().role).toEqual({ - elasticsearch: { - cluster: ['manage'], - indices: [], - run_as: [], - }, - kibana: [ - { - base: [], - feature: { - feature1: ['all'], - }, - spaces: ['*'], - }, - ], - name: '', - }); - - expect(wrapper.find(UnsupportedSpacePrivilegesWarning)).toHaveLength(0); - }); - - it('renders a warning when space privileges are found', () => { - const props = buildProps({ - role: { - elasticsearch: {}, - kibana: [ - { - spaces: ['*'], - base: ['read'], - feature: {}, - }, - { - spaces: ['marketing'], - base: ['read'], - feature: {}, - }, - ], - }, - }); - const wrapper = mountWithIntl(); - expect(wrapper.find(UnsupportedSpacePrivilegesWarning)).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx deleted file mode 100644 index dd1304ebdaac26..00000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiComboBox, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiSuperSelect, - EuiText, -} from '@elastic/eui'; -import React, { Component, Fragment } from 'react'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -import type { Role, RoleKibanaPrivilege } from '../../../../../../../common/model'; -import { copyRole } from '../../../../../../../common/model'; -import type { KibanaPrivileges } from '../../../../model'; -import { isGlobalPrivilegeDefinition } from '../../../privilege_utils'; -import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../constants'; -import { FeatureTable } from '../feature_table'; -import { PrivilegeFormCalculator } from '../privilege_form_calculator'; -import { UnsupportedSpacePrivilegesWarning } from './unsupported_space_privileges_warning'; - -interface Props { - role: Role; - kibanaPrivileges: KibanaPrivileges; - onChange: (role: Role) => void; - editable: boolean; - canCustomizeSubFeaturePrivileges: boolean; -} - -interface State { - isCustomizingGlobalPrivilege: boolean; - globalPrivsIndex: number; -} - -export class SimplePrivilegeSection extends Component { - constructor(props: Props) { - super(props); - - const globalPrivs = this.locateGlobalPrivilege(props.role); - const globalPrivsIndex = this.locateGlobalPrivilegeIndex(props.role); - - this.state = { - isCustomizingGlobalPrivilege: Boolean( - globalPrivs && Object.keys(globalPrivs.feature).length > 0 - ), - globalPrivsIndex, - }; - } - public render() { - const kibanaPrivilege = this.getDisplayedBasePrivilege(); - - const reservedPrivileges = this.props.role.kibana[this.state.globalPrivsIndex]?._reserved ?? []; - - const title = ( - - ); - - const description = ( -

- -

- ); - - return ( - - - - - {description} - - - - - {reservedPrivileges.length > 0 ? ( - ({ label: rp }))} - isDisabled - /> - ) : ( - - - - ), - dropdownDisplay: ( - - - - -

- -

-
- ), - }, - { - value: CUSTOM_PRIVILEGE_VALUE, - inputDisplay: ( - - - - ), - dropdownDisplay: ( - - - - -

- -

-
- ), - }, - { - value: 'read', - inputDisplay: ( - - - - ), - dropdownDisplay: ( - - - - -

- -

-
- ), - }, - { - value: 'all', - inputDisplay: ( - - - - ), - dropdownDisplay: ( - - - - -

- -

-
- ), - }, - ]} - hasDividers - valueOfSelected={kibanaPrivilege} - /> - )} -
- {this.state.isCustomizingGlobalPrivilege && ( - - - isGlobalPrivilegeDefinition(k) - )} - canCustomizeSubFeaturePrivileges={this.props.canCustomizeSubFeaturePrivileges} - /> - - )} - {this.maybeRenderSpacePrivilegeWarning()} -
-
-
- ); - } - - public getDisplayedBasePrivilege = () => { - if (this.state.isCustomizingGlobalPrivilege) { - return CUSTOM_PRIVILEGE_VALUE; - } - - const { role } = this.props; - - const form = this.locateGlobalPrivilege(role); - - return form && form.base.length > 0 ? form.base[0] : NO_PRIVILEGE_VALUE; - }; - - public onKibanaPrivilegeChange = (privilege: string) => { - const role = copyRole(this.props.role); - - const form = this.locateGlobalPrivilege(role) || this.createGlobalPrivilegeEntry(role); - - if (privilege === NO_PRIVILEGE_VALUE) { - // Remove global entry if no privilege value - role.kibana = role.kibana.filter((entry) => !isGlobalPrivilegeDefinition(entry)); - } else if (privilege === CUSTOM_PRIVILEGE_VALUE) { - // Remove base privilege if customizing feature privileges - form.base = []; - } else { - form.base = [privilege]; - form.feature = {}; - } - - this.props.onChange(role); - this.setState({ - isCustomizingGlobalPrivilege: privilege === CUSTOM_PRIVILEGE_VALUE, - globalPrivsIndex: role.kibana.indexOf(form), - }); - }; - - public onFeaturePrivilegeChange = (featureId: string, privileges: string[]) => { - const role = copyRole(this.props.role); - const form = this.locateGlobalPrivilege(role) || this.createGlobalPrivilegeEntry(role); - if (privileges.length > 0) { - form.feature[featureId] = [...privileges]; - } else { - delete form.feature[featureId]; - } - this.props.onChange(role); - }; - - private onChangeAllFeaturePrivileges = (privileges: string[]) => { - const role = copyRole(this.props.role); - - const form = this.locateGlobalPrivilege(role) || this.createGlobalPrivilegeEntry(role); - if (privileges.length > 0) { - this.props.kibanaPrivileges.getSecuredFeatures().forEach((feature) => { - form.feature[feature.id] = [...privileges]; - }); - } else { - form.feature = {}; - } - this.props.onChange(role); - }; - - private maybeRenderSpacePrivilegeWarning = () => { - const kibanaPrivileges = this.props.role.kibana; - const hasSpacePrivileges = kibanaPrivileges.some( - (privilege) => !isGlobalPrivilegeDefinition(privilege) - ); - - if (hasSpacePrivileges) { - return ( - - - - ); - } - return null; - }; - - private locateGlobalPrivilegeIndex = (role: Role) => { - return role.kibana.findIndex((privileges) => isGlobalPrivilegeDefinition(privileges)); - }; - - private locateGlobalPrivilege = (role: Role) => { - const spacePrivileges = role.kibana; - return spacePrivileges.find((privileges) => isGlobalPrivilegeDefinition(privileges)); - }; - - private createGlobalPrivilegeEntry(role: Role): RoleKibanaPrivilege { - const newEntry = { - spaces: ['*'], - base: [], - feature: {}, - }; - - role.kibana.push(newEntry); - - return newEntry; - } -} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx deleted file mode 100644 index 6a81d22aceeb6e..00000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiCallOut } from '@elastic/eui'; -import React, { Component } from 'react'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export class UnsupportedSpacePrivilegesWarning extends Component<{}, {}> { - public render() { - return ; - } - - private getMessage = () => { - return ( - - ); - }; -} diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.test.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.test.ts index 3bae230377b84c..5d510da8a331bd 100644 --- a/x-pack/plugins/security/public/management/roles/roles_api_client.test.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.test.ts @@ -11,487 +11,252 @@ import type { Role } from '../../../common/model'; import { RolesAPIClient } from './roles_api_client'; describe('RolesAPIClient', () => { - async function saveRole(role: Role, spacesEnabled: boolean) { + async function saveRole(role: Role) { const httpMock = httpServiceMock.createStartContract(); const rolesAPIClient = new RolesAPIClient(httpMock); - await rolesAPIClient.saveRole({ role, spacesEnabled }); + await rolesAPIClient.saveRole({ role }); expect(httpMock.put).toHaveBeenCalledTimes(1); return JSON.parse((httpMock.put.mock.calls[0] as any)[1]?.body as any); } - describe('spaces disabled', () => { - it('removes placeholder index privileges', async () => { - const role: Role = { - name: 'my role', - elasticsearch: { - cluster: [], - indices: [{ names: [], privileges: [] }], - run_as: [], - }, - kibana: [], - }; - - const result = await saveRole(role, false); - - expect(result).toEqual({ - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }); + it('removes placeholder index privileges', async () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [{ names: [], privileges: [] }], + run_as: [], + }, + kibana: [], + }; + + const result = await saveRole(role); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], }); + }); - it('removes placeholder query entries', async () => { - const role: Role = { - name: 'my role', - elasticsearch: { - cluster: [], - indices: [{ names: ['.kibana*'], privileges: ['all'], query: '' }], - run_as: [], - }, - kibana: [], - }; - - const result = await saveRole(role, false); - - expect(result).toEqual({ - elasticsearch: { - cluster: [], - indices: [{ names: ['.kibana*'], privileges: ['all'] }], - run_as: [], - }, - kibana: [], - }); + it('removes placeholder query entries', async () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'], query: '' }], + run_as: [], + }, + kibana: [], + }; + + const result = await saveRole(role); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'] }], + run_as: [], + }, + kibana: [], }); + }); - it('removes transient fields not required for save', async () => { - const role: Role = { - name: 'my role', - transient_metadata: { - foo: 'bar', - }, - _transform_error: ['kibana'], - metadata: { - someOtherMetadata: true, - }, - _unrecognized_applications: ['foo'], - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }; - - const result = await saveRole(role, false); - - expect(result).toEqual({ - metadata: { - someOtherMetadata: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }); + it('removes transient fields not required for save', async () => { + const role: Role = { + name: 'my role', + transient_metadata: { + foo: 'bar', + }, + _transform_error: ['kibana'], + metadata: { + someOtherMetadata: true, + }, + _unrecognized_applications: ['foo'], + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }; + + const result = await saveRole(role); + + expect(result).toEqual({ + metadata: { + someOtherMetadata: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], }); + }); - it('does not remove actual query entries', async () => { - const role: Role = { - name: 'my role', - elasticsearch: { - cluster: [], - indices: [{ names: ['.kibana*'], privileges: ['all'], query: 'something' }], - run_as: [], - }, - kibana: [], - }; - - const result = await saveRole(role, false); - - expect(result).toEqual({ - elasticsearch: { - cluster: [], - indices: [{ names: ['.kibana*'], privileges: ['all'], query: 'something' }], - run_as: [], - }, - kibana: [], - }); + it('does not remove actual query entries', async () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'], query: 'something' }], + run_as: [], + }, + kibana: [], + }; + + const result = await saveRole(role); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'], query: 'something' }], + run_as: [], + }, + kibana: [], }); + }); - it('should remove feature privileges if a corresponding base privilege is defined', async () => { - const role: Role = { - name: 'my role', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: { - feature1: ['read'], - feature2: ['write'], - }, - }, - ], - }; - - const result = await saveRole(role, false); - - expect(result).toEqual({ - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, + it('should remove feature privileges if a corresponding base privilege is defined', async () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['foo'], + base: ['all'], + feature: { + feature1: ['read'], + feature2: ['write'], }, - ], - }); - }); - - it('should not remove feature privileges if a corresponding base privilege is not defined', async () => { - const role: Role = { - name: 'my role', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], }, - kibana: [ - { - spaces: ['*'], - base: [], - feature: { - feature1: ['read'], - feature2: ['write'], - }, - }, - ], - }; + ], + }; - const result = await saveRole(role, false); + const result = await saveRole(role); - expect(result).toEqual({ - elasticsearch: { - cluster: [], - indices: [], - run_as: [], + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['foo'], + base: ['all'], + feature: {}, }, - kibana: [ - { - spaces: ['*'], - base: [], - feature: { - feature1: ['read'], - feature2: ['write'], - }, - }, - ], - }); + ], }); + }); - it('should remove space privileges', async () => { - const role: Role = { - name: 'my role', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [ - { - spaces: ['*'], - base: [], - feature: { - feature1: ['read'], - feature2: ['write'], - }, + it('should not remove feature privileges if a corresponding base privilege is not defined', async () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['foo'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], }, - { - spaces: ['marketing'], - base: [], - feature: { - feature1: ['read'], - feature2: ['write'], - }, - }, - ], - }; - - const result = await saveRole(role, false); - - expect(result).toEqual({ - elasticsearch: { - cluster: [], - indices: [], - run_as: [], }, - kibana: [ - { - spaces: ['*'], - base: [], - feature: { - feature1: ['read'], - feature2: ['write'], - }, + ], + }; + + const result = await saveRole(role); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['foo'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], }, - ], - }); - }); - }); - - describe('spaces enabled', () => { - it('removes placeholder index privileges', async () => { - const role: Role = { - name: 'my role', - elasticsearch: { - cluster: [], - indices: [{ names: [], privileges: [] }], - run_as: [], }, - kibana: [], - }; - - const result = await saveRole(role, true); - - expect(result).toEqual({ - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }); - }); - - it('removes placeholder query entries', async () => { - const role: Role = { - name: 'my role', - elasticsearch: { - cluster: [], - indices: [{ names: ['.kibana*'], privileges: ['all'], query: '' }], - run_as: [], - }, - kibana: [], - }; - - const result = await saveRole(role, true); - - expect(result).toEqual({ - elasticsearch: { - cluster: [], - indices: [{ names: ['.kibana*'], privileges: ['all'] }], - run_as: [], - }, - kibana: [], - }); - }); - - it('removes transient fields not required for save', async () => { - const role: Role = { - name: 'my role', - transient_metadata: { - foo: 'bar', - }, - _transform_error: ['kibana'], - metadata: { - someOtherMetadata: true, - }, - _unrecognized_applications: ['foo'], - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }; - - const result = await saveRole(role, true); - - expect(result).toEqual({ - metadata: { - someOtherMetadata: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }); - }); - - it('does not remove actual query entries', async () => { - const role: Role = { - name: 'my role', - elasticsearch: { - cluster: [], - indices: [{ names: ['.kibana*'], privileges: ['all'], query: 'something' }], - run_as: [], - }, - kibana: [], - }; - - const result = await saveRole(role, true); - - expect(result).toEqual({ - elasticsearch: { - cluster: [], - indices: [{ names: ['.kibana*'], privileges: ['all'], query: 'something' }], - run_as: [], - }, - kibana: [], - }); + ], }); + }); - it('should remove feature privileges if a corresponding base privilege is defined', async () => { - const role: Role = { - name: 'my role', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [ - { - spaces: ['foo'], - base: ['all'], - feature: { - feature1: ['read'], - feature2: ['write'], - }, - }, - ], - }; - - const result = await saveRole(role, true); - - expect(result).toEqual({ - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [ - { - spaces: ['foo'], - base: ['all'], - feature: {}, + it('should not remove space privileges', async () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], }, - ], - }); - }); - - it('should not remove feature privileges if a corresponding base privilege is not defined', async () => { - const role: Role = { - name: 'my role', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], }, - kibana: [ - { - spaces: ['foo'], - base: [], - feature: { - feature1: ['read'], - feature2: ['write'], - }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], }, - ], - }; - - const result = await saveRole(role, true); - - expect(result).toEqual({ - elasticsearch: { - cluster: [], - indices: [], - run_as: [], }, - kibana: [ - { - spaces: ['foo'], - base: [], - feature: { - feature1: ['read'], - feature2: ['write'], - }, + ], + }; + + const result = await saveRole(role); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], }, - ], - }); - }); - - it('should not remove space privileges', async () => { - const role: Role = { - name: 'my role', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], }, - kibana: [ - { - spaces: ['*'], - base: [], - feature: { - feature1: ['read'], - feature2: ['write'], - }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], }, - { - spaces: ['marketing'], - base: [], - feature: { - feature1: ['read'], - feature2: ['write'], - }, - }, - ], - }; - - const result = await saveRole(role, true); - - expect(result).toEqual({ - elasticsearch: { - cluster: [], - indices: [], - run_as: [], }, - kibana: [ - { - spaces: ['*'], - base: [], - feature: { - feature1: ['read'], - feature2: ['write'], - }, - }, - { - spaces: ['marketing'], - base: [], - feature: { - feature1: ['read'], - feature2: ['write'], - }, - }, - ], - }); + ], }); }); }); diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.ts index aff3a7ccacd66e..5de8827207ced0 100644 --- a/x-pack/plugins/security/public/management/roles/roles_api_client.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.ts @@ -9,7 +9,6 @@ import type { HttpStart } from 'src/core/public'; import type { Role, RoleIndexPrivilege } from '../../../common/model'; import { copyRole } from '../../../common/model'; -import { isGlobalPrivilegeDefinition } from './edit_role/privilege_utils'; export class RolesAPIClient { constructor(private readonly http: HttpStart) {} @@ -26,13 +25,13 @@ export class RolesAPIClient { await this.http.delete(`/api/security/role/${encodeURIComponent(roleName)}`); } - public async saveRole({ role, spacesEnabled }: { role: Role; spacesEnabled: boolean }) { + public async saveRole({ role }: { role: Role }) { await this.http.put(`/api/security/role/${encodeURIComponent(role.name)}`, { - body: JSON.stringify(this.transformRoleForSave(copyRole(role), spacesEnabled)), + body: JSON.stringify(this.transformRoleForSave(copyRole(role))), }); } - private transformRoleForSave(role: Role, spacesEnabled: boolean) { + private transformRoleForSave(role: Role) { // Remove any placeholder index privileges const isPlaceholderPrivilege = (indexPrivilege: RoleIndexPrivilege) => indexPrivilege.names.length === 0; @@ -43,11 +42,6 @@ export class RolesAPIClient { // Remove any placeholder query entries role.elasticsearch.indices.forEach((index) => index.query || delete index.query); - // If spaces are disabled, then do not persist any space privileges - if (!spacesEnabled) { - role.kibana = role.kibana.filter(isGlobalPrivilegeDefinition); - } - role.kibana.forEach((kibanaPrivilege) => { // If a base privilege is defined, then do not persist feature privileges if (kibanaPrivilege.base.length > 0) { diff --git a/x-pack/plugins/security/server/config_deprecations.test.ts b/x-pack/plugins/security/server/config_deprecations.test.ts index 808c0aeb85b12a..a629b6d73a682f 100644 --- a/x-pack/plugins/security/server/config_deprecations.test.ts +++ b/x-pack/plugins/security/server/config_deprecations.test.ts @@ -312,7 +312,7 @@ describe('Config Deprecations', () => { const { messages, configPaths } = applyConfigDeprecations(cloneDeep(config)); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"xpack.security.authc.providers.saml..maxRedirectURLSize\\" is no longer used.", + "This setting is no longer used.", ] `); @@ -333,7 +333,7 @@ describe('Config Deprecations', () => { expect(migrated).toEqual(config); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"xpack.security.authc.providers\\" accepts an extended \\"object\\" format instead of an array of provider types.", + "Use the new object format instead of an array of provider types.", ] `); }); @@ -352,8 +352,8 @@ describe('Config Deprecations', () => { expect(migrated).toEqual(config); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"xpack.security.authc.providers\\" accepts an extended \\"object\\" format instead of an array of provider types.", - "Enabling both \\"basic\\" and \\"token\\" authentication providers in \\"xpack.security.authc.providers\\" is deprecated. Login page will only use \\"token\\" provider.", + "Use the new object format instead of an array of provider types.", + "Use only one of these providers. When both providers are set, Kibana only uses the \\"token\\" provider.", ] `); }); diff --git a/x-pack/plugins/security/server/config_deprecations.ts b/x-pack/plugins/security/server/config_deprecations.ts index 46fbbcec5188ee..0c76840819b3d5 100644 --- a/x-pack/plugins/security/server/config_deprecations.ts +++ b/x-pack/plugins/security/server/config_deprecations.ts @@ -13,22 +13,23 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ renameFromRoot, unused, }) => [ - rename('sessionTimeout', 'session.idleTimeout'), - rename('authProviders', 'authc.providers'), + rename('sessionTimeout', 'session.idleTimeout', { level: 'warning' }), + rename('authProviders', 'authc.providers', { level: 'warning' }), - rename('audit.appender.kind', 'audit.appender.type'), - rename('audit.appender.layout.kind', 'audit.appender.layout.type'), - rename('audit.appender.policy.kind', 'audit.appender.policy.type'), - rename('audit.appender.strategy.kind', 'audit.appender.strategy.type'), - rename('audit.appender.path', 'audit.appender.fileName'), + rename('audit.appender.kind', 'audit.appender.type', { level: 'warning' }), + rename('audit.appender.layout.kind', 'audit.appender.layout.type', { level: 'warning' }), + rename('audit.appender.policy.kind', 'audit.appender.policy.type', { level: 'warning' }), + rename('audit.appender.strategy.kind', 'audit.appender.strategy.type', { level: 'warning' }), + rename('audit.appender.path', 'audit.appender.fileName', { level: 'warning' }), renameFromRoot( 'security.showInsecureClusterWarning', - 'xpack.security.showInsecureClusterWarning' + 'xpack.security.showInsecureClusterWarning', + { level: 'warning' } ), - unused('authorization.legacyFallback.enabled'), - unused('authc.saml.maxRedirectURLSize'), + unused('authorization.legacyFallback.enabled', { level: 'warning' }), + unused('authc.saml.maxRedirectURLSize', { level: 'warning' }), // Deprecation warning for the legacy audit logger. (settings, fromPath, addDeprecation, { branch }) => { const auditLoggingEnabled = settings?.xpack?.security?.audit?.enabled ?? false; @@ -57,30 +58,33 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ }, // Deprecation warning for the old array-based format of `xpack.security.authc.providers`. - (settings, fromPath, addDeprecation) => { + (settings, _fromPath, addDeprecation, { branch }) => { if (Array.isArray(settings?.xpack?.security?.authc?.providers)) { addDeprecation({ configPath: 'xpack.security.authc.providers', title: i18n.translate('xpack.security.deprecations.authcProvidersTitle', { - defaultMessage: - 'Defining "xpack.security.authc.providers" as an array of provider types is deprecated', + defaultMessage: 'The array format for "xpack.security.authc.providers" is deprecated', }), message: i18n.translate('xpack.security.deprecations.authcProvidersMessage', { - defaultMessage: - '"xpack.security.authc.providers" accepts an extended "object" format instead of an array of provider types.', + defaultMessage: 'Use the new object format instead of an array of provider types.', }), + level: 'warning', + documentationUrl: `https://www.elastic.co/guide/en/kibana/${branch}/security-settings-kb.html#authentication-security-settings`, correctiveActions: { manualSteps: [ - i18n.translate('xpack.security.deprecations.authcProviders.manualStepOneMessage', { + i18n.translate('xpack.security.deprecations.authcProviders.manualSteps1', { defaultMessage: - 'Use the extended object format for "xpack.security.authc.providers" in your Kibana configuration.', + 'Remove the "xpack.security.authc.providers" setting from kibana.yml.', + }), + i18n.translate('xpack.security.deprecations.authcProviders.manualSteps2', { + defaultMessage: 'Add your authentication providers using the new object format.', }), ], }, }); } }, - (settings, fromPath, addDeprecation) => { + (settings, _fromPath, addDeprecation, { branch }) => { const hasProviderType = (providerType: string) => { const providers = settings?.xpack?.security?.authc?.providers; if (Array.isArray(providers)) { @@ -93,31 +97,35 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ }; if (hasProviderType('basic') && hasProviderType('token')) { + const basicProvider = 'basic'; + const tokenProvider = 'token'; addDeprecation({ configPath: 'xpack.security.authc.providers', title: i18n.translate('xpack.security.deprecations.basicAndTokenProvidersTitle', { defaultMessage: - 'Both "basic" and "token" authentication providers are enabled in "xpack.security.authc.providers"', + 'Using both "{basicProvider}" and "{tokenProvider}" providers in "xpack.security.authc.providers" has no effect', + values: { basicProvider, tokenProvider }, }), message: i18n.translate('xpack.security.deprecations.basicAndTokenProvidersMessage', { defaultMessage: - 'Enabling both "basic" and "token" authentication providers in "xpack.security.authc.providers" is deprecated. Login page will only use "token" provider.', + 'Use only one of these providers. When both providers are set, Kibana only uses the "{tokenProvider}" provider.', + values: { tokenProvider }, }), + level: 'warning', + documentationUrl: `https://www.elastic.co/guide/en/kibana/${branch}/security-settings-kb.html#authentication-security-settings`, correctiveActions: { manualSteps: [ - i18n.translate( - 'xpack.security.deprecations.basicAndTokenProviders.manualStepOneMessage', - { - defaultMessage: - 'Remove either the "basic" or "token" auth provider in "xpack.security.authc.providers" from your Kibana configuration.', - } - ), + i18n.translate('xpack.security.deprecations.basicAndTokenProviders.manualSteps1', { + defaultMessage: + 'Remove the "{basicProvider}" provider from "xpack.security.authc.providers" in kibana.yml.', + values: { basicProvider }, + }), ], }, }); } }, - (settings, fromPath, addDeprecation) => { + (settings, _fromPath, addDeprecation, { branch }) => { const samlProviders = (settings?.xpack?.security?.authc?.providers?.saml ?? {}) as Record< string, any @@ -131,17 +139,18 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ configPath: `xpack.security.authc.providers.saml.${foundProvider[0]}.maxRedirectURLSize`, title: i18n.translate('xpack.security.deprecations.maxRedirectURLSizeTitle', { defaultMessage: - '"xpack.security.authc.providers.saml..maxRedirectURLSize" is deprecated', + '"xpack.security.authc.providers.saml..maxRedirectURLSize" has no effect', }), message: i18n.translate('xpack.security.deprecations.maxRedirectURLSizeMessage', { - defaultMessage: - '"xpack.security.authc.providers.saml..maxRedirectURLSize" is no longer used.', + defaultMessage: 'This setting is no longer used.', }), + level: 'warning', + documentationUrl: `https://www.elastic.co/guide/en/kibana/${branch}/security-settings-kb.html#authentication-security-settings`, correctiveActions: { manualSteps: [ - i18n.translate('xpack.security.deprecations.maxRedirectURLSize.manualStepOneMessage', { + i18n.translate('xpack.security.deprecations.maxRedirectURLSize.manualSteps1', { defaultMessage: - 'Remove "xpack.security.authc.providers.saml..maxRedirectURLSize" from your Kibana configuration.', + 'Remove "xpack.security.authc.providers.saml..maxRedirectURLSize" from kibana.yml.', }), ], }, diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 148390324c13f8..14b1bf8dc22dd4 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -16,7 +16,7 @@ export const allowedExperimentalValues = Object.freeze({ ruleRegistryEnabled: false, tGridEnabled: true, tGridEventRenderedViewEnabled: true, - trustedAppsByPolicyEnabled: false, + trustedAppsByPolicyEnabled: true, excludePoliciesInFilterEnabled: false, uebaEnabled: false, disableIsolationUIPendingStatuses: false, diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index da680bf45dc8d2..e383725a7e40c9 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -65,7 +65,7 @@ export const EVENT_FILTERS = i18n.translate( export const HOST_ISOLATION_EXCEPTIONS = i18n.translate( 'xpack.securitySolution.search.administration.hostIsolationExceptions', { - defaultMessage: 'Host Isolation Exceptions', + defaultMessage: 'Host isolation exceptions', } ); export const DETECT = i18n.translate('xpack.securitySolution.navigation.detect', { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx index 29861b14341472..396f431a3232de 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx @@ -18,12 +18,15 @@ import { UrlInputsModel } from '../../../store/inputs/model'; import { useRouteSpy } from '../../../utils/route/use_route_spy'; import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; import { TestProviders } from '../../../mock'; +import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pages/host_isolation_exceptions/view/hooks'; jest.mock('../../../lib/kibana/kibana_react'); jest.mock('../../../lib/kibana'); jest.mock('../../../hooks/use_selector'); jest.mock('../../../hooks/use_experimental_features'); jest.mock('../../../utils/route/use_route_spy'); +jest.mock('../../../../management/pages/host_isolation_exceptions/view/hooks'); + describe('useSecuritySolutionNavigation', () => { const mockUrlState = { [CONSTANTS.appQuery]: { query: 'host.name:"security-solution-es"', language: 'kuery' }, @@ -75,6 +78,7 @@ describe('useSecuritySolutionNavigation', () => { (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); (useDeepEqualSelector as jest.Mock).mockReturnValue({ urlState: mockUrlState }); (useRouteSpy as jest.Mock).mockReturnValue(mockRouteSpy); + (useCanSeeHostIsolationExceptionsMenu as jest.Mock).mockReturnValue(true); (useKibana as jest.Mock).mockReturnValue({ services: { @@ -240,7 +244,7 @@ describe('useSecuritySolutionNavigation', () => { "href": "securitySolution/host_isolation_exceptions", "id": "host_isolation_exceptions", "isSelected": false, - "name": "Host Isolation Exceptions", + "name": "Host isolation exceptions", "onClick": [Function], }, ], @@ -264,6 +268,19 @@ describe('useSecuritySolutionNavigation', () => { expect(result.current.items[2].items[2].id).toEqual(SecurityPageName.ueba); }); + it('should omit host isolation exceptions if hook reports false', () => { + (useCanSeeHostIsolationExceptionsMenu as jest.Mock).mockReturnValueOnce(false); + const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>( + () => useSecuritySolutionNavigation(), + { wrapper: TestProviders } + ); + expect( + result.current?.items + .find((item) => item.id === 'manage') + ?.items?.find((item) => item.id === 'host_isolation_exceptions') + ).toBeUndefined(); + }); + describe('Permission gated routes', () => { describe('cases', () => { it('should display the cases navigation item when the user has read permissions', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx index 976f15586b555c..a1be69dd077ade 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx @@ -14,6 +14,7 @@ import { PrimaryNavigationItemsProps } from './types'; import { useGetUserCasesPermissions } from '../../../lib/kibana'; import { useNavigation } from '../../../lib/kibana/hooks'; import { NavTab } from '../types'; +import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pages/host_isolation_exceptions/view/hooks'; export const usePrimaryNavigationItems = ({ navTabs, @@ -62,8 +63,9 @@ export const usePrimaryNavigationItems = ({ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { const hasCasesReadPermissions = useGetUserCasesPermissions()?.read; - return useMemo( - () => [ + const canSeeHostIsolationExceptions = useCanSeeHostIsolationExceptionsMenu(); + return useMemo(() => { + return [ { id: 'main', name: '', @@ -87,10 +89,9 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { navTabs.endpoints, navTabs.trusted_apps, navTabs.event_filters, - navTabs.host_isolation_exceptions, + ...(canSeeHostIsolationExceptions ? [navTabs.host_isolation_exceptions] : []), ], }, - ], - [navTabs, hasCasesReadPermissions] - ); + ]; + }, [navTabs, hasCasesReadPermissions, canSeeHostIsolationExceptions]); } diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts index 05e1c2c4dca816..ed75e3bbbe926c 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts @@ -37,7 +37,8 @@ export const useNavigateToAppEventHandler = ( options?: NavigateToAppHandlerOptions ): EventHandlerCallback => { const { services } = useKibana(); - const { path, state, onClick, deepLinkId } = options || {}; + const { path, state, onClick, deepLinkId, openInNewTab } = options || {}; + return useCallback( (ev) => { try { @@ -70,8 +71,8 @@ export const useNavigateToAppEventHandler = ( } ev.preventDefault(); - services.application.navigateToApp(appId, { deepLinkId, path, state }); + services.application.navigateToApp(appId, { deepLinkId, path, state, openInNewTab }); }, - [appId, deepLinkId, onClick, path, services.application, state] + [appId, deepLinkId, onClick, path, services.application, state, openInNewTab] ); }; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx index 50500a789fd4e1..36362463b5ea21 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx @@ -12,6 +12,8 @@ import { act, fireEvent, getByTestId } from '@testing-library/react'; import { AnyArtifact } from './types'; import { isTrustedApp } from './utils'; import { getTrustedAppProviderMock, getExceptionProviderMock } from './test_utils'; +import { OS_LINUX, OS_MAC, OS_WINDOWS } from './components/translations'; +import { TrustedApp } from '../../../../common/endpoint/types'; describe.each([ ['trusted apps', getTrustedAppProviderMock], @@ -111,6 +113,22 @@ describe.each([ ); }); + it('should display multiple OSs in the criteria conditions', () => { + if (isTrustedApp(item)) { + // Trusted apps does not support multiple OS, so this is just so the test will pass + // for the trusted app run (the top level `describe()` uses a `.each()`) + item.os = [OS_LINUX, OS_MAC, OS_WINDOWS].join(', ') as TrustedApp['os']; + } else { + item.os_types = ['linux', 'macos', 'windows']; + } + + render(); + + expect(renderResult.getByTestId('testCard-criteriaConditions').textContent).toEqual( + ` OSIS ${OS_LINUX}, ${OS_MAC}, ${OS_WINDOWS}AND process.hash.*IS 1234234659af249ddf3e40864e9fb241AND process.executable.caselessIS /one/two/three` + ); + }); + it('should NOT show the action menu button if no actions were provided', async () => { render(); const menuButton = await renderResult.queryByTestId('testCard-header-actions-button'); @@ -198,7 +216,9 @@ describe.each([ renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-popoverPanel') ).not.toBeNull(); - expect(renderResult.getByTestId('policyMenuItem').textContent).toEqual('Policy one'); + expect(renderResult.getByTestId('policyMenuItem').textContent).toEqual( + 'Policy oneView details' + ); }); it('should display policy ID if no policy menu item found in `policies` prop', async () => { diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.tsx index bee9a63c9cf69e..e974557c36e0a3 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.tsx @@ -33,6 +33,7 @@ export interface CommonArtifactEntryCardProps extends CommonProps { * `Record`. */ policies?: MenuItemPropsByPolicyId; + loadingPoliciesList?: boolean; } export interface ArtifactEntryCardProps extends CommonArtifactEntryCardProps { @@ -50,6 +51,7 @@ export const ArtifactEntryCard = memo( ({ item, policies, + loadingPoliciesList = false, actions, hideDescription = false, hideComments = false, @@ -74,10 +76,11 @@ export const ArtifactEntryCard = memo( createdBy={artifact.created_by} updatedBy={artifact.updated_by} policies={policyNavLinks} + loadingPoliciesList={loadingPoliciesList} data-test-subj={getTestId('subHeader')} /> - + {!hideDescription ? ( diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_header.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_header.tsx index 6964f5b339312f..c1f3f257b278ab 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_header.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_header.tsx @@ -24,9 +24,9 @@ export const CardHeader = memo( const getTestId = useTestIdGenerator(dataTestSubj); return ( - + - +

{name}

diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_section_panel.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_section_panel.tsx index 1d694ab1771d39..75e55a72f7f07d 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_section_panel.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_section_panel.tsx @@ -6,6 +6,7 @@ */ import React, { memo } from 'react'; +import styled from 'styled-components'; import { EuiPanel, EuiPanelProps } from '@elastic/eui'; export type CardSectionPanelProps = Exclude< @@ -13,7 +14,11 @@ export type CardSectionPanelProps = Exclude< 'hasBorder' | 'hasShadow' | 'paddingSize' >; +const StyledEuiPanel = styled(EuiPanel)` + padding: 32px; +`; + export const CardSectionPanel = memo((props) => { - return ; + return ; }); CardSectionPanel.displayName = 'CardSectionPanel'; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_sub_header.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_sub_header.tsx index fd787c01e50ff1..e502fd741115c6 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_sub_header.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_sub_header.tsx @@ -13,10 +13,18 @@ import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; export type SubHeaderProps = TouchedByUsersProps & EffectScopeProps & - Pick; + Pick & { + loadingPoliciesList?: boolean; + }; export const CardSubHeader = memo( - ({ createdBy, updatedBy, policies, 'data-test-subj': dataTestSubj }) => { + ({ + createdBy, + updatedBy, + policies, + loadingPoliciesList = false, + 'data-test-subj': dataTestSubj, + }) => { const getTestId = useTestIdGenerator(dataTestSubj); return ( @@ -29,7 +37,11 @@ export const CardSubHeader = memo( />
- +
); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/criteria_conditions.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/criteria_conditions.tsx index 260db313ced362..743eac7a15458c 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/criteria_conditions.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/criteria_conditions.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useCallback } from 'react'; +import React, { memo, useCallback, useMemo } from 'react'; import { CommonProps, EuiExpression, EuiToken, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; @@ -28,6 +28,7 @@ import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; const OS_LABELS = Object.freeze({ linux: OS_LINUX, mac: OS_MAC, + macos: OS_MAC, windows: OS_WINDOWS, }); @@ -49,6 +50,10 @@ const EuiFlexItemNested = styled(EuiFlexItem)` margin-top: 6px !important; `; +const StyledCondition = styled('span')` + margin-right: 6px; +`; + export type CriteriaConditionsProps = Pick & Pick; @@ -56,6 +61,12 @@ export const CriteriaConditions = memo( ({ os, entries, 'data-test-subj': dataTestSubj }) => { const getTestId = useTestIdGenerator(dataTestSubj); + const osLabel = useMemo(() => { + return os + .map((osValue) => OS_LABELS[osValue as keyof typeof OS_LABELS] ?? osValue) + .join(', '); + }, [os]); + const getNestedEntriesContent = useCallback( (type: string, nestedEntries: ArtifactInfoEntry[]) => { if (type === 'nested' && nestedEntries.length) { @@ -99,16 +110,17 @@ export const CriteriaConditions = memo(
- +
{entries.map(({ field, type, value, entries: nestedEntries = [] }) => { return (
- + {CONDITION_AND}} + value={field} + color="subdued" + /> { date: FormattedRelativePreferenceDateProps['value']; type: 'update' | 'create'; @@ -25,10 +30,15 @@ export const DateFieldValue = memo( const getTestId = useTestIdGenerator(dataTestSubj); return ( - - + + - + ` // This should dispaly it as "Applied t o 3 policies", but NOT as a menu with links +const StyledWithContextMenuShiftedWrapper = styled('div')` + margin-left: -10px; +`; + +const StyledEuiButtonEmpty = styled(EuiButtonEmpty)` + height: 10px !important; +`; export interface EffectScopeProps extends Pick { /** If set (even if empty), then effect scope will be policy specific. Else, it shows as global */ policies?: ContextMenuItemNavByRouterProps[]; + loadingPoliciesList?: boolean; } export const EffectScope = memo( - ({ policies, 'data-test-subj': dataTestSubj }) => { + ({ policies, loadingPoliciesList = false, 'data-test-subj': dataTestSubj }) => { const getTestId = useTestIdGenerator(dataTestSubj); const [icon, label] = useMemo(() => { @@ -43,18 +57,24 @@ export const EffectScope = memo( data-test-subj={dataTestSubj} > - + - {label} + {label} ); return policies && policies.length ? ( - - {effectiveScopeLabel} - + + + {effectiveScopeLabel} + + ) : ( effectiveScopeLabel ); @@ -65,22 +85,40 @@ EffectScope.displayName = 'EffectScope'; type WithContextMenuProps = Pick & PropsWithChildren<{ policies: Required['policies']; - }>; + }> & { + loadingPoliciesList?: boolean; + }; export const WithContextMenu = memo( - ({ policies, children, 'data-test-subj': dataTestSubj }) => { + ({ policies, loadingPoliciesList = false, children, 'data-test-subj': dataTestSubj }) => { const getTestId = useTestIdGenerator(dataTestSubj); + const hoverInfo = useMemo( + () => ( + + + + ), + [] + ); return ( 1 ? 'rightCenter' : 'rightUp'} data-test-subj={dataTestSubj} + loading={loadingPoliciesList} + hoverInfo={hoverInfo} button={ - + {children} } + title={POLICY_EFFECT_SCOPE_TITLE(policies.length)} /> ); } 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 3843d7992bdf25..b7e085a1f43c26 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 @@ -12,23 +12,26 @@ import classNames from 'classnames'; export type TextValueDisplayProps = PropsWithChildren<{ bold?: boolean; truncate?: boolean; + size?: 'xs' | 's' | 'm' | 'relative'; }>; /** * Common component for displaying consistent text across the card. Changes here could impact all of * display of values on the card */ -export const TextValueDisplay = memo(({ bold, truncate, children }) => { - const cssClassNames = useMemo(() => { - return classNames({ - 'eui-textTruncate': truncate, - }); - }, [truncate]); +export const TextValueDisplay = memo( + ({ bold, truncate, size = 's', children }) => { + const cssClassNames = useMemo(() => { + return classNames({ + 'eui-textTruncate': truncate, + }); + }, [truncate]); - return ( - - {bold ? {children} : children} - - ); -}); + return ( + + {bold ? {children} : children} + + ); + } +); TextValueDisplay.displayName = 'TextValueDisplay'; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/touched_by_users.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/touched_by_users.tsx index d897b6caaa45d2..6d9be470108f6b 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/touched_by_users.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/touched_by_users.tsx @@ -7,10 +7,15 @@ import React, { memo } from 'react'; import { CommonProps, EuiAvatar, EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; import { CREATED_BY, LAST_UPDATED_BY } from './translations'; import { TextValueDisplay } from './text_value_display'; import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; +const StyledEuiFlexItem = styled(EuiFlexItem)` + margin: 6px; +`; + export interface TouchedByUsersProps extends Pick { createdBy: string; updatedBy: string; @@ -59,10 +64,10 @@ const UserName = memo(({ label, value, 'data-test-subj': dataTest responsive={false} data-test-subj={dataTestSubj} > - + {label} - - + + @@ -75,7 +80,7 @@ const UserName = memo(({ label, value, 'data-test-subj': dataTest {value} - + ); }); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/translations.ts b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/translations.ts index 512724b66d50e6..4cdae5238a1ac9 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/translations.ts +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/translations.ts @@ -39,6 +39,14 @@ export const POLICY_EFFECT_SCOPE = (policyCount = 0) => { }); }; +export const POLICY_EFFECT_SCOPE_TITLE = (policyCount = 0) => + i18n.translate('xpack.securitySolution.artifactCard.policyEffectScope.title', { + defaultMessage: 'Applied to the following {count, plural, one {policy} other {policies}}', + values: { + count: policyCount, + }, + }); + export const CONDITION_OPERATOR_TYPE_MATCH = i18n.translate( 'xpack.securitySolution.artifactCard.conditions.matchOperator', { diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/types.ts b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/types.ts index fe50a15190f118..0fd3269500f34c 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/types.ts +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/types.ts @@ -27,7 +27,7 @@ export interface ArtifactInfo 'name' | 'created_at' | 'updated_at' | 'created_by' | 'updated_by' | 'description' | 'comments' > { effectScope: EffectScope; - os: string; + os: string[]; entries: ArtifactInfoEntries[]; } diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/map_to_artifact_info.ts b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/map_to_artifact_info.ts index 5969cf9d043b40..60224b63f426f8 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/map_to_artifact_info.ts +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/map_to_artifact_info.ts @@ -26,16 +26,11 @@ export const mapToArtifactInfo = (_item: MaybeImmutable): ArtifactI description, comments: isTrustedApp(item) ? [] : item.comments, entries: entries as unknown as ArtifactInfo['entries'], - os: isTrustedApp(item) ? item.os : getOsFromExceptionItem(item), + os: isTrustedApp(item) ? [item.os] : item.os_types ?? [], effectScope: isTrustedApp(item) ? item.effectScope : getEffectScopeFromExceptionItem(item), }; }; -const getOsFromExceptionItem = (item: ExceptionListItemSchema): string => { - // FYI: Exceptions seem to allow for items to be assigned to more than one OS, unlike Event Filters and Trusted Apps - return item.os_types.join(', '); -}; - const getEffectScopeFromExceptionItem = (item: ExceptionListItemSchema): EffectScope => { return tagsToEffectScope(item.tags); }; diff --git a/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_item_nav_by_router.tsx b/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_item_nav_by_router.tsx index b955d9fe71db76..1a410c977b0d21 100644 --- a/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_item_nav_by_router.tsx +++ b/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_item_nav_by_router.tsx @@ -6,7 +6,13 @@ */ import React, { memo } from 'react'; -import { EuiContextMenuItem, EuiContextMenuItemProps } from '@elastic/eui'; +import { + EuiContextMenuItem, + EuiContextMenuItemProps, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import styled from 'styled-components'; import { NavigateToAppOptions } from 'kibana/public'; import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { useTestIdGenerator } from '../hooks/use_test_id_generator'; @@ -22,15 +28,42 @@ export interface ContextMenuItemNavByRouterProps extends EuiContextMenuItemProps * is set on the menu component, this prop will be overridden */ textTruncate?: boolean; + /** Displays an additional info when hover an item */ + hoverInfo?: React.ReactNode; children: React.ReactNode; } +const StyledEuiContextMenuItem = styled(EuiContextMenuItem)` + .additional-info { + display: none; + } + &:hover { + .additional-info { + display: block !important; + } + } +`; + +const StyledEuiFlexItem = styled('div')` + max-width: 50%; + padding-right: 10px; +`; + /** * Just like `EuiContextMenuItem`, but allows for additional props to be defined which will * allow navigation to a URL path via React Router */ + export const ContextMenuItemNavByRouter = memo( - ({ navigateAppId, navigateOptions, onClick, textTruncate, children, ...otherMenuItemProps }) => { + ({ + navigateAppId, + navigateOptions, + onClick, + textTruncate, + hoverInfo, + children, + ...otherMenuItemProps + }) => { const handleOnClickViaNavigateToApp = useNavigateToAppEventHandler(navigateAppId ?? '', { ...navigateOptions, onClick, @@ -38,25 +71,37 @@ export const ContextMenuItemNavByRouter = memo( const getTestId = useTestIdGenerator(otherMenuItemProps['data-test-subj']); return ( - - {textTruncate ? ( -
- {children} -
- ) : ( - children - )} -
+ + {textTruncate ? ( + <> +
+ {children} +
+ {hoverInfo && ( + {hoverInfo} + )} + + ) : ( + <> + {children} + {hoverInfo && ( + {hoverInfo} + )} + + )} +
+ ); } ); diff --git a/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_with_router_support.test.tsx b/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_with_router_support.test.tsx index 8efa320c1789fa..ae343a57c734ff 100644 --- a/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_with_router_support.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_with_router_support.test.tsx @@ -130,4 +130,23 @@ describe('When using the ContextMenuWithRouterSupport component', () => { expect.objectContaining({ path: '/one/two/three' }) ); }); + + it('should display loading state', () => { + render({ loading: true }); + clickMenuTriggerButton(); + expect(renderResult.getByTestId('testMenu-item-loading-1')).not.toBeNull(); + expect(renderResult.getByTestId('testMenu-item-loading-2')).not.toBeNull(); + }); + + it('should display view details button when prop', () => { + render({ hoverInfo: 'test' }); + clickMenuTriggerButton(); + expect(renderResult.getByTestId('testMenu-item-1').textContent).toEqual('click me 2test'); + }); + + it("shouldn't display view details button when no prop", () => { + render(); + clickMenuTriggerButton(); + expect(renderResult.getByTestId('testMenu-item-1').textContent).toEqual('click me 2'); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_with_router_support.tsx b/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_with_router_support.tsx index 6e33ad9218bb64..cc9b652ff9344d 100644 --- a/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_with_router_support.tsx +++ b/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_with_router_support.tsx @@ -12,6 +12,8 @@ import { EuiContextMenuPanelProps, EuiPopover, EuiPopoverProps, + EuiPopoverTitle, + EuiLoadingContent, } from '@elastic/eui'; import uuid from 'uuid'; import { @@ -30,6 +32,16 @@ export interface ContextMenuWithRouterSupportProps * overwritten to `true`. Setting this prop's value to `undefined` will suppress the default behaviour. */ maxWidth?: CSSProperties['maxWidth']; + /** + * The max height for the popup menu. Default is `255px`. + */ + maxHeight?: CSSProperties['maxHeight']; + /** + * It makes the panel scrollable + */ + title?: string; + loading?: boolean; + hoverInfo?: React.ReactNode; } /** @@ -38,7 +50,18 @@ export interface ContextMenuWithRouterSupportProps * Menu also supports automatically closing the popup when an item is clicked. */ export const ContextMenuWithRouterSupport = memo( - ({ items, button, panelPaddingSize, anchorPosition, maxWidth = '32ch', ...commonProps }) => { + ({ + items, + button, + panelPaddingSize, + anchorPosition, + maxWidth = '32ch', + maxHeight = '255px', + title, + loading = false, + hoverInfo, + ...commonProps + }) => { const getTestId = useTestIdGenerator(commonProps['data-test-subj']); const [isOpen, setIsOpen] = useState(false); @@ -51,12 +74,22 @@ export const ContextMenuWithRouterSupport = memo { return items.map((itemProps, index) => { + if (loading) { + return ( + + ); + } return ( { handleCloseMenu(); if (itemProps.onClick) { @@ -66,7 +99,7 @@ export const ContextMenuWithRouterSupport = memo ); }); - }, [getTestId, handleCloseMenu, items, maxWidth]); + }, [getTestId, handleCloseMenu, items, maxWidth, loading, hoverInfo]); type AdditionalPanelProps = Partial>; const additionalContextMenuPanelProps = useMemo(() => { @@ -79,8 +112,15 @@ export const ContextMenuWithRouterSupport = memo - + {title ? {title} : null} + ); } diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx index d7db249475df73..084978d35d03ab 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx @@ -20,7 +20,6 @@ jest.mock('../../../common/components/user_privileges/use_endpoint_privileges'); let onSearchMock: jest.Mock; const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; -// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 describe('Search exceptions', () => { let appTestContext: AppContextTestRender; let renderResult: ReturnType; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts index e0b5837c2f78a7..c724773593f53b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts @@ -122,30 +122,27 @@ export const endpointActivityLogHttpMock = const responseData = fleetActionGenerator.generateResponse({ agent_id: endpointMetadata.agent.id, }); - return { - body: { - page: 1, - pageSize: 50, - startDate: 'now-1d', - endDate: 'now', - data: [ - { - type: 'response', - item: { - id: '', - data: responseData, - }, + page: 1, + pageSize: 50, + startDate: 'now-1d', + endDate: 'now', + data: [ + { + type: 'response', + item: { + id: '', + data: responseData, }, - { - type: 'action', - item: { - id: '', - data: actionData, - }, + }, + { + type: 'action', + item: { + id: '', + data: actionData, }, - ], - }, + }, + ], }; }, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index 43fa4e104067fe..81c4dc6f2f7dec 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -61,8 +61,7 @@ jest.mock('../../../../common/lib/kibana'); type EndpointListStore = Store, Immutable>; -// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 -describe.skip('endpoint list middleware', () => { +describe('endpoint list middleware', () => { const getKibanaServicesMock = KibanaServices.get as jest.Mock; let fakeCoreStart: jest.Mocked; let depsStart: DepsStartMock; @@ -390,7 +389,6 @@ describe.skip('endpoint list middleware', () => { it('should call get Activity Log API with correct paging options', async () => { dispatchUserChangedUrl(); - const updatePagingDispatched = waitForAction('endpointDetailsActivityLogUpdatePaging'); dispatchGetActivityLogPaging({ page: 3 }); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts index b58c2d901c2cc3..957fd2d4485bc7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts @@ -8,6 +8,7 @@ import { CreateExceptionListItemSchema, ExceptionListItemSchema, + ExceptionListSummarySchema, FoundExceptionListItemSchema, UpdateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; @@ -112,3 +113,13 @@ export async function updateOneHostIsolationExceptionItem( body: JSON.stringify(exception), }); } +export async function getHostIsolationExceptionSummary( + http: HttpStart +): Promise { + return http.get(`${EXCEPTION_LIST_URL}/summary`, { + query: { + list_id: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, + namespace_type: 'agnostic', + }, + }); +} diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.test.ts index 98b459fac41d38..17708516763bdd 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.test.ts @@ -13,7 +13,7 @@ import { hostIsolationExceptionsPageReducer } from './reducer'; import { getCurrentLocation } from './selector'; import { createEmptyHostIsolationException } from '../utils'; -describe('Host Isolation Exceptions Reducer', () => { +describe('Host isolation exceptions Reducer', () => { let initialState: HostIsolationExceptionsPageState; beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.test.tsx index 2118a8de9b9ed7..9cca87bf61d6a2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.test.tsx @@ -104,7 +104,7 @@ describe('When on the host isolation exceptions delete modal', () => { }); expect(coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith( - '"some name" has been removed from the Host Isolation Exceptions list.' + '"some name" has been removed from the Host isolation exceptions list.' ); }); @@ -129,7 +129,7 @@ describe('When on the host isolation exceptions delete modal', () => { }); expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith( - 'Unable to remove "some name" from the Host Isolation Exceptions list. Reason: That\'s not true. That\'s impossible' + 'Unable to remove "some name" from the Host isolation exceptions list. Reason: That\'s not true. That\'s impossible' ); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.tsx index 61b0bb7f930c3c..51e0ab5a5a1545 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.tsx @@ -54,7 +54,7 @@ export const HostIsolationExceptionDeleteModal = memo<{}>(() => { i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.deletionDialog.deleteSuccess', { - defaultMessage: '"{name}" has been removed from the Host Isolation Exceptions list.', + defaultMessage: '"{name}" has been removed from the Host isolation exceptions list.', values: { name: exception?.name }, } ) @@ -72,7 +72,7 @@ export const HostIsolationExceptionDeleteModal = memo<{}>(() => { 'xpack.securitySolution.hostIsolationExceptions.deletionDialog.deleteFailure', { defaultMessage: - 'Unable to remove "{name}" from the Host Isolation Exceptions list. Reason: {message}', + 'Unable to remove "{name}" from the Host isolation exceptions list. Reason: {message}', values: { name: exception?.name, message: deleteError.message }, } ) @@ -86,7 +86,7 @@ export const HostIsolationExceptionDeleteModal = memo<{}>(() => { diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/empty.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/empty.tsx index eb53268a9fbd88..88cd0abc365cfc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/empty.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/empty.tsx @@ -25,7 +25,7 @@ export const HostIsolationExceptionsEmptyState = memo<{ onAdd: () => void }>(({

} @@ -39,7 +39,7 @@ export const HostIsolationExceptionsEmptyState = memo<{ onAdd: () => void }>(({ } diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx index 7b13df16da4833..01fe8583bae606 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx @@ -179,7 +179,7 @@ export const HostIsolationExceptionsForm: React.FC<{ diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx index 4ab4ed785e4917..3f0a8b9990b839 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx @@ -18,6 +18,7 @@ import uuid from 'uuid'; import { createEmptyHostIsolationException } from '../../utils'; jest.mock('../../service.ts'); +jest.mock('../../../../../common/hooks/use_license'); describe('When on the host isolation exceptions flyout form', () => { let mockedContext: AppContextTestRender; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx index 799e327a3fb4ca..e87ac2adeab497 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx @@ -52,9 +52,7 @@ import { HostIsolationExceptionsForm } from './form'; export const HostIsolationExceptionsFormFlyout: React.FC<{}> = memo(() => { const dispatch = useDispatch>(); const toasts = useToasts(); - const location = useHostIsolationExceptionsSelector(getCurrentLocation); - const creationInProgress = useHostIsolationExceptionsSelector((state) => isLoadingResourceState(state.form.status) ); @@ -62,11 +60,8 @@ export const HostIsolationExceptionsFormFlyout: React.FC<{}> = memo(() => { isLoadedResourceState(state.form.status) ); const creationFailure = useHostIsolationExceptionsSelector(getFormStatusFailure); - const exceptionToEdit = useHostIsolationExceptionsSelector(getExceptionToEdit); - const navigateCallback = useHostIsolationExceptionsNavigateCallback(); - const history = useHistory(); const [formHasError, setFormHasError] = useState(true); @@ -186,12 +181,12 @@ export const HostIsolationExceptionsFormFlyout: React.FC<{}> = memo(() => { {exception?.item_id ? ( ) : ( )} @@ -211,14 +206,14 @@ export const HostIsolationExceptionsFormFlyout: React.FC<{}> = memo(() => {

) : (

)} diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/translations.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/translations.ts index 207e094453d905..69f2c7809a52ae 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/translations.ts @@ -32,7 +32,7 @@ export const NAME_ERROR = i18n.translate( export const DESCRIPTION_PLACEHOLDER = i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.form.description.placeholder', { - defaultMessage: 'Describe your Host Isolation Exception', + defaultMessage: 'Describe your Host isolation exception', } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.test.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.test.ts new file mode 100644 index 00000000000000..6a4e0cb8401495 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.test.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 { useLicense } from '../../../../common/hooks/use_license'; +import { useCanSeeHostIsolationExceptionsMenu } from './hooks'; +import { renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../../common/mock'; +import { getHostIsolationExceptionSummary } from '../service'; + +jest.mock('../../../../common/hooks/use_license'); +jest.mock('../service'); + +const getHostIsolationExceptionSummaryMock = getHostIsolationExceptionSummary as jest.Mock; + +describe('host isolation exceptions hooks', () => { + const isPlatinumPlusMock = useLicense().isPlatinumPlus as jest.Mock; + describe('useCanSeeHostIsolationExceptionsMenu', () => { + beforeEach(() => { + isPlatinumPlusMock.mockReset(); + }); + it('should return true if the license is platinum plus', () => { + isPlatinumPlusMock.mockReturnValue(true); + const { result } = renderHook(() => useCanSeeHostIsolationExceptionsMenu(), { + wrapper: TestProviders, + }); + expect(result.current).toBe(true); + }); + + it('should return false if the license is lower platinum plus and there are not existing host isolation items', () => { + isPlatinumPlusMock.mockReturnValue(false); + getHostIsolationExceptionSummaryMock.mockReturnValueOnce({ total: 0 }); + const { result } = renderHook(() => useCanSeeHostIsolationExceptionsMenu(), { + wrapper: TestProviders, + }); + expect(result.current).toBe(false); + }); + + it('should return true if the license is lower platinum plus and there are existing host isolation items', async () => { + isPlatinumPlusMock.mockReturnValue(false); + getHostIsolationExceptionSummaryMock.mockReturnValueOnce({ total: 11 }); + const { result, waitForNextUpdate } = renderHook( + () => useCanSeeHostIsolationExceptionsMenu(), + { + wrapper: TestProviders, + } + ); + await waitForNextUpdate(); + expect(result.current).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts index db9ec467e71701..4b6129785c84ac 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts @@ -4,15 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import { useHttp } from '../../../../common/lib/kibana/hooks'; +import { useLicense } from '../../../../common/hooks/use_license'; import { State } from '../../../../common/store'; import { MANAGEMENT_STORE_GLOBAL_NAMESPACE, MANAGEMENT_STORE_HOST_ISOLATION_EXCEPTIONS_NAMESPACE, } from '../../../common/constants'; import { getHostIsolationExceptionsListPath } from '../../../common/routing'; +import { getHostIsolationExceptionSummary } from '../service'; import { getCurrentLocation } from '../store/selector'; import { HostIsolationExceptionsPageLocation, HostIsolationExceptionsPageState } from '../types'; @@ -36,3 +39,33 @@ export function useHostIsolationExceptionsNavigateCallback() { [history, location] ); } + +/** + * Checks if the current user should be able to see the host isolation exceptions + * menu item based on their current license level and existing excepted items. + */ +export function useCanSeeHostIsolationExceptionsMenu() { + const license = useLicense(); + const http = useHttp(); + + const [hasExceptions, setHasExceptions] = useState(license.isPlatinumPlus()); + + useEffect(() => { + async function checkIfHasExceptions() { + try { + const summary = await getHostIsolationExceptionSummary(http); + if (summary?.total > 0) { + setHasExceptions(true); + } + } catch (error) { + // an error will ocurr if the exception list does not exist + setHasExceptions(false); + } + } + if (!license.isPlatinumPlus()) { + checkIfHasExceptions(); + } + }, [http, license]); + + return hasExceptions; +} diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx index 9de3d83ed8babd..5113457e5bccc6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx @@ -14,9 +14,12 @@ import { AppContextTestRender, createAppRootMockRenderer } from '../../../../com import { isFailedResourceState, isLoadedResourceState } from '../../../state'; import { getHostIsolationExceptionItems } from '../service'; import { HostIsolationExceptionsList } from './host_isolation_exceptions_list'; +import { useLicense } from '../../../../common/hooks/use_license'; jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); + jest.mock('../service'); +jest.mock('../../../../common/hooks/use_license'); const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock; @@ -25,9 +28,13 @@ describe('When on the host isolation exceptions page', () => { let renderResult: ReturnType; let history: AppContextTestRender['history']; let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction']; + let mockedContext: AppContextTestRender; + + const isPlatinumPlusMock = useLicense().isPlatinumPlus as jest.Mock; + beforeEach(() => { getHostIsolationExceptionItemsMock.mockReset(); - const mockedContext = createAppRootMockRenderer(); + mockedContext = createAppRootMockRenderer(); ({ history } = mockedContext); render = () => (renderResult = mockedContext.render()); waitForAction = mockedContext.middlewareSpy.waitForAction; @@ -60,11 +67,18 @@ describe('When on the host isolation exceptions page', () => { await dataReceived(); expect(renderResult.getByTestId('hostIsolationExceptionsEmpty')).toBeTruthy(); }); + + it('should not display the search bar', async () => { + render(); + await dataReceived(); + expect(renderResult.queryByTestId('searchExceptions')).toBeFalsy(); + }); }); describe('And data exists', () => { beforeEach(async () => { getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock); }); + it('should show loading indicator while retrieving data', async () => { let releaseApiResponse: (value?: unknown) => void; @@ -81,6 +95,12 @@ describe('When on the host isolation exceptions page', () => { expect(renderResult.container.querySelector('.euiProgress')).toBeNull(); }); + it('should display the search bar', async () => { + render(); + await dataReceived(); + expect(renderResult.getByTestId('searchExceptions')).toBeTruthy(); + }); + it('should show items on the list', async () => { render(); await dataReceived(); @@ -106,17 +126,40 @@ describe('When on the host isolation exceptions page', () => { ).toEqual(' Server is too far away'); }); }); - it('should show the create flyout when the add button is pressed', () => { - render(); - act(() => { - userEvent.click(renderResult.getByTestId('hostIsolationExceptionsListAddButton')); + + describe('is license platinum plus', () => { + beforeEach(() => { + isPlatinumPlusMock.mockReturnValue(true); + }); + it('should show the create flyout when the add button is pressed', () => { + render(); + act(() => { + userEvent.click(renderResult.getByTestId('hostIsolationExceptionsListAddButton')); + }); + expect(renderResult.getByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeTruthy(); + }); + it('should show the create flyout when the show location is create', () => { + history.push(`${HOST_ISOLATION_EXCEPTIONS_PATH}?show=create`); + render(); + expect(renderResult.getByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeTruthy(); + expect(renderResult.queryByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeTruthy(); }); - expect(renderResult.getByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeTruthy(); }); - it('should show the create flyout when the show location is create', () => { - history.push(`${HOST_ISOLATION_EXCEPTIONS_PATH}?show=create`); - render(); - expect(renderResult.getByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeTruthy(); + + describe('is not license platinum plus', () => { + beforeEach(() => { + isPlatinumPlusMock.mockReturnValue(false); + }); + it('should not show the create flyout if the user navigates to the create url', () => { + history.push(`${HOST_ISOLATION_EXCEPTIONS_PATH}?show=create`); + render(); + expect(renderResult.queryByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeFalsy(); + }); + it('should not show the create flyout if the user navigates to the edit url', () => { + history.push(`${HOST_ISOLATION_EXCEPTIONS_PATH}?show=edit`); + render(); + expect(renderResult.queryByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeFalsy(); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx index 3c634a917c0cef..096575bab360cd 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx @@ -7,11 +7,13 @@ import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { i18n } from '@kbn/i18n'; -import React, { Dispatch, useCallback } from 'react'; +import React, { Dispatch, useCallback, useEffect } from 'react'; import { EuiButton, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch } from 'react-redux'; +import { useHistory } from 'react-router-dom'; import { ExceptionItem } from '../../../../common/components/exceptions/viewer/exception_item'; +import { useLicense } from '../../../../common/hooks/use_license'; import { getCurrentLocation, getItemToDelete, @@ -37,6 +39,7 @@ import { DELETE_HOST_ISOLATION_EXCEPTION_LABEL, EDIT_HOST_ISOLATION_EXCEPTION_LABEL, } from './components/translations'; +import { getEndpointListPath } from '../../../common/routing'; type HostIsolationExceptionPaginatedContent = PaginatedContentProps< Immutable, @@ -51,10 +54,16 @@ export const HostIsolationExceptionsList = () => { const location = useHostIsolationExceptionsSelector(getCurrentLocation); const dispatch = useDispatch>(); const itemToDelete = useHostIsolationExceptionsSelector(getItemToDelete); - - const showFlyout = !!location.show; - const navigateCallback = useHostIsolationExceptionsNavigateCallback(); + const history = useHistory(); + const license = useLicense(); + const showFlyout = license.isPlatinumPlus() && !!location.show; + + useEffect(() => { + if (!isLoading && listItems.length === 0 && !license.isPlatinumPlus()) { + history.replace(getEndpointListPath({ name: 'endpointList' })); + } + }, [history, isLoading, license, listItems.length]); const handleOnSearch = useCallback( (query: string) => { @@ -63,34 +72,35 @@ export const HostIsolationExceptionsList = () => { [navigateCallback] ); - const handleItemComponentProps = (element: ExceptionListItemSchema): ArtifactEntryCardProps => ({ - item: element, - 'data-test-subj': `hostIsolationExceptionsCard`, - actions: [ - { - icon: 'trash', - onClick: () => { - navigateCallback({ - show: 'edit', - id: element.id, - }); - }, - 'data-test-subj': 'editHostIsolationException', - children: EDIT_HOST_ISOLATION_EXCEPTION_LABEL, + function handleItemComponentProps(element: ExceptionListItemSchema): ArtifactEntryCardProps { + const editAction = { + icon: 'trash', + onClick: () => { + navigateCallback({ + show: 'edit', + id: element.id, + }); }, - { - icon: 'trash', - onClick: () => { - dispatch({ - type: 'hostIsolationExceptionsMarkToDelete', - payload: element, - }); - }, - 'data-test-subj': 'deleteHostIsolationException', - children: DELETE_HOST_ISOLATION_EXCEPTION_LABEL, + 'data-test-subj': 'editHostIsolationException', + children: EDIT_HOST_ISOLATION_EXCEPTION_LABEL, + }; + const deleteAction = { + icon: 'trash', + onClick: () => { + dispatch({ + type: 'hostIsolationExceptionsMarkToDelete', + payload: element, + }); }, - ], - }); + 'data-test-subj': 'deleteHostIsolationException', + children: DELETE_HOST_ISOLATION_EXCEPTION_LABEL, + }; + return { + item: element, + 'data-test-subj': `hostIsolationExceptionsCard`, + actions: license.isPlatinumPlus() ? [editAction, deleteAction] : [deleteAction], + }; + } const handlePaginatedContentChange: HostIsolationExceptionPaginatedContent['onChange'] = useCallback( @@ -117,38 +127,47 @@ export const HostIsolationExceptionsList = () => { title={ } actions={ - - - + license.isPlatinumPlus() ? ( + + + + ) : ( + [] + ) } > {showFlyout && } - - {itemToDelete ? : null} + + {listItems.length ? ( + + ) : null} + + + items={listItems} ItemComponent={ArtifactEntryCard} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx index d46775d38834b8..43e19c00bcc8e6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx @@ -19,20 +19,14 @@ import { createLoadedResourceState, isLoadedResourceState } from '../../../../.. import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data'; import { policyListApiPathHandlers } from '../../../store/test_mock_utils'; -import { licenseService } from '../../../../../../common/hooks/use_license'; +import { + EndpointPrivileges, + useEndpointPrivileges, +} from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; jest.mock('../../../../trusted_apps/service'); -jest.mock('../../../../../../common/hooks/use_license', () => { - const licenseServiceInstance = { - isPlatinumPlus: jest.fn(), - }; - return { - licenseService: licenseServiceInstance, - useLicense: () => { - return licenseServiceInstance; - }, - }; -}); +jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges'); +const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; let mockedContext: AppContextTestRender; let waitForAction: MiddlewareActionSpyHelper['waitForAction']; @@ -42,8 +36,17 @@ let coreStart: AppContextTestRender['coreStart']; let http: typeof coreStart.http; const generator = new EndpointDocGenerator(); -// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 -describe.skip('Policy trusted apps layout', () => { +describe('Policy trusted apps layout', () => { + const loadedUserEndpointPrivilegesState = ( + endpointOverrides: Partial = {} + ): EndpointPrivileges => ({ + loading: false, + canAccessFleet: true, + canAccessEndpointManagement: true, + isPlatinumPlus: true, + ...endpointOverrides, + }); + beforeEach(() => { mockedContext = createAppRootMockRenderer(); http = mockedContext.coreStart.http; @@ -59,6 +62,14 @@ describe.skip('Policy trusted apps layout', () => { }); } + // GET Agent status for agent policy + if (path === '/api/fleet/agent-status') { + return Promise.resolve({ + results: { events: 0, total: 5, online: 3, error: 1, offline: 1 }, + success: true, + }); + } + // Get package data // Used in tests that route back to the list if (policyListApiHandlers[path]) { @@ -78,6 +89,10 @@ describe.skip('Policy trusted apps layout', () => { render = () => mockedContext.render(); }); + afterAll(() => { + mockUseEndpointPrivileges.mockReset(); + }); + afterEach(() => reactTestingLibrary.cleanup()); it('should renders layout with no existing TA data', async () => { @@ -117,11 +132,15 @@ describe.skip('Policy trusted apps layout', () => { await waitForAction('assignedTrustedAppsListStateChanged'); - expect(component.getByTestId('policyDetailsTrustedAppsCount')).not.toBeNull(); + expect(component.getAllByTestId('policyTrustedAppsGrid-card')).toHaveLength(10); }); it('should hide assign button on empty state with unassigned policies when downgraded to a gold or below license', async () => { - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); + mockUseEndpointPrivileges.mockReturnValue( + loadedUserEndpointPrivilegesState({ + isPlatinumPlus: false, + }) + ); const component = render(); mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234')); @@ -133,8 +152,13 @@ describe.skip('Policy trusted apps layout', () => { }); expect(component.queryByTestId('assign-ta-button')).toBeNull(); }); + it('should hide the `Assign trusted applications` button when there is data and the license is downgraded to gold or below', async () => { - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); + mockUseEndpointPrivileges.mockReturnValue( + loadedUserEndpointPrivilegesState({ + isPlatinumPlus: false, + }) + ); TrustedAppsHttpServiceMock.mockImplementation(() => { return { getTrustedAppsList: () => getMockListResponse(), 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 2421602f4e5af4..a3f1ed215286a5 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 @@ -7,12 +7,16 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiTitle, EuiPageHeader, EuiPageHeaderSection, EuiPageContent, + EuiText, + EuiSpacer, + EuiLink, } from '@elastic/eui'; import { PolicyTrustedAppsEmptyUnassigned, PolicyTrustedAppsEmptyUnexisting } from '../empty'; import { @@ -21,13 +25,18 @@ import { policyDetails, doesPolicyHaveTrustedApps, doesTrustedAppExistsLoading, + getPolicyTrustedAppsListPagination, } from '../../../store/policy_details/selectors'; import { usePolicyDetailsNavigateCallback, usePolicyDetailsSelector } from '../../policy_hooks'; import { PolicyTrustedAppsFlyout } from '../flyout'; import { PolicyTrustedAppsList } from '../list/policy_trusted_apps_list'; import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; +import { useAppUrl } from '../../../../../../common/lib/kibana'; +import { APP_ID } from '../../../../../../../common/constants'; +import { getTrustedAppsListPath } from '../../../../../common/routing'; export const PolicyTrustedAppsLayout = React.memo(() => { + const { getAppUrl } = useAppUrl(); const location = usePolicyDetailsSelector(getCurrentArtifactsLocation); const doesTrustedAppExists = usePolicyDetailsSelector(getDoesTrustedAppExists); const isDoesTrustedAppExistsLoading = usePolicyDetailsSelector(doesTrustedAppExistsLoading); @@ -35,6 +44,9 @@ export const PolicyTrustedAppsLayout = React.memo(() => { const navigateCallback = usePolicyDetailsNavigateCallback(); const hasAssignedTrustedApps = usePolicyDetailsSelector(doesPolicyHaveTrustedApps); const { isPlatinumPlus } = useEndpointPrivileges(); + const totalAssignedCount = usePolicyDetailsSelector( + getPolicyTrustedAppsListPagination + ).totalItemCount; const showListFlyout = location.show === 'list'; @@ -78,21 +90,57 @@ export const PolicyTrustedAppsLayout = React.memo(() => { [hasAssignedTrustedApps.loading, isDoesTrustedAppExistsLoading] ); + const aboutInfo = useMemo(() => { + const link = ( + + + + ); + + return ( + + ); + }, [getAppUrl, totalAssignedCount]); + return policyItem ? (
{!displaysEmptyStateIsLoading && !displaysEmptyState ? ( - - - -

- {i18n.translate('xpack.securitySolution.endpoint.policy.trustedApps.layout.title', { - defaultMessage: 'Assigned trusted applications', - })} -

-
-
- {isPlatinumPlus && assignTrustedAppButton} -
+ <> + + + +

+ {i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.layout.title', + { + defaultMessage: 'Assigned trusted applications', + } + )} +

+
+ + + + +

{aboutInfo}

+
+
+ + {isPlatinumPlus && assignTrustedAppButton} +
+ + + ) : null} { /> ) ) : ( - + )} {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 316b70064d9db0..a8d3cc1505463c 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 @@ -10,7 +10,7 @@ import { createAppRootMockRenderer, } from '../../../../../../common/mock/endpoint'; import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; -import { PolicyTrustedAppsList } from './policy_trusted_apps_list'; +import { PolicyTrustedAppsList, PolicyTrustedAppsListProps } from './policy_trusted_apps_list'; import React from 'react'; import { policyDetailsPageAllApiHttpMocks } from '../../../test_utils'; import { @@ -38,6 +38,7 @@ describe('when rendering the PolicyTrustedAppsList', () => { let render: (waitForLoadedState?: boolean) => Promise>; let mockedApis: ReturnType; let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction']; + let componentRenderProps: PolicyTrustedAppsListProps; const loadedUserEndpointPrivilegesState = ( endpointOverrides: Partial = {} @@ -93,6 +94,7 @@ describe('when rendering the PolicyTrustedAppsList', () => { mockedApis = policyDetailsPageAllApiHttpMocks(appTestContext.coreStart.http); appTestContext.setExperimentalFlag({ trustedAppsByPolicyEnabled: true }); waitForAction = appTestContext.middlewareSpy.waitForAction; + componentRenderProps = {}; render = async (waitForLoadedState: boolean = true) => { appTestContext.history.push( @@ -106,7 +108,7 @@ describe('when rendering the PolicyTrustedAppsList', () => { }) : Promise.resolve(); - renderResult = appTestContext.render(); + renderResult = appTestContext.render(); await trustedAppDataReceived; return renderResult; @@ -135,6 +137,13 @@ describe('when rendering the PolicyTrustedAppsList', () => { ); }); + it('should NOT show total number if `hideTotalShowingLabel` prop is true', async () => { + componentRenderProps.hideTotalShowingLabel = true; + await render(); + + expect(renderResult.queryByTestId('policyDetailsTrustedAppsCount')).toBeNull(); + }); + it('should show card grid', async () => { await render(); @@ -244,11 +253,11 @@ describe('when rendering the PolicyTrustedAppsList', () => { expect( renderResult.getByTestId('policyTrustedAppsGrid-card-header-effectScope-popupMenu-item-0') .textContent - ).toEqual('Endpoint Policy 0'); + ).toEqual('Endpoint Policy 0View details'); expect( renderResult.getByTestId('policyTrustedAppsGrid-card-header-effectScope-popupMenu-item-1') .textContent - ).toEqual('Endpoint Policy 1'); + ).toEqual('Endpoint Policy 1View details'); }); it('should navigate to policy details when clicking policy on assignment context menu', async () => { 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 8ab2f5fd465e06..f6afd9d5024860 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 @@ -42,217 +42,232 @@ import { useEndpointPrivileges } from '../../../../../../common/components/user_ const DATA_TEST_SUBJ = 'policyTrustedAppsGrid'; -export const PolicyTrustedAppsList = memo(() => { - const getTestId = useTestIdGenerator(DATA_TEST_SUBJ); - const toasts = useToasts(); - const history = useHistory(); - const { getAppUrl } = useAppUrl(); - const { isPlatinumPlus } = useEndpointPrivileges(); - const policyId = usePolicyDetailsSelector(policyIdFromParams); - const hasTrustedApps = usePolicyDetailsSelector(doesPolicyHaveTrustedApps); - const isLoading = usePolicyDetailsSelector(isPolicyTrustedAppListLoading); - const isTrustedAppExistsCheckLoading = usePolicyDetailsSelector(doesTrustedAppExistsLoading); - const trustedAppItems = usePolicyDetailsSelector(getPolicyTrustedAppList); - const pagination = usePolicyDetailsSelector(getPolicyTrustedAppsListPagination); - const urlParams = usePolicyDetailsSelector(getCurrentArtifactsLocation); - const allPoliciesById = usePolicyDetailsSelector(getTrustedAppsAllPoliciesById); - const trustedAppsApiError = usePolicyDetailsSelector(getPolicyTrustedAppListError); +export interface PolicyTrustedAppsListProps { + hideTotalShowingLabel?: boolean; +} - const [isCardExpanded, setCardExpanded] = useState>({}); - const [trustedAppsForRemoval, setTrustedAppsForRemoval] = useState([]); - const [showRemovalModal, setShowRemovalModal] = useState(false); +export const PolicyTrustedAppsList = memo( + ({ hideTotalShowingLabel = false }) => { + const getTestId = useTestIdGenerator(DATA_TEST_SUBJ); + const toasts = useToasts(); + const history = useHistory(); + const { getAppUrl } = useAppUrl(); + const { isPlatinumPlus } = useEndpointPrivileges(); + const policyId = usePolicyDetailsSelector(policyIdFromParams); + const hasTrustedApps = usePolicyDetailsSelector(doesPolicyHaveTrustedApps); + const isLoading = usePolicyDetailsSelector(isPolicyTrustedAppListLoading); + const isTrustedAppExistsCheckLoading = usePolicyDetailsSelector(doesTrustedAppExistsLoading); + const trustedAppItems = usePolicyDetailsSelector(getPolicyTrustedAppList); + const pagination = usePolicyDetailsSelector(getPolicyTrustedAppsListPagination); + const urlParams = usePolicyDetailsSelector(getCurrentArtifactsLocation); + const allPoliciesById = usePolicyDetailsSelector(getTrustedAppsAllPoliciesById); + const trustedAppsApiError = usePolicyDetailsSelector(getPolicyTrustedAppListError); - const handlePageChange = useCallback( - ({ pageIndex, pageSize }) => { - history.push( - getPolicyDetailsArtifactsListPath(policyId, { - ...urlParams, - // If user changed page size, then reset page index back to the first page - page_index: pageSize !== pagination.pageSize ? 0 : pageIndex, - page_size: pageSize, - }) - ); - }, - [history, pagination.pageSize, policyId, urlParams] - ); + const [isCardExpanded, setCardExpanded] = useState>({}); + const [trustedAppsForRemoval, setTrustedAppsForRemoval] = useState([]); + const [showRemovalModal, setShowRemovalModal] = useState(false); - const handleExpandCollapse = useCallback( - ({ expanded, collapsed }) => { - const newCardExpandedSettings: Record = {}; + const handlePageChange = useCallback( + ({ pageIndex, pageSize }) => { + history.push( + getPolicyDetailsArtifactsListPath(policyId, { + ...urlParams, + // If user changed page size, then reset page index back to the first page + page_index: pageSize !== pagination.pageSize ? 0 : pageIndex, + page_size: pageSize, + }) + ); + }, + [history, pagination.pageSize, policyId, urlParams] + ); - for (const trustedApp of expanded) { - newCardExpandedSettings[trustedApp.id] = true; - } + const handleExpandCollapse = useCallback( + ({ expanded, collapsed }) => { + const newCardExpandedSettings: Record = {}; - for (const trustedApp of collapsed) { - newCardExpandedSettings[trustedApp.id] = false; - } + for (const trustedApp of expanded) { + newCardExpandedSettings[trustedApp.id] = true; + } - setCardExpanded(newCardExpandedSettings); - }, - [] - ); + for (const trustedApp of collapsed) { + newCardExpandedSettings[trustedApp.id] = false; + } - const totalItemsCountLabel = useMemo(() => { - return i18n.translate('xpack.securitySolution.endpoint.policy.trustedApps.list.totalCount', { - defaultMessage: - 'Showing {totalItemsCount, plural, one {# trusted application} other {# trusted applications}}', - values: { totalItemsCount: pagination.totalItemCount }, - }); - }, [pagination.totalItemCount]); + setCardExpanded(newCardExpandedSettings); + }, + [] + ); - const cardProps = useMemo, ArtifactCardGridCardComponentProps>>(() => { - const newCardProps = new Map(); + const totalItemsCountLabel = useMemo(() => { + return i18n.translate('xpack.securitySolution.endpoint.policy.trustedApps.list.totalCount', { + defaultMessage: + 'Showing {totalItemsCount, plural, one {# trusted application} other {# trusted applications}}', + values: { totalItemsCount: pagination.totalItemCount }, + }); + }, [pagination.totalItemCount]); - for (const trustedApp of trustedAppItems) { - const isGlobal = trustedApp.effectScope.type === 'global'; - const viewUrlPath = getTrustedAppsListPath({ id: trustedApp.id, show: 'edit' }); - const assignedPoliciesMenuItems: ArtifactEntryCollapsibleCardProps['policies'] = - trustedApp.effectScope.type === 'global' - ? undefined - : trustedApp.effectScope.policies.reduce< - Required['policies'] - >((byIdPolicies, trustedAppAssignedPolicyId) => { - if (!allPoliciesById[trustedAppAssignedPolicyId]) { - byIdPolicies[trustedAppAssignedPolicyId] = { children: trustedAppAssignedPolicyId }; - return byIdPolicies; - } + const cardProps = useMemo< + Map, ArtifactCardGridCardComponentProps> + >(() => { + const newCardProps = new Map(); - const policyDetailsPath = getPolicyDetailPath(trustedAppAssignedPolicyId); + for (const trustedApp of trustedAppItems) { + const isGlobal = trustedApp.effectScope.type === 'global'; + const viewUrlPath = getTrustedAppsListPath({ id: trustedApp.id, show: 'edit' }); + const assignedPoliciesMenuItems: ArtifactEntryCollapsibleCardProps['policies'] = + trustedApp.effectScope.type === 'global' + ? undefined + : trustedApp.effectScope.policies.reduce< + Required['policies'] + >((byIdPolicies, trustedAppAssignedPolicyId) => { + if (!allPoliciesById[trustedAppAssignedPolicyId]) { + byIdPolicies[trustedAppAssignedPolicyId] = { + children: trustedAppAssignedPolicyId, + }; + return byIdPolicies; + } - const thisPolicyMenuProps: ContextMenuItemNavByRouterProps = { - navigateAppId: APP_ID, - navigateOptions: { - path: policyDetailsPath, - }, - href: getAppUrl({ path: policyDetailsPath }), - children: allPoliciesById[trustedAppAssignedPolicyId].name, - }; + const policyDetailsPath = getPolicyDetailPath(trustedAppAssignedPolicyId); - byIdPolicies[trustedAppAssignedPolicyId] = thisPolicyMenuProps; + const thisPolicyMenuProps: ContextMenuItemNavByRouterProps = { + navigateAppId: APP_ID, + navigateOptions: { + path: policyDetailsPath, + }, + href: getAppUrl({ path: policyDetailsPath }), + children: allPoliciesById[trustedAppAssignedPolicyId].name, + }; - return byIdPolicies; - }, {}); + byIdPolicies[trustedAppAssignedPolicyId] = thisPolicyMenuProps; - const fullDetailsAction: ArtifactCardGridCardComponentProps['actions'] = [ - { - icon: 'controlsHorizontal', - children: i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.list.viewAction', - { defaultMessage: 'View full details' } - ), - href: getAppUrl({ appId: APP_ID, path: viewUrlPath }), - navigateAppId: APP_ID, - navigateOptions: { path: viewUrlPath }, - 'data-test-subj': getTestId('viewFullDetailsAction'), - }, - ]; - const thisTrustedAppCardProps: ArtifactCardGridCardComponentProps = { - expanded: Boolean(isCardExpanded[trustedApp.id]), - actions: isPlatinumPlus - ? [ - ...fullDetailsAction, - { - icon: 'trash', - children: i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeAction', - { defaultMessage: 'Remove from policy' } - ), - onClick: () => { - setTrustedAppsForRemoval([trustedApp]); - setShowRemovalModal(true); + return byIdPolicies; + }, {}); + + const fullDetailsAction: ArtifactCardGridCardComponentProps['actions'] = [ + { + icon: 'controlsHorizontal', + children: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.list.viewAction', + { defaultMessage: 'View full details' } + ), + href: getAppUrl({ appId: APP_ID, path: viewUrlPath }), + navigateAppId: APP_ID, + navigateOptions: { path: viewUrlPath }, + 'data-test-subj': getTestId('viewFullDetailsAction'), + }, + ]; + const thisTrustedAppCardProps: ArtifactCardGridCardComponentProps = { + expanded: Boolean(isCardExpanded[trustedApp.id]), + actions: isPlatinumPlus + ? [ + ...fullDetailsAction, + { + icon: 'trash', + children: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeAction', + { defaultMessage: 'Remove from policy' } + ), + onClick: () => { + setTrustedAppsForRemoval([trustedApp]); + setShowRemovalModal(true); + }, + disabled: isGlobal, + toolTipContent: isGlobal + ? i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeActionNotAllowed', + { + defaultMessage: + 'Globally applied trusted applications cannot be removed from policy.', + } + ) + : undefined, + toolTipPosition: 'top', + 'data-test-subj': getTestId('removeAction'), }, - disabled: isGlobal, - toolTipContent: isGlobal - ? i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeActionNotAllowed', - { - defaultMessage: - 'Globally applied trusted applications cannot be removed from policy.', - } - ) - : undefined, - toolTipPosition: 'top', - 'data-test-subj': getTestId('removeAction'), - }, - ] - : fullDetailsAction, + ] + : fullDetailsAction, - policies: assignedPoliciesMenuItems, - }; + policies: assignedPoliciesMenuItems, + }; - newCardProps.set(trustedApp, thisTrustedAppCardProps); - } + newCardProps.set(trustedApp, thisTrustedAppCardProps); + } - return newCardProps; - }, [allPoliciesById, getAppUrl, getTestId, isCardExpanded, trustedAppItems, isPlatinumPlus]); + return newCardProps; + }, [allPoliciesById, getAppUrl, getTestId, isCardExpanded, trustedAppItems, isPlatinumPlus]); - const provideCardProps = useCallback['cardComponentProps']>( - (item) => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return cardProps.get(item as Immutable)!; - }, - [cardProps] - ); + const provideCardProps = useCallback['cardComponentProps']>( + (item) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return cardProps.get(item as Immutable)!; + }, + [cardProps] + ); - const handleRemoveModalClose = useCallback(() => { - setShowRemovalModal(false); - }, []); + const handleRemoveModalClose = useCallback(() => { + setShowRemovalModal(false); + }, []); - // Anytime a new set of data (trusted apps) is retrieved, reset the card expand state - useEffect(() => { - setCardExpanded({}); - }, [trustedAppItems]); + // Anytime a new set of data (trusted apps) is retrieved, reset the card expand state + useEffect(() => { + setCardExpanded({}); + }, [trustedAppItems]); - // if an error occurred while loading the data, show toast - useEffect(() => { - if (trustedAppsApiError) { - toasts.addError(trustedAppsApiError as unknown as Error, { - title: i18n.translate('xpack.securitySolution.endpoint.policy.trustedApps.list.apiError', { - defaultMessage: 'Error while retrieving list of trusted applications', - }), - }); + // if an error occurred while loading the data, show toast + useEffect(() => { + if (trustedAppsApiError) { + toasts.addError(trustedAppsApiError as unknown as Error, { + title: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.list.apiError', + { + defaultMessage: 'Error while retrieving list of trusted applications', + } + ), + }); + } + }, [toasts, trustedAppsApiError]); + + if (hasTrustedApps.loading || isTrustedAppExistsCheckLoading) { + return ( + + + + ); } - }, [toasts, trustedAppsApiError]); - if (hasTrustedApps.loading || isTrustedAppExistsCheckLoading) { return ( - - - - ); - } - - return ( - <> - - {totalItemsCountLabel} - - - + <> + {!hideTotalShowingLabel && ( + + {totalItemsCountLabel} + + )} - + - {showRemovalModal && ( - - )} - - ); -}); + + {showRemovalModal && ( + + )} + + ); + } +); PolicyTrustedAppsList.displayName = 'PolicyTrustedAppsList'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts index b8c2018cd87874..c780f480008792 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts @@ -228,6 +228,11 @@ export const listOfPolicies: ( return isLoadedResourceState(policies) ? policies.data.items : []; }); +export const isLoadingListOfPolicies: (state: Immutable) => boolean = + createSelector(policiesState, (policies) => { + return isLoadingResourceState(policies); + }); + export const getMapOfPoliciesById: ( state: Immutable ) => Immutable>> = createSelector( 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 e2b5ad43e40f24..0b1b5d4c5675f2 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 @@ -385,10 +385,22 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` position: relative; } +.c5 { + padding-top: 2px; +} + +.c6 { + margin: 6px; +} + .c3.artifactEntryCard + .c2.artifactEntryCard { margin-top: 24px; } +.c4 { + padding: 32px; +} + .c0 .trusted-app + .trusted-app { margin-top: 24px; } @@ -411,17 +423,17 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard" >
Applied globally
@@ -731,7 +743,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
Applied globally
@@ -1114,7 +1126,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
- macos + Mac @@ -1177,17 +1189,17 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard" >
Applied globally
@@ -1497,7 +1509,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
Applied globally
@@ -1880,7 +1892,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
Applied globally
@@ -2263,7 +2275,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
- macos + Mac @@ -2326,17 +2338,17 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard" >
Applied globally
@@ -2646,7 +2658,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
Applied globally
@@ -3029,7 +3041,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
Applied globally
@@ -3412,7 +3424,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
- macos + Mac @@ -3475,17 +3487,17 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` data-test-subj="trustedAppCard" >
Applied globally
@@ -3795,7 +3807,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
Applied globally
@@ -4178,7 +4190,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
Applied globally
@@ -4856,7 +4880,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
Applied globally
@@ -5239,7 +5263,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
- macos + Mac @@ -5302,17 +5326,17 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard" >
Applied globally
@@ -5622,7 +5646,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
Applied globally
@@ -6005,7 +6029,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
Applied globally
@@ -6388,7 +6412,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
- macos + Mac @@ -6451,17 +6475,17 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard" >
Applied globally
@@ -6771,7 +6795,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
Applied globally
@@ -7154,7 +7178,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
Applied globally
@@ -7537,7 +7561,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
- macos + Mac @@ -7600,17 +7624,17 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time data-test-subj="trustedAppCard" >
Applied globally
@@ -7920,7 +7944,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
Applied globally
@@ -8303,7 +8327,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
Applied globally
@@ -8938,7 +8974,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
Applied globally
@@ -9321,7 +9357,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
- macos + Mac @@ -9384,17 +9420,17 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard" >
Applied globally
@@ -9704,7 +9740,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
Applied globally
@@ -10087,7 +10123,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
Applied globally
@@ -10470,7 +10506,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
- macos + Mac @@ -10533,17 +10569,17 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard" >
Applied globally
@@ -10853,7 +10889,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
Applied globally
@@ -11236,7 +11272,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
Applied globally
@@ -11619,7 +11655,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
- macos + Mac @@ -11682,17 +11718,17 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not data-test-subj="trustedAppCard" >
Applied globally
@@ -12002,7 +12038,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
Applied globally
@@ -12385,7 +12421,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
{ const error = useTrustedAppsSelector(getListErrorMessage); const location = useTrustedAppsSelector(getCurrentLocation); const policyListById = useTrustedAppsSelector(getMapOfPoliciesById); + const loadingPoliciesList = useTrustedAppsSelector(isLoadingListOfPolicies); const handlePaginationChange: PaginatedContentProps< TrustedApp, @@ -129,13 +131,13 @@ export const TrustedAppsGrid = memo(() => { }; policyToNavOptionsMap[policyId] = { - navigateAppId: APP_ID, navigateOptions: { path: policyDetailsPath, state: routeState, }, href: getAppUrl({ path: policyDetailsPath }), children: policyListById[policyId]?.name ?? policyId, + target: '_blank', }; return policyToNavOptionsMap; }, {}); @@ -144,6 +146,7 @@ export const TrustedAppsGrid = memo(() => { cachedCardProps[trustedApp.id] = { item: trustedApp, policies, + loadingPoliciesList, hideComments: true, 'data-test-subj': 'trustedAppCard', actions: [ @@ -177,7 +180,7 @@ export const TrustedAppsGrid = memo(() => { } return cachedCardProps; - }, [dispatch, getAppUrl, history, listItems, location, policyListById]); + }, [dispatch, getAppUrl, history, listItems, location, policyListById, loadingPoliciesList]); const handleArtifactCardProps = useCallback( (trustedApp: TrustedApp) => { diff --git a/x-pack/plugins/security_solution/scripts/endpoint/host_isolation_exceptions/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/host_isolation_exceptions/index.ts index b9b70c4c1da195..15f0b2f65cb956 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/host_isolation_exceptions/index.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/host_isolation_exceptions/index.ts @@ -31,7 +31,7 @@ export const cli = () => { } }, { - description: 'Load Host Isolation Exceptions', + description: 'Load Host isolation exceptions', flags: { string: ['kibana'], default: { diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index d0bbb3b346ea89..2f31f54143f745 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -343,9 +343,10 @@ describe('ingest_integration tests ', () => { }); }); - it("doesn't remove policy from trusted app FF disabled", async () => { + it("doesn't remove policy from trusted app if feature flag is disabled", async () => { await invokeDeleteCallback({ ...allowedExperimentalValues, + trustedAppsByPolicyEnabled: false, // since it was changed to `true` by default }); expect(exceptionListClient.findExceptionListItem).toHaveBeenCalledTimes(0); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 200246ba1a3671..9d1cd3cbca3fba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -479,7 +479,6 @@ export const getRuleExecutionStatuses = (): Array< type: 'my-type', id: 'e0b86950-4e9f-11ea-bdbd-07b56aa159b3', attributes: { - alertId: '04128c15-0d1b-4716-a4c5-46997ac7f3bc', statusDate: '2020-02-18T15:26:49.783Z', status: RuleExecutionStatus.succeeded, lastFailureAt: undefined, @@ -492,7 +491,13 @@ export const getRuleExecutionStatuses = (): Array< bulkCreateTimeDurations: ['800.43'], }, score: 1, - references: [], + references: [ + { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bc', + type: 'alert', + name: 'alert_0', + }, + ], updated_at: '2020-02-18T15:26:51.333Z', version: 'WzQ2LDFd', }, @@ -500,7 +505,6 @@ export const getRuleExecutionStatuses = (): Array< type: 'my-type', id: '91246bd0-5261-11ea-9650-33b954270f67', attributes: { - alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', statusDate: '2020-02-18T15:15:58.806Z', status: RuleExecutionStatus.failed, lastFailureAt: '2020-02-18T15:15:58.806Z', @@ -514,7 +518,13 @@ export const getRuleExecutionStatuses = (): Array< bulkCreateTimeDurations: ['800.43'], }, score: 1, - references: [], + references: [ + { + id: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', + type: 'alert', + name: 'alert_0', + }, + ], updated_at: '2020-02-18T15:15:58.860Z', version: 'WzMyLDFd', }, @@ -523,7 +533,6 @@ export const getRuleExecutionStatuses = (): Array< export const getFindBulkResultStatus = (): FindBulkExecutionLogResponse => ({ '04128c15-0d1b-4716-a4c5-46997ac7f3bd': [ { - alertId: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', statusDate: '2020-02-18T15:26:49.783Z', status: RuleExecutionStatus.succeeded, lastFailureAt: undefined, @@ -538,7 +547,6 @@ export const getFindBulkResultStatus = (): FindBulkExecutionLogResponse => ({ ], '1ea5a820-4da1-4e82-92a1-2b43a7bece08': [ { - alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', statusDate: '2020-02-18T15:15:58.806Z', status: RuleExecutionStatus.failed, lastFailureAt: '2020-02-18T15:15:58.806Z', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 0048c735b0a7c7..fed34743e220aa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -31,7 +31,7 @@ import { updatePrepackagedRules } from '../../rules/update_prepacked_rules'; import { getRulesToInstall } from '../../rules/get_rules_to_install'; import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; -import { ruleAssetSavedObjectsClientFactory } from '../../rules/rule_asset_saved_objects_client'; +import { ruleAssetSavedObjectsClientFactory } from '../../rules/rule_asset/rule_asset_saved_objects_client'; import { buildSiemResponse } from '../utils'; import { RulesClient } from '../../../../../../alerting/server'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index 9a06928eee233f..a18507eea4977e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -20,7 +20,7 @@ import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { findRules } from '../../rules/find_rules'; import { getLatestPrepackagedRules } from '../../rules/get_prepackaged_rules'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; -import { ruleAssetSavedObjectsClientFactory } from '../../rules/rule_asset_saved_objects_client'; +import { ruleAssetSavedObjectsClientFactory } from '../../rules/rule_asset/rule_asset_saved_objects_client'; import { buildFrameworkRequest } from '../../../timeline/utils/common'; import { ConfigType } from '../../../../config'; import { SetupPlugins } from '../../../../plugin'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts index 10472fe1c0a039..6ddeeaa5ea1c2b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts @@ -136,16 +136,16 @@ describe.each([ describe('mergeStatuses', () => { it('merges statuses and converts from camelCase saved object to snake_case HTTP response', () => { + // const statusOne = exampleRuleStatus(); statusOne.attributes.status = RuleExecutionStatus.failed; const statusTwo = exampleRuleStatus(); statusTwo.attributes.status = RuleExecutionStatus.failed; const currentStatus = exampleRuleStatus(); const foundRules = [currentStatus.attributes, statusOne.attributes, statusTwo.attributes]; - const res = mergeStatuses(currentStatus.attributes.alertId, foundRules, { + const res = mergeStatuses(currentStatus.references[0].id, foundRules, { 'myfakealertid-8cfac': { current_status: { - alert_id: 'myfakealertid-8cfac', status_date: '2020-03-27T22:55:59.517Z', status: RuleExecutionStatus.succeeded, last_failure_at: null, @@ -163,7 +163,6 @@ describe.each([ expect(res).toEqual({ 'myfakealertid-8cfac': { current_status: { - alert_id: 'myfakealertid-8cfac', status_date: '2020-03-27T22:55:59.517Z', status: 'succeeded', last_failure_at: null, @@ -179,7 +178,6 @@ describe.each([ }, 'f4b8e31d-cf93-4bde-a265-298bde885cd7': { current_status: { - alert_id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7', status_date: '2020-03-27T22:55:59.517Z', status: 'succeeded', last_failure_at: null, @@ -193,7 +191,6 @@ describe.each([ }, failures: [ { - alert_id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7', status_date: '2020-03-27T22:55:59.517Z', status: 'failed', last_failure_at: null, @@ -206,7 +203,6 @@ describe.each([ last_look_back_date: null, // NOTE: This is no longer used on the UI, but left here in case users are using it within the API }, { - alert_id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7', status_date: '2020-03-27T22:55:59.517Z', status: 'failed', last_failure_at: null, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts index 086cc12788a40a..a3fb50f1f6b0b9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts @@ -42,7 +42,7 @@ export class EventLogAdapter implements IRuleExecutionLogClient { } public async update(args: UpdateExecutionLogArgs) { - const { attributes, spaceId, ruleName, ruleType } = args; + const { attributes, spaceId, ruleId, ruleName, ruleType } = args; await this.savedObjectsAdapter.update(args); @@ -51,7 +51,7 @@ export class EventLogAdapter implements IRuleExecutionLogClient { this.eventLogClient.logStatusChange({ ruleName, ruleType, - ruleId: attributes.alertId, + ruleId, newStatus: attributes.status, spaceId, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/rule_status_saved_objects_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/rule_status_saved_objects_client.ts index 720659b72194fc..66b646e96ea53b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/rule_status_saved_objects_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/rule_status_saved_objects_client.ts @@ -5,27 +5,33 @@ * 2.0. */ -import { get } from 'lodash'; import { - SavedObjectsClientContract, SavedObject, - SavedObjectsUpdateResponse, + SavedObjectsClientContract, + SavedObjectsCreateOptions, SavedObjectsFindOptions, + SavedObjectsFindOptionsReference, SavedObjectsFindResult, -} from '../../../../../../../../src/core/server'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; + SavedObjectsUpdateResponse, +} from 'kibana/server'; +import { get } from 'lodash'; +// eslint-disable-next-line no-restricted-imports +import { legacyRuleStatusSavedObjectType } from '../../rules/legacy_rule_status/legacy_rule_status_saved_object_mappings'; import { IRuleStatusSOAttributes } from '../../rules/types'; -import { buildChunkedOrFilter } from '../../signals/utils'; export interface RuleStatusSavedObjectsClient { find: ( options?: Omit ) => Promise>>; findBulk: (ids: string[], statusesPerId: number) => Promise; - create: (attributes: IRuleStatusSOAttributes) => Promise>; + create: ( + attributes: IRuleStatusSOAttributes, + options: SavedObjectsCreateOptions + ) => Promise>; update: ( id: string, - attributes: Partial + attributes: Partial, + options: SavedObjectsCreateOptions ) => Promise>; delete: (id: string) => Promise<{}>; } @@ -35,7 +41,7 @@ export interface FindBulkResponse { } /** - * @pdeprecated Use RuleExecutionLogClient instead + * @deprecated Use RuleExecutionLogClient instead */ export const ruleStatusSavedObjectsClientFactory = ( savedObjectsClient: SavedObjectsClientContract @@ -43,7 +49,7 @@ export const ruleStatusSavedObjectsClientFactory = ( find: async (options) => { const result = await savedObjectsClient.find({ ...options, - type: ruleStatusSavedObjectType, + type: legacyRuleStatusSavedObjectType, }); return result.saved_objects; }, @@ -51,47 +57,64 @@ export const ruleStatusSavedObjectsClientFactory = ( if (ids.length === 0) { return {}; } - const filter = buildChunkedOrFilter(`${ruleStatusSavedObjectType}.attributes.alertId`, ids); + const references = ids.map((alertId) => ({ + id: alertId, + type: 'alert', + })); const order: 'desc' = 'desc'; const aggs = { - alertIds: { - terms: { - field: `${ruleStatusSavedObjectType}.attributes.alertId`, - size: ids.length, + references: { + nested: { + path: `${legacyRuleStatusSavedObjectType}.references`, }, aggs: { - most_recent_statuses: { - top_hits: { - sort: [ - { - [`${ruleStatusSavedObjectType}.statusDate`]: { - order, + alertIds: { + terms: { + field: `${legacyRuleStatusSavedObjectType}.references.id`, + size: ids.length, + }, + aggs: { + rule_status: { + reverse_nested: {}, + aggs: { + most_recent_statuses: { + top_hits: { + sort: [ + { + [`${legacyRuleStatusSavedObjectType}.statusDate`]: { + order, + }, + }, + ], + size: statusesPerId, + }, }, }, - ], - size: statusesPerId, + }, }, }, }, }, }; const results = await savedObjectsClient.find({ - filter, + hasReference: references, aggs, - type: ruleStatusSavedObjectType, + type: legacyRuleStatusSavedObjectType, perPage: 0, }); - const buckets = get(results, 'aggregations.alertIds.buckets'); + const buckets = get(results, 'aggregations.references.alertIds.buckets'); return buckets.reduce((acc: Record, bucket: unknown) => { const key = get(bucket, 'key'); - const hits = get(bucket, 'most_recent_statuses.hits.hits'); + const hits = get(bucket, 'rule_status.most_recent_statuses.hits.hits'); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const statuses = hits.map((hit: any) => hit._source['siem-detection-engine-rule-status']); - acc[key] = statuses; + acc[key] = hits.map((hit: any) => hit._source[legacyRuleStatusSavedObjectType]); return acc; }, {}); }, - create: (attributes) => savedObjectsClient.create(ruleStatusSavedObjectType, attributes), - update: (id, attributes) => savedObjectsClient.update(ruleStatusSavedObjectType, id, attributes), - delete: (id) => savedObjectsClient.delete(ruleStatusSavedObjectType, id), + create: (attributes, options) => { + return savedObjectsClient.create(legacyRuleStatusSavedObjectType, attributes, options); + }, + update: (id, attributes, options) => + savedObjectsClient.update(legacyRuleStatusSavedObjectType, id, attributes, options), + delete: (id) => savedObjectsClient.delete(legacyRuleStatusSavedObjectType, id), }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts index ca806bd58e3690..9db7afce62ee4a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts @@ -5,9 +5,12 @@ * 2.0. */ -import { SavedObject } from 'src/core/server'; +import { SavedObject, SavedObjectReference } from 'src/core/server'; import { SavedObjectsClientContract } from '../../../../../../../../src/core/server'; import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; +// eslint-disable-next-line no-restricted-imports +import { legacyGetRuleReference } from '../../rules/legacy_rule_status/legacy_utils'; + import { IRuleStatusSOAttributes } from '../../rules/types'; import { RuleStatusSavedObjectsClient, @@ -51,7 +54,7 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient { sortField: 'statusDate', sortOrder: 'desc', search: ruleId, - searchFields: ['alertId'], + searchFields: ['references.id'], }); } @@ -59,8 +62,9 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient { return this.ruleStatusClient.findBulk(ruleIds, logsCount); } - public async update({ id, attributes }: UpdateExecutionLogArgs) { - await this.ruleStatusClient.update(id, attributes); + public async update({ id, attributes, ruleId }: UpdateExecutionLogArgs) { + const references: SavedObjectReference[] = [legacyGetRuleReference(ruleId)]; + await this.ruleStatusClient.update(id, attributes, { references }); } public async delete(id: string) { @@ -68,31 +72,39 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient { } public async logExecutionMetrics({ ruleId, metrics }: LogExecutionMetricsArgs) { + const references: SavedObjectReference[] = [legacyGetRuleReference(ruleId)]; const [currentStatus] = await this.getOrCreateRuleStatuses(ruleId); - await this.ruleStatusClient.update(currentStatus.id, { - ...currentStatus.attributes, - ...convertMetricFields(metrics), - }); + await this.ruleStatusClient.update( + currentStatus.id, + { + ...currentStatus.attributes, + ...convertMetricFields(metrics), + }, + { references } + ); } private createNewRuleStatus = async ( ruleId: string ): Promise> => { + const references: SavedObjectReference[] = [legacyGetRuleReference(ruleId)]; const now = new Date().toISOString(); - return this.ruleStatusClient.create({ - alertId: ruleId, - statusDate: now, - status: RuleExecutionStatus['going to run'], - lastFailureAt: null, - lastSuccessAt: null, - lastFailureMessage: null, - lastSuccessMessage: null, - gap: null, - bulkCreateTimeDurations: [], - searchAfterTimeDurations: [], - lastLookBackDate: null, - }); + return this.ruleStatusClient.create( + { + statusDate: now, + status: RuleExecutionStatus['going to run'], + lastFailureAt: null, + lastSuccessAt: null, + lastFailureMessage: null, + lastSuccessMessage: null, + gap: null, + bulkCreateTimeDurations: [], + searchAfterTimeDurations: [], + lastLookBackDate: null, + }, + { references } + ); }; private getOrCreateRuleStatuses = async ( @@ -112,6 +124,8 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient { }; public async logStatusChange({ newStatus, ruleId, message, metrics }: LogStatusChangeArgs) { + const references: SavedObjectReference[] = [legacyGetRuleReference(ruleId)]; + switch (newStatus) { case RuleExecutionStatus['going to run']: case RuleExecutionStatus.succeeded: @@ -119,10 +133,14 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient { case RuleExecutionStatus['partial failure']: { const [currentStatus] = await this.getOrCreateRuleStatuses(ruleId); - await this.ruleStatusClient.update(currentStatus.id, { - ...currentStatus.attributes, - ...buildRuleStatusAttributes(newStatus, message, metrics), - }); + await this.ruleStatusClient.update( + currentStatus.id, + { + ...currentStatus.attributes, + ...buildRuleStatusAttributes(newStatus, message, metrics), + }, + { references } + ); return; } @@ -137,8 +155,8 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient { }; // We always update the newest status, so to 'persist' a failure we push a copy to the head of the list - await this.ruleStatusClient.update(currentStatus.id, failureAttributes); - const lastStatus = await this.ruleStatusClient.create(failureAttributes); + await this.ruleStatusClient.update(currentStatus.id, failureAttributes, { references }); + const lastStatus = await this.ruleStatusClient.create(failureAttributes, { references }); // drop oldest failures const oldStatuses = [lastStatus, ...ruleStatuses].slice(MAX_RULE_STATUSES); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts index e38f974ddee2e6..564145cfc5d1f8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts @@ -53,6 +53,7 @@ export interface LogStatusChangeArgs { export interface UpdateExecutionLogArgs { id: string; attributes: IRuleStatusSOAttributes; + ruleId: string; ruleName: string; ruleType: string; spaceId: string; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts index f8e1f873377a98..2d82cd7f8732af 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts @@ -26,7 +26,6 @@ describe('deleteRules', () => { type: '', references: [], attributes: { - alertId: 'alertId', statusDate: '', lastFailureAt: null, lastFailureMessage: null, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts index 2f3d05e0c95860..b75a1b0d80e9a0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts @@ -44,6 +44,7 @@ export const enableRule = async ({ const currentStatusToDisable = ruleCurrentStatus[0]; await ruleStatusClient.update({ id: currentStatusToDisable.id, + ruleId: rule.id, ruleName: rule.name, ruleType: rule.alertTypeId, attributes: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts index 6fe326a8d85a32..8116a42f428273 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts @@ -18,7 +18,7 @@ import { // TODO: convert rules files to TS and add explicit type definitions import { rawRules } from './prepackaged_rules'; -import { RuleAssetSavedObjectsClient } from './rule_asset_saved_objects_client'; +import { RuleAssetSavedObjectsClient } from './rule_asset/rule_asset_saved_objects_client'; import { IRuleAssetSOAttributes } from './types'; import { SavedObjectAttributes } from '../../../../../../../src/core/types'; import { ConfigType } from '../../../config'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts new file mode 100644 index 00000000000000..92d7487be0cdb6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + SavedObjectMigrationFn, + SavedObjectReference, + SavedObjectSanitizedDoc, + SavedObjectUnsanitizedDoc, +} from 'kibana/server'; +import { isString } from 'lodash/fp'; +import { truncateMessage } from '../../rule_execution_log'; +import { IRuleSavedAttributesSavedObjectAttributes } from '../types'; +// eslint-disable-next-line no-restricted-imports +import { legacyGetRuleReference } from './legacy_utils'; + +export const truncateMessageFields: SavedObjectMigrationFn> = (doc) => { + const { lastFailureMessage, lastSuccessMessage, ...restAttributes } = doc.attributes; + + return { + ...doc, + attributes: { + lastFailureMessage: truncateMessage(lastFailureMessage), + lastSuccessMessage: truncateMessage(lastSuccessMessage), + ...restAttributes, + }, + references: doc.references ?? [], + }; +}; + +/** + * This side-car rule status SO is deprecated and is to be replaced by the RuleExecutionLog on Event-Log and + * additional fields on the Alerting Framework Rule SO. + * + * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) + */ +export const legacyRuleStatusSavedObjectMigration = { + '7.15.2': truncateMessageFields, + '7.16.0': ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + return legacyMigrateRuleAlertIdSOReferences(doc); + }, +}; + +/** + * This migrates alertId within legacy `siem-detection-engine-rule-status` to saved object references on an upgrade. + * We only migrate alertId if we find these conditions: + * - alertId is a string and not null, undefined, or malformed data. + * - The existing references do not already have a alertId found within it. + * + * Some of these issues could crop up during either user manual errors of modifying things, earlier migration + * issues, etc... so we are safer to check them as possibilities + * + * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) + * @param doc The document having an alertId to migrate into references + * @returns The document migrated with saved object references + */ +export const legacyMigrateRuleAlertIdSOReferences = ( + doc: SavedObjectUnsanitizedDoc +): SavedObjectSanitizedDoc => { + const { references } = doc; + + // Isolate alertId from the doc + const { alertId, ...attributesWithoutAlertId } = doc.attributes; + const existingReferences = references ?? []; + + if (!isString(alertId)) { + // early return if alertId is not a string as expected + return { ...doc, references: existingReferences }; + } else { + const alertReferences = legacyMigrateAlertId({ + alertId, + existingReferences, + }); + + return { + ...doc, + attributes: { + ...attributesWithoutAlertId.attributes, + }, + references: [...existingReferences, ...alertReferences], + }; + } +}; + +/** + * This is a helper to migrate "alertId" + * + * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) + * + * @param existingReferences The existing saved object references + * @param alertId The alertId to migrate + * + * @returns The savedObjectReferences migrated + */ +export const legacyMigrateAlertId = ({ + existingReferences, + alertId, +}: { + existingReferences: SavedObjectReference[]; + alertId: string; +}): SavedObjectReference[] => { + const existingReferenceFound = existingReferences.find((reference) => { + return reference.id === alertId && reference.type === 'alert'; + }); + if (existingReferenceFound) { + return []; + } else { + return [legacyGetRuleReference(alertId)]; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts new file mode 100644 index 00000000000000..3fe3fc06cc7d6f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts @@ -0,0 +1,73 @@ +/* + * 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 { SavedObjectsType } from 'kibana/server'; +// eslint-disable-next-line no-restricted-imports +import { legacyRuleStatusSavedObjectMigration } from './legacy_migrations'; + +/** + * This side-car rule status SO is deprecated and is to be replaced by the RuleExecutionLog on Event-Log and + * additional fields on the Alerting Framework Rule SO. + * + * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) + */ +export const legacyRuleStatusSavedObjectType = 'siem-detection-engine-rule-status'; + +/** + * This side-car rule status SO is deprecated and is to be replaced by the RuleExecutionLog on Event-Log and + * additional fields on the Alerting Framework Rule SO. + * + * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) + */ +export const ruleStatusSavedObjectMappings: SavedObjectsType['mappings'] = { + properties: { + status: { + type: 'keyword', + }, + statusDate: { + type: 'date', + }, + lastFailureAt: { + type: 'date', + }, + lastSuccessAt: { + type: 'date', + }, + lastFailureMessage: { + type: 'text', + }, + lastSuccessMessage: { + type: 'text', + }, + lastLookBackDate: { + type: 'date', + }, + gap: { + type: 'text', + }, + bulkCreateTimeDurations: { + type: 'float', + }, + searchAfterTimeDurations: { + type: 'float', + }, + }, +}; + +/** + * This side-car rule status SO is deprecated and is to be replaced by the RuleExecutionLog on Event-Log and + * additional fields on the Alerting Framework Rule SO. + * + * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) + */ +export const legacyRuleStatusType: SavedObjectsType = { + name: legacyRuleStatusSavedObjectType, + hidden: false, + namespaceType: 'single', + mappings: ruleStatusSavedObjectMappings, + migrations: legacyRuleStatusSavedObjectMigration, +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_utils.ts new file mode 100644 index 00000000000000..62de5ce5912303 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_utils.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. + */ + +/** + * Given an id this returns a legacy rule reference. + * @param id The id of the alert + * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) + */ +export const legacyGetRuleReference = (id: string) => ({ + id, + type: 'alert', + name: 'alert_0', +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/rule_asset/rule_asset_saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/rule_asset/rule_asset_saved_object_mappings.ts new file mode 100644 index 00000000000000..e2941b503664b6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/rule_asset/rule_asset_saved_object_mappings.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsType } from '../../../../../../../../src/core/server'; + +export const ruleAssetSavedObjectType = 'security-rule'; + +export const ruleAssetSavedObjectMappings: SavedObjectsType['mappings'] = { + dynamic: false, + properties: { + name: { + type: 'keyword', + }, + rule_id: { + type: 'keyword', + }, + version: { + type: 'long', + }, + }, +}; + +export const ruleAssetType: SavedObjectsType = { + name: ruleAssetSavedObjectType, + hidden: false, + namespaceType: 'agnostic', + mappings: ruleAssetSavedObjectMappings, +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/rule_asset_saved_objects_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/rule_asset/rule_asset_saved_objects_client.ts similarity index 88% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/rule_asset_saved_objects_client.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/rule_asset/rule_asset_saved_objects_client.ts index ac0969dfc975d0..c594385dce22b1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/rule_asset_saved_objects_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/rule_asset/rule_asset_saved_objects_client.ts @@ -9,9 +9,9 @@ import { SavedObjectsClientContract, SavedObjectsFindOptions, SavedObjectsFindResponse, -} from '../../../../../../../src/core/server'; -import { ruleAssetSavedObjectType } from '../rules/saved_object_mappings'; -import { IRuleAssetSavedObject } from '../rules/types'; +} from 'kibana/server'; +import { ruleAssetSavedObjectType } from './rule_asset_saved_object_mappings'; +import { IRuleAssetSavedObject } from '../types'; const DEFAULT_PAGE_SIZE = 100; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/saved_object_mappings.ts deleted file mode 100644 index d347fccf6b77b9..00000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/saved_object_mappings.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SavedObjectsType, SavedObjectMigrationFn } from 'kibana/server'; -import { truncateMessage } from '../rule_execution_log'; - -export const ruleStatusSavedObjectType = 'siem-detection-engine-rule-status'; - -export const ruleStatusSavedObjectMappings: SavedObjectsType['mappings'] = { - properties: { - alertId: { - type: 'keyword', - }, - status: { - type: 'keyword', - }, - statusDate: { - type: 'date', - }, - lastFailureAt: { - type: 'date', - }, - lastSuccessAt: { - type: 'date', - }, - lastFailureMessage: { - type: 'text', - }, - lastSuccessMessage: { - type: 'text', - }, - lastLookBackDate: { - type: 'date', - }, - gap: { - type: 'text', - }, - bulkCreateTimeDurations: { - type: 'float', - }, - searchAfterTimeDurations: { - type: 'float', - }, - }, -}; - -const truncateMessageFields: SavedObjectMigrationFn> = (doc) => { - const { lastFailureMessage, lastSuccessMessage, ...restAttributes } = doc.attributes; - - return { - ...doc, - attributes: { - lastFailureMessage: truncateMessage(lastFailureMessage), - lastSuccessMessage: truncateMessage(lastSuccessMessage), - ...restAttributes, - }, - references: doc.references ?? [], - }; -}; - -export const type: SavedObjectsType = { - name: ruleStatusSavedObjectType, - hidden: false, - namespaceType: 'single', - mappings: ruleStatusSavedObjectMappings, - migrations: { - '7.15.2': truncateMessageFields, - }, -}; - -export const ruleAssetSavedObjectType = 'security-rule'; - -export const ruleAssetSavedObjectMappings: SavedObjectsType['mappings'] = { - dynamic: false, - properties: { - name: { - type: 'keyword', - }, - rule_id: { - type: 'keyword', - }, - version: { - type: 'long', - }, - }, -}; - -export const ruleAssetType: SavedObjectsType = { - name: ruleAssetSavedObjectType, - hidden: false, - namespaceType: 'agnostic', - mappings: ruleAssetSavedObjectMappings, -}; 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 8adf19a53f92bb..53a83d61da78db 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 @@ -111,7 +111,6 @@ export type RuleAlertType = SanitizedAlert; // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface IRuleStatusSOAttributes extends Record { - alertId: string; // created alert id. statusDate: StatusDate; lastFailureAt: LastFailureAt | null | undefined; lastFailureMessage: LastFailureMessage | null | undefined; @@ -125,7 +124,6 @@ export interface IRuleStatusSOAttributes extends Record { } export interface IRuleStatusResponseAttributes { - alert_id: string; // created alert id. status_date: StatusDate; last_failure_at: LastFailureAt | null | undefined; last_failure_message: LastFailureMessage | null | undefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 207ea497c7e8ef..078d36a99ad176 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -18,7 +18,8 @@ import type { import { SavedObject } from '../../../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks'; import { IRuleStatusSOAttributes } from '../../rules/types'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +// eslint-disable-next-line no-restricted-imports +import { legacyRuleStatusSavedObjectType } from '../../rules/legacy_rule_status/legacy_rule_status_saved_object_mappings'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; import { RuleParams } from '../../schemas/rule_schemas'; @@ -725,10 +726,9 @@ export const sampleRuleGuid = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; export const sampleIdGuid = 'e1e08ddc-5e37-49ff-a258-5393aa44435a'; export const exampleRuleStatus: () => SavedObject = () => ({ - type: ruleStatusSavedObjectType, + type: legacyRuleStatusSavedObjectType, id: '042e6d90-7069-11ea-af8b-0f8ae4fa817e', attributes: { - alertId: 'f4b8e31d-cf93-4bde-a265-298bde885cd7', statusDate: '2020-03-27T22:55:59.517Z', status: RuleExecutionStatus.succeeded, lastFailureAt: null, @@ -740,7 +740,13 @@ export const exampleRuleStatus: () => SavedObject = () searchAfterTimeDurations: [], lastLookBackDate: null, }, - references: [], + references: [ + { + id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7', + type: 'alert', + name: 'alert_0', + }, + ], updated_at: '2020-03-27T22:55:59.577Z', version: 'WzgyMiwxXQ==', }); diff --git a/x-pack/plugins/security_solution/server/saved_objects.ts b/x-pack/plugins/security_solution/server/saved_objects.ts index 1523b3ddf4cbfe..53618d738984be 100644 --- a/x-pack/plugins/security_solution/server/saved_objects.ts +++ b/x-pack/plugins/security_solution/server/saved_objects.ts @@ -8,10 +8,9 @@ import { CoreSetup } from '../../../../src/core/server'; import { noteType, pinnedEventType, timelineType } from './lib/timeline/saved_object_mappings'; -import { - type as ruleStatusType, - ruleAssetType, -} from './lib/detection_engine/rules/saved_object_mappings'; +// eslint-disable-next-line no-restricted-imports +import { legacyRuleStatusType } from './lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings'; +import { ruleAssetType } from './lib/detection_engine/rules/rule_asset/rule_asset_saved_object_mappings'; // eslint-disable-next-line no-restricted-imports import { legacyType as legacyRuleActionsType } from './lib/detection_engine/rule_actions/legacy_saved_object_mappings'; import { type as signalsMigrationType } from './lib/detection_engine/migrations/saved_objects'; @@ -24,7 +23,7 @@ const types = [ noteType, pinnedEventType, legacyRuleActionsType, - ruleStatusType, + legacyRuleStatusType, ruleAssetType, timelineType, exceptionsArtifactType, diff --git a/x-pack/plugins/spaces/server/config.test.ts b/x-pack/plugins/spaces/server/config.test.ts deleted file mode 100644 index e8c8b02ef93c29..00000000000000 --- a/x-pack/plugins/spaces/server/config.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { applyDeprecations, configDeprecationFactory } from '@kbn/config'; -import { deepFreeze } from '@kbn/std'; - -import { configDeprecationsMock } from '../../../../src/core/server/mocks'; -import { spacesConfigDeprecationProvider } from './config'; - -const deprecationContext = configDeprecationsMock.createContext(); - -const applyConfigDeprecations = (settings: Record = {}) => { - const deprecations = spacesConfigDeprecationProvider(configDeprecationFactory); - const deprecationMessages: string[] = []; - const { config: migrated } = applyDeprecations( - settings, - deprecations.map((deprecation) => ({ - deprecation, - path: '', - context: deprecationContext, - })), - () => - ({ message }) => - deprecationMessages.push(message) - ); - return { - messages: deprecationMessages, - migrated, - }; -}; - -describe('spaces config', () => { - describe('deprecations', () => { - describe('enabled', () => { - it('logs a warning if xpack.spaces.enabled is set to false', () => { - const originalConfig = deepFreeze({ xpack: { spaces: { enabled: false } } }); - - const { messages, migrated } = applyConfigDeprecations({ ...originalConfig }); - - expect(messages).toMatchInlineSnapshot(` - Array [ - "Disabling the Spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)", - ] - `); - expect(migrated).toEqual(originalConfig); - }); - - it('does not log a warning if no settings are explicitly set', () => { - const originalConfig = deepFreeze({}); - - const { messages, migrated } = applyConfigDeprecations({ ...originalConfig }); - - expect(messages).toMatchInlineSnapshot(`Array []`); - expect(migrated).toEqual(originalConfig); - }); - - it('does not log a warning if xpack.spaces.enabled is set to true', () => { - const originalConfig = deepFreeze({ xpack: { spaces: { enabled: true } } }); - - const { messages, migrated } = applyConfigDeprecations({ ...originalConfig }); - - expect(messages).toMatchInlineSnapshot(`Array []`); - expect(migrated).toEqual(originalConfig); - }); - }); - }); -}); diff --git a/x-pack/plugins/spaces/server/config.ts b/x-pack/plugins/spaces/server/config.ts index 29f8e0cdf692f6..7b4c85e3edcdc2 100644 --- a/x-pack/plugins/spaces/server/config.ts +++ b/x-pack/plugins/spaces/server/config.ts @@ -9,37 +9,15 @@ import type { Observable } from 'rxjs'; import type { TypeOf } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema'; -import type { - ConfigDeprecation, - ConfigDeprecationProvider, - PluginInitializerContext, -} from 'src/core/server'; +import type { PluginInitializerContext } from 'src/core/server'; export const ConfigSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), maxSpaces: schema.number({ defaultValue: 1000 }), }); export function createConfig$(context: PluginInitializerContext) { return context.config.create>(); } - -const disabledDeprecation: ConfigDeprecation = (config, fromPath, addDeprecation) => { - if (config.xpack?.spaces?.enabled === false) { - addDeprecation({ - configPath: 'xpack.spaces.enabled', - message: `Disabling the Spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)`, - correctiveActions: { - manualSteps: [`Remove "xpack.spaces.enabled: false" from your Kibana configuration`], - }, - }); - } -}; - -export const spacesConfigDeprecationProvider: ConfigDeprecationProvider = () => { - return [disabledDeprecation]; -}; - export type ConfigType = ReturnType extends Observable ? P : ReturnType; diff --git a/x-pack/plugins/spaces/server/index.ts b/x-pack/plugins/spaces/server/index.ts index 31714df958d49c..ad27069759198b 100644 --- a/x-pack/plugins/spaces/server/index.ts +++ b/x-pack/plugins/spaces/server/index.ts @@ -7,7 +7,7 @@ import type { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/server'; -import { ConfigSchema, spacesConfigDeprecationProvider } from './config'; +import { ConfigSchema } from './config'; import { SpacesPlugin } from './plugin'; // These exports are part of public Spaces plugin contract, any change in signature of exported @@ -33,7 +33,6 @@ export type { export const config: PluginConfigDescriptor = { schema: ConfigSchema, - deprecations: spacesConfigDeprecationProvider, }; export const plugin = (initializerContext: PluginInitializerContext) => new SpacesPlugin(initializerContext); diff --git a/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts b/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts index ae9fc254c09347..e594306f5ee3aa 100644 --- a/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts @@ -16,7 +16,7 @@ const createMockDebugLogger = () => { return jest.fn(); }; -const createMockConfig = (mockConfig: ConfigType = { maxSpaces: 1000, enabled: true }) => { +const createMockConfig = (mockConfig: ConfigType = { maxSpaces: 1000 }) => { return ConfigSchema.validate(mockConfig); }; @@ -75,10 +75,7 @@ describe('#getAll', () => { mockCallWithRequestRepository.find.mockResolvedValue({ saved_objects: savedObjects, } as any); - const mockConfig = createMockConfig({ - maxSpaces: 1234, - enabled: true, - }); + const mockConfig = createMockConfig({ maxSpaces: 1234 }); const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository); const actualSpaces = await client.getAll(); @@ -182,10 +179,7 @@ describe('#create', () => { total: maxSpaces - 1, } as any); - const mockConfig = createMockConfig({ - maxSpaces, - enabled: true, - }); + const mockConfig = createMockConfig({ maxSpaces }); const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository); @@ -211,10 +205,7 @@ describe('#create', () => { total: maxSpaces, } as any); - const mockConfig = createMockConfig({ - maxSpaces, - enabled: true, - }); + const mockConfig = createMockConfig({ maxSpaces }); const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0445d9de0634eb..79d092cebe366f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4162,7 +4162,6 @@ "inspector.requests.noRequestsLoggedTitle": "リクエストが記録されていません", "inspector.requests.requestFailedTooltipTitle": "リクエストに失敗しました", "inspector.requests.requestInProgressAriaLabel": "リクエストが進行中", - "inspector.requests.requestLabel": "リクエスト:", "inspector.requests.requestsDescriptionTooltip": "データを収集したリクエストを表示します", "inspector.requests.requestsTitle": "リクエスト", "inspector.requests.requestSucceededTooltipTitle": "リクエスト成功", @@ -6239,7 +6238,6 @@ "xpack.apm.clearFilters": "フィルターを消去", "xpack.apm.compositeSpanCallsLabel": "、{count}件の呼び出し、平均{duration}", "xpack.apm.compositeSpanDurationLabel": "平均時間", - "xpack.apm.correlations.correlationsTable.actionsLabel": "フィルター", "xpack.apm.correlations.correlationsTable.excludeDescription": "値を除外", "xpack.apm.correlations.correlationsTable.excludeLabel": "除外", "xpack.apm.correlations.correlationsTable.filterDescription": "値でフィルタリング", @@ -10860,13 +10858,10 @@ "xpack.fleet.createPackagePolicy.cancelButton": "キャンセル", "xpack.fleet.createPackagePolicy.cancelLinkText": "キャンセル", "xpack.fleet.createPackagePolicy.errorOnSaveText": "統合ポリシーにはエラーがあります。保存前に修正してください。", - "xpack.fleet.createPackagePolicy.integrationsContextAddAgentLinkMessage": "エージェントを追加", - "xpack.fleet.createPackagePolicy.integrationsContextaddAgentNextNotificationMessage": "次に、{link}して、データの取り込みを開始します。", "xpack.fleet.createPackagePolicy.pageDescriptionfromPackage": "次の手順に従い、この統合をエージェントポリシーに追加します。", "xpack.fleet.createPackagePolicy.pageDescriptionfromPolicy": "選択したエージェントポリシーの統合を構成します。", "xpack.fleet.createPackagePolicy.pageTitle": "統合の追加", "xpack.fleet.createPackagePolicy.pageTitleWithPackageName": "{packageName}統合の追加", - "xpack.fleet.createPackagePolicy.policyContextAddAgentNextNotificationMessage": "ポリシーが更新されました。エージェントを'{agentPolicyName}'ポリシーに追加して、このポリシーをデプロイします。", "xpack.fleet.createPackagePolicy.saveButton": "統合の保存", "xpack.fleet.createPackagePolicy.stepConfigure.advancedOptionsToggleLinkText": "高度なオプション", "xpack.fleet.createPackagePolicy.stepConfigure.hideStreamsAriaLabel": "{type}入力を非表示", @@ -17944,8 +17939,7 @@ "xpack.monitoring.cluster.overview.logstashPanel.withPersistentQueuesLabel": "永続キューあり", "xpack.monitoring.cluster.overview.pageTitle": "クラスターの概要", "xpack.monitoring.cluster.overviewTitle": "概要", - "xpack.monitoring.clusterAlertsNavigation.clusterAlertsLinkText": "クラスターアラート", - "xpack.monitoring.clustersNavigation.clustersLinkText": "クラスター", + "xpack.monitoring.cluster.listing.tabTitle": "クラスター", "xpack.monitoring.clusterStats.uuidNotFoundErrorMessage": "選択された時間範囲にクラスターが見つかりませんでした。UUID:{clusterUuid}", "xpack.monitoring.clusterStats.uuidNotSpecifiedErrorMessage": "{clusterUuid} が指定されていません", "xpack.monitoring.elasticsearch.ccr.ccrListingTable.alertsColumnTitle": "アラート", @@ -17957,10 +17951,10 @@ "xpack.monitoring.elasticsearch.ccr.ccrListingTable.syncLagOpsColumnTitle": "同期の遅延(オペレーション数)", "xpack.monitoring.elasticsearch.ccr.heading": "CCR", "xpack.monitoring.elasticsearch.ccr.pageTitle": "Elasticsearch - CCR", - "xpack.monitoring.elasticsearch.ccr.routeTitle": "Elasticsearch - CCR", + "xpack.monitoring.elasticsearch.ccr.title": "Elasticsearch - CCR", "xpack.monitoring.elasticsearch.ccr.shard.instanceTitle": "インデックス{followerIndex} シャード:{shardId}", "xpack.monitoring.elasticsearch.ccr.shard.pageTitle": "Elasticsearch Ccrシャード - インデックス:{followerIndex} シャード:{shardId}", - "xpack.monitoring.elasticsearch.ccr.shard.routeTitle": "Elasticsearch - CCR - シャード", + "xpack.monitoring.elasticsearch.ccr.shard.title": "Elasticsearch - CCR - シャード", "xpack.monitoring.elasticsearch.ccr.shardsTable.alertsColumnTitle": "アラート", "xpack.monitoring.elasticsearch.ccr.shardsTable.errorColumnTitle": "エラー", "xpack.monitoring.elasticsearch.ccr.shardsTable.lastFetchTimeColumnTitle": "最終取得時刻", @@ -17994,7 +17988,7 @@ "xpack.monitoring.elasticsearch.indexDetailStatus.totalShardsTitle": "合計シャード数", "xpack.monitoring.elasticsearch.indexDetailStatus.totalTitle": "合計", "xpack.monitoring.elasticsearch.indexDetailStatus.unassignedShardsTitle": "未割り当てシャード", - "xpack.monitoring.elasticsearch.indices.advanced.routeTitle": "Elasticsearch - インデックス - {indexName} - 高度な設定", + "xpack.monitoring.elasticsearch.index.advanced.title": "Elasticsearch - インデックス - {indexName} - 高度な設定", "xpack.monitoring.elasticsearch.indices.alertsColumnTitle": "アラート", "xpack.monitoring.elasticsearch.indices.dataTitle": "データ", "xpack.monitoring.elasticsearch.indices.documentCountTitle": "ドキュメントカウント", @@ -18003,8 +17997,8 @@ "xpack.monitoring.elasticsearch.indices.monitoringTablePlaceholder": "インデックスのフィルタリング…", "xpack.monitoring.elasticsearch.indices.nameTitle": "名前", "xpack.monitoring.elasticsearch.indices.noIndicesMatchYourSelectionDescription": "選択項目に一致するインデックスがありません。時間範囲を変更してみてください。", - "xpack.monitoring.elasticsearch.indices.overview.pageTitle": "インデックス:{indexName}", - "xpack.monitoring.elasticsearch.indices.overview.routeTitle": "Elasticsearch - インデックス - {indexName} - 概要", + "xpack.monitoring.elasticsearch.index.overview.pageTitle": "インデックス:{indexName}", + "xpack.monitoring.elasticsearch.index.overview.title": "Elasticsearch - インデックス - {indexName} - 概要", "xpack.monitoring.elasticsearch.indices.pageTitle": "デフォルトのインデックス", "xpack.monitoring.elasticsearch.indices.routeTitle": "Elasticsearch - インデックス", "xpack.monitoring.elasticsearch.indices.searchRateTitle": "検索レート", @@ -18023,7 +18017,7 @@ "xpack.monitoring.elasticsearch.mlJobListing.statusIconLabel": "ジョブ状態:{status}", "xpack.monitoring.elasticsearch.mlJobs.pageTitle": "Elasticsearch - 機械学習ジョブ", "xpack.monitoring.elasticsearch.mlJobs.routeTitle": "Elasticsearch - 機械学習ジョブ", - "xpack.monitoring.elasticsearch.node.advanced.routeTitle": "Elasticsearch - ノード - {nodeSummaryName} - 高度な設定", + "xpack.monitoring.elasticsearch.node.advanced.title": "Elasticsearch - ノード - {nodeName} - 高度な設定", "xpack.monitoring.elasticsearch.node.cells.tooltip.iconLabel": "このメトリックの詳細", "xpack.monitoring.elasticsearch.node.cells.tooltip.max": "最高値", "xpack.monitoring.elasticsearch.node.cells.tooltip.min": "最低値", @@ -18032,7 +18026,7 @@ "xpack.monitoring.elasticsearch.node.cells.trendingDownText": "ダウン", "xpack.monitoring.elasticsearch.node.cells.trendingUpText": "アップ", "xpack.monitoring.elasticsearch.node.overview.pageTitle": "Elasticsearchノード:{node}", - "xpack.monitoring.elasticsearch.node.overview.routeTitle": "Elasticsearch - ノード - {nodeName} - 概要", + "xpack.monitoring.elasticsearch.node.overview.title": "Elasticsearch - ノード - {nodeName} - 概要", "xpack.monitoring.elasticsearch.node.statusIconLabel": "ステータス:{status}", "xpack.monitoring.elasticsearch.nodeDetailStatus.alerts": "アラート", "xpack.monitoring.elasticsearch.nodeDetailStatus.dataLabel": "データ", @@ -18121,8 +18115,7 @@ "xpack.monitoring.es.nodeType.nodeLabel": "ノード", "xpack.monitoring.esNavigation.ccrLinkText": "CCR", "xpack.monitoring.esNavigation.indicesLinkText": "インデックス", - "xpack.monitoring.esNavigation.instance.advancedLinkText": "高度な設定", - "xpack.monitoring.esNavigation.instance.overviewLinkText": "概要", + "xpack.monitoring.esItemNavigation.advancedLinkText": "高度な設定", "xpack.monitoring.esNavigation.jobsLinkText": "機械学習ジョブ", "xpack.monitoring.esNavigation.nodesLinkText": "ノード", "xpack.monitoring.esNavigation.overviewLinkText": "概要", @@ -18241,7 +18234,6 @@ "xpack.monitoring.logstash.node.advanced.pageTitle": "Logstashノード:{nodeName}", "xpack.monitoring.logstash.node.advanced.routeTitle": "Logstash - {nodeName} - 高度な設定", "xpack.monitoring.logstash.node.pageTitle": "Logstashノード:{nodeName}", - "xpack.monitoring.logstash.node.pipelines.notAvailableDescription": "パイプラインの監視は Logstash バージョン 6.0.0 以降でのみ利用できます。このノードはバージョン {logstashVersion} を実行しています。", "xpack.monitoring.logstash.node.pipelines.pageTitle": "Logstashノードパイプライン:{nodeName}", "xpack.monitoring.logstash.node.pipelines.routeTitle": "Logstash - {nodeName} - パイプライン", "xpack.monitoring.logstash.node.routeTitle": "Logstash - {nodeName}", @@ -20002,23 +19994,7 @@ "xpack.security.management.editRole.roleNameFormRowTitle": "ロール名", "xpack.security.management.editRole.roleSuccessfullyDeletedNotificationMessage": "ロールを削除しました", "xpack.security.management.editRole.roleSuccessfullySavedNotificationMessage": "ロールを保存しました", - "xpack.security.management.editRole.setPrivilegesToKibanaDescription": "Elasticsearch データの権限を設定し、Kibana へのアクセスを管理します。", "xpack.security.management.editRole.setPrivilegesToKibanaSpacesDescription": "Elasticsearch データの権限を設定し、Kibana スペースへのアクセスを管理します。", - "xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeDropdown": "すべて", - "xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeDropdownDescription": "Kibana 全体への完全アクセスを許可します", - "xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeInput": "すべて", - "xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeDropdown": "カスタム", - "xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeDropdownDescription": "Kibana へのアクセスをカスタマイズします", - "xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeInput": "カスタム", - "xpack.security.management.editRole.simplePrivilegeForm.kibanaPrivilegesTitle": "Kibanaの権限", - "xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeDropdown": "なし", - "xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeDropdownDescription": "Kibana へのアクセス不可", - "xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeInput": "なし", - "xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeDropdown": "読み取り", - "xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeDropdownDescription": "Kibana 全体への読み込み専用アクセスを許可します", - "xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeInput": "読み取り", - "xpack.security.management.editRole.simplePrivilegeForm.specifyPrivilegeForRoleDescription": "このロールの Kibana の権限を指定します。", - "xpack.security.management.editRole.simplePrivilegeForm.unsupportedSpacePrivilegesWarning": "このロールはスペースへの権限が定義されていますが、Kibana でスペースが有効ではありません。このロールを保存するとこれらの権限が削除されます。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "{kibanaAdmin}ロールによりアカウントにすべての権限が提供されていることを確認し、再試行してください。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.globalSpacesName": "*すべてのスペース", "xpack.security.management.editRole.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "利用可能なすべてのスペースを表示する権限がありません。", @@ -24915,11 +24891,8 @@ "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.categoryTitle": "カテゴリー", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.commentsTextAreaFieldLabel": "追加のコメント", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.descriptionTextAreaFieldLabel": "説明", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.destinationIPTitle": "デスティネーション IP", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.impactSelectFieldLabel": "インパクト", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.invalidApiUrlTextField": "URL が無効です。", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.malwareHashTitle": "マルウェアハッシュ", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.malwareURLTitle": "マルウェアURL", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.passwordTextFieldLabel": "パスワード", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.prioritySelectFieldLabel": "優先度", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.reenterValuesLabel": "ユーザー名とパスワードは暗号化されます。これらのフィールドの値を再入力してください。", @@ -24929,7 +24902,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUsernameTextField": "ユーザー名が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requireHttpsApiUrlTextField": "URL は https:// から始める必要があります。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectFieldLabel": "深刻度", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.sourceIPTitle": "ソース IP", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.subcategoryTitle": "サブカテゴリー", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.title": "インシデント", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.titleFieldLabel": "短い説明(必須)", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 210392d11514e8..c80cd968f38a89 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4201,7 +4201,6 @@ "inspector.requests.noRequestsLoggedTitle": "未记录任何请求", "inspector.requests.requestFailedTooltipTitle": "请求失败", "inspector.requests.requestInProgressAriaLabel": "进行中的请求", - "inspector.requests.requestLabel": "请求:", "inspector.requests.requestsDescriptionTooltip": "查看已收集数据的请求", "inspector.requests.requestsTitle": "请求", "inspector.requests.requestSucceededTooltipTitle": "请求成功", @@ -6289,7 +6288,6 @@ "xpack.apm.clearFilters": "清除筛选", "xpack.apm.compositeSpanCallsLabel": ",{count} 个调用,平均 {duration}", "xpack.apm.compositeSpanDurationLabel": "平均持续时间", - "xpack.apm.correlations.correlationsTable.actionsLabel": "筛选", "xpack.apm.correlations.correlationsTable.excludeDescription": "筛除值", "xpack.apm.correlations.correlationsTable.excludeLabel": "排除", "xpack.apm.correlations.correlationsTable.filterDescription": "按值筛选", @@ -10969,13 +10967,10 @@ "xpack.fleet.createPackagePolicy.cancelButton": "取消", "xpack.fleet.createPackagePolicy.cancelLinkText": "取消", "xpack.fleet.createPackagePolicy.errorOnSaveText": "您的集成策略有错误。请在保存前修复这些错误。", - "xpack.fleet.createPackagePolicy.integrationsContextAddAgentLinkMessage": "添加代理", - "xpack.fleet.createPackagePolicy.integrationsContextaddAgentNextNotificationMessage": "接着,{link}以开始采集数据。", "xpack.fleet.createPackagePolicy.pageDescriptionfromPackage": "按照以下说明将此集成添加到代理策略。", "xpack.fleet.createPackagePolicy.pageDescriptionfromPolicy": "为选定代理策略配置集成。", "xpack.fleet.createPackagePolicy.pageTitle": "添加集成", "xpack.fleet.createPackagePolicy.pageTitleWithPackageName": "添加 {packageName} 集成", - "xpack.fleet.createPackagePolicy.policyContextAddAgentNextNotificationMessage": "策略已更新。将代理添加到“{agentPolicyName}”代理,以部署此策略。", "xpack.fleet.createPackagePolicy.saveButton": "保存集成", "xpack.fleet.createPackagePolicy.stepConfigure.advancedOptionsToggleLinkText": "高级选项", "xpack.fleet.createPackagePolicy.stepConfigure.errorCountText": "{count, plural, other {# 个错误}}", @@ -18219,8 +18214,7 @@ "xpack.monitoring.cluster.overview.logstashPanel.withPersistentQueuesLabel": "持久性队列", "xpack.monitoring.cluster.overview.pageTitle": "集群概览", "xpack.monitoring.cluster.overviewTitle": "概览", - "xpack.monitoring.clusterAlertsNavigation.clusterAlertsLinkText": "集群告警", - "xpack.monitoring.clustersNavigation.clustersLinkText": "集群", + "xpack.monitoring.cluster.listing.tabTitle": "集群", "xpack.monitoring.clusterStats.uuidNotFoundErrorMessage": "在选定时间范围内找不到该集群。UUID:{clusterUuid}", "xpack.monitoring.clusterStats.uuidNotSpecifiedErrorMessage": "{clusterUuid} 未指定", "xpack.monitoring.elasticsearch.ccr.ccrListingTable.alertsColumnTitle": "告警", @@ -18232,10 +18226,10 @@ "xpack.monitoring.elasticsearch.ccr.ccrListingTable.syncLagOpsColumnTitle": "同步延迟(操作)", "xpack.monitoring.elasticsearch.ccr.heading": "CCR", "xpack.monitoring.elasticsearch.ccr.pageTitle": "Elasticsearch Ccr", - "xpack.monitoring.elasticsearch.ccr.routeTitle": "Elasticsearch - CCR", + "xpack.monitoring.elasticsearch.ccr.title": "Elasticsearch - CCR", "xpack.monitoring.elasticsearch.ccr.shard.instanceTitle": "索引:{followerIndex} 分片:{shardId}", "xpack.monitoring.elasticsearch.ccr.shard.pageTitle": "Elasticsearch Ccr 分片 - 索引:{followerIndex} 分片:{shardId}", - "xpack.monitoring.elasticsearch.ccr.shard.routeTitle": "Elasticsearch - CCR - 分片", + "xpack.monitoring.elasticsearch.ccr.shard.title": "Elasticsearch - CCR - 分片", "xpack.monitoring.elasticsearch.ccr.shardsTable.alertsColumnTitle": "告警", "xpack.monitoring.elasticsearch.ccr.shardsTable.errorColumnTitle": "错误", "xpack.monitoring.elasticsearch.ccr.shardsTable.lastFetchTimeColumnTitle": "上次提取时间", @@ -18269,7 +18263,7 @@ "xpack.monitoring.elasticsearch.indexDetailStatus.totalShardsTitle": "分片合计", "xpack.monitoring.elasticsearch.indexDetailStatus.totalTitle": "合计", "xpack.monitoring.elasticsearch.indexDetailStatus.unassignedShardsTitle": "未分配分片", - "xpack.monitoring.elasticsearch.indices.advanced.routeTitle": "Elasticsearch - 索引 - {indexName} - 高级", + "xpack.monitoring.elasticsearch.index.advanced.title": "Elasticsearch - 索引 - {indexName} - 高级", "xpack.monitoring.elasticsearch.indices.alertsColumnTitle": "告警", "xpack.monitoring.elasticsearch.indices.dataTitle": "数据", "xpack.monitoring.elasticsearch.indices.documentCountTitle": "文档计数", @@ -18278,8 +18272,8 @@ "xpack.monitoring.elasticsearch.indices.monitoringTablePlaceholder": "筛选索引……", "xpack.monitoring.elasticsearch.indices.nameTitle": "名称", "xpack.monitoring.elasticsearch.indices.noIndicesMatchYourSelectionDescription": "没有索引匹配您的选择。请尝试更改时间范围选择。", - "xpack.monitoring.elasticsearch.indices.overview.pageTitle": "索引:{indexName}", - "xpack.monitoring.elasticsearch.indices.overview.routeTitle": "Elasticsearch - 索引 - {indexName} - 概览", + "xpack.monitoring.elasticsearch.index.overview.pageTitle": "索引:{indexName}", + "xpack.monitoring.elasticsearch.index.overview.title": "Elasticsearch - 索引 - {indexName} - 概览", "xpack.monitoring.elasticsearch.indices.pageTitle": "Elasticsearch 索引", "xpack.monitoring.elasticsearch.indices.routeTitle": "Elasticsearch - 索引", "xpack.monitoring.elasticsearch.indices.searchRateTitle": "搜索速率", @@ -18298,7 +18292,7 @@ "xpack.monitoring.elasticsearch.mlJobListing.statusIconLabel": "作业状态:{status}", "xpack.monitoring.elasticsearch.mlJobs.pageTitle": "Elasticsearch Machine Learning 作业", "xpack.monitoring.elasticsearch.mlJobs.routeTitle": "Elasticsearch - Machine Learning 作业", - "xpack.monitoring.elasticsearch.node.advanced.routeTitle": "Elasticsearch - 节点 - {nodeSummaryName} - 高级", + "xpack.monitoring.elasticsearch.node.advanced.title": "Elasticsearch - 节点 - {nodeName} - 高级", "xpack.monitoring.elasticsearch.node.cells.tooltip.iconLabel": "有关此指标的更多信息", "xpack.monitoring.elasticsearch.node.cells.tooltip.max": "最大值", "xpack.monitoring.elasticsearch.node.cells.tooltip.min": "最小值", @@ -18307,7 +18301,7 @@ "xpack.monitoring.elasticsearch.node.cells.trendingDownText": "向下", "xpack.monitoring.elasticsearch.node.cells.trendingUpText": "向上", "xpack.monitoring.elasticsearch.node.overview.pageTitle": "Elasticsearch 节点:{node}", - "xpack.monitoring.elasticsearch.node.overview.routeTitle": "Elasticsearch - 节点 - {nodeName} - 概览", + "xpack.monitoring.elasticsearch.node.overview.title": "Elasticsearch - 节点 - {nodeName} - 概览", "xpack.monitoring.elasticsearch.node.statusIconLabel": "状态:{status}", "xpack.monitoring.elasticsearch.nodeDetailStatus.alerts": "告警", "xpack.monitoring.elasticsearch.nodeDetailStatus.dataLabel": "数据", @@ -18396,8 +18390,7 @@ "xpack.monitoring.es.nodeType.nodeLabel": "节点", "xpack.monitoring.esNavigation.ccrLinkText": "CCR", "xpack.monitoring.esNavigation.indicesLinkText": "索引", - "xpack.monitoring.esNavigation.instance.advancedLinkText": "高级", - "xpack.monitoring.esNavigation.instance.overviewLinkText": "概览", + "xpack.monitoring.esItemNavigation.advancedLinkText": "高级", "xpack.monitoring.esNavigation.jobsLinkText": "Machine Learning 作业", "xpack.monitoring.esNavigation.nodesLinkText": "节点", "xpack.monitoring.esNavigation.overviewLinkText": "概览", @@ -18516,7 +18509,6 @@ "xpack.monitoring.logstash.node.advanced.pageTitle": "Logstash 节点:{nodeName}", "xpack.monitoring.logstash.node.advanced.routeTitle": "Logstash - {nodeName} - 高级", "xpack.monitoring.logstash.node.pageTitle": "Logstash 节点:{nodeName}", - "xpack.monitoring.logstash.node.pipelines.notAvailableDescription": "仅 Logstash 版本 6.0.0 或更高版本提供管道监测功能。此节点正在运行版本 {logstashVersion}。", "xpack.monitoring.logstash.node.pipelines.pageTitle": "Logstash 节点管道:{nodeName}", "xpack.monitoring.logstash.node.pipelines.routeTitle": "Logstash - {nodeName} - 管道", "xpack.monitoring.logstash.node.routeTitle": "Logstash - {nodeName}", @@ -20299,23 +20291,7 @@ "xpack.security.management.editRole.roleNameFormRowTitle": "角色名称", "xpack.security.management.editRole.roleSuccessfullyDeletedNotificationMessage": "已删除角色", "xpack.security.management.editRole.roleSuccessfullySavedNotificationMessage": "保存的角色", - "xpack.security.management.editRole.setPrivilegesToKibanaDescription": "设置 Elasticsearch 数据的权限并控制对 Kibana 的访问权限。", "xpack.security.management.editRole.setPrivilegesToKibanaSpacesDescription": "设置 Elasticsearch 数据的权限并控制对 Kibana 空间的访问权限。", - "xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeDropdown": "全部", - "xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeDropdownDescription": "授予对 Kibana 全部功能的完全权限", - "xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeInput": "全部", - "xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeDropdown": "定制", - "xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeDropdownDescription": "定制对 Kibana 的访问权限", - "xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeInput": "定制", - "xpack.security.management.editRole.simplePrivilegeForm.kibanaPrivilegesTitle": "Kibana 权限", - "xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeDropdown": "无", - "xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeDropdownDescription": "没有对 Kibana 的访问权限", - "xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeInput": "无", - "xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeDropdown": "读取", - "xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeDropdownDescription": "授予对 Kibana 全部功能的只读权限。", - "xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeInput": "读取", - "xpack.security.management.editRole.simplePrivilegeForm.specifyPrivilegeForRoleDescription": "为此角色指定 Kibana 权限。", - "xpack.security.management.editRole.simplePrivilegeForm.unsupportedSpacePrivilegesWarning": "此角色包含工作区的权限定义,但在 Kibana 中未启用工作区。保存此角色将会移除这些权限。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "请确保您的帐户具有 {kibanaAdmin} 角色授予的所有权限,然后重试。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.globalSpacesName": "* 所有工作区", "xpack.security.management.editRole.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "您无权查看所有可用工作区。", @@ -25342,11 +25318,8 @@ "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.categoryTitle": "类别", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.commentsTextAreaFieldLabel": "其他注释", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.descriptionTextAreaFieldLabel": "描述", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.destinationIPTitle": "目标 IP", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.impactSelectFieldLabel": "影响", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.invalidApiUrlTextField": "URL 无效。", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.malwareHashTitle": "恶意软件哈希", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.malwareURLTitle": "恶意软件 URL", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.passwordTextFieldLabel": "密码", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.prioritySelectFieldLabel": "优先级", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.reenterValuesLabel": "用户名和密码已加密。请为这些字段重新输入值。", @@ -25356,7 +25329,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUsernameTextField": "“用户名”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requireHttpsApiUrlTextField": "URL 必须以 https:// 开头。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectFieldLabel": "严重性", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.sourceIPTitle": "源 IP", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.subcategoryTitle": "子类别", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.title": "事件", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.titleFieldLabel": "简短描述(必填)", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/application_required_callout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/application_required_callout.tsx index 561dae95fe1b7d..2faa5a9f4a5e0e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/application_required_callout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/application_required_callout.tsx @@ -35,7 +35,7 @@ const ApplicationRequiredCalloutComponent: React.FC = ({ message }) => { ['action']; @@ -41,24 +30,6 @@ const CredentialsComponent: React.FC = ({ editActionSecrets, editActionConfig, }) => { - const { docLinks } = useKibana().services; - const { apiUrl } = action.config; - const { username, password } = action.secrets; - - const isApiUrlInvalid = isFieldInvalid(apiUrl, errors.apiUrl); - const isUsernameInvalid = isFieldInvalid(username, errors.username); - const isPasswordInvalid = isFieldInvalid(password, errors.password); - - const handleOnChangeActionConfig = useCallback( - (key: string, value: string) => editActionConfig(key, value), - [editActionConfig] - ); - - const handleOnChangeSecretConfig = useCallback( - (key: string, value: string) => editActionSecrets(key, value), - [editActionSecrets] - ); - return ( <> @@ -66,45 +37,13 @@ const CredentialsComponent: React.FC = ({

{i18n.SN_INSTANCE_LABEL}

-

- - {i18n.SETUP_DEV_INSTANCE} - - ), - }} - /> -

- - - - handleOnChangeActionConfig('apiUrl', evt.target.value)} - onBlur={() => { - if (!apiUrl) { - editActionConfig('apiUrl', ''); - } - }} - disabled={isLoading} - /> - +
@@ -115,75 +54,15 @@ const CredentialsComponent: React.FC = ({ - - - - - {getEncryptedFieldNotifyLabel( - !action.id, - 2, - action.isMissingSecrets ?? false, - i18n.REENTER_VALUES_LABEL - )} - - - - - - - - handleOnChangeSecretConfig('username', evt.target.value)} - onBlur={() => { - if (!username) { - editActionSecrets('username', ''); - } - }} - disabled={isLoading} - /> - - - - - - - - handleOnChangeSecretConfig('password', evt.target.value)} - onBlur={() => { - if (!password) { - editActionSecrets('password', ''); - } - }} - disabled={isLoading} - /> - - - + + + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials_api_url.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials_api_url.tsx new file mode 100644 index 00000000000000..5ddef8bab67006 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials_api_url.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFormRow, EuiLink, EuiFieldText, EuiSpacer } from '@elastic/eui'; +import { useKibana } from '../../../../common/lib/kibana'; +import type { ActionConnectorFieldsProps } from '../../../../types'; +import * as i18n from './translations'; +import type { ServiceNowActionConnector } from './types'; +import { isFieldInvalid } from './helpers'; + +interface Props { + action: ActionConnectorFieldsProps['action']; + errors: ActionConnectorFieldsProps['errors']; + readOnly: boolean; + isLoading: boolean; + editActionConfig: ActionConnectorFieldsProps['editActionConfig']; +} + +const CredentialsApiUrlComponent: React.FC = ({ + action, + errors, + isLoading, + readOnly, + editActionConfig, +}) => { + const { docLinks } = useKibana().services; + const { apiUrl } = action.config; + + const isApiUrlInvalid = isFieldInvalid(apiUrl, errors.apiUrl); + + const onChangeApiUrlEvent = useCallback( + (event?: React.ChangeEvent) => + editActionConfig('apiUrl', event?.target.value ?? ''), + [editActionConfig] + ); + + return ( + <> + +

+ + {i18n.SETUP_DEV_INSTANCE} + + ), + }} + /> +

+
+ + + { + if (!apiUrl) { + onChangeApiUrlEvent(); + } + }} + disabled={isLoading} + /> + + + ); +}; + +export const CredentialsApiUrl = memo(CredentialsApiUrlComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials_auth.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials_auth.tsx new file mode 100644 index 00000000000000..c9fccc9faec99e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials_auth.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useCallback } from 'react'; +import { EuiFormRow, EuiFieldText, EuiFieldPassword } from '@elastic/eui'; +import type { ActionConnectorFieldsProps } from '../../../../types'; +import * as i18n from './translations'; +import type { ServiceNowActionConnector } from './types'; +import { isFieldInvalid } from './helpers'; +import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; + +interface Props { + action: ActionConnectorFieldsProps['action']; + errors: ActionConnectorFieldsProps['errors']; + readOnly: boolean; + isLoading: boolean; + editActionSecrets: ActionConnectorFieldsProps['editActionSecrets']; +} + +const NUMBER_OF_FIELDS = 2; + +const CredentialsAuthComponent: React.FC = ({ + action, + errors, + isLoading, + readOnly, + editActionSecrets, +}) => { + const { username, password } = action.secrets; + + const isUsernameInvalid = isFieldInvalid(username, errors.username); + const isPasswordInvalid = isFieldInvalid(password, errors.password); + + const onChangeUsernameEvent = useCallback( + (event?: React.ChangeEvent) => + editActionSecrets('username', event?.target.value ?? ''), + [editActionSecrets] + ); + + const onChangePasswordEvent = useCallback( + (event?: React.ChangeEvent) => + editActionSecrets('password', event?.target.value ?? ''), + [editActionSecrets] + ); + + return ( + <> + + {getEncryptedFieldNotifyLabel( + !action.id, + NUMBER_OF_FIELDS, + action.isMissingSecrets ?? false, + i18n.REENTER_VALUES_LABEL + )} + + + { + if (!username) { + onChangeUsernameEvent(); + } + }} + disabled={isLoading} + /> + + + { + if (!password) { + onChangePasswordEvent(); + } + }} + disabled={isLoading} + /> + + + ); +}; + +export const CredentialsAuth = memo(CredentialsAuthComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.test.tsx index 767b38ebcf6ade..0c125f38516362 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.test.tsx @@ -19,7 +19,7 @@ describe('DeprecatedCallout', () => { wrapper: ({ children }) => {children}, }); - expect(screen.getByText('Deprecated connector type')).toBeInTheDocument(); + expect(screen.getByText('This connector type is deprecated')).toBeInTheDocument(); }); test('it calls onMigrate when pressing the button', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.tsx index 101d1572a67ad3..faeeaa1bbbffea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.tsx @@ -6,7 +6,7 @@ */ import React, { memo } from 'react'; -import { EuiSpacer, EuiCallOut, EuiButtonEmpty } from '@elastic/eui'; +import { EuiSpacer, EuiCallOut, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -26,23 +26,33 @@ const DeprecatedCalloutComponent: React.FC = ({ onMigrate }) => { title={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.deprecatedCalloutTitle', { - defaultMessage: 'Deprecated connector type', + defaultMessage: 'This connector type is deprecated', } )} > + update: ( + {i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.deprecatedCalloutMigrate', { - defaultMessage: 'update this connector.', + defaultMessage: 'Update this connector,', } )} - + + ), + create: ( + + {i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.deprecatedCalloutCreate', + { + defaultMessage: 'or create a new one.', + } + )} + ), }} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts index ca557b31c4f4f3..0134133645bb3f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts @@ -14,6 +14,8 @@ import { import { IErrorObject } from '../../../../../public/types'; import { AppInfo, Choice, RESTApiError, ServiceNowActionConnector } from './types'; +export const DEFAULT_CORRELATION_ID = '{{rule.id}}:{{alert.id}}'; + export const choicesToEuiOptions = (choices: Choice[]): EuiSelectOption[] => choices.map((choice) => ({ value: choice.value, text: choice.label })); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/installation_callout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/installation_callout.test.tsx index 8e1c1820920c52..ee63a546e6aa18 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/installation_callout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/installation_callout.test.tsx @@ -15,7 +15,7 @@ describe('DeprecatedCallout', () => { render(); expect( screen.getByText( - 'To use this connector, you must first install the Elastic App from the ServiceNow App Store' + 'To use this connector, first install the Elastic app from the ServiceNow app store.' ) ).toBeInTheDocument(); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx index 02f3ae47728abd..7c720148780a45 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx @@ -6,29 +6,52 @@ */ import React from 'react'; +import { act } from '@testing-library/react'; import { mountWithIntl } from '@kbn/test/jest'; + +import { useKibana } from '../../../../common/lib/kibana'; +import { ActionConnectorFieldsSetCallbacks } from '../../../../types'; +import { updateActionConnector } from '../../../lib/action_connector_api'; import ServiceNowConnectorFields from './servicenow_connectors'; import { ServiceNowActionConnector } from './types'; +import { getAppInfo } from './api'; + jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../lib/action_connector_api'); +jest.mock('./api'); + +const useKibanaMock = useKibana as jest.Mocked; +const getAppInfoMock = getAppInfo as jest.Mock; +const updateActionConnectorMock = updateActionConnector as jest.Mock; describe('ServiceNowActionConnectorFields renders', () => { + const usesTableApiConnector = { + secrets: { + username: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.servicenow', + isPreconfigured: false, + name: 'SN', + config: { + apiUrl: 'https://test/', + isLegacy: true, + }, + } as ServiceNowActionConnector; + + const usesImportSetApiConnector = { + ...usesTableApiConnector, + config: { + ...usesTableApiConnector.config, + isLegacy: false, + }, + } as ServiceNowActionConnector; + test('alerting servicenow connector fields are rendered', () => { - const actionConnector = { - secrets: { - username: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.webhook', - isPreconfigured: false, - name: 'webhook', - config: { - apiUrl: 'https://test/', - }, - } as ServiceNowActionConnector; const wrapper = mountWithIntl( {}} editActionSecrets={() => {}} @@ -41,30 +64,16 @@ describe('ServiceNowActionConnectorFields renders', () => { wrapper.find('[data-test-subj="connector-servicenow-username-form-input"]').length > 0 ).toBeTruthy(); - expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="credentialsApiUrlFromInput"]').length > 0).toBeTruthy(); expect( wrapper.find('[data-test-subj="connector-servicenow-password-form-input"]').length > 0 ).toBeTruthy(); }); test('case specific servicenow connector fields is rendered', () => { - const actionConnector = { - secrets: { - username: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.servicenow', - isPreconfigured: false, - name: 'servicenow', - config: { - apiUrl: 'https://test/', - isLegacy: false, - }, - } as ServiceNowActionConnector; const wrapper = mountWithIntl( {}} editActionSecrets={() => {}} @@ -74,7 +83,8 @@ describe('ServiceNowActionConnectorFields renders', () => { isEdit={false} /> ); - expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); + + expect(wrapper.find('[data-test-subj="credentialsApiUrlFromInput"]').length > 0).toBeTruthy(); expect( wrapper.find('[data-test-subj="connector-servicenow-password-form-input"]').length > 0 ).toBeTruthy(); @@ -87,6 +97,7 @@ describe('ServiceNowActionConnectorFields renders', () => { config: {}, secrets: {}, } as ServiceNowActionConnector; + const wrapper = mountWithIntl( { config: {}, secrets: {}, } as ServiceNowActionConnector; + const wrapper = mountWithIntl( { }); test('should display a message on edit to re-enter credentials', () => { - const actionConnector = { - secrets: { - username: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.servicenow', - isPreconfigured: false, - name: 'servicenow', - config: { - apiUrl: 'https://test/', - }, - } as ServiceNowActionConnector; const wrapper = mountWithIntl( {}} editActionSecrets={() => {}} @@ -152,4 +151,268 @@ describe('ServiceNowActionConnectorFields renders', () => { expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); }); + + describe('Elastic certified ServiceNow application', () => { + const { services } = useKibanaMock(); + const applicationInfoData = { + name: 'Elastic', + scope: 'x_elas2_inc_int', + version: '1.0.0', + }; + + let beforeActionConnectorSaveFn: () => Promise; + const setCallbacks = (({ + beforeActionConnectorSave, + }: { + beforeActionConnectorSave: () => Promise; + }) => { + beforeActionConnectorSaveFn = beforeActionConnectorSave; + }) as ActionConnectorFieldsSetCallbacks; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should render the correct callouts when the connectors needs the application', () => { + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + setCallbacks={() => {}} + isEdit={false} + /> + ); + expect(wrapper.find('[data-test-subj="snInstallationCallout"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="snDeprecatedCallout"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="snApplicationCallout"]').exists()).toBeFalsy(); + }); + + test('should render the correct callouts if the connector uses the table API', () => { + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + setCallbacks={() => {}} + isEdit={false} + /> + ); + expect(wrapper.find('[data-test-subj="snInstallationCallout"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="snDeprecatedCallout"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="snApplicationCallout"]').exists()).toBeFalsy(); + }); + + test('should get application information when saving the connector', async () => { + getAppInfoMock.mockResolvedValue(applicationInfoData); + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + setCallbacks={setCallbacks} + isEdit={false} + /> + ); + + await act(async () => { + await beforeActionConnectorSaveFn(); + }); + + expect(getAppInfoMock).toHaveBeenCalledTimes(1); + expect(wrapper.find('[data-test-subj="snApplicationCallout"]').exists()).toBeFalsy(); + }); + + test('should NOT get application information when the connector uses the old API', async () => { + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + setCallbacks={setCallbacks} + isEdit={false} + /> + ); + + await act(async () => { + await beforeActionConnectorSaveFn(); + }); + + expect(getAppInfoMock).toHaveBeenCalledTimes(0); + expect(wrapper.find('[data-test-subj="snApplicationCallout"]').exists()).toBeFalsy(); + }); + + test('should render error when save failed', async () => { + expect.assertions(4); + + const errorMessage = 'request failed'; + getAppInfoMock.mockRejectedValueOnce(new Error(errorMessage)); + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + setCallbacks={setCallbacks} + isEdit={false} + /> + ); + + await expect( + // The async is needed so the act will finished before asserting for the callout + async () => await act(async () => await beforeActionConnectorSaveFn()) + ).rejects.toThrow(errorMessage); + expect(getAppInfoMock).toHaveBeenCalledTimes(1); + + wrapper.update(); + expect(wrapper.find('[data-test-subj="snApplicationCallout"]').exists()).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="snApplicationCallout"]') + .first() + .text() + .includes(errorMessage) + ).toBeTruthy(); + }); + + test('should render error when the response is a REST api error', async () => { + expect.assertions(4); + + const errorMessage = 'request failed'; + getAppInfoMock.mockResolvedValue({ error: { message: errorMessage }, status: 'failure' }); + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + setCallbacks={setCallbacks} + isEdit={false} + /> + ); + + await expect( + // The async is needed so the act will finished before asserting for the callout + async () => await act(async () => await beforeActionConnectorSaveFn()) + ).rejects.toThrow(errorMessage); + expect(getAppInfoMock).toHaveBeenCalledTimes(1); + + wrapper.update(); + expect(wrapper.find('[data-test-subj="snApplicationCallout"]').exists()).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="snApplicationCallout"]') + .first() + .text() + .includes(errorMessage) + ).toBeTruthy(); + }); + + test('should migrate the deprecated connector when the application throws', async () => { + getAppInfoMock.mockResolvedValue(applicationInfoData); + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + setCallbacks={setCallbacks} + isEdit={false} + /> + ); + + expect(wrapper.find('[data-test-subj="update-connector-btn"]').exists()).toBeTruthy(); + wrapper.find('[data-test-subj="update-connector-btn"]').first().simulate('click'); + expect(wrapper.find('[data-test-subj="updateConnectorForm"]').exists()).toBeTruthy(); + + await act(async () => { + // Update the connector + wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().simulate('click'); + }); + + expect(getAppInfoMock).toHaveBeenCalledTimes(1); + expect(updateActionConnectorMock).toHaveBeenCalledWith( + expect.objectContaining({ + id: usesTableApiConnector.id, + connector: { + name: usesTableApiConnector.name, + config: { ...usesTableApiConnector.config, isLegacy: false }, + secrets: usesTableApiConnector.secrets, + }, + }) + ); + + expect(services.notifications.toasts.addSuccess).toHaveBeenCalledWith({ + text: 'Connector has been updated.', + title: 'SN connector updated', + }); + + // The flyout is closed + wrapper.update(); + expect(wrapper.find('[data-test-subj="updateConnectorForm"]').exists()).toBeFalsy(); + }); + + test('should NOT migrate the deprecated connector when there is an error', async () => { + const errorMessage = 'request failed'; + getAppInfoMock.mockRejectedValueOnce(new Error(errorMessage)); + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + setCallbacks={setCallbacks} + isEdit={false} + /> + ); + + expect(wrapper.find('[data-test-subj="update-connector-btn"]').exists()).toBeTruthy(); + wrapper.find('[data-test-subj="update-connector-btn"]').first().simulate('click'); + expect(wrapper.find('[data-test-subj="updateConnectorForm"]').exists()).toBeTruthy(); + + // The async is needed so the act will finished before asserting for the callout + await act(async () => { + wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().simulate('click'); + }); + + expect(getAppInfoMock).toHaveBeenCalledTimes(1); + expect(updateActionConnectorMock).not.toHaveBeenCalled(); + + expect(services.notifications.toasts.addSuccess).not.toHaveBeenCalled(); + + // The flyout is still open + wrapper.update(); + expect(wrapper.find('[data-test-subj="updateConnectorForm"]').exists()).toBeTruthy(); + + // The error message should be shown to the user + expect( + wrapper + .find('[data-test-subj="updateConnectorForm"] [data-test-subj="snApplicationCallout"]') + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="updateConnectorForm"] [data-test-subj="snApplicationCallout"]') + .first() + .text() + .includes(errorMessage) + ).toBeTruthy(); + }); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx index 2cf738c5e0c13e..20d38cfc7cea81 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -17,7 +17,7 @@ import { useGetAppInfo } from './use_get_app_info'; import { ApplicationRequiredCallout } from './application_required_callout'; import { isRESTApiError, isLegacyConnector } from './helpers'; import { InstallationCallout } from './installation_callout'; -import { UpdateConnectorModal } from './update_connector_modal'; +import { UpdateConnector } from './update_connector'; import { updateActionConnector } from '../../../lib/action_connector_api'; import { Credentials } from './credentials'; @@ -40,7 +40,7 @@ const ServiceNowConnectorFields: React.FC setShowModal(true), []); - const onModalCancel = useCallback(() => setShowModal(false), []); - - const onModalConfirm = useCallback(async () => { - await getApplicationInfo(); - await updateActionConnector({ - http, - connector: { - name: action.name, - config: { apiUrl, isLegacy: false }, - secrets: { username, password }, - }, - id: action.id, - }); - - editActionConfig('isLegacy', false); - setShowModal(false); - - toasts.addSuccess({ - title: i18n.MIGRATION_SUCCESS_TOAST_TITLE(action.name), - text: i18n.MIGRATION_SUCCESS_TOAST_TEXT, - }); + const onMigrateClick = useCallback(() => setShowUpdateConnector(true), []); + const onModalCancel = useCallback(() => setShowUpdateConnector(false), []); + + const onUpdateConnectorConfirm = useCallback(async () => { + try { + await getApplicationInfo(); + + await updateActionConnector({ + http, + connector: { + name: action.name, + config: { apiUrl, isLegacy: false }, + secrets: { username, password }, + }, + id: action.id, + }); + + editActionConfig('isLegacy', false); + setShowUpdateConnector(false); + + toasts.addSuccess({ + title: i18n.UPDATE_SUCCESS_TOAST_TITLE(action.name), + text: i18n.UPDATE_SUCCESS_TOAST_TEXT, + }); + } catch (err) { + /** + * getApplicationInfo may throw an error if the request + * fails or if there is a REST api error. + * + * We silent the errors as a callout will show and inform the user + */ + } }, [ getApplicationInfo, http, @@ -115,8 +125,8 @@ const ServiceNowConnectorFields: React.FC - {showModal && ( - )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx index 30e09356e95dd1..078b5535c16eb9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { mount } from 'enzyme'; +import { mountWithIntl } from '@kbn/test/jest'; import { act } from '@testing-library/react'; import { ActionConnector } from '../../../../types'; @@ -115,13 +115,15 @@ describe('ServiceNowITSMParamsFields renders', () => { }); test('all params fields is rendered', () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="urgencySelect"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="severitySelect"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="impactSelect"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="categorySelect"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="subcategorySelect"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="short_descriptionInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="correlation_idInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="correlation_displayInput"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="descriptionTextArea"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="commentsTextArea"]').exists()).toBeTruthy(); }); @@ -132,7 +134,7 @@ describe('ServiceNowITSMParamsFields renders', () => { // eslint-disable-next-line @typescript-eslint/naming-convention errors: { 'subActionParams.incident.short_description': ['error'] }, }; - const wrapper = mount(); + const wrapper = mountWithIntl(); const title = wrapper.find('[data-test-subj="short_descriptionInput"]').first(); expect(title.prop('isInvalid')).toBeTruthy(); }); @@ -144,10 +146,9 @@ describe('ServiceNowITSMParamsFields renders', () => { ...defaultProps, actionParams: newParams, }; - mount(); + mountWithIntl(); expect(editAction.mock.calls[0][1]).toEqual({ incident: { - correlation_display: 'Alerting', correlation_id: '{{rule.id}}:{{alert.id}}', }, comments: [], @@ -161,18 +162,17 @@ describe('ServiceNowITSMParamsFields renders', () => { ...defaultProps, actionParams: newParams, }; - mount(); + mountWithIntl(); expect(editAction.mock.calls[0][1]).toEqual('pushToService'); }); test('Resets fields when connector changes', () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); expect(editAction.mock.calls.length).toEqual(0); wrapper.setProps({ actionConnector: { ...connector, id: '1234' } }); expect(editAction.mock.calls.length).toEqual(1); expect(editAction.mock.calls[0][1]).toEqual({ incident: { - correlation_display: 'Alerting', correlation_id: '{{rule.id}}:{{alert.id}}', }, comments: [], @@ -180,7 +180,7 @@ describe('ServiceNowITSMParamsFields renders', () => { }); test('it transforms the categories to options correctly', async () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); act(() => { onChoices(useGetChoicesResponse.choices); }); @@ -195,7 +195,7 @@ describe('ServiceNowITSMParamsFields renders', () => { }); test('it transforms the subcategories to options correctly', async () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); act(() => { onChoices(useGetChoicesResponse.choices); }); @@ -210,7 +210,7 @@ describe('ServiceNowITSMParamsFields renders', () => { }); test('it transforms the options correctly', async () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); act(() => { onChoices(useGetChoicesResponse.choices); }); @@ -231,6 +231,11 @@ describe('ServiceNowITSMParamsFields renders', () => { const changeEvent = { target: { value: 'Bug' } } as React.ChangeEvent; const simpleFields = [ { dataTestSubj: 'input[data-test-subj="short_descriptionInput"]', key: 'short_description' }, + { dataTestSubj: 'input[data-test-subj="correlation_idInput"]', key: 'correlation_id' }, + { + dataTestSubj: 'input[data-test-subj="correlation_displayInput"]', + key: 'correlation_display', + }, { dataTestSubj: 'textarea[data-test-subj="descriptionTextArea"]', key: 'description' }, { dataTestSubj: '[data-test-subj="urgencySelect"]', key: 'urgency' }, { dataTestSubj: '[data-test-subj="severitySelect"]', key: 'severity' }, @@ -241,7 +246,7 @@ describe('ServiceNowITSMParamsFields renders', () => { simpleFields.forEach((field) => test(`${field.key} update triggers editAction :D`, () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); const theField = wrapper.find(field.dataTestSubj).first(); theField.prop('onChange')!(changeEvent); expect(editAction.mock.calls[0][1].incident[field.key]).toEqual(changeEvent.target.value); @@ -249,14 +254,14 @@ describe('ServiceNowITSMParamsFields renders', () => { ); test('A comment triggers editAction', () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); const comments = wrapper.find('textarea[data-test-subj="commentsTextArea"]'); expect(comments.simulate('change', changeEvent)); expect(editAction.mock.calls[0][1].comments.length).toEqual(1); }); test('An empty comment does not trigger editAction', () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); const emptyComment = { target: { value: '' } }; const comments = wrapper.find('[data-test-subj="commentsTextArea"] textarea'); expect(comments.simulate('change', emptyComment)); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx index 81428cd7f0a731..09b04f0fa3c485 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx @@ -13,18 +13,19 @@ import { EuiFlexItem, EuiSpacer, EuiTitle, - EuiSwitch, + EuiLink, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + import { useKibana } from '../../../../common/lib/kibana'; import { ActionParamsProps } from '../../../../types'; import { ServiceNowITSMActionParams, Choice, Fields, ServiceNowActionConnector } from './types'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; import { useGetChoices } from './use_get_choices'; -import { choicesToEuiOptions, isLegacyConnector } from './helpers'; +import { choicesToEuiOptions, DEFAULT_CORRELATION_ID, isLegacyConnector } from './helpers'; import * as i18n from './translations'; -import { UPDATE_INCIDENT_VARIABLE, NOT_UPDATE_INCIDENT_VARIABLE } from './config'; const useGetChoicesFields = ['urgency', 'severity', 'impact', 'category', 'subcategory']; const defaultFields: Fields = { @@ -40,11 +41,14 @@ const ServiceNowParamsFields: React.FunctionComponent< ActionParamsProps > = ({ actionConnector, actionParams, editAction, index, errors, messageVariables }) => { const { + docLinks, http, notifications: { toasts }, } = useKibana().services; - const isOldConnector = isLegacyConnector(actionConnector as unknown as ServiceNowActionConnector); + const isDeprecatedConnector = isLegacyConnector( + actionConnector as unknown as ServiceNowActionConnector + ); const actionConnectorRef = useRef(actionConnector?.id ?? ''); const { incident, comments } = useMemo( @@ -57,13 +61,8 @@ const ServiceNowParamsFields: React.FunctionComponent< [actionParams.subActionParams] ); - const hasUpdateIncident = - incident.correlation_id != null && incident.correlation_id === UPDATE_INCIDENT_VARIABLE; - const [updateIncident, setUpdateIncident] = useState(hasUpdateIncident); const [choices, setChoices] = useState(defaultFields); - const correlationID = updateIncident ? UPDATE_INCIDENT_VARIABLE : NOT_UPDATE_INCIDENT_VARIABLE; - const editSubActionProperty = useCallback( (key: string, value: any) => { const newProps = @@ -99,14 +98,6 @@ const ServiceNowParamsFields: React.FunctionComponent< ); }, []); - const onUpdateIncidentSwitchChange = useCallback(() => { - const newCorrelationID = !updateIncident - ? UPDATE_INCIDENT_VARIABLE - : NOT_UPDATE_INCIDENT_VARIABLE; - editSubActionProperty('correlation_id', newCorrelationID); - setUpdateIncident(!updateIncident); - }, [editSubActionProperty, updateIncident]); - const categoryOptions = useMemo(() => choicesToEuiOptions(choices.category), [choices.category]); const urgencyOptions = useMemo(() => choicesToEuiOptions(choices.urgency), [choices.urgency]); const severityOptions = useMemo(() => choicesToEuiOptions(choices.severity), [choices.severity]); @@ -136,7 +127,7 @@ const ServiceNowParamsFields: React.FunctionComponent< editAction( 'subActionParams', { - incident: { correlation_id: correlationID, correlation_display: 'Alerting' }, + incident: { correlation_id: DEFAULT_CORRELATION_ID }, comments: [], }, index @@ -153,7 +144,7 @@ const ServiceNowParamsFields: React.FunctionComponent< editAction( 'subActionParams', { - incident: { correlation_id: correlationID, correlation_display: 'Alerting' }, + incident: { correlation_id: DEFAULT_CORRELATION_ID }, comments: [], }, index @@ -253,6 +244,46 @@ const ServiceNowParamsFields: React.FunctionComponent< + {!isDeprecatedConnector && ( + <> + + + + + + } + > + + + + + + + + + + + + )} - {!isOldConnector && ( - - - - - - )} { }); test('all params fields is rendered', () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="short_descriptionInput"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="source_ipInput"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="dest_ipInput"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="malware_urlInput"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="malware_hashInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="correlation_idInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="correlation_displayInput"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="prioritySelect"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="categorySelect"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="subcategorySelect"]').exists()).toBeTruthy(); @@ -162,7 +160,7 @@ describe('ServiceNowSIRParamsFields renders', () => { // eslint-disable-next-line @typescript-eslint/naming-convention errors: { 'subActionParams.incident.short_description': ['error'] }, }; - const wrapper = mount(); + const wrapper = mountWithIntl(); const title = wrapper.find('[data-test-subj="short_descriptionInput"]').first(); expect(title.prop('isInvalid')).toBeTruthy(); }); @@ -174,10 +172,9 @@ describe('ServiceNowSIRParamsFields renders', () => { ...defaultProps, actionParams: newParams, }; - mount(); + mountWithIntl(); expect(editAction.mock.calls[0][1]).toEqual({ incident: { - correlation_display: 'Alerting', correlation_id: '{{rule.id}}:{{alert.id}}', }, comments: [], @@ -191,18 +188,17 @@ describe('ServiceNowSIRParamsFields renders', () => { ...defaultProps, actionParams: newParams, }; - mount(); + mountWithIntl(); expect(editAction.mock.calls[0][1]).toEqual('pushToService'); }); test('Resets fields when connector changes', () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); expect(editAction.mock.calls.length).toEqual(0); wrapper.setProps({ actionConnector: { ...connector, id: '1234' } }); expect(editAction.mock.calls.length).toEqual(1); expect(editAction.mock.calls[0][1]).toEqual({ incident: { - correlation_display: 'Alerting', correlation_id: '{{rule.id}}:{{alert.id}}', }, comments: [], @@ -210,7 +206,7 @@ describe('ServiceNowSIRParamsFields renders', () => { }); test('it transforms the categories to options correctly', async () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); act(() => { onChoicesSuccess(choicesResponse.choices); }); @@ -227,7 +223,7 @@ describe('ServiceNowSIRParamsFields renders', () => { }); test('it transforms the subcategories to options correctly', async () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); act(() => { onChoicesSuccess(choicesResponse.choices); }); @@ -250,7 +246,7 @@ describe('ServiceNowSIRParamsFields renders', () => { }); test('it transforms the priorities to options correctly', async () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); act(() => { onChoicesSuccess(choicesResponse.choices); }); @@ -284,11 +280,12 @@ describe('ServiceNowSIRParamsFields renders', () => { const changeEvent = { target: { value: 'Bug' } } as React.ChangeEvent; const simpleFields = [ { dataTestSubj: 'input[data-test-subj="short_descriptionInput"]', key: 'short_description' }, + { dataTestSubj: 'input[data-test-subj="correlation_idInput"]', key: 'correlation_id' }, + { + dataTestSubj: 'input[data-test-subj="correlation_displayInput"]', + key: 'correlation_display', + }, { dataTestSubj: 'textarea[data-test-subj="descriptionTextArea"]', key: 'description' }, - { dataTestSubj: '[data-test-subj="source_ipInput"]', key: 'source_ip' }, - { dataTestSubj: '[data-test-subj="dest_ipInput"]', key: 'dest_ip' }, - { dataTestSubj: '[data-test-subj="malware_urlInput"]', key: 'malware_url' }, - { dataTestSubj: '[data-test-subj="malware_hashInput"]', key: 'malware_hash' }, { dataTestSubj: '[data-test-subj="prioritySelect"]', key: 'priority' }, { dataTestSubj: '[data-test-subj="categorySelect"]', key: 'category' }, { dataTestSubj: '[data-test-subj="subcategorySelect"]', key: 'subcategory' }, @@ -296,7 +293,7 @@ describe('ServiceNowSIRParamsFields renders', () => { simpleFields.forEach((field) => test(`${field.key} update triggers editAction :D`, () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); const theField = wrapper.find(field.dataTestSubj).first(); theField.prop('onChange')!(changeEvent); expect(editAction.mock.calls[0][1].incident[field.key]).toEqual(changeEvent.target.value); @@ -304,14 +301,14 @@ describe('ServiceNowSIRParamsFields renders', () => { ); test('A comment triggers editAction', () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); const comments = wrapper.find('textarea[data-test-subj="commentsTextArea"]'); expect(comments.simulate('change', changeEvent)); expect(editAction.mock.calls[0][1].comments.length).toEqual(1); }); test('An empty comment does not trigger editAction', () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); const emptyComment = { target: { value: '' } }; const comments = wrapper.find('[data-test-subj="commentsTextArea"] textarea'); expect(comments.simulate('change', emptyComment)); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx index 7b7cfc67d99719..72f6d7635268f3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx @@ -13,8 +13,10 @@ import { EuiFlexItem, EuiSpacer, EuiTitle, - EuiSwitch, + EuiLink, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + import { useKibana } from '../../../../common/lib/kibana'; import { ActionParamsProps } from '../../../../types'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; @@ -23,8 +25,7 @@ import { TextFieldWithMessageVariables } from '../../text_field_with_message_var import * as i18n from './translations'; import { useGetChoices } from './use_get_choices'; import { ServiceNowSIRActionParams, Fields, Choice, ServiceNowActionConnector } from './types'; -import { choicesToEuiOptions, isLegacyConnector } from './helpers'; -import { UPDATE_INCIDENT_VARIABLE, NOT_UPDATE_INCIDENT_VARIABLE } from './config'; +import { choicesToEuiOptions, isLegacyConnector, DEFAULT_CORRELATION_ID } from './helpers'; const useGetChoicesFields = ['category', 'subcategory', 'priority']; const defaultFields: Fields = { @@ -33,23 +34,18 @@ const defaultFields: Fields = { priority: [], }; -const valuesToString = (value: string | string[] | null): string | undefined => { - if (Array.isArray(value)) { - return value.join(','); - } - - return value ?? undefined; -}; - const ServiceNowSIRParamsFields: React.FunctionComponent< ActionParamsProps > = ({ actionConnector, actionParams, editAction, index, errors, messageVariables }) => { const { + docLinks, http, notifications: { toasts }, } = useKibana().services; - const isOldConnector = isLegacyConnector(actionConnector as unknown as ServiceNowActionConnector); + const isDeprecatedConnector = isLegacyConnector( + actionConnector as unknown as ServiceNowActionConnector + ); const actionConnectorRef = useRef(actionConnector?.id ?? ''); const { incident, comments } = useMemo( @@ -62,13 +58,8 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< [actionParams.subActionParams] ); - const hasUpdateIncident = - incident.correlation_id != null && incident.correlation_id === UPDATE_INCIDENT_VARIABLE; - const [updateIncident, setUpdateIncident] = useState(hasUpdateIncident); const [choices, setChoices] = useState(defaultFields); - const correlationID = updateIncident ? UPDATE_INCIDENT_VARIABLE : NOT_UPDATE_INCIDENT_VARIABLE; - const editSubActionProperty = useCallback( (key: string, value: any) => { const newProps = @@ -104,14 +95,6 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< ); }, []); - const onUpdateIncidentSwitchChange = useCallback(() => { - const newCorrelationID = !updateIncident - ? UPDATE_INCIDENT_VARIABLE - : NOT_UPDATE_INCIDENT_VARIABLE; - editSubActionProperty('correlation_id', newCorrelationID); - setUpdateIncident(!updateIncident); - }, [editSubActionProperty, updateIncident]); - const { isLoading: isLoadingChoices } = useGetChoices({ http, toastNotifications: toasts, @@ -140,7 +123,7 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< editAction( 'subActionParams', { - incident: { correlation_id: correlationID, correlation_display: 'Alerting' }, + incident: { correlation_id: DEFAULT_CORRELATION_ID }, comments: [], }, index @@ -157,7 +140,7 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< editAction( 'subActionParams', { - incident: { correlation_id: correlationID, correlation_display: 'Alerting' }, + incident: { correlation_id: DEFAULT_CORRELATION_ID }, comments: [], }, index @@ -192,46 +175,6 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< /> - - - - - - - - - - - - - - - - + {!isDeprecatedConnector && ( + <> + + + + + + } + > + + + + + + + + + + + + )} - {!isOldConnector && ( - - - - )} ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.test.tsx index fe73653234170b..500325202b6517 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.test.tsx @@ -7,21 +7,43 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { SNStoreButton } from './sn_store_button'; +import { SNStoreButton, SNStoreLink } from './sn_store_button'; describe('SNStoreButton', () => { - test('it renders the button', () => { + it('should render the button', () => { render(); expect(screen.getByText('Visit ServiceNow app store')).toBeInTheDocument(); }); - test('it renders a danger button', () => { + it('should render a danger button', () => { render(); expect(screen.getByRole('link')).toHaveClass('euiButton--danger'); }); - test('it renders with correct href', () => { + it('should render with correct href', () => { render(); expect(screen.getByRole('link')).toHaveAttribute('href', 'https://store.servicenow.com/'); }); + + it('should render with target blank', () => { + render(); + expect(screen.getByRole('link')).toHaveAttribute('target', '_blank'); + }); +}); + +describe('SNStoreLink', () => { + it('should render the link', () => { + render(); + expect(screen.getByText('Visit ServiceNow app store')).toBeInTheDocument(); + }); + + it('should render with correct href', () => { + render(); + expect(screen.getByRole('link')).toHaveAttribute('href', 'https://store.servicenow.com/'); + }); + + it('should render with target blank', () => { + render(); + expect(screen.getByRole('link')).toHaveAttribute('target', '_blank'); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.tsx index 5921f679d3f504..5a33237159a021 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.tsx @@ -6,7 +6,7 @@ */ import React, { memo } from 'react'; -import { EuiButtonProps, EuiButton } from '@elastic/eui'; +import { EuiButtonProps, EuiButton, EuiLink } from '@elastic/eui'; import * as i18n from './translations'; @@ -18,10 +18,18 @@ interface Props { const SNStoreButtonComponent: React.FC = ({ color }) => { return ( - + {i18n.VISIT_SN_STORE} ); }; export const SNStoreButton = memo(SNStoreButtonComponent); + +const SNStoreLinkComponent: React.FC = () => ( + + {i18n.VISIT_SN_STORE} + +); + +export const SNStoreLink = memo(SNStoreLinkComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts index 90292a35a88dfe..d068b120bd7ce5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts @@ -17,7 +17,7 @@ export const API_URL_LABEL = i18n.translate( export const API_URL_HELPTEXT = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlHelpText', { - defaultMessage: 'Include the full URL', + defaultMessage: 'Include the full URL.', } ); @@ -60,7 +60,7 @@ export const REMEMBER_VALUES_LABEL = i18n.translate( export const REENTER_VALUES_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.reenterValuesLabel', { - defaultMessage: 'You will need to re-authenticate each time you edit the connector', + defaultMessage: 'You must authenticate each time you edit the connector.', } ); @@ -99,34 +99,6 @@ export const TITLE_REQUIRED = i18n.translate( } ); -export const SOURCE_IP_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.sourceIPTitle', - { - defaultMessage: 'Source IPs', - } -); - -export const SOURCE_IP_HELP_TEXT = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.sourceIPHelpText', - { - defaultMessage: 'List of source IPs (comma, or pipe delimited)', - } -); - -export const DEST_IP_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.destinationIPTitle', - { - defaultMessage: 'Destination IPs', - } -); - -export const DEST_IP_HELP_TEXT = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.destIPHelpText', - { - defaultMessage: 'List of destination IPs (comma, or pipe delimited)', - } -); - export const INCIDENT = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.title', { @@ -155,34 +127,6 @@ export const COMMENTS_LABEL = i18n.translate( } ); -export const MALWARE_URL_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.malwareURLTitle', - { - defaultMessage: 'Malware URLs', - } -); - -export const MALWARE_URL_HELP_TEXT = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.malwareURLHelpText', - { - defaultMessage: 'List of malware URLs (comma, or pipe delimited)', - } -); - -export const MALWARE_HASH_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.malwareHashTitle', - { - defaultMessage: 'Malware Hashes', - } -); - -export const MALWARE_HASH_HELP_TEXT = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.malwareHashHelpText', - { - defaultMessage: 'List of malware hashes (comma, or pipe delimited)', - } -); - export const CHOICES_API_ERROR = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.unableToGetChoicesMessage', { @@ -249,25 +193,25 @@ export const INSTALLATION_CALLOUT_TITLE = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.installationCalloutTitle', { defaultMessage: - 'To use this connector, you must first install the Elastic App from the ServiceNow App Store', + 'To use this connector, first install the Elastic app from the ServiceNow app store.', } ); -export const MIGRATION_SUCCESS_TOAST_TITLE = (connectorName: string) => +export const UPDATE_SUCCESS_TOAST_TITLE = (connectorName: string) => i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.migrationSuccessToastTitle', + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.updateSuccessToastTitle', { - defaultMessage: 'Migrated connector {connectorName}', + defaultMessage: '{connectorName} connector updated', values: { connectorName, }, } ); -export const MIGRATION_SUCCESS_TOAST_TEXT = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.installationCalloutText', +export const UPDATE_SUCCESS_TOAST_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.updateCalloutText', { - defaultMessage: 'Connector has been successfully migrated.', + defaultMessage: 'Connector has been updated.', } ); @@ -299,23 +243,16 @@ export const UNKNOWN = i18n.translate( } ); -export const UPDATE_INCIDENT_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.updateIncidentCheckboxLabel', - { - defaultMessage: 'Update incident', - } -); - -export const ON = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.updateIncidentOn', +export const CORRELATION_ID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.correlationID', { - defaultMessage: 'On', + defaultMessage: 'Correlation ID (optional)', } ); -export const OFF = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.updateIncidentOff', +export const CORRELATION_DISPLAY = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.correlationDisplay', { - defaultMessage: 'Off', + defaultMessage: 'Correlation display (optional)', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.test.tsx new file mode 100644 index 00000000000000..2d95bfa85ceb99 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.test.tsx @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { UpdateConnector, Props } from './update_connector'; +import { ServiceNowActionConnector } from './types'; +jest.mock('../../../../common/lib/kibana'); + +const actionConnector: ServiceNowActionConnector = { + secrets: { + username: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.servicenow', + isPreconfigured: false, + name: 'servicenow', + config: { + apiUrl: 'https://test/', + isLegacy: true, + }, +}; + +const mountUpdateConnector = (props: Partial = {}) => { + return mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + isLoading={false} + onConfirm={() => {}} + onCancel={() => {}} + {...props} + /> + ); +}; + +describe('UpdateConnector renders', () => { + it('should render update connector fields', () => { + const wrapper = mountUpdateConnector(); + + expect(wrapper.find('[data-test-subj="snUpdateInstallationCallout"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="updateConnectorForm"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="credentialsApiUrlFromInput"]').exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="connector-servicenow-username-form-input"]').exists() + ).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="connector-servicenow-password-form-input"]').exists() + ).toBeTruthy(); + }); + + it('should disable inputs on loading', () => { + const wrapper = mountUpdateConnector({ isLoading: true }); + expect( + wrapper.find('[data-test-subj="credentialsApiUrlFromInput"]').first().prop('disabled') + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="connector-servicenow-username-form-input"]') + .first() + .prop('disabled') + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="connector-servicenow-password-form-input"]') + .first() + .prop('disabled') + ).toBeTruthy(); + }); + + it('should set inputs as read-only', () => { + const wrapper = mountUpdateConnector({ readOnly: true }); + + expect( + wrapper.find('[data-test-subj="credentialsApiUrlFromInput"]').first().prop('readOnly') + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="connector-servicenow-username-form-input"]') + .first() + .prop('readOnly') + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="connector-servicenow-password-form-input"]') + .first() + .prop('readOnly') + ).toBeTruthy(); + }); + + it('should disable submit button if errors or fields missing', () => { + const wrapper = mountUpdateConnector({ + errors: { apiUrl: ['some error'], username: [], password: [] }, + }); + + expect( + wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().prop('disabled') + ).toBeTruthy(); + + wrapper.setProps({ ...wrapper.props(), errors: { apiUrl: [], username: [], password: [] } }); + expect( + wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().prop('disabled') + ).toBeFalsy(); + + wrapper.setProps({ + ...wrapper.props(), + action: { ...actionConnector, secrets: { ...actionConnector.secrets, username: undefined } }, + }); + expect( + wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().prop('disabled') + ).toBeTruthy(); + }); + + it('should call editActionConfig when editing api url', () => { + const editActionConfig = jest.fn(); + const wrapper = mountUpdateConnector({ editActionConfig }); + + expect(editActionConfig).not.toHaveBeenCalled(); + wrapper + .find('input[data-test-subj="credentialsApiUrlFromInput"]') + .simulate('change', { target: { value: 'newUrl' } }); + expect(editActionConfig).toHaveBeenCalledWith('apiUrl', 'newUrl'); + }); + + it('should call editActionSecrets when editing username or password', () => { + const editActionSecrets = jest.fn(); + const wrapper = mountUpdateConnector({ editActionSecrets }); + + expect(editActionSecrets).not.toHaveBeenCalled(); + wrapper + .find('input[data-test-subj="connector-servicenow-username-form-input"]') + .simulate('change', { target: { value: 'new username' } }); + expect(editActionSecrets).toHaveBeenCalledWith('username', 'new username'); + + wrapper + .find('input[data-test-subj="connector-servicenow-password-form-input"]') + .simulate('change', { target: { value: 'new pass' } }); + + expect(editActionSecrets).toHaveBeenCalledTimes(2); + expect(editActionSecrets).toHaveBeenLastCalledWith('password', 'new pass'); + }); + + it('should confirm the update when submit button clicked', () => { + const onConfirm = jest.fn(); + const wrapper = mountUpdateConnector({ onConfirm }); + + expect(onConfirm).not.toHaveBeenCalled(); + wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().simulate('click'); + expect(onConfirm).toHaveBeenCalled(); + }); + + it('should cancel the update when cancel button clicked', () => { + const onCancel = jest.fn(); + const wrapper = mountUpdateConnector({ onCancel }); + + expect(onCancel).not.toHaveBeenCalled(); + wrapper.find('[data-test-subj="snUpdateInstallationCancel"]').first().simulate('click'); + expect(onCancel).toHaveBeenCalled(); + }); + + it('should show error message if present', () => { + const applicationInfoErrorMsg = 'some application error'; + const wrapper = mountUpdateConnector({ + applicationInfoErrorMsg, + }); + + expect(wrapper.find('[data-test-subj="snApplicationCallout"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="snApplicationCallout"]').first().text()).toContain( + applicationInfoErrorMsg + ); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.tsx new file mode 100644 index 00000000000000..02127eb6ff4f05 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.tsx @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiCallOut, + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiSteps, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ActionConnectorFieldsProps } from '../../../../../public/types'; +import { ServiceNowActionConnector } from './types'; +import { CredentialsApiUrl } from './credentials_api_url'; +import { isFieldInvalid } from './helpers'; +import { ApplicationRequiredCallout } from './application_required_callout'; +import { SNStoreLink } from './sn_store_button'; +import { CredentialsAuth } from './credentials_auth'; + +const title = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.updateFormTitle', + { + defaultMessage: 'Update ServiceNow connector', + } +); + +const step1InstallTitle = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.updateFormInstallTitle', + { + defaultMessage: 'Install the Elastic ServiceNow app', + } +); + +const step2InstanceUrlTitle = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.updateFormUrlTitle', + { + defaultMessage: 'Enter your ServiceNow instance URL', + } +); + +const step3CredentialsTitle = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.updateFormCredentialsTitle', + { + defaultMessage: 'Provide authentication credentials', + } +); + +const cancelButtonText = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.cancelButtonText', + { + defaultMessage: 'Cancel', + } +); + +const confirmButtonText = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.confirmButtonText', + { + defaultMessage: 'Update', + } +); + +const warningMessage = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.warningMessage', + { + defaultMessage: 'This updates all instances of this connector and cannot be reversed.', + } +); + +export interface Props { + action: ActionConnectorFieldsProps['action']; + applicationInfoErrorMsg: string | null; + errors: ActionConnectorFieldsProps['errors']; + isLoading: boolean; + readOnly: boolean; + editActionSecrets: ActionConnectorFieldsProps['editActionSecrets']; + editActionConfig: ActionConnectorFieldsProps['editActionConfig']; + onCancel: () => void; + onConfirm: () => void; +} + +const UpdateConnectorComponent: React.FC = ({ + action, + applicationInfoErrorMsg, + errors, + isLoading, + readOnly, + editActionSecrets, + editActionConfig, + onCancel, + onConfirm, +}) => { + const { apiUrl } = action.config; + const { username, password } = action.secrets; + + const hasErrorsOrEmptyFields = + apiUrl === undefined || + username === undefined || + password === undefined || + isFieldInvalid(apiUrl, errors.apiUrl) || + isFieldInvalid(username, errors.username) || + isFieldInvalid(password, errors.password); + + return ( + + + +

{title}

+
+
+ + } + > + + , + }} + /> + ), + }, + { + title: step2InstanceUrlTitle, + children: ( + + ), + }, + { + title: step3CredentialsTitle, + children: ( + + ), + }, + ]} + /> + + + + {applicationInfoErrorMsg && ( + + )} + + + + + + + + {cancelButtonText} + + + + + {confirmButtonText} + + + + +
+ ); +}; + +export const UpdateConnector = memo(UpdateConnectorComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector_modal.tsx deleted file mode 100644 index b9d660f16dff7a..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector_modal.tsx +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo } from 'react'; -import { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiCallOut, - EuiTextColor, - EuiHorizontalRule, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { ActionConnectorFieldsProps } from '../../../../../public/types'; -import { ServiceNowActionConnector } from './types'; -import { Credentials } from './credentials'; -import { isFieldInvalid } from './helpers'; -import { ApplicationRequiredCallout } from './application_required_callout'; - -const title = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.confirmationModalTitle', - { - defaultMessage: 'Update ServiceNow connector', - } -); - -const cancelButtonText = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.cancelButtonText', - { - defaultMessage: 'Cancel', - } -); - -const confirmButtonText = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.confirmButtonText', - { - defaultMessage: 'Update', - } -); - -const calloutTitle = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.modalCalloutTitle', - { - defaultMessage: - 'The Elastic App from the ServiceNow App Store must be installed prior to running the update.', - } -); - -const warningMessage = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.modalWarningMessage', - { - defaultMessage: 'This will update all instances of this connector. This can not be reversed.', - } -); - -interface Props { - action: ActionConnectorFieldsProps['action']; - applicationInfoErrorMsg: string | null; - errors: ActionConnectorFieldsProps['errors']; - isLoading: boolean; - readOnly: boolean; - editActionSecrets: ActionConnectorFieldsProps['editActionSecrets']; - editActionConfig: ActionConnectorFieldsProps['editActionConfig']; - onCancel: () => void; - onConfirm: () => void; -} - -const UpdateConnectorModalComponent: React.FC = ({ - action, - applicationInfoErrorMsg, - errors, - isLoading, - readOnly, - editActionSecrets, - editActionConfig, - onCancel, - onConfirm, -}) => { - const { apiUrl } = action.config; - const { username, password } = action.secrets; - - const hasErrorsOrEmptyFields = - apiUrl === undefined || - username === undefined || - password === undefined || - isFieldInvalid(apiUrl, errors.apiUrl) || - isFieldInvalid(username, errors.username) || - isFieldInvalid(password, errors.password); - - return ( - - - -

{title}

-
-
- - - - - - - - - - - {warningMessage} - - - - - {applicationInfoErrorMsg && ( - - )} - - - - - {cancelButtonText} - - {confirmButtonText} - - -
- ); -}; - -export const UpdateConnectorModal = memo(UpdateConnectorModalComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index 04f2334f8e8fa6..844f28f0225477 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { ClassNames } from '@emotion/react'; import React, { useState, useEffect } from 'react'; import { EuiInMemoryTable, @@ -24,6 +25,7 @@ import { import { i18n } from '@kbn/i18n'; import { omit } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; +import { withTheme, EuiTheme } from '../../../../../../../../src/plugins/kibana_react/common'; import { loadAllActions, loadActionTypes, deleteActions } from '../../../lib/action_connector_api'; import { hasDeleteActionsCapability, @@ -52,6 +54,33 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../../actions/server/constants/connectors'; +const ConnectorIconTipWithSpacing = withTheme(({ theme }: { theme: EuiTheme }) => { + return ( + + {({ css }) => ( + + )} + + ); +}); + const ActionsConnectorsList: React.FunctionComponent = () => { const { http, @@ -204,23 +233,7 @@ const ActionsConnectorsList: React.FunctionComponent = () => { position="right" /> ) : null} - {showLegacyTooltip && ( - - )} + {showLegacyTooltip && } ); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/mocked_responses.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/mocked_responses.ts index 5f1e30ec52b1d7..57373dbf072695 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/mocked_responses.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/mocked_responses.ts @@ -44,8 +44,7 @@ export const kibanaDeprecations: DomainDeprecationDetails[] = [ correctiveActions: { manualSteps: ['test-step'] }, domainId: 'xpack.spaces', level: 'critical', - message: - 'Disabling the Spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)', + message: 'Sample warning deprecation', }, { title: 'mock-deprecation-title', diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index 3124324d90389d..409436d734011c 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -14,6 +14,7 @@ "requiredPlugins": [ "alerting", "embeddable", + "inspector", "features", "licensing", "triggersActionsUi", diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index ed936fe1649835..124de9a60110cb 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -42,6 +42,7 @@ import { LazySyntheticsPolicyEditExtension, } from '../components/fleet_package'; import { LazySyntheticsCustomAssetsExtension } from '../components/fleet_package/lazy_synthetics_custom_assets_extension'; +import { Start as InspectorPluginStart } from '../../../../../src/plugins/inspector/public'; export interface ClientPluginsSetup { data: DataPublicPluginSetup; @@ -56,6 +57,7 @@ export interface ClientPluginsStart { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; fleet?: FleetStart; observability: ObservabilityPublicStart; + inspector: InspectorPluginStart; } export interface UptimePluginServices extends Partial { diff --git a/x-pack/plugins/uptime/public/apps/uptime_app.tsx b/x-pack/plugins/uptime/public/apps/uptime_app.tsx index 73f228f6215400..991657f2580290 100644 --- a/x-pack/plugins/uptime/public/apps/uptime_app.tsx +++ b/x-pack/plugins/uptime/public/apps/uptime_app.tsx @@ -33,6 +33,7 @@ import { ActionMenu } from '../components/common/header/action_menu'; import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { UptimeIndexPatternContextProvider } from '../contexts/uptime_index_pattern_context'; +import { InspectorContextProvider } from '../../../observability/public'; export interface UptimeAppColors { danger: string; @@ -110,6 +111,7 @@ const Application = (props: UptimeAppProps) => { ...plugins, storage, data: startPlugins.data, + inspector: startPlugins.inspector, triggersActionsUi: startPlugins.triggersActionsUi, observability: startPlugins.observability, }} @@ -126,9 +128,11 @@ const Application = (props: UptimeAppProps) => { className={APP_WRAPPER_CLASS} application={core.application} > - - - + + + + +
diff --git a/x-pack/plugins/uptime/public/apps/uptime_page_template.tsx b/x-pack/plugins/uptime/public/apps/uptime_page_template.tsx index 817bbf9bedcb1b..c6eaeb78a7c1ed 100644 --- a/x-pack/plugins/uptime/public/apps/uptime_page_template.tsx +++ b/x-pack/plugins/uptime/public/apps/uptime_page_template.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import styled from 'styled-components'; import { EuiPageHeaderProps } from '@elastic/eui'; import { CERTIFICATES_ROUTE, OVERVIEW_ROUTE } from '../../common/constants'; @@ -15,6 +15,7 @@ import { useNoDataConfig } from './use_no_data_config'; import { EmptyStateLoading } from '../components/overview/empty_state/empty_state_loading'; import { EmptyStateError } from '../components/overview/empty_state/empty_state_error'; import { useHasData } from '../components/overview/empty_state/use_has_data'; +import { useInspectorContext } from '../../../observability/public'; interface Props { path: string; @@ -39,6 +40,11 @@ export const UptimePageTemplateComponent: React.FC = ({ path, pageHeader, const noDataConfig = useNoDataConfig(); const { loading, error, data } = useHasData(); + const { inspectorAdapters } = useInspectorContext(); + + useEffect(() => { + inspectorAdapters.requests.reset(); + }, [inspectorAdapters.requests]); if (error) { return ; diff --git a/x-pack/plugins/uptime/public/components/certificates/use_cert_search.ts b/x-pack/plugins/uptime/public/components/certificates/use_cert_search.ts index 22531faff2da19..c4379e550b47a0 100644 --- a/x-pack/plugins/uptime/public/components/certificates/use_cert_search.ts +++ b/x-pack/plugins/uptime/public/components/certificates/use_cert_search.ts @@ -7,7 +7,7 @@ import { useSelector } from 'react-redux'; import { useContext } from 'react'; -import { useEsSearch, createEsParams } from '../../../../observability/public'; +import { createEsParams, useEsSearch } from '../../../../observability/public'; import { CertResult, GetCertsParams, Ping } from '../../../common/runtime_types'; @@ -48,13 +48,13 @@ export const useCertSearch = ({ body: searchBody, }); - const { data: result, loading } = useEsSearch(esParams, [ - settings.settings?.heartbeatIndices, - size, - pageIndex, - lastRefresh, - search, - ]); + const { data: result, loading } = useEsSearch( + esParams, + [settings.settings?.heartbeatIndices, size, pageIndex, lastRefresh, search], + { + name: 'getTLSCertificates', + } + ); return result ? { ...processCertsResult(result), loading } : { certs: [], total: 0, loading }; }; diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index c459fe46da9758..21ef3428696e9e 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -18,6 +18,7 @@ import { useGetUrlParams } from '../../../hooks'; import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers'; import { SETTINGS_ROUTE } from '../../../../common/constants'; import { stringifyUrlParams } from '../../../lib/helper/stringify_url_params'; +import { InspectorHeaderLink } from './inspector_header_link'; import { monitorStatusSelector } from '../../../state/selectors'; const ADD_DATA_LABEL = i18n.translate('xpack.uptime.addDataButtonLabel', { @@ -107,6 +108,7 @@ export function ActionMenuContent(): React.ReactElement { > {ADD_DATA_LABEL} + ); } diff --git a/x-pack/plugins/uptime/public/components/common/header/inspector_header_link.tsx b/x-pack/plugins/uptime/public/components/common/header/inspector_header_link.tsx new file mode 100644 index 00000000000000..8f787512aaf9d3 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/common/header/inspector_header_link.tsx @@ -0,0 +1,39 @@ +/* + * 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 { EuiHeaderLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { enableInspectEsQueries, useInspectorContext } from '../../../../../observability/public'; +import { ClientPluginsStart } from '../../../apps/plugin'; + +export function InspectorHeaderLink() { + const { + services: { inspector, uiSettings }, + } = useKibana(); + + const { inspectorAdapters } = useInspectorContext(); + + const isInspectorEnabled = uiSettings?.get(enableInspectEsQueries); + + const inspect = () => { + inspector.open(inspectorAdapters); + }; + + if (!isInspectorEnabled) { + return null; + } + + return ( + + {i18n.translate('xpack.uptime.inspectButtonText', { + defaultMessage: 'Inspect', + })} + + ); +} 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 d066bf416e0838..29c4a852e208bd 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 @@ -33,6 +33,7 @@ describe('ML Flyout component', () => { spy1.mockReturnValue(false); const value = { + isDevMode: true, basePath: '', dateRangeStart: DATE_RANGE_START, dateRangeEnd: DATE_RANGE_END, @@ -48,6 +49,7 @@ describe('ML Flyout component', () => { onClose={onClose} canCreateMLJob={true} /> + uptime/public/state/api/utils.ts ); @@ -57,6 +59,7 @@ describe('ML Flyout component', () => { it('able to create job if valid license is available', async () => { const value = { + isDevMode: true, basePath: '', dateRangeStart: DATE_RANGE_START, dateRangeEnd: DATE_RANGE_END, diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_step_waterfall_metrics.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_step_waterfall_metrics.test.tsx index 8b23d867572f3d..441ede99fd8b40 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_step_waterfall_metrics.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_step_waterfall_metrics.test.tsx @@ -87,7 +87,8 @@ describe('useStepWaterfallMetrics', () => { }, index: 'heartbeat-*', }, - ['heartbeat-*', '44D-444FFF-444-FFF-3333', true] + ['heartbeat-*', '44D-444FFF-444-FFF-3333', true], + { name: 'getWaterfallStepMetrics' } ); expect(result.current).toEqual({ loading: false, diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_step_waterfall_metrics.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_step_waterfall_metrics.ts index cf60f6d7d5567b..ad2762826c91fa 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_step_waterfall_metrics.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_step_waterfall_metrics.ts @@ -57,7 +57,10 @@ export const useStepWaterfallMetrics = ({ checkGroup, hasNavigationRequest, step }, }) : {}, - [heartbeatIndices, checkGroup, hasNavigationRequest] + [heartbeatIndices, checkGroup, hasNavigationRequest], + { + name: 'getWaterfallStepMetrics', + } ); if (!hasNavigationRequest) { diff --git a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx index 3980b4bf9d3da7..835cbb80601429 100644 --- a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx +++ b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx @@ -8,9 +8,10 @@ import React, { useCallback, useState } from 'react'; import { EuiFilterGroup } from '@elastic/eui'; import styled from 'styled-components'; +import { capitalize } from 'lodash'; import { useFilterUpdate } from '../../../hooks/use_filter_update'; import { useSelectedFilters } from '../../../hooks/use_selected_filters'; -import { FieldValueSuggestions } from '../../../../../observability/public'; +import { FieldValueSuggestions, useInspectorContext } from '../../../../../observability/public'; import { SelectedFilters } from './selected_filters'; import { useIndexPattern } from '../../../contexts/uptime_index_pattern_context'; import { useGetUrlParams } from '../../../hooks'; @@ -34,6 +35,8 @@ export const FilterGroup = () => { const { dateRangeStart, dateRangeEnd } = useGetUrlParams(); + const { inspectorAdapters } = useInspectorContext(); + const { filtersList } = useSelectedFilters(); const indexPattern = useIndexPattern(); @@ -67,6 +70,10 @@ export const FilterGroup = () => { filters={[]} cardinalityField="monitor.id" time={{ from: dateRangeStart, to: dateRangeEnd }} + inspector={{ + adapter: inspectorAdapters.requests, + title: 'get' + capitalize(label) + 'FilterValues', + }} /> ))} diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/use_monitor_histogram.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/use_monitor_histogram.ts index e1ef3d9efee896..a3985fe5ccca5f 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/use_monitor_histogram.ts +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/use_monitor_histogram.ts @@ -37,10 +37,13 @@ export const useMonitorHistogram = ({ items }: { items: MonitorSummary[] }) => { monitorIds ); - const { data, loading } = useEsSearch(queryParams, [ - JSON.stringify(monitorIds), - lastRefresh, - ]); + const { data, loading } = useEsSearch( + queryParams, + [JSON.stringify(monitorIds), lastRefresh], + { + name: 'getMonitorDownHistory', + } + ); const histogramBuckets = data?.aggregations?.histogram.buckets ?? []; const simplified = histogramBuckets.map((histogramBucket) => { diff --git a/x-pack/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx index 9f7310b43e5561..54f2110c88bc42 100644 --- a/x-pack/plugins/uptime/public/routes.tsx +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -39,6 +39,8 @@ import { StepDetailPageRightSideItem, } from './pages/synthetics/step_detail_page'; import { UptimePageTemplateComponent } from './apps/uptime_page_template'; +import { apiService } from './state/api/utils'; +import { useInspectorContext } from '../../observability/public'; interface RouteProps { path: string; @@ -178,6 +180,10 @@ const RouteInit: React.FC> = }; export const PageRouter: FC = () => { + const { addInspectorRequest } = useInspectorContext(); + + apiService.addInspectorRequest = addInspectorRequest; + return ( {Routes.map( diff --git a/x-pack/plugins/uptime/public/state/api/snapshot.test.ts b/x-pack/plugins/uptime/public/state/api/snapshot.test.ts index 6c10bd0fa3cb75..38be97d74844f5 100644 --- a/x-pack/plugins/uptime/public/state/api/snapshot.test.ts +++ b/x-pack/plugins/uptime/public/state/api/snapshot.test.ts @@ -18,6 +18,7 @@ describe('snapshot API', () => { get: jest.fn(), fetch: jest.fn(), } as any; + apiService.addInspectorRequest = jest.fn(); fetchMock = jest.spyOn(apiService.http, 'fetch'); mockResponse = { up: 3, down: 12, total: 15 }; }); diff --git a/x-pack/plugins/uptime/public/state/api/utils.ts b/x-pack/plugins/uptime/public/state/api/utils.ts index 91e017292d00f9..d10064f1ff7a13 100644 --- a/x-pack/plugins/uptime/public/state/api/utils.ts +++ b/x-pack/plugins/uptime/public/state/api/utils.ts @@ -9,7 +9,7 @@ import { PathReporter } from 'io-ts/lib/PathReporter'; import { isRight } from 'fp-ts/lib/Either'; import { HttpFetchQuery, HttpSetup } from 'src/core/public'; import * as t from 'io-ts'; -import { startsWith } from 'lodash'; +import { FETCH_STATUS, AddInspectorRequest } from '../../../../observability/public'; function isObject(value: unknown) { const type = typeof value; @@ -43,6 +43,7 @@ export const formatErrors = (errors: t.Errors): string[] => { class ApiService { private static instance: ApiService; private _http!: HttpSetup; + private _addInspectorRequest!: AddInspectorRequest; public get http() { return this._http; @@ -52,6 +53,14 @@ class ApiService { this._http = httpSetup; } + public get addInspectorRequest() { + return this._addInspectorRequest; + } + + public set addInspectorRequest(addInspectorRequest: AddInspectorRequest) { + this._addInspectorRequest = addInspectorRequest; + } + private constructor() {} static getInstance(): ApiService { @@ -63,15 +72,14 @@ class ApiService { } public async get(apiUrl: string, params?: HttpFetchQuery, decodeType?: any, asResponse = false) { - const debugEnabled = - sessionStorage.getItem('uptime_debug') === 'true' && startsWith(apiUrl, '/api/uptime'); - const response = await this._http!.fetch({ path: apiUrl, - query: { ...params, ...(debugEnabled ? { _inspect: true } : {}) }, + query: params, asResponse, }); + this.addInspectorRequest?.({ data: response, status: FETCH_STATUS.SUCCESS, loading: false }); + if (decodeType) { const decoded = decodeType.decode(response); if (isRight(decoded)) { diff --git a/x-pack/plugins/uptime/server/lib/lib.ts b/x-pack/plugins/uptime/server/lib/lib.ts index 9b8ea6b98c8be4..eb2ad9ce21b9e1 100644 --- a/x-pack/plugins/uptime/server/lib/lib.ts +++ b/x-pack/plugins/uptime/server/lib/lib.ts @@ -18,6 +18,9 @@ import { UMLicenseCheck } from './domains'; import { UptimeRequests } from './requests'; import { savedObjectsAdapter } from './saved_objects'; import { ESSearchResponse } from '../../../../../src/core/types/elasticsearch'; +import { RequestStatus } from '../../../../../src/plugins/inspector'; +import { getInspectResponse } from '../../../observability/server'; +import { InspectResponse } from '../../../observability/typings/common'; export interface UMDomainLibs { requests: UptimeRequests; @@ -45,6 +48,8 @@ export interface CountResponse { export type UptimeESClient = ReturnType; +export const inspectableEsQueriesMap = new WeakMap(); + export function createUptimeESClient({ esClient, request, @@ -59,7 +64,8 @@ export function createUptimeESClient({ return { baseESClient: esClient, async search( - params: TParams + params: TParams, + operationName?: string ): Promise<{ body: ESSearchResponse }> { let res: any; let esError: any; @@ -70,11 +76,33 @@ export function createUptimeESClient({ const esParams = { index: dynamicSettings!.heartbeatIndices, ...params }; const startTime = process.hrtime(); + const startTimeNow = Date.now(); + + let esRequestStatus: RequestStatus = RequestStatus.PENDING; + try { res = await esClient.search(esParams); + esRequestStatus = RequestStatus.OK; } catch (e) { esError = e; + esRequestStatus = RequestStatus.ERROR; } + + const inspectableEsQueries = inspectableEsQueriesMap.get(request!); + if (inspectableEsQueries) { + inspectableEsQueries.push( + getInspectResponse({ + esError, + esRequestParams: esParams, + esRequestStatus, + esResponse: res.body, + kibanaRequest: request!, + operationName: operationName ?? '', + startTime: startTimeNow, + }) + ); + } + if (_inspect && request) { debugESCall({ startTime, request, esError, operationName: 'search', params: esParams }); } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts index 107a0f29e55fac..600a335effe2ce 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts @@ -87,7 +87,7 @@ export const getPingHistogram: UMElasticsearchQueryFn { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts index aef01f29f4d576..ee4e3eb96eb5af 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts @@ -43,9 +43,12 @@ export const getSnapshotCount: UMElasticsearchQueryFn => { - const { body: res } = await context.search({ - body: statusCountBody(await context.dateAndCustomFilters(), context), - }); + const { body: res } = await context.search( + { + body: statusCountBody(await context.dateAndCustomFilters(), context), + }, + 'geSnapshotCount' + ); return ( (res.aggregations?.counts?.value as Snapshot) ?? { diff --git a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts index 179e9e809e59b9..d0d8e61d02181a 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts @@ -40,7 +40,7 @@ const query = async (queryContext: QueryContext, searchAfter: any, size: number) body, }; - const response = await queryContext.search(params); + const response = await queryContext.search(params, 'getMonitorList-potentialMatches'); return response; }; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts index 0bc503093f1315..84c170363b1eef 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts @@ -43,8 +43,8 @@ export class QueryContext { this.query = query; } - async search(params: TParams) { - return this.callES.search(params); + async search(params: TParams, operationName?: string) { + return this.callES.search(params, operationName); } async count(params: any): Promise { diff --git a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts index 3ba2a90d076356..f8357e80665739 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts @@ -165,5 +165,5 @@ export const query = async ( }, }; - return await queryContext.search(params); + return await queryContext.search(params, 'getMonitorList-refinePotentialMatches'); }; diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts index 29a7a06f1530a2..66f6d597344b6b 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; import { API_URLS } from '../../../common/constants'; @@ -13,11 +12,7 @@ import { API_URLS } from '../../../common/constants'; export const createGetIndexStatusRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', path: API_URLS.INDEX_STATUS, - validate: { - query: schema.object({ - _inspect: schema.maybe(schema.boolean()), - }), - }, + validate: {}, handler: async ({ uptimeEsClient }): Promise => { return await libs.requests.getIndexStatus({ uptimeEsClient }); }, diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts index 36bc5a80ef47a1..df8463786449ba 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts @@ -21,7 +21,6 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({ statusFilter: schema.maybe(schema.string()), query: schema.maybe(schema.string()), pageSize: schema.number(), - _inspect: schema.maybe(schema.boolean()), }), }, options: { diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts index 77f265d0b81e87..de102f153d650d 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts @@ -18,7 +18,6 @@ export const createGetMonitorLocationsRoute: UMRestApiRouteFactory = (libs: UMSe monitorId: schema.string(), dateStart: schema.string(), dateEnd: schema.string(), - _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts index 94b50386ac2161..ac5133fbb7b4e0 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts @@ -18,7 +18,6 @@ export const createGetStatusBarRoute: UMRestApiRouteFactory = (libs: UMServerLib monitorId: schema.string(), dateStart: schema.string(), dateEnd: schema.string(), - _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts index 50712153a5fea5..64e9ed504e7cd7 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts @@ -18,7 +18,6 @@ export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServ monitorId: schema.string(), dateStart: schema.maybe(schema.string()), dateEnd: schema.maybe(schema.string()), - _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, context, request }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts index e94198ee4e0632..8dd9fbb7adb001 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts @@ -19,7 +19,6 @@ export const createGetMonitorDurationRoute: UMRestApiRouteFactory = (libs: UMSer monitorId: schema.string(), dateStart: schema.string(), dateEnd: schema.string(), - _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts index db111390cfaf78..439975a0f52158 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts @@ -21,7 +21,6 @@ export const createGetPingHistogramRoute: UMRestApiRouteFactory = (libs: UMServe filters: schema.maybe(schema.string()), bucketSize: schema.maybe(schema.string()), query: schema.maybe(schema.string()), - _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts index abb2c85f9ea0c7..2be838c5e8658b 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts @@ -24,7 +24,6 @@ export const createGetPingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) = size: schema.maybe(schema.number()), sort: schema.maybe(schema.string()), status: schema.maybe(schema.string()), - _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request, response }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshot_blocks.ts b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshot_blocks.ts index 3127c34590ef5c..4b06a13d29f4ee 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshot_blocks.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshot_blocks.ts @@ -22,9 +22,6 @@ export const createJourneyScreenshotBlocksRoute: UMRestApiRouteFactory = (libs: body: schema.object({ hashes: schema.arrayOf(schema.string()), }), - query: schema.object({ - _inspect: schema.maybe(schema.boolean()), - }), }, handler: async ({ request, response, uptimeEsClient }) => { const { hashes: blockIds } = request.body; diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts index 5f0825279ecfae..3e71051816d30f 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts @@ -26,10 +26,6 @@ export const createJourneyScreenshotRoute: UMRestApiRouteFactory = (libs: UMServ params: schema.object({ checkGroup: schema.string(), stepIndex: schema.number(), - _inspect: schema.maybe(schema.boolean()), - }), - query: schema.object({ - _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request, response }) => { diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts b/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts index 284feda2c662b6..7c3dcdfbe845c1 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts @@ -22,7 +22,6 @@ export const createJourneyRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => syntheticEventTypes: schema.maybe( schema.oneOf([schema.arrayOf(schema.string()), schema.string()]) ), - _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request, response }): Promise => { @@ -59,7 +58,6 @@ export const createJourneyFailedStepsRoute: UMRestApiRouteFactory = (libs: UMSer validate: { query: schema.object({ checkGroups: schema.arrayOf(schema.string()), - _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request, response }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts index 67b106fdf68140..2fae13db7fa0df 100644 --- a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts +++ b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts @@ -19,7 +19,6 @@ export const createGetSnapshotCount: UMRestApiRouteFactory = (libs: UMServerLibs dateRangeEnd: schema.string(), filters: schema.maybe(schema.string()), query: schema.maybe(schema.string()), - _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics/last_successful_step.ts b/x-pack/plugins/uptime/server/rest_api/synthetics/last_successful_step.ts index cb90de50e25108..5d1407a8679c8f 100644 --- a/x-pack/plugins/uptime/server/rest_api/synthetics/last_successful_step.ts +++ b/x-pack/plugins/uptime/server/rest_api/synthetics/last_successful_step.ts @@ -22,7 +22,6 @@ export const createLastSuccessfulStepRoute: UMRestApiRouteFactory = (libs: UMSer monitorId: schema.string(), stepIndex: schema.number(), timestamp: schema.string(), - _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request, response }) => { diff --git a/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts b/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts index 088cf494efbf73..ec7de05dd2cf1f 100644 --- a/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts +++ b/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts @@ -22,7 +22,6 @@ export const createLogPageViewRoute: UMRestApiRouteFactory = () => ({ autoRefreshEnabled: schema.boolean(), autorefreshInterval: schema.number(), refreshTelemetryHistory: schema.maybe(schema.boolean()), - _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ savedObjectsClient, uptimeEsClient, request }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts index 24e501a1bddb83..ddde993cc9c705 100644 --- a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts +++ b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts @@ -6,10 +6,11 @@ */ import { UMKibanaRouteWrapper } from './types'; -import { createUptimeESClient } from '../lib/lib'; +import { createUptimeESClient, inspectableEsQueriesMap } from '../lib/lib'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { KibanaResponse } from '../../../../../src/core/server/http/router'; +import { enableInspectEsQueries } from '../../../observability/common'; export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute) => ({ ...uptimeRoute, @@ -20,11 +21,18 @@ export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute) => ({ const { client: esClient } = context.core.elasticsearch; const { client: savedObjectsClient } = context.core.savedObjects; + const isInspectorEnabled = await context.core.uiSettings.client.get( + enableInspectEsQueries + ); + const uptimeEsClient = createUptimeESClient({ request, savedObjectsClient, esClient: esClient.asCurrentUser, }); + if (isInspectorEnabled) { + inspectableEsQueriesMap.set(request, []); + } const res = await uptimeRoute.handler({ uptimeEsClient, @@ -41,6 +49,7 @@ export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute) => ({ return response.ok({ body: { ...res, + ...(isInspectorEnabled ? { _inspect: inspectableEsQueriesMap.get(request) } : {}), }, }); }, diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 6cb80d6d4b74d6..e31b12cd0115d7 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -36,7 +36,6 @@ const onlyNotInCoverageTests = [ require.resolve('../test/case_api_integration/security_and_spaces/config_basic.ts'), require.resolve('../test/case_api_integration/security_and_spaces/config_trial.ts'), require.resolve('../test/case_api_integration/spaces_only/config.ts'), - require.resolve('../test/case_api_integration/security_only/config.ts'), require.resolve('../test/apm_api_integration/basic/config.ts'), require.resolve('../test/apm_api_integration/trial/config.ts'), require.resolve('../test/apm_api_integration/rules/config.ts'), @@ -71,11 +70,8 @@ const onlyNotInCoverageTests = [ require.resolve('../test/spaces_api_integration/security_and_spaces/config_basic.ts'), require.resolve('../test/saved_object_api_integration/security_and_spaces/config_trial.ts'), require.resolve('../test/saved_object_api_integration/security_and_spaces/config_basic.ts'), - require.resolve('../test/saved_object_api_integration/security_only/config_trial.ts'), - require.resolve('../test/saved_object_api_integration/security_only/config_basic.ts'), require.resolve('../test/saved_object_api_integration/spaces_only/config.ts'), require.resolve('../test/ui_capabilities/security_and_spaces/config.ts'), - require.resolve('../test/ui_capabilities/security_only/config.ts'), require.resolve('../test/ui_capabilities/spaces_only/config.ts'), require.resolve('../test/upgrade_assistant_integration/config.js'), require.resolve('../test/licensing_plugin/config.ts'), diff --git a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts index 337dc65b532ff3..6e2025a7fa2ca2 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts @@ -217,6 +217,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(finalRawResponse?.percentileThresholdValue).to.be(1309695.875); expect(finalRawResponse?.errorHistogram.length).to.be(101); expect(finalRawResponse?.overallHistogram.length).to.be(101); + expect(finalRawResponse?.fieldStats.length).to.be(26); expect(finalRawResponse?.failedTransactionsCorrelations.length).to.eql( 30, @@ -227,6 +228,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { 'Fetched 95th percentile value of 1309695.875 based on 1244 documents.', 'Identified 68 fieldCandidates.', 'Identified correlations for 68 fields out of 68 candidates.', + 'Identified 26 fields to sample for field statistics.', + 'Retrieved field statistics for 26 fields out of 26 fields.', 'Identified 30 significant correlations relating to failed transactions.', ]); @@ -243,6 +246,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(typeof correlation?.normalizedScore).to.be('number'); expect(typeof correlation?.failurePercentage).to.be('number'); expect(typeof correlation?.successPercentage).to.be('number'); + + const fieldStats = finalRawResponse?.fieldStats[0]; + expect(typeof fieldStats).to.be('object'); + expect(fieldStats.topValues.length).to.greaterThan(0); + expect(fieldStats.topValuesSampleSize).to.greaterThan(0); }); }); } diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency.ts b/x-pack/test/apm_api_integration/tests/correlations/latency.ts index 496d4966efb864..99aee770c625d3 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/latency.ts @@ -236,6 +236,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(typeof finalRawResponse?.took).to.be('number'); expect(finalRawResponse?.percentileThresholdValue).to.be(1309695.875); expect(finalRawResponse?.overallHistogram.length).to.be(101); + expect(finalRawResponse?.fieldStats.length).to.be(12); expect(finalRawResponse?.latencyCorrelations.length).to.eql( 13, @@ -250,15 +251,23 @@ export default function ApiTest({ getService }: FtrProviderContext) { 'Identified 379 fieldValuePairs.', 'Loaded fractions and totalDocCount of 1244.', 'Identified 13 significant correlations out of 379 field/value pairs.', + 'Identified 12 fields to sample for field statistics.', + 'Retrieved field statistics for 12 fields out of 12 fields.', ]); const correlation = finalRawResponse?.latencyCorrelations[0]; + expect(typeof correlation).to.be('object'); expect(correlation?.fieldName).to.be('transaction.result'); expect(correlation?.fieldValue).to.be('success'); expect(correlation?.correlation).to.be(0.6275246559191225); expect(correlation?.ksTest).to.be(4.806503252860024e-13); expect(correlation?.histogram.length).to.be(101); + + const fieldStats = finalRawResponse?.fieldStats[0]; + expect(typeof fieldStats).to.be('object'); + expect(fieldStats.topValues.length).to.greaterThan(0); + expect(fieldStats.topValuesSampleSize).to.greaterThan(0); }); } ); diff --git a/x-pack/test/case_api_integration/common/lib/authentication/roles.ts b/x-pack/test/case_api_integration/common/lib/authentication/roles.ts index 9ded86ef6524fc..8e67d8dfc15261 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/roles.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/roles.ts @@ -276,17 +276,3 @@ export const observabilityOnlyReadSpacesAll: Role = { ], }, }; - -/** - * These roles are specifically for the security_only tests where the spaces plugin is disabled. Most of the roles (except - * for noKibanaPrivileges) have spaces: ['*'] effectively giving it access to the default space since no other spaces - * will exist when the spaces plugin is disabled. - */ -export const rolesDefaultSpace = [ - noKibanaPrivileges, - globalRead, - securitySolutionOnlyAllSpacesAll, - securitySolutionOnlyReadSpacesAll, - observabilityOnlyAllSpacesAll, - observabilityOnlyReadSpacesAll, -]; diff --git a/x-pack/test/case_api_integration/common/lib/authentication/users.ts b/x-pack/test/case_api_integration/common/lib/authentication/users.ts index d10e932f924053..f848a37c87e490 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/users.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/users.ts @@ -132,18 +132,3 @@ export const obsSecReadSpacesAll: User = { password: 'obs_sec_read', roles: [securitySolutionOnlyReadSpacesAll.name, observabilityOnlyReadSpacesAll.name], }; - -/** - * These users are for the security_only tests because most of them have access to the default space instead of 'space1' - */ -export const usersDefaultSpace = [ - superUser, - secOnlySpacesAll, - secOnlyReadSpacesAll, - obsOnlySpacesAll, - obsOnlyReadSpacesAll, - obsSecSpacesAll, - obsSecReadSpacesAll, - globalRead, - noKibanaPrivileges, -]; diff --git a/x-pack/test/case_api_integration/security_only/config.ts b/x-pack/test/case_api_integration/security_only/config.ts deleted file mode 100644 index 5946b8d25b4641..00000000000000 --- a/x-pack/test/case_api_integration/security_only/config.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createTestConfig } from '../common/config'; - -// eslint-disable-next-line import/no-default-export -export default createTestConfig('security_only', { - disabledPlugins: ['spaces'], - license: 'trial', - ssl: true, - testFiles: [require.resolve('./tests/trial')], -}); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts deleted file mode 100644 index f55427d13b32bf..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -import { getPostCaseRequest, postCommentAlertReq } from '../../../../common/lib/mock'; -import { - createCase, - createComment, - getCasesByAlert, - deleteAllCaseItems, -} from '../../../../common/lib/utils'; -import { - globalRead, - noKibanaPrivileges, - obsOnlyReadSpacesAll, - obsSecSpacesAll, - obsSecReadSpacesAll, - secOnlyReadSpacesAll, - superUser, -} from '../../../../common/lib/authentication/users'; -import { - obsOnlyDefaultSpaceAuth, - secOnlyDefaultSpaceAuth, - superUserDefaultSpaceAuth, - obsSecDefaultSpaceAuth, -} from '../../../utils'; -import { validateCasesFromAlertIDResponse } from '../../../../common/lib/validation'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const es = getService('es'); - - describe('get_cases using alertID', () => { - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - it('should return the correct cases info', async () => { - const [case1, case2, case3] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - obsOnlyDefaultSpaceAuth - ), - ]); - - await Promise.all([ - createComment({ - supertest: supertestWithoutAuth, - caseId: case1.id, - params: postCommentAlertReq, - auth: secOnlyDefaultSpaceAuth, - }), - createComment({ - supertest: supertestWithoutAuth, - caseId: case2.id, - params: postCommentAlertReq, - auth: secOnlyDefaultSpaceAuth, - }), - createComment({ - supertest: supertestWithoutAuth, - caseId: case3.id, - params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth: obsOnlyDefaultSpaceAuth, - }), - ]); - - for (const scenario of [ - { - user: globalRead, - cases: [case1, case2, case3], - }, - { - user: superUser, - cases: [case1, case2, case3], - }, - { user: secOnlyReadSpacesAll, cases: [case1, case2] }, - { user: obsOnlyReadSpacesAll, cases: [case3] }, - { - user: obsSecReadSpacesAll, - cases: [case1, case2, case3], - }, - ]) { - const cases = await getCasesByAlert({ - supertest: supertestWithoutAuth, - // cast because the official type is string | string[] but the ids will always be a single value in the tests - alertID: postCommentAlertReq.alertId as string, - auth: { - user: scenario.user, - space: null, - }, - }); - - expect(cases.length).to.eql(scenario.cases.length); - validateCasesFromAlertIDResponse(cases, scenario.cases); - } - }); - - it(`User ${ - noKibanaPrivileges.username - } with role(s) ${noKibanaPrivileges.roles.join()} - should not get cases`, async () => { - const caseInfo = await createCase(supertest, getPostCaseRequest(), 200, { - user: superUser, - space: null, - }); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentAlertReq, - auth: superUserDefaultSpaceAuth, - }); - - await getCasesByAlert({ - supertest: supertestWithoutAuth, - alertID: postCommentAlertReq.alertId as string, - auth: { user: noKibanaPrivileges, space: null }, - expectedHttpCode: 403, - }); - }); - - it('should return a 404 when attempting to access a space', async () => { - const [case1, case2] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecDefaultSpaceAuth), - createCase( - supertestWithoutAuth, - { ...getPostCaseRequest(), owner: 'observabilityFixture' }, - 200, - obsSecDefaultSpaceAuth - ), - ]); - - await Promise.all([ - createComment({ - supertest: supertestWithoutAuth, - caseId: case1.id, - params: postCommentAlertReq, - auth: obsSecDefaultSpaceAuth, - }), - createComment({ - supertest: supertestWithoutAuth, - caseId: case2.id, - params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth: obsSecDefaultSpaceAuth, - }), - ]); - - await getCasesByAlert({ - supertest: supertestWithoutAuth, - alertID: postCommentAlertReq.alertId as string, - auth: { user: obsSecSpacesAll, space: 'space1' }, - query: { owner: 'securitySolutionFixture' }, - expectedHttpCode: 404, - }); - }); - - it('should respect the owner filter when have permissions', async () => { - const [case1, case2] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecDefaultSpaceAuth), - createCase( - supertestWithoutAuth, - { ...getPostCaseRequest(), owner: 'observabilityFixture' }, - 200, - obsSecDefaultSpaceAuth - ), - ]); - - await Promise.all([ - createComment({ - supertest: supertestWithoutAuth, - caseId: case1.id, - params: postCommentAlertReq, - auth: obsSecDefaultSpaceAuth, - }), - createComment({ - supertest: supertestWithoutAuth, - caseId: case2.id, - params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth: obsSecDefaultSpaceAuth, - }), - ]); - - const cases = await getCasesByAlert({ - supertest: supertestWithoutAuth, - alertID: postCommentAlertReq.alertId as string, - auth: obsSecDefaultSpaceAuth, - query: { owner: 'securitySolutionFixture' }, - }); - - expect(cases).to.eql([{ id: case1.id, title: case1.title }]); - }); - - it('should return the correct cases info when the owner query parameter contains unprivileged values', async () => { - const [case1, case2] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecDefaultSpaceAuth), - createCase( - supertestWithoutAuth, - { ...getPostCaseRequest(), owner: 'observabilityFixture' }, - 200, - obsSecDefaultSpaceAuth - ), - ]); - - await Promise.all([ - createComment({ - supertest: supertestWithoutAuth, - caseId: case1.id, - params: postCommentAlertReq, - auth: obsSecDefaultSpaceAuth, - }), - createComment({ - supertest: supertestWithoutAuth, - caseId: case2.id, - params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth: obsSecDefaultSpaceAuth, - }), - ]); - - const cases = await getCasesByAlert({ - supertest: supertestWithoutAuth, - alertID: postCommentAlertReq.alertId as string, - auth: secOnlyDefaultSpaceAuth, - // The secOnlyDefaultSpace user does not have permissions for observability cases, so it should only return the security solution one - query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, - }); - - expect(cases).to.eql([{ id: case1.id, title: case1.title }]); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts deleted file mode 100644 index 9ece177b214914..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -import { getPostCaseRequest } from '../../../../common/lib/mock'; -import { - deleteCasesByESQuery, - deleteCasesUserActions, - deleteComments, - createCase, - deleteCases, - getCase, -} from '../../../../common/lib/utils'; -import { - secOnlySpacesAll, - secOnlyReadSpacesAll, - globalRead, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - noKibanaPrivileges, -} from '../../../../common/lib/authentication/users'; -import { - obsOnlyDefaultSpaceAuth, - secOnlyDefaultSpaceAuth, - superUserDefaultSpaceAuth, -} from '../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('es'); - - describe('delete_cases', () => { - afterEach(async () => { - await deleteCasesByESQuery(es); - await deleteComments(es); - await deleteCasesUserActions(es); - }); - - it('User: security solution only - should delete a case', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - secOnlyDefaultSpaceAuth - ); - - await deleteCases({ - supertest: supertestWithoutAuth, - caseIDs: [postedCase.id], - expectedHttpCode: 204, - auth: secOnlyDefaultSpaceAuth, - }); - }); - - it('User: security solution only - should NOT delete a case of different owner', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - secOnlyDefaultSpaceAuth - ); - - await deleteCases({ - supertest: supertestWithoutAuth, - caseIDs: [postedCase.id], - expectedHttpCode: 403, - auth: obsOnlyDefaultSpaceAuth, - }); - }); - - it('should get an error if the user has not permissions to all requested cases', async () => { - const caseSec = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - secOnlyDefaultSpaceAuth - ); - - const caseObs = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - obsOnlyDefaultSpaceAuth - ); - - await deleteCases({ - supertest: supertestWithoutAuth, - caseIDs: [caseSec.id, caseObs.id], - expectedHttpCode: 403, - auth: obsOnlyDefaultSpaceAuth, - }); - - // Cases should have not been deleted. - await getCase({ - supertest: supertestWithoutAuth, - caseId: caseSec.id, - expectedHttpCode: 200, - auth: superUserDefaultSpaceAuth, - }); - - await getCase({ - supertest: supertestWithoutAuth, - caseId: caseObs.id, - expectedHttpCode: 200, - auth: superUserDefaultSpaceAuth, - }); - }); - - for (const user of [ - globalRead, - secOnlyReadSpacesAll, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - noKibanaPrivileges, - ]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT delete a case`, async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - await deleteCases({ - supertest: supertestWithoutAuth, - caseIDs: [postedCase.id], - expectedHttpCode: 403, - auth: { user, space: null }, - }); - }); - } - - it('should return a 404 when attempting to access a space', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - await deleteCases({ - supertest: supertestWithoutAuth, - caseIDs: [postedCase.id], - expectedHttpCode: 404, - auth: { user: secOnlySpacesAll, space: 'space1' }, - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts deleted file mode 100644 index 711eccbe162786..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { getPostCaseRequest } from '../../../../common/lib/mock'; -import { - deleteAllCaseItems, - ensureSavedObjectIsAuthorized, - findCases, - createCase, -} from '../../../../common/lib/utils'; -import { - secOnlySpacesAll, - obsOnlyReadSpacesAll, - secOnlyReadSpacesAll, - noKibanaPrivileges, - superUser, - globalRead, - obsSecReadSpacesAll, -} from '../../../../common/lib/authentication/users'; -import { - obsOnlyDefaultSpaceAuth, - obsSecDefaultSpaceAuth, - secOnlyDefaultSpaceAuth, - superUserDefaultSpaceAuth, -} from '../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const es = getService('es'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - describe('find_cases', () => { - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should return the correct cases', async () => { - await Promise.all([ - // Create case owned by the security solution user - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - secOnlyDefaultSpaceAuth - ), - // Create case owned by the observability user - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - obsOnlyDefaultSpaceAuth - ), - ]); - - for (const scenario of [ - { - user: globalRead, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, - { - user: superUser, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, - { - user: secOnlyReadSpacesAll, - numberOfExpectedCases: 1, - owners: ['securitySolutionFixture'], - }, - { - user: obsOnlyReadSpacesAll, - numberOfExpectedCases: 1, - owners: ['observabilityFixture'], - }, - { - user: obsSecReadSpacesAll, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, - ]) { - const res = await findCases({ - supertest: supertestWithoutAuth, - auth: { - user: scenario.user, - space: null, - }, - }); - - ensureSavedObjectIsAuthorized(res.cases, scenario.numberOfExpectedCases, scenario.owners); - } - }); - - it(`User ${ - noKibanaPrivileges.username - } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT read a case`, async () => { - await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); - - await findCases({ - supertest: supertestWithoutAuth, - auth: { - user: noKibanaPrivileges, - space: null, - }, - expectedHttpCode: 403, - }); - }); - - it('should return a 404 when attempting to access a space', async () => { - await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); - - await findCases({ - supertest: supertestWithoutAuth, - auth: { user: secOnlySpacesAll, space: 'space1' }, - expectedHttpCode: 404, - }); - }); - - it('should return the correct cases when trying to exploit RBAC through the search query parameter', async () => { - await Promise.all([ - // super user creates a case with owner securitySolutionFixture - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth), - // super user creates a case with owner observabilityFixture - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - superUserDefaultSpaceAuth - ), - ]); - - const res = await findCases({ - supertest: supertestWithoutAuth, - query: { - search: 'securitySolutionFixture observabilityFixture', - searchFields: 'owner', - }, - auth: secOnlyDefaultSpaceAuth, - }); - - ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); - }); - - // This test is to prevent a future developer to add the filter attribute without taking into consideration - // the authorizationFilter produced by the cases authorization class - it('should NOT allow to pass a filter query parameter', async () => { - await supertest - .get( - `${CASES_URL}/_find?sortOrder=asc&filter=cases.attributes.owner:"observabilityFixture"` - ) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - }); - - // This test ensures that the user is not allowed to define the namespaces query param - // so she cannot search across spaces - it('should NOT allow to pass a namespaces query parameter', async () => { - await supertest - .get(`${CASES_URL}/_find?sortOrder=asc&namespaces[0]=*`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - - await supertest - .get(`${CASES_URL}/_find?sortOrder=asc&namespaces=*`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - }); - - it('should NOT allow to pass a non supported query parameter', async () => { - await supertest - .get(`${CASES_URL}/_find?notExists=papa`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - }); - - it('should respect the owner filter when having permissions', async () => { - await Promise.all([ - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - obsSecDefaultSpaceAuth - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - obsOnlyDefaultSpaceAuth - ), - ]); - - const res = await findCases({ - supertest: supertestWithoutAuth, - query: { - owner: 'securitySolutionFixture', - searchFields: 'owner', - }, - auth: obsSecDefaultSpaceAuth, - }); - - ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); - }); - - it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { - await Promise.all([ - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - obsSecDefaultSpaceAuth - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - obsSecDefaultSpaceAuth - ), - ]); - - // User with permissions only to security solution request cases from observability - const res = await findCases({ - supertest: supertestWithoutAuth, - query: { - owner: ['securitySolutionFixture', 'observabilityFixture'], - }, - auth: secOnlyDefaultSpaceAuth, - }); - - // Only security solution cases are being returned - ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts deleted file mode 100644 index 3bdb4c5ed310e3..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api'; -import { postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; -import { - deleteCasesByESQuery, - createCase, - getCase, - createComment, - removeServerGeneratedPropertiesFromSavedObject, -} from '../../../../common/lib/utils'; -import { - secOnlySpacesAll, - obsOnlySpacesAll, - globalRead, - superUser, - secOnlyReadSpacesAll, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - noKibanaPrivileges, - obsSecSpacesAll, -} from '../../../../common/lib/authentication/users'; -import { getUserInfo } from '../../../../common/lib/authentication'; -import { secOnlyDefaultSpaceAuth, superUserDefaultSpaceAuth } from '../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('es'); - - describe('get_case', () => { - afterEach(async () => { - await deleteCasesByESQuery(es); - }); - - it('should get a case', async () => { - const newCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - for (const user of [ - globalRead, - superUser, - secOnlySpacesAll, - secOnlyReadSpacesAll, - obsSecSpacesAll, - obsSecReadSpacesAll, - ]) { - const theCase = await getCase({ - supertest: supertestWithoutAuth, - caseId: newCase.id, - auth: { user, space: null }, - }); - - expect(theCase.owner).to.eql('securitySolutionFixture'); - } - }); - - it('should get a case with comments', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - secOnlyDefaultSpaceAuth - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - expectedHttpCode: 200, - auth: secOnlyDefaultSpaceAuth, - }); - - const theCase = await getCase({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - includeComments: true, - auth: secOnlyDefaultSpaceAuth, - }); - - const comment = removeServerGeneratedPropertiesFromSavedObject( - theCase.comments![0] as AttributesTypeUser - ); - - expect(theCase.comments?.length).to.eql(1); - expect(comment).to.eql({ - type: postCommentUserReq.type, - comment: postCommentUserReq.comment, - associationType: 'case', - created_by: getUserInfo(secOnlySpacesAll), - pushed_at: null, - pushed_by: null, - updated_by: null, - owner: 'securitySolutionFixture', - }); - }); - - it('should not get a case when the user does not have access to owner', async () => { - const newCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - for (const user of [noKibanaPrivileges, obsOnlySpacesAll, obsOnlyReadSpacesAll]) { - await getCase({ - supertest: supertestWithoutAuth, - caseId: newCase.id, - expectedHttpCode: 403, - auth: { user, space: null }, - }); - } - }); - - it('should return a 404 when attempting to access a space', async () => { - const newCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - await getCase({ - supertest: supertestWithoutAuth, - caseId: newCase.id, - expectedHttpCode: 404, - auth: { user: secOnlySpacesAll, space: 'space1' }, - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts deleted file mode 100644 index bfab3fce7adbe8..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -import { getPostCaseRequest, postCaseReq } from '../../../../common/lib/mock'; -import { - deleteAllCaseItems, - createCase, - updateCase, - findCases, - getAuthWithSuperUser, -} from '../../../../common/lib/utils'; - -import { - globalRead, - noKibanaPrivileges, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - secOnlySpacesAll, - secOnlyReadSpacesAll, - superUser, -} from '../../../../common/lib/authentication/users'; -import { - obsOnlyDefaultSpaceAuth, - secOnlyDefaultSpaceAuth, - superUserDefaultSpaceAuth, -} from '../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const es = getService('es'); - - describe('patch_cases', () => { - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - it('should update a case when the user has the correct permissions', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - postCaseReq, - 200, - secOnlyDefaultSpaceAuth - ); - - const patchedCases = await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: 'new title', - }, - ], - }, - auth: secOnlyDefaultSpaceAuth, - }); - - expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); - }); - - it('should update multiple cases when the user has the correct permissions', async () => { - const [case1, case2, case3] = await Promise.all([ - createCase(supertestWithoutAuth, postCaseReq, 200, superUserDefaultSpaceAuth), - createCase(supertestWithoutAuth, postCaseReq, 200, superUserDefaultSpaceAuth), - createCase(supertestWithoutAuth, postCaseReq, 200, superUserDefaultSpaceAuth), - ]); - - const patchedCases = await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: case1.id, - version: case1.version, - title: 'new title', - }, - { - id: case2.id, - version: case2.version, - title: 'new title', - }, - { - id: case3.id, - version: case3.version, - title: 'new title', - }, - ], - }, - auth: secOnlyDefaultSpaceAuth, - }); - - expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); - expect(patchedCases[1].owner).to.eql('securitySolutionFixture'); - expect(patchedCases[2].owner).to.eql('securitySolutionFixture'); - }); - - it('should not update a case when the user does not have the correct ownership', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - obsOnlyDefaultSpaceAuth - ); - - await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: 'new title', - }, - ], - }, - auth: secOnlyDefaultSpaceAuth, - expectedHttpCode: 403, - }); - }); - - it('should not update any cases when the user does not have the correct ownership', async () => { - const [case1, case2, case3] = await Promise.all([ - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - superUserDefaultSpaceAuth - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - superUserDefaultSpaceAuth - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - superUserDefaultSpaceAuth - ), - ]); - - await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: case1.id, - version: case1.version, - title: 'new title', - }, - { - id: case2.id, - version: case2.version, - title: 'new title', - }, - { - id: case3.id, - version: case3.version, - title: 'new title', - }, - ], - }, - auth: secOnlyDefaultSpaceAuth, - expectedHttpCode: 403, - }); - - const resp = await findCases({ supertest, auth: getAuthWithSuperUser(null) }); - expect(resp.cases.length).to.eql(3); - // the update should have failed and none of the title should have been changed - expect(resp.cases[0].title).to.eql(postCaseReq.title); - expect(resp.cases[1].title).to.eql(postCaseReq.title); - expect(resp.cases[2].title).to.eql(postCaseReq.title); - }); - - for (const user of [ - globalRead, - secOnlyReadSpacesAll, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - noKibanaPrivileges, - ]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT update a case`, async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: 'new title', - }, - ], - }, - auth: { user, space: null }, - expectedHttpCode: 403, - }); - }); - } - - it('should return a 404 when attempting to access a space', async () => { - const postedCase = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: null, - }); - - await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: 'new title', - }, - ], - }, - auth: { user: secOnlySpacesAll, space: 'space1' }, - expectedHttpCode: 404, - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts deleted file mode 100644 index 28043d7155e4a6..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; - -import { getPostCaseRequest } from '../../../../common/lib/mock'; -import { deleteCasesByESQuery, createCase } from '../../../../common/lib/utils'; -import { - secOnlySpacesAll, - secOnlyReadSpacesAll, - globalRead, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - noKibanaPrivileges, -} from '../../../../common/lib/authentication/users'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { secOnlyDefaultSpaceAuth } from '../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const es = getService('es'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - describe('post_case', () => { - afterEach(async () => { - await deleteCasesByESQuery(es); - }); - - it('User: security solution only - should create a case', async () => { - const theCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - secOnlyDefaultSpaceAuth - ); - expect(theCase.owner).to.eql('securitySolutionFixture'); - }); - - it('User: security solution only - should NOT create a case of different owner', async () => { - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 403, - secOnlyDefaultSpaceAuth - ); - }); - - for (const user of [ - globalRead, - secOnlyReadSpacesAll, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - noKibanaPrivileges, - ]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT create a case`, async () => { - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 403, - { user, space: null } - ); - }); - } - - it('should return a 404 when attempting to access a space', async () => { - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 404, - { - user: secOnlySpacesAll, - space: 'space1', - } - ); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts deleted file mode 100644 index 8266b456ea1f27..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; - -import { getPostCaseRequest } from '../../../../../common/lib/mock'; -import { createCase, deleteCasesByESQuery, getReporters } from '../../../../../common/lib/utils'; -import { - secOnlySpacesAll, - obsOnlySpacesAll, - globalRead, - superUser, - secOnlyReadSpacesAll, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - noKibanaPrivileges, - obsSecSpacesAll, -} from '../../../../../common/lib/authentication/users'; -import { getUserInfo } from '../../../../../common/lib/authentication'; -import { - secOnlyDefaultSpaceAuth, - obsOnlyDefaultSpaceAuth, - superUserDefaultSpaceAuth, - obsSecDefaultSpaceAuth, -} from '../../../../utils'; -import { UserInfo } from '../../../../../common/lib/authentication/types'; - -const sortReporters = (reporters: UserInfo[]) => - reporters.sort((a, b) => a.username.localeCompare(b.username)); - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('es'); - - describe('get_reporters', () => { - afterEach(async () => { - await deleteCasesByESQuery(es); - }); - - it('User: security solution only - should read the correct reporters', async () => { - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - secOnlyDefaultSpaceAuth - ); - - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - obsOnlyDefaultSpaceAuth - ); - - for (const scenario of [ - { - user: globalRead, - expectedReporters: [getUserInfo(secOnlySpacesAll), getUserInfo(obsOnlySpacesAll)], - }, - { - user: superUser, - expectedReporters: [getUserInfo(secOnlySpacesAll), getUserInfo(obsOnlySpacesAll)], - }, - { user: secOnlyReadSpacesAll, expectedReporters: [getUserInfo(secOnlySpacesAll)] }, - { user: obsOnlyReadSpacesAll, expectedReporters: [getUserInfo(obsOnlySpacesAll)] }, - { - user: obsSecReadSpacesAll, - expectedReporters: [getUserInfo(secOnlySpacesAll), getUserInfo(obsOnlySpacesAll)], - }, - ]) { - const reporters = await getReporters({ - supertest: supertestWithoutAuth, - expectedHttpCode: 200, - auth: { - user: scenario.user, - space: null, - }, - }); - - // sort reporters to prevent order failure - expect(sortReporters(reporters as unknown as UserInfo[])).to.eql( - sortReporters(scenario.expectedReporters) - ); - } - }); - - it(`User ${ - noKibanaPrivileges.username - } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT get all reporters`, async () => { - // super user creates a case at the appropriate space - await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); - - // user should not be able to get all reporters at the appropriate space - await getReporters({ - supertest: supertestWithoutAuth, - expectedHttpCode: 403, - auth: { user: noKibanaPrivileges, space: null }, - }); - }); - - it('should return a 404 when attempting to access a space', async () => { - await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: null, - }); - - await getReporters({ - supertest: supertestWithoutAuth, - expectedHttpCode: 404, - auth: { user: obsSecSpacesAll, space: 'space1' }, - }); - }); - - it('should respect the owner filter when having permissions', async () => { - await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - obsOnlyDefaultSpaceAuth - ), - ]); - - const reporters = await getReporters({ - supertest: supertestWithoutAuth, - auth: obsSecDefaultSpaceAuth, - query: { owner: 'securitySolutionFixture' }, - }); - - expect(reporters).to.eql([getUserInfo(secOnlySpacesAll)]); - }); - - it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { - await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - obsOnlyDefaultSpaceAuth - ), - ]); - - // User with permissions only to security solution request reporters from observability - const reporters = await getReporters({ - supertest: supertestWithoutAuth, - auth: secOnlyDefaultSpaceAuth, - query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, - }); - - // Only security solution reporters are being returned - expect(reporters).to.eql([getUserInfo(secOnlySpacesAll)]); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts deleted file mode 100644 index 245c7d1fdbfc54..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; - -import { CaseStatuses } from '../../../../../../../plugins/cases/common/api'; -import { getPostCaseRequest } from '../../../../../common/lib/mock'; -import { - createCase, - updateCase, - getAllCasesStatuses, - deleteAllCaseItems, -} from '../../../../../common/lib/utils'; -import { - globalRead, - noKibanaPrivileges, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - secOnlySpacesAll, - secOnlyReadSpacesAll, - superUser, -} from '../../../../../common/lib/authentication/users'; -import { superUserDefaultSpaceAuth } from '../../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const es = getService('es'); - - describe('get_status', () => { - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - it('should return the correct status stats', async () => { - /** - * Owner: Sec - * open: 0, in-prog: 1, closed: 1 - * Owner: Obs - * open: 1, in-prog: 1 - */ - const [inProgressSec, closedSec, , inProgressObs] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth), - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - superUserDefaultSpaceAuth - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - superUserDefaultSpaceAuth - ), - ]); - - await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: inProgressSec.id, - version: inProgressSec.version, - status: CaseStatuses['in-progress'], - }, - { - id: closedSec.id, - version: closedSec.version, - status: CaseStatuses.closed, - }, - { - id: inProgressObs.id, - version: inProgressObs.version, - status: CaseStatuses['in-progress'], - }, - ], - }, - auth: superUserDefaultSpaceAuth, - }); - - for (const scenario of [ - { user: globalRead, stats: { open: 1, inProgress: 2, closed: 1 } }, - { user: superUser, stats: { open: 1, inProgress: 2, closed: 1 } }, - { user: secOnlyReadSpacesAll, stats: { open: 0, inProgress: 1, closed: 1 } }, - { user: obsOnlyReadSpacesAll, stats: { open: 1, inProgress: 1, closed: 0 } }, - { user: obsSecReadSpacesAll, stats: { open: 1, inProgress: 2, closed: 1 } }, - { - user: obsSecReadSpacesAll, - stats: { open: 1, inProgress: 1, closed: 0 }, - owner: 'observabilityFixture', - }, - { - user: obsSecReadSpacesAll, - stats: { open: 1, inProgress: 2, closed: 1 }, - owner: ['observabilityFixture', 'securitySolutionFixture'], - }, - ]) { - const statuses = await getAllCasesStatuses({ - supertest: supertestWithoutAuth, - auth: { user: scenario.user, space: null }, - query: { - owner: scenario.owner, - }, - }); - - expect(statuses).to.eql({ - count_open_cases: scenario.stats.open, - count_closed_cases: scenario.stats.closed, - count_in_progress_cases: scenario.stats.inProgress, - }); - } - }); - - it(`should return a 403 when retrieving the statuses when the user ${ - noKibanaPrivileges.username - } with role(s) ${noKibanaPrivileges.roles.join()}`, async () => { - await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); - - await getAllCasesStatuses({ - supertest: supertestWithoutAuth, - auth: { user: noKibanaPrivileges, space: null }, - expectedHttpCode: 403, - }); - }); - - it('should return a 404 when attempting to access a space', async () => { - await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); - - await getAllCasesStatuses({ - supertest: supertestWithoutAuth, - auth: { user: secOnlySpacesAll, space: 'space1' }, - expectedHttpCode: 404, - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts deleted file mode 100644 index c05d956028752d..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; - -import { deleteCasesByESQuery, createCase, getTags } from '../../../../../common/lib/utils'; -import { getPostCaseRequest } from '../../../../../common/lib/mock'; -import { - secOnlySpacesAll, - globalRead, - superUser, - secOnlyReadSpacesAll, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - noKibanaPrivileges, -} from '../../../../../common/lib/authentication/users'; -import { - secOnlyDefaultSpaceAuth, - obsOnlyDefaultSpaceAuth, - obsSecDefaultSpaceAuth, - superUserDefaultSpaceAuth, -} from '../../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('es'); - - describe('get_tags', () => { - afterEach(async () => { - await deleteCasesByESQuery(es); - }); - - it('should read the correct tags', async () => { - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), - 200, - secOnlyDefaultSpaceAuth - ); - - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), - 200, - obsOnlyDefaultSpaceAuth - ); - - for (const scenario of [ - { - user: globalRead, - expectedTags: ['sec', 'obs'], - }, - { - user: superUser, - expectedTags: ['sec', 'obs'], - }, - { user: secOnlyReadSpacesAll, expectedTags: ['sec'] }, - { user: obsOnlyReadSpacesAll, expectedTags: ['obs'] }, - { - user: obsSecReadSpacesAll, - expectedTags: ['sec', 'obs'], - }, - ]) { - const tags = await getTags({ - supertest: supertestWithoutAuth, - expectedHttpCode: 200, - auth: { - user: scenario.user, - space: null, - }, - }); - - expect(tags).to.eql(scenario.expectedTags); - } - }); - - it(`User ${ - noKibanaPrivileges.username - } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT get all tags`, async () => { - // super user creates a case at the appropriate space - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), - 200, - superUserDefaultSpaceAuth - ); - - // user should not be able to get all tags at the appropriate space - await getTags({ - supertest: supertestWithoutAuth, - expectedHttpCode: 403, - auth: { user: noKibanaPrivileges, space: null }, - }); - }); - - it('should return a 404 when attempting to access a space', async () => { - // super user creates a case at the appropriate space - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), - 200, - superUserDefaultSpaceAuth - ); - - await getTags({ - supertest: supertestWithoutAuth, - expectedHttpCode: 404, - auth: { user: secOnlySpacesAll, space: 'space1' }, - }); - }); - - it('should respect the owner filter when having permissions', async () => { - await Promise.all([ - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), - 200, - obsSecDefaultSpaceAuth - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), - 200, - obsSecDefaultSpaceAuth - ), - ]); - - const tags = await getTags({ - supertest: supertestWithoutAuth, - auth: obsSecDefaultSpaceAuth, - query: { owner: 'securitySolutionFixture' }, - }); - - expect(tags).to.eql(['sec']); - }); - - it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { - await Promise.all([ - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), - 200, - obsSecDefaultSpaceAuth - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), - 200, - obsSecDefaultSpaceAuth - ), - ]); - - // User with permissions only to security solution request tags from observability - const tags = await getTags({ - supertest: supertestWithoutAuth, - auth: secOnlyDefaultSpaceAuth, - query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, - }); - - // Only security solution tags are being returned - expect(tags).to.eql(['sec']); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts deleted file mode 100644 index 6a2ddeecdb2728..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; -import { - deleteAllCaseItems, - deleteCasesByESQuery, - deleteCasesUserActions, - deleteComments, - createCase, - createComment, - deleteComment, - deleteAllComments, - getAuthWithSuperUser, -} from '../../../../common/lib/utils'; -import { - globalRead, - noKibanaPrivileges, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - secOnlySpacesAll, - secOnlyReadSpacesAll, -} from '../../../../common/lib/authentication/users'; -import { obsOnlyDefaultSpaceAuth, secOnlyDefaultSpaceAuth } from '../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const es = getService('es'); - const superUserNoSpaceAuth = getAuthWithSuperUser(null); - - describe('delete_comment', () => { - afterEach(async () => { - await deleteCasesByESQuery(es); - await deleteComments(es); - await deleteCasesUserActions(es); - }); - - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should delete a comment from the appropriate owner', async () => { - const secCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - secOnlyDefaultSpaceAuth - ); - - const commentResp = await createComment({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - params: postCommentUserReq, - auth: secOnlyDefaultSpaceAuth, - }); - - await deleteComment({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - commentId: commentResp.comments![0].id, - auth: secOnlyDefaultSpaceAuth, - }); - }); - - it('should delete multiple comments from the appropriate owner', async () => { - const secCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - secOnlyDefaultSpaceAuth - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - params: postCommentUserReq, - auth: secOnlyDefaultSpaceAuth, - }); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - params: postCommentUserReq, - auth: secOnlyDefaultSpaceAuth, - }); - - await deleteAllComments({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - auth: secOnlyDefaultSpaceAuth, - }); - }); - - it('should not delete a comment from a different owner', async () => { - const secCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - secOnlyDefaultSpaceAuth - ); - - const commentResp = await createComment({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - params: postCommentUserReq, - auth: secOnlyDefaultSpaceAuth, - }); - - await deleteComment({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - commentId: commentResp.comments![0].id, - auth: obsOnlyDefaultSpaceAuth, - expectedHttpCode: 403, - }); - - await deleteAllComments({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - auth: obsOnlyDefaultSpaceAuth, - expectedHttpCode: 403, - }); - }); - - for (const user of [ - globalRead, - secOnlyReadSpacesAll, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - noKibanaPrivileges, - ]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT delete a comment`, async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserNoSpaceAuth - ); - - const commentResp = await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: superUserNoSpaceAuth, - }); - - await deleteComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - commentId: commentResp.comments![0].id, - auth: { user, space: null }, - expectedHttpCode: 403, - }); - - await deleteAllComments({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - auth: { user, space: null }, - expectedHttpCode: 403, - }); - }); - } - - it('should return a 404 when attempting to access a space', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserNoSpaceAuth - ); - - const commentResp = await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: superUserNoSpaceAuth, - }); - - await deleteComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - commentId: commentResp.comments![0].id, - auth: { user: secOnlySpacesAll, space: 'space1' }, - expectedHttpCode: 404, - }); - - await deleteAllComments({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - auth: { user: secOnlySpacesAll, space: 'space1' }, - expectedHttpCode: 404, - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts deleted file mode 100644 index 5239c616603a8a..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { CommentsResponse } from '../../../../../../plugins/cases/common/api'; -import { - getPostCaseRequest, - postCommentAlertReq, - postCommentUserReq, -} from '../../../../common/lib/mock'; -import { - createComment, - deleteAllCaseItems, - deleteCasesByESQuery, - deleteCasesUserActions, - deleteComments, - ensureSavedObjectIsAuthorized, - getSpaceUrlPrefix, - createCase, -} from '../../../../common/lib/utils'; - -import { - secOnlySpacesAll, - obsOnlyReadSpacesAll, - secOnlyReadSpacesAll, - noKibanaPrivileges, - superUser, - globalRead, - obsSecReadSpacesAll, -} from '../../../../common/lib/authentication/users'; -import { - obsOnlyDefaultSpaceAuth, - secOnlyDefaultSpaceAuth, - superUserDefaultSpaceAuth, -} from '../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const es = getService('es'); - - describe('find_comments', () => { - afterEach(async () => { - await deleteCasesByESQuery(es); - await deleteComments(es); - await deleteCasesUserActions(es); - }); - - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should return the correct comments', async () => { - const [secCase, obsCase] = await Promise.all([ - // Create case owned by the security solution user - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - obsOnlyDefaultSpaceAuth - ), - // Create case owned by the observability user - ]); - - await Promise.all([ - createComment({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - params: postCommentUserReq, - auth: secOnlyDefaultSpaceAuth, - }), - createComment({ - supertest: supertestWithoutAuth, - caseId: obsCase.id, - params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth: obsOnlyDefaultSpaceAuth, - }), - ]); - - for (const scenario of [ - { - user: globalRead, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: secCase.id, - }, - { - user: globalRead, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: obsCase.id, - }, - { - user: superUser, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: secCase.id, - }, - { - user: superUser, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: obsCase.id, - }, - { - user: secOnlyReadSpacesAll, - numExpectedEntites: 1, - owners: ['securitySolutionFixture'], - caseID: secCase.id, - }, - { - user: obsOnlyReadSpacesAll, - numExpectedEntites: 1, - owners: ['observabilityFixture'], - caseID: obsCase.id, - }, - { - user: obsSecReadSpacesAll, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: secCase.id, - }, - { - user: obsSecReadSpacesAll, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: obsCase.id, - }, - ]) { - const { body: caseComments }: { body: CommentsResponse } = await supertestWithoutAuth - .get(`${getSpaceUrlPrefix(null)}${CASES_URL}/${scenario.caseID}/comments/_find`) - .auth(scenario.user.username, scenario.user.password) - .expect(200); - - ensureSavedObjectIsAuthorized( - caseComments.comments, - scenario.numExpectedEntites, - scenario.owners - ); - } - }); - - it(`User ${ - noKibanaPrivileges.username - } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT read a comment`, async () => { - // super user creates a case and comment in the appropriate space - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - await createComment({ - supertest: supertestWithoutAuth, - auth: { user: superUser, space: null }, - params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, - caseId: caseInfo.id, - }); - - // user should not be able to read comments - await supertestWithoutAuth - .get(`${getSpaceUrlPrefix(null)}${CASES_URL}/${caseInfo.id}/comments/_find`) - .auth(noKibanaPrivileges.username, noKibanaPrivileges.password) - .expect(403); - }); - - it('should return a 404 when attempting to access a space', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - await createComment({ - supertest: supertestWithoutAuth, - auth: superUserDefaultSpaceAuth, - params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, - caseId: caseInfo.id, - }); - - await supertestWithoutAuth - .get(`${getSpaceUrlPrefix('space1')}${CASES_URL}/${caseInfo.id}/comments/_find`) - .auth(secOnlySpacesAll.username, secOnlySpacesAll.password) - .expect(404); - }); - - it('should not return any comments when trying to exploit RBAC through the search query parameter', async () => { - const obsCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - superUserDefaultSpaceAuth - ); - - await createComment({ - supertest: supertestWithoutAuth, - auth: superUserDefaultSpaceAuth, - params: { ...postCommentUserReq, owner: 'observabilityFixture' }, - caseId: obsCase.id, - }); - - const { body: res }: { body: CommentsResponse } = await supertestWithoutAuth - .get( - `${getSpaceUrlPrefix(null)}${CASES_URL}/${ - obsCase.id - }/comments/_find?search=securitySolutionFixture+observabilityFixture` - ) - .auth(secOnlySpacesAll.username, secOnlySpacesAll.password) - .expect(200); - - // shouldn't find any comments since they were created under the observability ownership - ensureSavedObjectIsAuthorized(res.comments, 0, ['securitySolutionFixture']); - }); - - it('should not allow retrieving unauthorized comments using the filter field', async () => { - const obsCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - superUserDefaultSpaceAuth - ); - - await createComment({ - supertest: supertestWithoutAuth, - auth: superUserDefaultSpaceAuth, - params: { ...postCommentUserReq, owner: 'observabilityFixture' }, - caseId: obsCase.id, - }); - - const { body: res } = await supertestWithoutAuth - .get( - `${getSpaceUrlPrefix(null)}${CASES_URL}/${ - obsCase.id - }/comments/_find?filter=cases-comments.attributes.owner:"observabilityFixture"` - ) - .auth(secOnlySpacesAll.username, secOnlySpacesAll.password) - .expect(200); - expect(res.comments.length).to.be(0); - }); - - // This test ensures that the user is not allowed to define the namespaces query param - // so she cannot search across spaces - it('should NOT allow to pass a namespaces query parameter', async () => { - const obsCase = await createCase( - supertest, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200 - ); - - await createComment({ - supertest, - params: { ...postCommentUserReq, owner: 'observabilityFixture' }, - caseId: obsCase.id, - }); - - await supertest.get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces[0]=*`).expect(400); - - await supertest.get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces=*`).expect(400); - }); - - it('should NOT allow to pass a non supported query parameter', async () => { - await supertest.get(`${CASES_URL}/id/comments/_find?notExists=papa`).expect(400); - await supertest.get(`${CASES_URL}/id/comments/_find?owner=papa`).expect(400); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts deleted file mode 100644 index a0010ef19499fa..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; -import { - deleteAllCaseItems, - createCase, - createComment, - getAllComments, -} from '../../../../common/lib/utils'; -import { - globalRead, - noKibanaPrivileges, - obsOnlySpacesAll, - obsOnlyReadSpacesAll, - obsSecSpacesAll, - obsSecReadSpacesAll, - secOnlySpacesAll, - secOnlyReadSpacesAll, - superUser, -} from '../../../../common/lib/authentication/users'; -import { superUserDefaultSpaceAuth } from '../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const es = getService('es'); - - describe('get_all_comments', () => { - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - it('should get all comments when the user has the correct permissions', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: superUserDefaultSpaceAuth, - }); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: superUserDefaultSpaceAuth, - }); - - for (const user of [ - globalRead, - superUser, - secOnlySpacesAll, - secOnlyReadSpacesAll, - obsSecSpacesAll, - obsSecReadSpacesAll, - ]) { - const comments = await getAllComments({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - auth: { user, space: null }, - }); - - expect(comments.length).to.eql(2); - } - }); - - it('should not get comments when the user does not have correct permission', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: superUserDefaultSpaceAuth, - }); - - for (const scenario of [ - { user: noKibanaPrivileges, returnCode: 403 }, - { user: obsOnlySpacesAll, returnCode: 200 }, - { user: obsOnlyReadSpacesAll, returnCode: 200 }, - ]) { - const comments = await getAllComments({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - auth: { user: scenario.user, space: null }, - expectedHttpCode: scenario.returnCode, - }); - - // only check the length if we get a 200 in response - if (scenario.returnCode === 200) { - expect(comments.length).to.be(0); - } - } - }); - - it('should return a 404 when attempting to access a space', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: superUserDefaultSpaceAuth, - }); - - await getAllComments({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - auth: { user: secOnlySpacesAll, space: 'space1' }, - expectedHttpCode: 404, - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts deleted file mode 100644 index 79693d3e0a574e..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -import { postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; -import { - deleteAllCaseItems, - createCase, - createComment, - getComment, -} from '../../../../common/lib/utils'; -import { - globalRead, - noKibanaPrivileges, - obsOnlySpacesAll, - obsOnlyReadSpacesAll, - obsSecSpacesAll, - obsSecReadSpacesAll, - secOnlySpacesAll, - secOnlyReadSpacesAll, - superUser, -} from '../../../../common/lib/authentication/users'; -import { superUserDefaultSpaceAuth } from '../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const es = getService('es'); - - describe('get_comment', () => { - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - it('should get a comment when the user has the correct permissions', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - const caseWithComment = await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: superUserDefaultSpaceAuth, - }); - - for (const user of [ - globalRead, - superUser, - secOnlySpacesAll, - secOnlyReadSpacesAll, - obsSecSpacesAll, - obsSecReadSpacesAll, - ]) { - await getComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - commentId: caseWithComment.comments![0].id, - auth: { user, space: null }, - }); - } - }); - - it('should not get comment when the user does not have correct permissions', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - const caseWithComment = await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: superUserDefaultSpaceAuth, - }); - - for (const user of [noKibanaPrivileges, obsOnlySpacesAll, obsOnlyReadSpacesAll]) { - await getComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - commentId: caseWithComment.comments![0].id, - auth: { user, space: null }, - expectedHttpCode: 403, - }); - } - }); - - it('should return a 404 when attempting to access a space', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - const caseWithComment = await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: superUserDefaultSpaceAuth, - }); - - await getComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - commentId: caseWithComment.comments![0].id, - auth: { user: secOnlySpacesAll, space: 'space1' }, - expectedHttpCode: 404, - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts deleted file mode 100644 index 7a25ec4ec39812..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -import { AttributesTypeUser, CommentType } from '../../../../../../plugins/cases/common/api'; -import { defaultUser, postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; -import { - deleteAllCaseItems, - deleteCasesByESQuery, - deleteCasesUserActions, - deleteComments, - createCase, - createComment, - updateComment, -} from '../../../../common/lib/utils'; -import { - globalRead, - noKibanaPrivileges, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - secOnlySpacesAll, - secOnlyReadSpacesAll, -} from '../../../../common/lib/authentication/users'; -import { - obsOnlyDefaultSpaceAuth, - secOnlyDefaultSpaceAuth, - superUserDefaultSpaceAuth, -} from '../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const es = getService('es'); - - describe('patch_comment', () => { - afterEach(async () => { - await deleteCasesByESQuery(es); - await deleteComments(es); - await deleteCasesUserActions(es); - }); - - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should update a comment that the user has permissions for', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - auth: superUserDefaultSpaceAuth, - }); - - const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - const updatedCase = await updateComment({ - supertest, - caseId: postedCase.id, - req: { - ...postCommentUserReq, - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - comment: newComment, - }, - auth: secOnlyDefaultSpaceAuth, - }); - - const userComment = updatedCase.comments![0] as AttributesTypeUser; - expect(userComment.comment).to.eql(newComment); - expect(userComment.type).to.eql(CommentType.user); - expect(updatedCase.updated_by).to.eql(defaultUser); - expect(userComment.owner).to.eql('securitySolutionFixture'); - }); - - it('should not update a comment that has a different owner thant he user has access to', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - const patchedCase = await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: superUserDefaultSpaceAuth, - }); - - const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - await updateComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - req: { - ...postCommentUserReq, - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - comment: newComment, - }, - auth: obsOnlyDefaultSpaceAuth, - expectedHttpCode: 403, - }); - }); - - for (const user of [ - globalRead, - secOnlyReadSpacesAll, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - noKibanaPrivileges, - ]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT update a comment`, async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - const patchedCase = await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: superUserDefaultSpaceAuth, - }); - - const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - await updateComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - req: { - ...postCommentUserReq, - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - comment: newComment, - }, - auth: { user, space: null }, - expectedHttpCode: 403, - }); - }); - } - - it('should return a 404 when attempting to access a space', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - const patchedCase = await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: superUserDefaultSpaceAuth, - }); - - const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - await updateComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - req: { - ...postCommentUserReq, - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - comment: newComment, - }, - auth: { user: secOnlySpacesAll, space: 'space1' }, - expectedHttpCode: 404, - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts deleted file mode 100644 index 500308305d131e..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -import { postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; -import { - deleteAllCaseItems, - deleteCasesByESQuery, - deleteCasesUserActions, - deleteComments, - createCase, - createComment, -} from '../../../../common/lib/utils'; - -import { - globalRead, - noKibanaPrivileges, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - secOnlySpacesAll, - secOnlyReadSpacesAll, -} from '../../../../common/lib/authentication/users'; -import { - obsOnlyDefaultSpaceAuth, - secOnlyDefaultSpaceAuth, - superUserDefaultSpaceAuth, -} from '../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const es = getService('es'); - - describe('post_comment', () => { - afterEach(async () => { - await deleteCasesByESQuery(es); - await deleteComments(es); - await deleteCasesUserActions(es); - }); - - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should create a comment when the user has the correct permissions for that owner', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserDefaultSpaceAuth - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: secOnlyDefaultSpaceAuth, - }); - }); - - it('should not create a comment when the user does not have permissions for that owner', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - obsOnlyDefaultSpaceAuth - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: { ...postCommentUserReq, owner: 'observabilityFixture' }, - auth: secOnlyDefaultSpaceAuth, - expectedHttpCode: 403, - }); - }); - - for (const user of [ - globalRead, - secOnlyReadSpacesAll, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - noKibanaPrivileges, - ]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should not create a comment`, async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserDefaultSpaceAuth - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: { user, space: null }, - expectedHttpCode: 403, - }); - }); - } - - it('should return a 404 when attempting to access a space', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserDefaultSpaceAuth - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: { user: secOnlySpacesAll, space: 'space1' }, - expectedHttpCode: 404, - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts deleted file mode 100644 index 0a8b3ebd8981e3..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -import { - deleteConfiguration, - getConfiguration, - createConfiguration, - getConfigurationRequest, - ensureSavedObjectIsAuthorized, -} from '../../../../common/lib/utils'; -import { - secOnlySpacesAll, - obsOnlyReadSpacesAll, - secOnlyReadSpacesAll, - noKibanaPrivileges, - superUser, - globalRead, - obsSecReadSpacesAll, -} from '../../../../common/lib/authentication/users'; -import { - obsOnlyDefaultSpaceAuth, - obsSecDefaultSpaceAuth, - secOnlyDefaultSpaceAuth, - superUserDefaultSpaceAuth, -} from '../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('es'); - - describe('get_configure', () => { - afterEach(async () => { - await deleteConfiguration(es); - }); - - it('should return the correct configuration', async () => { - await createConfiguration( - supertestWithoutAuth, - getConfigurationRequest(), - 200, - secOnlyDefaultSpaceAuth - ); - - await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'observabilityFixture' }, - 200, - obsOnlyDefaultSpaceAuth - ); - - for (const scenario of [ - { - user: globalRead, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, - { - user: superUser, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, - { - user: secOnlyReadSpacesAll, - numberOfExpectedCases: 1, - owners: ['securitySolutionFixture'], - }, - { - user: obsOnlyReadSpacesAll, - numberOfExpectedCases: 1, - owners: ['observabilityFixture'], - }, - { - user: obsSecReadSpacesAll, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, - ]) { - const configuration = await getConfiguration({ - supertest: supertestWithoutAuth, - query: { owner: scenario.owners }, - expectedHttpCode: 200, - auth: { - user: scenario.user, - space: null, - }, - }); - - ensureSavedObjectIsAuthorized( - configuration, - scenario.numberOfExpectedCases, - scenario.owners - ); - } - }); - - it(`User ${ - noKibanaPrivileges.username - } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT read a case configuration`, async () => { - // super user creates a configuration at the appropriate space - await createConfiguration( - supertestWithoutAuth, - getConfigurationRequest(), - 200, - superUserDefaultSpaceAuth - ); - - // user should not be able to read configurations at the appropriate space - await getConfiguration({ - supertest: supertestWithoutAuth, - expectedHttpCode: 403, - auth: { - user: noKibanaPrivileges, - space: null, - }, - }); - }); - - it('should return a 404 when attempting to access a space', async () => { - await createConfiguration( - supertestWithoutAuth, - getConfigurationRequest(), - 200, - superUserDefaultSpaceAuth - ); - - await getConfiguration({ - supertest: supertestWithoutAuth, - expectedHttpCode: 404, - auth: { - user: secOnlySpacesAll, - space: 'space1', - }, - }); - }); - - it('should respect the owner filter when having permissions', async () => { - await Promise.all([ - createConfiguration( - supertestWithoutAuth, - getConfigurationRequest(), - 200, - obsSecDefaultSpaceAuth - ), - createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'observabilityFixture' }, - 200, - obsSecDefaultSpaceAuth - ), - ]); - - const res = await getConfiguration({ - supertest: supertestWithoutAuth, - query: { owner: 'securitySolutionFixture' }, - auth: obsSecDefaultSpaceAuth, - }); - - ensureSavedObjectIsAuthorized(res, 1, ['securitySolutionFixture']); - }); - - it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { - await Promise.all([ - createConfiguration( - supertestWithoutAuth, - getConfigurationRequest(), - 200, - obsSecDefaultSpaceAuth - ), - createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'observabilityFixture' }, - 200, - obsSecDefaultSpaceAuth - ), - ]); - - // User with permissions only to security solution request cases from observability - const res = await getConfiguration({ - supertest: supertestWithoutAuth, - query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, - auth: secOnlyDefaultSpaceAuth, - }); - - // Only security solution cases are being returned - ensureSavedObjectIsAuthorized(res, 1, ['securitySolutionFixture']); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts deleted file mode 100644 index eb1fa01221ae89..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; - -import { - getConfigurationRequest, - deleteConfiguration, - createConfiguration, - updateConfiguration, -} from '../../../../common/lib/utils'; -import { - secOnlySpacesAll, - obsOnlyReadSpacesAll, - secOnlyReadSpacesAll, - noKibanaPrivileges, - globalRead, - obsSecReadSpacesAll, -} from '../../../../common/lib/authentication/users'; -import { secOnlyDefaultSpaceAuth, superUserDefaultSpaceAuth } from '../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('es'); - - describe('patch_configure', () => { - const actionsRemover = new ActionsRemover(supertest); - - afterEach(async () => { - await deleteConfiguration(es); - await actionsRemover.removeAll(); - }); - - it('User: security solution only - should update a configuration', async () => { - const configuration = await createConfiguration( - supertestWithoutAuth, - getConfigurationRequest(), - 200, - secOnlyDefaultSpaceAuth - ); - - const newConfiguration = await updateConfiguration( - supertestWithoutAuth, - configuration.id, - { - closure_type: 'close-by-pushing', - version: configuration.version, - }, - 200, - secOnlyDefaultSpaceAuth - ); - - expect(newConfiguration.owner).to.eql('securitySolutionFixture'); - }); - - it('User: security solution only - should NOT update a configuration of different owner', async () => { - const configuration = await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'observabilityFixture' }, - 200, - superUserDefaultSpaceAuth - ); - - await updateConfiguration( - supertestWithoutAuth, - configuration.id, - { - closure_type: 'close-by-pushing', - version: configuration.version, - }, - 403, - secOnlyDefaultSpaceAuth - ); - }); - - for (const user of [ - globalRead, - secOnlyReadSpacesAll, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - noKibanaPrivileges, - ]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT update a configuration`, async () => { - const configuration = await createConfiguration( - supertestWithoutAuth, - getConfigurationRequest(), - 200, - superUserDefaultSpaceAuth - ); - - await updateConfiguration( - supertestWithoutAuth, - configuration.id, - { - closure_type: 'close-by-pushing', - version: configuration.version, - }, - 403, - { - user, - space: null, - } - ); - }); - } - - it('should return a 404 when attempting to access a space', async () => { - const configuration = await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, - 200, - superUserDefaultSpaceAuth - ); - - await updateConfiguration( - supertestWithoutAuth, - configuration.id, - { - closure_type: 'close-by-pushing', - version: configuration.version, - }, - 404, - { - user: secOnlySpacesAll, - space: 'space1', - } - ); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts deleted file mode 100644 index b3de6ec0487bb0..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; - -import { - getConfigurationRequest, - deleteConfiguration, - createConfiguration, - getConfiguration, - ensureSavedObjectIsAuthorized, -} from '../../../../common/lib/utils'; - -import { - secOnlySpacesAll, - obsOnlyReadSpacesAll, - secOnlyReadSpacesAll, - noKibanaPrivileges, - globalRead, - obsSecReadSpacesAll, -} from '../../../../common/lib/authentication/users'; -import { secOnlyDefaultSpaceAuth, superUserDefaultSpaceAuth } from '../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('es'); - - describe('post_configure', () => { - const actionsRemover = new ActionsRemover(supertest); - - afterEach(async () => { - await deleteConfiguration(es); - await actionsRemover.removeAll(); - }); - - it('User: security solution only - should create a configuration', async () => { - const configuration = await createConfiguration( - supertestWithoutAuth, - getConfigurationRequest(), - 200, - secOnlyDefaultSpaceAuth - ); - - expect(configuration.owner).to.eql('securitySolutionFixture'); - }); - - it('User: security solution only - should NOT create a configuration of different owner', async () => { - await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'observabilityFixture' }, - 403, - secOnlyDefaultSpaceAuth - ); - }); - - for (const user of [ - globalRead, - secOnlyReadSpacesAll, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - noKibanaPrivileges, - ]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT create a configuration`, async () => { - await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, - 403, - { - user, - space: null, - } - ); - }); - } - - it('should return a 404 when attempting to access a space', async () => { - await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, - 404, - { - user: secOnlySpacesAll, - space: 'space1', - } - ); - }); - - it('it deletes the correct configurations', async () => { - await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, - 200, - superUserDefaultSpaceAuth - ); - - /** - * This API call should not delete the previously created configuration - * as it belongs to a different owner - */ - await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'observabilityFixture' }, - 200, - superUserDefaultSpaceAuth - ); - - const configuration = await getConfiguration({ - supertest: supertestWithoutAuth, - query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, - auth: superUserDefaultSpaceAuth, - }); - - /** - * This ensures that both configuration are returned as expected - * and neither of has been deleted - */ - ensureSavedObjectIsAuthorized(configuration, 2, [ - 'securitySolutionFixture', - 'observabilityFixture', - ]); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/index.ts b/x-pack/test/case_api_integration/security_only/tests/common/index.ts deleted file mode 100644 index 7dd6dd4e22711b..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -// eslint-disable-next-line import/no-default-export -export default ({ loadTestFile }: FtrProviderContext): void => { - describe('Common', function () { - loadTestFile(require.resolve('./comments/delete_comment')); - loadTestFile(require.resolve('./comments/find_comments')); - loadTestFile(require.resolve('./comments/get_comment')); - loadTestFile(require.resolve('./comments/get_all_comments')); - loadTestFile(require.resolve('./comments/patch_comment')); - loadTestFile(require.resolve('./comments/post_comment')); - loadTestFile(require.resolve('./alerts/get_cases')); - loadTestFile(require.resolve('./cases/delete_cases')); - loadTestFile(require.resolve('./cases/find_cases')); - loadTestFile(require.resolve('./cases/get_case')); - loadTestFile(require.resolve('./cases/patch_cases')); - loadTestFile(require.resolve('./cases/post_case')); - loadTestFile(require.resolve('./cases/reporters/get_reporters')); - loadTestFile(require.resolve('./cases/status/get_status')); - loadTestFile(require.resolve('./cases/tags/get_tags')); - loadTestFile(require.resolve('./user_actions/get_all_user_actions')); - loadTestFile(require.resolve('./configure/get_configure')); - loadTestFile(require.resolve('./configure/patch_configure')); - loadTestFile(require.resolve('./configure/post_configure')); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts deleted file mode 100644 index bd36ce1b0d9d6a..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -import { CaseResponse, CaseStatuses } from '../../../../../../plugins/cases/common/api'; -import { getPostCaseRequest } from '../../../../common/lib/mock'; -import { - deleteAllCaseItems, - createCase, - updateCase, - getCaseUserActions, -} from '../../../../common/lib/utils'; -import { - globalRead, - noKibanaPrivileges, - obsSecSpacesAll, - obsSecReadSpacesAll, - secOnlySpacesAll, - secOnlyReadSpacesAll, - superUser, -} from '../../../../common/lib/authentication/users'; -import { superUserDefaultSpaceAuth } from '../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const es = getService('es'); - - describe('get_all_user_actions', () => { - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - let caseInfo: CaseResponse; - beforeEach(async () => { - caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserDefaultSpaceAuth - ); - - await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: caseInfo.id, - version: caseInfo.version, - status: CaseStatuses.closed, - }, - ], - }, - auth: superUserDefaultSpaceAuth, - }); - }); - - it('should get the user actions for a case when the user has the correct permissions', async () => { - for (const user of [ - globalRead, - superUser, - secOnlySpacesAll, - secOnlyReadSpacesAll, - obsSecSpacesAll, - obsSecReadSpacesAll, - ]) { - const userActions = await getCaseUserActions({ - supertest: supertestWithoutAuth, - caseID: caseInfo.id, - auth: { user, space: null }, - }); - - expect(userActions.length).to.eql(2); - } - }); - - it(`should 403 when requesting the user actions of a case with user ${ - noKibanaPrivileges.username - } with role(s) ${noKibanaPrivileges.roles.join()}`, async () => { - await getCaseUserActions({ - supertest: supertestWithoutAuth, - caseID: caseInfo.id, - auth: { user: noKibanaPrivileges, space: null }, - expectedHttpCode: 403, - }); - }); - - it('should return a 404 when attempting to access a space', async () => { - await getCaseUserActions({ - supertest: supertestWithoutAuth, - caseID: caseInfo.id, - auth: { user: secOnlySpacesAll, space: 'space1' }, - expectedHttpCode: 404, - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts deleted file mode 100644 index 69d403ea15301e..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import http from 'http'; - -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; - -import { getPostCaseRequest } from '../../../../common/lib/mock'; -import { - pushCase, - deleteAllCaseItems, - createCaseWithConnector, - getServiceNowSimulationServer, -} from '../../../../common/lib/utils'; -import { - globalRead, - noKibanaPrivileges, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - secOnlySpacesAll, - secOnlyReadSpacesAll, -} from '../../../../common/lib/authentication/users'; -import { secOnlyDefaultSpaceAuth } from '../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const es = getService('es'); - - describe('push_case', () => { - const actionsRemover = new ActionsRemover(supertest); - let serviceNowSimulatorURL: string = ''; - let serviceNowServer: http.Server; - - before(async () => { - const { server, url } = await getServiceNowSimulationServer(); - serviceNowServer = server; - serviceNowSimulatorURL = url; - }); - - afterEach(async () => { - await deleteAllCaseItems(es); - await actionsRemover.removeAll(); - }); - - after(async () => { - serviceNowServer.close(); - }); - - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - it('should push a case that the user has permissions for', async () => { - const { postedCase, connector } = await createCaseWithConnector({ - supertest, - serviceNowSimulatorURL, - actionsRemover, - }); - - await pushCase({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - connectorId: connector.id, - auth: secOnlyDefaultSpaceAuth, - }); - }); - - it('should not push a case that the user does not have permissions for', async () => { - const { postedCase, connector } = await createCaseWithConnector({ - supertest, - serviceNowSimulatorURL, - actionsRemover, - createCaseReq: getPostCaseRequest({ owner: 'observabilityFixture' }), - }); - - await pushCase({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - connectorId: connector.id, - auth: secOnlyDefaultSpaceAuth, - expectedHttpCode: 403, - }); - }); - - for (const user of [ - globalRead, - secOnlyReadSpacesAll, - obsOnlyReadSpacesAll, - obsSecReadSpacesAll, - noKibanaPrivileges, - ]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT push a case`, async () => { - const { postedCase, connector } = await createCaseWithConnector({ - supertest, - serviceNowSimulatorURL, - actionsRemover, - }); - - await pushCase({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - connectorId: connector.id, - auth: { user, space: null }, - expectedHttpCode: 403, - }); - }); - } - - it('should return a 404 when attempting to access a space', async () => { - const { postedCase, connector } = await createCaseWithConnector({ - supertest, - serviceNowSimulatorURL, - actionsRemover, - }); - - await pushCase({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - connectorId: connector.id, - auth: { user: secOnlySpacesAll, space: 'space1' }, - expectedHttpCode: 404, - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/index.ts b/x-pack/test/case_api_integration/security_only/tests/trial/index.ts deleted file mode 100644 index 86a44459a58370..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/trial/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { rolesDefaultSpace } from '../../../common/lib/authentication/roles'; -import { usersDefaultSpace } from '../../../common/lib/authentication/users'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { createUsersAndRoles, deleteUsersAndRoles } from '../../../common/lib/authentication'; - -// eslint-disable-next-line import/no-default-export -export default ({ loadTestFile, getService }: FtrProviderContext): void => { - describe('cases security only enabled: trial', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - - before(async () => { - // since spaces are disabled this changes each role to have access to all available spaces (it'll just be the default one) - await createUsersAndRoles(getService, usersDefaultSpace, rolesDefaultSpace); - }); - - after(async () => { - await deleteUsersAndRoles(getService, usersDefaultSpace, rolesDefaultSpace); - }); - - // Trial - loadTestFile(require.resolve('./cases/push_case')); - - // Common - loadTestFile(require.resolve('../common')); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/utils.ts b/x-pack/test/case_api_integration/security_only/utils.ts deleted file mode 100644 index 7c5764c558bbe4..00000000000000 --- a/x-pack/test/case_api_integration/security_only/utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - obsOnlySpacesAll, - obsSecSpacesAll, - secOnlySpacesAll, -} from '../common/lib/authentication/users'; -import { getAuthWithSuperUser } from '../common/lib/utils'; - -export const secOnlyDefaultSpaceAuth = { user: secOnlySpacesAll, space: null }; -export const obsOnlyDefaultSpaceAuth = { user: obsOnlySpacesAll, space: null }; -export const obsSecDefaultSpaceAuth = { user: obsSecSpacesAll, space: null }; -export const superUserDefaultSpaceAuth = getAuthWithSuperUser(null); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts index d5e9050ed9d410..147e6058dffa8c 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts @@ -392,7 +392,8 @@ export default ({ getService }: FtrProviderContext) => { }); }); - describe('"exists" operator', () => { + // FLAKY: https://github.com/elastic/kibana/issues/115315 + describe.skip('"exists" operator', () => { it('will return 1 empty result if matching against ip', async () => { const rule = getRuleForSignalTesting(['ip_as_array']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts index a938ee991e1ac9..4e4823fcf747f8 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts @@ -56,7 +56,8 @@ export default ({ getService }: FtrProviderContext) => { await deleteListsIndex(supertest); }); - describe('"is" operator', () => { + // FLAKY: https://github.com/elastic/kibana/issues/115310 + describe.skip('"is" operator', () => { it('should find all the text from the data set when no exceptions are set on the rule', async () => { const rule = getRuleForSignalTesting(['text']); const { id } = await createRule(supertest, rule); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts index 4c0f21df8c0ffe..6d1d64a04cd930 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts @@ -86,6 +86,33 @@ export default ({ getService }: FtrProviderContext): void => { '7d' ); }); + + it('migrates legacy siem-detection-engine-rule-status to use saved object references', async () => { + const response = await es.get<{ + 'siem-detection-engine-rule-status': { + alertId: string; + }; + references: [{}]; + }>({ + index: '.kibana', + id: 'siem-detection-engine-rule-status:d62d2980-27c4-11ec-92b0-f7b47106bb35', + }); + expect(response.statusCode).to.eql(200); + + // references exist and are expected values + expect(response.body._source?.references).to.eql([ + { + name: 'alert_0', + id: 'fb1046a0-0452-11ec-9b15-d13d79d162f3', + type: 'alert', + }, + ]); + + // alertId no longer exist + expect(response.body._source?.['siem-detection-engine-rule-status'].alertId).to.eql( + undefined + ); + }); }); }); }; diff --git a/x-pack/test/functional/apps/discover/value_suggestions_non_timebased.ts b/x-pack/test/functional/apps/discover/value_suggestions_non_timebased.ts index cfddd33f4197e7..e8cc34604eabaa 100644 --- a/x-pack/test/functional/apps/discover/value_suggestions_non_timebased.ts +++ b/x-pack/test/functional/apps/discover/value_suggestions_non_timebased.ts @@ -13,7 +13,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const queryBar = getService('queryBar'); const PageObjects = getPageObjects(['common', 'settings', 'context']); - describe('value suggestions non time based', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/114745 + describe.skip('value suggestions non time based', function describeIndexTests() { before(async function () { await esArchiver.loadIfNeeded( 'test/functional/fixtures/es_archiver/index_pattern_without_timefield' diff --git a/x-pack/test/functional/apps/lens/geo_field.ts b/x-pack/test/functional/apps/lens/geo_field.ts index 2ba833177a1355..499188683c0a47 100644 --- a/x-pack/test/functional/apps/lens/geo_field.ts +++ b/x-pack/test/functional/apps/lens/geo_field.ts @@ -15,6 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should visualize geo fields in maps', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.switchDataPanelIndexPattern('logstash-*'); await PageObjects.timePicker.setAbsoluteRange( 'Sep 22, 2015 @ 00:00:00.000', 'Sep 22, 2015 @ 04:00:00.000' diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index db0f41cc9e2708..5241d9724abb90 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -11,6 +11,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); const log = getService('log'); const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); describe('lens app', () => { before(async () => { @@ -18,16 +19,23 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await browser.setWindowSize(1280, 800); await esArchiver.load('x-pack/test/functional/es_archives/logstash_functional'); await esArchiver.load('x-pack/test/functional/es_archives/lens/basic'); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/lens/default' + ); }); after(async () => { await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); await esArchiver.unload('x-pack/test/functional/es_archives/lens/basic'); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/lens/default' + ); }); describe('', function () { this.tags(['ciGroup3', 'skipFirefox']); loadTestFile(require.resolve('./smokescreen')); + loadTestFile(require.resolve('./persistent_context')); }); describe('', function () { @@ -37,7 +45,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./table')); loadTestFile(require.resolve('./runtime_fields')); loadTestFile(require.resolve('./dashboard')); - loadTestFile(require.resolve('./persistent_context')); loadTestFile(require.resolve('./colors')); loadTestFile(require.resolve('./chart_data')); loadTestFile(require.resolve('./time_shift')); diff --git a/x-pack/test/functional/apps/lens/persistent_context.ts b/x-pack/test/functional/apps/lens/persistent_context.ts index e7b99ad804cd03..8a7ac6df76496d 100644 --- a/x-pack/test/functional/apps/lens/persistent_context.ts +++ b/x-pack/test/functional/apps/lens/persistent_context.ts @@ -9,11 +9,20 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['visualize', 'lens', 'header', 'timePicker']); + const PageObjects = getPageObjects([ + 'visualize', + 'lens', + 'header', + 'timePicker', + 'common', + 'navigationalSearch', + ]); const browser = getService('browser'); const filterBar = getService('filterBar'); const appsMenu = getService('appsMenu'); const security = getService('security'); + const listingTable = getService('listingTable'); + const queryBar = getService('queryBar'); describe('lens query context', () => { before(async () => { @@ -27,6 +36,122 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.testUser.restoreDefaults(); }); + describe('Navigation search', () => { + describe('when opening from empty visualization to existing one', () => { + before(async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.navigationalSearch.focus(); + await PageObjects.navigationalSearch.searchFor('type:lens lnsTableVis'); + await PageObjects.navigationalSearch.clickOnOption(0); + await PageObjects.lens.waitForWorkspaceWithVisualization(); + }); + it('filters, time and query reflect the visualization state', async () => { + expect(await PageObjects.lens.getDatatableHeaderText(1)).to.equal( + '404 › Median of bytes' + ); + expect(await PageObjects.lens.getDatatableHeaderText(2)).to.equal( + '503 › Median of bytes' + ); + expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('TG'); + expect(await PageObjects.lens.getDatatableCellText(0, 1)).to.eql('9,931'); + }); + it('preserves time range', async () => { + const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); + expect(timePickerValues.start).to.eql(PageObjects.timePicker.defaultStartTime); + expect(timePickerValues.end).to.eql(PageObjects.timePicker.defaultEndTime); + // data is correct and top nav is correct + }); + it('loads filters', async () => { + const filterCount = await filterBar.getFilterCount(); + expect(filterCount).to.equal(1); + }); + it('loads query', async () => { + const query = await queryBar.getQueryString(); + expect(query).to.equal('extension.raw : "jpg" or extension.raw : "gif" '); + }); + }); + describe('when opening from existing visualization to empty one', () => { + before(async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsTableVis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsTableVis'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.navigationalSearch.focus(); + await PageObjects.navigationalSearch.searchFor('type:application lens'); + await PageObjects.navigationalSearch.clickOnOption(0); + await PageObjects.lens.waitForEmptyWorkspace(); + await PageObjects.lens.switchToVisualization('lnsMetric'); + await PageObjects.lens.dragFieldToWorkspace('@timestamp'); + }); + it('preserves time range', async () => { + // fill the navigation search and select empty + // see the time + const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); + expect(timePickerValues.start).to.eql(PageObjects.timePicker.defaultStartTime); + expect(timePickerValues.end).to.eql(PageObjects.timePicker.defaultEndTime); + }); + it('cleans filters', async () => { + const filterCount = await filterBar.getFilterCount(); + expect(filterCount).to.equal(0); + }); + it('cleans query', async () => { + const query = await queryBar.getQueryString(); + expect(query).to.equal(''); + }); + it('filters, time and query reflect the visualization state', async () => { + await PageObjects.lens.assertMetric('Unique count of @timestamp', '14,181'); + }); + }); + }); + + describe('Switching in Visualize App', () => { + it('when moving from existing to empty workspace, preserves time range, cleans filters and query', async () => { + // go to existing vis + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsTableVis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsTableVis'); + await PageObjects.lens.goToTimeRange(); + // go to empty vis + await PageObjects.lens.goToListingPageViaBreadcrumbs(); + await PageObjects.visualize.clickNewVisualization(); + await PageObjects.visualize.waitForGroupsSelectPage(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.waitForEmptyWorkspace(); + await PageObjects.lens.switchToVisualization('lnsMetric'); + await PageObjects.lens.dragFieldToWorkspace('@timestamp'); + + const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); + expect(timePickerValues.start).to.eql(PageObjects.timePicker.defaultStartTime); + expect(timePickerValues.end).to.eql(PageObjects.timePicker.defaultEndTime); + const filterCount = await filterBar.getFilterCount(); + expect(filterCount).to.equal(0); + const query = await queryBar.getQueryString(); + expect(query).to.equal(''); + await PageObjects.lens.assertMetric('Unique count of @timestamp', '14,181'); + }); + it('when moving from empty to existing workspace, preserves time range and loads filters and query', async () => { + // go to existing vis + await PageObjects.lens.goToListingPageViaBreadcrumbs(); + await listingTable.searchForItemWithName('lnsTableVis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsTableVis'); + + expect(await PageObjects.lens.getDatatableHeaderText(1)).to.equal('404 › Median of bytes'); + expect(await PageObjects.lens.getDatatableHeaderText(2)).to.equal('503 › Median of bytes'); + expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('TG'); + expect(await PageObjects.lens.getDatatableCellText(0, 1)).to.eql('9,931'); + + const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); + expect(timePickerValues.start).to.eql(PageObjects.timePicker.defaultStartTime); + expect(timePickerValues.end).to.eql(PageObjects.timePicker.defaultEndTime); + const filterCount = await filterBar.getFilterCount(); + expect(filterCount).to.equal(1); + const query = await queryBar.getQueryString(); + expect(query).to.equal('extension.raw : "jpg" or extension.raw : "gif" '); + }); + }); + it('should carry over time range and pinned filters to discover', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); diff --git a/x-pack/test/functional/apps/maps/embeddable/dashboard.js b/x-pack/test/functional/apps/maps/embeddable/dashboard.js index 6c962c98c6a988..a892a0d547339f 100644 --- a/x-pack/test/functional/apps/maps/embeddable/dashboard.js +++ b/x-pack/test/functional/apps/maps/embeddable/dashboard.js @@ -77,9 +77,12 @@ export default function ({ getPageObjects, getService }) { await inspector.close(); await dashboardPanelActions.openInspectorByTitle('geo grid vector grid example'); - const gridExampleRequestNames = await inspector.getRequestNames(); + const singleExampleRequest = await inspector.hasSingleRequest(); + const selectedExampleRequest = await inspector.getSelectedOption(); await inspector.close(); - expect(gridExampleRequestNames).to.equal('logstash-*'); + + expect(singleExampleRequest).to.be(true); + expect(selectedExampleRequest).to.equal('logstash-*'); }); it('should apply container state (time, query, filters) to embeddable when loaded', async () => { diff --git a/x-pack/test/functional/apps/saved_objects_management/spaces_integration.ts b/x-pack/test/functional/apps/saved_objects_management/spaces_integration.ts index 62c742e39d02b8..509cfbbab666f3 100644 --- a/x-pack/test/functional/apps/saved_objects_management/spaces_integration.ts +++ b/x-pack/test/functional/apps/saved_objects_management/spaces_integration.ts @@ -31,7 +31,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { return bools.every((currBool) => currBool === true); }; - describe('spaces integration', () => { + // FLAKY: https://github.com/elastic/kibana/issues/115303 + describe.skip('spaces integration', () => { before(async () => { await spacesService.create({ id: spaceId, name: spaceId }); await kibanaServer.importExport.load( diff --git a/x-pack/test/functional/apps/uptime/ping_redirects.ts b/x-pack/test/functional/apps/uptime/ping_redirects.ts index 03185ac9f14669..748163cb5ec780 100644 --- a/x-pack/test/functional/apps/uptime/ping_redirects.ts +++ b/x-pack/test/functional/apps/uptime/ping_redirects.ts @@ -19,7 +19,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const monitor = () => uptime.monitor; - describe('Ping redirects', () => { + // FLAKY: https://github.com/elastic/kibana/issues/84992 + describe.skip('Ping redirects', () => { const start = '~ 15 minutes ago'; const end = 'now'; diff --git a/x-pack/test/functional/apps/uptime/synthetics_integration.ts b/x-pack/test/functional/apps/uptime/synthetics_integration.ts index e403c4d25097c8..bc2d5cdd95e89a 100644 --- a/x-pack/test/functional/apps/uptime/synthetics_integration.ts +++ b/x-pack/test/functional/apps/uptime/synthetics_integration.ts @@ -166,7 +166,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const saveButton = await uptimePage.syntheticsIntegration.findSaveButton(); await saveButton.click(); - await testSubjects.missingOrFail('packagePolicyCreateSuccessToast'); + await testSubjects.missingOrFail('postInstallAddAgentModal'); }); }); diff --git a/x-pack/test/functional/es_archives/security_solution/migrations/data.json b/x-pack/test/functional/es_archives/security_solution/migrations/data.json index 7b8d81135065dc..97a2596f9dba1c 100644 --- a/x-pack/test/functional/es_archives/security_solution/migrations/data.json +++ b/x-pack/test/functional/es_archives/security_solution/migrations/data.json @@ -1,4 +1,4 @@ -{ + { "type": "doc", "value": { "id": "siem-detection-engine-rule-actions:fce024a0-0452-11ec-9b15-d13d79d162f3", @@ -29,3 +29,35 @@ } } } + +{ + "type": "doc", + "value": { + "id": "siem-detection-engine-rule-status:d62d2980-27c4-11ec-92b0-f7b47106bb35", + "index": ".kibana_1", + "source": { + "siem-detection-engine-rule-status": { + "alertId": "fb1046a0-0452-11ec-9b15-d13d79d162f3", + "statusDate": "2021-10-11T20:51:26.622Z", + "status": "succeeded", + "lastFailureAt": "2021-10-11T18:10:08.982Z", + "lastSuccessAt": "2021-10-11T20:51:26.622Z", + "lastFailureMessage": "4 days (323690920ms) were not queried between this rule execution and the last execution, so signals may have been missed. Consider increasing your look behind time or adding more Kibana instances. name: \"Threshy\" id: \"fb1046a0-0452-11ec-9b15-d13d79d162f3\" rule id: \"b789c80f-f6d8-41f1-8b4f-b4a23342cde2\" signals index: \".siem-signals-spong-default\"", + "lastSuccessMessage": "succeeded", + "gap": "4 days", + "bulkCreateTimeDurations": [ + "34.49" + ], + "searchAfterTimeDurations": [ + "62.58" + ], + "lastLookBackDate": null + }, + "type": "siem-detection-engine-rule-status", + "references": [], + "coreMigrationVersion": "7.14.0", + "updated_at": "2021-10-11T20:51:26.657Z" + } + } +} + diff --git a/x-pack/test/functional/fixtures/kbn_archiver/lens/default.json b/x-pack/test/functional/fixtures/kbn_archiver/lens/default.json new file mode 100644 index 00000000000000..6f1007703a8326 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/lens/default.json @@ -0,0 +1,154 @@ +{ + "attributes": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "8.0.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z", + "version": "WzEzLDJd" +} + +{ + "attributes": { + "description": "", + "state": { + "datasourceStates": { + "indexpattern": { + "layers": { + "4ba1a1be-6e67-434b-b3a0-f30db8ea5395": { + "columnOrder": [ + "70d52318-354d-47d5-b33b-43d50eb34425", + "bafe3009-1776-4227-a0fe-b0d6ccbb4961", + "3dc0bd55-2087-4e60-aea2-f9910714f7db" + ], + "columns": { + "3dc0bd55-2087-4e60-aea2-f9910714f7db": { + "dataType": "number", + "isBucketed": false, + "label": "Median of bytes", + "operationType": "median", + "scale": "ratio", + "sourceField": "bytes" + }, + "70d52318-354d-47d5-b33b-43d50eb34425": { + "dataType": "string", + "isBucketed": true, + "label": "Top values of response.raw", + "operationType": "terms", + "params": { + "missingBucket": false, + "orderBy": { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "type": "column" + }, + "orderDirection": "desc", + "otherBucket": true, + "size": 3 + }, + "scale": "ordinal", + "sourceField": "response.raw" + }, + "bafe3009-1776-4227-a0fe-b0d6ccbb4961": { + "dataType": "string", + "isBucketed": true, + "label": "Top values of geo.src", + "operationType": "terms", + "params": { + "orderBy": { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "type": "column" + }, + "orderDirection": "desc", + "size": 7 + }, + "scale": "ordinal", + "sourceField": "geo.src" + } + }, + "incompleteColumns": {} + } + } + } + }, + "filters": [ + { + "$state": { + "store": "appState" + }, + "meta": { + "alias": null, + "disabled": false, + "indexRefName": "filter-index-pattern-0", + "key": "response", + "negate": true, + "params": { + "query": "200" + }, + "type": "phrase" + }, + "query": { + "match_phrase": { + "response": "200" + } + } + } + ], + "query": { + "language": "kuery", + "query": "extension.raw : \"jpg\" or extension.raw : \"gif\" " + }, + "visualization": { + "columns": [ + { + "columnId": "bafe3009-1776-4227-a0fe-b0d6ccbb4961", + "isTransposed": false + }, + { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "isTransposed": false + }, + { + "columnId": "70d52318-354d-47d5-b33b-43d50eb34425", + "isTransposed": true + } + ], + "layerId": "4ba1a1be-6e67-434b-b3a0-f30db8ea5395", + "layerType": "data" + } + }, + "title": "lnsTableVis", + "visualizationType": "lnsDatatable" +}, + "coreMigrationVersion": "7.16.0", + "id": "a800e2b0-268c-11ec-b2b6-f1bd289a74d4", + "migrationVersion": { + "lens": "7.15.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern" + }, + { + "id": "logstash-*", + "name": "indexpattern-datasource-layer-4ba1a1be-6e67-434b-b3a0-f30db8ea5395", + "type": "index-pattern" + }, + { + "id": "logstash-*", + "name": "filter-index-pattern-0", + "type": "index-pattern" + } + ], + "type": "lens", + "updated_at": "2020-11-23T19:57:52.834Z", + "version": "WzUyLDJd" +} diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts index 002dc575e956bd..cd208630656885 100644 --- a/x-pack/test/functional/page_objects/gis_page.ts +++ b/x-pack/test/functional/page_objects/gis_page.ts @@ -521,7 +521,7 @@ export class GisPageObject extends FtrService { } async selectEMSBoundariesSource() { - this.log.debug(`Select EMS boundaries source`); + this.log.debug(`Select Elastic Maps Service boundaries source`); await this.testSubjects.click('emsBoundaries'); } diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index e26ea8f598c46f..01e860cf4bec59 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -247,6 +247,18 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }); }, + async waitForEmptyWorkspace() { + await retry.try(async () => { + await testSubjects.existOrFail(`empty-workspace`); + }); + }, + + async waitForWorkspaceWithVisualization() { + await retry.try(async () => { + await testSubjects.existOrFail(`lnsVisualizationContainer`); + }); + }, + async waitForFieldMissing(field: string) { await retry.try(async () => { await testSubjects.missingOrFail(`lnsFieldListPanelField-${field}`); @@ -1096,6 +1108,17 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.click('lnsFormula-fullscreen'); }, + async goToListingPageViaBreadcrumbs() { + await retry.try(async () => { + await testSubjects.click('breadcrumb first'); + if (await testSubjects.exists('appLeaveConfirmModal')) { + await testSubjects.exists('confirmModalConfirmButton'); + await testSubjects.click('confirmModalConfirmButton'); + } + await testSubjects.existOrFail('visualizationLandingPage', { timeout: 3000 }); + }); + }, + async typeFormula(formula: string) { await find.byCssSelector('.monaco-editor'); await find.clickByCssSelectorWhenNotDisabled('.monaco-editor'); diff --git a/x-pack/test/functional/page_objects/synthetics_integration_page.ts b/x-pack/test/functional/page_objects/synthetics_integration_page.ts index 5551ea2c3bcd0d..80c4699f6c2115 100644 --- a/x-pack/test/functional/page_objects/synthetics_integration_page.ts +++ b/x-pack/test/functional/page_objects/synthetics_integration_page.ts @@ -61,7 +61,7 @@ export function SyntheticsIntegrationPageProvider({ * Determines if the policy was created successfully by looking for the creation success toast */ async isPolicyCreatedSuccessfully() { - await testSubjects.existOrFail('packagePolicyCreateSuccessToast'); + await testSubjects.existOrFail('postInstallAddAgentModal'); }, /** diff --git a/x-pack/test/rule_registry/common/lib/authentication/users.ts b/x-pack/test/rule_registry/common/lib/authentication/users.ts index e142b3d1f56a34..39f837c6df41d3 100644 --- a/x-pack/test/rule_registry/common/lib/authentication/users.ts +++ b/x-pack/test/rule_registry/common/lib/authentication/users.ts @@ -173,21 +173,6 @@ export const obsSecReadSpacesAll: User = { roles: [securitySolutionOnlyReadSpacesAll.name, observabilityOnlyReadSpacesAll.name], }; -/** - * These users are for the security_only tests because most of them have access to the default space instead of 'space1' - */ -export const usersDefaultSpace = [ - superUser, - secOnlySpacesAll, - secOnlyReadSpacesAll, - obsOnlySpacesAll, - obsOnlyReadSpacesAll, - obsSecSpacesAll, - obsSecReadSpacesAll, - globalRead, - noKibanaPrivileges, -]; - /** * Trial users with trial roles */ diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts index 1ffbd239624d2d..2c1fbf442b0ecb 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts @@ -60,7 +60,7 @@ const createTestCases = (spaceId: string) => { { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1, namespaces: [SPACE_1_ID] }, // second try searches for it in a single other space, which is valid { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, namespaces: [SPACE_2_ID], ...fail404() }, { ...CASES.MULTI_NAMESPACE_ALL_SPACES, namespaces: [SPACE_2_ID, 'x'] }, // unknown space is allowed / ignored - { ...CASES.MULTI_NAMESPACE_ALL_SPACES, namespaces: [ALL_SPACES_ID] }, // this is different than the same test case in the spaces_only and security_only suites, since MULTI_NAMESPACE_ONLY_SPACE_1 *may* return a 404 error to a partially authorized user + { ...CASES.MULTI_NAMESPACE_ALL_SPACES, namespaces: [ALL_SPACES_ID] }, // this is different than the same test case in the spaces_only suite, since MULTI_NAMESPACE_ONLY_SPACE_1 *may* return a 404 error to a partially authorized user ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; const allTypes = [...normalTypes, ...crossNamespace, ...hiddenType]; diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts deleted file mode 100644 index 4b3b2126dd8c2c..00000000000000 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces'; -import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; -import { TestUser } from '../../common/lib/types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - bulkCreateTestSuiteFactory, - TEST_CASES as CASES, - BulkCreateTestDefinition, -} from '../../common/suites/bulk_create'; - -const { - DEFAULT: { spaceId: DEFAULT_SPACE_ID }, -} = SPACES; -const { fail400, fail409 } = testCaseFailures; -const unresolvableConflict = () => ({ fail409Param: 'unresolvableConflict' }); - -const createTestCases = (overwrite: boolean) => { - // for each permitted (non-403) outcome, if failure !== undefined then we expect - // to receive an error; otherwise, we expect to receive a success result - const expectedNamespaces = [DEFAULT_SPACE_ID]; // newly created objects should have this `namespaces` array in their return value - const normalTypes = [ - { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, expectedNamespaces }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, expectedNamespaces }, - { ...CASES.MULTI_NAMESPACE_ALL_SPACES, ...fail409(!overwrite) }, - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(), ...unresolvableConflict() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(), ...unresolvableConflict() }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_DEFAULT_SPACE, ...fail409(!overwrite) }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1, ...fail409(), ...unresolvableConflict() }, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, - { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, - CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, - { - ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, - initialNamespaces: ['x', 'y'], - ...fail400(), // cannot be created in multiple spaces - }, - CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid - { - ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, - initialNamespaces: [ALL_SPACES_ID], - ...fail400(), // cannot be created in multiple spaces - }, - CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid - CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE, - CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES, - ]; - const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; - const allTypes = normalTypes.concat(hiddenType); - return { normalTypes, hiddenType, allTypes }; -}; - -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions, expectSavedObjectForbidden } = - bulkCreateTestSuiteFactory(esArchiver, supertest); - const createTests = (overwrite: boolean, user: TestUser) => { - const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite); - // use singleRequest to reduce execution time and/or test combined cases - return { - unauthorized: createTestDefinitions(allTypes, true, overwrite, { user }), - authorized: [ - createTestDefinitions(normalTypes, false, overwrite, { user, singleRequest: true }), - createTestDefinitions(hiddenType, true, overwrite, { user }), - createTestDefinitions(allTypes, true, overwrite, { - user, - singleRequest: true, - responseBodyOverride: expectSavedObjectForbidden(['hiddentype']), - }), - ].flat(), - superuser: createTestDefinitions(allTypes, false, overwrite, { user, singleRequest: true }), - }; - }; - - describe('_bulk_create', () => { - getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { - const suffix = overwrite ? ' with overwrite enabled' : ''; - const _addTests = (user: TestUser, tests: BulkCreateTestDefinition[]) => { - addTests(`${user.description}${suffix}`, { user, tests }); - }; - - [ - users.noAccess, - users.legacyAll, - users.dualRead, - users.readGlobally, - users.allAtDefaultSpace, - users.readAtDefaultSpace, - users.allAtSpace1, - users.readAtSpace1, - ].forEach((user) => { - const { unauthorized } = createTests(overwrite!, user); - _addTests(user, unauthorized); - }); - [users.dualAll, users.allGlobally].forEach((user) => { - const { authorized } = createTests(overwrite!, user); - _addTests(user, authorized); - }); - const { superuser } = createTests(overwrite!, users.superuser); - _addTests(users.superuser, superuser); - }); - }); -} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_get.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_get.ts deleted file mode 100644 index 4aa722bfc6b07c..00000000000000 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_get.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces'; -import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; -import { TestUser } from '../../common/lib/types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - bulkGetTestSuiteFactory, - TEST_CASES as CASES, - BulkGetTestDefinition, -} from '../../common/suites/bulk_get'; - -const { - SPACE_1: { spaceId: SPACE_1_ID }, - SPACE_2: { spaceId: SPACE_2_ID }, -} = SPACES; -const { fail400, fail404 } = testCaseFailures; - -const createTestCases = () => { - // for each permitted (non-403) outcome, if failure !== undefined then we expect - // to receive an error; otherwise, we expect to receive a success result - const normalTypes = [ - CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404() }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404() }, - CASES.MULTI_NAMESPACE_ALL_SPACES, - CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404() }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_DEFAULT_SPACE }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1, ...fail404() }, - CASES.NAMESPACE_AGNOSTIC, - { ...CASES.DOES_NOT_EXIST, ...fail404() }, - { - ...CASES.SINGLE_NAMESPACE_SPACE_2, - namespaces: ['x', 'y'], - ...fail400(), // cannot be searched for in multiple spaces - }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, namespaces: [SPACE_2_ID] }, // second try searches for it in a single other space, which is valid - { - ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1, - namespaces: [ALL_SPACES_ID], - ...fail400(), // cannot be searched for in multiple spaces - }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1, namespaces: [SPACE_1_ID] }, // second try searches for it in a single other space, which is valid - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, namespaces: [SPACE_2_ID], ...fail404() }, - { ...CASES.MULTI_NAMESPACE_ALL_SPACES, namespaces: [SPACE_2_ID, 'x'] }, // unknown space is allowed / ignored - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, namespaces: [ALL_SPACES_ID] }, - ]; - const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; - const allTypes = normalTypes.concat(hiddenType); - return { normalTypes, hiddenType, allTypes }; -}; - -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions, expectSavedObjectForbidden } = bulkGetTestSuiteFactory( - esArchiver, - supertest - ); - const createTests = () => { - const { normalTypes, hiddenType, allTypes } = createTestCases(); - // use singleRequest to reduce execution time and/or test combined cases - return { - unauthorized: createTestDefinitions(allTypes, true), - authorized: [ - createTestDefinitions(normalTypes, false, { singleRequest: true }), - createTestDefinitions(hiddenType, true), - createTestDefinitions(allTypes, true, { - singleRequest: true, - responseBodyOverride: expectSavedObjectForbidden(['hiddentype']), - }), - ].flat(), - superuser: createTestDefinitions(allTypes, false, { singleRequest: true }), - }; - }; - - describe('_bulk_get', () => { - getTestScenarios().security.forEach(({ users }) => { - const { unauthorized, authorized, superuser } = createTests(); - const _addTests = (user: TestUser, tests: BulkGetTestDefinition[]) => { - addTests(user.description, { user, tests }); - }; - - [ - users.noAccess, - users.legacyAll, - users.allAtDefaultSpace, - users.readAtDefaultSpace, - users.allAtSpace1, - users.readAtSpace1, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - [users.dualAll, users.dualRead, users.allGlobally, users.readGlobally].forEach((user) => { - _addTests(user, authorized); - }); - _addTests(users.superuser, superuser); - }); - }); -} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_resolve.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_resolve.ts deleted file mode 100644 index 6d91cf8eae67da..00000000000000 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_resolve.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; -import { TestUser } from '../../common/lib/types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - bulkResolveTestSuiteFactory, - TEST_CASES as CASES, - BulkResolveTestDefinition, -} from '../../common/suites/bulk_resolve'; - -const { fail400, fail404 } = testCaseFailures; - -const createTestCases = () => { - // for each permitted (non-403) outcome, if failure !== undefined then we expect - // to receive an error; otherwise, we expect to receive a success result - const normalTypes = [ - { ...CASES.EXACT_MATCH }, - { ...CASES.ALIAS_MATCH, ...fail404() }, - { ...CASES.CONFLICT, expectedOutcome: 'exactMatch' as const }, - { ...CASES.DISABLED, ...fail404() }, - { ...CASES.DOES_NOT_EXIST, ...fail404() }, - ]; - const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; - const allTypes = [...normalTypes, ...hiddenType]; - return { normalTypes, hiddenType, allTypes }; -}; - -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions } = bulkResolveTestSuiteFactory(esArchiver, supertest); - const createTests = () => { - const { normalTypes, hiddenType, allTypes } = createTestCases(); - return { - unauthorized: createTestDefinitions(allTypes, true), - authorized: [ - createTestDefinitions(normalTypes, false, { singleRequest: true }), - createTestDefinitions(hiddenType, true), - ].flat(), - superuser: createTestDefinitions(allTypes, false, { singleRequest: true }), - }; - }; - - describe('_bulk_resolve', () => { - getTestScenarios().security.forEach(({ users }) => { - const { unauthorized, authorized, superuser } = createTests(); - const _addTests = (user: TestUser, tests: BulkResolveTestDefinition[]) => { - addTests(user.description, { user, tests }); - }; - - [ - users.noAccess, - users.legacyAll, - users.allAtDefaultSpace, - users.readAtDefaultSpace, - users.allAtSpace1, - users.readAtSpace1, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - [users.dualAll, users.dualRead, users.allGlobally, users.readGlobally].forEach((user) => { - _addTests(user, authorized); - }); - _addTests(users.superuser, superuser); - }); - }); -} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts deleted file mode 100644 index 77567f296aa6da..00000000000000 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SPACES } from '../../common/lib/spaces'; -import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; -import { TestUser } from '../../common/lib/types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - bulkUpdateTestSuiteFactory, - TEST_CASES as CASES, - BulkUpdateTestDefinition, -} from '../../common/suites/bulk_update'; - -const { - DEFAULT: { spaceId: DEFAULT_SPACE_ID }, - SPACE_1: { spaceId: SPACE_1_ID }, - SPACE_2: { spaceId: SPACE_2_ID }, -} = SPACES; -const { fail404 } = testCaseFailures; - -const createTestCases = () => { - // for each permitted (non-403) outcome, if failure !== undefined then we expect - // to receive an error; otherwise, we expect to receive a success result - const normalTypes = [ - CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404() }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404() }, - CASES.MULTI_NAMESPACE_ALL_SPACES, - CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404() }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_DEFAULT_SPACE }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1, ...fail404() }, - CASES.NAMESPACE_AGNOSTIC, - { ...CASES.DOES_NOT_EXIST, ...fail404() }, - ]; - const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; - const allTypes = normalTypes.concat(hiddenType); - // an "object namespace" string can be specified for individual objects (to bulkUpdate across namespaces) - // even if the Spaces plugin is disabled, this should work, as `namespace` is handled by the Core API - const withObjectNamespaces = [ - { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, namespace: DEFAULT_SPACE_ID }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, namespace: SPACE_1_ID }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, namespace: SPACE_1_ID, ...fail404() }, // intentional 404 test case - { ...CASES.MULTI_NAMESPACE_ALL_SPACES, namespace: DEFAULT_SPACE_ID }, // any spaceId will work (not '*') - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, namespace: DEFAULT_SPACE_ID }, // SPACE_1_ID would also work - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, namespace: SPACE_2_ID, ...fail404() }, // intentional 404 test case - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, namespace: SPACE_2_ID }, - CASES.NAMESPACE_AGNOSTIC, // any namespace would work and would make no difference - { ...CASES.DOES_NOT_EXIST, ...fail404() }, - ]; - return { normalTypes, hiddenType, allTypes, withObjectNamespaces }; -}; - -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions, expectSavedObjectForbidden } = - bulkUpdateTestSuiteFactory(esArchiver, supertest); - const createTests = () => { - const { normalTypes, hiddenType, allTypes, withObjectNamespaces } = createTestCases(); - // use singleRequest to reduce execution time and/or test combined cases - return { - unauthorized: [ - createTestDefinitions(allTypes, true), - createTestDefinitions(withObjectNamespaces, true, { singleRequest: true }), - ].flat(), - authorized: [ - createTestDefinitions(normalTypes, false, { singleRequest: true }), - createTestDefinitions(hiddenType, true), - createTestDefinitions(allTypes, true, { - singleRequest: true, - responseBodyOverride: expectSavedObjectForbidden(['hiddentype']), - }), - createTestDefinitions(withObjectNamespaces, false, { singleRequest: true }), - ].flat(), - superuser: [ - createTestDefinitions(allTypes, false, { singleRequest: true }), - createTestDefinitions(withObjectNamespaces, false, { singleRequest: true }), - ].flat(), - }; - }; - - describe('_bulk_update', () => { - getTestScenarios().security.forEach(({ users }) => { - const { unauthorized, authorized, superuser } = createTests(); - const _addTests = (user: TestUser, tests: BulkUpdateTestDefinition[]) => { - addTests(user.description, { user, tests }); - }; - - [ - users.noAccess, - users.legacyAll, - users.dualRead, - users.readGlobally, - users.allAtDefaultSpace, - users.readAtDefaultSpace, - users.allAtSpace1, - users.readAtSpace1, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - [users.dualAll, users.allGlobally].forEach((user) => { - _addTests(user, authorized); - }); - _addTests(users.superuser, superuser); - }); - }); -} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts deleted file mode 100644 index 67195637f0c0ac..00000000000000 --- a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces'; -import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; -import { TestUser } from '../../common/lib/types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - createTestSuiteFactory, - TEST_CASES as CASES, - CreateTestDefinition, -} from '../../common/suites/create'; - -const { - DEFAULT: { spaceId: DEFAULT_SPACE_ID }, -} = SPACES; -const { fail400, fail409 } = testCaseFailures; - -const createTestCases = (overwrite: boolean) => { - // for each permitted (non-403) outcome, if failure !== undefined then we expect - // to receive an error; otherwise, we expect to receive a success result - const expectedNamespaces = [DEFAULT_SPACE_ID]; // newly created objects should have this `namespaces` array in their return value - const normalTypes = [ - { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, expectedNamespaces }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, expectedNamespaces }, - { ...CASES.MULTI_NAMESPACE_ALL_SPACES, ...fail409(!overwrite) }, - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409() }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_DEFAULT_SPACE, ...fail409(!overwrite) }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1, ...fail409() }, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, - { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, - CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, - { - ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, - initialNamespaces: ['x', 'y'], - ...fail400(), // cannot be created in multiple spaces - }, - CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid - { - ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, - initialNamespaces: [ALL_SPACES_ID], - ...fail400(), // cannot be created in multiple spaces - }, - CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid - CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE, - CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES, - ]; - const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; - const allTypes = normalTypes.concat(hiddenType); - return { normalTypes, hiddenType, allTypes }; -}; - -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions } = createTestSuiteFactory(esArchiver, supertest); - const createTests = (overwrite: boolean, user: TestUser) => { - const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite); - return { - unauthorized: createTestDefinitions(allTypes, true, overwrite, { user }), - authorized: [ - createTestDefinitions(normalTypes, false, overwrite, { user }), - createTestDefinitions(hiddenType, true, overwrite, { user }), - ].flat(), - superuser: createTestDefinitions(allTypes, false, overwrite, { user }), - }; - }; - - describe('_create', () => { - getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { - const suffix = overwrite ? ' with overwrite enabled' : ''; - const _addTests = (user: TestUser, tests: CreateTestDefinition[]) => { - addTests(`${user.description}${suffix}`, { user, tests }); - }; - - [ - users.noAccess, - users.legacyAll, - users.dualRead, - users.readGlobally, - users.allAtDefaultSpace, - users.readAtDefaultSpace, - users.allAtSpace1, - users.readAtSpace1, - ].forEach((user) => { - const { unauthorized } = createTests(overwrite!, user); - _addTests(user, unauthorized); - }); - [users.dualAll, users.allGlobally].forEach((user) => { - const { authorized } = createTests(overwrite!, user); - _addTests(user, authorized); - }); - const { superuser } = createTests(overwrite!, users.superuser); - _addTests(users.superuser, superuser); - }); - }); -} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/delete.ts b/x-pack/test/saved_object_api_integration/security_only/apis/delete.ts deleted file mode 100644 index 7d9ec0b152174f..00000000000000 --- a/x-pack/test/saved_object_api_integration/security_only/apis/delete.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; -import { TestUser } from '../../common/lib/types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - deleteTestSuiteFactory, - TEST_CASES as CASES, - DeleteTestDefinition, -} from '../../common/suites/delete'; - -const { fail400, fail404 } = testCaseFailures; - -const createTestCases = () => { - // for each permitted (non-403) outcome, if failure !== undefined then we expect - // to receive an error; otherwise, we expect to receive a success result - const normalTypes = [ - CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404() }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404() }, - { ...CASES.MULTI_NAMESPACE_ALL_SPACES, ...fail400() }, - // try to delete this object again, this time using the `force` option - { ...CASES.MULTI_NAMESPACE_ALL_SPACES, force: true }, - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, - // try to delete this object again, this time using the `force` option - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, force: true }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404() }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_DEFAULT_SPACE }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1, ...fail404() }, - CASES.NAMESPACE_AGNOSTIC, - { ...CASES.DOES_NOT_EXIST, ...fail404() }, - ]; - const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; - const allTypes = normalTypes.concat(hiddenType); - return { normalTypes, hiddenType, allTypes }; -}; - -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions } = deleteTestSuiteFactory(esArchiver, supertest); - const createTests = () => { - const { normalTypes, hiddenType, allTypes } = createTestCases(); - return { - unauthorized: createTestDefinitions(allTypes, true), - authorized: [ - createTestDefinitions(normalTypes, false), - createTestDefinitions(hiddenType, true), - ].flat(), - superuser: createTestDefinitions(allTypes, false), - }; - }; - - describe('_delete', () => { - getTestScenarios().security.forEach(({ users }) => { - const { unauthorized, authorized, superuser } = createTests(); - const _addTests = (user: TestUser, tests: DeleteTestDefinition[]) => { - addTests(user.description, { user, tests }); - }; - - [ - users.noAccess, - users.legacyAll, - users.dualRead, - users.readGlobally, - users.allAtDefaultSpace, - users.readAtDefaultSpace, - users.allAtSpace1, - users.readAtSpace1, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - [users.dualAll, users.allGlobally].forEach((user) => { - _addTests(user, authorized); - }); - _addTests(users.superuser, superuser); - }); - }); -} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/export.ts b/x-pack/test/saved_object_api_integration/security_only/apis/export.ts deleted file mode 100644 index 2cba94967e5de8..00000000000000 --- a/x-pack/test/saved_object_api_integration/security_only/apis/export.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; -import { TestUser } from '../../common/lib/types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - exportTestSuiteFactory, - getTestCases, - ExportTestDefinition, -} from '../../common/suites/export'; - -const createTestCases = () => { - const cases = getTestCases(); - const exportableObjects = [ - cases.singleNamespaceObject, - cases.multiNamespaceObject, - cases.multiNamespaceIsolatedObject, - cases.namespaceAgnosticObject, - ]; - const exportableTypes = [ - cases.singleNamespaceType, - cases.multiNamespaceType, - cases.multiNamespaceIsolatedType, - cases.namespaceAgnosticType, - ]; - const nonExportableObjectsAndTypes = [cases.hiddenObject, cases.hiddenType]; - const allObjectsAndTypes = [ - exportableObjects, - exportableTypes, - nonExportableObjectsAndTypes, - ].flat(); - return { exportableObjects, exportableTypes, nonExportableObjectsAndTypes, allObjectsAndTypes }; -}; - -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions } = exportTestSuiteFactory(esArchiver, supertest); - const createTests = () => { - const { exportableObjects, exportableTypes, nonExportableObjectsAndTypes, allObjectsAndTypes } = - createTestCases(); - return { - unauthorized: [ - createTestDefinitions(exportableObjects, { statusCode: 403, reason: 'unauthorized' }), - createTestDefinitions(exportableTypes, { statusCode: 403, reason: 'unauthorized' }), // failure with empty result - createTestDefinitions(nonExportableObjectsAndTypes, false), - ].flat(), - authorized: createTestDefinitions(allObjectsAndTypes, false), - }; - }; - - describe('_export', () => { - getTestScenarios().security.forEach(({ users }) => { - const { unauthorized, authorized } = createTests(); - const _addTests = (user: TestUser, tests: ExportTestDefinition[]) => { - addTests(user.description, { user, tests }); - }; - - [ - users.noAccess, - users.legacyAll, - users.allAtDefaultSpace, - users.readAtDefaultSpace, - users.allAtSpace1, - users.readAtSpace1, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - [ - users.dualAll, - users.dualRead, - users.allGlobally, - users.readGlobally, - users.superuser, - ].forEach((user) => { - _addTests(user, authorized); - }); - }); - }); -} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/find.ts b/x-pack/test/saved_object_api_integration/security_only/apis/find.ts deleted file mode 100644 index eb30024015fbb2..00000000000000 --- a/x-pack/test/saved_object_api_integration/security_only/apis/find.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SPACES } from '../../common/lib/spaces'; -import { AUTHENTICATION } from '../../common/lib/authentication'; -import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; -import { TestUser } from '../../common/lib/types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { findTestSuiteFactory, getTestCases } from '../../common/suites/find'; - -const { - DEFAULT: { spaceId: DEFAULT_SPACE_ID }, - SPACE_1: { spaceId: SPACE_1_ID }, - SPACE_2: { spaceId: SPACE_2_ID }, -} = SPACES; - -const createTestCases = (crossSpaceSearch?: string[]) => { - const cases = getTestCases({ crossSpaceSearch }); - - const normalTypes = [ - cases.singleNamespaceType, - cases.multiNamespaceType, - cases.multiNamespaceIsolatedType, - cases.namespaceAgnosticType, - cases.eachType, - cases.pageBeyondTotal, - cases.unknownSearchField, - cases.filterWithNamespaceAgnosticType, - cases.filterWithDisallowedType, - ]; - const hiddenAndUnknownTypes = [ - cases.hiddenType, - cases.unknownType, - cases.filterWithHiddenType, - cases.filterWithUnknownType, - ]; - const allTypes = normalTypes.concat(hiddenAndUnknownTypes); - return { normalTypes, hiddenAndUnknownTypes, allTypes }; -}; - -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions } = findTestSuiteFactory(esArchiver, supertest); - const createTests = (user: TestUser) => { - const defaultCases = createTestCases(); - const crossSpaceCases = createTestCases([DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]); - - if (user.username === AUTHENTICATION.SUPERUSER.username) { - return { - defaultCases: createTestDefinitions(defaultCases.allTypes, false, { user }), - crossSpace: createTestDefinitions( - crossSpaceCases.allTypes, - { statusCode: 400, reason: 'cross_namespace_not_permitted' }, - { user } - ), - }; - } - - const isAuthorizedGlobally = user.authorizedAtSpaces.includes('*'); - - return { - defaultCases: isAuthorizedGlobally - ? [ - createTestDefinitions(defaultCases.normalTypes, false, { user }), - createTestDefinitions(defaultCases.hiddenAndUnknownTypes, { - statusCode: 200, - reason: 'unauthorized', - }), - ].flat() - : createTestDefinitions(defaultCases.allTypes, { statusCode: 200, reason: 'unauthorized' }), - crossSpace: createTestDefinitions( - crossSpaceCases.allTypes, - { statusCode: 400, reason: 'cross_namespace_not_permitted' }, - { user } - ), - }; - }; - - describe('_find', () => { - getTestScenarios().security.forEach(({ users }) => { - Object.values(users).forEach((user) => { - const { defaultCases, crossSpace } = createTests(user); - addTests(`${user.description}`, { user, tests: [...defaultCases, ...crossSpace] }); - }); - }); - }); -} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/get.ts b/x-pack/test/saved_object_api_integration/security_only/apis/get.ts deleted file mode 100644 index 9910900c2f51bc..00000000000000 --- a/x-pack/test/saved_object_api_integration/security_only/apis/get.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; -import { TestUser } from '../../common/lib/types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - getTestSuiteFactory, - TEST_CASES as CASES, - GetTestDefinition, -} from '../../common/suites/get'; - -const { fail404 } = testCaseFailures; - -const createTestCases = () => { - // for each permitted (non-403) outcome, if failure !== undefined then we expect - // to receive an error; otherwise, we expect to receive a success result - const normalTypes = [ - CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404() }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404() }, - CASES.MULTI_NAMESPACE_ALL_SPACES, - CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404() }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_DEFAULT_SPACE }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1, ...fail404() }, - CASES.NAMESPACE_AGNOSTIC, - { ...CASES.DOES_NOT_EXIST, ...fail404() }, - ]; - const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; - const allTypes = normalTypes.concat(hiddenType); - return { normalTypes, hiddenType, allTypes }; -}; - -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions } = getTestSuiteFactory(esArchiver, supertest); - const createTests = () => { - const { normalTypes, hiddenType, allTypes } = createTestCases(); - return { - unauthorized: createTestDefinitions(allTypes, true), - authorized: [ - createTestDefinitions(normalTypes, false), - createTestDefinitions(hiddenType, true), - ].flat(), - superuser: createTestDefinitions(allTypes, false), - }; - }; - - describe('_get', () => { - getTestScenarios().security.forEach(({ users }) => { - const { unauthorized, authorized, superuser } = createTests(); - const _addTests = (user: TestUser, tests: GetTestDefinition[]) => { - addTests(user.description, { user, tests }); - }; - - [ - users.noAccess, - users.legacyAll, - users.allAtDefaultSpace, - users.readAtDefaultSpace, - users.allAtSpace1, - users.readAtSpace1, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - [users.dualAll, users.dualRead, users.allGlobally, users.readGlobally].forEach((user) => { - _addTests(user, authorized); - }); - _addTests(users.superuser, superuser); - }); - }); -} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts deleted file mode 100644 index 6f0d48fbf1b52f..00000000000000 --- a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; -import { TestUser } from '../../common/lib/types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - importTestSuiteFactory, - TEST_CASES as CASES, - ImportTestDefinition, -} from '../../common/suites/import'; - -const { fail400, fail409 } = testCaseFailures; -const destinationId = (condition?: boolean) => - condition !== false ? { successParam: 'destinationId' } : {}; -const newCopy = () => ({ successParam: 'createNewCopy' }); -const ambiguousConflict = (suffix: string) => ({ - failure: 409 as 409, - fail409Param: `ambiguous_conflict_${suffix}`, -}); - -const createNewCopiesTestCases = () => { - // for each outcome, if failure !== undefined then we expect to receive - // an error; otherwise, we expect to receive a success result - const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); - const importable = cases.map(([, val]) => ({ ...val, successParam: 'createNewCopies' })); - const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; - const all = [...importable, ...nonImportable]; - return { importable, nonImportable, all }; -}; - -const createTestCases = (overwrite: boolean) => { - // for each permitted (non-403) outcome, if failure !== undefined then we expect - // to receive an error; otherwise, we expect to receive a success result - const group1Importable = [ - // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes - { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, - CASES.SINGLE_NAMESPACE_SPACE_1, - CASES.SINGLE_NAMESPACE_SPACE_2, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, - ]; - const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; - const group1All = group1Importable.concat(group1NonImportable); - const group2 = [ - // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes - CASES.NEW_MULTI_NAMESPACE_OBJ, - { ...CASES.MULTI_NAMESPACE_ALL_SPACES, ...fail409(!overwrite) }, - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...destinationId() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...destinationId() }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_DEFAULT_SPACE, ...fail409(!overwrite) }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1, ...destinationId() }, - { ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID - { ...CASES.CONFLICT_1B_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID - { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict - { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict - ]; - const group3 = [ - // when overwrite=true, all of the objects in this group are errors, so we cannot check the created object attributes - // grouping errors together simplifies the test suite code - { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict - ]; - const group4 = [ - // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes - { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict - CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match - CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match - { ...CASES.CONFLICT_2C_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID - { ...CASES.CONFLICT_2D_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID - ]; - return { group1Importable, group1NonImportable, group1All, group2, group3, group4 }; -}; - -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - const es = getService('es'); - - const { addTests, createTestDefinitions, expectSavedObjectForbidden } = importTestSuiteFactory( - es, - esArchiver, - supertest - ); - const createTests = (overwrite: boolean, createNewCopies: boolean) => { - // use singleRequest to reduce execution time and/or test combined cases - const singleRequest = true; - - if (createNewCopies) { - const { importable, nonImportable, all } = createNewCopiesTestCases(); - return { - unauthorized: [ - createTestDefinitions(importable, true, { createNewCopies }), - createTestDefinitions(nonImportable, false, { createNewCopies, singleRequest }), - createTestDefinitions(all, true, { - createNewCopies, - singleRequest, - responseBodyOverride: expectSavedObjectForbidden([ - 'dashboard', - 'globaltype', - 'isolatedtype', - 'sharedtype', - 'sharecapabletype', - ]), - }), - ].flat(), - authorized: createTestDefinitions(all, false, { createNewCopies, singleRequest }), - }; - } - - const { group1Importable, group1NonImportable, group1All, group2, group3, group4 } = - createTestCases(overwrite); - return { - unauthorized: [ - createTestDefinitions(group1Importable, true, { overwrite }), - createTestDefinitions(group1NonImportable, false, { overwrite, singleRequest }), - createTestDefinitions(group1All, true, { - overwrite, - singleRequest, - responseBodyOverride: expectSavedObjectForbidden([ - 'dashboard', - 'globaltype', - 'isolatedtype', - ]), - }), - createTestDefinitions(group2, true, { overwrite, singleRequest }), - createTestDefinitions(group3, true, { overwrite, singleRequest }), - createTestDefinitions(group4, true, { overwrite, singleRequest }), - ].flat(), - authorized: [ - createTestDefinitions(group1All, false, { overwrite, singleRequest }), - createTestDefinitions(group2, false, { overwrite, singleRequest }), - createTestDefinitions(group3, false, { overwrite, singleRequest }), - createTestDefinitions(group4, false, { overwrite, singleRequest }), - ].flat(), - }; - }; - - describe('_import', () => { - getTestScenarios([ - [false, false], - [false, true], - [true, false], - ]).security.forEach(({ users, modifier }) => { - const [overwrite, createNewCopies] = modifier!; - const suffix = overwrite - ? ' with overwrite enabled' - : createNewCopies - ? ' with createNewCopies enabled' - : ''; - const { unauthorized, authorized } = createTests(overwrite, createNewCopies); - const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => { - addTests(`${user.description}${suffix}`, { user, tests }); - }; - - [ - users.noAccess, - users.legacyAll, - users.dualRead, - users.readGlobally, - users.allAtDefaultSpace, - users.readAtDefaultSpace, - users.allAtSpace1, - users.readAtSpace1, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - [users.dualAll, users.allGlobally, users.superuser].forEach((user) => { - _addTests(user, authorized); - }); - }); - }); -} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/index.ts b/x-pack/test/saved_object_api_integration/security_only/apis/index.ts deleted file mode 100644 index 35fd8c6e0b3d92..00000000000000 --- a/x-pack/test/saved_object_api_integration/security_only/apis/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createUsersAndRoles } from '../../common/lib/create_users_and_roles'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function ({ getService, loadTestFile }: FtrProviderContext) { - const es = getService('es'); - const supertest = getService('supertest'); - - describe('saved objects security only enabled', function () { - this.tags('ciGroup9'); - - before(async () => { - await createUsersAndRoles(es, supertest); - }); - - loadTestFile(require.resolve('./bulk_create')); - loadTestFile(require.resolve('./bulk_get')); - loadTestFile(require.resolve('./bulk_resolve')); - loadTestFile(require.resolve('./bulk_update')); - loadTestFile(require.resolve('./create')); - loadTestFile(require.resolve('./delete')); - loadTestFile(require.resolve('./export')); - loadTestFile(require.resolve('./find')); - loadTestFile(require.resolve('./get')); - loadTestFile(require.resolve('./import')); - loadTestFile(require.resolve('./resolve_import_errors')); - loadTestFile(require.resolve('./resolve')); - loadTestFile(require.resolve('./update')); - }); -} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/resolve.ts b/x-pack/test/saved_object_api_integration/security_only/apis/resolve.ts deleted file mode 100644 index fc4148a88c9791..00000000000000 --- a/x-pack/test/saved_object_api_integration/security_only/apis/resolve.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; -import { TestUser } from '../../common/lib/types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - resolveTestSuiteFactory, - TEST_CASES as CASES, - ResolveTestDefinition, -} from '../../common/suites/resolve'; - -const { fail400, fail404 } = testCaseFailures; - -const createTestCases = () => { - // for each permitted (non-403) outcome, if failure !== undefined then we expect - // to receive an error; otherwise, we expect to receive a success result - const normalTypes = [ - { ...CASES.EXACT_MATCH }, - { ...CASES.ALIAS_MATCH, ...fail404() }, - { ...CASES.CONFLICT, expectedOutcome: 'exactMatch' as const }, - { ...CASES.DISABLED, ...fail404() }, - { ...CASES.DOES_NOT_EXIST, ...fail404() }, - ]; - const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; - const allTypes = [...normalTypes, ...hiddenType]; - return { normalTypes, hiddenType, allTypes }; -}; - -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions } = resolveTestSuiteFactory(esArchiver, supertest); - const createTests = () => { - const { normalTypes, hiddenType, allTypes } = createTestCases(); - return { - unauthorized: createTestDefinitions(allTypes, true), - authorized: [ - createTestDefinitions(normalTypes, false), - createTestDefinitions(hiddenType, true), - ].flat(), - superuser: createTestDefinitions(allTypes, false), - }; - }; - - describe('_resolve', () => { - getTestScenarios().security.forEach(({ users }) => { - const { unauthorized, authorized, superuser } = createTests(); - const _addTests = (user: TestUser, tests: ResolveTestDefinition[]) => { - addTests(user.description, { user, tests }); - }; - - [ - users.noAccess, - users.legacyAll, - users.allAtDefaultSpace, - users.readAtDefaultSpace, - users.allAtSpace1, - users.readAtSpace1, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - [users.dualAll, users.dualRead, users.allGlobally, users.readGlobally].forEach((user) => { - _addTests(user, authorized); - }); - _addTests(users.superuser, superuser); - }); - }); -} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts deleted file mode 100644 index b524e312132212..00000000000000 --- a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { v4 as uuidv4 } from 'uuid'; -import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; -import { TestUser } from '../../common/lib/types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - resolveImportErrorsTestSuiteFactory, - TEST_CASES as CASES, - ResolveImportErrorsTestDefinition, -} from '../../common/suites/resolve_import_errors'; - -const { fail400, fail409 } = testCaseFailures; -const destinationId = (condition?: boolean) => - condition !== false ? { successParam: 'destinationId' } : {}; -const newCopy = () => ({ successParam: 'createNewCopy' }); - -const createNewCopiesTestCases = () => { - // for each outcome, if failure !== undefined then we expect to receive - // an error; otherwise, we expect to receive a success result - const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); - const importable = cases.map(([, val]) => ({ - ...val, - successParam: 'createNewCopies', - expectedNewId: uuidv4(), - })); - const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; - const all = [...importable, ...nonImportable]; - return { importable, nonImportable, all }; -}; - -const createTestCases = (overwrite: boolean) => { - // for each permitted (non-403) outcome, if failure !== undefined then we expect - // to receive an error; otherwise, we expect to receive a success result - const group1Importable = [ - { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - ]; - const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; - const group1All = [...group1Importable, ...group1NonImportable]; - const group2 = [ - { ...CASES.MULTI_NAMESPACE_ALL_SPACES, ...fail409(!overwrite) }, - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_DEFAULT_SPACE, ...fail409(!overwrite) }, - { ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID - { ...CASES.CONFLICT_1B_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID - // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict - // if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that - // `expectedDestinationId` already exists - { ...CASES.CONFLICT_2C_OBJ, ...fail409(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a' - { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' - { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' - ]; - return { group1Importable, group1NonImportable, group1All, group2 }; -}; - -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - const es = getService('es'); - - const { addTests, createTestDefinitions, expectSavedObjectForbidden } = - resolveImportErrorsTestSuiteFactory(es, esArchiver, supertest); - const createTests = (overwrite: boolean, createNewCopies: boolean) => { - // use singleRequest to reduce execution time and/or test combined cases - const singleRequest = true; - - if (createNewCopies) { - const { importable, nonImportable, all } = createNewCopiesTestCases(); - return { - unauthorized: [ - createTestDefinitions(importable, true, { createNewCopies }), - createTestDefinitions(nonImportable, false, { createNewCopies, singleRequest }), - createTestDefinitions(all, true, { - createNewCopies, - singleRequest, - responseBodyOverride: expectSavedObjectForbidden([ - 'globaltype', - 'isolatedtype', - 'sharedtype', - 'sharecapabletype', - ]), - }), - ].flat(), - authorized: createTestDefinitions(all, false, { createNewCopies, singleRequest }), - }; - } - - const { group1Importable, group1NonImportable, group1All, group2 } = createTestCases(overwrite); - return { - unauthorized: [ - createTestDefinitions(group1Importable, true, { overwrite }), - createTestDefinitions(group1NonImportable, false, { overwrite, singleRequest }), - createTestDefinitions(group1All, true, { - overwrite, - singleRequest, - responseBodyOverride: expectSavedObjectForbidden(['globaltype', 'isolatedtype']), - }), - createTestDefinitions(group2, true, { overwrite, singleRequest }), - ].flat(), - authorized: [ - createTestDefinitions(group1All, false, { overwrite, singleRequest }), - createTestDefinitions(group2, false, { overwrite, singleRequest }), - ].flat(), - }; - }; - - describe('_resolve_import_errors', () => { - getTestScenarios([ - [false, false], - [false, true], - [true, false], - ]).security.forEach(({ users, modifier }) => { - const [overwrite, createNewCopies] = modifier!; - const suffix = overwrite - ? ' with overwrite enabled' - : createNewCopies - ? ' with createNewCopies enabled' - : ''; - const { unauthorized, authorized } = createTests(overwrite, createNewCopies); - const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { - addTests(`${user.description}${suffix}`, { user, tests }); - }; - - [ - users.noAccess, - users.legacyAll, - users.dualRead, - users.readGlobally, - users.allAtDefaultSpace, - users.readAtDefaultSpace, - users.allAtSpace1, - users.readAtSpace1, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - [users.dualAll, users.allGlobally, users.superuser].forEach((user) => { - _addTests(user, authorized); - }); - }); - }); -} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/update.ts b/x-pack/test/saved_object_api_integration/security_only/apis/update.ts deleted file mode 100644 index c0ec36fcf75c4d..00000000000000 --- a/x-pack/test/saved_object_api_integration/security_only/apis/update.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; -import { TestUser } from '../../common/lib/types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - updateTestSuiteFactory, - TEST_CASES as CASES, - UpdateTestDefinition, -} from '../../common/suites/update'; - -const { fail404 } = testCaseFailures; - -const createTestCases = () => { - // for each permitted (non-403) outcome, if failure !== undefined then we expect - // to receive an error; otherwise, we expect to receive a success result - const normalTypes = [ - CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404() }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404() }, - CASES.MULTI_NAMESPACE_ALL_SPACES, - CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404() }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_DEFAULT_SPACE }, - { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1, ...fail404() }, - CASES.NAMESPACE_AGNOSTIC, - { ...CASES.DOES_NOT_EXIST, ...fail404() }, - ]; - const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; - const allTypes = normalTypes.concat(hiddenType); - return { normalTypes, hiddenType, allTypes }; -}; - -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions } = updateTestSuiteFactory(esArchiver, supertest); - const createTests = () => { - const { normalTypes, hiddenType, allTypes } = createTestCases(); - return { - unauthorized: createTestDefinitions(allTypes, true), - authorized: [ - createTestDefinitions(normalTypes, false), - createTestDefinitions(hiddenType, true), - ].flat(), - superuser: createTestDefinitions(allTypes, false), - }; - }; - - describe('_update', () => { - getTestScenarios().security.forEach(({ users }) => { - const { unauthorized, authorized, superuser } = createTests(); - const _addTests = (user: TestUser, tests: UpdateTestDefinition[]) => { - addTests(user.description, { user, tests }); - }; - - [ - users.noAccess, - users.legacyAll, - users.dualRead, - users.readGlobally, - users.allAtDefaultSpace, - users.readAtDefaultSpace, - users.allAtSpace1, - users.readAtSpace1, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - [users.dualAll, users.allGlobally].forEach((user) => { - _addTests(user, authorized); - }); - _addTests(users.superuser, superuser); - }); - }); -} diff --git a/x-pack/test/saved_object_api_integration/security_only/config_trial.ts b/x-pack/test/saved_object_api_integration/security_only/config_trial.ts deleted file mode 100644 index fa5a7f67fe819e..00000000000000 --- a/x-pack/test/saved_object_api_integration/security_only/config_trial.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createTestConfig } from '../common/config'; - -// eslint-disable-next-line import/no-default-export -export default createTestConfig('security_only', { disabledPlugins: ['spaces'], license: 'trial' }); diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index ed52be26c7e538..3b2a87d924e88a 100644 --- a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -126,7 +126,7 @@ "name": "CTS Vis 2" }, { "type": "visualization", - "id": "cts_vis_3", + "id": "cts_vis_3_default", "name": "CTS Vis 3" }], "type": "dashboard", @@ -158,7 +158,8 @@ } ], "type": "visualization", - "updated_at": "2017-09-21T18:49:16.270Z" + "updated_at": "2017-09-21T18:49:16.270Z", + "namespaces": ["default"] }, "type": "_doc" } @@ -186,7 +187,8 @@ } ], "type": "visualization", - "updated_at": "2017-09-21T18:49:16.270Z" + "updated_at": "2017-09-21T18:49:16.270Z", + "namespaces": ["default"] }, "type": "_doc" } @@ -195,9 +197,10 @@ { "type": "_doc", "value": { - "id": "visualization:cts_vis_3", + "id": "visualization:cts_vis_3_default", "index": ".kibana", "source": { + "originId": "cts_vis_3", "visualization": { "title": "CTS vis 3 from default space", "description": "AreaChart", @@ -214,7 +217,8 @@ } ], "type": "visualization", - "updated_at": "2017-09-21T18:49:16.270Z" + "updated_at": "2017-09-21T18:49:16.270Z", + "namespaces": ["default"] }, "type": "_doc" } @@ -243,7 +247,7 @@ }, { "type": "visualization", - "id": "cts_vis_3", + "id": "cts_vis_3_space_1", "name": "CTS Vis 3" } ], @@ -258,7 +262,7 @@ { "type": "_doc", "value": { - "id": "space_1:visualization:cts_vis_1_space_1", + "id": "visualization:cts_vis_1_space_1", "index": ".kibana", "source": { "visualization": { @@ -278,7 +282,7 @@ ], "type": "visualization", "updated_at": "2017-09-21T18:49:16.270Z", - "namespace": "space_1" + "namespaces": ["space_1"] }, "type": "_doc" } @@ -287,7 +291,7 @@ { "type": "_doc", "value": { - "id": "space_1:visualization:cts_vis_2_space_1", + "id": "visualization:cts_vis_2_space_1", "index": ".kibana", "source": { "visualization": { @@ -307,7 +311,7 @@ ], "type": "visualization", "updated_at": "2017-09-21T18:49:16.270Z", - "namespace": "space_1" + "namespaces": ["space_1"] }, "type": "_doc" } @@ -316,9 +320,10 @@ { "type": "_doc", "value": { - "id": "space_1:visualization:cts_vis_3", + "id": "visualization:cts_vis_3_space_1", "index": ".kibana", "source": { + "originId": "cts_vis_3", "visualization": { "title": "CTS vis 3 from space_1 space", "description": "AreaChart", @@ -336,7 +341,7 @@ ], "type": "visualization", "updated_at": "2017-09-21T18:49:16.270Z", - "namespace": "space_1" + "namespaces": ["space_1"] }, "type": "_doc" } diff --git a/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts b/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts index 12333fc7460709..28b19d5db20b60 100644 --- a/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts +++ b/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; export function getUrlPrefix(spaceId?: string) { @@ -35,3 +36,31 @@ export function getTestScenariosForSpace(spaceId: string) { return [explicitScenario]; } + +export function getAggregatedSpaceData(es: KibanaClient, objectTypes: string[]) { + return es.search({ + index: '.kibana', + body: { + size: 0, + runtime_mappings: { + normalized_namespace: { + type: 'keyword', + script: ` + if (doc["namespaces"].size() > 0) { + emit(doc["namespaces"].value); + } else if (doc["namespace"].size() > 0) { + emit(doc["namespace"].value); + } + `, + }, + }, + query: { terms: { type: objectTypes } }, + aggs: { + count: { + terms: { field: 'normalized_namespace', missing: DEFAULT_SPACE_ID, size: 10 }, + aggs: { countByType: { terms: { field: 'type', missing: 'UNKNOWN', size: 10 } } }, + }, + }, + }, + }); +} diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index 27f1e55c3a90a7..3a3f0f889c91c4 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -11,7 +11,7 @@ import { EsArchiver } from '@kbn/es-archiver'; import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; import { CopyResponse } from '../../../../plugins/spaces/server/lib/copy_to_spaces'; -import { getUrlPrefix } from '../lib/space_test_utils'; +import { getAggregatedSpaceData, getUrlPrefix } from '../lib/space_test_utils'; import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; type TestResponse = Record; @@ -68,6 +68,9 @@ const INITIAL_COUNTS: Record> = { space_1: { dashboard: 2, visualization: 3, 'index-pattern': 1 }, space_2: { dashboard: 1 }, }; +const UUID_PATTERN = new RegExp( + /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i +); const getDestinationWithoutConflicts = () => 'space_2'; const getDestinationWithConflicts = (originSpaceId?: string) => @@ -79,19 +82,11 @@ export function copyToSpaceTestSuiteFactory( supertest: SuperTest ) { const collectSpaceContents = async () => { - const { body: response } = await es.search({ - index: '.kibana', - body: { - size: 0, - query: { terms: { type: ['visualization', 'dashboard', 'index-pattern'] } }, - aggs: { - count: { - terms: { field: 'namespace', missing: DEFAULT_SPACE_ID, size: 10 }, - aggs: { countByType: { terms: { field: 'type', missing: 'UNKNOWN', size: 10 } } }, - }, - }, - }, - }); + const { body: response } = await getAggregatedSpaceData(es, [ + 'visualization', + 'dashboard', + 'index-pattern', + ]); const aggs = response.aggregations as Record< string, @@ -187,6 +182,14 @@ export function copyToSpaceTestSuiteFactory( async (resp: TestResponse) => { const destination = getDestinationWithoutConflicts(); const result = resp.body as CopyResponse; + + const vis1DestinationId = result[destination].successResults![1].destinationId; + expect(vis1DestinationId).to.match(UUID_PATTERN); // this was copied to space 2 and hit an unresolvable conflict, so the object ID was regenerated silently / the destinationId is a UUID + const vis2DestinationId = result[destination].successResults![2].destinationId; + expect(vis2DestinationId).to.match(UUID_PATTERN); // this was copied to space 2 and hit an unresolvable conflict, so the object ID was regenerated silently / the destinationId is a UUID + const vis3DestinationId = result[destination].successResults![3].destinationId; + expect(vis3DestinationId).to.match(UUID_PATTERN); // this was copied to space 2 and hit an unresolvable conflict, so the object ID was regenerated silently / the destinationId is a UUID + expect(result).to.eql({ [destination]: { success: true, @@ -204,16 +207,19 @@ export function copyToSpaceTestSuiteFactory( id: `cts_vis_1_${spaceId}`, type: 'visualization', meta: { icon: 'visualizeApp', title: `CTS vis 1 from ${spaceId} space` }, + destinationId: vis1DestinationId, }, { id: `cts_vis_2_${spaceId}`, type: 'visualization', meta: { icon: 'visualizeApp', title: `CTS vis 2 from ${spaceId} space` }, + destinationId: vis2DestinationId, }, { - id: 'cts_vis_3', + id: `cts_vis_3_${spaceId}`, type: 'visualization', meta: { icon: 'visualizeApp', title: `CTS vis 3 from ${spaceId} space` }, + destinationId: vis3DestinationId, }, { id: 'cts_dashboard', @@ -303,6 +309,12 @@ export function copyToSpaceTestSuiteFactory( (spaceId?: string) => async (resp: { [key: string]: any }) => { const destination = getDestinationWithConflicts(spaceId); const result = resp.body as CopyResponse; + + const vis1DestinationId = result[destination].successResults![1].destinationId; + expect(vis1DestinationId).to.match(UUID_PATTERN); // this was copied to space 2 and hit an unresolvable conflict, so the object ID was regenerated silently / the destinationId is a UUID + const vis2DestinationId = result[destination].successResults![2].destinationId; + expect(vis2DestinationId).to.match(UUID_PATTERN); // this was copied to space 2 and hit an unresolvable conflict, so the object ID was regenerated silently / the destinationId is a UUID + expect(result).to.eql({ [destination]: { success: true, @@ -321,17 +333,20 @@ export function copyToSpaceTestSuiteFactory( id: `cts_vis_1_${spaceId}`, type: 'visualization', meta: { icon: 'visualizeApp', title: `CTS vis 1 from ${spaceId} space` }, + destinationId: vis1DestinationId, }, { id: `cts_vis_2_${spaceId}`, type: 'visualization', meta: { icon: 'visualizeApp', title: `CTS vis 2 from ${spaceId} space` }, + destinationId: vis2DestinationId, }, { - id: 'cts_vis_3', + id: `cts_vis_3_${spaceId}`, type: 'visualization', meta: { icon: 'visualizeApp', title: `CTS vis 3 from ${spaceId} space` }, overwrite: true, + destinationId: `cts_vis_3_${destination}`, // this conflicted with another visualization in the destination space because of a shared originId }, { id: 'cts_dashboard', @@ -363,16 +378,23 @@ export function copyToSpaceTestSuiteFactory( const result = resp.body as CopyResponse; result[destination].errors!.sort(errorSorter); + const vis1DestinationId = result[destination].successResults![0].destinationId; + expect(vis1DestinationId).to.match(UUID_PATTERN); // this was copied to space 2 and hit an unresolvable conflict, so the object ID was regenerated silently / the destinationId is a UUID + const vis2DestinationId = result[destination].successResults![1].destinationId; + expect(vis2DestinationId).to.match(UUID_PATTERN); // this was copied to space 2 and hit an unresolvable conflict, so the object ID was regenerated silently / the destinationId is a UUID + const expectedSuccessResults = [ { id: `cts_vis_1_${spaceId}`, type: 'visualization', meta: { icon: 'visualizeApp', title: `CTS vis 1 from ${spaceId} space` }, + destinationId: vis1DestinationId, }, { id: `cts_vis_2_${spaceId}`, type: 'visualization', meta: { icon: 'visualizeApp', title: `CTS vis 2 from ${spaceId} space` }, + destinationId: vis2DestinationId, }, ]; const expectedErrors = [ @@ -397,8 +419,11 @@ export function copyToSpaceTestSuiteFactory( }, }, { - error: { type: 'conflict' }, - id: 'cts_vis_3', + error: { + type: 'conflict', + destinationId: `cts_vis_3_${destination}`, // this conflicted with another visualization in the destination space because of a shared originId + }, + id: `cts_vis_3_${spaceId}`, title: `CTS vis 3 from ${spaceId} space`, type: 'visualization', meta: { @@ -437,9 +462,6 @@ export function copyToSpaceTestSuiteFactory( // a 403 error actually comes back as an HTTP 200 response const statusCode = outcome === 'noAccess' ? 403 : 200; const type = 'sharedtype'; - const v4 = new RegExp( - /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i - ); const noConflictId = `${spaceId}_only`; const exactMatchId = 'each_space'; const inexactMatchId = `conflict_1_${spaceId}`; @@ -463,7 +485,7 @@ export function copyToSpaceTestSuiteFactory( expect(success).to.eql(true); expect(successCount).to.eql(1); const destinationId = successResults![0].destinationId; - expect(destinationId).to.match(v4); + expect(destinationId).to.match(UUID_PATTERN); const meta = { title, icon: 'beaker' }; expect(successResults).to.eql([{ type, id: sourceId, meta, destinationId }]); expect(errors).to.be(undefined); diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts index 57fa6d8533890f..aaca4fa843d67f 100644 --- a/x-pack/test/spaces_api_integration/common/suites/delete.ts +++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; -import { getTestScenariosForSpace } from '../lib/space_test_utils'; +import { getAggregatedSpaceData, getTestScenariosForSpace } from '../lib/space_test_utils'; import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; @@ -43,38 +43,15 @@ export function deleteTestSuiteFactory( // Query ES to ensure that we deleted everything we expected, and nothing we didn't // Grouping first by namespace, then by saved object type - const { body: response } = await es.search({ - index: '.kibana', - body: { - size: 0, - query: { - terms: { - type: ['visualization', 'dashboard', 'space', 'index-pattern'], - // TODO: add assertions for config objects -- these assertions were removed because of flaky behavior in #92358, but we should - // consider adding them again at some point, especially if we convert config objects to `namespaceType: 'multiple-isolated'` in - // the future. - }, - }, - aggs: { - count: { - terms: { - field: 'namespace', - missing: 'default', - size: 10, - }, - aggs: { - countByType: { - terms: { - field: 'type', - missing: 'UNKNOWN', - size: 10, - }, - }, - }, - }, - }, - }, - }); + const { body: response } = await getAggregatedSpaceData(es, [ + 'visualization', + 'dashboard', + 'space', + 'index-pattern', + // TODO: add assertions for config objects -- these assertions were removed because of flaky behavior in #92358, but we should + // consider adding them again at some point, especially if we convert config objects to `namespaceType: 'multiple-isolated'` in + // the future. + ]); // @ts-expect-error @elastic/elasticsearch doesn't defined `count.buckets`. const buckets = response.aggregations?.count.buckets; diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts index b66949cbffe004..b190a37965b0bc 100644 --- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts @@ -58,7 +58,7 @@ export function resolveCopyToSpaceConflictsSuite( ) { const getVisualizationAtSpace = async (spaceId: string): Promise> => { return supertestWithAuth - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/visualization/cts_vis_3`) + .get(`${getUrlPrefix(spaceId)}/api/saved_objects/visualization/cts_vis_3_${spaceId}`) .then((response: any) => response.body); }; const getDashboardAtSpace = async (spaceId: string): Promise> => { @@ -85,12 +85,13 @@ export function resolveCopyToSpaceConflictsSuite( successCount: 1, successResults: [ { - id: 'cts_vis_3', + id: `cts_vis_3_${sourceSpaceId}`, type: 'visualization', meta: { title: `CTS vis 3 from ${sourceSpaceId} space`, icon: 'visualizeApp', }, + destinationId: `cts_vis_3_${destination}`, // this conflicted with another visualization in the destination space because of a shared originId overwrite: true, }, ], @@ -146,8 +147,11 @@ export function resolveCopyToSpaceConflictsSuite( successCount: 0, errors: [ { - error: { type: 'conflict' }, - id: 'cts_vis_3', + error: { + type: 'conflict', + destinationId: `cts_vis_3_${destination}`, // this conflicted with another visualization in the destination space because of a shared originId + }, + id: `cts_vis_3_${sourceSpaceId}`, title: `CTS vis 3 from ${sourceSpaceId} space`, meta: { title: `CTS vis 3 from ${sourceSpaceId} space`, @@ -444,7 +448,7 @@ export function resolveCopyToSpaceConflictsSuite( ); const dashboardObject = { type: 'dashboard', id: 'cts_dashboard' }; - const visualizationObject = { type: 'visualization', id: 'cts_vis_3' }; + const visualizationObject = { type: 'visualization', id: `cts_vis_3_${spaceId}` }; it(`should return ${tests.withReferencesNotOverwriting.statusCode} when not overwriting, with references`, async () => { const destination = getDestinationSpace(spaceId); @@ -456,7 +460,15 @@ export function resolveCopyToSpaceConflictsSuite( objects: [dashboardObject], includeReferences: true, createNewCopies: false, - retries: { [destination]: [{ ...visualizationObject, overwrite: false }] }, + retries: { + [destination]: [ + { + ...visualizationObject, + destinationId: `cts_vis_3_${destination}`, + overwrite: false, + }, + ], + }, }) .expect(tests.withReferencesNotOverwriting.statusCode) .then(tests.withReferencesNotOverwriting.response); @@ -472,7 +484,15 @@ export function resolveCopyToSpaceConflictsSuite( objects: [dashboardObject], includeReferences: true, createNewCopies: false, - retries: { [destination]: [{ ...visualizationObject, overwrite: true }] }, + retries: { + [destination]: [ + { + ...visualizationObject, + destinationId: `cts_vis_3_${destination}`, + overwrite: true, + }, + ], + }, }) .expect(tests.withReferencesOverwriting.statusCode) .then(tests.withReferencesOverwriting.response); diff --git a/x-pack/test/timeline/security_only/config_basic.ts b/x-pack/test/timeline/security_only/config_basic.ts deleted file mode 100644 index 470b9097755f6a..00000000000000 --- a/x-pack/test/timeline/security_only/config_basic.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createTestConfig } from '../common/config'; - -// eslint-disable-next-line import/no-default-export -export default createTestConfig('security_only', { - license: 'basic', - disabledPlugins: ['spaces'], - ssl: false, - testFiles: [require.resolve('./tests/basic')], -}); diff --git a/x-pack/test/timeline/security_only/config_trial.ts b/x-pack/test/timeline/security_only/config_trial.ts deleted file mode 100644 index 8ca7dc950b78b8..00000000000000 --- a/x-pack/test/timeline/security_only/config_trial.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createTestConfig } from '../common/config'; - -// eslint-disable-next-line import/no-default-export -export default createTestConfig('security_only', { - license: 'trial', - disabledPlugins: ['spaces'], - ssl: false, - testFiles: [require.resolve('./tests/trial')], -}); diff --git a/x-pack/test/timeline/security_only/tests/basic/events.ts b/x-pack/test/timeline/security_only/tests/basic/events.ts deleted file mode 100644 index bf6ef53d766038..00000000000000 --- a/x-pack/test/timeline/security_only/tests/basic/events.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { JsonObject } from '@kbn/utility-types'; -import { ALERT_INSTANCE_ID, ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; - -import { getSpaceUrlPrefix } from '../../../../rule_registry/common/lib/authentication/spaces'; - -import { - superUser, - globalRead, - secOnly, - secOnlyRead, - noKibanaPrivileges, -} from '../../../../rule_registry/common/lib/authentication/users'; -import { - Direction, - TimelineEventsQueries, -} from '../../../../../plugins/security_solution/common/search_strategy'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -const TO = '3000-01-01T00:00:00.000Z'; -const FROM = '2000-01-01T00:00:00.000Z'; -const TEST_URL = '/internal/search/timelineSearchStrategy/'; -const SPACE_1 = 'space1'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext) => { - const esArchiver = getService('esArchiver'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const getPostBody = (): JsonObject => ({ - defaultIndex: ['.alerts-*'], - entityType: 'alerts', - docValueFields: [ - { - field: '@timestamp', - }, - { - field: ALERT_RULE_CONSUMER, - }, - { - field: ALERT_INSTANCE_ID, - }, - { - field: 'event.kind', - }, - ], - factoryQueryType: TimelineEventsQueries.all, - fieldRequested: ['@timestamp', 'message', ALERT_RULE_CONSUMER, ALERT_INSTANCE_ID, 'event.kind'], - fields: [], - filterQuery: { - bool: { - filter: [ - { - match_all: {}, - }, - ], - }, - }, - pagination: { - activePage: 0, - querySize: 25, - }, - language: 'kuery', - sort: [ - { - field: '@timestamp', - direction: Direction.desc, - type: 'number', - }, - ], - timerange: { - from: FROM, - to: TO, - interval: '12h', - }, - }); - - describe('Timeline - Events', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); - }); - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); - }); - - const authorizedSecSpace1 = [secOnly, secOnlyRead]; - const authorizedInAllSpaces = [superUser, globalRead]; - const unauthorized = [noKibanaPrivileges]; - - [...authorizedSecSpace1, ...authorizedInAllSpaces].forEach(({ username, password }) => { - it(`${username} - should return a 404 when accessing a spaces route`, async () => { - await supertestWithoutAuth - .post(`${getSpaceUrlPrefix(SPACE_1)}${TEST_URL}`) - .auth(username, password) - .set('kbn-xsrf', 'true') - .set('Content-Type', 'application/json') - .send({ - ...getPostBody(), - defaultIndex: ['.alerts-*'], - entityType: 'alerts', - alertConsumers: ['siem'], - }) - .expect(404); - }); - }); - - [...authorizedInAllSpaces].forEach(({ username, password }) => { - it(`${username} - should return 200 for authorized users`, async () => { - await supertestWithoutAuth - .post(`${getSpaceUrlPrefix()}${TEST_URL}`) - .auth(username, password) - .set('kbn-xsrf', 'true') - .set('Content-Type', 'application/json') - .send({ - ...getPostBody(), - alertConsumers: ['siem', 'apm'], - }) - .expect(200); - }); - }); - - [...unauthorized].forEach(({ username, password }) => { - it(`${username} - should return 403 for unauthorized users`, async () => { - await supertestWithoutAuth - .post(`${getSpaceUrlPrefix()}${TEST_URL}`) - .auth(username, password) - .set('kbn-xsrf', 'true') - .set('Content-Type', 'application/json') - .send({ - ...getPostBody(), - alertConsumers: ['siem', 'apm'], - }) - // TODO - This should be updated to be a 403 once this ticket is resolved - // https://github.com/elastic/kibana/issues/106005 - .expect(500); - }); - }); - }); -}; diff --git a/x-pack/test/timeline/security_only/tests/basic/index.ts b/x-pack/test/timeline/security_only/tests/basic/index.ts deleted file mode 100644 index 60957c09561100..00000000000000 --- a/x-pack/test/timeline/security_only/tests/basic/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { - createUsersAndRoles, - deleteUsersAndRoles, -} from '../../../../rule_registry/common/lib/authentication'; - -// eslint-disable-next-line import/no-default-export -export default ({ loadTestFile, getService }: FtrProviderContext): void => { - describe('timeline security only: basic', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - - before(async () => { - await createUsersAndRoles(getService); - }); - - after(async () => { - await deleteUsersAndRoles(getService); - }); - - // Basic - loadTestFile(require.resolve('./events')); - }); -}; diff --git a/x-pack/test/timeline/security_only/tests/trial/events.ts b/x-pack/test/timeline/security_only/tests/trial/events.ts deleted file mode 100644 index bf6ef53d766038..00000000000000 --- a/x-pack/test/timeline/security_only/tests/trial/events.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { JsonObject } from '@kbn/utility-types'; -import { ALERT_INSTANCE_ID, ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; - -import { getSpaceUrlPrefix } from '../../../../rule_registry/common/lib/authentication/spaces'; - -import { - superUser, - globalRead, - secOnly, - secOnlyRead, - noKibanaPrivileges, -} from '../../../../rule_registry/common/lib/authentication/users'; -import { - Direction, - TimelineEventsQueries, -} from '../../../../../plugins/security_solution/common/search_strategy'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -const TO = '3000-01-01T00:00:00.000Z'; -const FROM = '2000-01-01T00:00:00.000Z'; -const TEST_URL = '/internal/search/timelineSearchStrategy/'; -const SPACE_1 = 'space1'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext) => { - const esArchiver = getService('esArchiver'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const getPostBody = (): JsonObject => ({ - defaultIndex: ['.alerts-*'], - entityType: 'alerts', - docValueFields: [ - { - field: '@timestamp', - }, - { - field: ALERT_RULE_CONSUMER, - }, - { - field: ALERT_INSTANCE_ID, - }, - { - field: 'event.kind', - }, - ], - factoryQueryType: TimelineEventsQueries.all, - fieldRequested: ['@timestamp', 'message', ALERT_RULE_CONSUMER, ALERT_INSTANCE_ID, 'event.kind'], - fields: [], - filterQuery: { - bool: { - filter: [ - { - match_all: {}, - }, - ], - }, - }, - pagination: { - activePage: 0, - querySize: 25, - }, - language: 'kuery', - sort: [ - { - field: '@timestamp', - direction: Direction.desc, - type: 'number', - }, - ], - timerange: { - from: FROM, - to: TO, - interval: '12h', - }, - }); - - describe('Timeline - Events', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); - }); - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); - }); - - const authorizedSecSpace1 = [secOnly, secOnlyRead]; - const authorizedInAllSpaces = [superUser, globalRead]; - const unauthorized = [noKibanaPrivileges]; - - [...authorizedSecSpace1, ...authorizedInAllSpaces].forEach(({ username, password }) => { - it(`${username} - should return a 404 when accessing a spaces route`, async () => { - await supertestWithoutAuth - .post(`${getSpaceUrlPrefix(SPACE_1)}${TEST_URL}`) - .auth(username, password) - .set('kbn-xsrf', 'true') - .set('Content-Type', 'application/json') - .send({ - ...getPostBody(), - defaultIndex: ['.alerts-*'], - entityType: 'alerts', - alertConsumers: ['siem'], - }) - .expect(404); - }); - }); - - [...authorizedInAllSpaces].forEach(({ username, password }) => { - it(`${username} - should return 200 for authorized users`, async () => { - await supertestWithoutAuth - .post(`${getSpaceUrlPrefix()}${TEST_URL}`) - .auth(username, password) - .set('kbn-xsrf', 'true') - .set('Content-Type', 'application/json') - .send({ - ...getPostBody(), - alertConsumers: ['siem', 'apm'], - }) - .expect(200); - }); - }); - - [...unauthorized].forEach(({ username, password }) => { - it(`${username} - should return 403 for unauthorized users`, async () => { - await supertestWithoutAuth - .post(`${getSpaceUrlPrefix()}${TEST_URL}`) - .auth(username, password) - .set('kbn-xsrf', 'true') - .set('Content-Type', 'application/json') - .send({ - ...getPostBody(), - alertConsumers: ['siem', 'apm'], - }) - // TODO - This should be updated to be a 403 once this ticket is resolved - // https://github.com/elastic/kibana/issues/106005 - .expect(500); - }); - }); - }); -}; diff --git a/x-pack/test/timeline/security_only/tests/trial/index.ts b/x-pack/test/timeline/security_only/tests/trial/index.ts deleted file mode 100644 index fbe8d3ec9ee0e3..00000000000000 --- a/x-pack/test/timeline/security_only/tests/trial/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { - createUsersAndRoles, - deleteUsersAndRoles, -} from '../../../../rule_registry/common/lib/authentication'; - -// eslint-disable-next-line import/no-default-export -export default ({ loadTestFile, getService }: FtrProviderContext): void => { - describe('timeline security only: trial', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - - before(async () => { - await createUsersAndRoles(getService); - }); - - after(async () => { - await deleteUsersAndRoles(getService); - }); - - // Basic - loadTestFile(require.resolve('./events')); - }); -}; diff --git a/x-pack/test/timeline/spaces_only/config.ts b/x-pack/test/timeline/spaces_only/config.ts deleted file mode 100644 index 442ebed0c125c1..00000000000000 --- a/x-pack/test/timeline/spaces_only/config.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createTestConfig } from '../common/config'; - -// eslint-disable-next-line import/no-default-export -export default createTestConfig('spaces_only', { - license: 'trial', - disabledPlugins: ['security'], - ssl: false, - testFiles: [require.resolve('./tests')], -}); diff --git a/x-pack/test/timeline/spaces_only/tests/events.ts b/x-pack/test/timeline/spaces_only/tests/events.ts deleted file mode 100644 index a7c2a9abeb2116..00000000000000 --- a/x-pack/test/timeline/spaces_only/tests/events.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { JsonObject } from '@kbn/utility-types'; -import expect from '@kbn/expect'; -import { ALERT_INSTANCE_ID, ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; - -import { FtrProviderContext } from '../../../rule_registry/common/ftr_provider_context'; -import { getSpaceUrlPrefix } from '../../../rule_registry/common/lib/authentication/spaces'; -import { - Direction, - TimelineEventsQueries, -} from '../../../../plugins/security_solution/common/search_strategy'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext) => { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - const TO = '3000-01-01T00:00:00.000Z'; - const FROM = '2000-01-01T00:00:00.000Z'; - const TEST_URL = '/internal/search/timelineSearchStrategy/'; - const SPACE1 = 'space1'; - const OTHER = 'other'; - - const getPostBody = (): JsonObject => ({ - defaultIndex: ['.alerts-*'], - entityType: 'alerts', - docValueFields: [ - { - field: '@timestamp', - }, - { - field: ALERT_RULE_CONSUMER, - }, - { - field: ALERT_INSTANCE_ID, - }, - { - field: 'event.kind', - }, - ], - factoryQueryType: TimelineEventsQueries.all, - fieldRequested: ['@timestamp', 'message', ALERT_RULE_CONSUMER, ALERT_INSTANCE_ID, 'event.kind'], - fields: [], - filterQuery: { - bool: { - filter: [ - { - match_all: {}, - }, - ], - }, - }, - pagination: { - activePage: 0, - querySize: 25, - }, - language: 'kuery', - sort: [ - { - field: '@timestamp', - direction: Direction.desc, - type: 'number', - }, - ], - timerange: { - from: FROM, - to: TO, - interval: '12h', - }, - }); - - describe('Timeline - Events', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); - }); - - it('should handle alerts request appropriately', async () => { - const resp = await supertest - .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) - .set('kbn-xsrf', 'true') - .set('Content-Type', 'application/json') - .send({ - ...getPostBody(), - alertConsumers: ['siem', 'apm'], - }) - .expect(200); - - // there's 5 total alerts, one is assigned to space2 only - expect(resp.body.totalCount).to.be(4); - }); - - it('should not return alerts from another space', async () => { - const resp = await supertest - .post(`${getSpaceUrlPrefix(OTHER)}${TEST_URL}`) - .set('kbn-xsrf', 'true') - .set('Content-Type', 'application/json') - .send({ - ...getPostBody(), - alertConsumers: ['siem', 'apm'], - }) - .expect(200); - - expect(resp.body.totalCount).to.be(0); - }); - }); -}; diff --git a/x-pack/test/timeline/spaces_only/tests/index.ts b/x-pack/test/timeline/spaces_only/tests/index.ts deleted file mode 100644 index 857ca027a23711..00000000000000 --- a/x-pack/test/timeline/spaces_only/tests/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { createSpaces, deleteSpaces } from '../../../rule_registry/common/lib/authentication'; - -// eslint-disable-next-line import/no-default-export -export default ({ loadTestFile, getService }: FtrProviderContext): void => { - describe('timeline spaces only: trial', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - - before(async () => { - await createSpaces(getService); - }); - - after(async () => { - await deleteSpaces(getService); - }); - - // Basic - loadTestFile(require.resolve('./events')); - }); -}; diff --git a/x-pack/test/ui_capabilities/security_and_spaces/scenarios.ts b/x-pack/test/ui_capabilities/security_and_spaces/scenarios.ts index a13a5589f6007e..7ed14214d23b49 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/scenarios.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/scenarios.ts @@ -126,6 +126,47 @@ const GlobalRead: User = { }, }; +const FooAll: User = { + username: 'foo_all', + fullName: 'foo_all', + password: 'foo_all-password', + role: { + name: 'foo_all_role', + kibana: [ + { + feature: { + foo: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +const FooRead: User = { + username: 'foo_read', + fullName: 'foo_read', + password: 'foo_read-password', + role: { + name: 'foo_read_role', + kibana: [ + { + feature: { + foo: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +interface FooAll extends User { + username: 'foo_all'; +} +interface FooRead extends User { + username: 'foo_read'; +} + const EverythingSpaceAll: User = { username: 'everything_space_all', fullName: 'everything_space_all', @@ -194,6 +235,8 @@ export const Users: User[] = [ DualPrivilegesRead, GlobalAll, GlobalRead, + FooAll, + FooRead, EverythingSpaceAll, EverythingSpaceRead, NothingSpaceAll, @@ -349,6 +392,42 @@ const GlobalReadAtNothingSpace: GlobalReadAtNothingSpace = { space: NothingSpace, }; +interface FooAllAtEverythingSpace extends Scenario { + id: 'foo_all at everything_space'; +} +const FooAllAtEverythingSpace: FooAllAtEverythingSpace = { + id: 'foo_all at everything_space', + user: FooAll, + space: EverythingSpace, +}; + +interface FooAllAtNothingSpace extends Scenario { + id: 'foo_all at nothing_space'; +} +const FooAllAtNothingSpace: FooAllAtNothingSpace = { + id: 'foo_all at nothing_space', + user: FooAll, + space: NothingSpace, +}; + +interface FooReadAtEverythingSpace extends Scenario { + id: 'foo_read at everything_space'; +} +const FooReadAtEverythingSpace: FooReadAtEverythingSpace = { + id: 'foo_read at everything_space', + user: FooRead, + space: EverythingSpace, +}; + +interface FooReadAtNothingSpace extends Scenario { + id: 'foo_read at nothing_space'; +} +const FooReadAtNothingSpace: FooReadAtNothingSpace = { + id: 'foo_read at nothing_space', + user: FooRead, + space: NothingSpace, +}; + interface EverythingSpaceAllAtEverythingSpace extends Scenario { id: 'everything_space_all at everything_space'; } @@ -421,30 +500,7 @@ const NothingSpaceReadAtNothingSpace: NothingSpaceReadAtNothingSpace = { space: NothingSpace, }; -export const UserAtSpaceScenarios: [ - NoKibanaPrivilegesAtEverythingSpace, - NoKibanaPrivilegesAtNothingSpace, - SuperuserAtEverythingSpace, - SuperuserAtNothingSpace, - LegacyAllAtEverythingSpace, - LegacyAllAtNothingSpace, - DualPrivilegesAllAtEverythingSpace, - DualPrivilegesAllAtNothingSpace, - DualPrivilegesReadAtEverythingSpace, - DualPrivilegesReadAtNothingSpace, - GlobalAllAtEverythingSpace, - GlobalAllAtNothingSpace, - GlobalReadAtEverythingSpace, - GlobalReadAtNothingSpace, - EverythingSpaceAllAtEverythingSpace, - EverythingSpaceAllAtNothingSpace, - EverythingSpaceReadAtEverythingSpace, - EverythingSpaceReadAtNothingSpace, - NothingSpaceAllAtEverythingSpace, - NothingSpaceAllAtNothingSpace, - NothingSpaceReadAtEverythingSpace, - NothingSpaceReadAtNothingSpace -] = [ +export const UserAtSpaceScenarios = [ NoKibanaPrivilegesAtEverythingSpace, NoKibanaPrivilegesAtNothingSpace, SuperuserAtEverythingSpace, @@ -459,6 +515,10 @@ export const UserAtSpaceScenarios: [ GlobalAllAtNothingSpace, GlobalReadAtEverythingSpace, GlobalReadAtNothingSpace, + FooAllAtEverythingSpace, + FooAllAtNothingSpace, + FooReadAtEverythingSpace, + FooReadAtNothingSpace, EverythingSpaceAllAtEverythingSpace, EverythingSpaceAllAtNothingSpace, EverythingSpaceReadAtEverythingSpace, @@ -467,4 +527,4 @@ export const UserAtSpaceScenarios: [ NothingSpaceAllAtNothingSpace, NothingSpaceReadAtEverythingSpace, NothingSpaceReadAtNothingSpace, -]; +] as const; diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts index aeaaf7fca1cb73..3d272977be625c 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts @@ -85,6 +85,18 @@ export default function catalogueTests({ getService }: FtrProviderContext) { expect(uiCapabilities.value!.catalogue).to.eql(expected); break; } + case 'foo_all at everything_space': + case 'foo_read at everything_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything except foo is disabled + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => catalogueId === 'foo' + ); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } // the nothing_space has no Kibana features enabled, so even if we have // privileges to perform these actions, we won't be able to. // Note that ES features may still be enabled if the user has privileges, since @@ -116,6 +128,8 @@ export default function catalogueTests({ getService }: FtrProviderContext) { // the nothing_space has no Kibana features enabled, so even if we have // privileges to perform these actions, we won't be able to. case 'global_read at nothing_space': + case 'foo_all at nothing_space': + case 'foo_read at nothing_space': case 'dual_privileges_all at nothing_space': case 'dual_privileges_read at nothing_space': case 'nothing_space_all at nothing_space': diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts index d1c5c392f48c7d..7e00864b547614 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts @@ -26,6 +26,7 @@ export default function fooTests({ getService }: FtrProviderContext) { // these users have a read/write view case 'superuser at everything_space': case 'global_all at everything_space': + case 'foo_all at everything_space': case 'dual_privileges_all at everything_space': case 'everything_space_all at everything_space': expect(uiCapabilities.success).to.be(true); @@ -39,6 +40,7 @@ export default function fooTests({ getService }: FtrProviderContext) { break; // these users have a read only view case 'global_read at everything_space': + case 'foo_read at everything_space': case 'dual_privileges_read at everything_space': case 'everything_space_read at everything_space': expect(uiCapabilities.success).to.be(true); @@ -55,6 +57,8 @@ export default function fooTests({ getService }: FtrProviderContext) { case 'superuser at nothing_space': case 'global_all at nothing_space': case 'global_read at nothing_space': + case 'foo_all at nothing_space': + case 'foo_read at nothing_space': case 'dual_privileges_all at nothing_space': case 'dual_privileges_read at nothing_space': case 'nothing_space_all at nothing_space': diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts index 6a6b618c2c8c89..5712cfeb8c1410 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts @@ -62,11 +62,21 @@ export default function navLinksTests({ getService }: FtrProviderContext) { ) ); break; + case 'foo_all at everything_space': + case 'foo_read at everything_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql( + navLinksBuilder.only('kibana', 'foo', 'management') + ); + break; case 'superuser at nothing_space': case 'global_all at nothing_space': + case 'global_read at nothing_space': + case 'foo_all at nothing_space': + case 'foo_read at nothing_space': case 'dual_privileges_all at nothing_space': case 'dual_privileges_read at nothing_space': - case 'global_read at nothing_space': case 'nothing_space_all at nothing_space': case 'nothing_space_read at nothing_space': case 'no_kibana_privileges at everything_space': diff --git a/x-pack/test/ui_capabilities/security_only/config.ts b/x-pack/test/ui_capabilities/security_only/config.ts deleted file mode 100644 index fa5a7f67fe819e..00000000000000 --- a/x-pack/test/ui_capabilities/security_only/config.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createTestConfig } from '../common/config'; - -// eslint-disable-next-line import/no-default-export -export default createTestConfig('security_only', { disabledPlugins: ['spaces'], license: 'trial' }); diff --git a/x-pack/test/ui_capabilities/security_only/scenarios.ts b/x-pack/test/ui_capabilities/security_only/scenarios.ts deleted file mode 100644 index 1bbb23720c1e27..00000000000000 --- a/x-pack/test/ui_capabilities/security_only/scenarios.ts +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CustomRoleSpecification, User } from '../common/types'; - -// For all scenarios, we define both an instance in addition -// to a "type" definition so that we can use the exhaustive switch in -// typescript to ensure all scenarios are handled. - -const allRole: CustomRoleSpecification = { - name: 'all_role', - kibana: [ - { - base: ['all'], - spaces: ['*'], - }, - ], -}; - -interface NoKibanaPrivileges extends User { - username: 'no_kibana_privileges'; -} -const NoKibanaPrivileges: NoKibanaPrivileges = { - username: 'no_kibana_privileges', - fullName: 'no_kibana_privileges', - password: 'no_kibana_privileges-password', - role: { - name: 'no_kibana_privileges', - elasticsearch: { - indices: [ - { - names: ['foo'], - privileges: ['all'], - }, - ], - }, - }, -}; - -interface Superuser extends User { - username: 'superuser'; -} -const Superuser: Superuser = { - username: 'superuser', - fullName: 'superuser', - password: 'superuser-password', - role: { - name: 'superuser', - }, -}; - -interface LegacyAll extends User { - username: 'legacy_all'; -} -const LegacyAll: LegacyAll = { - username: 'legacy_all', - fullName: 'legacy_all', - password: 'legacy_all-password', - role: { - name: 'legacy_all_role', - elasticsearch: { - indices: [ - { - names: ['.kibana*'], - privileges: ['all'], - }, - ], - }, - }, -}; - -interface DualPrivilegesAll extends User { - username: 'dual_privileges_all'; -} -const DualPrivilegesAll: DualPrivilegesAll = { - username: 'dual_privileges_all', - fullName: 'dual_privileges_all', - password: 'dual_privileges_all-password', - role: { - name: 'dual_privileges_all_role', - elasticsearch: { - indices: [ - { - names: ['.kibana*'], - privileges: ['all'], - }, - ], - }, - kibana: [ - { - base: ['all'], - spaces: ['*'], - }, - ], - }, -}; - -interface DualPrivilegesRead extends User { - username: 'dual_privileges_read'; -} -const DualPrivilegesRead: DualPrivilegesRead = { - username: 'dual_privileges_read', - fullName: 'dual_privileges_read', - password: 'dual_privileges_read-password', - role: { - name: 'dual_privileges_read_role', - elasticsearch: { - indices: [ - { - names: ['.kibana*'], - privileges: ['read'], - }, - ], - }, - kibana: [ - { - base: ['read'], - spaces: ['*'], - }, - ], - }, -}; - -interface All extends User { - username: 'all'; -} -const All: All = { - username: 'all', - fullName: 'all', - password: 'all-password', - role: allRole, -}; - -interface Read extends User { - username: 'read'; -} -const Read: Read = { - username: 'read', - fullName: 'read', - password: 'read-password', - role: { - name: 'read_role', - kibana: [ - { - base: ['read'], - spaces: ['*'], - }, - ], - }, -}; - -interface FooAll extends User { - username: 'foo_all'; -} -const FooAll: FooAll = { - username: 'foo_all', - fullName: 'foo_all', - password: 'foo_all-password', - role: { - name: 'foo_all_role', - kibana: [ - { - feature: { - foo: ['all'], - }, - spaces: ['*'], - }, - ], - }, -}; - -interface FooRead extends User { - username: 'foo_read'; -} -const FooRead: FooRead = { - username: 'foo_read', - fullName: 'foo_read', - password: 'foo_read-password', - role: { - name: 'foo_read_role', - kibana: [ - { - feature: { - foo: ['read'], - }, - spaces: ['*'], - }, - ], - }, -}; - -export const UserScenarios: [ - NoKibanaPrivileges, - Superuser, - LegacyAll, - DualPrivilegesAll, - DualPrivilegesRead, - All, - Read, - FooAll, - FooRead -] = [ - NoKibanaPrivileges, - Superuser, - LegacyAll, - DualPrivilegesAll, - DualPrivilegesRead, - All, - Read, - FooAll, - FooRead, -]; diff --git a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts deleted file mode 100644 index da4b26106afacd..00000000000000 --- a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { mapValues } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { UICapabilitiesService } from '../../common/services/ui_capabilities'; -import { UserScenarios } from '../scenarios'; - -export default function catalogueTests({ getService }: FtrProviderContext) { - const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); - - const esFeatureExceptions = [ - 'security', - 'index_lifecycle_management', - 'snapshot_restore', - 'rollup_jobs', - 'reporting', - 'transform', - 'watcher', - ]; - - describe('catalogue', () => { - UserScenarios.forEach((scenario) => { - it(`${scenario.fullName}`, async () => { - const uiCapabilities = await uiCapabilitiesService.get({ - credentials: { - username: scenario.username, - password: scenario.password, - }, - }); - switch (scenario.username) { - case 'superuser': { - expect(uiCapabilities.success).to.be(true); - expect(uiCapabilities.value).to.have.property('catalogue'); - // everything is enabled - const expected = mapValues(uiCapabilities.value!.catalogue, () => true); - expect(uiCapabilities.value!.catalogue).to.eql(expected); - break; - } - case 'all': - case 'dual_privileges_all': { - expect(uiCapabilities.success).to.be(true); - expect(uiCapabilities.value).to.have.property('catalogue'); - // everything except ml, monitoring, and ES features are enabled - const expected = mapValues( - uiCapabilities.value!.catalogue, - (enabled, catalogueId) => - catalogueId !== 'ml' && - catalogueId !== 'monitoring' && - catalogueId !== 'ml_file_data_visualizer' && - catalogueId !== 'osquery' && - !esFeatureExceptions.includes(catalogueId) - ); - expect(uiCapabilities.value!.catalogue).to.eql(expected); - break; - } - case 'read': - case 'dual_privileges_read': { - expect(uiCapabilities.success).to.be(true); - expect(uiCapabilities.value).to.have.property('catalogue'); - // everything except ml and monitoring and enterprise search is enabled - const exceptions = [ - 'ml', - 'ml_file_data_visualizer', - 'monitoring', - 'enterpriseSearch', - 'appSearch', - 'workplaceSearch', - 'osquery', - ...esFeatureExceptions, - ]; - const expected = mapValues( - uiCapabilities.value!.catalogue, - (enabled, catalogueId) => !exceptions.includes(catalogueId) - ); - expect(uiCapabilities.value!.catalogue).to.eql(expected); - break; - } - case 'foo_all': - case 'foo_read': { - expect(uiCapabilities.success).to.be(true); - expect(uiCapabilities.value).to.have.property('catalogue'); - // only foo is enabled - const expected = mapValues( - uiCapabilities.value!.catalogue, - (value, catalogueId) => catalogueId === 'foo' - ); - expect(uiCapabilities.value!.catalogue).to.eql(expected); - break; - } - // these users have no access to even get the ui capabilities - case 'legacy_all': - case 'no_kibana_privileges': - expect(uiCapabilities.success).to.be(true); - expect(uiCapabilities.value).to.have.property('catalogue'); - // only foo is enabled - const expected = mapValues(uiCapabilities.value!.catalogue, () => false); - expect(uiCapabilities.value!.catalogue).to.eql(expected); - break; - default: - throw new UnreachableError(scenario); - } - }); - }); - }); -} diff --git a/x-pack/test/ui_capabilities/security_only/tests/foo.ts b/x-pack/test/ui_capabilities/security_only/tests/foo.ts deleted file mode 100644 index f4d0d48863107f..00000000000000 --- a/x-pack/test/ui_capabilities/security_only/tests/foo.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { UICapabilitiesService } from '../../common/services/ui_capabilities'; -import { UserScenarios } from '../scenarios'; - -export default function fooTests({ getService }: FtrProviderContext) { - const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); - - describe('foo', () => { - UserScenarios.forEach((scenario) => { - it(`${scenario.fullName}`, async () => { - const uiCapabilities = await uiCapabilitiesService.get({ - credentials: { - username: scenario.username, - password: scenario.password, - }, - }); - switch (scenario.username) { - // these users have a read/write view of Foo - case 'superuser': - case 'all': - case 'dual_privileges_all': - case 'foo_all': - expect(uiCapabilities.success).to.be(true); - expect(uiCapabilities.value).to.have.property('foo'); - expect(uiCapabilities.value!.foo).to.eql({ - create: true, - edit: true, - delete: true, - show: true, - }); - break; - // these users have a read-only view of Foo - case 'read': - case 'dual_privileges_read': - case 'foo_read': - expect(uiCapabilities.success).to.be(true); - expect(uiCapabilities.value).to.have.property('foo'); - expect(uiCapabilities.value!.foo).to.eql({ - create: false, - edit: false, - delete: false, - show: true, - }); - break; - // these users have no access to even get the ui capabilities - case 'legacy_all': - case 'no_kibana_privileges': - expect(uiCapabilities.success).to.be(true); - expect(uiCapabilities.value).to.have.property('foo'); - expect(uiCapabilities.value!.foo).to.eql({ - create: false, - edit: false, - delete: false, - show: false, - }); - break; - // all other users can't do anything with Foo - default: - throw new UnreachableError(scenario); - } - }); - }); - }); -} diff --git a/x-pack/test/ui_capabilities/security_only/tests/index.ts b/x-pack/test/ui_capabilities/security_only/tests/index.ts deleted file mode 100644 index 37d79d4ef37736..00000000000000 --- a/x-pack/test/ui_capabilities/security_only/tests/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { isCustomRoleSpecification } from '../../common/types'; -import { UserScenarios } from '../scenarios'; - -export default function uiCapabilitesTests({ loadTestFile, getService }: FtrProviderContext) { - const securityService = getService('security'); - - describe('ui capabilities', function () { - this.tags('ciGroup9'); - - before(async () => { - for (const user of UserScenarios) { - const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; - - await securityService.user.create(user.username, { - password: user.password, - full_name: user.fullName, - roles: roles.map((role) => role.name), - }); - - for (const role of roles) { - if (isCustomRoleSpecification(role)) { - await securityService.role.create(role.name, { - kibana: role.kibana, - }); - } - } - } - }); - - after(async () => { - for (const user of UserScenarios) { - await securityService.user.delete(user.username); - - const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; - for (const role of roles) { - if (isCustomRoleSpecification(role)) { - await securityService.role.delete(role.name); - } - } - } - }); - - loadTestFile(require.resolve('./catalogue')); - loadTestFile(require.resolve('./foo')); - loadTestFile(require.resolve('./nav_links')); - }); -} diff --git a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts deleted file mode 100644 index 6a44b3d8f0b71d..00000000000000 --- a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { NavLinksBuilder } from '../../common/nav_links_builder'; -import { FeaturesService } from '../../common/services'; -import { UICapabilitiesService } from '../../common/services/ui_capabilities'; -import { UserScenarios } from '../scenarios'; - -export default function navLinksTests({ getService }: FtrProviderContext) { - const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); - const featuresService: FeaturesService = getService('features'); - - describe('navLinks', () => { - let navLinksBuilder: NavLinksBuilder; - before(async () => { - const features = await featuresService.get(); - navLinksBuilder = new NavLinksBuilder(features); - }); - - UserScenarios.forEach((scenario) => { - it(`${scenario.fullName}`, async () => { - const uiCapabilities = await uiCapabilitiesService.get({ - credentials: { - username: scenario.username, - password: scenario.password, - }, - }); - switch (scenario.username) { - case 'superuser': - expect(uiCapabilities.success).to.be(true); - expect(uiCapabilities.value).to.have.property('navLinks'); - expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.all()); - break; - case 'all': - case 'dual_privileges_all': - expect(uiCapabilities.success).to.be(true); - expect(uiCapabilities.value).to.have.property('navLinks'); - expect(uiCapabilities.value!.navLinks).to.eql( - navLinksBuilder.except('ml', 'monitoring', 'osquery') - ); - break; - case 'read': - case 'dual_privileges_read': - expect(uiCapabilities.success).to.be(true); - expect(uiCapabilities.value).to.have.property('navLinks'); - expect(uiCapabilities.value!.navLinks).to.eql( - navLinksBuilder.except( - 'ml', - 'monitoring', - 'enterpriseSearch', - 'appSearch', - 'workplaceSearch', - 'osquery' - ) - ); - break; - case 'foo_all': - case 'foo_read': - expect(uiCapabilities.success).to.be(true); - expect(uiCapabilities.value).to.have.property('navLinks'); - expect(uiCapabilities.value!.navLinks).to.eql( - navLinksBuilder.only('management', 'foo', 'kibana') - ); - break; - case 'legacy_all': - case 'no_kibana_privileges': - expect(uiCapabilities.success).to.be(true); - expect(uiCapabilities.value).to.have.property('navLinks'); - expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.only('management')); - break; - default: - throw new UnreachableError(scenario); - } - }); - }); - }); -} diff --git a/x-pack/test/ui_capabilities/spaces_only/scenarios.ts b/x-pack/test/ui_capabilities/spaces_only/scenarios.ts index f3af7db6eb2462..c914b5f056aed4 100644 --- a/x-pack/test/ui_capabilities/spaces_only/scenarios.ts +++ b/x-pack/test/ui_capabilities/spaces_only/scenarios.ts @@ -38,8 +38,4 @@ const FooDisabledSpace: FooDisabledSpace = { disabledFeatures: ['foo'], }; -export const SpaceScenarios: [EverythingSpace, NothingSpace, FooDisabledSpace] = [ - EverythingSpace, - NothingSpace, - FooDisabledSpace, -]; +export const SpaceScenarios = [EverythingSpace, NothingSpace, FooDisabledSpace] as const;