diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 36e1e3813275ea..7052a19806d528 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -313,9 +313,11 @@ /x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security /x-pack/plugins/security/ @elastic/kibana-security /x-pack/test/api_integration/apis/security/ @elastic/kibana-security +/x-pack/test/api_integration/apis/spaces/ @elastic/kibana-security /x-pack/test/ui_capabilities/ @elastic/kibana-security /x-pack/test/encrypted_saved_objects_api_integration/ @elastic/kibana-security /x-pack/test/functional/apps/security/ @elastic/kibana-security +/x-pack/test/functional/apps/spaces/ @elastic/kibana-security /x-pack/test/security_api_integration/ @elastic/kibana-security /x-pack/test/security_functional/ @elastic/kibana-security /x-pack/test/spaces_api_integration/ @elastic/kibana-security diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx index ac90d149a99672..c2f0322f6d6805 100644 --- a/api_docs/cases.mdx +++ b/api_docs/cases.mdx @@ -12,13 +12,13 @@ import casesObj from './cases.json'; The Case management system in Kibana -Contact [Security Solution Threat Hunting](https://github.com/orgs/elastic/teams/security-threat-hunting) for questions regarding this plugin. +Contact [ResponseOps](https://github.com/orgs/elastic/teams/response-ops) for questions regarding this plugin. **Code health stats** -| Public API count | Any count | Items lacking comments | Missing exports | -|-------------------|-----------|------------------------|-----------------| -| 83 | 0 | 57 | 23 | +| Public API count | Any count | Items lacking comments | Missing exports | +| ---------------- | --------- | ---------------------- | --------------- | +| 83 | 0 | 57 | 23 | ## Client diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index 5f5fb28f74d7ae..d038fa59ee9a3f 100644 --- a/api_docs/plugin_directory.mdx +++ b/api_docs/plugin_directory.mdx @@ -31,7 +31,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 9 | 0 | 9 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Considering using bfetch capabilities when fetching large amounts of data. This services supports batching HTTP requests and streaming responses back. | 76 | 1 | 67 | 2 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds Canvas application to Kibana | 9 | 0 | 8 | 3 | -| | [Security Solution Threat Hunting](https://github.com/orgs/elastic/teams/security-threat-hunting) | The Case management system in Kibana | 83 | 0 | 57 | 23 | +| | [ResponseOps](https://github.com/orgs/elastic/teams/response-ops) | The Case management system in Kibana | 83 | 0 | 57 | 23 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | - | 314 | 2 | 281 | 4 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 22 | 0 | 22 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 13 | 0 | 13 | 1 | diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 8861681827ef39..b406ced798c0c0 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -134,17 +134,19 @@ The `xpack.apm.autocreateApmIndexPattern` APM setting has been removed. For more *Impact* + To automatically create data views in APM, use `xpack.apm.autoCreateApmDataView`. ==== - + [discrete] [[deprecation-119494]] -.Updates Fleet API responses for consistency +.Updates Fleet API to improve consistency [%collapsible] ==== *Details* + -To make sure all Fleet API GET resposes return `items`, the following have been updated: +The Fleet API has been updated to improve consistency: -* `/api/fleet/enrollment-api-keys` -* `/api/fleet/agents` +* Hyphens are changed to underscores in some names. +* The `pkgkey` path parameter in the packages endpoint is split. +* The `response` and `list` properties are renamed to `items` or `item` in some +responses. For more information, refer to {kibana-pull}119494[#119494]. @@ -157,24 +159,30 @@ When you upgrade to 8.0.0, use the following API changes: * Use `service_tokens` instead of `service-tokens`. -* `check-permissions` is no longer supported. - * Use `/epm/packages/{packageName}/{version}` instead of `/epm/packages/{pkgkey}`. -* Use `items[]` or `item` instead of `response[]` in the following: - +* Use `items[]` instead of `response[]` in: ++ [source,text] -- +/api/fleet/enrollment_api_keys +/api/fleet/agents /epm/packages/ -/epm/packages/{pkgkey} /epm/categories /epm/packages/_bulk /epm/packages/limited +/epm/packages/{packageName}/{version} <1> -- +<1> Use `items[]` when the verb is `POST` or `DELETE`. Use `item` when the verb +is `GET` or `PUT`. + +For more information, refer to {fleet-guide}/fleet-api-docs.html[Fleet APIs]. + ==== -To review the depcrecations in previous versions, refer to the <>. - +To review the deprecations in previous versions, refer to the <>. + + [float] [[features-8.0.0-rc1]] === Features diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index d44de3c2efe2f5..814a7d374506fe 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -185,7 +185,7 @@ There are two things you can do to if you'd like to ensure a field is searchable 1. Index your additional data as {apm-guide-ref}/metadata.html[labels] instead. These are dynamic by default, which means they will be indexed and become searchable and aggregatable. -2. Use the {apm-guide-ref}/configuration-template.html[`append_fields`] feature. As an example, +2. Use the `append_fields` feature. As an example, adding the following to `apm-server.yml` will enable dynamic indexing for `http.request.cookies`: [source,yml] diff --git a/docs/management/connectors/action-types/email.asciidoc b/docs/management/connectors/action-types/email.asciidoc index 5523201dce36f8..c080c412f0f6b6 100644 --- a/docs/management/connectors/action-types/email.asciidoc +++ b/docs/management/connectors/action-types/email.asciidoc @@ -16,7 +16,7 @@ NOTE: For emails to have a footer with a link back to {kib}, set the <"` format. See the https://nodemailer.com/message/addresses/[Nodemailer address documentation] for more information. +Sender:: The from address for all emails sent with this connector. This must be specified in `user@host-name` format. See the https://nodemailer.com/message/addresses/[Nodemailer address documentation] for more information. Service:: The name of the email service. If `service` is one of Nodemailer's https://nodemailer.com/smtp/well-known/[well-known email service providers], the `host`, `port`, and `secure` properties are defined with the default values and disabled for modification. If `service` is `MS Exchange Server`, the `host`, `port`, and `secure` properties are ignored and `tenantId`, `clientId`, `clientSecret` are required instead. If `service` is `other`, the `host` and `port` properties must be defined. Host:: Host name of the service provider. If you are using the <> setting, make sure this hostname is added to the allowed hosts. Port:: The port to connect to on the service provider. diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index f0c22ebf8b7304..1b8d467713ab8b 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -190,7 +190,7 @@ Specifies the default timeout for the all rule types tasks. The time is formatte + `[ms,s,m,h,d,w,M,Y]` + -For example, `20m`, `24h`, `7d`, `1w`. Default: `60s`. +For example, `20m`, `24h`, `7d`, `1w`. Default: `5m`. `xpack.alerting.cancelAlertsOnRuleTimeout`:: Specifies whether to skip writing alerts and scheduling actions if rule execution is cancelled due to timeout. Default: `true`. This setting can be overridden by individual rule types. \ No newline at end of file diff --git a/package.json b/package.json index ed36f3277ee643..25cea24c1a3a24 100644 --- a/package.json +++ b/package.json @@ -725,7 +725,7 @@ "cpy": "^8.1.1", "css-loader": "^3.4.2", "cssnano": "^4.1.11", - "cypress": "^9.2.0", + "cypress": "^9.2.1", "cypress-axe": "^0.14.0", "cypress-cucumber-preprocessor": "^2.5.2", "cypress-file-upload": "^5.0.8", diff --git a/packages/elastic-apm-synthtrace/src/scripts/examples/01_simple_trace.ts b/packages/elastic-apm-synthtrace/src/scripts/examples/01_simple_trace.ts index 559c636cfaeec9..4ea1af15f43ef5 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/examples/01_simple_trace.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/examples/01_simple_trace.ts @@ -12,14 +12,14 @@ import { getApmWriteTargets } from '../../lib/apm/utils/get_apm_write_targets'; import { Scenario } from '../scenario'; import { getCommonServices } from '../utils/get_common_services'; -const scenario: Scenario = async ({ target, logLevel }) => { +const scenario: Scenario = async ({ target, logLevel, scenarioOpts }) => { const { client, logger } = getCommonServices({ target, logLevel }); const writeTargets = await getApmWriteTargets({ client }); + const { numServices = 3 } = scenarioOpts || {}; + return { generate: ({ from, to }) => { - const numServices = 3; - const range = timerange(from, to); const transactionName = '240rpm/75% 1000ms'; diff --git a/packages/elastic-apm-synthtrace/src/scripts/run.ts b/packages/elastic-apm-synthtrace/src/scripts/run.ts index 4078c848aa480c..96bef3e958bdc2 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/run.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/run.ts @@ -69,6 +69,12 @@ function options(y: Argv) { describe: 'Target to index', string: true, }) + .option('scenarioOpts', { + describe: 'Options specific to the scenario', + coerce: (arg) => { + return arg as Record | undefined; + }, + }) .conflicts('to', 'live'); } diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/parse_run_cli_flags.ts b/packages/elastic-apm-synthtrace/src/scripts/utils/parse_run_cli_flags.ts index 5c081707bb75ca..47359bd07aa8a5 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/utils/parse_run_cli_flags.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/parse_run_cli_flags.ts @@ -47,7 +47,15 @@ export function parseRunCliFlags(flags: RunCliFlags) { } return { - ...pick(flags, 'target', 'workers', 'clientWorkers', 'batchSize', 'writeTarget'), + ...pick( + flags, + 'target', + 'workers', + 'clientWorkers', + 'batchSize', + 'writeTarget', + 'scenarioOpts' + ), intervalInMs, bucketSizeInMs, logLevel: parsedLogLevel, diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/start_historical_data_upload.ts b/packages/elastic-apm-synthtrace/src/scripts/utils/start_historical_data_upload.ts index dd848d9f66c637..ee462085ef79cd 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/utils/start_historical_data_upload.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/start_historical_data_upload.ts @@ -24,6 +24,7 @@ export async function startHistoricalDataUpload({ target, file, writeTarget, + scenarioOpts, }: RunOptions & { from: number; to: number }) { let requestedUntil: number = from; @@ -57,6 +58,7 @@ export async function startHistoricalDataUpload({ target, workers, writeTarget, + scenarioOpts, }; const worker = new Worker(Path.join(__dirname, './upload_next_batch.js'), { diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/start_live_data_upload.ts b/packages/elastic-apm-synthtrace/src/scripts/utils/start_live_data_upload.ts index 3610ffae3c7e64..ab4eee4f255b93 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/utils/start_live_data_upload.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/start_live_data_upload.ts @@ -24,6 +24,7 @@ export async function startLiveDataUpload({ logLevel, workers, writeTarget, + scenarioOpts, }: RunOptions & { start: number }) { let queuedEvents: ElasticsearchOutput[] = []; let requestedUntil: number = start; @@ -41,6 +42,7 @@ export async function startLiveDataUpload({ target, workers, writeTarget, + scenarioOpts, }); function uploadNextBatch() { diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/upload_next_batch.ts b/packages/elastic-apm-synthtrace/src/scripts/utils/upload_next_batch.ts index c25fc7ca9f1c2e..973cbc2266cbe3 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/utils/upload_next_batch.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/upload_next_batch.ts @@ -17,6 +17,7 @@ export interface WorkerData { bucketFrom: number; bucketTo: number; file: string; + scenarioOpts: Record | undefined; logLevel: LogLevel; clientWorkers: number; batchSize: number; @@ -39,6 +40,7 @@ const { workers, target, writeTarget, + scenarioOpts, } = workerData as WorkerData; async function uploadNextBatch() { @@ -63,6 +65,7 @@ async function uploadNextBatch() { target, workers, writeTarget, + scenarioOpts, }); const events = logger.perf('execute_scenario', () => diff --git a/packages/kbn-es-query/src/filters/build_filters/query_string_filter.ts b/packages/kbn-es-query/src/filters/build_filters/query_string_filter.ts index 69f10efd97d66c..e3143a318a16e0 100644 --- a/packages/kbn-es-query/src/filters/build_filters/query_string_filter.ts +++ b/packages/kbn-es-query/src/filters/build_filters/query_string_filter.ts @@ -39,11 +39,9 @@ export const isQueryStringFilter = (filter: Filter): filter is QueryStringFilter * * @public */ -export const buildQueryFilter = (query: QueryStringFilter['query'], index: string, alias: string) => - ({ - query, - meta: { - index, - alias, - }, - } as QueryStringFilter); +export const buildQueryFilter = ( + query: QueryStringFilter['query'], + index: string, + alias?: string, + meta: QueryStringFilterMeta = {} +) => ({ query, meta: { index, alias, ...meta } }); diff --git a/packages/kbn-typed-react-router-config/src/types/index.ts b/packages/kbn-typed-react-router-config/src/types/index.ts index 25e6c32705f4e0..97b58ce5a700ff 100644 --- a/packages/kbn-typed-react-router-config/src/types/index.ts +++ b/packages/kbn-typed-react-router-config/src/types/index.ts @@ -257,6 +257,18 @@ type MapRoutes = TRoutes extends [Route] MapRoute & MapRoute & MapRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute : {}; // const element = null as any; diff --git a/src/core/server/elasticsearch/client/cluster_client.test.ts b/src/core/server/elasticsearch/client/cluster_client.test.ts index f96f39349887ea..b9fb8a21f0a8bc 100644 --- a/src/core/server/elasticsearch/client/cluster_client.test.ts +++ b/src/core/server/elasticsearch/client/cluster_client.test.ts @@ -144,13 +144,13 @@ describe('ClusterClient', () => { }); }); - it('creates a scoped facade with filtered auth headers', () => { + it('does not filter auth headers', () => { const config = createConfig({ requestHeadersWhitelist: ['authorization'], }); getAuthHeaders.mockReturnValue({ authorization: 'auth', - other: 'nope', + other: 'yep', }); const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders); @@ -160,7 +160,12 @@ describe('ClusterClient', () => { expect(scopedClient.child).toHaveBeenCalledTimes(1); expect(scopedClient.child).toHaveBeenCalledWith({ - headers: { ...DEFAULT_HEADERS, authorization: 'auth', 'x-opaque-id': expect.any(String) }, + headers: { + ...DEFAULT_HEADERS, + authorization: 'auth', + other: 'yep', + 'x-opaque-id': expect.any(String), + }, }); }); @@ -170,7 +175,7 @@ describe('ClusterClient', () => { }); getAuthHeaders.mockReturnValue({ authorization: 'auth', - other: 'nope', + other: 'yep', }); const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders); @@ -184,7 +189,12 @@ describe('ClusterClient', () => { expect(scopedClient.child).toHaveBeenCalledTimes(1); expect(scopedClient.child).toHaveBeenCalledWith({ - headers: { ...DEFAULT_HEADERS, authorization: 'auth', 'x-opaque-id': expect.any(String) }, + headers: { + ...DEFAULT_HEADERS, + authorization: 'auth', + other: 'yep', + 'x-opaque-id': expect.any(String), + }, }); }); diff --git a/src/core/server/elasticsearch/client/cluster_client.ts b/src/core/server/elasticsearch/client/cluster_client.ts index 1f3118c77aa0f9..1744d7a41841bb 100644 --- a/src/core/server/elasticsearch/client/cluster_client.ts +++ b/src/core/server/elasticsearch/client/cluster_client.ts @@ -54,8 +54,6 @@ export interface ICustomClusterClient extends IClusterClient { export class ClusterClient implements ICustomClusterClient { public readonly asInternalUser: KibanaClient; private readonly rootScopedClient: KibanaClient; - private readonly allowListHeaders: string[]; - private isClosed = false; constructor( @@ -72,8 +70,6 @@ export class ClusterClient implements ICustomClusterClient { getExecutionContext, scoped: true, }); - - this.allowListHeaders = ['x-opaque-id', ...this.config.requestHeadersWhitelist]; } asScoped(request: ScopeableRequest) { @@ -95,14 +91,15 @@ export class ClusterClient implements ICustomClusterClient { private getScopedHeaders(request: ScopeableRequest): Headers { let scopedHeaders: Headers; if (isRealRequest(request)) { - const requestHeaders = ensureRawRequest(request).headers; + const requestHeaders = ensureRawRequest(request).headers ?? {}; const requestIdHeaders = isKibanaRequest(request) ? { 'x-opaque-id': request.id } : {}; - const authHeaders = this.getAuthHeaders(request); + const authHeaders = this.getAuthHeaders(request) ?? {}; - scopedHeaders = filterHeaders( - { ...requestHeaders, ...requestIdHeaders, ...authHeaders }, - this.allowListHeaders - ); + scopedHeaders = { + ...filterHeaders(requestHeaders, this.config.requestHeadersWhitelist), + ...requestIdHeaders, + ...authHeaders, + }; } else { scopedHeaders = filterHeaders(request?.headers ?? {}, this.config.requestHeadersWhitelist); } diff --git a/src/core/server/http/router/headers.ts b/src/core/server/http/router/headers.ts index 89a32d6dc5d2ff..68a19935652096 100644 --- a/src/core/server/http/router/headers.ts +++ b/src/core/server/http/router/headers.ts @@ -61,7 +61,7 @@ export function filterHeaders( headers: Headers, fieldsToKeep: string[], fieldsToExclude: string[] = [] -) { +): Headers { const fieldsToExcludeNormalized = fieldsToExclude.map(normalizeHeaderField); // Normalize list of headers we want to allow in upstream request const fieldsToKeepNormalized = fieldsToKeep diff --git a/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts index 3d8dcad08149c9..b1c421ec9168ae 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts @@ -67,7 +67,7 @@ describe('migration v2', () => { es: { license: 'basic', dataArchive: Path.join(__dirname, 'archives', '7.14.0_xpack_sample_saved_objects.zip'), - esArgs: ['http.max_content_length=1715275b'], + esArgs: ['http.max_content_length=1715329b'], }, }, })); @@ -85,7 +85,7 @@ describe('migration v2', () => { }); it('completes the migration even when a full batch would exceed ES http.max_content_length', async () => { - root = createRoot({ maxBatchSizeBytes: 1715275 }); + root = createRoot({ maxBatchSizeBytes: 1715329 }); esServer = await startES(); await root.preboot(); await root.setup(); @@ -109,7 +109,7 @@ describe('migration v2', () => { await root.preboot(); await root.setup(); await expect(root.start()).rejects.toMatchInlineSnapshot( - `[Error: Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715274 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.]` + `[Error: Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715329 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.]` ); await retryAsync( @@ -122,7 +122,7 @@ describe('migration v2', () => { expect( records.find((rec) => rec.message.startsWith( - `Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715274 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.` + `Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715329 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.` ) ) ).toBeDefined(); diff --git a/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts index 33f00248a110a7..0352e655937da3 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts @@ -54,7 +54,7 @@ describe('migration v2', () => { }); it('fails with a descriptive message when maxBatchSizeBytes exceeds ES http.max_content_length', async () => { - root = createRoot({ maxBatchSizeBytes: 1715275 }); + root = createRoot({ maxBatchSizeBytes: 1715329 }); esServer = await startES(); await root.preboot(); await root.setup(); diff --git a/src/core/server/saved_objects/service/lib/internal_bulk_resolve.ts b/src/core/server/saved_objects/service/lib/internal_bulk_resolve.ts index 967122e414731f..6c11fa1f245c70 100644 --- a/src/core/server/saved_objects/service/lib/internal_bulk_resolve.ts +++ b/src/core/server/saved_objects/service/lib/internal_bulk_resolve.ts @@ -212,7 +212,7 @@ export async function internalBulkResolve( } ); - await incrementCounterInternal( + incrementCounterInternal( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, resolveCounter.getCounterFields(), diff --git a/src/plugins/console/public/application/components/settings_modal.tsx b/src/plugins/console/public/application/components/settings_modal.tsx index aad45b6fc0bcf3..c4be329dabcb88 100644 --- a/src/plugins/console/public/application/components/settings_modal.tsx +++ b/src/plugins/console/public/application/components/settings_modal.tsx @@ -7,7 +7,7 @@ */ import _ from 'lodash'; -import React, { Fragment, useCallback, useState, ChangeEventHandler } from 'react'; +import React, { Fragment, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -23,22 +23,38 @@ import { EuiModalHeader, EuiModalHeaderTitle, EuiSwitch, - EuiSelect, - EuiFlexGroup, - EuiFlexItem, + EuiSuperSelect, } from '@elastic/eui'; import { DevToolsSettings } from '../../services'; export type AutocompleteOptions = 'fields' | 'indices' | 'templates'; -const PRESETS_IN_MINUTES = [1, 5, 10]; -const intervalOptions = PRESETS_IN_MINUTES.map((value) => ({ - value: value * 60000, - text: i18n.translate('console.settingsPage.refreshInterval.timeInterval', { - defaultMessage: '{value} {value, plural, one {minute} other {minutes}}', +const onceTimeInterval = () => + i18n.translate('console.settingsPage.refreshInterval.onceTimeInterval', { + defaultMessage: 'Once, when console loads', + }); + +const everyNMinutesTimeInterval = (value: number) => + i18n.translate('console.settingsPage.refreshInterval.everyNMinutesTimeInterval', { + defaultMessage: 'Every {value} {value, plural, one {minute} other {minutes}}', values: { value }, - }), + }); + +const everyHourTimeInterval = () => + i18n.translate('console.settingsPage.refreshInterval.everyHourTimeInterval', { + defaultMessage: 'Every hour', + }); + +const PRESETS_IN_MINUTES = [0, 1, 10, 20, 60]; +const intervalOptions = PRESETS_IN_MINUTES.map((value) => ({ + value: (value * 60000).toString(), + inputDisplay: + value === 0 + ? onceTimeInterval() + : value === 60 + ? everyHourTimeInterval() + : everyNMinutesTimeInterval(value), })); interface Props { @@ -112,10 +128,12 @@ export function DevToolsSettingsModal(props: Props) { }); } - const onIntervalChange: ChangeEventHandler = useCallback( - (e) => setPollInterval(parseInt(e.target.value, 10)), - [] - ); + const onPollingIntervalChange = useCallback((value: string) => { + const sanitizedValue = parseInt(value, 10); + + setPolling(!!sanitizedValue); + setPollInterval(sanitizedValue); + }, []); // It only makes sense to show polling options if the user needs to fetch any data. const pollingFields = @@ -125,43 +143,22 @@ export function DevToolsSettingsModal(props: Props) { label={ } helpText={ } > - - - - } - onChange={(e) => setPolling(e.target.checked)} - /> - - - - - + { + const { filters = [] } = input; + return { + type: 'filter', + filterType: 'filter', + and: filters.map(adaptToExpressionValueFilter), + }; + }, }, }; diff --git a/src/plugins/data/common/search/expressions/select_filter.test.ts b/src/plugins/data/common/search/expressions/select_filter.test.ts index a2515dbcb171d6..8ef2b77b1fcc6c 100644 --- a/src/plugins/data/common/search/expressions/select_filter.test.ts +++ b/src/plugins/data/common/search/expressions/select_filter.test.ts @@ -28,6 +28,12 @@ describe('interpreter/functions#selectFilter', () => { }, query: {}, }, + { + meta: { + group: 'g3', + }, + query: {}, + }, { meta: { group: 'g1', @@ -68,6 +74,12 @@ describe('interpreter/functions#selectFilter', () => { }, "query": Object {}, }, + Object { + "meta": Object { + "group": "g3", + }, + "query": Object {}, + }, Object { "meta": Object { "controlledBy": "i1", @@ -94,8 +106,8 @@ describe('interpreter/functions#selectFilter', () => { `); }); - it('selects filters belonging to certain group', () => { - const actual = fn(kibanaContext, { group: 'g1' }, createMockContext()); + it('selects filters belonging to certain groups', () => { + const actual = fn(kibanaContext, { group: ['g1', 'g3'] }, createMockContext()); expect(actual).toMatchInlineSnapshot(` Object { "filters": Array [ @@ -105,6 +117,12 @@ describe('interpreter/functions#selectFilter', () => { }, "query": Object {}, }, + Object { + "meta": Object { + "group": "g3", + }, + "query": Object {}, + }, Object { "meta": Object { "controlledBy": "i1", diff --git a/src/plugins/data/common/search/expressions/select_filter.ts b/src/plugins/data/common/search/expressions/select_filter.ts index 3e76f3a6426c2a..600da4b16d2749 100644 --- a/src/plugins/data/common/search/expressions/select_filter.ts +++ b/src/plugins/data/common/search/expressions/select_filter.ts @@ -11,7 +11,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { KibanaContext } from './kibana_context_type'; interface Arguments { - group?: string; + group: string[]; from?: string; ungrouped?: boolean; } @@ -37,6 +37,7 @@ export const selectFilterFunction: ExpressionFunctionSelectFilter = { help: i18n.translate('data.search.functions.selectFilter.group.help', { defaultMessage: 'Select only filters belonging to the provided group', }), + multi: true, }, from: { types: ['string'], @@ -54,13 +55,15 @@ export const selectFilterFunction: ExpressionFunctionSelectFilter = { }, }, - fn(input, { group, ungrouped, from }) { + fn(input, { group = [], ungrouped, from }) { return { ...input, filters: input.filters?.filter(({ meta }) => { const isGroupMatching = - (!group && !ungrouped) || group === meta.group || (ungrouped && !meta.group); + (!group.length && !ungrouped) || + (meta.group && group.length && group.includes(meta.group)) || + (ungrouped && !meta.group); const isOriginMatching = !from || from === meta.controlledBy; return isGroupMatching && isOriginMatching; }) || [], diff --git a/src/plugins/data/common/search/expressions/utils/filters_adapter.ts b/src/plugins/data/common/search/expressions/utils/filters_adapter.ts new file mode 100644 index 00000000000000..304150ad94813a --- /dev/null +++ b/src/plugins/data/common/search/expressions/utils/filters_adapter.ts @@ -0,0 +1,74 @@ +/* + * Copyright 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 { Filter } from '@kbn/es-query'; +import { ExpressionValueFilter } from 'src/plugins/expressions/common'; + +function getGroupFromFilter(filter: Filter) { + const { meta } = filter; + const { group } = meta ?? {}; + return group; +} + +function range(filter: Filter): ExpressionValueFilter { + const { query } = filter; + const { range: rangeQuery } = query ?? {}; + const column = Object.keys(rangeQuery)[0]; + const { gte: from, lte: to } = rangeQuery[column] ?? {}; + return { + filterGroup: getGroupFromFilter(filter), + from, + to, + column, + type: 'filter', + filterType: 'time', + and: [], + }; +} + +function luceneQueryString(filter: Filter): ExpressionValueFilter { + const { query } = filter; + const { query_string: queryString } = query ?? {}; + const { query: queryValue } = queryString; + + return { + filterGroup: getGroupFromFilter(filter), + query: queryValue, + type: 'filter', + filterType: 'luceneQueryString', + and: [], + }; +} + +function term(filter: Filter): ExpressionValueFilter { + const { query } = filter; + const { term: termQuery } = query ?? {}; + const column = Object.keys(termQuery)[0]; + const { value } = termQuery[column] ?? {}; + + return { + filterGroup: getGroupFromFilter(filter), + column, + value, + type: 'filter', + filterType: 'exactly', + and: [], + }; +} + +const adapters = { range, term, luceneQueryString }; + +export function adaptToExpressionValueFilter(filter: Filter): ExpressionValueFilter { + const { query = {} } = filter; + const filterType = Object.keys(query)[0] as keyof typeof adapters; + const adapt = adapters[filterType]; + if (!adapt || typeof adapt !== 'function') { + throw new Error(`Unknown filter type: ${filterType}`); + } + return adapt(filter); +} diff --git a/src/plugins/data/common/search/expressions/utils/index.ts b/src/plugins/data/common/search/expressions/utils/index.ts index a6ea8da6ac6e91..b678bd8781d936 100644 --- a/src/plugins/data/common/search/expressions/utils/index.ts +++ b/src/plugins/data/common/search/expressions/utils/index.ts @@ -7,3 +7,4 @@ */ export * from './function_wrapper'; +export { adaptToExpressionValueFilter } from './filters_adapter'; diff --git a/src/plugins/discover/public/application/context/context_app.tsx b/src/plugins/discover/public/application/context/context_app.tsx index a1a7cd0c0206e7..8faabdbd0682d7 100644 --- a/src/plugins/discover/public/application/context/context_app.tsx +++ b/src/plugins/discover/public/application/context/context_app.tsx @@ -152,7 +152,7 @@ export const ContextApp = ({ indexPattern, anchorId }: ContextAppProps) => { - + (); + const anchorId = decodeURIComponent(id); const breadcrumb = useMainRouteBreadcrumb(); useEffect(() => { @@ -68,5 +69,5 @@ export function ContextAppRoute(props: DiscoverRouteProps) { return ; } - return ; + return ; } diff --git a/src/plugins/expressions/common/executor/__snapshots__/executor.test.ts.snap b/src/plugins/expressions/common/executor/__snapshots__/executor.test.ts.snap index 1d4a8614b29216..2714dbd2265a4f 100644 --- a/src/plugins/expressions/common/executor/__snapshots__/executor.test.ts.snap +++ b/src/plugins/expressions/common/executor/__snapshots__/executor.test.ts.snap @@ -4,5 +4,6 @@ exports[`Executor .inject .getAllMigrations returns list of all registered migra Object { "7.10.0": [Function], "7.10.1": [Function], + "8.1.0": [Function], } `; diff --git a/src/plugins/expressions/common/executor/executor.test.ts b/src/plugins/expressions/common/executor/executor.test.ts index 7e314788b03fdc..be985c2720f8bd 100644 --- a/src/plugins/expressions/common/executor/executor.test.ts +++ b/src/plugins/expressions/common/executor/executor.test.ts @@ -246,6 +246,40 @@ describe('Executor', () => { }); describe('.migrateToLatest', () => { + const fnMigrateTo = { + name: 'fnMigrateTo', + help: 'test', + args: { + bar: { + types: ['string'], + help: 'test', + }, + }, + fn: jest.fn(), + }; + + const fnMigrateFrom = { + name: 'fnMigrateFrom', + help: 'test', + args: { + bar: { + types: ['string'], + help: 'test', + }, + }, + migrations: { + '8.1.0': ((state: ExpressionAstFunction, version: string) => { + const migrateToAst = parseExpression('fnMigrateTo'); + const { arguments: args } = state; + const ast = { ...migrateToAst.chain[0], arguments: args }; + return { type: 'expression', chain: [ast, ast] }; + }) as unknown as MigrateFunction, + }, + fn: jest.fn(), + }; + executor.registerFunction(fnMigrateFrom); + executor.registerFunction(fnMigrateTo); + test('calls migrate function for every expression function in expression', () => { executor.migrateToLatest({ state: parseExpression( @@ -255,6 +289,25 @@ describe('Executor', () => { }); expect(migrateFn).toBeCalledTimes(5); }); + + test('migrates expression function to expression function or chain of expression functions', () => { + const plainExpression = 'foo bar={foo bar="baz" | foo bar={foo bar="baz"}}'; + const plainExpressionAst = parseExpression(plainExpression); + const migratedExpressionAst = executor.migrateToLatest({ + state: parseExpression(`${plainExpression} | fnMigrateFrom bar="baz" | fnMigrateTo`), + version: '8.0.0', + }); + + expect(migratedExpressionAst).toEqual({ + type: 'expression', + chain: [ + ...plainExpressionAst.chain, + { type: 'function', function: 'fnMigrateTo', arguments: { bar: ['baz'] } }, + { type: 'function', function: 'fnMigrateTo', arguments: { bar: ['baz'] } }, + { type: 'function', function: 'fnMigrateTo', arguments: {} }, + ], + }); + }); }); }); }); diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 01b54d13f8a76c..86516344031a08 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -241,6 +241,61 @@ export class Executor = Record ExpressionAstFunction | ExpressionAstExpression + ): ExpressionAstExpression { + let additionalFunctions = 0; + return ( + ast.chain.reduce( + (newAst: ExpressionAstExpression, funcAst: ExpressionAstFunction, index: number) => { + const realIndex = index + additionalFunctions; + const { function: fnName, arguments: fnArgs } = funcAst; + const fn = getByAlias(this.getFunctions(), fnName); + if (!fn) { + return newAst; + } + + // if any of arguments are expressions we should migrate those first + funcAst.arguments = mapValues(fnArgs, (asts) => + asts.map((arg) => + arg != null && typeof arg === 'object' + ? this.walkAstAndTransform(arg, transform) + : arg + ) + ); + + const transformedFn = transform(fn, funcAst); + if (transformedFn.type === 'function') { + const prevChain = realIndex > 0 ? newAst.chain.slice(0, realIndex) : []; + const nextChain = newAst.chain.slice(realIndex + 1); + return { + ...newAst, + chain: [...prevChain, transformedFn, ...nextChain], + }; + } + + if (transformedFn.type === 'expression') { + const { chain } = transformedFn; + const prevChain = realIndex > 0 ? newAst.chain.slice(0, realIndex) : []; + const nextChain = newAst.chain.slice(realIndex + 1); + additionalFunctions += chain.length - 1; + return { + ...newAst, + chain: [...prevChain, ...chain, ...nextChain], + }; + } + + return newAst; + }, + ast + ) ?? ast + ); + } + public inject(ast: ExpressionAstExpression, references: SavedObjectReference[]) { let linkId = 0; return this.walkAst(cloneDeep(ast), (fn, link) => { @@ -296,14 +351,12 @@ export class Executor = Record { + return this.walkAstAndTransform(cloneDeep(ast) as ExpressionAstExpression, (fn, link) => { if (!fn.migrations[version]) { - return; + return link; } - ({ arguments: link.arguments, type: link.type } = fn.migrations[version]( - link - ) as ExpressionAstFunction); + return fn.migrations[version](link) as ExpressionAstExpression; }); } diff --git a/src/plugins/expressions/common/expression_types/specs/filter.ts b/src/plugins/expressions/common/expression_types/specs/filter.ts index dad69f9433a238..915beceb988fdf 100644 --- a/src/plugins/expressions/common/expression_types/specs/filter.ts +++ b/src/plugins/expressions/common/expression_types/specs/filter.ts @@ -15,6 +15,7 @@ export type ExpressionValueFilter = ExpressionValueBoxed< 'filter', { filterType?: string; + filterGroup?: string; value?: string; column?: string; and: ExpressionValueFilter[]; diff --git a/src/plugins/home/server/services/sample_data/errors.ts b/src/plugins/home/server/services/sample_data/errors.ts new file mode 100644 index 00000000000000..832c520b9ade8d --- /dev/null +++ b/src/plugins/home/server/services/sample_data/errors.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export class SampleDataInstallError extends Error { + constructor(message: string, public readonly httpCode: number) { + super(message); + } +} diff --git a/src/plugins/home/server/services/sample_data/lib/insert_data_into_index.ts b/src/plugins/home/server/services/sample_data/lib/insert_data_into_index.ts new file mode 100644 index 00000000000000..4a7d7e9813dcc3 --- /dev/null +++ b/src/plugins/home/server/services/sample_data/lib/insert_data_into_index.ts @@ -0,0 +1,74 @@ +/* + * Copyright 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 { IScopedClusterClient, Logger } from 'kibana/server'; +import type { DataIndexSchema } from './sample_dataset_registry_types'; +import { + translateTimeRelativeToDifference, + translateTimeRelativeToWeek, +} from './translate_timestamp'; +import { loadData } from './load_data'; + +export const insertDataIntoIndex = ({ + dataIndexConfig, + logger, + esClient, + index, + nowReference, +}: { + dataIndexConfig: DataIndexSchema; + index: string; + nowReference: string; + esClient: IScopedClusterClient; + logger: Logger; +}) => { + const updateTimestamps = (doc: any) => { + dataIndexConfig.timeFields + .filter((timeFieldName: string) => doc[timeFieldName]) + .forEach((timeFieldName: string) => { + doc[timeFieldName] = dataIndexConfig.preserveDayOfWeekTimeOfDay + ? translateTimeRelativeToWeek( + doc[timeFieldName], + dataIndexConfig.currentTimeMarker, + nowReference + ) + : translateTimeRelativeToDifference( + doc[timeFieldName], + dataIndexConfig.currentTimeMarker, + nowReference + ); + }); + return doc; + }; + + const bulkInsert = async (docs: unknown[]) => { + const insertCmd = { index: { _index: index } }; + const bulk: unknown[] = []; + docs.forEach((doc: unknown) => { + bulk.push(insertCmd); + bulk.push(updateTimestamps(doc)); + }); + + const { body: resp } = await esClient.asCurrentUser.bulk({ + body: bulk, + }); + + if (resp.errors) { + const errMsg = `sample_data install errors while bulk inserting. Elasticsearch response: ${JSON.stringify( + resp, + null, + '' + )}`; + logger.warn(errMsg); + return Promise.reject( + new Error(`Unable to load sample data into index "${index}", see kibana logs for details`) + ); + } + }; + return loadData(dataIndexConfig.dataPath, bulkInsert); // this returns a Promise +}; diff --git a/src/plugins/home/server/services/sample_data/lib/load_data.ts b/src/plugins/home/server/services/sample_data/lib/load_data.ts index 4d203f791da97d..b039243b0cc25c 100644 --- a/src/plugins/home/server/services/sample_data/lib/load_data.ts +++ b/src/plugins/home/server/services/sample_data/lib/load_data.ts @@ -12,7 +12,10 @@ import { createUnzip } from 'zlib'; const BULK_INSERT_SIZE = 500; -export function loadData(path: any, bulkInsert: (docs: any[]) => Promise) { +export function loadData( + path: string, + bulkInsert: (docs: unknown[]) => Promise +): Promise { return new Promise((resolve, reject) => { let count: number = 0; let docs: any[] = []; diff --git a/src/plugins/home/server/services/sample_data/routes/install.ts b/src/plugins/home/server/services/sample_data/routes/install.ts index 17d35c6cb4b7ee..21c77ec51e5efe 100644 --- a/src/plugins/home/server/services/sample_data/routes/install.ts +++ b/src/plugins/home/server/services/sample_data/routes/install.ts @@ -6,73 +6,12 @@ * Side Public License, v 1. */ -import { Readable } from 'stream'; import { schema } from '@kbn/config-schema'; -import { IRouter, Logger, IScopedClusterClient } from 'src/core/server'; +import { IRouter, Logger } from 'src/core/server'; import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types'; -import { createIndexName } from '../lib/create_index_name'; -import { - dateToIso8601IgnoringTime, - translateTimeRelativeToDifference, - translateTimeRelativeToWeek, -} from '../lib/translate_timestamp'; -import { loadData } from '../lib/load_data'; import { SampleDataUsageTracker } from '../usage/usage'; -import { getSavedObjectsClient } from './utils'; -import { getUniqueObjectTypes } from '../lib/utils'; - -const insertDataIntoIndex = ( - dataIndexConfig: any, - index: string, - nowReference: string, - esClient: IScopedClusterClient, - logger: Logger -) => { - function updateTimestamps(doc: any) { - dataIndexConfig.timeFields - .filter((timeFieldName: string) => doc[timeFieldName]) - .forEach((timeFieldName: string) => { - doc[timeFieldName] = dataIndexConfig.preserveDayOfWeekTimeOfDay - ? translateTimeRelativeToWeek( - doc[timeFieldName], - dataIndexConfig.currentTimeMarker, - nowReference - ) - : translateTimeRelativeToDifference( - doc[timeFieldName], - dataIndexConfig.currentTimeMarker, - nowReference - ); - }); - return doc; - } - - const bulkInsert = async (docs: any) => { - const insertCmd = { index: { _index: index } }; - const bulk: any[] = []; - docs.forEach((doc: any) => { - bulk.push(insertCmd); - bulk.push(updateTimestamps(doc)); - }); - - const { body: resp } = await esClient.asCurrentUser.bulk({ - body: bulk, - }); - - if (resp.errors) { - const errMsg = `sample_data install errors while bulk inserting. Elasticsearch response: ${JSON.stringify( - resp, - null, - '' - )}`; - logger.warn(errMsg); - return Promise.reject( - new Error(`Unable to load sample data into index "${index}", see kibana logs for details`) - ); - } - }; - return loadData(dataIndexConfig.dataPath, bulkInsert); // this returns a Promise -}; +import { getSampleDataInstaller } from './utils'; +import { SampleDataInstallError } from '../errors'; export function createInstallRoute( router: IRouter, @@ -95,86 +34,38 @@ export function createInstallRoute( if (!sampleDataset) { return res.notFound(); } + // @ts-ignore Custom query validation used const now = query.now ? new Date(query.now) : new Date(); - const nowReference = dateToIso8601IgnoringTime(now); - const counts = {}; - for (let i = 0; i < sampleDataset.dataIndices.length; i++) { - const dataIndexConfig = sampleDataset.dataIndices[i]; - const index = createIndexName(sampleDataset.id, dataIndexConfig.id); - // clean up any old installation of dataset - try { - await context.core.elasticsearch.client.asCurrentUser.indices.delete({ - index, - }); - } catch (err) { - // ignore delete errors - } + const sampleDataInstaller = getSampleDataInstaller({ + datasetId: sampleDataset.id, + sampleDatasets, + logger, + context, + }); - try { - await context.core.elasticsearch.client.asCurrentUser.indices.create({ - index, + try { + const installResult = await sampleDataInstaller.install(params.id, now); + // track the usage operation in a non-blocking way + usageTracker.addInstall(params.id); + return res.ok({ + body: { + elasticsearchIndicesCreated: installResult.createdDocsPerIndex, + kibanaSavedObjectsLoaded: installResult.createdSavedObjects, + }, + }); + } catch (e) { + if (e instanceof SampleDataInstallError) { + return res.customError({ body: { - settings: { index: { number_of_shards: 1, auto_expand_replicas: '0-1' } }, - mappings: { properties: dataIndexConfig.fields }, + message: e.message, }, + statusCode: e.httpCode, }); - } catch (err) { - const errMsg = `Unable to create sample data index "${index}", error: ${err.message}`; - logger.warn(errMsg); - return res.customError({ body: errMsg, statusCode: err.status }); - } - - try { - const count = await insertDataIntoIndex( - dataIndexConfig, - index, - nowReference, - context.core.elasticsearch.client, - logger - ); - (counts as any)[index] = count; - } catch (err) { - const errMsg = `sample_data install errors while loading data. Error: ${err}`; - throw new Error(errMsg); } + throw e; } - - const { getImporter } = context.core.savedObjects; - const objectTypes = getUniqueObjectTypes(sampleDataset.savedObjects); - const savedObjectsClient = getSavedObjectsClient(context, objectTypes); - const importer = getImporter(savedObjectsClient); - - const savedObjects = sampleDataset.savedObjects.map(({ version, ...obj }) => obj); - const readStream = Readable.from(savedObjects); - - try { - const { errors = [] } = await importer.import({ - readStream, - overwrite: true, - createNewCopies: false, - }); - if (errors.length > 0) { - const errMsg = `sample_data install errors while loading saved objects. Errors: ${JSON.stringify( - errors.map(({ type, id, error }) => ({ type, id, error })) // discard other fields - )}`; - logger.warn(errMsg); - return res.customError({ body: errMsg, statusCode: 500 }); - } - } catch (err) { - const errMsg = `import failed, error: ${err.message}`; - throw new Error(errMsg); - } - usageTracker.addInstall(params.id); - - // FINALLY - return res.ok({ - body: { - elasticsearchIndicesCreated: counts, - kibanaSavedObjectsLoaded: sampleDataset.savedObjects.length, - }, - }); } ); } diff --git a/src/plugins/home/server/services/sample_data/routes/uninstall.ts b/src/plugins/home/server/services/sample_data/routes/uninstall.ts index b0e8e6f102f1ea..52f725da4906bc 100644 --- a/src/plugins/home/server/services/sample_data/routes/uninstall.ts +++ b/src/plugins/home/server/services/sample_data/routes/uninstall.ts @@ -6,15 +6,12 @@ * Side Public License, v 1. */ -import { isBoom } from '@hapi/boom'; import { schema } from '@kbn/config-schema'; import type { IRouter, Logger } from 'src/core/server'; import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types'; -import { createIndexName } from '../lib/create_index_name'; import { SampleDataUsageTracker } from '../usage/usage'; -import { findSampleObjects } from '../lib/find_sample_objects'; -import { getUniqueObjectTypes } from '../lib/utils'; -import { getSavedObjectsClient } from './utils'; +import { getSampleDataInstaller } from './utils'; +import { SampleDataInstallError } from '../errors'; export function createUninstallRoute( router: IRouter, @@ -31,62 +28,33 @@ export function createUninstallRoute( }, async (context, request, response) => { const sampleDataset = sampleDatasets.find(({ id }) => id === request.params.id); - if (!sampleDataset) { return response.notFound(); } - for (let i = 0; i < sampleDataset.dataIndices.length; i++) { - const dataIndexConfig = sampleDataset.dataIndices[i]; - const index = createIndexName(sampleDataset.id, dataIndexConfig.id); - - try { - // TODO: don't delete the index if sample data exists in other spaces (#116677) - await context.core.elasticsearch.client.asCurrentUser.indices.delete({ index }); - } catch (err) { - // if the index doesn't exist, ignore the error and proceed - if (err.body.status !== 404) { - return response.customError({ - statusCode: err.body.status, - body: { - message: `Unable to delete sample data index "${index}", error: ${err.body.error.type}`, - }, - }); - } - } - } - - const objects = sampleDataset.savedObjects.map(({ type, id }) => ({ type, id })); - const objectTypes = getUniqueObjectTypes(objects); - const client = getSavedObjectsClient(context, objectTypes); - const findSampleObjectsResult = await findSampleObjects({ client, logger, objects }); - - const objectsToDelete = findSampleObjectsResult.filter(({ foundObjectId }) => foundObjectId); - const deletePromises = objectsToDelete.map(({ type, foundObjectId }) => - client.delete(type, foundObjectId!).catch((err) => { - // if the object doesn't exist, ignore the error and proceed - if (isBoom(err) && err.output.statusCode === 404) { - return; - } - throw err; - }) - ); + const sampleDataInstaller = getSampleDataInstaller({ + datasetId: sampleDataset.id, + sampleDatasets, + logger, + context, + }); try { - await Promise.all(deletePromises); - } catch (err) { - return response.customError({ - statusCode: err.body.status, - body: { - message: `Unable to delete sample dataset saved objects, error: ${err.body.error.type}`, - }, - }); + await sampleDataInstaller.uninstall(request.params.id); + // track the usage operation in a non-blocking way + usageTracker.addUninstall(request.params.id); + return response.noContent(); + } catch (e) { + if (e instanceof SampleDataInstallError) { + return response.customError({ + body: { + message: e.message, + }, + statusCode: e.httpCode, + }); + } + throw e; } - - // track the usage operation in a non-blocking way - usageTracker.addUninstall(request.params.id); - - return response.noContent(); } ); } diff --git a/src/plugins/home/server/services/sample_data/routes/utils.ts b/src/plugins/home/server/services/sample_data/routes/utils.ts index 6bab00895440ab..36b5534d9f4af6 100644 --- a/src/plugins/home/server/services/sample_data/routes/utils.ts +++ b/src/plugins/home/server/services/sample_data/routes/utils.ts @@ -6,12 +6,41 @@ * Side Public License, v 1. */ -import type { RequestHandlerContext } from 'src/core/server'; +import type { RequestHandlerContext, Logger } from 'src/core/server'; +import type { SampleDatasetSchema } from '../lib/sample_dataset_registry_types'; +import { SampleDataInstaller } from '../sample_data_installer'; +import { getUniqueObjectTypes } from '../lib/utils'; -export function getSavedObjectsClient(context: RequestHandlerContext, objectTypes: string[]) { +export const getSampleDataInstaller = ({ + datasetId, + context, + sampleDatasets, + logger, +}: { + datasetId: string; + context: RequestHandlerContext; + sampleDatasets: SampleDatasetSchema[]; + logger: Logger; +}) => { + const sampleDataset = sampleDatasets.find(({ id }) => id === datasetId)!; + const { getImporter, client: soClient } = context.core.savedObjects; + const objectTypes = getUniqueObjectTypes(sampleDataset.savedObjects); + const savedObjectsClient = getSavedObjectsClient(context, objectTypes); + const soImporter = getImporter(savedObjectsClient); + + return new SampleDataInstaller({ + esClient: context.core.elasticsearch.client, + soImporter, + soClient, + logger, + sampleDatasets, + }); +}; + +export const getSavedObjectsClient = (context: RequestHandlerContext, objectTypes: string[]) => { const { getClient, typeRegistry } = context.core.savedObjects; const includedHiddenTypes = objectTypes.filter((supportedType) => typeRegistry.isHidden(supportedType) ); return getClient({ includedHiddenTypes }); -} +}; diff --git a/src/plugins/home/server/services/sample_data/sample_data_installer.test.mocks.ts b/src/plugins/home/server/services/sample_data/sample_data_installer.test.mocks.ts new file mode 100644 index 00000000000000..c8bdf0cc692b81 --- /dev/null +++ b/src/plugins/home/server/services/sample_data/sample_data_installer.test.mocks.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const insertDataIntoIndexMock = jest.fn(); +jest.doMock('./lib/insert_data_into_index', () => ({ + insertDataIntoIndex: insertDataIntoIndexMock, +})); + +export const findSampleObjectsMock = jest.fn(); +jest.doMock('./lib/find_sample_objects', () => ({ + findSampleObjects: findSampleObjectsMock, +})); diff --git a/src/plugins/home/server/services/sample_data/sample_data_installer.test.ts b/src/plugins/home/server/services/sample_data/sample_data_installer.test.ts new file mode 100644 index 00000000000000..22079cbcafdb3d --- /dev/null +++ b/src/plugins/home/server/services/sample_data/sample_data_installer.test.ts @@ -0,0 +1,331 @@ +/* + * Copyright 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 { Readable } from 'stream'; +import { insertDataIntoIndexMock, findSampleObjectsMock } from './sample_data_installer.test.mocks'; +import type { SavedObjectsImportFailure } from 'kibana/server'; +import { + savedObjectsClientMock, + savedObjectsServiceMock, + elasticsearchServiceMock, + loggingSystemMock, +} from '../../../../../core/server/mocks'; +import type { SampleDatasetSchema } from './lib/sample_dataset_registry_types'; +import { SampleDataInstaller } from './sample_data_installer'; +import { SampleDataInstallError } from './errors'; + +const testDatasets: SampleDatasetSchema[] = [ + { + id: 'test_single_data_index', + name: 'Test with a single data index', + description: 'See name', + previewImagePath: 'previewImagePath', + darkPreviewImagePath: 'darkPreviewImagePath', + overviewDashboard: 'overviewDashboard', + defaultIndex: 'defaultIndex', + savedObjects: [ + { + id: 'some-dashboard', + type: 'dashboard', + attributes: { + hello: 'dolly', + }, + references: [], + }, + { + id: 'another-dashboard', + type: 'dashboard', + attributes: { + foo: 'bar', + }, + references: [], + }, + ], + dataIndices: [ + { + id: 'test_single_data_index', + dataPath: '/dataPath', + fields: { someField: { type: 'keyword' } }, + currentTimeMarker: '2018-01-09T00:00:00', + timeFields: ['@timestamp'], + preserveDayOfWeekTimeOfDay: true, + }, + ], + }, +]; + +describe('SampleDataInstaller', () => { + let esClient: ReturnType; + let soClient: ReturnType; + let soImporter: ReturnType; + let logger: ReturnType; + let installer: SampleDataInstaller; + + beforeEach(() => { + esClient = elasticsearchServiceMock.createScopedClusterClient(); + soClient = savedObjectsClientMock.create(); + soImporter = savedObjectsServiceMock.createImporter(); + logger = loggingSystemMock.createLogger(); + + installer = new SampleDataInstaller({ + esClient, + soClient, + soImporter, + logger, + sampleDatasets: testDatasets, + }); + + soImporter.import.mockResolvedValue({ + success: true, + successCount: 1, + errors: [], + warnings: [], + }); + + soClient.delete.mockResolvedValue({}); + + esClient.asCurrentUser.indices.getAlias.mockImplementation(() => { + throw new Error('alias not found'); + }); + + findSampleObjectsMock.mockResolvedValue([]); + }); + + afterEach(() => { + insertDataIntoIndexMock.mockReset(); + findSampleObjectsMock.mockReset(); + }); + + describe('#install', () => { + it('cleanups the data index before installing', async () => { + await installer.install('test_single_data_index'); + + expect(esClient.asCurrentUser.indices.delete).toHaveBeenCalledTimes(1); + expect(esClient.asCurrentUser.indices.delete).toHaveBeenCalledWith({ + index: 'kibana_sample_data_test_single_data_index', + }); + }); + + it('creates the data index', async () => { + await installer.install('test_single_data_index'); + + expect(esClient.asCurrentUser.indices.create).toHaveBeenCalledTimes(1); + expect(esClient.asCurrentUser.indices.create).toHaveBeenCalledWith({ + index: 'kibana_sample_data_test_single_data_index', + body: { + settings: { index: { number_of_shards: 1, auto_expand_replicas: '0-1' } }, + mappings: { properties: { someField: { type: 'keyword' } } }, + }, + }); + }); + + it('inserts the data into the index', async () => { + await installer.install('test_single_data_index'); + + expect(insertDataIntoIndexMock).toHaveBeenCalledTimes(1); + expect(insertDataIntoIndexMock).toHaveBeenCalledWith({ + index: 'kibana_sample_data_test_single_data_index', + nowReference: expect.any(String), + logger, + esClient, + dataIndexConfig: testDatasets[0].dataIndices[0], + }); + }); + + it('imports the saved objects', async () => { + await installer.install('test_single_data_index'); + + expect(soImporter.import).toHaveBeenCalledTimes(1); + expect(soImporter.import).toHaveBeenCalledWith({ + readStream: expect.any(Readable), + overwrite: true, + createNewCopies: false, + }); + }); + + it('throws a SampleDataInstallError with code 404 when the dataset is not found', async () => { + try { + await installer.install('unknown_data_set'); + expect('should have returned an error').toEqual('but it did not'); + } catch (e) { + expect(e).toBeInstanceOf(SampleDataInstallError); + expect((e as SampleDataInstallError).httpCode).toEqual(404); + } + }); + + it('does not throw when the index removal fails', async () => { + esClient.asCurrentUser.indices.delete.mockImplementation(() => { + throw new Error('cannot delete index'); + }); + + await expect(installer.install('test_single_data_index')).resolves.toBeDefined(); + }); + + it('throws a SampleDataInstallError when the index creation fails', async () => { + esClient.asCurrentUser.indices.create.mockImplementation(() => { + // eslint-disable-next-line no-throw-literal + throw { + message: 'Cannot create index', + status: 500, + }; + }); + + try { + await installer.install('test_single_data_index'); + expect('should have returned an error').toEqual('but it did not'); + } catch (e) { + expect(e).toBeInstanceOf(SampleDataInstallError); + expect((e as SampleDataInstallError).httpCode).toEqual(500); + } + }); + + it('throws a SampleDataInstallError if the savedObject import returns any error', async () => { + soImporter.import.mockResolvedValue({ + success: true, + successCount: 1, + errors: [{ type: 'type', id: 'id' } as SavedObjectsImportFailure], + warnings: [], + }); + + try { + await installer.install('test_single_data_index'); + expect('should have returned an error').toEqual('but it did not'); + } catch (e) { + expect(e).toBeInstanceOf(SampleDataInstallError); + expect(e.message).toContain('sample_data install errors while loading saved objects'); + expect((e as SampleDataInstallError).httpCode).toEqual(500); + } + }); + + describe('when the data index is using an alias', () => { + it('deletes the alias and the index', async () => { + const indexName = 'target_index'; + + esClient.asCurrentUser.indices.getAlias.mockResolvedValue( + elasticsearchServiceMock.createApiResponse({ + body: { + [indexName]: { + aliases: { + kibana_sample_data_test_single_data_index: {}, + }, + }, + }, + }) + ); + + await installer.install('test_single_data_index'); + + expect(esClient.asCurrentUser.indices.deleteAlias).toHaveBeenCalledTimes(1); + expect(esClient.asCurrentUser.indices.deleteAlias).toHaveBeenCalledWith({ + name: 'kibana_sample_data_test_single_data_index', + index: indexName, + }); + + expect(esClient.asCurrentUser.indices.delete).toHaveBeenCalledTimes(1); + expect(esClient.asCurrentUser.indices.delete).toHaveBeenCalledWith({ + index: indexName, + }); + }); + }); + }); + + describe('#uninstall', () => { + it('deletes the data index', async () => { + await installer.uninstall('test_single_data_index'); + + expect(esClient.asCurrentUser.indices.delete).toHaveBeenCalledTimes(1); + expect(esClient.asCurrentUser.indices.delete).toHaveBeenCalledWith({ + index: 'kibana_sample_data_test_single_data_index', + }); + }); + + it('deletes the saved objects', async () => { + findSampleObjectsMock.mockResolvedValue([ + { type: 'dashboard', id: 'foo', foundObjectId: 'foo' }, + { type: 'dashboard', id: 'hello', foundObjectId: 'dolly' }, + ]); + + await installer.uninstall('test_single_data_index'); + + expect(soClient.delete).toHaveBeenCalledTimes(2); + expect(soClient.delete).toHaveBeenCalledWith('dashboard', 'foo'); + expect(soClient.delete).toHaveBeenCalledWith('dashboard', 'dolly'); + }); + + it('throws a SampleDataInstallError with code 404 when the dataset is not found', async () => { + try { + await installer.uninstall('unknown_data_set'); + expect('should have returned an error').toEqual('but it did not'); + } catch (e) { + expect(e).toBeInstanceOf(SampleDataInstallError); + expect((e as SampleDataInstallError).httpCode).toEqual(404); + } + }); + + it('does not throw when the index removal fails', async () => { + esClient.asCurrentUser.indices.delete.mockImplementation(() => { + throw new Error('cannot delete index'); + }); + + await expect(installer.uninstall('test_single_data_index')).resolves.toBeDefined(); + }); + + it('throws a SampleDataInstallError if any SO deletion fails', async () => { + findSampleObjectsMock.mockResolvedValue([ + { type: 'dashboard', id: 'foo', foundObjectId: 'foo' }, + { type: 'dashboard', id: 'hello', foundObjectId: 'dolly' }, + ]); + + soClient.delete.mockImplementation(async (type: string, id: string) => { + if (id === 'dolly') { + throw new Error('could not delete dolly'); + } + return {}; + }); + + try { + await installer.uninstall('test_single_data_index'); + expect('should have returned an error').toEqual('but it did not'); + } catch (e) { + expect(e).toBeInstanceOf(SampleDataInstallError); + expect((e as SampleDataInstallError).httpCode).toEqual(500); + } + }); + + describe('when the data index is using an alias', () => { + it('deletes the alias and the index', async () => { + const indexName = 'target_index'; + + esClient.asCurrentUser.indices.getAlias.mockResolvedValue( + elasticsearchServiceMock.createApiResponse({ + body: { + [indexName]: { + aliases: { + kibana_sample_data_test_single_data_index: {}, + }, + }, + }, + }) + ); + + await installer.uninstall('test_single_data_index'); + + expect(esClient.asCurrentUser.indices.deleteAlias).toHaveBeenCalledTimes(1); + expect(esClient.asCurrentUser.indices.deleteAlias).toHaveBeenCalledWith({ + name: 'kibana_sample_data_test_single_data_index', + index: indexName, + }); + + expect(esClient.asCurrentUser.indices.delete).toHaveBeenCalledTimes(1); + expect(esClient.asCurrentUser.indices.delete).toHaveBeenCalledWith({ + index: indexName, + }); + }); + }); + }); +}); diff --git a/src/plugins/home/server/services/sample_data/sample_data_installer.ts b/src/plugins/home/server/services/sample_data/sample_data_installer.ts new file mode 100644 index 00000000000000..8e9315719bc166 --- /dev/null +++ b/src/plugins/home/server/services/sample_data/sample_data_installer.ts @@ -0,0 +1,205 @@ +/* + * Copyright 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 { Readable } from 'stream'; +import { isBoom } from '@hapi/boom'; +import type { + IScopedClusterClient, + ISavedObjectsImporter, + Logger, + SavedObjectsClientContract, +} from 'src/core/server'; +import type { SampleDatasetSchema, DataIndexSchema } from './lib/sample_dataset_registry_types'; +import { dateToIso8601IgnoringTime } from './lib/translate_timestamp'; +import { createIndexName } from './lib/create_index_name'; +import { insertDataIntoIndex } from './lib/insert_data_into_index'; +import { SampleDataInstallError } from './errors'; +import { findSampleObjects } from './lib/find_sample_objects'; + +export interface SampleDataInstallerOptions { + esClient: IScopedClusterClient; + soClient: SavedObjectsClientContract; + soImporter: ISavedObjectsImporter; + sampleDatasets: SampleDatasetSchema[]; + logger: Logger; +} + +export interface SampleDataInstallResult { + createdDocsPerIndex: Record; + createdSavedObjects: number; +} + +/** + * Utility class in charge of installing and uninstalling sample datasets + */ +export class SampleDataInstaller { + private readonly esClient: IScopedClusterClient; + private readonly soClient: SavedObjectsClientContract; + private readonly soImporter: ISavedObjectsImporter; + private readonly sampleDatasets: SampleDatasetSchema[]; + private readonly logger: Logger; + + constructor({ + esClient, + soImporter, + soClient, + sampleDatasets, + logger, + }: SampleDataInstallerOptions) { + this.esClient = esClient; + this.soClient = soClient; + this.soImporter = soImporter; + this.sampleDatasets = sampleDatasets; + this.logger = logger; + } + + async install( + datasetId: string, + installDate: Date = new Date() + ): Promise { + const sampleDataset = this.sampleDatasets.find(({ id }) => id === datasetId); + if (!sampleDataset) { + throw new SampleDataInstallError(`Sample dataset ${datasetId} not found`, 404); + } + + const nowReference = dateToIso8601IgnoringTime(installDate); + const createdDocsPerIndex: Record = {}; + + for (let i = 0; i < sampleDataset.dataIndices.length; i++) { + const dataIndex = sampleDataset.dataIndices[i]; + const indexName = createIndexName(sampleDataset.id, dataIndex.id); + // clean up any old installation of dataset + await this.uninstallDataIndex(sampleDataset, dataIndex); + await this.installDataIndex(sampleDataset, dataIndex); + + const injectedCount = await insertDataIntoIndex({ + index: indexName, + nowReference, + logger: this.logger, + esClient: this.esClient, + dataIndexConfig: dataIndex, + }); + createdDocsPerIndex[indexName] = injectedCount; + } + + const createdSavedObjects = await this.importSavedObjects(sampleDataset); + + return { + createdDocsPerIndex, + createdSavedObjects, + }; + } + + async uninstall(datasetId: string) { + const sampleDataset = this.sampleDatasets.find(({ id }) => id === datasetId); + if (!sampleDataset) { + throw new SampleDataInstallError(`Sample dataset ${datasetId} not found`, 404); + } + + for (let i = 0; i < sampleDataset.dataIndices.length; i++) { + const dataIndex = sampleDataset.dataIndices[i]; + await this.uninstallDataIndex(sampleDataset, dataIndex); + } + const deletedObjects = await this.deleteSavedObjects(sampleDataset); + + return { + deletedSavedObjects: deletedObjects, + }; + } + + private async uninstallDataIndex(dataset: SampleDatasetSchema, dataIndex: DataIndexSchema) { + let index = createIndexName(dataset.id, dataIndex.id); + + try { + // if the sample data was reindexed using UA, the index name is actually an alias pointing to the reindexed + // index. In that case, we need to get rid of the alias and to delete the underlying index + const { body: response } = await this.esClient.asCurrentUser.indices.getAlias({ + name: index, + }); + const aliasName = index; + index = Object.keys(response)[0]; + await this.esClient.asCurrentUser.indices.deleteAlias({ name: aliasName, index }); + } catch (err) { + // ignore errors from missing alias + } + + try { + await this.esClient.asCurrentUser.indices.delete({ + index, + }); + } catch (err) { + // ignore delete errors + } + } + + private async installDataIndex(dataset: SampleDatasetSchema, dataIndex: DataIndexSchema) { + const index = createIndexName(dataset.id, dataIndex.id); + try { + await this.esClient.asCurrentUser.indices.create({ + index, + body: { + settings: { index: { number_of_shards: 1, auto_expand_replicas: '0-1' } }, + mappings: { properties: dataIndex.fields }, + }, + }); + } catch (err) { + const errMsg = `Unable to create sample data index "${index}", error: ${err.message}`; + this.logger.warn(errMsg); + throw new SampleDataInstallError(errMsg, err.status); + } + } + + private async importSavedObjects(dataset: SampleDatasetSchema) { + const savedObjects = dataset.savedObjects.map(({ version, ...obj }) => obj); + const readStream = Readable.from(savedObjects); + + const { errors = [] } = await this.soImporter.import({ + readStream, + overwrite: true, + createNewCopies: false, + }); + if (errors.length > 0) { + const errMsg = `sample_data install errors while loading saved objects. Errors: ${JSON.stringify( + errors.map(({ type, id, error }) => ({ type, id, error })) // discard other fields + )}`; + this.logger.warn(errMsg); + throw new SampleDataInstallError(errMsg, 500); + } + return savedObjects.length; + } + + private async deleteSavedObjects(dataset: SampleDatasetSchema) { + const objects = dataset.savedObjects.map(({ type, id }) => ({ type, id })); + const findSampleObjectsResult = await findSampleObjects({ + client: this.soClient, + logger: this.logger, + objects, + }); + const objectsToDelete = findSampleObjectsResult.filter(({ foundObjectId }) => foundObjectId); + const deletePromises = objectsToDelete.map(({ type, foundObjectId }) => + this.soClient.delete(type, foundObjectId!).catch((err) => { + // if the object doesn't exist, ignore the error and proceed + if (isBoom(err) && err.output.statusCode === 404) { + return; + } + throw err; + }) + ); + try { + await Promise.all(deletePromises); + } catch (err) { + throw new SampleDataInstallError( + `Unable to delete sample dataset saved objects, error: ${ + err.body?.error?.type ?? err.message + }`, + err.body?.status ?? 500 + ); + } + return objectsToDelete.length; + } +} diff --git a/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts b/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts new file mode 100644 index 00000000000000..29702c33568655 --- /dev/null +++ b/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { DependencyManager } from './dependency_manager'; + +describe('DependencyManager', () => { + it('orderDependencies. Should sort topology by dependencies', () => { + const graph = { + N: [], + R: [], + A: ['B', 'C'], + B: ['D'], + C: ['F', 'B'], + F: ['E'], + E: ['D'], + D: ['L'], + }; + const sortedTopology = ['N', 'R', 'L', 'D', 'B', 'E', 'F', 'C', 'A']; + expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology); + }); + + it('orderDependencies. Should return base topology if no depended vertices', () => { + const graph = { + N: [], + R: [], + D: undefined, + }; + const sortedTopology = ['N', 'R', 'D']; + expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology); + }); + + it('orderDependencies. Should detect circular dependencies and throw error with path', () => { + const graph = { + N: ['R'], + R: ['A'], + A: ['B'], + B: ['C'], + C: ['D'], + D: ['E'], + E: ['F'], + F: ['L'], + L: ['G'], + G: ['N'], + }; + const circularPath = ['N', 'R', 'A', 'B', 'C', 'D', 'E', 'F', 'L', 'G', 'N'].join(' -> '); + const errorMessage = `Circular dependency detected while setting up services: ${circularPath}`; + + expect(() => DependencyManager.orderDependencies(graph)).toThrowError(errorMessage); + }); +}); diff --git a/src/plugins/presentation_util/public/services/create/dependency_manager.ts b/src/plugins/presentation_util/public/services/create/dependency_manager.ts new file mode 100644 index 00000000000000..de30b180607fe4 --- /dev/null +++ b/src/plugins/presentation_util/public/services/create/dependency_manager.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +type GraphVertex = string | number | symbol; +type Graph = Record; +type BreadCrumbs = Record; + +interface CycleDetectionResult { + hasCycle: boolean; + path: T[]; +} + +export class DependencyManager { + static orderDependencies(graph: Graph) { + const cycleInfo = DependencyManager.getSortedDependencies(graph); + if (cycleInfo.hasCycle) { + const error = DependencyManager.getCyclePathError(cycleInfo.path); + DependencyManager.throwCyclicPathError(error); + } + + return cycleInfo.path; + } + + /** + * DFS algorithm for checking if graph is a DAG (Directed Acyclic Graph) + * and sorting topogy (dependencies) if graph is DAG. + * @param {Graph} graph - graph of dependencies. + */ + private static getSortedDependencies( + graph: Graph = {} as Graph + ): CycleDetectionResult { + const sortedVertices: Set = new Set(); + const vertices = Object.keys(graph) as T[]; + return vertices.reduce>((cycleInfo, srcVertex) => { + if (cycleInfo.hasCycle) { + return cycleInfo; + } + + return DependencyManager.sortVerticesFrom(srcVertex, graph, sortedVertices, {}, {}); + }, DependencyManager.createCycleInfo()); + } + + /** + * Modified DFS algorithm for topological sort. + * @param {T extends GraphVertex} srcVertex - a source vertex - the start point of dependencies ordering. + * @param {Graph} graph - graph of dependencies, represented in the adjacency list form. + * @param {Set} sortedVertices - ordered dependencies path from the free to the dependent vertex. + * @param {BreadCrumbs} visited - record of visited vertices. + * @param {BreadCrumbs} inpath - record of vertices, which was met in the path. Is used for detecting cycles. + */ + private static sortVerticesFrom( + srcVertex: T, + graph: Graph, + sortedVertices: Set, + visited: BreadCrumbs = {}, + inpath: BreadCrumbs = {} + ): CycleDetectionResult { + visited[srcVertex] = true; + inpath[srcVertex] = true; + const cycleInfo = graph[srcVertex]?.reduce | undefined>( + (info, vertex) => { + if (inpath[vertex]) { + const path = (Object.keys(inpath) as T[]).filter( + (visitedVertex) => inpath[visitedVertex] + ); + return DependencyManager.createCycleInfo([...path, vertex], true); + } else if (!visited[vertex]) { + return DependencyManager.sortVerticesFrom(vertex, graph, sortedVertices, visited, inpath); + } + return info; + }, + undefined + ); + + inpath[srcVertex] = false; + + if (!sortedVertices.has(srcVertex)) { + sortedVertices.add(srcVertex); + } + + return cycleInfo ?? DependencyManager.createCycleInfo([...sortedVertices]); + } + + private static createCycleInfo( + path: T[] = [], + hasCycle: boolean = false + ): CycleDetectionResult { + return { hasCycle, path }; + } + + private static getCyclePathError( + cyclePath: CycleDetectionResult['path'] + ) { + const cycleString = cyclePath.join(' -> '); + return `Circular dependency detected while setting up services: ${cycleString}`; + } + + private static throwCyclicPathError(error: string) { + throw new Error(error); + } +} diff --git a/src/plugins/presentation_util/public/services/create/factory.ts b/src/plugins/presentation_util/public/services/create/factory.ts index ddc2e5845b0375..49ed5ef8aaf8d2 100644 --- a/src/plugins/presentation_util/public/services/create/factory.ts +++ b/src/plugins/presentation_util/public/services/create/factory.ts @@ -16,7 +16,10 @@ import { CoreStart, AppUpdater, PluginInitializerContext } from 'src/core/public * The `StartParameters` generic determines what parameters are expected to * create the service. */ -export type PluginServiceFactory = (params: Parameters) => Service; +export type PluginServiceFactory = ( + params: Parameters, + requiredServices: RequiredServices +) => Service; /** * Parameters necessary to create a Kibana-based service, (e.g. during Plugin @@ -38,6 +41,7 @@ export interface KibanaPluginServiceParams { * The `Setup` generic refers to the specific Plugin `TPluginsSetup`. * The `Start` generic refers to the specific Plugin `TPluginsStart`. */ -export type KibanaPluginServiceFactory = ( - params: KibanaPluginServiceParams +export type KibanaPluginServiceFactory = ( + params: KibanaPluginServiceParams, + requiredServices: RequiredServices ) => Service; diff --git a/src/plugins/presentation_util/public/services/create/provider.tsx b/src/plugins/presentation_util/public/services/create/provider.tsx index 06590bcfbb3d0c..3271dc52fd9d08 100644 --- a/src/plugins/presentation_util/public/services/create/provider.tsx +++ b/src/plugins/presentation_util/public/services/create/provider.tsx @@ -17,7 +17,25 @@ import { PluginServiceFactory } from './factory'; * start the service. */ export type PluginServiceProviders = { - [K in keyof Services]: PluginServiceProvider; + [K in keyof Services]: PluginServiceProvider< + Services[K], + StartParameters, + Services, + Array + >; +}; + +type ElementOfArray = ArrayType extends Array< + infer ElementType +> + ? ElementType + : never; + +export type PluginServiceRequiredServices< + RequiredServices extends Array, + AvailableServices +> = { + [K in ElementOfArray]: AvailableServices[K]; }; /** @@ -27,16 +45,34 @@ export type PluginServiceProviders = { * The `StartParameters` generic determines what parameters are expected to * start the service. */ -export class PluginServiceProvider { - private factory: PluginServiceFactory; +export class PluginServiceProvider< + Service extends {}, + StartParameters = {}, + Services = {}, + RequiredServices extends Array = [] +> { + private factory: PluginServiceFactory< + Service, + StartParameters, + PluginServiceRequiredServices + >; + private _requiredServices?: RequiredServices; private context = createContext(null); private pluginService: Service | null = null; public readonly Provider: React.FC = ({ children }) => { return {children}; }; - constructor(factory: PluginServiceFactory) { + constructor( + factory: PluginServiceFactory< + Service, + StartParameters, + PluginServiceRequiredServices + >, + requiredServices?: RequiredServices + ) { this.factory = factory; + this._requiredServices = requiredServices; this.context.displayName = 'PluginServiceContext'; } @@ -55,8 +91,11 @@ export class PluginServiceProvider { * * @param params Parameters used to start the service. */ - start(params: StartParameters) { - this.pluginService = this.factory(params); + start( + params: StartParameters, + requiredServices: PluginServiceRequiredServices + ) { + this.pluginService = this.factory(params, requiredServices); } /** @@ -80,4 +119,8 @@ export class PluginServiceProvider { stop() { this.pluginService = null; } + + public get requiredServices() { + return this._requiredServices ?? []; + } } diff --git a/src/plugins/presentation_util/public/services/create/providers_mediator.ts b/src/plugins/presentation_util/public/services/create/providers_mediator.ts new file mode 100644 index 00000000000000..dd5937149850cf --- /dev/null +++ b/src/plugins/presentation_util/public/services/create/providers_mediator.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { DependencyManager } from './dependency_manager'; +import { PluginServiceProviders, PluginServiceRequiredServices } from './provider'; + +export class PluginServiceProvidersMediator { + constructor(private readonly providers: PluginServiceProviders) {} + + start(params: StartParameters) { + this.getOrderedDependencies().forEach((service) => { + this.providers[service].start(params, this.getServiceDependencies(service)); + }); + } + + stop() { + this.getOrderedDependencies().forEach((service) => this.providers[service].stop()); + } + + private getOrderedDependencies() { + const dependenciesGraph = this.getGraphOfDependencies(); + return DependencyManager.orderDependencies(dependenciesGraph); + } + + private getGraphOfDependencies() { + return this.getProvidersNames().reduce>>( + (graph, vertex) => ({ ...graph, [vertex]: this.providers[vertex].requiredServices ?? [] }), + {} as Record> + ); + } + + private getProvidersNames() { + return Object.keys(this.providers) as Array; + } + + private getServiceDependencies(service: keyof Services) { + const requiredServices = this.providers[service].requiredServices ?? []; + return this.getServicesByDeps(requiredServices); + } + + private getServicesByDeps(deps: Array) { + return deps.reduce, Services>>( + (services, dependency) => ({ + ...services, + [dependency]: this.providers[dependency].getService(), + }), + {} as PluginServiceRequiredServices, Services> + ); + } +} diff --git a/src/plugins/presentation_util/public/services/create/registry.tsx b/src/plugins/presentation_util/public/services/create/registry.tsx index e8f85666bcac41..8369815a042afd 100644 --- a/src/plugins/presentation_util/public/services/create/registry.tsx +++ b/src/plugins/presentation_util/public/services/create/registry.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { PluginServiceProvider, PluginServiceProviders } from './provider'; +import { PluginServiceProvidersMediator } from './providers_mediator'; /** * A `PluginServiceRegistry` maintains a set of service providers which can be collectively @@ -19,10 +20,12 @@ import { PluginServiceProvider, PluginServiceProviders } from './provider'; */ export class PluginServiceRegistry { private providers: PluginServiceProviders; + private providersMediator: PluginServiceProvidersMediator; private _isStarted = false; constructor(providers: PluginServiceProviders) { this.providers = providers; + this.providersMediator = new PluginServiceProvidersMediator(providers); } /** @@ -69,8 +72,7 @@ export class PluginServiceRegistry { * @param params Parameters used to start the registry. */ start(params: StartParameters) { - const providerNames = Object.keys(this.providers) as Array; - providerNames.forEach((providerName) => this.providers[providerName].start(params)); + this.providersMediator.start(params); this._isStarted = true; return this; } @@ -79,8 +81,7 @@ export class PluginServiceRegistry { * Stop the registry. */ stop() { - const providerNames = Object.keys(this.providers) as Array; - providerNames.forEach((providerName) => this.providers[providerName].stop()); + this.providersMediator.stop(); this._isStarted = false; return this; } diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts index fd8a5fd7541a68..684d29caeb312b 100644 --- a/src/plugins/share/public/plugin.ts +++ b/src/plugins/share/public/plugin.ts @@ -86,7 +86,7 @@ export class SharePlugin implements Plugin { const { basePath } = http; this.url = new UrlService({ - baseUrl: basePath.publicBaseUrl || basePath.serverBasePath, + baseUrl: basePath.get(), version: this.initializerContext.env.packageInfo.version, navigate: async ({ app, path, state }, { replace = false } = {}) => { const [start] = await core.getStartServices(); diff --git a/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx b/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx index cb2ba498a06647..063f60b82927e3 100644 --- a/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx +++ b/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx @@ -18,6 +18,7 @@ import { LayoutDirection, } from '@elastic/charts'; import { EuiTitle } from '@elastic/eui'; +import { RangeFilterParams } from '@kbn/es-query'; import { useKibana } from '../../../../kibana_react/public'; import { useActiveCursor } from '../../../../charts/public'; @@ -38,7 +39,6 @@ import { getCharts } from '../helpers/plugin_services'; import type { Sheet } from '../helpers/timelion_request_handler'; import type { IInterpreterRenderHandlers } from '../../../../expressions'; import type { TimelionVisDependencies } from '../plugin'; -import type { RangeFilterParams } from '../../../../data/public'; import type { Series } from '../helpers/timelion_request_handler'; import './timelion_vis.scss'; diff --git a/src/plugins/vis_types/timelion/public/legacy/timelion_vis_component.tsx b/src/plugins/vis_types/timelion/public/legacy/timelion_vis_component.tsx index 136544ac068a3f..edb250dfe1200c 100644 --- a/src/plugins/vis_types/timelion/public/legacy/timelion_vis_component.tsx +++ b/src/plugins/vis_types/timelion/public/legacy/timelion_vis_component.tsx @@ -11,6 +11,7 @@ import $ from 'jquery'; import moment from 'moment-timezone'; import { debounce, compact, get, each, cloneDeep, last, map } from 'lodash'; import { useResizeObserver } from '@elastic/eui'; +import { RangeFilterParams } from '@kbn/es-query'; import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { useKibana } from '../../../../kibana_react/public'; @@ -31,7 +32,6 @@ import { tickFormatters } from './tick_formatters'; import { generateTicksProvider } from '../helpers/tick_generator'; import type { TimelionVisDependencies } from '../plugin'; -import type { RangeFilterParams } from '../../../../data/common'; import './timelion_vis.scss'; diff --git a/src/plugins/vis_types/timelion/public/timelion_vis_renderer.tsx b/src/plugins/vis_types/timelion/public/timelion_vis_renderer.tsx index 3c9fb1b1b268fe..6b799d3e349468 100644 --- a/src/plugins/vis_types/timelion/public/timelion_vis_renderer.tsx +++ b/src/plugins/vis_types/timelion/public/timelion_vis_renderer.tsx @@ -10,12 +10,12 @@ import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { ExpressionRenderDefinition } from 'src/plugins/expressions'; +import { RangeFilterParams } from '@kbn/es-query'; import { KibanaContextProvider, KibanaThemeProvider } from '../../../kibana_react/public'; import { VisualizationContainer } from '../../../visualizations/public'; import { TimelionVisDependencies } from './plugin'; import { TimelionRenderValue } from './timelion_vis_fn'; import { UI_SETTINGS } from '../common/constants'; -import { RangeFilterParams } from '../../../data/public'; const LazyTimelionVisComponent = lazy(() => import('./async_services').then(({ TimelionVisComponent }) => ({ default: TimelionVisComponent })) diff --git a/src/plugins/vis_types/timeseries/public/application/visualizations/views/gauge.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/gauge.js index 0e4322a7ba82c7..0976d8f9cf47e7 100644 --- a/src/plugins/vis_types/timeseries/public/application/visualizations/views/gauge.js +++ b/src/plugins/vis_types/timeseries/public/application/visualizations/views/gauge.js @@ -9,6 +9,7 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { EuiResizeObserver } from '@elastic/eui'; import classNames from 'classnames'; import { isBackgroundInverted, isBackgroundDark } from '../../lib/set_is_reversed'; import { getLastValue } from '../../../../common/last_value_utils'; @@ -29,19 +30,12 @@ export class Gauge extends Component { }; this.handleResize = this.handleResize.bind(this); - } - - UNSAFE_componentWillMount() { - const check = () => { - this.timeout = setTimeout(() => { - const newState = calculateCoordinates(this.inner, this.resize, this.state); - if (newState && this.state && !_.isEqual(newState, this.state)) { - this.handleResize(); - } - check(); - }, 500); - }; - check(); + this.checkResizeThrottled = _.throttle(() => { + const newState = calculateCoordinates(this.inner, this.resize, this.state); + if (newState && this.state && !_.isEqual(newState, this.state)) { + this.handleResize(); + } + }, 200); } componentWillUnmount() { @@ -155,16 +149,20 @@ export class Gauge extends Component { }); return ( -
-
(this.resize = el)} - className={`tvbVisGauge__resize`} - data-test-subj="tvbVisGaugeContainer" - > - {metrics} - -
-
+ + {(resizeRef) => ( +
+
(this.resize = el)} + className={`tvbVisGauge__resize`} + data-test-subj="tvbVisGaugeContainer" + > + {metrics} + +
+
+ )} +
); } } diff --git a/src/plugins/vis_types/timeseries/public/application/visualizations/views/gauge_vis.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/gauge_vis.js index 165f5080af93a4..0cc96ba3c74da3 100644 --- a/src/plugins/vis_types/timeseries/public/application/visualizations/views/gauge_vis.js +++ b/src/plugins/vis_types/timeseries/public/application/visualizations/views/gauge_vis.js @@ -9,6 +9,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import _ from 'lodash'; +import { EuiResizeObserver } from '@elastic/eui'; import reactcss from 'reactcss'; import { calculateCoordinates } from '../lib/calculate_coordinates'; import { COLORS } from '../constants/chart'; @@ -25,19 +26,12 @@ export class GaugeVis extends Component { translateY: 1, }; this.handleResize = this.handleResize.bind(this); - } - - UNSAFE_componentWillMount() { - const check = () => { - this.timeout = setTimeout(() => { - const newState = calculateCoordinates(this.inner, this.resize, this.state); - if (newState && this.state && !_.isEqual(newState, this.state)) { - this.handleResize(); - } - check(); - }, 500); - }; - check(); + this.checkResizeThrottled = _.throttle(() => { + const newState = calculateCoordinates(this.inner, this.resize, this.state); + if (newState && this.state && !_.isEqual(newState, this.state)) { + this.handleResize(); + } + }, 200); } componentWillUnmount() { @@ -148,11 +142,21 @@ export class GaugeVis extends Component { ); } return ( -
(this.resize = el)} style={styles.resize}> -
(this.inner = el)}> - {svg} -
-
+ + {(resizeRef) => ( +
{ + this.resize = el; + resizeRef(el); + }} + style={styles.resize} + > +
(this.inner = el)}> + {svg} +
+
+ )} +
); } } diff --git a/src/plugins/vis_types/timeseries/public/application/visualizations/views/metric.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/metric.js index 0ceb2daa831bec..6c9cc942b362f3 100644 --- a/src/plugins/vis_types/timeseries/public/application/visualizations/views/metric.js +++ b/src/plugins/vis_types/timeseries/public/application/visualizations/views/metric.js @@ -9,6 +9,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import _ from 'lodash'; +import { EuiResizeObserver } from '@elastic/eui'; import reactcss from 'reactcss'; import { getLastValue } from '../../../../common/last_value_utils'; @@ -25,19 +26,12 @@ export class Metric extends Component { translateY: 1, }; this.handleResize = this.handleResize.bind(this); - } - - UNSAFE_componentWillMount() { - const check = () => { - this.timeout = setTimeout(() => { - const newState = calculateCoordinates(this.inner, this.resize, this.state); - if (newState && this.state && !_.isEqual(newState, this.state)) { - this.handleResize(); - } - check(); - }, 500); - }; - check(); + this.checkResizeThrottled = _.throttle(() => { + const newState = calculateCoordinates(this.inner, this.resize, this.state); + if (newState && this.state && !_.isEqual(newState, this.state)) { + this.handleResize(); + } + }, 200); } componentWillUnmount() { @@ -123,25 +117,33 @@ export class Metric extends Component { className += ' tvbVisMetric--reversed'; } return ( -
-
(this.resize = el)} className="tvbVisMetric__resize"> -
(this.inner = el)} className="tvbVisMetric__inner" style={styles.inner}> -
- {primaryLabel} + + {(resizeRef) => ( +
+
(this.resize = el)} className="tvbVisMetric__resize">
(this.inner = el)} + className="tvbVisMetric__inner" + style={styles.inner} > - {/* eslint-disable-next-line react/no-danger */} - +
+ {primaryLabel} +
+ {/* eslint-disable-next-line react/no-danger */} + +
+
+ {secondarySnippet} + {additionalLabel}
- {secondarySnippet} - {additionalLabel}
-
-
+ )} + ); } } diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts index b84aee949471b5..6ab50858fe98b4 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts @@ -7,8 +7,8 @@ */ import type { IUiSettingsClient } from 'kibana/server'; +import { EsQueryConfig } from '@kbn/es-query'; import type { FetchedIndexPattern, Panel } from '../../../../../common/types'; -import type { EsQueryConfig } from '../../../../../../../data/common'; import type { SearchCapabilities } from '../../../search_strategies'; import type { VisTypeTimeseriesVisDataRequest } from '../../../../types'; diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx index b52a3f3a6040e1..9c328a175c10b6 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx @@ -12,14 +12,13 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { render } from 'react-dom'; import { EuiLoadingChart } from '@elastic/eui'; -import { Filter } from '@kbn/es-query'; +import { Filter, onlyDisabledFiltersChanged } from '@kbn/es-query'; import { KibanaThemeProvider } from '../../../kibana_react/public'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { IndexPattern, TimeRange, Query, - esFilters, TimefilterContract, } from '../../../../plugins/data/public'; import { @@ -239,7 +238,7 @@ export class VisualizeEmbeddable } // Check if filters has changed - if (!esFilters.onlyDisabledFiltersChanged(this.input.filters, this.filters)) { + if (!onlyDisabledFiltersChanged(this.input.filters, this.filters)) { this.filters = this.input.filters; dirty = true; } diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 59d7c25c6f41d5..eae4f704b7c3cc 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -57,7 +57,6 @@ import { import { VisualizeLocatorDefinition } from '../common/locator'; import { showNewVisModal } from './wizard'; import { createVisEditorsRegistry, VisEditorsRegistry } from './vis_editors_registry'; -import { esFilters } from '../../../plugins/data/public'; import { FeatureCatalogueCategory } from '../../home/public'; import type { VisualizeServices } from './visualize_app/types'; @@ -189,7 +188,7 @@ export class VisualizationsPlugin ), map(({ state }) => ({ ...state, - filters: state.filters?.filter(esFilters.isFilterPinned), + filters: data.query.filterManager.getGlobalFilters(), })) ), }, diff --git a/src/plugins/visualizations/public/visualize_app/utils/get_visualize_list_item_link.test.ts b/src/plugins/visualizations/public/visualize_app/utils/get_visualize_list_item_link.test.ts index e138acf2e9e859..7fe571b25f98cb 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/get_visualize_list_item_link.test.ts +++ b/src/plugins/visualizations/public/visualize_app/utils/get_visualize_list_item_link.test.ts @@ -9,8 +9,8 @@ import { getVisualizeListItemLink } from './get_visualize_list_item_link'; import { ApplicationStart } from 'kibana/public'; import { createHashHistory } from 'history'; +import { FilterStateStore } from '@kbn/es-query'; import { createKbnUrlStateStorage } from '../../../../kibana_utils/public'; -import { esFilters } from '../../../../data/public'; import { GLOBAL_STATE_STORAGE_KEY } from '../../../common/constants'; jest.mock('../../services', () => { @@ -104,7 +104,7 @@ describe('listing item link is correct for each app', () => { }, query: { query: 'q1' }, $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, + store: FilterStateStore.GLOBAL_STATE, }, }, ]; diff --git a/src/plugins/visualizations/public/visualize_app/utils/use/use_visualize_app_state.tsx b/src/plugins/visualizations/public/visualize_app/utils/use/use_visualize_app_state.tsx index 661717f99ed888..c6e8f9efdd0358 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/use/use_visualize_app_state.tsx +++ b/src/plugins/visualizations/public/visualize_app/utils/use/use_visualize_app_state.tsx @@ -11,6 +11,7 @@ import { cloneDeep, isEqual } from 'lodash'; import { map } from 'rxjs/operators'; import { EventEmitter } from 'events'; import { i18n } from '@kbn/i18n'; +import { FilterStateStore } from '@kbn/es-query'; import { KibanaThemeProvider, @@ -18,7 +19,7 @@ import { toMountPoint, } from '../../../../../kibana_react/public'; import { migrateLegacyQuery } from '../migrate_legacy_query'; -import { esFilters, connectToQueryState } from '../../../../../data/public'; +import { connectToQueryState } from '../../../../../data/public'; import { VisualizeServices, VisualizeAppStateContainer, @@ -87,7 +88,7 @@ export const useVisualizeAppState = ( ), }, { - filters: esFilters.FilterStateStore.APP_STATE, + filters: FilterStateStore.APP_STATE, query: true, } ); diff --git a/test/api_integration/apis/home/sample_data.ts b/test/api_integration/apis/home/sample_data.ts index 1a324ef844e2e7..6636a490118b44 100644 --- a/test/api_integration/apis/home/sample_data.ts +++ b/test/api_integration/apis/home/sample_data.ts @@ -21,8 +21,7 @@ export default function ({ getService }: FtrProviderContext) { const FLIGHTS_CANVAS_APPLINK_PATH = '/app/canvas#/workpad/workpad-a474e74b-aedc-47c3-894a-db77e62c41e0'; // includes default ID of the flights canvas applink path - // Failing: See https://github.com/elastic/kibana/issues/121051 - describe.skip('sample data apis', () => { + describe('sample data apis', () => { before(async () => { await esArchiver.emptyKibanaIndex(); }); @@ -63,22 +62,23 @@ export default function ({ getService }: FtrProviderContext) { }); }); - it('should load elasticsearch index containing sample data with dates relative to current time', async () => { - const resp = await es.search<{ timestamp: string }>({ - index: 'kibana_sample_data_flights', - body: { - sort: [{ timestamp: { order: 'desc' } }], - }, - }); + // Failing: See https://github.com/elastic/kibana/issues/121051 + describe.skip('dates', () => { + it('should load elasticsearch index containing sample data with dates relative to current time', async () => { + const resp = await es.search<{ timestamp: string }>({ + index: 'kibana_sample_data_flights', + body: { + sort: [{ timestamp: { order: 'desc' } }], + }, + }); - const doc = resp.hits.hits[0]; - const docMilliseconds = Date.parse(doc._source!.timestamp); - const nowMilliseconds = Date.now(); - const delta = Math.abs(nowMilliseconds - docMilliseconds); - expect(delta).to.be.lessThan(MILLISECOND_IN_WEEK * 5); - }); + const doc = resp.hits.hits[0]; + const docMilliseconds = Date.parse(doc._source!.timestamp); + const nowMilliseconds = Date.now(); + const delta = Math.abs(nowMilliseconds - docMilliseconds); + expect(delta).to.be.lessThan(MILLISECOND_IN_WEEK * 5); + }); - describe('parameters', () => { it('should load elasticsearch index containing sample data with dates relative to now parameter', async () => { const nowString = `2000-01-01T00:00:00`; await supertest.post(`${apiPath}/flights?now=${nowString}`).set('kbn-xsrf', 'kibana'); diff --git a/test/functional/apps/discover/_context_encoded_url_param.ts b/test/functional/apps/discover/_context_encoded_url_param.ts new file mode 100644 index 00000000000000..83ac63afd915fd --- /dev/null +++ b/test/functional/apps/discover/_context_encoded_url_param.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const dataGrid = getService('dataGrid'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'settings', 'header']); + const testSubjects = getService('testSubjects'); + const es = getService('es'); + + describe('context encoded id param', () => { + before(async function () { + await PageObjects.common.navigateToApp('settings'); + await es.transport.request({ + path: '/includes-plus-symbol-doc-id/_doc/1+1=2', + method: 'PUT', + body: { + username: 'Dmitry', + '@timestamp': '2015-09-21T09:30:23', + }, + }); + await PageObjects.settings.createIndexPattern('includes-plus-symbol-doc-id'); + + await kibanaServer.uiSettings.update({ 'doc_table:legacy': false }); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await PageObjects.common.navigateToApp('discover'); + }); + + it('should navigate to context page correctly', async () => { + await PageObjects.discover.selectIndexPattern('includes-plus-symbol-doc-id'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + // navigate to the context view + await dataGrid.clickRowToggle({ rowIndex: 0 }); + const [, surroundingActionEl] = await dataGrid.getRowActions({ + isAnchorRow: false, + rowIndex: 0, + }); + await surroundingActionEl.click(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + const headerElement = await testSubjects.find('contextDocumentSurroundingHeader'); + + expect(await headerElement.getVisibleText()).to.be('Documents surrounding #1+1=2'); + }); + }); +} diff --git a/test/functional/apps/discover/_runtime_fields_editor.ts b/test/functional/apps/discover/_runtime_fields_editor.ts index 4757807cb7ac1e..2e21b2e1f8ec6c 100644 --- a/test/functional/apps/discover/_runtime_fields_editor.ts +++ b/test/functional/apps/discover/_runtime_fields_editor.ts @@ -31,8 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await fieldEditor.save(); }; - // Failing: https://github.com/elastic/kibana/issues/111922 - describe.skip('discover integration with runtime fields editor', function describeIndexTests() { + describe('discover integration with runtime fields editor', function describeIndexTests() { before(async function () { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); @@ -63,7 +62,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('allows creation of a new field', async function () { await createRuntimeField('runtimefield'); await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.waitFor('fieldNames to include runtimefield', async () => { + await retry.waitForWithTimeout('fieldNames to include runtimefield', 5000, async () => { const fieldNames = await PageObjects.discover.getAllFieldNames(); return fieldNames.includes('runtimefield'); }); @@ -76,7 +75,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await fieldEditor.confirmSave(); await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.waitFor('fieldNames to include edits', async () => { + await retry.waitForWithTimeout('fieldNames to include edits', 5000, async () => { const fieldNames = await PageObjects.discover.getAllFieldNames(); return fieldNames.includes('runtimefield edited'); }); @@ -105,7 +104,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.removeField('delete'); await fieldEditor.confirmDelete(); await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.waitFor('fieldNames to include edits', async () => { + await retry.waitForWithTimeout('fieldNames to include edits', 5000, async () => { const fieldNames = await PageObjects.discover.getAllFieldNames(); return !fieldNames.includes('delete'); }); @@ -127,16 +126,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await rowActions[idxToClick].click(); }); - await retry.waitFor('doc viewer is displayed with runtime field', async () => { - const hasDocHit = await testSubjects.exists('doc-hit'); - if (!hasDocHit) { - // Maybe loading has not completed - throw new Error('test subject doc-hit is not yet displayed'); + await retry.waitForWithTimeout( + 'doc viewer is displayed with runtime field', + 5000, + async () => { + const hasDocHit = await testSubjects.exists('doc-hit'); + if (!hasDocHit) { + // Maybe loading has not completed + throw new Error('test subject doc-hit is not yet displayed'); + } + const runtimeFieldsRow = await testSubjects.exists( + 'tableDocViewRow-discover runtimefield' + ); + + return hasDocHit && runtimeFieldsRow; } - const runtimeFieldsRow = await testSubjects.exists('tableDocViewRow-discover runtimefield'); - - return hasDocHit && runtimeFieldsRow; - }); + ); }); }); } diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index 13658215e9e592..1241b0e892e9c2 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -53,5 +53,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_date_nested')); loadTestFile(require.resolve('./_search_on_page_load')); loadTestFile(require.resolve('./_chart_hidden')); + loadTestFile(require.resolve('./_context_encoded_url_param')); }); } diff --git a/test/functional/services/lib/compare_pngs.ts b/test/functional/services/lib/compare_pngs.ts index 521781c5a6d2bb..5fb0c4d6ad1dc6 100644 --- a/test/functional/services/lib/compare_pngs.ts +++ b/test/functional/services/lib/compare_pngs.ts @@ -9,6 +9,8 @@ import { parse, join } from 'path'; import Jimp from 'jimp'; import { ToolingLog } from '@kbn/dev-utils'; +import { promises as fs } from 'fs'; +import path from 'path'; interface PngDescriptor { path: string; @@ -102,3 +104,53 @@ export async function comparePngs( } return percent; } + +export async function checkIfPngsMatch( + actualpngPath: string, + baselinepngPath: string, + screenshotsDirectory: string, + log: any +) { + log.debug(`checkIfpngsMatch: ${actualpngPath} vs ${baselinepngPath}`); + // Copy the pngs into the screenshot session directory, as that's where the generated pngs will automatically be + // stored. + const sessionDirectoryPath = path.resolve(screenshotsDirectory, 'session'); + const failureDirectoryPath = path.resolve(screenshotsDirectory, 'failure'); + + await fs.mkdir(sessionDirectoryPath, { recursive: true }); + await fs.mkdir(failureDirectoryPath, { recursive: true }); + + const actualpngFileName = path.basename(actualpngPath, '.png'); + const baselinepngFileName = path.basename(baselinepngPath, '.png'); + + const baselineCopyPath = path.resolve( + sessionDirectoryPath, + `${baselinepngFileName}_baseline.png` + ); + const actualCopyPath = path.resolve(sessionDirectoryPath, `${actualpngFileName}_actual.png`); + + // Don't cause a test failure if the baseline snapshot doesn't exist - we don't have all OS's covered and we + // don't want to start causing failures for other devs working on OS's which are lacking snapshots. We have + // mac and linux covered which is better than nothing for now. + try { + log.debug(`writeFile: ${baselineCopyPath}`); + await fs.writeFile(baselineCopyPath, await fs.readFile(baselinepngPath)); + } catch (error) { + throw new Error(`No baseline png found at ${baselinepngPath}`); + } + log.debug(`writeFile: ${actualCopyPath}`); + await fs.writeFile(actualCopyPath, await fs.readFile(actualpngPath)); + + let diffTotal = 0; + + const diffPngPath = path.resolve(failureDirectoryPath, `${baselinepngFileName}-${1}.png`); + diffTotal += await comparePngs( + actualCopyPath, + baselineCopyPath, + diffPngPath, + sessionDirectoryPath, + log + ); + + return diffTotal; +} diff --git a/test/plugin_functional/test_suites/saved_objects_management/hidden_types.ts b/test/plugin_functional/test_suites/saved_objects_management/hidden_types.ts index 8e7adb504ebee3..439ece04e615f2 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/hidden_types.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/hidden_types.ts @@ -21,7 +21,8 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); - describe('saved objects management with hidden types', () => { + // Failing: See https://github.com/elastic/kibana/issues/116059 + describe.skip('saved objects management with hidden types', () => { before(async () => { await esArchiver.load( 'test/functional/fixtures/es_archiver/saved_objects_management/hidden_types' diff --git a/x-pack/examples/embedded_lens_example/public/app.tsx b/x-pack/examples/embedded_lens_example/public/app.tsx index f1b683f2430f7a..510d9469c78780 100644 --- a/x-pack/examples/embedded_lens_example/public/app.tsx +++ b/x-pack/examples/embedded_lens_example/public/app.tsx @@ -17,37 +17,32 @@ import { EuiPageHeader, EuiPageHeaderSection, EuiTitle, - EuiCallOut, } from '@elastic/eui'; -import { IndexPattern } from 'src/plugins/data/public'; -import { CoreStart } from 'kibana/public'; -import { ViewMode } from '../../../../src/plugins/embeddable/public'; -import { + +import type { DataView } from 'src/plugins/data_views/public'; +import type { CoreStart } from 'kibana/public'; +import type { StartDependencies } from './plugin'; +import type { TypedLensByValueInput, PersistedIndexPatternLayer, XYState, LensEmbeddableInput, + FormulaPublicApi, DateHistogramIndexPatternColumn, } from '../../../plugins/lens/public'; -import { StartDependencies } from './plugin'; + +import { ViewMode } from '../../../../src/plugins/embeddable/public'; // Generate a Lens state based on some app-specific input parameters. // `TypedLensByValueInput` can be used for type-safety - it uses the same interfaces as Lens-internal code. function getLensAttributes( - defaultIndexPattern: IndexPattern, - color: string + color: string, + dataView: DataView, + formula: FormulaPublicApi ): TypedLensByValueInput['attributes'] { - const dataLayer: PersistedIndexPatternLayer = { - columnOrder: ['col1', 'col2'], + const baseLayer: PersistedIndexPatternLayer = { + columnOrder: ['col1'], columns: { - col2: { - dataType: 'number', - isBucketed: false, - label: 'Count of records', - operationType: 'count', - scale: 'ratio', - sourceField: 'Records', - }, col1: { dataType: 'date', isBucketed: true, @@ -55,11 +50,18 @@ function getLensAttributes( operationType: 'date_histogram', params: { interval: 'auto' }, scale: 'interval', - sourceField: defaultIndexPattern.timeFieldName!, + sourceField: dataView.timeFieldName!, } as DateHistogramIndexPatternColumn, }, }; + const dataLayer = formula.insertOrReplaceFormulaColumn( + 'col2', + { formula: 'count()' }, + baseLayer, + dataView + ); + const xyConfig: XYState = { axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, fittingFunction: 'None', @@ -85,12 +87,12 @@ function getLensAttributes( title: 'Prefilled from example app', references: [ { - id: defaultIndexPattern.id!, + id: dataView.id!, name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { - id: defaultIndexPattern.id!, + id: dataView.id!, name: 'indexpattern-datasource-layer-layer1', type: 'index-pattern', }, @@ -99,7 +101,7 @@ function getLensAttributes( datasourceStates: { indexpattern: { layers: { - layer1: dataLayer, + layer1: dataLayer!, }, }, }, @@ -113,19 +115,22 @@ function getLensAttributes( export const App = (props: { core: CoreStart; plugins: StartDependencies; - defaultIndexPattern: IndexPattern | null; + defaultDataView: DataView; + formula: FormulaPublicApi; }) => { const [color, setColor] = useState('green'); const [isLoading, setIsLoading] = useState(false); const [isSaveModalVisible, setIsSaveModalVisible] = useState(false); - const LensComponent = props.plugins.lens.EmbeddableComponent; - const LensSaveModalComponent = props.plugins.lens.SaveModalComponent; - const [time, setTime] = useState({ from: 'now-5d', to: 'now', }); + const LensComponent = props.plugins.lens.EmbeddableComponent; + const LensSaveModalComponent = props.plugins.lens.SaveModalComponent; + + const attributes = getLensAttributes(color, props.defaultDataView, props.formula); + return ( @@ -147,138 +152,122 @@ export const App = (props: { the series which causes Lens to re-render. The Edit button will take the current configuration and navigate to a prefilled editor.

- {props.defaultIndexPattern && props.defaultIndexPattern.isTimeBased() ? ( - <> - - - { - // eslint-disable-next-line no-bitwise - const newColor = '#' + ((Math.random() * 0xffffff) << 0).toString(16); - setColor(newColor); - }} - > - Change color - - - - { - props.plugins.lens.navigateToPrefilledEditor( - { - id: '', - timeRange: time, - attributes: getLensAttributes(props.defaultIndexPattern!, color), - }, - { - openInNewTab: true, - } - ); - // eslint-disable-next-line no-bitwise - const newColor = '#' + ((Math.random() * 0xffffff) << 0).toString(16); - setColor(newColor); - }} - > - Edit in Lens (new tab) - - - - { - props.plugins.lens.navigateToPrefilledEditor( - { - id: '', - timeRange: time, - attributes: getLensAttributes(props.defaultIndexPattern!, color), - }, - { - openInNewTab: false, - } - ); - }} - > - Edit in Lens (same tab) - - - - { - setIsSaveModalVisible(true); - }} - > - Save Visualization - - - - { - setTime({ - from: '2015-09-18T06:31:44.000Z', - to: '2015-09-23T18:31:44.000Z', - }); - }} - > - Change time range - - - - { - setIsLoading(val); + + + + { + // eslint-disable-next-line no-bitwise + const newColor = '#' + ((Math.random() * 0xffffff) << 0).toString(16); + setColor(newColor); }} - onBrushEnd={({ range }) => { - setTime({ - from: new Date(range[0]).toISOString(), - to: new Date(range[1]).toISOString(), - }); + > + Change color + + + + { + props.plugins.lens.navigateToPrefilledEditor( + { + id: '', + timeRange: time, + attributes, + }, + { + openInNewTab: true, + } + ); + // eslint-disable-next-line no-bitwise + const newColor = '#' + ((Math.random() * 0xffffff) << 0).toString(16); + setColor(newColor); + }} + > + Edit in Lens (new tab) + + + + { + props.plugins.lens.navigateToPrefilledEditor( + { + id: '', + timeRange: time, + attributes, + }, + { + openInNewTab: false, + } + ); }} - onFilter={(_data) => { - // call back event for on filter event + > + Edit in Lens (same tab) + + + + { + setIsSaveModalVisible(true); }} - onTableRowClick={(_data) => { - // call back event for on table row click event + > + Save Visualization + + + + { + setTime({ + from: '2015-09-18T06:31:44.000Z', + to: '2015-09-23T18:31:44.000Z', + }); }} - viewMode={ViewMode.VIEW} - /> - {isSaveModalVisible && ( - {}} - onClose={() => setIsSaveModalVisible(false)} - /> - )} - - ) : ( - -

This demo only works if your default index pattern is set and time based

-
+ > + Change time range +
+
+
+ { + setIsLoading(val); + }} + onBrushEnd={({ range }) => { + setTime({ + from: new Date(range[0]).toISOString(), + to: new Date(range[1]).toISOString(), + }); + }} + onFilter={(_data) => { + // call back event for on filter event + }} + onTableRowClick={(_data) => { + // call back event for on table row click event + }} + viewMode={ViewMode.VIEW} + /> + {isSaveModalVisible && ( + {}} + onClose={() => setIsSaveModalVisible(false)} + /> )} diff --git a/x-pack/examples/embedded_lens_example/public/mount.tsx b/x-pack/examples/embedded_lens_example/public/mount.tsx index 58ec3632232707..e438b6946b8b6f 100644 --- a/x-pack/examples/embedded_lens_example/public/mount.tsx +++ b/x-pack/examples/embedded_lens_example/public/mount.tsx @@ -7,8 +7,10 @@ import * as React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { CoreSetup, AppMountParameters } from 'kibana/public'; -import { StartDependencies } from './plugin'; +import { EuiCallOut } from '@elastic/eui'; + +import type { CoreSetup, AppMountParameters } from 'kibana/public'; +import type { StartDependencies } from './plugin'; export const mount = (coreSetup: CoreSetup) => @@ -16,20 +18,27 @@ export const mount = const [core, plugins] = await coreSetup.getStartServices(); const { App } = await import('./app'); - const deps = { - core, - plugins, - }; - - const defaultIndexPattern = await plugins.data.indexPatterns.getDefault(); + const defaultDataView = await plugins.data.indexPatterns.getDefault(); + const { formula } = await plugins.lens.stateHelperApi(); const i18nCore = core.i18n; const reactElement = ( - + {defaultDataView && defaultDataView.isTimeBased() ? ( + + ) : ( + +

This demo only works if your default index pattern is set and time based

+
+ )}
); + render(reactElement, element); return () => unmountComponentAtNode(element); }; diff --git a/x-pack/plugins/apm/dev_docs/routing_and_linking.md b/x-pack/plugins/apm/dev_docs/routing_and_linking.md index 562af3d01ef779..d5c1d6c6306354 100644 --- a/x-pack/plugins/apm/dev_docs/routing_and_linking.md +++ b/x-pack/plugins/apm/dev_docs/routing_and_linking.md @@ -46,7 +46,7 @@ To be able to use the parameters, you can use `useApmParams`, which will automat ```ts const { path: { serviceName }, // string - query: { transactionType } // string | undefined + query: { transactionType }, // string | undefined } = useApmParams('/services/:serviceName'); ``` @@ -64,13 +64,16 @@ For links that stay inside APM, the preferred way of linking is to call the `use ```ts const apmRouter = useApmRouter(); -const serviceOverviewLink = apmRouter.link('/services/:serviceName', { path: { serviceName: 'opbeans-java' }, query: { transactionType: 'request' }}); +const serviceOverviewLink = apmRouter.link('/services/:serviceName', { + path: { serviceName: 'opbeans-java' }, + query: { transactionType: 'request' }, +}); ``` - If you're not in React context, you can also import `apmRouter` directly and call its `link` function - but you have to prepend the basePath manually in that case. +If you're not in React context, you can also import `apmRouter` directly and call its `link` function - but you have to prepend the basePath manually in that case. -We also have the [`getLegacyApmHref` function and `APMLink` component](../public/components/shared/Links/apm/APMLink.tsx), but we should consider them deprecated, in favor of `router.link`. Other components inside that directory contain other functions and components that provide the same functionality for linking to more specific sections inside the APM plugin. +We also have the [`getLegacyApmHref` function and `APMLink` component](../public/components/shared/links/apm/APMLink.tsx), but we should consider them deprecated, in favor of `router.link`. Other components inside that directory contain other functions and components that provide the same functionality for linking to more specific sections inside the APM plugin. ### Cross-app linking -Other helpers and components in [the Links directory](../public/components/shared/Links) allow linking to other Kibana apps. +Other helpers and components in [the Links directory](../public/components/shared/links) allow linking to other Kibana apps. diff --git a/x-pack/plugins/apm/public/application/application.test.tsx b/x-pack/plugins/apm/public/application/application.test.tsx index 12170ac20b7dff..396e853ca54a7f 100644 --- a/x-pack/plugins/apm/public/application/application.test.tsx +++ b/x-pack/plugins/apm/public/application/application.test.tsx @@ -20,13 +20,13 @@ import { disableConsoleWarning } from '../utils/testHelpers'; import { dataPluginMock } from 'src/plugins/data/public/mocks'; import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; -import { RumHome } from '../components/app/RumDashboard/RumHome'; +import { RumHome } from '../components/app/rum_dashboard/rum_home'; jest.mock('../services/rest/data_view', () => ({ createStaticDataView: () => Promise.resolve(undefined), })); -jest.mock('../components/app/RumDashboard/RumHome', () => ({ +jest.mock('../components/app/rum_dashboard/rum_home', () => ({ RumHome: () =>

Home Mock

, })); diff --git a/x-pack/plugins/apm/public/application/uxApp.tsx b/x-pack/plugins/apm/public/application/uxApp.tsx index cfb1a5c354c2da..dde7cfe5399d37 100644 --- a/x-pack/plugins/apm/public/application/uxApp.tsx +++ b/x-pack/plugins/apm/public/application/uxApp.tsx @@ -21,18 +21,18 @@ import { useUiSetting$, } from '../../../../../src/plugins/kibana_react/public'; import { APMRouteDefinition } from '../application/routes'; -import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; +import { ScrollToTopOnPathChange } from '../components/app/main/ScrollToTopOnPathChange'; import { RumHome, DASHBOARD_LABEL, -} from '../components/app/RumDashboard/RumHome'; +} from '../components/app/rum_dashboard/rum_home'; import { ApmPluginContext } from '../context/apm_plugin/apm_plugin_context'; import { UrlParamsProvider } from '../context/url_params_context/url_params_context'; import { ConfigSchema } from '../index'; import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; import { createStaticDataView } from '../services/rest/data_view'; -import { UXActionMenu } from '../components/app/RumDashboard/ActionMenu'; +import { UXActionMenu } from '../components/app/rum_dashboard/action_menu'; import { redirectTo } from '../components/routing/redirect_to'; import { InspectorContextProvider, diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/ServicePage/FormRowSelect.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/ServicePage/FormRowSelect.tsx index 45c8ddaf2f4dff..b45a513bf9d648 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/ServicePage/FormRowSelect.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/ServicePage/FormRowSelect.tsx @@ -11,7 +11,7 @@ import { EuiSelectOption, EuiFormRow, } from '@elastic/eui'; -import { SelectWithPlaceholder } from '../../../../../shared/SelectWithPlaceholder'; +import { SelectWithPlaceholder } from '../../../../../shared/select_with_placeholder'; interface Props { title: string; diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx index d71751805ce416..50f4e34a1c6b4d 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx @@ -17,7 +17,7 @@ import { } from '../../../../../../../common/agent_configuration/all_option'; import { useFetcher, FETCH_STATUS } from '../../../../../../hooks/use_fetcher'; import { FormRowSelect } from './FormRowSelect'; -import { APMLink } from '../../../../../shared/Links/apm/APMLink'; +import { APMLink } from '../../../../../shared/links/apm/apm_link'; interface Props { newConfig: AgentConfigurationIntake; diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx index cee38e4186454f..ccc483182d772a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx @@ -24,7 +24,7 @@ import { amountAndUnitToString, amountAndUnitToObject, } from '../../../../../../../common/agent_configuration/amount_and_unit'; -import { SelectWithPlaceholder } from '../../../../../shared/SelectWithPlaceholder'; +import { SelectWithPlaceholder } from '../../../../../shared/select_with_placeholder'; function FormRow({ setting, diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/index.tsx index a0ca7daf826106..eaf7bb711e54e9 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/index.tsx @@ -16,7 +16,7 @@ import { AgentConfigurationIntake, } from '../../../../../../common/agent_configuration/configuration_types'; import { FetcherResult } from '../../../../../hooks/use_fetcher'; -import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; +import { fromQuery, toQuery } from '../../../../shared/links/url_helpers'; import { ServicePage } from './ServicePage/ServicePage'; import { SettingsPage } from './SettingsPage/SettingsPage'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx index 4804e52b16d4fc..15efd28756b0b6 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx @@ -23,9 +23,9 @@ import { getOptionLabel } from '../../../../../../common/agent_configuration/all import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; import { FETCH_STATUS } from '../../../../../hooks/use_fetcher'; import { useTheme } from '../../../../../hooks/use_theme'; -import { LoadingStatePrompt } from '../../../../shared/LoadingStatePrompt'; +import { LoadingStatePrompt } from '../../../../shared/loading_state_prompt'; import { ITableColumn, ManagedTable } from '../../../../shared/managed_table'; -import { TimestampTooltip } from '../../../../shared/TimestampTooltip'; +import { TimestampTooltip } from '../../../../shared/timestamp_tooltip'; import { ConfirmDeleteModal } from './ConfirmDeleteModal'; type Config = diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/agent_keys_table.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/agent_keys_table.tsx index ccd409f1798a5f..e1ced3fabcd77b 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/agent_keys_table.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/agent_keys_table.tsx @@ -12,7 +12,7 @@ import { EuiBasicTableColumn, EuiInMemoryTableProps, } from '@elastic/eui'; -import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { TimestampTooltip } from '../../../shared/timestamp_tooltip'; import { ApiKey } from '../../../../../../security/common/model'; import { ConfirmDeleteModal } from './confirm_delete_modal'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index 1faab4092361d0..15b2d393a97a0c 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -27,9 +27,9 @@ import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plug import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useMlManageJobsHref } from '../../../../hooks/use_ml_manage_jobs_href'; import { callApmApi } from '../../../../services/rest/createCallApmApi'; -import { MLExplorerLink } from '../../../shared/Links/MachineLearningLinks/MLExplorerLink'; -import { MLManageJobsLink } from '../../../shared/Links/MachineLearningLinks/MLManageJobsLink'; -import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; +import { MLExplorerLink } from '../../../shared/links/machine_learning_links/mlexplorer_link'; +import { MLManageJobsLink } from '../../../shared/links/machine_learning_links/mlmanage_jobs_link'; +import { LoadingStatePrompt } from '../../../shared/loading_state_prompt'; import { ITableColumn, ManagedTable } from '../../../shared/managed_table'; import { MLCallout, shouldDisplayMlCallout } from '../../../shared/ml_callout'; import { AnomalyDetectionApiResponse } from './index'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list_status.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list_status.tsx index 6145e9f9ca7da0..1df6dc0332b2a8 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list_status.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list_status.tsx @@ -8,7 +8,7 @@ import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { DATAFEED_STATE, JOB_STATE } from '../../../../../../ml/common'; -import { MLManageJobsLink } from '../../../shared/Links/MachineLearningLinks/MLManageJobsLink'; +import { MLManageJobsLink } from '../../../shared/links/machine_learning_links/mlmanage_jobs_link'; export function JobsListStatus({ jobId, diff --git a/x-pack/plugins/apm/public/components/app/Settings/custom_link/custom_link_table.tsx b/x-pack/plugins/apm/public/components/app/Settings/custom_link/custom_link_table.tsx index b6f344751b59bb..5ce98f8b10884c 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/custom_link/custom_link_table.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/custom_link/custom_link_table.tsx @@ -18,9 +18,9 @@ import { isEmpty } from 'lodash'; import React, { useState } from 'react'; import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; -import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; +import { LoadingStatePrompt } from '../../../shared/loading_state_prompt'; import { ITableColumn, ManagedTable } from '../../../shared/managed_table'; -import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { TimestampTooltip } from '../../../shared/timestamp_tooltip'; interface Props { items: CustomLink[]; diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx index 1847ea90bd7fa6..375a4b5ac11566 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useUiTracker } from '../../../../../../observability/public'; -import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink'; +import { ElasticDocsLink } from '../../../shared/links/elastic_docs_link'; interface Props { onConfirm: () => void; diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/card_footer_content.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/card_footer_content.tsx index 62b4b242b1b688..6017d16ba2dad6 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/card_footer_content.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/card_footer_content.tsx @@ -9,8 +9,8 @@ import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; -import { APMLink } from '../../../../shared/Links/apm/APMLink'; -import { useFleetCloudAgentPolicyHref } from '../../../../shared/Links/kibana'; +import { APMLink } from '../../../../shared/links/apm/apm_link'; +import { useFleetCloudAgentPolicyHref } from '../../../../shared/links/kibana'; export function CardFooterContent() { const fleetCloudAgentPolicyHref = useFleetCloudAgentPolicyHref(); diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/upgrade_available_card.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/upgrade_available_card.tsx index eee8ca66dd08f3..8741cd76676982 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/upgrade_available_card.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/upgrade_available_card.tsx @@ -9,7 +9,7 @@ import { EuiCard, EuiIcon, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; -import { useUpgradeApmPackagePolicyHref } from '../../../../shared/Links/kibana'; +import { useUpgradeApmPackagePolicyHref } from '../../../../shared/links/kibana'; import { CardFooterContent } from './card_footer_content'; export function UpgradeAvailableCard({ diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx index d9540115d0d70b..4c50b47a4b9a61 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx @@ -22,7 +22,7 @@ import React from 'react'; import semverLt from 'semver/functions/lt'; import { SUPPORTED_APM_PACKAGE_VERSION } from '../../../../../common/fleet'; import { PackagePolicy } from '../../../../../../fleet/common/types'; -import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink'; +import { ElasticDocsLink } from '../../../shared/links/elastic_docs_link'; import rocketLaunchGraphic from './blog-rocket-720x420.png'; import { MigrationInProgressPanel } from './migration_in_progress_panel'; import { UpgradeAvailableCard } from './migrated/upgrade_available_card'; 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 c642ca7bd577ff..2d09abc15f8540 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 @@ -38,8 +38,8 @@ import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { useTheme } from '../../../hooks/use_theme'; -import { ImpactBar } from '../../shared/ImpactBar'; -import { push } from '../../shared/Links/url_helpers'; +import { ImpactBar } from '../../shared/impact_bar'; +import { push } from '../../shared/links/url_helpers'; import { CorrelationsTable } from './correlations_table'; import { FailedTransactionsCorrelationsHelpPopover } from './failed_transactions_correlations_help_popover'; diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx index 0656ab045efc25..5bf7e30a3df5d4 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx @@ -25,7 +25,7 @@ import { mockApmPluginContextValue, MockApmPluginContextWrapper, } from '../../../context/apm_plugin/mock_apm_plugin_context'; -import { fromQuery } from '../../shared/Links/url_helpers'; +import { fromQuery } from '../../shared/links/url_helpers'; import { LatencyCorrelations } from './latency_correlations'; 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 f79e9555957170..5b37a14b4e4e5c 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 @@ -33,7 +33,7 @@ import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_ import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { TransactionDistributionChart } from '../../shared/charts/transaction_distribution_chart'; -import { push } from '../../shared/Links/url_helpers'; +import { push } from '../../shared/links/url_helpers'; import { CorrelationsTable } from './correlations_table'; import { LatencyCorrelationsHelpPopover } from './latency_correlations_help_popover'; diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx b/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx index 929cc4f7f4cd3f..d68c11f981a0f5 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx @@ -17,7 +17,7 @@ import { } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { delay } from '../../../utils/testHelpers'; -import { fromQuery } from '../../shared/Links/url_helpers'; +import { fromQuery } from '../../shared/links/url_helpers'; import { useFailedTransactionsCorrelations } from './use_failed_transactions_correlations'; diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.test.tsx b/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.test.tsx index 90d976c389c589..c2c5a63a2ab030 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.test.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.test.tsx @@ -17,7 +17,7 @@ import { } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { delay } from '../../../utils/testHelpers'; -import { fromQuery } from '../../shared/Links/url_helpers'; +import { fromQuery } from '../../shared/links/url_helpers'; import { useLatencyCorrelations } from './use_latency_correlations'; diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/exception_stacktrace.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/exception_stacktrace.tsx index 77d54e0dad698a..85ad5c5336c8c2 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/exception_stacktrace.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/exception_stacktrace.tsx @@ -8,8 +8,8 @@ import { EuiTitle } from '@elastic/eui'; import React from 'react'; import { Exception } from '../../../../../typings/es_schemas/raw/error_raw'; -import { Stacktrace } from '../../../shared/Stacktrace'; -import { CauseStacktrace } from '../../../shared/Stacktrace/cause_stacktrace'; +import { Stacktrace } from '../../../shared/stacktrace'; +import { CauseStacktrace } from '../../../shared/stacktrace/cause_stacktrace'; interface ExceptionStacktraceProps { codeLanguage?: string; diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx index 5438fce7c48819..af7283af8c5264 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx @@ -23,15 +23,15 @@ import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/comm import type { APIReturnType } from '../../../../services/rest/createCallApmApi'; import type { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; import type { ApmUrlParams } from '../../../../context/url_params_context/types'; -import { TransactionDetailLink } from '../../../shared/Links/apm/transaction_detail_link'; -import { DiscoverErrorLink } from '../../../shared/Links/DiscoverLinks/DiscoverErrorLink'; -import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; -import { ErrorMetadata } from '../../../shared/MetadataTable/ErrorMetadata'; -import { Stacktrace } from '../../../shared/Stacktrace'; -import { Summary } from '../../../shared/Summary'; -import { HttpInfoSummaryItem } from '../../../shared/Summary/http_info_summary_item'; -import { UserAgentSummaryItem } from '../../../shared/Summary/UserAgentSummaryItem'; -import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { TransactionDetailLink } from '../../../shared/links/apm/transaction_detail_link'; +import { DiscoverErrorLink } from '../../../shared/links/discover_links/discover_error_link'; +import { fromQuery, toQuery } from '../../../shared/links/url_helpers'; +import { ErrorMetadata } from '../../../shared/metadata_table/error_metadata'; +import { Stacktrace } from '../../../shared/stacktrace'; +import { Summary } from '../../../shared/summary'; +import { HttpInfoSummaryItem } from '../../../shared/summary/http_info_summary_item'; +import { UserAgentSummaryItem } from '../../../shared/summary/user_agent_summary_item'; +import { TimestampTooltip } from '../../../shared/timestamp_tooltip'; import { ErrorTab, exceptionStacktraceTab, diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/error_group_list/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/error_group_list/index.tsx index e252eba15ade15..65681a398d8e65 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/error_group_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/error_group_list/index.tsx @@ -19,11 +19,11 @@ import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { truncate, unit } from '../../../../utils/style'; -import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; -import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; -import { APMQueryParams } from '../../../shared/Links/url_helpers'; +import { ErrorDetailLink } from '../../../shared/links/apm/error_detail_link'; +import { ErrorOverviewLink } from '../../../shared/links/apm/error_overview_link'; +import { APMQueryParams } from '../../../shared/links/url_helpers'; import { ITableColumn, ManagedTable } from '../../../shared/managed_table'; -import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { TimestampTooltip } from '../../../shared/timestamp_tooltip'; import { SparkPlot } from '../../../shared/charts/spark_plot'; const GroupIdLink = euiStyled(ErrorDetailLink)` diff --git a/x-pack/plugins/apm/public/components/app/infra_overview/index.tsx b/x-pack/plugins/apm/public/components/app/infra_overview/index.tsx new file mode 100644 index 00000000000000..c1360ba3b13ad2 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/infra_overview/index.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export function InfraOverview() { + return ( + } + title={ +

+ {i18n.translate('xpack.apm.infra.announcement', { + defaultMessage: 'Infrastructure data coming soon', + })} +

+ } + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Main/ScrollToTopOnPathChange.tsx b/x-pack/plugins/apm/public/components/app/main/ScrollToTopOnPathChange.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Main/ScrollToTopOnPathChange.tsx rename to x-pack/plugins/apm/public/components/app/main/ScrollToTopOnPathChange.tsx diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/action_menu/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/action_menu/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/inpector_link.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/action_menu/inpector_link.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/inpector_link.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/action_menu/inpector_link.tsx diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/breakdowns/breakdown_filter.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/breakdowns/breakdown_filter.tsx diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/chart_wrapper/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/chart_wrapper/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/charts/page_load_dist_chart.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/charts/page_load_dist_chart.tsx index 51349ff22a9621..8b34ad8980774a 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/charts/page_load_dist_chart.tsx @@ -28,13 +28,13 @@ import { EUI_CHARTS_THEME_LIGHT, } from '@elastic/eui/dist/eui_charts_theme'; import styled from 'styled-components'; -import { PercentileAnnotations } from '../PageLoadDistribution/PercentileAnnotations'; +import { PercentileAnnotations } from '../page_load_distribution/percentile_annotations'; import { I18LABELS } from '../translations'; -import { ChartWrapper } from '../ChartWrapper'; -import { PercentileRange } from '../PageLoadDistribution'; +import { ChartWrapper } from '../chart_wrapper'; +import { PercentileRange } from '../page_load_distribution'; import { BreakdownItem } from '../../../../../typings/ui_filters'; import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public'; -import { BreakdownSeries } from '../PageLoadDistribution/BreakdownSeries'; +import { BreakdownSeries } from '../page_load_distribution/breakdown_series'; interface PageLoadData { pageLoadDistribution: Array<{ x: number; y: number }>; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/charts/page_views_chart.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/charts/page_views_chart.tsx index e5ee427e166771..059feb0915fdbd 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/charts/page_views_chart.tsx @@ -29,8 +29,8 @@ import React from 'react'; import { useHistory } from 'react-router-dom'; import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; -import { ChartWrapper } from '../ChartWrapper'; +import { fromQuery, toQuery } from '../../../shared/links/url_helpers'; +import { ChartWrapper } from '../chart_wrapper'; import { I18LABELS } from '../translations'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/charts/visitor_breakdown_chart.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/charts/visitor_breakdown_chart.tsx index 149e9b8ee763a1..89f49a9669b451 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/charts/visitor_breakdown_chart.tsx @@ -22,7 +22,7 @@ import { EUI_CHARTS_THEME_LIGHT, } from '@elastic/eui/dist/eui_charts_theme'; import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public'; -import { ChartWrapper } from '../ChartWrapper'; +import { ChartWrapper } from '../chart_wrapper'; import { I18LABELS } from '../translations'; const StyleChart = styled.div` diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/client_metrics/index.tsx similarity index 91% rename from x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/client_metrics/index.tsx index 7c48531d21990c..a017d1b5304e3c 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/client_metrics/index.tsx @@ -14,9 +14,9 @@ import { EuiSpacer, } from '@elastic/eui'; import { I18LABELS } from '../translations'; -import { getPercentileLabel } from '../UXMetrics/translations'; +import { getPercentileLabel } from '../ux_metrics/translations'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { Metrics } from './Metrics'; +import { Metrics } from './metrics'; export function ClientMetrics() { const { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/Metrics.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/client_metrics/metrics.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/Metrics.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/client_metrics/metrics.tsx index ded242e2ce558a..82dac9cc8f0163 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/Metrics.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/client_metrics/metrics.tsx @@ -18,9 +18,9 @@ import { } from '@elastic/eui'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { I18LABELS } from '../translations'; -import { useUxQuery } from '../hooks/useUxQuery'; -import { formatToSec } from '../UXMetrics/KeyUXMetrics'; -import { CsmSharedContext } from '../CsmSharedContext'; +import { useUxQuery } from '../hooks/use_ux_query'; +import { formatToSec } from '../ux_metrics/key_ux_metrics'; +import { CsmSharedContext } from '../csm_shared_context'; const ClFlexGroup = styled(EuiFlexGroup)` flex-direction: row; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CsmSharedContext/index.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/csm_shared_context/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/CsmSharedContext/index.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/csm_shared_context/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/empty_state_loading.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/empty_state_loading.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/empty_state_loading.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/empty_state_loading.tsx diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/use_call_api.ts b/x-pack/plugins/apm/public/components/app/rum_dashboard/hooks/use_call_api.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/hooks/use_call_api.ts rename to x-pack/plugins/apm/public/components/app/rum_dashboard/hooks/use_call_api.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useHasRumData.ts b/x-pack/plugins/apm/public/components/app/rum_dashboard/hooks/use_has_rum_data.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useHasRumData.ts rename to x-pack/plugins/apm/public/components/app/rum_dashboard/hooks/use_has_rum_data.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/components/app/rum_dashboard/hooks/use_local_ui_filters.ts similarity index 95% rename from x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts rename to x-pack/plugins/apm/public/components/app/rum_dashboard/hooks/use_local_ui_filters.ts index 8045e4947dcb0b..5c0bc7fdfd462d 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/hooks/use_local_ui_filters.ts @@ -15,10 +15,10 @@ import { import { fromQuery, toQuery, -} from '../../../../components/shared/Links/url_helpers'; +} from '../../../../components/shared/links/url_helpers'; import { removeUndefinedProps } from '../../../../context/url_params_context/helpers'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { getExcludedName } from '../LocalUIFilters'; +import { getExcludedName } from '../local_ui_filters'; export type FiltersUIHook = ReturnType; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useUxQuery.ts b/x-pack/plugins/apm/public/components/app/rum_dashboard/hooks/use_ux_query.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useUxQuery.ts rename to x-pack/plugins/apm/public/components/app/rum_dashboard/hooks/use_ux_query.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/impactful_metrics/index.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/index.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/impactful_metrics/index.tsx index b696a46f59bd12..640ab0b0daad22 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/impactful_metrics/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiFlexItem, EuiPanel, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; -import { JSErrors } from './JSErrors'; +import { JSErrors } from './js_errors'; export function ImpactfulMetrics() { return ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/impactful_metrics/js_errors.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/impactful_metrics/js_errors.tsx index 96ec397f5f94fa..9f7ad5d053fa4a 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/impactful_metrics/js_errors.tsx @@ -21,8 +21,8 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { I18LABELS } from '../translations'; -import { CsmSharedContext } from '../CsmSharedContext'; -import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; +import { CsmSharedContext } from '../csm_shared_context'; +import { ErrorDetailLink } from '../../../shared/links/apm/error_detail_link'; interface JSErrorItem { errorMessage: string; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/index.tsx similarity index 86% rename from x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/index.tsx index ee11c301441b6d..d3eadeaf016c01 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/index.tsx @@ -8,8 +8,8 @@ import { EuiSpacer } from '@elastic/eui'; import React from 'react'; import { useTrackPageview } from '../../../../../observability/public'; -import { LocalUIFilters } from './LocalUIFilters'; -import { RumDashboard } from './RumDashboard'; +import { LocalUIFilters } from './local_ui_filters'; +import { RumDashboard } from './rum_dashboard'; export function RumOverview() { useTrackPageview({ app: 'ux', path: 'home' }); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/local_ui_filters/index.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/local_ui_filters/index.tsx index 655bf93e4fb934..1c54713125c7ea 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/local_ui_filters/index.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ESFilter } from 'src/core/types/elasticsearch'; -import { useLocalUIFilters } from '../hooks/useLocalUIFilters'; +import { useLocalUIFilters } from '../hooks/use_local_ui_filters'; import { uxFiltersByName, UxLocalUIFilterName, @@ -24,8 +24,8 @@ import { } from '../../../../../common/ux_ui_filter'; import { useBreakpoints } from '../../../../hooks/use_breakpoints'; import { FieldValueSuggestions } from '../../../../../../observability/public'; -import { URLFilter } from '../URLFilter'; -import { SelectedFilters } from './SelectedFilters'; +import { URLFilter } from '../url_filter'; +import { SelectedFilters } from './selected_filters'; import { SERVICE_NAME, TRANSACTION_TYPE, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/queries.ts b/x-pack/plugins/apm/public/components/app/rum_dashboard/local_ui_filters/queries.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/queries.ts rename to x-pack/plugins/apm/public/components/app/rum_dashboard/local_ui_filters/queries.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/SelectedFilters.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/local_ui_filters/selected_filters.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/SelectedFilters.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/local_ui_filters/selected_filters.tsx index d9f9154c5c100e..787bcd88266e23 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/SelectedFilters.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/local_ui_filters/selected_filters.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FilterValueLabel } from '../../../../../../observability/public'; -import { FiltersUIHook } from '../hooks/useLocalUIFilters'; +import { FiltersUIHook } from '../hooks/use_local_ui_filters'; import { UxLocalUIFilterName } from '../../../../../common/ux_ui_filter'; import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { SelectedWildcards } from './selected_wildcards'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/selected_wildcards.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/local_ui_filters/selected_wildcards.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/selected_wildcards.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/local_ui_filters/selected_wildcards.tsx index 6a9dfd1fddd11a..d18381cee77af1 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/selected_wildcards.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/local_ui_filters/selected_wildcards.tsx @@ -10,7 +10,7 @@ import { useCallback } from 'react'; import { useHistory } from 'react-router-dom'; import { FilterValueLabel } from '../../../../../../observability/public'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; +import { fromQuery, toQuery } from '../../../shared/links/url_helpers'; import { TRANSACTION_URL } from '../../../../../common/elasticsearch_fieldnames'; import { IndexPattern } from '../../../../../../../../src/plugins/data_views/common'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.test.js b/x-pack/plugins/apm/public/components/app/rum_dashboard/local_ui_filters/use_data_view.test.js similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.test.js rename to x-pack/plugins/apm/public/components/app/rum_dashboard/local_ui_filters/use_data_view.test.js diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.ts b/x-pack/plugins/apm/public/components/app/rum_dashboard/local_ui_filters/use_data_view.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.ts rename to x-pack/plugins/apm/public/components/app/rum_dashboard/local_ui_filters/use_data_view.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/page_load_distribution/breakdown_series.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/page_load_distribution/breakdown_series.tsx diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/page_load_distribution/index.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/page_load_distribution/index.tsx index 0daf620dc50098..e8e24c121ea6d5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/page_load_distribution/index.tsx @@ -17,10 +17,10 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { I18LABELS } from '../translations'; -import { BreakdownFilter } from '../Breakdowns/BreakdownFilter'; -import { PageLoadDistChart } from '../Charts/PageLoadDistChart'; +import { BreakdownFilter } from '../breakdowns/breakdown_filter'; +import { PageLoadDistChart } from '../charts/page_load_dist_chart'; import { BreakdownItem } from '../../../../../typings/ui_filters'; -import { ResetPercentileZoom } from './ResetPercentileZoom'; +import { ResetPercentileZoom } from './reset_percentile_zoom'; import { createExploratoryViewUrl } from '../../../../../../observability/public'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/page_load_distribution/percentile_annotations.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/page_load_distribution/percentile_annotations.tsx diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/ResetPercentileZoom.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/page_load_distribution/reset_percentile_zoom.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/ResetPercentileZoom.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/page_load_distribution/reset_percentile_zoom.tsx diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts b/x-pack/plugins/apm/public/components/app/rum_dashboard/page_load_distribution/use_breakdowns.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts rename to x-pack/plugins/apm/public/components/app/rum_dashboard/page_load_distribution/use_breakdowns.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/page_views_trend/index.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/page_views_trend/index.tsx index a334c1e23892a3..2510420d3eac40 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/page_views_trend/index.tsx @@ -17,8 +17,8 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { I18LABELS } from '../translations'; -import { BreakdownFilter } from '../Breakdowns/BreakdownFilter'; -import { PageViewsChart } from '../Charts/PageViewsChart'; +import { BreakdownFilter } from '../breakdowns/breakdown_filter'; +import { PageViewsChart } from '../charts/page_views_chart'; import { BreakdownItem } from '../../../../../typings/ui_filters'; import { createExploratoryViewUrl } from '../../../../../../observability/public'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/PageLoadAndViews.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/panels/page_load_and_views.tsx similarity index 86% rename from x-pack/plugins/apm/public/components/app/RumDashboard/Panels/PageLoadAndViews.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/panels/page_load_and_views.tsx index 9dd83fd1c8fd1f..1d6a65317122ca 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/PageLoadAndViews.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/panels/page_load_and_views.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import { PageLoadDistribution } from '../PageLoadDistribution'; -import { PageViewsTrend } from '../PageViewsTrend'; +import { PageLoadDistribution } from '../page_load_distribution'; +import { PageViewsTrend } from '../page_views_trend'; export function PageLoadAndViews() { return ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/VisitorBreakdowns.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/panels/visitor_breakdowns.tsx similarity index 86% rename from x-pack/plugins/apm/public/components/app/RumDashboard/Panels/VisitorBreakdowns.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/panels/visitor_breakdowns.tsx index ff79feaa924f37..04127ec702262c 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/VisitorBreakdowns.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/panels/visitor_breakdowns.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import { VisitorBreakdown } from '../VisitorBreakdown'; -import { VisitorBreakdownMap } from '../VisitorBreakdownMap'; +import { VisitorBreakdown } from '../visitor_breakdown'; +import { VisitorBreakdownMap } from '../visitor_breakdown_map'; export function VisitorBreakdownsPanel() { return ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/WebApplicationSelect.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/panels/web_application_select.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/app/RumDashboard/Panels/WebApplicationSelect.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/panels/web_application_select.tsx index ecba89b2651ac9..9c5494c1ea5330 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/WebApplicationSelect.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/panels/web_application_select.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { ServiceNameFilter } from '../URLFilter/ServiceNameFilter'; +import { ServiceNameFilter } from '../url_filter/service_name_filter'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { RUM_AGENT_NAMES } from '../../../../../common/agent_name'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/rum_dashboard.tsx similarity index 76% rename from x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/rum_dashboard.tsx index 4ed011441c81bd..d6b454bd948f76 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/rum_dashboard.tsx @@ -7,12 +7,12 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import { UXMetrics } from './UXMetrics'; -import { ImpactfulMetrics } from './ImpactfulMetrics'; -import { PageLoadAndViews } from './Panels/PageLoadAndViews'; -import { VisitorBreakdownsPanel } from './Panels/VisitorBreakdowns'; +import { UXMetrics } from './ux_metrics'; +import { ImpactfulMetrics } from './impactful_metrics'; +import { PageLoadAndViews } from './panels/page_load_and_views'; +import { VisitorBreakdownsPanel } from './panels/visitor_breakdowns'; import { useBreakpoints } from '../../../hooks/use_breakpoints'; -import { ClientMetrics } from './ClientMetrics'; +import { ClientMetrics } from './client_metrics'; export function RumDashboard() { const { isSmall } = useBreakpoints(); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/rum_datepicker/index.test.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/rum_datepicker/index.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/rum_datepicker/index.test.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/rum_datepicker/index.test.tsx diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/rum_datepicker/index.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/rum_datepicker/index.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/app/RumDashboard/rum_datepicker/index.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/rum_datepicker/index.tsx index 9bc18d772a4a10..4e39ad4397b41e 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/rum_datepicker/index.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/rum_datepicker/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { useUxUrlParams } from '../../../../context/url_params_context/use_ux_url_params'; import { useDateRangeRedirect } from '../../../../hooks/use_date_range_redirect'; -import { DatePicker } from '../../../shared/DatePicker'; +import { DatePicker } from '../../../shared/date_picker'; export function RumDatePicker() { const { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/rum_home.tsx similarity index 90% rename from x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/rum_home.tsx index 54c34121ea0cb3..bb0427d462d545 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/rum_home.tsx @@ -8,15 +8,15 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiTitle, EuiFlexItem } from '@elastic/eui'; -import { RumOverview } from '../RumDashboard'; -import { CsmSharedContextProvider } from './CsmSharedContext'; -import { WebApplicationSelect } from './Panels/WebApplicationSelect'; +import { RumOverview } from '../rum_dashboard'; +import { CsmSharedContextProvider } from './csm_shared_context'; +import { WebApplicationSelect } from './panels/web_application_select'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; -import { UxEnvironmentFilter } from '../../shared/EnvironmentFilter'; -import { UserPercentile } from './UserPercentile'; +import { UxEnvironmentFilter } from '../../shared/environment_filter'; +import { UserPercentile } from './user_percentile'; import { useBreakpoints } from '../../../hooks/use_breakpoints'; import { KibanaPageTemplateProps } from '../../../../../../../src/plugins/kibana_react/public'; -import { useHasRumData } from './hooks/useHasRumData'; +import { useHasRumData } from './hooks/use_has_rum_data'; import { RumDatePicker } from './rum_datepicker'; import { EmptyStateLoading } from './empty_state_loading'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/rum_dashboard/translations.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts rename to x-pack/plugins/apm/public/components/app/rum_dashboard/translations.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/index.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/index.tsx index cac899665d98b5..558092db4a4582 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/index.tsx @@ -8,8 +8,8 @@ import React, { useCallback } from 'react'; import { useHistory } from 'react-router-dom'; import { omit } from 'lodash'; -import { URLSearch } from './URLSearch'; -import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; +import { URLSearch } from './url_search'; +import { fromQuery, toQuery } from '../../../shared/links/url_helpers'; import { removeUndefinedProps } from '../../../../context/url_params_context/helpers'; export function URLFilter() { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/service_name_filter/index.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/service_name_filter/index.tsx index f6891a5d8fb672..b3560f0ebc97b8 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/service_name_filter/index.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; import { useLegacyUrlParams } from '../../../../../context/url_params_context/use_url_params'; -import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; +import { fromQuery, toQuery } from '../../../../shared/links/url_helpers'; interface Props { serviceNames: string[]; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/url_search/index.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/url_search/index.tsx index e34270f963599d..9ecb2f31112ea8 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/url_search/index.tsx @@ -10,8 +10,8 @@ import { isEqual, map } from 'lodash'; import { i18n } from '@kbn/i18n'; import { useLegacyUrlParams } from '../../../../../context/url_params_context/use_url_params'; import { I18LABELS } from '../../translations'; -import { formatToSec } from '../../UXMetrics/KeyUXMetrics'; -import { getPercentileLabel } from '../../UXMetrics/translations'; +import { formatToSec } from '../../ux_metrics/key_ux_metrics'; +import { getPercentileLabel } from '../../ux_metrics/translations'; import { SelectableUrlList } from '../../../../../../../observability/public'; import { selectableRenderOptions, UrlOption } from './render_option'; import { useUrlSearch } from './use_url_search'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/render_option.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/url_search/render_option.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/render_option.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/url_search/render_option.tsx diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/use_url_search.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/url_search/use_url_search.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/use_url_search.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/url_search/use_url_search.tsx index 8228ab4c6e83e7..e7a025985fca7a 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/use_url_search.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/url_search/use_url_search.tsx @@ -8,7 +8,7 @@ import useDebounce from 'react-use/lib/useDebounce'; import { useState } from 'react'; import { useFetcher } from '../../../../../hooks/use_fetcher'; -import { useUxQuery } from '../../hooks/useUxQuery'; +import { useUxQuery } from '../../hooks/use_ux_query'; import { useLegacyUrlParams } from '../../../../../context/url_params_context/use_url_params'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/user_percentile/index.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/user_percentile/index.tsx index 7d05b188e9bbed..a1e70f4f25d218 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/user_percentile/index.tsx @@ -10,7 +10,7 @@ import React, { useCallback, useEffect } from 'react'; import { EuiSelect } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; +import { fromQuery, toQuery } from '../../../shared/links/url_helpers'; import { I18LABELS } from '../translations'; const DEFAULT_P = 50; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/utils/test_helper.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/utils/test_helper.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/utils/test_helper.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/utils/test_helper.tsx diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/FormatToSec.test.ts b/x-pack/plugins/apm/public/components/app/rum_dashboard/ux_metrics/format_to_sec.test.ts similarity index 94% rename from x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/FormatToSec.test.ts rename to x-pack/plugins/apm/public/components/app/rum_dashboard/ux_metrics/format_to_sec.test.ts index ca29bb4ce29441..3bb1e8bd923f1a 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/FormatToSec.test.ts +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/ux_metrics/format_to_sec.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { formatToSec } from './KeyUXMetrics'; +import { formatToSec } from './key_ux_metrics'; describe('FormatToSec', () => { test('it returns the expected value', () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/ux_metrics/index.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/ux_metrics/index.tsx index ab6843f94ee439..cc1223de7b1771 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/ux_metrics/index.tsx @@ -15,11 +15,11 @@ import { EuiTitle, } from '@elastic/eui'; import { I18LABELS } from '../translations'; -import { KeyUXMetrics } from './KeyUXMetrics'; +import { KeyUXMetrics } from './key_ux_metrics'; import { useFetcher } from '../../../../hooks/use_fetcher'; -import { useUxQuery } from '../hooks/useUxQuery'; +import { useUxQuery } from '../hooks/use_ux_query'; import { getCoreVitalsComponent } from '../../../../../../observability/public'; -import { CsmSharedContext } from '../CsmSharedContext'; +import { CsmSharedContext } from '../csm_shared_context'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { getPercentileLabel } from './translations'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.test.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.test.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.test.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.test.tsx index e1253b41a45f59..2f92e5efedf420 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.test.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import * as fetcherHook from '../../../../hooks/use_fetcher'; -import { KeyUXMetrics } from './KeyUXMetrics'; +import { KeyUXMetrics } from './key_ux_metrics'; describe('KeyUXMetrics', () => { it('renders metrics with correct formats', () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.tsx index 4eaf0dccc3225f..3e1c64c484edb9 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.tsx @@ -22,7 +22,7 @@ import { TBT_TOOLTIP, } from './translations'; import { useFetcher } from '../../../../hooks/use_fetcher'; -import { useUxQuery } from '../hooks/useUxQuery'; +import { useUxQuery } from '../hooks/use_ux_query'; import { UXMetrics } from '../../../../../../observability/public'; export function formatToSec( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/translations.ts b/x-pack/plugins/apm/public/components/app/rum_dashboard/ux_metrics/translations.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/translations.ts rename to x-pack/plugins/apm/public/components/app/rum_dashboard/ux_metrics/translations.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts b/x-pack/plugins/apm/public/components/app/rum_dashboard/ux_overview_fetchers.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts rename to x-pack/plugins/apm/public/components/app/rum_dashboard/ux_overview_fetchers.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown/index.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown/index.tsx index 7a19690a4582ef..822cb0f591bce4 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui'; -import { VisitorBreakdownChart } from '../Charts/VisitorBreakdownChart'; +import { VisitorBreakdownChart } from '../charts/visitor_breakdown_chart'; import { I18LABELS, VisitorBreakdownLabel } from '../translations'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__mocks__/regions_layer.mock.ts b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/__mocks__/regions_layer.mock.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__mocks__/regions_layer.mock.ts rename to x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/__mocks__/regions_layer.mock.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__snapshots__/EmbeddedMap.test.tsx.snap b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/__snapshots__/embedded_map.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__snapshots__/EmbeddedMap.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/__snapshots__/embedded_map.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__snapshots__/MapToolTip.test.tsx.snap b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/__snapshots__/map_tooltip.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__snapshots__/MapToolTip.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/__snapshots__/map_tooltip.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/__stories__/MapTooltip.stories.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/__stories__/MapTooltip.stories.tsx index 8263db648cd398..da4aad4102195f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/__stories__/MapTooltip.stories.tsx @@ -7,10 +7,10 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; -import { MapToolTip } from '../MapToolTip'; -import { COUNTRY_NAME, TRANSACTION_DURATION_COUNTRY } from '../useLayerList'; +import { MapToolTip } from '../map_tooltip'; +import { COUNTRY_NAME, TRANSACTION_DURATION_COUNTRY } from '../use_layer_list'; -storiesOf('app/RumDashboard/VisitorsRegionMap', module).add( +storiesOf('app/rum_dashboard/VisitorsRegionMap', module).add( 'Tooltip', () => { const loadFeatureProps = async () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.test.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/embedded_map.test.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.test.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/embedded_map.test.tsx index f286f963b4fa0f..572661dc8cea4f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.test.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/embedded_map.test.tsx @@ -8,7 +8,7 @@ import { render } from 'enzyme'; import React from 'react'; -import { EmbeddedMap } from './EmbeddedMap'; +import { EmbeddedMap } from './embedded_map'; import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; import { embeddablePluginMock } from '../../../../../../../../src/plugins/embeddable/public/mocks'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/embedded_map.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/embedded_map.tsx index 17a380f4d5e35c..32dcf3d5d34397 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/embedded_map.tsx @@ -20,12 +20,12 @@ import { ViewMode, isErrorEmbeddable, } from '../../../../../../../../src/plugins/embeddable/public'; -import { useLayerList } from './useLayerList'; +import { useLayerList } from './use_layer_list'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import type { RenderTooltipContentParams } from '../../../../../../maps/public'; -import { MapToolTip } from './MapToolTip'; -import { useMapFilters } from './useMapFilters'; +import { MapToolTip } from './map_tooltip'; +import { useMapFilters } from './use_map_filters'; import { EmbeddableStart } from '../../../../../../../../src/plugins/embeddable/public'; const EmbeddedPanel = styled.div` diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/index.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/index.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/index.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/index.tsx index 142337ef3e160d..253f989b389a8d 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiTitle, EuiSpacer } from '@elastic/eui'; -import { EmbeddedMap } from './EmbeddedMap'; +import { EmbeddedMap } from './embedded_map'; import { I18LABELS } from '../translations'; export function VisitorBreakdownMap() { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.test.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/map_tooltip.test.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.test.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/map_tooltip.test.tsx index be3b28f07cdaf8..6d59b15876d20e 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.test.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/map_tooltip.test.tsx @@ -8,7 +8,7 @@ import { render, shallow } from 'enzyme'; import React from 'react'; -import { MapToolTip } from './MapToolTip'; +import { MapToolTip } from './map_tooltip'; describe('Map Tooltip', () => { test('it shallow renders', () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/map_tooltip.tsx similarity index 99% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx rename to x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/map_tooltip.tsx index e923795fad95f1..56f1ae20144734 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/map_tooltip.tsx @@ -19,7 +19,7 @@ import { REGION_NAME, TRANSACTION_DURATION_COUNTRY, TRANSACTION_DURATION_REGION, -} from './useLayerList'; +} from './use_layer_list'; import type { RenderTooltipContentParams } from '../../../../../../maps/public'; import { I18LABELS } from '../translations'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.test.ts b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/use_layer_list.test.ts similarity index 92% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.test.ts rename to x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/use_layer_list.test.ts index 254d15f025c1b9..7c0cbf694aed7d 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.test.ts +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/use_layer_list.test.ts @@ -7,7 +7,7 @@ import { renderHook } from '@testing-library/react-hooks'; import { mockLayerList } from './__mocks__/regions_layer.mock'; -import { useLayerList } from './useLayerList'; +import { useLayerList } from './use_layer_list'; describe('useLayerList', () => { test('it returns the region layer', () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/use_layer_list.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts rename to x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/use_layer_list.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts b/x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/use_map_filters.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts rename to x-pack/plugins/apm/public/components/app/rum_dashboard/visitor_breakdown_map/use_map_filters.ts diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index 163082cf044cd0..549ccc8e692597 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -22,7 +22,7 @@ import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { useAnyOfApmParams } from '../../../hooks/use_apm_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; -import { useUpgradeAssistantHref } from '../../shared/Links/kibana'; +import { useUpgradeAssistantHref } from '../../shared/links/kibana'; import { SearchBar } from '../../shared/search_bar'; import { getTimeRangeComparison } from '../../shared/time_comparison/get_time_range_comparison'; import { ServiceList } from './service_list'; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx index fe91b14e64e8ac..2d83f1f46bd388 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx @@ -36,7 +36,7 @@ import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { unit } from '../../../../utils/style'; import { ApmRoutes } from '../../../routing/apm_route_config'; import { AggregatedTransactionsBadge } from '../../../shared/aggregated_transactions_badge'; -import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; +import { EnvironmentBadge } from '../../../shared/environment_badge'; import { ListMetric } from '../../../shared/list_metric'; import { ITableColumn, ManagedTable } from '../../../shared/managed_table'; import { ServiceLink } from '../../../shared/service_link'; diff --git a/x-pack/plugins/apm/public/components/app/service_map/Controls.tsx b/x-pack/plugins/apm/public/components/app/service_map/Controls.tsx index a48fb77b45585c..9236d9c21a6c64 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Controls.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Controls.tsx @@ -11,9 +11,9 @@ import React, { useContext, useEffect, useState } from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useTheme } from '../../../hooks/use_theme'; -import { getLegacyApmHref } from '../../shared/Links/apm/APMLink'; +import { getLegacyApmHref } from '../../shared/links/apm/apm_link'; import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params'; -import { APMQueryParams } from '../../shared/Links/url_helpers'; +import { APMQueryParams } from '../../shared/links/url_helpers'; import { CytoscapeContext } from './Cytoscape'; import { getAnimationOptions, getNodeHeight } from './cytoscape_options'; import { useAnyOfApmParams } from '../../../hooks/use_apm_params'; diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/anomaly_detection.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/anomaly_detection.tsx index 1ceb90ff838ad1..dd728be30c71a4 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/anomaly_detection.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/anomaly_detection.tsx @@ -26,7 +26,7 @@ import { import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types'; import { asDuration, asInteger } from '../../../../../common/utils/formatters'; import { useTheme } from '../../../../hooks/use_theme'; -import { MLSingleMetricLink } from '../../../shared/Links/MachineLearningLinks/MLSingleMetricLink'; +import { MLSingleMetricLink } from '../../../shared/links/machine_learning_links/mlsingle_metric_link'; import { popoverWidth } from '../cytoscape_options'; const HealthStatusTitle = euiStyled(EuiTitle)` diff --git a/x-pack/plugins/apm/public/components/app/service_map/empty_prompt.tsx b/x-pack/plugins/apm/public/components/app/service_map/empty_prompt.tsx index 6c7a8718dee584..ce9dbe7ce417f7 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/empty_prompt.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/empty_prompt.tsx @@ -8,7 +8,7 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink'; +import { SetupInstructionsLink } from '../../shared/links/setup_instructions_link'; export function EmptyPrompt() { return ( diff --git a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx index 04bb578b0c4348..0436c27cdd6b7b 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx @@ -22,7 +22,7 @@ import { useApmParams } from '../../../hooks/use_apm_params'; import { useFetcher, FETCH_STATUS } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; import { truncate, unit } from '../../../utils/style'; -import { ServiceNodeMetricOverviewLink } from '../../shared/Links/apm/ServiceNodeMetricOverviewLink'; +import { ServiceNodeMetricOverviewLink } from '../../shared/links/apm/service_node_metric_overview_link'; import { ITableColumn, ManagedTable } from '../../shared/managed_table'; const INITIAL_PAGE_SIZE = 25; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 6f3b5c71af8109..9e5508a5810df7 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -26,7 +26,7 @@ import { useApmParams } from '../../../hooks/use_apm_params'; import { AggregatedTransactionsBadge } from '../../shared/aggregated_transactions_badge'; import { useApmRouter } from '../../../hooks/use_apm_router'; import { useTimeRange } from '../../../hooks/use_time_range'; -import { replace } from '../../shared/Links/url_helpers'; +import { replace } from '../../shared/links/url_helpers'; /** * The height a chart should be if it's next to a table with 5 rows and a title. diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_columns.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_columns.tsx index aba1073bfe9c27..fea48dee5d6c05 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_columns.tsx @@ -11,8 +11,8 @@ import React from 'react'; import { asInteger } from '../../../../../common/utils/formatters'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { SparkPlot } from '../../../shared/charts/spark_plot'; -import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; -import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { ErrorDetailLink } from '../../../shared/links/apm/error_detail_link'; +import { TimestampTooltip } from '../../../shared/timestamp_tooltip'; import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; type ErrorGroupMainStatistics = diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index 28824b3b8a399d..d9658f9d5e0472 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -19,7 +19,7 @@ import { useApmServiceContext } from '../../../../context/apm_service/use_apm_se import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; +import { ErrorOverviewLink } from '../../../shared/links/apm/error_overview_link'; import { getTimeRangeComparison } from '../../../shared/time_comparison/get_time_range_comparison'; import { OverviewTableContainer } from '../../../shared/overview_table_container'; import { getColumns } from './get_columns'; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx index 853ea37112ad8f..ae543adf2b852c 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx @@ -25,8 +25,8 @@ import { asTransactionRate, } from '../../../../../common/utils/formatters'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { MetricOverviewLink } from '../../../shared/Links/apm/MetricOverviewLink'; -import { ServiceNodeMetricOverviewLink } from '../../../shared/Links/apm/ServiceNodeMetricOverviewLink'; +import { MetricOverviewLink } from '../../../shared/links/apm/metric_overview_link'; +import { ServiceNodeMetricOverviewLink } from '../../../shared/links/apm/service_node_metric_overview_link'; import { ListMetric } from '../../../shared/list_metric'; import { getLatencyColumnLabel } from '../../../shared/transactions_table/get_latency_column_label'; import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx index e5e460e3b28123..932f2f5d215e78 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx @@ -20,8 +20,8 @@ import { SERVICE_NODE_NAME } from '../../../../../../common/elasticsearch_fieldn import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; import { FETCH_STATUS } from '../../../../../hooks/use_fetcher'; import { pushNewItemToKueryBar } from '../../../../shared/kuery_bar/utils'; -import { useMetricOverviewHref } from '../../../../shared/Links/apm/MetricOverviewLink'; -import { useServiceNodeMetricOverviewHref } from '../../../../shared/Links/apm/ServiceNodeMetricOverviewLink'; +import { useMetricOverviewHref } from '../../../../shared/links/apm/metric_overview_link'; +import { useServiceNodeMetricOverviewHref } from '../../../../shared/links/apm/service_node_metric_overview_link'; import { useInstanceDetailsFetcher } from '../use_instance_details_fetcher'; import { getMenuSections } from './menu_sections'; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/menu_sections.ts b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/menu_sections.ts index 7e7f30065c9585..7a537125203ead 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/menu_sections.ts +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/menu_sections.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { IBasePath } from 'kibana/public'; import moment from 'moment'; import { APIReturnType } from '../../../../../services/rest/createCallApmApi'; -import { getInfraHref } from '../../../../shared/Links/InfraLink'; +import { getInfraHref } from '../../../../shared/links/infra_link'; import { Action, getNonEmptySections, diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx index 40283ae1929473..e5f3c7bcbee4e7 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx @@ -18,9 +18,9 @@ import { import { useApmParams } from '../../../hooks/use_apm_params'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { truncate } from '../../../utils/style'; -import { EmptyMessage } from '../../shared/EmptyMessage'; -import { ImpactBar } from '../../shared/ImpactBar'; -import { TransactionDetailLink } from '../../shared/Links/apm/transaction_detail_link'; +import { EmptyMessage } from '../../shared/empty_message'; +import { ImpactBar } from '../../shared/impact_bar'; +import { TransactionDetailLink } from '../../shared/links/apm/transaction_detail_link'; import { ITableColumn, ManagedTable } from '../../shared/managed_table'; import { ServiceLink } from '../../shared/service_link'; import { TruncateWithTooltip } from '../../shared/truncate_with_tooltip'; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx index 8430e620f59abe..1e4d1816bf84af 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx @@ -21,7 +21,7 @@ import { MockApmPluginContextWrapper, } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import * as useFetcherModule from '../../../../hooks/use_fetcher'; -import { fromQuery } from '../../../shared/Links/url_helpers'; +import { fromQuery } from '../../../shared/links/url_helpers'; import { getFormattedSelection, TransactionDistribution } from './index'; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx index a2f6fd493313f2..7d387a39f9334a 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx @@ -33,7 +33,7 @@ import { useWaterfallFetcher } from '../use_waterfall_fetcher'; import { WaterfallWithSummary } from '../waterfall_with_summary'; import { useTransactionDistributionChartData } from './use_transaction_distribution_chart_data'; -import { HeightRetainer } from '../../../shared/HeightRetainer'; +import { HeightRetainer } from '../../../shared/height_retainer'; import { ChartTitleToolTip } from '../../correlations/chart_title_tool_tip'; // Enforce min height so it's consistent across all tabs on the same level diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx index cf8d69410d26b0..45c9d30419e857 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx @@ -16,7 +16,7 @@ import { useApmRouter } from '../../../hooks/use_apm_router'; import { useTimeRange } from '../../../hooks/use_time_range'; import { AggregatedTransactionsBadge } from '../../shared/aggregated_transactions_badge'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; -import { replace } from '../../shared/Links/url_helpers'; +import { replace } from '../../shared/links/url_helpers'; import { TransactionDetailsTabs } from './transaction_details_tabs'; export function TransactionDetails() { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx index 9f7d2977dfa843..5e9f1f8149d9d7 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx @@ -18,7 +18,7 @@ import { useApmParams } from '../../../hooks/use_apm_params'; import { useTransactionTraceSamplesFetcher } from '../../../hooks/use_transaction_trace_samples_fetcher'; import { maybe } from '../../../../common/utils/maybe'; -import { fromQuery, push, toQuery } from '../../shared/Links/url_helpers'; +import { fromQuery, push, toQuery } from '../../shared/links/url_helpers'; import { failedTransactionsCorrelationsTab } from './failed_transactions_correlations_tab'; import { latencyCorrelationsTab } from './latency_correlations_tab'; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts b/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts index 4f26a5875347ca..c35de9d5109513 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts @@ -10,7 +10,7 @@ import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_ import { useApmParams } from '../../../hooks/use_apm_params'; import { useFetcher } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; -import { getWaterfall } from './waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers'; +import { getWaterfall } from './waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers'; const INITIAL_DATA = { errorDocs: [], diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx index 1c421032ac7d3d..f528ce17c02f03 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx @@ -17,14 +17,14 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; import type { ApmUrlParams } from '../../../../context/url_params_context/types'; -import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; -import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; -import { TransactionSummary } from '../../../shared/Summary/TransactionSummary'; -import { TransactionActionMenu } from '../../../shared/transaction_action_menu/TransactionActionMenu'; +import { fromQuery, toQuery } from '../../../shared/links/url_helpers'; +import { LoadingStatePrompt } from '../../../shared/loading_state_prompt'; +import { TransactionSummary } from '../../../shared/summary/transaction_summary'; +import { TransactionActionMenu } from '../../../shared/transaction_action_menu/transaction_action_menu'; import type { TraceSample } from '../../../../hooks/use_transaction_trace_samples_fetcher'; -import { MaybeViewTraceLink } from './MaybeViewTraceLink'; -import { TransactionTabs } from './TransactionTabs'; -import { IWaterfall } from './waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers'; +import { MaybeViewTraceLink } from './maybe_view_trace_link'; +import { TransactionTabs } from './transaction_tabs'; +import { IWaterfall } from './waterfall_container/waterfall/waterfall_helpers/waterfall_helpers'; import { useApmParams } from '../../../../hooks/use_apm_params'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/MaybeViewTraceLink.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/maybe_view_trace_link.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/MaybeViewTraceLink.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/maybe_view_trace_link.tsx index b146afae539079..dfc89f78e4b3b7 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/MaybeViewTraceLink.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/maybe_view_trace_link.tsx @@ -11,8 +11,8 @@ import React from 'react'; import { getNextEnvironmentUrlParam } from '../../../../../common/environment_filter_values'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { Transaction as ITransaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { TransactionDetailLink } from '../../../shared/Links/apm/transaction_detail_link'; -import { IWaterfall } from './waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers'; +import { TransactionDetailLink } from '../../../shared/links/apm/transaction_detail_link'; +import { IWaterfall } from './waterfall_container/waterfall/waterfall_helpers/waterfall_helpers'; import { Environment } from '../../../../../common/environment_rt'; export function MaybeViewTraceLink({ diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/PercentOfParent.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/percent_of_parent.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/PercentOfParent.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/percent_of_parent.tsx diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/TransactionTabs.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/TransactionTabs.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx index 0e01c44b3fb5a5..c5001e25b0801d 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/TransactionTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx @@ -12,10 +12,10 @@ import { useHistory } from 'react-router-dom'; import { LogStream } from '../../../../../../infra/public'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import type { ApmUrlParams } from '../../../../context/url_params_context/types'; -import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; -import { TransactionMetadata } from '../../../shared/MetadataTable/TransactionMetadata'; +import { fromQuery, toQuery } from '../../../shared/links/url_helpers'; +import { TransactionMetadata } from '../../../shared/metadata_table/transaction_metadata'; import { WaterfallContainer } from './waterfall_container'; -import { IWaterfall } from './waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers'; +import { IWaterfall } from './waterfall_container/waterfall/waterfall_helpers/waterfall_helpers'; interface Props { transaction: Transaction; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx index 6ef7651a1e4048..71e4b6a6f1aad2 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx @@ -11,9 +11,9 @@ import type { ApmUrlParams } from '../../../../../context/url_params_context/typ import { IWaterfall, WaterfallLegendType, -} from './Waterfall/waterfall_helpers/waterfall_helpers'; -import { Waterfall } from './Waterfall'; -import { WaterfallLegends } from './WaterfallLegends'; +} from './waterfall/waterfall_helpers/waterfall_helpers'; +import { Waterfall } from './waterfall'; +import { WaterfallLegends } from './waterfall_legends'; import { useApmServiceContext } from '../../../../../context/apm_service/use_apm_service_context'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_agent_marks.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_agent_marks.test.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_agent_marks.test.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_agent_marks.test.ts diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_agent_marks.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_agent_marks.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_agent_marks.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_agent_marks.ts diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_error_marks.test.ts similarity index 97% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks.test.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_error_marks.test.ts index 62507efc64401f..5331ae1ae3d36a 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks.test.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_error_marks.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IWaterfallError } from '../Waterfall/waterfall_helpers/waterfall_helpers'; +import { IWaterfallError } from '../waterfall/waterfall_helpers/waterfall_helpers'; import { getErrorMarks } from './get_error_marks'; describe('getErrorMarks', () => { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_error_marks.ts similarity index 93% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_error_marks.ts index 1012c3dfa6fd15..25baf42572d063 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_error_marks.ts @@ -7,7 +7,7 @@ import { isEmpty } from 'lodash'; import { ErrorRaw } from '../../../../../../../typings/es_schemas/raw/error_raw'; -import { IWaterfallError } from '../Waterfall/waterfall_helpers/waterfall_helpers'; +import { IWaterfallError } from '../waterfall/waterfall_helpers/waterfall_helpers'; import { Mark } from '.'; export interface ErrorMark extends Mark { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/index.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/marks/index.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/index.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/marks/index.ts diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/accordion_waterfall.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/accordion_waterfall.tsx index 25b306723b2d20..6ca1a78db28b19 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/accordion_waterfall.tsx @@ -15,7 +15,7 @@ import { } from '@elastic/eui'; import React, { Dispatch, SetStateAction, useState } from 'react'; import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; -import { Margins } from '../../../../../shared/charts/Timeline'; +import { Margins } from '../../../../../shared/charts/timeline'; import { IWaterfall, IWaterfallSpanOrTransaction, diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/failure_badge.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/failure_badge.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/failure_badge.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/failure_badge.tsx diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/FlyoutTopLevelProperties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/flyout_top_level_properties.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/FlyoutTopLevelProperties.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/flyout_top_level_properties.tsx index c81dfb6283c94d..20e278000266a9 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/FlyoutTopLevelProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/flyout_top_level_properties.tsx @@ -15,7 +15,7 @@ import { getNextEnvironmentUrlParam } from '../../../../../../../common/environm import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; import { useLegacyUrlParams } from '../../../../../../context/url_params_context/use_url_params'; import { useApmParams } from '../../../../../../hooks/use_apm_params'; -import { TransactionDetailLink } from '../../../../../shared/Links/apm/transaction_detail_link'; +import { TransactionDetailLink } from '../../../../../shared/links/apm/transaction_detail_link'; import { ServiceLink } from '../../../../../shared/service_link'; import { StickyProperties } from '../../../../../shared/sticky_properties'; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/index.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/index.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/index.tsx index 50ec934961e1ae..c64a121c91882a 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/index.tsx @@ -11,10 +11,10 @@ import { History } from 'history'; import React, { useState } from 'react'; import { useHistory } from 'react-router-dom'; import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; -import { Timeline } from '../../../../../shared/charts/Timeline'; -import { fromQuery, toQuery } from '../../../../../shared/Links/url_helpers'; -import { getAgentMarks } from '../Marks/get_agent_marks'; -import { getErrorMarks } from '../Marks/get_error_marks'; +import { Timeline } from '../../../../../shared/charts/timeline'; +import { fromQuery, toQuery } from '../../../../../shared/links/url_helpers'; +import { getAgentMarks } from '../marks/get_agent_marks'; +import { getErrorMarks } from '../marks/get_error_marks'; import { AccordionWaterfall } from './accordion_waterfall'; import { WaterfallFlyout } from './waterfall_flyout'; import { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/ResponsiveFlyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/responsive_flyout.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/ResponsiveFlyout.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/responsive_flyout.tsx diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/index.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx index 457daef851bcfb..57bfc2b61fc536 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx @@ -22,18 +22,18 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; -import { CompositeSpanDurationSummaryItem } from '../../../../../../shared/Summary/CompositeSpanDurationSummaryItem'; +import { CompositeSpanDurationSummaryItem } from '../../../../../../shared/summary/composite_span_duration_summary_item'; import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; -import { DiscoverSpanLink } from '../../../../../../shared/Links/DiscoverLinks/DiscoverSpanLink'; -import { SpanMetadata } from '../../../../../../shared/MetadataTable/SpanMetadata'; -import { Stacktrace } from '../../../../../../shared/Stacktrace'; -import { Summary } from '../../../../../../shared/Summary'; -import { DurationSummaryItem } from '../../../../../../shared/Summary/DurationSummaryItem'; -import { HttpInfoSummaryItem } from '../../../../../../shared/Summary/http_info_summary_item'; -import { TimestampTooltip } from '../../../../../../shared/TimestampTooltip'; -import { ResponsiveFlyout } from '../ResponsiveFlyout'; +import { DiscoverSpanLink } from '../../../../../../shared/links/discover_links/discover_span_link'; +import { SpanMetadata } from '../../../../../../shared/metadata_table/span_metadata'; +import { Stacktrace } from '../../../../../../shared/stacktrace'; +import { Summary } from '../../../../../../shared/summary'; +import { DurationSummaryItem } from '../../../../../../shared/summary/duration_summary_item'; +import { HttpInfoSummaryItem } from '../../../../../../shared/summary/http_info_summary_item'; +import { TimestampTooltip } from '../../../../../../shared/timestamp_tooltip'; +import { ResponsiveFlyout } from '../responsive_flyout'; import { SyncBadge } from '../sync_badge'; import { SpanDatabase } from './span_db'; import { StickySpanProperties } from './sticky_span_properties'; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/span_db.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/span_db.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/span_db.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/span_db.tsx diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/span_flyout.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/span_flyout.stories.tsx similarity index 99% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/span_flyout.stories.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/span_flyout.stories.tsx index 33a0069e19ef17..f87967f762f25e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/span_flyout.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/span_flyout.stories.tsx @@ -14,7 +14,7 @@ import { SpanFlyout } from './'; type Args = ComponentProps; export default { - title: 'app/TransactionDetails/Waterfall/SpanFlyout', + title: 'app/TransactionDetails/waterfall/SpanFlyout', component: SpanFlyout, decorators: [ (StoryComponent: ComponentType) => { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/sticky_span_properties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx similarity index 99% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/sticky_span_properties.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx index 9e7a32a6808ec9..3067b335f4861b 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/sticky_span_properties.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx @@ -23,7 +23,7 @@ import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; import { useApmParams } from '../../../../../../../hooks/use_apm_params'; import { BackendLink } from '../../../../../../shared/backend_link'; -import { TransactionDetailLink } from '../../../../../../shared/Links/apm/transaction_detail_link'; +import { TransactionDetailLink } from '../../../../../../shared/links/apm/transaction_detail_link'; import { ServiceLink } from '../../../../../../shared/service_link'; import { StickyProperties } from '../../../../../../shared/sticky_properties'; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/truncate_height_section.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/truncate_height_section.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/truncate_height_section.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/truncate_height_section.tsx diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/sync_badge.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/sync_badge.stories.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/sync_badge.stories.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/sync_badge.stories.tsx diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/sync_badge.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/sync_badge.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/sync_badge.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/sync_badge.tsx diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/DroppedSpansWarning.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/dropped_spans_warning.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/DroppedSpansWarning.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/dropped_spans_warning.tsx diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/index.tsx similarity index 85% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/index.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/index.tsx index 6468e6ed1e2c8c..5f1e0cacd8483c 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/index.tsx @@ -18,12 +18,12 @@ import { import { i18n } from '@kbn/i18n'; import React from 'react'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; -import { TransactionActionMenu } from '../../../../../../shared/transaction_action_menu/TransactionActionMenu'; -import { TransactionSummary } from '../../../../../../shared/Summary/TransactionSummary'; -import { FlyoutTopLevelProperties } from '../FlyoutTopLevelProperties'; -import { ResponsiveFlyout } from '../ResponsiveFlyout'; -import { TransactionMetadata } from '../../../../../../shared/MetadataTable/TransactionMetadata'; -import { DroppedSpansWarning } from './DroppedSpansWarning'; +import { TransactionActionMenu } from '../../../../../../shared/transaction_action_menu/transaction_action_menu'; +import { TransactionSummary } from '../../../../../../shared/summary/transaction_summary'; +import { FlyoutTopLevelProperties } from '../flyout_top_level_properties'; +import { ResponsiveFlyout } from '../responsive_flyout'; +import { TransactionMetadata } from '../../../../../../shared/metadata_table/transaction_metadata'; +import { DroppedSpansWarning } from './dropped_spans_warning'; interface Props { onClose: () => void; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/transaction_flyout.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/transaction_flyout.stories.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/transaction_flyout.stories.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/transaction_flyout.stories.tsx index 33f1de91b61cca..7f8fbf62130b3d 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/transaction_flyout.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/transaction_flyout.stories.tsx @@ -14,7 +14,7 @@ import { TransactionFlyout } from './'; type Args = ComponentProps; export default { - title: 'app/TransactionDetails/Waterfall/TransactionFlyout', + title: 'app/TransactionDetails/waterfall/TransactionFlyout', component: TransactionFlyout, decorators: [ (StoryComponent: ComponentType) => { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_flyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_flyout.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_flyout.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_flyout.tsx diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/mock_responses/spans.json b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/mock_responses/spans.json similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/mock_responses/spans.json rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/mock_responses/spans.json diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/mock_responses/transaction.json b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/mock_responses/transaction.json similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/mock_responses/transaction.json rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/mock_responses/transaction.json diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers.test.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers.test.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers.test.ts diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers.ts diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx similarity index 99% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx index c9e6e08ac759fe..056a2847c68bf8 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx @@ -16,7 +16,7 @@ import { TRANSACTION_ID, } from '../../../../../../../common/elasticsearch_fieldnames'; import { asDuration } from '../../../../../../../common/utils/formatters'; -import { Margins } from '../../../../../shared/charts/Timeline'; +import { Margins } from '../../../../../shared/charts/timeline'; import { TruncateWithTooltip } from '../../../../../shared/truncate_with_tooltip'; import { SyncBadge } from './sync_badge'; import { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers'; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx index 895b83136a0978..312412a8cf8273 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx @@ -10,7 +10,7 @@ import React, { ComponentProps } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; import { WaterfallContainer } from './index'; -import { getWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers'; +import { getWaterfall } from './waterfall/waterfall_helpers/waterfall_helpers'; import { inferredSpans, manyChildrenWithSameLength, @@ -23,7 +23,7 @@ import { type Args = ComponentProps; const stories: Meta = { - title: 'app/TransactionDetails/Waterfall', + title: 'app/TransactionDetails/waterfall', component: WaterfallContainer, decorators: [ (StoryComponent) => ( diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallLegends.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_legends.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallLegends.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_legends.tsx index aaa9b3e45ee22f..30d4b0049d1bd2 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallLegends.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_legends.tsx @@ -10,11 +10,11 @@ import { EuiFlexItem } from '@elastic/eui'; import { EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { Legend } from '../../../../shared/charts/Timeline/legend'; +import { Legend } from '../../../../shared/charts/timeline/legend'; import { IWaterfallLegend, WaterfallLegendType, -} from './Waterfall/waterfall_helpers/waterfall_helpers'; +} from './waterfall/waterfall_helpers/waterfall_helpers'; interface Props { legends: IWaterfallLegend[]; diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index e1d6fb65bbc569..39d522ca088fcf 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -13,7 +13,7 @@ import { useApmParams } from '../../../hooks/use_apm_params'; import { useTimeRange } from '../../../hooks/use_time_range'; import { AggregatedTransactionsBadge } from '../../shared/aggregated_transactions_badge'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; -import { replace } from '../../shared/Links/url_helpers'; +import { replace } from '../../shared/links/url_helpers'; import { TransactionsTable } from '../../shared/transactions_table'; export function TransactionOverview() { diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx index a1b24fc5166642..ede47a65d7ea56 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx @@ -21,7 +21,7 @@ import { disableConsoleWarning, renderWithTheme, } from '../../../utils/testHelpers'; -import { fromQuery } from '../../shared/Links/url_helpers'; +import { fromQuery } from '../../shared/links/url_helpers'; import { TransactionOverview } from './'; const KibanaReactContext = createKibanaReactContext({ diff --git a/x-pack/plugins/apm/public/components/routing/app_root.tsx b/x-pack/plugins/apm/public/components/routing/app_root.tsx index 5750146bef722b..c7b98743c6bea9 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root.tsx @@ -20,7 +20,7 @@ import { HeaderMenuPortal, InspectorContextProvider, } from '../../../../observability/public'; -import { ScrollToTopOnPathChange } from '../../components/app/Main/ScrollToTopOnPathChange'; +import { ScrollToTopOnPathChange } from '../../components/app/main/ScrollToTopOnPathChange'; import { AnomalyDetectionJobsContextProvider } from '../../context/anomaly_detection_jobs/anomaly_detection_jobs_context'; import { ApmPluginContext, diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx index d8a996d2163bca..713292c6338912 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx @@ -27,6 +27,7 @@ import { TransactionDetails } from '../../app/transaction_details'; import { ServiceProfiling } from '../../app/service_profiling'; import { ServiceDependencies } from '../../app/service_dependencies'; import { ServiceLogs } from '../../app/service_logs'; +import { InfraOverview } from '../../app/infra_overview'; function page({ path, @@ -265,6 +266,17 @@ export const serviceDetail = { }), element: , }), + page({ + path: '/services/{serviceName}/infra', + tab: 'infra', + title: i18n.translate('xpack.apm.views.infra.title', { + defaultMessage: 'Infrastructure', + }), + element: , + searchBarOptions: { + hidden: true, + }, + }), { path: '/services/{serviceName}/', element: , diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx index 710b4b6f71a35f..fcc69c5055bad4 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx @@ -15,7 +15,7 @@ import { import { EnvironmentsContextProvider } from '../../../context/environments_context/environments_context'; import { useFetcher } from '../../../hooks/use_fetcher'; import { ApmPluginStartDeps } from '../../../plugin'; -import { ApmEnvironmentFilter } from '../../shared/EnvironmentFilter'; +import { ApmEnvironmentFilter } from '../../shared/environment_filter'; import { getNoDataConfig } from './no_data_config'; // Paths that must skip the no data screen diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx index 0b97301b4e7c52..962fbb4eb6be6d 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx @@ -42,6 +42,7 @@ type Tab = NonNullable[0] & { | 'errors' | 'metrics' | 'nodes' + | 'infra' | 'service-map' | 'logs' | 'profiling'; @@ -240,6 +241,16 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { }), hidden: isJVMsTabHidden({ agentName, runtimeName }), }, + { + key: 'infra', + href: router.link('/services/{serviceName}/infra', { + path: { serviceName }, + query, + }), + label: i18n.translate('xpack.apm.home.infraTabLabel', { + defaultMessage: 'Infrastructure', + }), + }, { key: 'service-map', href: router.link('/services/{serviceName}/service-map', { diff --git a/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx index fc87b24579695b..43a865c0584c90 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx @@ -13,7 +13,7 @@ import { useHistory } from 'react-router-dom'; import { CoreStart } from 'kibana/public'; import { ApmMainTemplate } from './apm_main_template'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; -import { getLegacyApmHref } from '../../shared/Links/apm/APMLink'; +import { getLegacyApmHref } from '../../shared/links/apm/apm_link'; type Tab = NonNullable[0] & { key: diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx index e1bda5475acc49..40335f0cab61d2 100644 --- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx @@ -19,7 +19,7 @@ import { useAnomalyDetectionJobsContext } from '../../../context/anomaly_detecti import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useTheme } from '../../../hooks/use_theme'; -import { getLegacyApmHref } from '../Links/apm/APMLink'; +import { getLegacyApmHref } from '../links/apm/apm_link'; export function AnomalyDetectionSetupLink() { const { query } = useApmParams('/*'); diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/index.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/index.tsx index 655fc2da2b097c..158705970640c1 100644 --- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/index.tsx @@ -9,7 +9,7 @@ import { EuiHeaderLink, EuiHeaderLinks } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { getAlertingCapabilities } from '../../alerting/get_alerting_capabilities'; -import { getLegacyApmHref } from '../Links/apm/APMLink'; +import { getLegacyApmHref } from '../links/apm/apm_link'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { AlertingPopoverAndFlyout } from './alerting_popover_flyout'; import { AnomalyDetectionSetupLink } from './anomaly_detection_setup_link'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts index 9dccddd509387b..d203bd8cfe0228 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts @@ -8,7 +8,7 @@ import { XYBrushEvent } from '@elastic/charts'; import { History } from 'history'; import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; -import { fromQuery, toQuery } from '../../Links/url_helpers'; +import { fromQuery, toQuery } from '../../links/url_helpers'; export const onBrushEnd = ({ x, diff --git a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx index 30bf5908ef3767..0e9ad848648630 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx @@ -31,7 +31,7 @@ import { import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useTheme } from '../../../../hooks/use_theme'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import * as urlHelpers from '../../Links/url_helpers'; +import * as urlHelpers from '../../links/url_helpers'; import { ChartContainer } from '../chart_container'; import { getResponseTimeTickFormatter } from '../transaction_charts/helper'; import { CustomTooltip } from './custom_tooltip'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx index 0b4eca8b9373c7..d654cb9c0f5d36 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx @@ -24,7 +24,7 @@ import { getResponseTimeTickFormatter, } from '../../../shared/charts/transaction_charts/helper'; import { MLHeader } from '../../../shared/charts/transaction_charts/ml_header'; -import * as urlHelpers from '../../../shared/Links/url_helpers'; +import * as urlHelpers from '../../../shared/links/url_helpers'; import { getComparisonChartTheme } from '../../time_comparison/get_time_range_comparison'; import { useEnvironmentsContext } from '../../../../context/environments_context/use_environments_context'; import { ApmMlDetectorType } from '../../../../../common/anomaly_detection/apm_ml_detectors'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Timeline.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeline/Timeline.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/Timeline.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/timeline/Timeline.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/charts/timeline/__snapshots__/Timeline.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/charts/timeline/__snapshots__/Timeline.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeline/index.tsx similarity index 89% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/timeline/index.tsx index 6c7cb7a067d2e4..4a56f1a28e4bef 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeline/index.tsx @@ -8,11 +8,11 @@ import PropTypes from 'prop-types'; import React, { PureComponent, ReactNode } from 'react'; import { makeWidthFlexible } from 'react-vis'; -import { AgentMark } from '../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_agent_marks'; -import { ErrorMark } from '../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks'; -import { getPlotValues } from './plotUtils'; +import { AgentMark } from '../../../app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_agent_marks'; +import { ErrorMark } from '../../../app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_error_marks'; +import { getPlotValues } from './plot_utils'; import { TimelineAxis } from './timeline_axis'; -import { VerticalLines } from './VerticalLines'; +import { VerticalLines } from './vertical_lines'; export type Mark = AgentMark | ErrorMark; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/LastTickValue.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeline/last_tick_value.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/LastTickValue.tsx rename to x-pack/plugins/apm/public/components/shared/charts/timeline/last_tick_value.tsx diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/legend.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeline/legend.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/legend.tsx rename to x-pack/plugins/apm/public/components/shared/charts/timeline/legend.tsx diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__snapshots__/agent_marker.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/charts/timeline/marker/__snapshots__/agent_marker.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__snapshots__/agent_marker.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/charts/timeline/marker/__snapshots__/agent_marker.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__snapshots__/index.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/charts/timeline/marker/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/charts/timeline/marker/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/agent_marker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeline/marker/agent_marker.test.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/agent_marker.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/timeline/marker/agent_marker.test.tsx index 0b7e405ae5d953..e421190d6793cc 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/agent_marker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeline/marker/agent_marker.test.tsx @@ -8,7 +8,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { AgentMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_agent_marks'; +import { AgentMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_agent_marks'; import { AgentMarker } from './agent_marker'; describe('AgentMarker', () => { diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/agent_marker.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeline/marker/agent_marker.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/agent_marker.tsx rename to x-pack/plugins/apm/public/components/shared/charts/timeline/marker/agent_marker.tsx index 947c7a93f38b14..9e97129b199dbc 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/agent_marker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeline/marker/agent_marker.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { asDuration } from '../../../../../../common/utils/formatters'; import { useTheme } from '../../../../../hooks/use_theme'; -import { AgentMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_agent_marks'; +import { AgentMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_agent_marks'; import { Legend } from '../legend'; const NameContainer = euiStyled.div` diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/error_marker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeline/marker/error_marker.test.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/error_marker.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/timeline/marker/error_marker.test.tsx index cef97c46fd2e23..d3b378ab5cb0bb 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/error_marker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeline/marker/error_marker.test.tsx @@ -14,7 +14,7 @@ import { expectTextsInDocument, renderWithTheme, } from '../../../../../utils/testHelpers'; -import { ErrorMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks'; +import { ErrorMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_error_marks'; import { ErrorMarker } from './error_marker'; function Wrapper({ children }: { children?: ReactNode }) { diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/error_marker.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeline/marker/error_marker.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/error_marker.tsx rename to x-pack/plugins/apm/public/components/shared/charts/timeline/marker/error_marker.tsx index a1e3f42cb56842..3f27ddbf78af47 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/error_marker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeline/marker/error_marker.tsx @@ -15,8 +15,8 @@ import { import { asDuration } from '../../../../../../common/utils/formatters'; import { useLegacyUrlParams } from '../../../../../context/url_params_context/use_url_params'; import { useTheme } from '../../../../../hooks/use_theme'; -import { ErrorMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks'; -import { ErrorDetailLink } from '../../../Links/apm/ErrorDetailLink'; +import { ErrorMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_error_marks'; +import { ErrorDetailLink } from '../../../links/apm/error_detail_link'; import { Legend, Shape } from '../legend'; interface Props { diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeline/marker/index.test.tsx similarity index 90% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/timeline/marker/index.test.tsx index f2c8f9e3b318de..698801b5a8d37c 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeline/marker/index.test.tsx @@ -8,8 +8,8 @@ import { shallow } from 'enzyme'; import React from 'react'; import { Marker } from './'; -import { AgentMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_agent_marks'; -import { ErrorMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks'; +import { AgentMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_agent_marks'; +import { ErrorMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_error_marks'; describe('Marker', () => { it('renders agent marker', () => { diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeline/marker/index.tsx similarity index 89% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/timeline/marker/index.tsx index 88318eb0e0c2d5..e4e26ce637bfe7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeline/marker/index.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { AgentMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_agent_marks'; -import { ErrorMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks'; +import { AgentMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_agent_marks'; +import { ErrorMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_error_marks'; import { AgentMarker } from './agent_marker'; import { ErrorMarker } from './error_marker'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/plotUtils.ts b/x-pack/plugins/apm/public/components/shared/charts/timeline/plot_utils.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/plotUtils.ts rename to x-pack/plugins/apm/public/components/shared/charts/timeline/plot_utils.ts diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/timeline_axis.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeline/timeline_axis.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/timeline_axis.tsx rename to x-pack/plugins/apm/public/components/shared/charts/timeline/timeline_axis.tsx index cf942ebb757763..4dddb4bee226df 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/timeline_axis.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeline/timeline_axis.tsx @@ -11,9 +11,9 @@ import { XAxis, XYPlot } from 'react-vis'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; import { useTheme } from '../../../../hooks/use_theme'; import { Mark } from './'; -import { LastTickValue } from './LastTickValue'; -import { Marker } from './Marker'; -import { PlotValues } from './plotUtils'; +import { LastTickValue } from './last_tick_value'; +import { Marker } from './marker'; +import { PlotValues } from './plot_utils'; // Remove any tick that is too close to topTraceDuration const getXAxisTickValues = ( diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeline/vertical_lines.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx rename to x-pack/plugins/apm/public/components/shared/charts/timeline/vertical_lines.tsx index 2dcc969f661b87..6f07efeed7c46b 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeline/vertical_lines.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { VerticalGridLines, XYPlot } from 'react-vis'; import { useTheme } from '../../../../hooks/use_theme'; -import { Mark } from '../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks'; -import { PlotValues } from './plotUtils'; +import { Mark } from '../../../app/transaction_details/waterfall_with_summary/waterfall_container/marks'; +import { PlotValues } from './plot_utils'; interface VerticalLinesProps { marks?: Mark[]; diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx index 76e85b1d9998d7..0ae6fc00b28044 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useApmParams } from '../../../../hooks/use_apm_params'; -import { MLSingleMetricLink } from '../../Links/MachineLearningLinks/MLSingleMetricLink'; +import { MLSingleMetricLink } from '../../links/machine_learning_links/mlsingle_metric_link'; interface Props { hasValidMlLicense?: boolean; diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/date_picker.test.tsx b/x-pack/plugins/apm/public/components/shared/date_picker/date_picker.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/DatePicker/date_picker.test.tsx rename to x-pack/plugins/apm/public/components/shared/date_picker/date_picker.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx b/x-pack/plugins/apm/public/components/shared/date_picker/index.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx rename to x-pack/plugins/apm/public/components/shared/date_picker/index.tsx index 12cc137d621423..c792c4ae9b426d 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/date_picker/index.tsx @@ -11,7 +11,7 @@ import { useHistory, useLocation } from 'react-router-dom'; import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { clearCache } from '../../../services/rest/callApi'; -import { fromQuery, toQuery } from '../Links/url_helpers'; +import { fromQuery, toQuery } from '../links/url_helpers'; import { TimePickerQuickRange } from './typings'; export function DatePicker({ diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/typings.ts b/x-pack/plugins/apm/public/components/shared/date_picker/typings.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/DatePicker/typings.ts rename to x-pack/plugins/apm/public/components/shared/date_picker/typings.ts diff --git a/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx index 8791075158c95d..7c2bc722ac1e6b 100644 --- a/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx @@ -21,8 +21,8 @@ import { } from '../../../../common/utils/formatters'; import { useBreakpoints } from '../../../hooks/use_breakpoints'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { EmptyMessage } from '../EmptyMessage'; -import { ImpactBar } from '../ImpactBar'; +import { EmptyMessage } from '../empty_message'; +import { ImpactBar } from '../impact_bar'; import { ListMetric } from '../list_metric'; import { ITableColumn, ManagedTable } from '../managed_table'; import { OverviewTableContainer } from '../overview_table_container'; diff --git a/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx b/x-pack/plugins/apm/public/components/shared/empty_message.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx rename to x-pack/plugins/apm/public/components/shared/empty_message.tsx diff --git a/x-pack/plugins/apm/public/components/shared/EnvironmentBadge/index.tsx b/x-pack/plugins/apm/public/components/shared/environment_badge/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/EnvironmentBadge/index.tsx rename to x-pack/plugins/apm/public/components/shared/environment_badge/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/environment_filter/index.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx rename to x-pack/plugins/apm/public/components/shared/environment_filter/index.tsx index 4344ed3a80f649..a7a8c90c31c165 100644 --- a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/environment_filter/index.tsx @@ -15,7 +15,7 @@ import { ENVIRONMENT_NOT_DEFINED, } from '../../../../common/environment_filter_values'; import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; -import { fromQuery, toQuery } from '../Links/url_helpers'; +import { fromQuery, toQuery } from '../links/url_helpers'; import { useUxUrlParams } from '../../../context/url_params_context/use_ux_url_params'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { Environment } from '../../../../common/environment_rt'; diff --git a/x-pack/plugins/apm/public/components/shared/ErrorStatePrompt.tsx b/x-pack/plugins/apm/public/components/shared/error_state_prompt.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/ErrorStatePrompt.tsx rename to x-pack/plugins/apm/public/components/shared/error_state_prompt.tsx diff --git a/x-pack/plugins/apm/public/components/shared/HeightRetainer/index.tsx b/x-pack/plugins/apm/public/components/shared/height_retainer/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/HeightRetainer/index.tsx rename to x-pack/plugins/apm/public/components/shared/height_retainer/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap b/x-pack/plugins/apm/public/components/shared/impact_bar/__snapshots__/impact_bar.test.js.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap rename to x-pack/plugins/apm/public/components/shared/impact_bar/__snapshots__/impact_bar.test.js.snap diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/ImpactBar.test.js b/x-pack/plugins/apm/public/components/shared/impact_bar/impact_bar.test.js similarity index 100% rename from x-pack/plugins/apm/public/components/shared/ImpactBar/ImpactBar.test.js rename to x-pack/plugins/apm/public/components/shared/impact_bar/impact_bar.test.js diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx b/x-pack/plugins/apm/public/components/shared/impact_bar/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx rename to x-pack/plugins/apm/public/components/shared/impact_bar/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx b/x-pack/plugins/apm/public/components/shared/key_value_table/formatted_value.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx rename to x-pack/plugins/apm/public/components/shared/key_value_table/formatted_value.tsx diff --git a/x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx b/x-pack/plugins/apm/public/components/shared/key_value_table/index.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx rename to x-pack/plugins/apm/public/components/shared/key_value_table/index.tsx index e9525728bc3c57..8b5595a67fa78b 100644 --- a/x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/key_value_table/index.tsx @@ -13,7 +13,7 @@ import { EuiTableRow, EuiTableRowCell, } from '@elastic/eui'; -import { FormattedValue } from './FormattedValue'; +import { FormattedValue } from './formatted_value'; import { KeyValuePair } from '../../../utils/flattenObject'; export function KeyValueTable({ diff --git a/x-pack/plugins/apm/public/components/shared/KeyValueTable/KeyValueTable.test.tsx b/x-pack/plugins/apm/public/components/shared/key_value_table/key_value_table.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/KeyValueTable/KeyValueTable.test.tsx rename to x-pack/plugins/apm/public/components/shared/key_value_table/key_value_table.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx b/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx index 20484e691d50b8..a04d3218f9fffd 100644 --- a/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx @@ -19,10 +19,10 @@ import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_ import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useDynamicDataViewFetcher } from '../../../hooks/use_dynamic_data_view'; -import { fromQuery, toQuery } from '../Links/url_helpers'; +import { fromQuery, toQuery } from '../links/url_helpers'; import { getBoolFilter } from './get_bool_filter'; // @ts-expect-error -import { Typeahead } from './Typeahead'; +import { Typeahead } from './typeahead'; import { useProcessorEvent } from './use_processor_event'; interface State { diff --git a/x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/ClickOutside.js b/x-pack/plugins/apm/public/components/shared/kuery_bar/typeahead/click_outside.js similarity index 100% rename from x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/ClickOutside.js rename to x-pack/plugins/apm/public/components/shared/kuery_bar/typeahead/click_outside.js diff --git a/x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/index.js b/x-pack/plugins/apm/public/components/shared/kuery_bar/typeahead/index.js similarity index 98% rename from x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/index.js rename to x-pack/plugins/apm/public/components/shared/kuery_bar/typeahead/index.js index f8f51679a5192a..d06bfcceab9840 100644 --- a/x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/index.js +++ b/x-pack/plugins/apm/public/components/shared/kuery_bar/typeahead/index.js @@ -7,8 +7,8 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import Suggestions from './Suggestions'; -import ClickOutside from './ClickOutside'; +import Suggestions from './suggestions'; +import ClickOutside from './click_outside'; import { EuiFieldSearch, EuiProgress } from '@elastic/eui'; const KEY_CODES = { diff --git a/x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/Suggestion.js b/x-pack/plugins/apm/public/components/shared/kuery_bar/typeahead/suggestion.js similarity index 100% rename from x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/Suggestion.js rename to x-pack/plugins/apm/public/components/shared/kuery_bar/typeahead/suggestion.js diff --git a/x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/Suggestions.js b/x-pack/plugins/apm/public/components/shared/kuery_bar/typeahead/suggestions.js similarity index 98% rename from x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/Suggestions.js rename to x-pack/plugins/apm/public/components/shared/kuery_bar/typeahead/suggestions.js index 386eb7e1e0d7d9..e6f482e88afc0f 100644 --- a/x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/Suggestions.js +++ b/x-pack/plugins/apm/public/components/shared/kuery_bar/typeahead/suggestions.js @@ -11,7 +11,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { unit } from '../../../../utils/style'; -import Suggestion from './Suggestion'; +import Suggestion from './suggestion'; const List = euiStyled.ul` width: 100%; diff --git a/x-pack/plugins/apm/public/components/shared/kuery_bar/utils.ts b/x-pack/plugins/apm/public/components/shared/kuery_bar/utils.ts index 1d4dd04ce9df43..805e162c67fe6b 100644 --- a/x-pack/plugins/apm/public/components/shared/kuery_bar/utils.ts +++ b/x-pack/plugins/apm/public/components/shared/kuery_bar/utils.ts @@ -7,7 +7,7 @@ import { History } from 'history'; import { isEmpty } from 'lodash'; -import { push } from '../Links/url_helpers'; +import { push } from '../links/url_helpers'; export function pushNewItemToKueryBar({ kuery, diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/APMLink.test.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/APMLink.test.tsx index c422b78d661e40..ea10852c7d6683 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/apm/APMLink.test.tsx @@ -8,7 +8,7 @@ import { Location } from 'history'; import React from 'react'; import { getRenderedHref } from '../../../../utils/testHelpers'; -import { APMLink } from './APMLink'; +import { APMLink } from './apm_link'; describe('APMLink', () => { test('APMLink should produce the correct URL', async () => { diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/agent_configuration_links.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/agent_configuration_links.tsx index c45ab22682ef46..366003c101789d 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/apm/agent_configuration_links.tsx @@ -7,7 +7,7 @@ import { IBasePath } from 'kibana/public'; import { AgentConfigurationIntake } from '../../../../../common/agent_configuration/configuration_types'; -import { getLegacyApmHref } from './APMLink'; +import { getLegacyApmHref } from './apm_link'; export function editAgentConfigurationHref( configService: AgentConfigurationIntake['service'], diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/apm_link.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/apm_link.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorDetailLink.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/error_detail_link.tsx similarity index 91% rename from x-pack/plugins/apm/public/components/shared/Links/apm/ErrorDetailLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/error_detail_link.tsx index 75fd8ba1a5f11b..4bf744e29c1ee2 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorDetailLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/apm/error_detail_link.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { APMLink, APMLinkExtendProps } from './APMLink'; +import { APMLink, APMLinkExtendProps } from './apm_link'; interface Props extends APMLinkExtendProps { serviceName: string; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/error_overview_link.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/error_overview_link.tsx index ccd9bdff960b60..b517a39c1004df 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/apm/error_overview_link.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { pickKeys } from '../../../../../common/utils/pick_keys'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams } from '../url_helpers'; -import { APMLink, APMLinkExtendProps } from './APMLink'; +import { APMLink, APMLinkExtendProps } from './apm_link'; const persistedFilters: Array = [ 'host', diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/HomeLink.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/home_link.tsx similarity index 87% rename from x-pack/plugins/apm/public/components/shared/Links/apm/HomeLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/home_link.tsx index 66fdb7913b5d53..5337f1a2f16b9b 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/HomeLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/apm/home_link.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { APMLink, APMLinkExtendProps } from './APMLink'; +import { APMLink, APMLinkExtendProps } from './apm_link'; function HomeLink(props: APMLinkExtendProps) { return ; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/metric_overview_link.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/metric_overview_link.tsx index c3d418b63426b5..9d9a013d1c4390 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/apm/metric_overview_link.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { APMQueryParams } from '../url_helpers'; -import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; +import { APMLink, APMLinkExtendProps, useAPMHref } from './apm_link'; const persistedFilters: Array = [ 'host', diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/service_inventory_link.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/service_inventory_link.tsx index 0a51cb4cacd0b5..c4a589dca3ad83 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/apm/service_inventory_link.tsx @@ -6,7 +6,7 @@ */ import { APMQueryParams } from '../url_helpers'; -import { useAPMHref } from './APMLink'; +import { useAPMHref } from './apm_link'; const persistedFilters: Array = ['host', 'agentName']; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/service_map_link.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/service_map_link.tsx index 2c4dec9f4bcbac..84eff7eb444bde 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/apm/service_map_link.tsx @@ -7,7 +7,7 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; -import { APMLinkExtendProps, useAPMHref } from './APMLink'; +import { APMLinkExtendProps, useAPMHref } from './apm_link'; export function useServiceMapHref(serviceName?: string) { const path = serviceName diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/service_node_metric_overview_link.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/service_node_metric_overview_link.tsx index aad5756b70e7e1..b8f0a0c53cb7dd 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/apm/service_node_metric_overview_link.tsx @@ -8,7 +8,7 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; import { APMQueryParams } from '../url_helpers'; -import { APMLinkExtendProps, useAPMHref } from './APMLink'; +import { APMLinkExtendProps, useAPMHref } from './apm_link'; interface Props extends APMLinkExtendProps { serviceName: string; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/service_node_overview_link.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/service_node_overview_link.tsx index b76c468ae21538..e13a38143ef259 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/apm/service_node_overview_link.tsx @@ -6,7 +6,7 @@ */ import { APMQueryParams } from '../url_helpers'; -import { useAPMHref } from './APMLink'; +import { useAPMHref } from './apm_link'; const persistedFilters: Array = [ 'host', diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_profiling_link.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/service_profiling_link.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/shared/Links/apm/service_profiling_link.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/service_profiling_link.tsx index ab3b085e4e2559..01e23cba48e04d 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/service_profiling_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/apm/service_profiling_link.tsx @@ -7,7 +7,7 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; -import { APMLinkExtendProps, useAPMHref } from './APMLink'; +import { APMLinkExtendProps, useAPMHref } from './apm_link'; interface ServiceProfilingLinkProps extends APMLinkExtendProps { serviceName: string; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.test.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/service_transactions_overview_link.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.test.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/service_transactions_overview_link.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/service_transactions_overview_link.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/service_transactions_overview_link.tsx index 982d25802e2021..b33f22cd9faf94 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/apm/service_transactions_overview_link.tsx @@ -8,7 +8,7 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; import { APMQueryParams } from '../url_helpers'; -import { APMLinkExtendProps, useAPMHref } from './APMLink'; +import { APMLinkExtendProps, useAPMHref } from './apm_link'; import { removeUndefinedProps } from '../../../../context/url_params_context/helpers'; const persistedFilters: Array = [ diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/trace_overview_link.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/trace_overview_link.tsx index 92682a88efb170..9353de8162b08a 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/apm/trace_overview_link.tsx @@ -6,7 +6,7 @@ */ import { APMQueryParams } from '../url_helpers'; -import { useAPMHref } from './APMLink'; +import { useAPMHref } from './apm_link'; const persistedFilters: Array = [ 'transactionResult', diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_detail_link.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/transaction_detail_link.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/shared/Links/apm/transaction_detail_link.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/transaction_detail_link.tsx index 9df035515b0402..af299c6095e59e 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_detail_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/apm/transaction_detail_link.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; import { EuiLink } from '@elastic/eui'; import { pickBy, identity } from 'lodash'; -import { getLegacyApmHref, APMLinkExtendProps } from './APMLink'; +import { getLegacyApmHref, APMLinkExtendProps } from './apm_link'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { pickKeys } from '../../../../../common/utils/pick_keys'; import { APMQueryParams } from '../url_helpers'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.test.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/transaction_overview_link.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.test.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/transaction_overview_link.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.tsx b/x-pack/plugins/apm/public/components/shared/links/apm/transaction_overview_link.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.tsx rename to x-pack/plugins/apm/public/components/shared/links/apm/transaction_overview_link.tsx index d3e40aebc1dafb..bd0ac78b855f0c 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/apm/transaction_overview_link.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; import { removeUndefinedProps } from '../../../../context/url_params_context/helpers'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; -import { APMLinkExtendProps, getLegacyApmHref } from './APMLink'; +import { APMLinkExtendProps, getLegacyApmHref } from './apm_link'; interface Props extends APMLinkExtendProps { serviceName: string; diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__fixtures__/mock_transaction.json b/x-pack/plugins/apm/public/components/shared/links/discover_links/__fixtures__/mock_transaction.json similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__fixtures__/mock_transaction.json rename to x-pack/plugins/apm/public/components/shared/links/discover_links/__fixtures__/mock_transaction.json diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverErrorButton.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/links/discover_links/__snapshots__/discover_error_button.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverErrorButton.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/links/discover_links/__snapshots__/discover_error_button.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverErrorLink.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/links/discover_links/__snapshots__/discover_error_link.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverErrorLink.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/links/discover_links/__snapshots__/discover_error_link.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/discover_transaction_button.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/links/discover_links/__snapshots__/discover_transaction_button.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/discover_transaction_button.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/links/discover_links/__snapshots__/discover_transaction_button.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverTransactionLink.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/links/discover_links/__snapshots__/discover_transaction_link.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverTransactionLink.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/links/discover_links/__snapshots__/discover_transaction_link.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.test.tsx b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_error_button.test.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/links/discover_links/discover_error_button.test.tsx index 3d70653d58d198..3cbe8d7e316322 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_error_button.test.tsx @@ -8,7 +8,7 @@ import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; -import { DiscoverErrorLink } from './DiscoverErrorLink'; +import { DiscoverErrorLink } from './discover_error_link'; describe('DiscoverErrorLink without kuery', () => { let wrapper: ShallowWrapper; diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorButton.test.tsx b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_error_link.test.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorButton.test.tsx rename to x-pack/plugins/apm/public/components/shared/links/discover_links/discover_error_link.test.tsx index 3d70653d58d198..3cbe8d7e316322 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorButton.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_error_link.test.tsx @@ -8,7 +8,7 @@ import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; -import { DiscoverErrorLink } from './DiscoverErrorLink'; +import { DiscoverErrorLink } from './discover_error_link'; describe('DiscoverErrorLink without kuery', () => { let wrapper: ShallowWrapper; diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_error_link.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/discover_links/discover_error_link.tsx index 467a6d604ee60b..c7d35e7c983e83 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_error_link.tsx @@ -11,7 +11,7 @@ import { SERVICE_NAME, } from '../../../../../common/elasticsearch_fieldnames'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; -import { DiscoverLink } from './DiscoverLink'; +import { DiscoverLink } from './discover_link'; function getDiscoverQuery(error: APMError, kuery?: string) { const serviceName = error.service.name; diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_link.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/discover_links/discover_link.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLinks.integration.test.tsx b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_links.integration.test.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLinks.integration.test.tsx rename to x-pack/plugins/apm/public/components/shared/links/discover_links/discover_links.integration.test.tsx index 53ea5735c8b22f..4e47a1d4217de8 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLinks.integration.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_links.integration.test.tsx @@ -11,9 +11,9 @@ import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; import { Span } from '../../../../../typings/es_schemas/ui/span'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { getRenderedHref } from '../../../../utils/testHelpers'; -import { DiscoverErrorLink } from './DiscoverErrorLink'; -import { DiscoverSpanLink } from './DiscoverSpanLink'; -import { DiscoverTransactionLink } from './DiscoverTransactionLink'; +import { DiscoverErrorLink } from './discover_error_link'; +import { DiscoverSpanLink } from './discover_span_link'; +import { DiscoverTransactionLink } from './discover_transaction_link'; describe('DiscoverLinks', () => { it('produces the correct URL for a transaction', async () => { diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_span_link.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/discover_links/discover_span_link.tsx index 631ac6e2129f02..f943e6fb49985b 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_span_link.tsx @@ -8,7 +8,7 @@ import React, { ReactNode } from 'react'; import { SPAN_ID } from '../../../../../common/elasticsearch_fieldnames'; import { Span } from '../../../../../typings/es_schemas/ui/span'; -import { DiscoverLink } from './DiscoverLink'; +import { DiscoverLink } from './discover_link'; function getDiscoverQuery(span: Span) { const query = `${SPAN_ID}:"${span.span.id}"`; diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/discover_transaction_button.test.tsx b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_transaction_button.test.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/discover_transaction_button.test.tsx rename to x-pack/plugins/apm/public/components/shared/links/discover_links/discover_transaction_button.test.tsx index b8278f3d285f4f..5505036087dbfc 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/discover_transaction_button.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_transaction_button.test.tsx @@ -11,7 +11,7 @@ import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { DiscoverTransactionLink, getDiscoverQuery, -} from './DiscoverTransactionLink'; +} from './discover_transaction_link'; import mockTransaction from './__fixtures__/mock_transaction.json'; describe('DiscoverTransactionLink component', () => { diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.test.tsx b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_transaction_link.test.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/links/discover_links/discover_transaction_link.test.tsx index a73f569827aa53..498a7fd26c138e 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_transaction_link.test.tsx @@ -8,7 +8,7 @@ import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; // @ts-expect-error import configureStore from '../../../../../store/config/configureStore'; -import { getDiscoverQuery } from './DiscoverTransactionLink'; +import { getDiscoverQuery } from './discover_transaction_link'; function getMockTransaction() { return { diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_transaction_link.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/discover_links/discover_transaction_link.tsx index 21e7a1e45263ff..d776572cc9e32f 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_transaction_link.tsx @@ -12,7 +12,7 @@ import { TRANSACTION_ID, } from '../../../../../common/elasticsearch_fieldnames'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { DiscoverLink } from './DiscoverLink'; +import { DiscoverLink } from './discover_link'; export function getDiscoverQuery(transaction: Transaction) { const transactionId = transaction.transaction.id; diff --git a/x-pack/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx b/x-pack/plugins/apm/public/components/shared/links/elastic_docs_link.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/elastic_docs_link.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Links/InfraLink.test.tsx b/x-pack/plugins/apm/public/components/shared/links/infra_link.test.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/shared/Links/InfraLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/links/infra_link.test.tsx index c5a8c3299daa51..8301bd844b5cb3 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/InfraLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/infra_link.test.tsx @@ -8,7 +8,7 @@ import { Location } from 'history'; import React from 'react'; import { getRenderedHref } from '../../../utils/testHelpers'; -import { InfraLink } from './InfraLink'; +import { InfraLink } from './infra_link'; test('InfraLink produces the correct URL', async () => { const href = await getRenderedHref( diff --git a/x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx b/x-pack/plugins/apm/public/components/shared/links/infra_link.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/infra_link.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Links/kibana.ts b/x-pack/plugins/apm/public/components/shared/links/kibana.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/kibana.ts rename to x-pack/plugins/apm/public/components/shared/links/kibana.ts diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.test.tsx b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlexplorer_link.test.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlexplorer_link.test.tsx index 44e33e6bf419d7..9a8f2bd98106bd 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlexplorer_link.test.tsx @@ -8,7 +8,7 @@ import { Location } from 'history'; import React from 'react'; import { getRenderedHref } from '../../../../utils/testHelpers'; -import { MLExplorerLink } from './MLExplorerLink'; +import { MLExplorerLink } from './mlexplorer_link'; describe('MLExplorerLink', () => { it('should produce the correct URL with jobId', async () => { diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.tsx b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlexplorer_link.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlexplorer_link.tsx index 72a29a079bc679..541b00f8291b04 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlexplorer_link.tsx @@ -11,7 +11,7 @@ import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/common'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useMlHref, ML_PAGES } from '../../../../../../ml/public'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { TimePickerRefreshInterval } from '../../DatePicker/typings'; +import { TimePickerRefreshInterval } from '../../date_picker/typings'; interface Props { children?: ReactNode; diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLManageJobsLink.test.tsx b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlmanage_jobs_link.test.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLManageJobsLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlmanage_jobs_link.test.tsx index edea1f916eb184..8f7ff4f11ab22f 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLManageJobsLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlmanage_jobs_link.test.tsx @@ -8,7 +8,7 @@ import { Location } from 'history'; import React from 'react'; import { getRenderedHref } from '../../../../utils/testHelpers'; -import { MLManageJobsLink } from './MLManageJobsLink'; +import { MLManageJobsLink } from './mlmanage_jobs_link'; test('MLManageJobsLink', async () => { const href = await getRenderedHref(() => , { diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLManageJobsLink.tsx b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlmanage_jobs_link.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLManageJobsLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlmanage_jobs_link.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLSingleMetricLink.test.tsx b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlsingle_metric_link.test.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLSingleMetricLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlsingle_metric_link.test.tsx index cbafbd3fde2fac..ddcd5503853fea 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLSingleMetricLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlsingle_metric_link.test.tsx @@ -8,7 +8,7 @@ import { Location } from 'history'; import React from 'react'; import { getRenderedHref } from '../../../../utils/testHelpers'; -import { MLSingleMetricLink } from './MLSingleMetricLink'; +import { MLSingleMetricLink } from './mlsingle_metric_link'; describe('MLSingleMetricLink', () => { it('should produce the correct URL with jobId', async () => { diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLSingleMetricLink.tsx b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlsingle_metric_link.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLSingleMetricLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlsingle_metric_link.tsx index 2964a8e2578c70..ef92691336c34b 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLSingleMetricLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlsingle_metric_link.tsx @@ -11,7 +11,7 @@ import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/common'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useMlHref, ML_PAGES } from '../../../../../../ml/public'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { TimePickerRefreshInterval } from '../../DatePicker/typings'; +import { TimePickerRefreshInterval } from '../../date_picker/typings'; interface Props { children?: ReactNode; diff --git a/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.test.ts b/x-pack/plugins/apm/public/components/shared/links/rison_helpers.test.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/rison_helpers.test.ts rename to x-pack/plugins/apm/public/components/shared/links/rison_helpers.test.ts diff --git a/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts b/x-pack/plugins/apm/public/components/shared/links/rison_helpers.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts rename to x-pack/plugins/apm/public/components/shared/links/rison_helpers.ts diff --git a/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx b/x-pack/plugins/apm/public/components/shared/links/setup_instructions_link.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx rename to x-pack/plugins/apm/public/components/shared/links/setup_instructions_link.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.test.tsx b/x-pack/plugins/apm/public/components/shared/links/url_helpers.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/url_helpers.test.tsx rename to x-pack/plugins/apm/public/components/shared/links/url_helpers.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/plugins/apm/public/components/shared/links/url_helpers.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts rename to x-pack/plugins/apm/public/components/shared/links/url_helpers.ts diff --git a/x-pack/plugins/apm/public/components/shared/LoadingStatePrompt.tsx b/x-pack/plugins/apm/public/components/shared/loading_state_prompt.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/LoadingStatePrompt.tsx rename to x-pack/plugins/apm/public/components/shared/loading_state_prompt.tsx diff --git a/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx b/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx index 03ae13c06c6132..16ab8cb1d92021 100644 --- a/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx @@ -11,7 +11,7 @@ import { orderBy } from 'lodash'; import React, { ReactNode, useCallback, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params'; -import { fromQuery, toQuery } from '../Links/url_helpers'; +import { fromQuery, toQuery } from '../links/url_helpers'; // TODO: this should really be imported from EUI export interface ITableColumn { diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/metadata_table/error_metadata/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx rename to x-pack/plugins/apm/public/components/shared/metadata_table/error_metadata/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts b/x-pack/plugins/apm/public/components/shared/metadata_table/helper.test.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts rename to x-pack/plugins/apm/public/components/shared/metadata_table/helper.test.ts diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts b/x-pack/plugins/apm/public/components/shared/metadata_table/helper.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts rename to x-pack/plugins/apm/public/components/shared/metadata_table/helper.ts diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx b/x-pack/plugins/apm/public/components/shared/metadata_table/index.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx rename to x-pack/plugins/apm/public/components/shared/metadata_table/index.tsx index 5e2c180bcfd552..ab6c132c61e13f 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/metadata_table/index.tsx @@ -21,10 +21,10 @@ import React, { useCallback } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { EuiLoadingSpinner } from '@elastic/eui'; import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params'; -import { HeightRetainer } from '../HeightRetainer'; -import { fromQuery, toQuery } from '../Links/url_helpers'; +import { HeightRetainer } from '../height_retainer'; +import { fromQuery, toQuery } from '../links/url_helpers'; import { filterSectionsByTerm } from './helper'; -import { Section } from './Section'; +import { Section } from './section'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { SectionDescriptor } from './types'; diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx b/x-pack/plugins/apm/public/components/shared/metadata_table/metadata_table.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx rename to x-pack/plugins/apm/public/components/shared/metadata_table/metadata_table.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx b/x-pack/plugins/apm/public/components/shared/metadata_table/section.test.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx rename to x-pack/plugins/apm/public/components/shared/metadata_table/section.test.tsx index ed816b1c7a3372..1d14d04a2be652 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/metadata_table/section.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { Section } from './Section'; +import { Section } from './section'; import { expectTextsInDocument } from '../../../utils/testHelpers'; describe('Section', () => { diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx b/x-pack/plugins/apm/public/components/shared/metadata_table/section.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx rename to x-pack/plugins/apm/public/components/shared/metadata_table/section.tsx index 03ae237f470c3b..4093d8128c7ca2 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx +++ b/x-pack/plugins/apm/public/components/shared/metadata_table/section.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { isEmpty } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiText } from '@elastic/eui'; -import { KeyValueTable } from '../KeyValueTable'; +import { KeyValueTable } from '../key_value_table'; interface Props { properties: Array<{ field: string; value: string[] | number[] }>; diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/metadata_table/span_metadata/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx rename to x-pack/plugins/apm/public/components/shared/metadata_table/span_metadata/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/metadata_table/transaction_metadata/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx rename to x-pack/plugins/apm/public/components/shared/metadata_table/transaction_metadata/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/types.ts b/x-pack/plugins/apm/public/components/shared/metadata_table/types.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/types.ts rename to x-pack/plugins/apm/public/components/shared/metadata_table/types.ts diff --git a/x-pack/plugins/apm/public/components/shared/ml_callout/index.tsx b/x-pack/plugins/apm/public/components/shared/ml_callout/index.tsx index 926be5ef93cf7f..9fa8bb5f04960e 100644 --- a/x-pack/plugins/apm/public/components/shared/ml_callout/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ml_callout/index.tsx @@ -16,7 +16,7 @@ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { AnomalyDetectionSetupState } from '../../../../common/anomaly_detection/get_anomaly_detection_setup_state'; import { useMlManageJobsHref } from '../../../hooks/use_ml_manage_jobs_href'; -import { APMLink } from '../Links/apm/APMLink'; +import { APMLink } from '../links/apm/apm_link'; export function shouldDisplayMlCallout( anomalyDetectionSetupState: AnomalyDetectionSetupState diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.test.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.test.tsx index db30e73c86dc77..d91af5b11c49bc 100644 --- a/x-pack/plugins/apm/public/components/shared/search_bar.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/search_bar.test.tsx @@ -17,7 +17,7 @@ import type { ApmUrlParams } from '../../context/url_params_context/types'; import * as useFetcherHook from '../../hooks/use_fetcher'; import * as useServiceTransactionTypesHook from '../../context/apm_service/use_service_transaction_types_fetcher'; import { renderWithTheme } from '../../utils/testHelpers'; -import { fromQuery } from './Links/url_helpers'; +import { fromQuery } from './links/url_helpers'; import { CoreStart } from 'kibana/public'; import { SearchBar } from './search_bar'; diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.tsx index 1a6e9a803d7358..f1d82e2e307e1c 100644 --- a/x-pack/plugins/apm/public/components/shared/search_bar.tsx +++ b/x-pack/plugins/apm/public/components/shared/search_bar.tsx @@ -17,7 +17,7 @@ import { useTimeRangeId } from '../../context/time_range_id/use_time_range_id'; import { toBoolean, toNumber } from '../../context/url_params_context/helpers'; import { useApmParams } from '../../hooks/use_apm_params'; import { useBreakpoints } from '../../hooks/use_breakpoints'; -import { DatePicker } from './DatePicker'; +import { DatePicker } from './date_picker'; import { KueryBar } from './kuery_bar'; import { TimeComparison } from './time_comparison'; import { TransactionTypeSelect } from './transaction_type_select'; diff --git a/x-pack/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx b/x-pack/plugins/apm/public/components/shared/select_with_placeholder/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx rename to x-pack/plugins/apm/public/components/shared/select_with_placeholder/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/Context.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx rename to x-pack/plugins/apm/public/components/shared/stacktrace/Context.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.test.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/Stackframe.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.test.tsx rename to x-pack/plugins/apm/public/components/shared/stacktrace/Stackframe.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/Stackframe.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx rename to x-pack/plugins/apm/public/components/shared/stacktrace/Stackframe.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/Variables.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx rename to x-pack/plugins/apm/public/components/shared/stacktrace/Variables.tsx index a43cd26e7f94a5..3d92f3c584f450 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx +++ b/x-pack/plugins/apm/public/components/shared/stacktrace/Variables.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; -import { KeyValueTable } from '../KeyValueTable'; +import { KeyValueTable } from '../key_value_table'; import { flattenObject } from '../../../utils/flattenObject'; const VariablesContainer = euiStyled.div` diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/__fixtures__/stacktraces.json b/x-pack/plugins/apm/public/components/shared/stacktrace/__fixtures__/stacktraces.json similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/__fixtures__/stacktraces.json rename to x-pack/plugins/apm/public/components/shared/stacktrace/__fixtures__/stacktraces.json diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/cause_stacktrace.test.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/cause_stacktrace.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/cause_stacktrace.test.tsx rename to x-pack/plugins/apm/public/components/shared/stacktrace/cause_stacktrace.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/cause_stacktrace.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/cause_stacktrace.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/cause_stacktrace.tsx rename to x-pack/plugins/apm/public/components/shared/stacktrace/cause_stacktrace.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading.test.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/frame_heading.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading.test.tsx rename to x-pack/plugins/apm/public/components/shared/stacktrace/frame_heading.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/frame_heading.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading.tsx rename to x-pack/plugins/apm/public/components/shared/stacktrace/frame_heading.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading_renderers/c_sharp_frame_heading_renderer.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/frame_heading_renderers/c_sharp_frame_heading_renderer.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading_renderers/c_sharp_frame_heading_renderer.tsx rename to x-pack/plugins/apm/public/components/shared/stacktrace/frame_heading_renderers/c_sharp_frame_heading_renderer.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading_renderers/default_frame_heading_renderer.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/frame_heading_renderers/default_frame_heading_renderer.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading_renderers/default_frame_heading_renderer.tsx rename to x-pack/plugins/apm/public/components/shared/stacktrace/frame_heading_renderers/default_frame_heading_renderer.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading_renderers/index.ts b/x-pack/plugins/apm/public/components/shared/stacktrace/frame_heading_renderers/index.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading_renderers/index.ts rename to x-pack/plugins/apm/public/components/shared/stacktrace/frame_heading_renderers/index.ts diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading_renderers/java_frame_heading_renderer.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/frame_heading_renderers/java_frame_heading_renderer.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading_renderers/java_frame_heading_renderer.tsx rename to x-pack/plugins/apm/public/components/shared/stacktrace/frame_heading_renderers/java_frame_heading_renderer.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading_renderers/java_script_frame_heading_renderer.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/frame_heading_renderers/java_script_frame_heading_renderer.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading_renderers/java_script_frame_heading_renderer.tsx rename to x-pack/plugins/apm/public/components/shared/stacktrace/frame_heading_renderers/java_script_frame_heading_renderer.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading_renderers/ruby_frame_heading_renderer.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/frame_heading_renderers/ruby_frame_heading_renderer.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading_renderers/ruby_frame_heading_renderer.tsx rename to x-pack/plugins/apm/public/components/shared/stacktrace/frame_heading_renderers/ruby_frame_heading_renderer.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/index.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/index.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/index.tsx rename to x-pack/plugins/apm/public/components/shared/stacktrace/index.tsx index 3395b22988e8cd..de24482c06d502 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/stacktrace/index.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { isEmpty, last } from 'lodash'; import React, { Fragment } from 'react'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; -import { EmptyMessage } from '../../shared/EmptyMessage'; +import { EmptyMessage } from '../../shared/empty_message'; import { LibraryStacktrace } from './library_stacktrace'; import { Stackframe as StackframeComponent } from './Stackframe'; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/library_stacktrace.test.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/library_stacktrace.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/library_stacktrace.test.tsx rename to x-pack/plugins/apm/public/components/shared/stacktrace/library_stacktrace.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/library_stacktrace.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/library_stacktrace.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/library_stacktrace.tsx rename to x-pack/plugins/apm/public/components/shared/stacktrace/library_stacktrace.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/stacktrace.test.ts b/x-pack/plugins/apm/public/components/shared/stacktrace/stacktrace.test.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/stacktrace.test.ts rename to x-pack/plugins/apm/public/components/shared/stacktrace/stacktrace.test.ts diff --git a/x-pack/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts b/x-pack/plugins/apm/public/components/shared/summary/__fixtures__/transactions.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts rename to x-pack/plugins/apm/public/components/shared/summary/__fixtures__/transactions.ts diff --git a/x-pack/plugins/apm/public/components/shared/Summary/CompositeSpanDurationSummaryItem.tsx b/x-pack/plugins/apm/public/components/shared/summary/composite_span_duration_summary_item.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Summary/CompositeSpanDurationSummaryItem.tsx rename to x-pack/plugins/apm/public/components/shared/summary/composite_span_duration_summary_item.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx b/x-pack/plugins/apm/public/components/shared/summary/duration_summary_item.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx rename to x-pack/plugins/apm/public/components/shared/summary/duration_summary_item.tsx index e0710556096c90..9993fd27b61720 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx +++ b/x-pack/plugins/apm/public/components/shared/summary/duration_summary_item.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip, EuiText } from '@elastic/eui'; import { asDuration } from '../../../../common/utils/formatters'; -import { PercentOfParent } from '../../app/transaction_details/waterfall_with_summary/PercentOfParent'; +import { PercentOfParent } from '../../app/transaction_details/waterfall_with_summary/percent_of_parent'; interface Props { duration: number; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/error_count_summary_item_badge.test.tsx b/x-pack/plugins/apm/public/components/shared/summary/error_count_summary_item_badge.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Summary/error_count_summary_item_badge.test.tsx rename to x-pack/plugins/apm/public/components/shared/summary/error_count_summary_item_badge.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Summary/error_count_summary_item_badge.tsx b/x-pack/plugins/apm/public/components/shared/summary/error_count_summary_item_badge.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Summary/error_count_summary_item_badge.tsx rename to x-pack/plugins/apm/public/components/shared/summary/error_count_summary_item_badge.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Summary/http_info_summary_item/http_info_summary_item.test.tsx b/x-pack/plugins/apm/public/components/shared/summary/http_info_summary_item/http_info_summary_item.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Summary/http_info_summary_item/http_info_summary_item.test.tsx rename to x-pack/plugins/apm/public/components/shared/summary/http_info_summary_item/http_info_summary_item.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Summary/http_info_summary_item/index.tsx b/x-pack/plugins/apm/public/components/shared/summary/http_info_summary_item/index.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/shared/Summary/http_info_summary_item/index.tsx rename to x-pack/plugins/apm/public/components/shared/summary/http_info_summary_item/index.tsx index d10d15f8240a1c..51e574324dfa64 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/http_info_summary_item/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/summary/http_info_summary_item/index.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { truncate, unit } from '../../../../utils/style'; -import { HttpStatusBadge } from '../HttpStatusBadge'; +import { HttpStatusBadge } from '../http_status_badge'; const HttpInfoBadge = euiStyled(EuiBadge)` margin-right: ${({ theme }) => theme.eui.euiSizeXS}; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/HttpStatusBadge.test.tsx b/x-pack/plugins/apm/public/components/shared/summary/http_status_badge/http_status_badge.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/HttpStatusBadge.test.tsx rename to x-pack/plugins/apm/public/components/shared/summary/http_status_badge/http_status_badge.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/index.tsx b/x-pack/plugins/apm/public/components/shared/summary/http_status_badge/index.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/index.tsx rename to x-pack/plugins/apm/public/components/shared/summary/http_status_badge/index.tsx index 41983a9185b8f9..4ef9634fe0279a 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/summary/http_status_badge/index.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip, EuiBadge } from '@elastic/eui'; -import { statusCodes } from './statusCodes'; +import { statusCodes } from './status_codes'; import { httpStatusCodeToColor } from '../../../../utils/httpStatusCodeToColor'; interface HttpStatusBadgeProps { diff --git a/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/statusCodes.ts b/x-pack/plugins/apm/public/components/shared/summary/http_status_badge/status_codes.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/statusCodes.ts rename to x-pack/plugins/apm/public/components/shared/summary/http_status_badge/status_codes.ts diff --git a/x-pack/plugins/apm/public/components/shared/Summary/index.tsx b/x-pack/plugins/apm/public/components/shared/summary/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Summary/index.tsx rename to x-pack/plugins/apm/public/components/shared/summary/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Summary/TransactionResultSummaryItem.tsx b/x-pack/plugins/apm/public/components/shared/summary/transaction_result_summary_item.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Summary/TransactionResultSummaryItem.tsx rename to x-pack/plugins/apm/public/components/shared/summary/transaction_result_summary_item.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx b/x-pack/plugins/apm/public/components/shared/summary/transaction_summary.test.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx rename to x-pack/plugins/apm/public/components/shared/summary/transaction_summary.test.tsx index 5c2b8383ee3b65..a1ce5e99333d29 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/summary/transaction_summary.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { TransactionSummary } from './TransactionSummary'; +import { TransactionSummary } from './transaction_summary'; import * as exampleTransactions from './__fixtures__/transactions'; describe('TransactionSummary', () => { diff --git a/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx b/x-pack/plugins/apm/public/components/shared/summary/transaction_summary.tsx similarity index 86% rename from x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx rename to x-pack/plugins/apm/public/components/shared/summary/transaction_summary.tsx index dc1a62e591b172..399121b710ce91 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx +++ b/x-pack/plugins/apm/public/components/shared/summary/transaction_summary.tsx @@ -8,12 +8,12 @@ import React from 'react'; import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { Summary } from './'; -import { TimestampTooltip } from '../TimestampTooltip'; -import { DurationSummaryItem } from './DurationSummaryItem'; +import { TimestampTooltip } from '../timestamp_tooltip'; +import { DurationSummaryItem } from './duration_summary_item'; import { ErrorCountSummaryItemBadge } from './error_count_summary_item_badge'; import { HttpInfoSummaryItem } from './http_info_summary_item'; -import { TransactionResultSummaryItem } from './TransactionResultSummaryItem'; -import { UserAgentSummaryItem } from './UserAgentSummaryItem'; +import { TransactionResultSummaryItem } from './transaction_result_summary_item'; +import { UserAgentSummaryItem } from './user_agent_summary_item'; interface Props { transaction: Transaction; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.test.tsx b/x-pack/plugins/apm/public/components/shared/summary/user_agent_summary_item.test.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.test.tsx rename to x-pack/plugins/apm/public/components/shared/summary/user_agent_summary_item.test.tsx index 8884020f0364d0..9d68c5912f513e 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/summary/user_agent_summary_item.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { UserAgentSummaryItem } from './UserAgentSummaryItem'; +import { UserAgentSummaryItem } from './user_agent_summary_item'; import { mountWithTheme } from '../../../utils/testHelpers'; describe('UserAgentSummaryItem', () => { diff --git a/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx b/x-pack/plugins/apm/public/components/shared/summary/user_agent_summary_item.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx rename to x-pack/plugins/apm/public/components/shared/summary/user_agent_summary_item.tsx diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx index e20a6df12ad464..d3c3369f6a72a9 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx @@ -14,7 +14,7 @@ import { expectTextsNotInDocument, } from '../../../utils/testHelpers'; import { getSelectOptions, TimeComparison } from './'; -import * as urlHelpers from '../../shared/Links/url_helpers'; +import * as urlHelpers from '../../shared/links/url_helpers'; import moment from 'moment'; import { getComparisonTypes } from './get_comparison_types'; import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx index 2cb4e0964686f1..e61ffbbbc5bab1 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx @@ -18,7 +18,7 @@ import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_ import { useAnyOfApmParams } from '../../../hooks/use_apm_params'; import { useBreakpoints } from '../../../hooks/use_breakpoints'; import { useTimeRange } from '../../../hooks/use_time_range'; -import * as urlHelpers from '../../shared/Links/url_helpers'; +import * as urlHelpers from '../../shared/links/url_helpers'; import { getComparisonEnabled } from './get_comparison_enabled'; import { getComparisonTypes } from './get_comparison_types'; import { getTimeRangeComparison } from './get_time_range_comparison'; diff --git a/x-pack/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx b/x-pack/plugins/apm/public/components/shared/timestamp_tooltip/index.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx rename to x-pack/plugins/apm/public/components/shared/timestamp_tooltip/index.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/TimestampTooltip/index.tsx b/x-pack/plugins/apm/public/components/shared/timestamp_tooltip/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/TimestampTooltip/index.tsx rename to x-pack/plugins/apm/public/components/shared/timestamp_tooltip/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/__snapshots__/TransactionActionMenu.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/__snapshots__/transaction_action_menu.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/transaction_action_menu/__snapshots__/TransactionActionMenu.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/transaction_action_menu/__snapshots__/transaction_action_menu.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/CustomLinkToolbar.test.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.test.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/CustomLinkToolbar.test.tsx rename to x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.test.tsx index ecef563e729483..300b0ec345f42d 100644 --- a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/CustomLinkToolbar.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.test.tsx @@ -17,7 +17,7 @@ import { expectTextsInDocument, expectTextsNotInDocument, } from '../../../../utils/testHelpers'; -import { CustomLinkToolbar } from './CustomLinkToolbar'; +import { CustomLinkToolbar } from './custom_link_toolbar'; function getMockAPMContext({ canSave }: { canSave: boolean }) { return { diff --git a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/CustomLinkToolbar.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/CustomLinkToolbar.tsx rename to x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.tsx index 0818c8915d6220..935939761a496c 100644 --- a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/CustomLinkToolbar.tsx +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.tsx @@ -16,7 +16,7 @@ import { import { i18n } from '@kbn/i18n'; import { NO_PERMISSION_LABEL } from '../../../../../common/custom_link'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; -import { APMLink } from '../../Links/apm/APMLink'; +import { APMLink } from '../../links/apm/apm_link'; export function CustomLinkToolbar({ onClickCreate, diff --git a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/index.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/index.tsx index 187f82963ad602..7e377f2a756eea 100644 --- a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/index.tsx @@ -32,8 +32,8 @@ import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plug import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { CreateEditCustomLinkFlyout } from '../../../app/Settings/custom_link/create_edit_custom_link_flyout'; import { convertFiltersToQuery } from '../../../app/Settings/custom_link/create_edit_custom_link_flyout/helper'; -import { LoadingStatePrompt } from '../../LoadingStatePrompt'; -import { CustomLinkToolbar } from './CustomLinkToolbar'; +import { LoadingStatePrompt } from '../../loading_state_prompt'; +import { CustomLinkToolbar } from './custom_link_toolbar'; import { CustomLinkList } from './custom_link_list'; const DEFAULT_LINKS_TO_SHOW = 3; diff --git a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.ts b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.ts index 3095c44d54e786..daf5cb0833b61c 100644 --- a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.ts +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.ts @@ -13,10 +13,10 @@ import moment from 'moment'; import url from 'url'; import type { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import type { ApmUrlParams } from '../../../context/url_params_context/types'; -import { getDiscoverHref } from '../Links/DiscoverLinks/DiscoverLink'; -import { getDiscoverQuery } from '../Links/DiscoverLinks/DiscoverTransactionLink'; -import { getInfraHref } from '../Links/InfraLink'; -import { fromQuery } from '../Links/url_helpers'; +import { getDiscoverHref } from '../links/discover_links/discover_link'; +import { getDiscoverQuery } from '../links/discover_links/discover_transaction_link'; +import { getInfraHref } from '../links/infra_link'; +import { fromQuery } from '../links/url_helpers'; import { SectionRecord, getNonEmptySections, Action } from './sections_helper'; function getInfraMetricsQuery(transaction: Transaction) { diff --git a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/TransactionActionMenu.test.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/transaction_action_menu.test.tsx similarity index 99% rename from x-pack/plugins/apm/public/components/shared/transaction_action_menu/TransactionActionMenu.test.tsx rename to x-pack/plugins/apm/public/components/shared/transaction_action_menu/transaction_action_menu.test.tsx index fa2d3700eaf8c8..be91fb8adfe6c0 100644 --- a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/TransactionActionMenu.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/transaction_action_menu.test.tsx @@ -22,7 +22,7 @@ import { expectTextsInDocument, expectTextsNotInDocument, } from '../../../utils/testHelpers'; -import { TransactionActionMenu } from './TransactionActionMenu'; +import { TransactionActionMenu } from './transaction_action_menu'; import * as Transactions from './__fixtures__/mockData'; function getMockAPMContext({ canSave }: { canSave: boolean }) { diff --git a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/transaction_action_menu.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/transaction_action_menu/TransactionActionMenu.tsx rename to x-pack/plugins/apm/public/components/shared/transaction_action_menu/transaction_action_menu.tsx diff --git a/x-pack/plugins/apm/public/components/shared/transaction_type_select.tsx b/x-pack/plugins/apm/public/components/shared/transaction_type_select.tsx index 84f3b1e45d4a14..292aaaed816af6 100644 --- a/x-pack/plugins/apm/public/components/shared/transaction_type_select.tsx +++ b/x-pack/plugins/apm/public/components/shared/transaction_type_select.tsx @@ -11,7 +11,7 @@ import { useHistory } from 'react-router-dom'; import styled from 'styled-components'; import { useApmServiceContext } from '../../context/apm_service/use_apm_service_context'; import { useBreakpoints } from '../../hooks/use_breakpoints'; -import * as urlHelpers from './Links/url_helpers'; +import * as urlHelpers from './links/url_helpers'; // The default transaction type (for non-RUM services) is "request". Set the // min-width on here to the width when "request" is loaded so it doesn't start diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx index c44fbb8b7f87aa..b6e02b1b08c3cb 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx @@ -22,8 +22,8 @@ import { asTransactionRate, } from '../../../../common/utils/formatters'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; -import { ImpactBar } from '../ImpactBar'; -import { TransactionDetailLink } from '../Links/apm/transaction_detail_link'; +import { ImpactBar } from '../impact_bar'; +import { TransactionDetailLink } from '../links/apm/transaction_detail_link'; import { ListMetric } from '../list_metric'; import { TruncateWithTooltip } from '../truncate_with_tooltip'; import { getLatencyColumnLabel } from './get_latency_column_label'; diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx index d2abd9dc4c15ae..f943cf4da4b059 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx @@ -22,11 +22,11 @@ import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; -import { TransactionOverviewLink } from '../Links/apm/transaction_overview_link'; +import { TransactionOverviewLink } from '../links/apm/transaction_overview_link'; import { getTimeRangeComparison } from '../time_comparison/get_time_range_comparison'; import { OverviewTableContainer } from '../overview_table_container'; import { getColumns } from './get_columns'; -import { ElasticDocsLink } from '../Links/ElasticDocsLink'; +import { ElasticDocsLink } from '../links/elastic_docs_link'; import { useBreakpoints } from '../../../hooks/use_breakpoints'; type ApiResponse = diff --git a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts index c37d83983a00be..4311a9c75de85b 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts +++ b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts @@ -11,7 +11,7 @@ import { uxLocalUIFilterNames } from '../../../common/ux_ui_filter'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { LatencyAggregationType } from '../../../common/latency_aggregation_types'; import { pickKeys } from '../../../common/utils/pick_keys'; -import { toQuery } from '../../components/shared/Links/url_helpers'; +import { toQuery } from '../../components/shared/links/url_helpers'; import { getDateRange, removeUndefinedProps, diff --git a/x-pack/plugins/apm/public/hooks/use_date_range_redirect.ts b/x-pack/plugins/apm/public/hooks/use_date_range_redirect.ts index 0446b358720455..a0b06c241a28c8 100644 --- a/x-pack/plugins/apm/public/hooks/use_date_range_redirect.ts +++ b/x-pack/plugins/apm/public/hooks/use_date_range_redirect.ts @@ -7,7 +7,7 @@ import qs from 'query-string'; import { useHistory, useLocation } from 'react-router-dom'; import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; -import { TimePickerTimeDefaults } from '../components/shared/DatePicker/typings'; +import { TimePickerTimeDefaults } from '../components/shared/date_picker/typings'; import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; export function useDateRangeRedirect() { diff --git a/x-pack/plugins/apm/public/hooks/use_ml_manage_jobs_href.ts b/x-pack/plugins/apm/public/hooks/use_ml_manage_jobs_href.ts index cc187c6cf619ac..d289c29b60b372 100644 --- a/x-pack/plugins/apm/public/hooks/use_ml_manage_jobs_href.ts +++ b/x-pack/plugins/apm/public/hooks/use_ml_manage_jobs_href.ts @@ -7,7 +7,7 @@ import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; import { ML_PAGES, useMlHref } from '../../../ml/public'; -import { TimePickerRefreshInterval } from '../components/shared/DatePicker/typings'; +import { TimePickerRefreshInterval } from '../components/shared/date_picker/typings'; import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; import { useLegacyUrlParams } from '../context/url_params_context/use_url_params'; diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 77c52e1afeec35..2d000802017097 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -238,7 +238,7 @@ export class ApmPlugin implements Plugin { const getUxDataHelper = async () => { const { fetchUxOverviewDate, hasRumData } = await import( - './components/app/RumDashboard/ux_overview_fetchers' + './components/app/rum_dashboard/ux_overview_fetchers' ); const { createCallApmApi } = await import( './services/rest/createCallApmApi' diff --git a/x-pack/plugins/apm/public/setHelpExtension.ts b/x-pack/plugins/apm/public/setHelpExtension.ts index e70d9aecf9a9ce..9015a28822a75e 100644 --- a/x-pack/plugins/apm/public/setHelpExtension.ts +++ b/x-pack/plugins/apm/public/setHelpExtension.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { CoreStart } from 'kibana/public'; -import { getUpgradeAssistantHref } from './components/shared/Links/kibana'; +import { getUpgradeAssistantHref } from './components/shared/links/kibana'; export function setHelpExtension({ chrome, http }: CoreStart) { chrome.setHelpExtension({ diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts index 38dfd2261da004..3a91039d81c7a9 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts @@ -13,9 +13,10 @@ export const areaChart: ElementFactory = () => ({ help: 'A line chart with a filled body', type: 'chart', icon: 'visArea', - expression: `filters - | demodata - | pointseries x="time" y="mean(price)" - | plot defaultStyle={seriesStyle lines=1 fill=1} - | render`, + expression: `kibana +| selectFilter +| demodata +| pointseries x="time" y="mean(price)" +| plot defaultStyle={seriesStyle lines=1 fill=1} +| render`, }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts index c3f07ae601db92..b1b657bb37ff5f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts @@ -15,7 +15,8 @@ export const bubbleChart: ElementFactory = () => ({ width: 700, height: 300, icon: 'heatmap', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="project" y="sum(price)" color="state" size="size(username)" | plot defaultStyle={seriesStyle points=5 fill=1} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts index d4bf6ef6f569b5..c6db5ff4e3309e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts @@ -12,6 +12,7 @@ export const filterDebug: ElementFactory = () => ({ displayName: 'Debug filter', help: 'Shows the underlying global filters in a workpad', icon: 'bug', - expression: `filters + expression: `kibana +| selectFilter | render as=debug`, }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts index c15ca14572606f..9c01259c6d9e88 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts @@ -13,7 +13,8 @@ export const horizontalBarChart: ElementFactory = () => ({ type: 'chart', help: 'A customizable horizontal bar chart', icon: 'visBarHorizontal', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="size(cost)" y="project" color="project" | plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} legend=false diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts index f4aabba4ca2161..ef278fbea3411e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts @@ -15,7 +15,8 @@ export const horizontalProgressBar: ElementFactory = () => ({ help: 'Displays progress as a portion of a horizontal bar', width: 400, height: 30, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="horizontalBar" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts index d1d723a176b458..1675c2c78cdcbc 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts @@ -15,7 +15,8 @@ export const horizontalProgressPill: ElementFactory = () => ({ help: 'Displays progress as a portion of a horizontal pill', width: 400, height: 30, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="horizontalPill" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts index 84a3aee434141b..cdcb9bb584b5dd 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts @@ -13,7 +13,8 @@ export const lineChart: ElementFactory = () => ({ type: 'chart', help: 'A customizable line chart', icon: 'visLine', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="time" y="mean(price)" | plot defaultStyle={seriesStyle lines=3} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts index 6d8edd21c7e732..7bffff4fe95cd8 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts @@ -12,7 +12,8 @@ export const markdown: ElementFactory = () => ({ type: 'text', help: 'Add text using Markdown', icon: 'visText', - expression: `filters + expression: `kibana +| selectFilter | demodata | markdown "### Welcome to the Markdown element diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts index 76176f6ba21330..aa18e235f5fd93 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts @@ -19,13 +19,14 @@ export const metricElementInitializer: SetupInitializer = (core, width: 200, height: 100, icon: 'visMetric', - expression: `filters - | demodata - | math "unique(country)" - | metric "Countries" - metricFont={font size=48 family="${openSans.value}" color="#000000" align="center" lHeight=48} - labelFont={font size=14 family="${openSans.value}" color="#000000" align="center"} - metricFormat="${core.uiSettings.get(FORMATS_UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN)}" - | render`, + expression: `kibana +| selectFilter +| demodata +| math "unique(country)" +| metric "Countries" + metricFont={font size=48 family="${openSans.value}" color="#000000" align="center" lHeight=48} + labelFont={font size=14 family="${openSans.value}" color="#000000" align="center"} + metricFormat="${core.uiSettings.get(FORMATS_UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN)}" +| render`, }); }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts index 3f01a8ccb3e731..3c5a4c16565c67 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts @@ -12,9 +12,10 @@ export const metricVis: ElementFactory = () => ({ type: 'chart', help: 'Metric visualization', icon: 'visMetric', - expression: `filters - | demodata - | head 1 - | metricVis metric={visdimension "percent_uptime"} colorMode="Labels" - | render`, + expression: `kibana +| selectFilter +| demodata +| head 1 +| metricVis metric={visdimension "percent_uptime"} colorMode="Labels" +| render`, }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/pie/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/pie/index.ts index 2094af748ab165..4739e6ca164749 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/pie/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/pie/index.ts @@ -14,7 +14,8 @@ export const pie: ElementFactory = () => ({ height: 300, help: 'A simple pie chart', icon: 'visPie', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries color="state" size="max(price)" | pie diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/plot/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/plot/index.ts index 3e879b7fb58dba..c0ebfa60708d4a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/plot/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/plot/index.ts @@ -12,7 +12,8 @@ export const plot: ElementFactory = () => ({ displayName: 'Coordinate plot', type: 'chart', help: 'Mixed line, bar or dot charts', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="time" y="sum(price)" color="state" | plot defaultStyle={seriesStyle points=5} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts index e07a848263f50a..85f853cea759b0 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts @@ -16,7 +16,8 @@ export const progressGauge: ElementFactory = () => ({ width: 200, height: 200, icon: 'visGoal', - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="gauge" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts index 6c61ab24d13b20..100f5c65eb94ac 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts @@ -15,7 +15,8 @@ export const progressSemicircle: ElementFactory = () => ({ help: 'Displays progress as a portion of a semicircle', width: 200, height: 100, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="semicircle" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts index 15fec0d3b63906..1d9ffde49ff8be 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts @@ -15,7 +15,8 @@ export const progressWheel: ElementFactory = () => ({ help: 'Displays progress as a portion of a wheel', width: 200, height: 200, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="wheel" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts index 783b17e7d93621..6a064ffd297ecc 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts @@ -12,7 +12,8 @@ export const repeatImage: ElementFactory = () => ({ displayName: 'Image repeat', type: 'image', help: 'Repeats an image N times', - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(cost)" | repeatImage image=null diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts index b2b4ea4a942a31..b78e0d1d5cf241 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts @@ -12,7 +12,8 @@ export const revealImage: ElementFactory = () => ({ displayName: 'Image reveal', type: 'image', help: 'Reveals a percentage of an image', - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | revealImage origin=bottom image=null diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/table/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/table/index.ts index 710f595ba71792..417fe09fbc5863 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/table/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/table/index.ts @@ -13,7 +13,8 @@ export const table: ElementFactory = () => ({ type: 'chart', help: 'A scrollable grid for displaying data in a tabular format', icon: 'visTable', - expression: `filters + expression: `kibana +| selectFilter | demodata | table | render`, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/tag_cloud/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/tag_cloud/index.ts index b3543d532b9bea..698468ab2e1503 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/tag_cloud/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/tag_cloud/index.ts @@ -12,10 +12,11 @@ export const tagCloud: ElementFactory = () => ({ type: 'chart', help: 'Tagcloud visualization', icon: 'visTagCloud', - expression: `filters - | demodata - | ply by="country" fn={math "count(country)" | as "Count"} - | filterrows fn={getCell "Count" | gte 10} - | tagcloud metric={visdimension "Count"} bucket={visdimension "country"} - | render`, + expression: `kibana +| selectFilter +| demodata +| ply by="country" fn={math "count(country)" | as "Count"} +| filterrows fn={getCell "Count" | gte 10} +| tagcloud metric={visdimension "Count"} bucket={visdimension "country"} +| render`, }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts index de573166c8e9a7..a90f79aa995c59 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts @@ -13,7 +13,8 @@ export const verticalBarChart: ElementFactory = () => ({ type: 'chart', help: 'A customizable vertical bar chart', icon: 'visBarVertical', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="project" y="size(cost)" color="project" | plot defaultStyle={seriesStyle bars=0.75} legend=false diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts index 04ee9c8cb7db2a..89ffc18766bcd1 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts @@ -15,7 +15,8 @@ export const verticalProgressBar: ElementFactory = () => ({ help: 'Displays progress as a portion of a vertical bar', width: 80, height: 400, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="verticalBar" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts index 7bbf3874f175f3..b3a977c1d795a7 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts @@ -15,7 +15,8 @@ export const verticalProgressPill: ElementFactory = () => ({ help: 'Displays progress as a portion of a vertical pill', width: 80, height: 400, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="verticalPill" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts index c0ed25849ac97b..66553b6fda6c08 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts @@ -47,13 +47,14 @@ export function exactly(): ExpressionFunctionDefinition< }, }, fn: (input, args) => { - const { value, column } = args; + const { value, column, filterGroup } = args; const filter: ExpressionValueFilter = { type: 'filter', filterType: 'exactly', value, column, + filterGroup, and: [], }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts index e4a6a102844a99..b61e03319b916f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts @@ -58,11 +58,12 @@ export function timefilter(): ExpressionFunctionDefinition< return input; } - const { from, to, column } = args; + const { from, to, column, filterGroup } = args; const filter: ExpressionValueFilter = { type: 'filter', filterType: 'time', column, + filterGroup, and: [], }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.test.ts new file mode 100644 index 00000000000000..75bd97421e58e3 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fromExpression } from '@kbn/interpreter'; +import { filters } from './filters'; + +const { migrations } = filters(); + +describe('filters migrations', () => { + const expression = 'filters group="1" group="3" ungrouped=true'; + const ast = fromExpression(expression); + it('8.1.0. Should migrate `filters` expression to `kibana | selectFilter`', () => { + const migratedAst = migrations?.['8.1.0'](ast.chain[0]); + expect(migratedAst !== null && typeof migratedAst === 'object').toBeTruthy(); + expect(migratedAst.type).toBe('expression'); + expect(Array.isArray(migratedAst.chain)).toBeTruthy(); + expect(migratedAst.chain[0].function === 'kibana').toBeTruthy(); + expect(migratedAst.chain[0].arguments).toEqual({}); + expect(migratedAst.chain[1].function === 'selectFilter').toBeTruthy(); + expect(migratedAst.chain[1].arguments).toEqual(ast.chain[0].arguments); + }); +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.ts new file mode 100644 index 00000000000000..8b46e818209f3e --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ExpressionValueFilter, + ExpressionAstExpression, + ExpressionAstFunction, +} from 'src/plugins/expressions'; +import { fromExpression } from '@kbn/interpreter'; +import { buildFiltersFunction } from '../../../common/functions'; +import type { FiltersFunction } from '../../../common/functions'; + +/* + Expression function `filters` can't be used on the server, because it is tightly coupled with the redux store. + It is replaced with `kibana | selectFilter`. + + Current filters function definition is used only for the purpose of enabling migrations. + The function has to be registered on the server while the plugin's setup, to be able to run its migration. +*/ +const filtersFn = (): ExpressionValueFilter => ({ + type: 'filter', + and: [], +}); + +const migrations: FiltersFunction['migrations'] = { + '8.1.0': (ast: ExpressionAstFunction): ExpressionAstFunction | ExpressionAstExpression => { + const SELECT_FILTERS = 'selectFilter'; + const newExpression = `kibana | ${SELECT_FILTERS}`; + const newAst: ExpressionAstExpression = fromExpression(newExpression); + const selectFiltersAstIndex = newAst.chain.findIndex( + ({ function: fnName }) => fnName === SELECT_FILTERS + ); + const selectFilterAst = newAst.chain[selectFiltersAstIndex]; + newAst.chain.splice(selectFiltersAstIndex, 1, { ...selectFilterAst, arguments: ast.arguments }); + return newAst; + }, +}; + +export const filters = buildFiltersFunction(filtersFn, migrations); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/index.ts index ae3778366651cf..388db9e6e59604 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/index.ts @@ -7,5 +7,6 @@ import { demodata } from './demodata'; import { pointseries } from './pointseries'; +import { filters } from './filters'; -export const functions = [demodata, pointseries]; +export const functions = [filters, demodata, pointseries]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx index 643d7cdedc50d2..38d1d502704e2a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx @@ -21,7 +21,6 @@ export const defaultHandlers: RendererHandlers = { onEmbeddableInputChange: action('onEmbeddableInputChange'), onResize: action('onResize'), resize: action('resize'), - setFilter: action('setFilter'), done: action('done'), onDestroy: action('onDestroy'), reload: action('reload'), diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx index b831c9aa70e49d..a31021cba4c108 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx @@ -25,7 +25,10 @@ export const advancedFilterFactory: StartInitializer> = render(domNode, _, handlers) { ReactDOM.render( - + handlers.event({ name: 'applyFilterAction', data: filter })} + value={handlers.getFilter()} + /> , domNode, () => handlers.done() diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx index 372bcbb5642cbb..5e4ea42990e47c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx @@ -55,20 +55,19 @@ export const dropdownFilterFactory: StartInitializer> = (filterExpression === undefined || !filterExpression.includes('exactly')) ) { filterExpression = ''; - handlers.setFilter(filterExpression); + handlers.event({ name: 'applyFilterAction', data: filterExpression }); } else if (filterExpression !== '') { // NOTE: setFilter() will cause a data refresh, avoid calling unless required // compare expression and filter, update filter if needed const { changed, newAst } = syncFilterExpression(config, filterExpression, ['filterGroup']); if (changed) { - handlers.setFilter(toExpression(newAst)); + handlers.event({ name: 'applyFilterAction', data: toExpression(newAst) }); } } - const commit = (commitValue: string) => { if (commitValue === '%%CANVAS_MATCH_ALL%%') { - handlers.setFilter(''); + handlers.event({ name: 'applyFilterAction', data: '' }); } else { const newFilterAST: Ast = { type: 'expression', @@ -86,18 +85,19 @@ export const dropdownFilterFactory: StartInitializer> = }; const newFilter = toExpression(newFilterAST); - handlers.setFilter(newFilter); + handlers.event({ name: 'applyFilterAction', data: newFilter }); } }; + const filter = ( + + ); ReactDOM.render( - - - , + {filter}, domNode, () => handlers.done() ); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx index e81ca2cc1f0571..f7e9d333f86835 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx @@ -45,7 +45,7 @@ export const timeFilterFactory: StartInitializer> = ( if (filterExpression === undefined || filterExpression.indexOf('timefilter') !== 0) { filterExpression = defaultTimeFilterExpression; - handlers.setFilter(filterExpression); + handlers.event({ name: 'applyFilterAction', data: filterExpression }); } else if (filterExpression !== '') { // NOTE: setFilter() will cause a data refresh, avoid calling unless required // compare expression and filter, update filter if needed @@ -55,14 +55,14 @@ export const timeFilterFactory: StartInitializer> = ( ]); if (changed) { - handlers.setFilter(toExpression(newAst)); + handlers.event({ name: 'applyFilterAction', data: toExpression(newAst) }); } } ReactDOM.render( handlers.event({ name: 'applyFilterAction', data: filter })} filter={filterExpression} commonlyUsedRanges={customQuickRanges} dateFormat={customDateFormat} diff --git a/x-pack/plugins/canvas/common/functions/filters.ts b/x-pack/plugins/canvas/common/functions/filters.ts new file mode 100644 index 00000000000000..5c48fbd10862af --- /dev/null +++ b/x-pack/plugins/canvas/common/functions/filters.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public'; +import { ExpressionValueFilter } from '../../types'; +import { getFunctionHelp } from '../../i18n'; + +export interface Arguments { + group: string[]; + ungrouped: boolean; +} + +export type FiltersFunction = ExpressionFunctionDefinition< + 'filters', + null, + Arguments, + ExpressionValueFilter +>; + +export function buildFiltersFunction( + fn: FiltersFunction['fn'], + migrations?: FiltersFunction['migrations'] +) { + return function filters(): FiltersFunction { + const { help, args: argHelp } = getFunctionHelp().filters; + + return { + name: 'filters', + type: 'filter', + help, + context: { + types: ['null'], + }, + args: { + group: { + aliases: ['_'], + types: ['string'], + help: argHelp.group, + multi: true, + }, + ungrouped: { + aliases: ['nogroup', 'nogroups'], + types: ['boolean'], + help: argHelp.ungrouped, + default: false, + }, + }, + fn, + migrations, + }; + }; +} diff --git a/x-pack/plugins/canvas/common/functions/index.ts b/x-pack/plugins/canvas/common/functions/index.ts new file mode 100644 index 00000000000000..08d9391f81c134 --- /dev/null +++ b/x-pack/plugins/canvas/common/functions/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { FiltersFunction } from './filters'; +export { buildFiltersFunction } from './filters'; diff --git a/x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts b/x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts index 57fdc7d7309ce6..c98d2f080452ab 100644 --- a/x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts +++ b/x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts @@ -6,6 +6,8 @@ */ import { buildQueryFilter, Filter } from '@kbn/es-query'; +import dateMath from '@elastic/datemath'; +import { maxBy, minBy } from 'lodash'; import { ExpressionValueFilter } from '../../types'; // @ts-expect-error untyped local import { buildBoolArray } from './build_bool_array'; @@ -16,24 +18,45 @@ export interface EmbeddableFilterInput { timeRange?: TimeRange; } +type ESFilter = Record; + const TimeFilterType = 'time'; +const formatTime = (str: string | undefined, roundUp: boolean = false) => { + if (!str) { + return null; + } + const moment = dateMath.parse(str, { roundUp }); + return !moment || !moment.isValid() ? null : moment.valueOf(); +}; + function getTimeRangeFromFilters(filters: ExpressionValueFilter[]): TimeRange | undefined { - const timeFilter = filters.find( - (filter) => filter.filterType !== undefined && filter.filterType === TimeFilterType + const timeFilters = filters.filter( + (filter) => + filter.filterType !== undefined && + filter.filterType === TimeFilterType && + filter.from !== undefined && + filter.to !== undefined ); - return timeFilter !== undefined && timeFilter.from !== undefined && timeFilter.to !== undefined - ? { - from: timeFilter.from, - to: timeFilter.to, - } + const validatedTimeFilters = timeFilters.filter( + (filter) => formatTime(filter.from) !== null && formatTime(filter.to, true) !== null + ); + + const minFromFilter = minBy(validatedTimeFilters, (filter) => formatTime(filter.from)); + const maxToFilter = maxBy(validatedTimeFilters, (filter) => formatTime(filter.to, true)); + + return minFromFilter?.from && maxToFilter?.to + ? { from: minFromFilter.from, to: maxToFilter.to } : undefined; } export function getQueryFilters(filters: ExpressionValueFilter[]): Filter[] { const dataFilters = filters.map((filter) => ({ ...filter, type: filter.filterType })); - return buildBoolArray(dataFilters).map(buildQueryFilter); + return buildBoolArray(dataFilters).map((filter: ESFilter, index: number) => { + const { group, ...restFilter } = filter; + return buildQueryFilter(restFilter, index.toString(), '', { group }); + }); } export function buildEmbeddableFilters(filters: ExpressionValueFilter[]): EmbeddableFilterInput { diff --git a/x-pack/plugins/canvas/common/lib/constants.ts b/x-pack/plugins/canvas/common/lib/constants.ts index 6a61ec595acb7c..fa938f2c07c748 100644 --- a/x-pack/plugins/canvas/common/lib/constants.ts +++ b/x-pack/plugins/canvas/common/lib/constants.ts @@ -16,6 +16,8 @@ export const APP_ROUTE = '/app/canvas'; export const APP_ROUTE_WORKPAD = `${APP_ROUTE}#/workpad`; export const API_ROUTE = '/api/canvas'; export const API_ROUTE_WORKPAD = `${API_ROUTE}/workpad`; +export const API_ROUTE_WORKPAD_EXPORT = `${API_ROUTE_WORKPAD}/export`; +export const API_ROUTE_WORKPAD_IMPORT = `${API_ROUTE_WORKPAD}/import`; export const API_ROUTE_WORKPAD_ASSETS = `${API_ROUTE}/workpad-assets`; export const API_ROUTE_WORKPAD_STRUCTURES = `${API_ROUTE}/workpad-structures`; export const API_ROUTE_CUSTOM_ELEMENT = `${API_ROUTE}/custom-element`; diff --git a/x-pack/plugins/canvas/common/lib/filters.js b/x-pack/plugins/canvas/common/lib/filters.js index 08caded52aa26b..f43e2dd3b46065 100644 --- a/x-pack/plugins/canvas/common/lib/filters.js +++ b/x-pack/plugins/canvas/common/lib/filters.js @@ -13,15 +13,16 @@ export function time(filter) { if (!filter.column) { throw new Error('column is required for Elasticsearch range filters'); } + const { from, to, column, filterGroup: group } = filter; return { - range: { - [filter.column]: { gte: filter.from, lte: filter.to }, - }, + group, + range: { [column]: { gte: from, lte: to } }, }; } export function luceneQueryString(filter) { return { + group: filter.filterGroup, query_string: { query: filter.query || '*', }, @@ -30,6 +31,7 @@ export function luceneQueryString(filter) { export function exactly(filter) { return { + group: filter.filterGroup, term: { [filter.column]: { value: filter.value, diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js index 89faef29a3b02c..1ca674bfb6f9d3 100644 --- a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js +++ b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js @@ -7,16 +7,19 @@ import React, { useState, useEffect } from 'react'; import { PropTypes } from 'prop-types'; -import { interpretAst } from '../../../lib/run_interpreter'; import { Loading } from '../../loading'; +import { useExpressionsService } from '../../../services'; import { DatasourcePreview as Component } from './datasource_preview'; export const DatasourcePreview = (props) => { const [datatable, setDatatable] = useState(); + const expressionsService = useExpressionsService(); useEffect(() => { - interpretAst({ type: 'expression', chain: [props.function] }, {}).then(setDatatable); - }, [props.function, setDatatable]); + expressionsService + .interpretAst({ type: 'expression', chain: [props.function] }, {}) + .then(setDatatable); + }, [expressionsService, props.function, setDatatable]); if (!datatable) { return ; diff --git a/x-pack/plugins/canvas/public/components/function_form_list/index.js b/x-pack/plugins/canvas/public/components/function_form_list/index.js index 6048ac360386c9..31db3366ce3b5c 100644 --- a/x-pack/plugins/canvas/public/components/function_form_list/index.js +++ b/x-pack/plugins/canvas/public/components/function_form_list/index.js @@ -8,7 +8,7 @@ import { compose, withProps } from 'recompose'; import { get } from 'lodash'; import { toExpression } from '@kbn/interpreter'; -import { interpretAst } from '../../lib/run_interpreter'; +import { pluginServices } from '../../services'; import { getArgTypeDef } from '../../lib/args'; import { FunctionFormList as Component } from './function_form_list'; @@ -77,24 +77,27 @@ const componentFactory = ({ path, parentPath, removable, -}) => ({ - args, - nestedFunctionsArgs: argsWithExprFunctions, - argType: argType.function, - argTypeDef: Object.assign(argTypeDef, { - args: argumentsView, - name: argUiConfig?.name ?? argTypeDef.name, - displayName: argUiConfig?.displayName ?? argTypeDef.displayName, - help: argUiConfig?.help ?? argTypeDef.name, - }), - argResolver: (argAst) => interpretAst(argAst, prevContext), - contextExpression: getExpression(prevContext), - expressionIndex, // preserve the index in the AST - nextArgType: nextArg && nextArg.function, - path, - parentPath, - removable, -}); +}) => { + const { expressions } = pluginServices.getServices(); + return { + args, + nestedFunctionsArgs: argsWithExprFunctions, + argType: argType.function, + argTypeDef: Object.assign(argTypeDef, { + args: argumentsView, + name: argUiConfig?.name ?? argTypeDef.name, + displayName: argUiConfig?.displayName ?? argTypeDef.displayName, + help: argUiConfig?.help ?? argTypeDef.name, + }), + argResolver: (argAst) => expressions.interpretAst(argAst, prevContext), + contextExpression: getExpression(prevContext), + expressionIndex, // preserve the index in the AST + nextArgType: nextArg && nextArg.function, + path, + parentPath, + removable, + }; +}; /** * Converts expression functions at the arguments for the expression, to the array of UI component configurations. diff --git a/x-pack/plugins/canvas/public/components/function_reference_generator/function_examples.ts b/x-pack/plugins/canvas/public/components/function_reference_generator/function_examples.ts index a8409270752ad8..785f183b193f13 100644 --- a/x-pack/plugins/canvas/public/components/function_reference_generator/function_examples.ts +++ b/x-pack/plugins/canvas/public/components/function_reference_generator/function_examples.ts @@ -22,7 +22,8 @@ export const getFunctionExamples = (): FunctionExampleDict => ({ syntax: `all {neq "foo"} {neq "bar"} {neq "fizz"} all condition={gt 10} condition={lt 20}`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | formatnumber "0.0%" @@ -42,7 +43,8 @@ all condition={gt 10} condition={lt 20}`, syntax: `alterColumn "cost" type="string" alterColumn column="@timestamp" name="foo"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | alterColumn "time" name="time_in_ms" type="number" | table @@ -54,7 +56,8 @@ alterColumn column="@timestamp" name="foo"`, syntax: `any {eq "foo"} {eq "bar"} {eq "fizz"} any condition={lte 10} condition={gt 30}`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | filterrows { getCell "project" | any {eq "elasticsearch"} {eq "kibana"} {eq "x-pack"} @@ -70,7 +73,8 @@ any condition={lte 10} condition={gt 30}`, as "foo" as name="bar"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | ply by="project" fn={math "count(username)" | as "num_users"} fn={math "mean(price)" | as "price"} | pointseries x="project" y="num_users" size="price" color="project" @@ -94,7 +98,8 @@ asset id="asset-498f7429-4d56-42a2-a7e4-8bf08d98d114"`, syntax: `axisConfig show=false axisConfig position="right" min=0 max=10 tickSize=1`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="size(cost)" y="project" color="project" | plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} @@ -133,7 +138,8 @@ case if={lte 50} then="green"`, syntax: `columns include="@timestamp, projects, cost" columns exclude="username, country, age"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | columns include="price, cost, state, project" | table @@ -145,7 +151,8 @@ columns exclude="username, country, age"`, syntax: `compare "neq" to="elasticsearch" compare op="lte" to=100`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | mapColumn project fn={getCell project | @@ -229,7 +236,8 @@ date "01/31/2019" format="MM/DD/YYYY"`, demodata "ci" demodata type="shirts"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | table | render`, @@ -252,7 +260,8 @@ eq null eq 10 eq "foo"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | mapColumn project fn={getCell project | @@ -272,7 +281,8 @@ eq "foo"`, escount "currency:\"EUR\"" index="kibana_sample_data_ecommerce" escount query="response:404" index="kibana_sample_data_logs"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | escount "Cancelled:true" index="kibana_sample_data_flights" | math "value" | progress shape="semicircle" @@ -290,7 +300,8 @@ esdocs query="response:404" index="kibana_sample_data_logs" esdocs index="kibana_sample_data_flights" count=100 esdocs index="kibana_sample_data_flights" sort="AvgTicketPrice, asc"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | esdocs index="kibana_sample_data_ecommerce" fields="customer_gender, taxful_total_price, order_date" sort="order_date, asc" @@ -309,7 +320,8 @@ esdocs index="kibana_sample_data_flights" sort="AvgTicketPrice, asc"`, syntax: `essql query="SELECT * FROM \"logstash*\"" essql "SELECT * FROM \"apm*\"" count=10000`, usage: { - expression: `filters + expression: `kibana +| selectFilter | essql query="SELECT Carrier, FlightDelayMin, AvgTicketPrice FROM \"kibana_sample_data_flights\"" | table | render`, @@ -321,7 +333,8 @@ essql "SELECT * FROM \"apm*\"" count=10000`, exactly "age" value=50 filterGroup="group2" exactly column="project" value="beats"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | exactly column=project value=elasticsearch | demodata | pointseries x=project y="mean(age)" @@ -334,7 +347,8 @@ exactly column="project" value="beats"`, syntax: `filterrows {getCell "project" | eq "kibana"} filterrows fn={getCell "age" | gt 50}`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | filterrows {getCell "country" | any {eq "IN"} {eq "US"} {eq "CN"}} | mapColumn "@timestamp" @@ -379,7 +393,8 @@ font underline=true font italic=false font lHeight=32`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="project" y="size(cost)" color="project" | plot defaultStyle={seriesStyle bars=0.75} legend=false @@ -399,7 +414,8 @@ font lHeight=32`, syntax: `formatdate format="YYYY-MM-DD" formatdate "MM/DD/YYYY"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | mapColumn "time" fn={getCell time | formatdate "MMM 'YY"} | pointseries x="time" y="sum(price)" color="state" @@ -412,7 +428,8 @@ formatdate "MM/DD/YYYY"`, syntax: `formatnumber format="$0,0.00" formatnumber "0.0a"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="gauge" diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts index eb87f4720deec9..3290bc8227a292 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts @@ -29,7 +29,7 @@ export const useCreateWorkpad = () => { history.push(`/workpad/${workpad.id}/page/1`); } catch (err) { notifyService.error(err, { - title: errors.getUploadFailureErrorMessage(), + title: errors.getCreateFailureErrorMessage(), }); } return; @@ -39,8 +39,8 @@ export const useCreateWorkpad = () => { }; const errors = { - getUploadFailureErrorMessage: () => - i18n.translate('xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage', { - defaultMessage: `Couldn't upload workpad`, + getCreateFailureErrorMessage: () => + i18n.translate('xpack.canvas.error.useCreateWorkpad.createFailureErrorMessage', { + defaultMessage: `Couldn't create workpad`, }), }; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_import_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_import_workpad.ts new file mode 100644 index 00000000000000..8c8d2e26d8a224 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_import_workpad.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; + +// @ts-expect-error +import { getDefaultWorkpad } from '../../../state/defaults'; +import { useNotifyService, useWorkpadService } from '../../../services'; + +import type { CanvasWorkpad } from '../../../../types'; + +export const useImportWorkpad = () => { + const workpadService = useWorkpadService(); + const notifyService = useNotifyService(); + const history = useHistory(); + + return useCallback( + async (workpad: CanvasWorkpad) => { + try { + const importedWorkpad = await workpadService.import(workpad); + history.push(`/workpad/${importedWorkpad.id}/page/1`); + } catch (err) { + notifyService.error(err, { + title: errors.getUploadFailureErrorMessage(), + }); + } + return; + }, + [notifyService, history, workpadService] + ); +}; + +const errors = { + getUploadFailureErrorMessage: () => + i18n.translate('xpack.canvas.error.useUploadWorkpad.uploadFailureErrorMessage', { + defaultMessage: `Couldn't upload workpad`, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts index caec30e083d405..045ff8b52e2590 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts @@ -9,19 +9,25 @@ import { useCallback } from 'react'; import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { SavedObject } from 'kibana/public'; import { CANVAS, JSON as JSONString } from '../../../../i18n/constants'; import { useNotifyService } from '../../../services'; import { getId } from '../../../lib/get_id'; - -import { useCreateWorkpad } from './use_create_workpad'; +import { useImportWorkpad as useImportWorkpadHook } from './use_import_workpad'; import type { CanvasWorkpad } from '../../../../types'; +const isInvalidWorkpad = (workpad: CanvasWorkpad) => + !Array.isArray(workpad.pages) || workpad.pages.length === 0 || !workpad.assets; + export const useImportWorkpad = () => { const notifyService = useNotifyService(); - const createWorkpad = useCreateWorkpad(); + const importWorkpad = useImportWorkpadHook(); return useCallback( - (file?: File, onComplete: (workpad?: CanvasWorkpad) => void = () => {}) => { + ( + file?: File, + onComplete: (workpad?: CanvasWorkpad | SavedObject) => void = () => {} + ) => { if (!file) { onComplete(); return; @@ -42,16 +48,17 @@ export const useImportWorkpad = () => { // handle reading the uploaded file reader.onload = async () => { try { - const workpad = JSON.parse(reader.result as string); // Type-casting because we catch below. + const workpad: CanvasWorkpad = JSON.parse(reader.result as string); // Type-casting because we catch below. + workpad.id = getId('workpad'); // sanity check for workpad object - if (!Array.isArray(workpad.pages) || workpad.pages.length === 0 || !workpad.assets) { + if (isInvalidWorkpad(workpad)) { onComplete(); throw new Error(errors.getMissingPropertiesErrorMessage()); } - await createWorkpad(workpad); + await importWorkpad(workpad); onComplete(workpad); } catch (e) { notifyService.error(e, { @@ -66,7 +73,7 @@ export const useImportWorkpad = () => { // read the uploaded file reader.readAsText(file); }, - [notifyService, createWorkpad] + [notifyService, importWorkpad] ); }; diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts b/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts index 85b195214d44bf..21bcc89304b3c8 100644 --- a/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts +++ b/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts @@ -5,19 +5,22 @@ * 2.0. */ -import { fromExpression } from '@kbn/interpreter'; +import { ExpressionFunctionAST, fromExpression } from '@kbn/interpreter'; import { shallowEqual, useSelector } from 'react-redux'; import { State } from '../../../../types'; -import { getFiltersByGroups } from '../../../lib/filter'; +import { getFiltersByFilterExpressions } from '../../../lib/filter'; import { adaptCanvasFilter } from '../../../lib/filter_adapters'; -import { getGlobalFilters } from '../../../state/selectors/workpad'; +import { useFiltersService } from '../../../services'; -const extractExpressionAST = (filtersExpressions: string[]) => - fromExpression(filtersExpressions.join(' | ')); +const extractExpressionAST = (filters: string[]) => fromExpression(filters.join(' | ')); -export function useCanvasFilters(groups: string[] = [], ungrouped: boolean = false) { - const filterExpressions = useSelector((state: State) => getGlobalFilters(state), shallowEqual); - const filtersByGroups = getFiltersByGroups(filterExpressions, groups, ungrouped); +export function useCanvasFilters(filterExprsToGroupBy: ExpressionFunctionAST[] = []) { + const filtersService = useFiltersService(); + const filterExpressions = useSelector( + (state: State) => filtersService.getFilters(state), + shallowEqual + ); + const filtersByGroups = getFiltersByFilterExpressions(filterExpressions, filterExprsToGroupBy); const expression = extractExpressionAST(filtersByGroups); const filters = expression.chain.map(adaptCanvasFilter); diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.tsx b/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.tsx index 610e6e56af3502..20ec56706480d3 100644 --- a/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.tsx @@ -8,11 +8,7 @@ import React, { FC, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { State, FilterField, PositionedElement } from '../../../types'; -import { - extractGroupsFromElementsFilters, - groupFiltersBy, - extractUngroupedFromElementsFilters, -} from '../../lib/filter'; +import { groupFiltersBy, getFiltersExprsFromExpression } from '../../lib/filter'; import { setGroupFiltersByOption } from '../../state/actions/sidebar'; import { getGroupFiltersByOption } from '../../state/selectors/sidebar'; import { useCanvasFilters } from './hooks'; @@ -35,11 +31,8 @@ export const WorkpadFilters: FC = ({ element }) => { }, [dispatch] ); - - const groups = element ? extractGroupsFromElementsFilters(element.expression) : undefined; - const ungrouped = element ? extractUngroupedFromElementsFilters(element.expression) : false; - - const canvasFilters = useCanvasFilters(groups, ungrouped); + const filterExprs = element ? getFiltersExprsFromExpression(element.expression) : []; + const canvasFilters = useCanvasFilters(filterExprs); const filtersGroups = groupFiltersByField ? groupFiltersBy(canvasFilters, groupFiltersByField) diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx index 9d37873bcae0a4..62d070dbf00f54 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx @@ -17,7 +17,8 @@ const testElements: { [key: string]: ElementSpec } = { displayName: 'Area chart', help: 'A line chart with a filled body', type: 'chart', - expression: `filters + expression: `kibana + | selectFilter | demodata | pointseries x="time" y="mean(price)" | plot defaultStyle={seriesStyle lines=1 fill=1} @@ -47,7 +48,8 @@ const testElements: { [key: string]: ElementSpec } = { displayName: 'Debug filter', help: 'Shows the underlying global filters in a workpad', icon: 'bug', - expression: `filters + expression: `kibana + | selectFilter | render as=debug`, }, image: { @@ -64,7 +66,8 @@ const testElements: { [key: string]: ElementSpec } = { type: 'text', help: 'Add text using Markdown', icon: 'visText', - expression: `filters + expression: `kibana +| selectFilter | demodata | markdown "### Welcome to the Markdown element @@ -89,7 +92,8 @@ You can use standard Markdown in here, but you can also access your piped-in dat width: 200, height: 200, icon: 'visGoal', - expression: `filters + expression: `kibana + | selectFilter | demodata | math "mean(percent_uptime)" | progress shape="gauge" label={formatnumber 0%} font={font size=24 family="Helvetica" color="#000000" align=center} @@ -111,7 +115,8 @@ You can use standard Markdown in here, but you can also access your piped-in dat displayName: 'Data table', type: 'chart', help: 'A scrollable grid for displaying data in a tabular format', - expression: `filters + expression: `kibana + | selectFilter | demodata | table | render`, diff --git a/x-pack/plugins/canvas/public/functions/filters.ts b/x-pack/plugins/canvas/public/functions/filters.ts index 2634d76297b588..a168020b6eef87 100644 --- a/x-pack/plugins/canvas/public/functions/filters.ts +++ b/x-pack/plugins/canvas/public/functions/filters.ts @@ -7,15 +7,10 @@ import { fromExpression } from '@kbn/interpreter'; import { get } from 'lodash'; -import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public'; -import { interpretAst } from '../lib/run_interpreter'; -// @ts-expect-error untyped local -import { getState } from '../state/store'; -import { getGlobalFilters, getWorkpadVariablesAsObject } from '../state/selectors/workpad'; -import { ExpressionValueFilter } from '../../types'; -import { getFunctionHelp } from '../../i18n'; +import { pluginServices } from '../services'; +import type { FiltersFunction } from '../../common/functions'; +import { buildFiltersFunction } from '../../common/functions'; import { InitializeArguments } from '.'; -import { getFiltersByGroups } from '../lib/filter'; export interface Arguments { group: string[]; @@ -31,58 +26,34 @@ function getFiltersByGroup(allFilters: string[], groups?: string[], ungrouped = // remove all allFilters that belong to a group return allFilters.filter((filter: string) => { const ast = fromExpression(filter); - const expGroups = get(ast, 'chain[0].arguments.filterGroup', []); + const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []); return expGroups.length === 0; }); } - return getFiltersByGroups(allFilters, groups); + return allFilters.filter((filter: string) => { + const ast = fromExpression(filter); + const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []); + return expGroups.length > 0 && expGroups.every((expGroup) => groups.includes(expGroup)); + }); } -type FiltersFunction = ExpressionFunctionDefinition< - 'filters', - null, - Arguments, - ExpressionValueFilter ->; - export function filtersFunctionFactory(initialize: InitializeArguments): () => FiltersFunction { - return function filters(): FiltersFunction { - const { help, args: argHelp } = getFunctionHelp().filters; - - return { - name: 'filters', - type: 'filter', - help, - context: { - types: ['null'], - }, - args: { - group: { - aliases: ['_'], - types: ['string'], - help: argHelp.group, - multi: true, - }, - ungrouped: { - aliases: ['nogroup', 'nogroups'], - types: ['boolean'], - help: argHelp.ungrouped, - default: false, - }, - }, - fn: (input, { group, ungrouped }) => { - const filterList = getFiltersByGroup(getGlobalFilters(getState()), group, ungrouped); - - if (filterList && filterList.length) { - const filterExpression = filterList.join(' | '); - const filterAST = fromExpression(filterExpression); - return interpretAst(filterAST, getWorkpadVariablesAsObject(getState())); - } else { - const filterType = initialize.types.filter; - return filterType?.from(null, {}); - } - }, - }; + const fn: FiltersFunction['fn'] = (input, { group, ungrouped }) => { + const { expressions, filters: filtersService } = pluginServices.getServices(); + + const filterList = getFiltersByGroup(filtersService.getFilters(), group, ungrouped); + + if (filterList && filterList.length) { + const filterExpression = filterList.join(' | '); + const filterAST = fromExpression(filterExpression); + const { variables } = filtersService.getFiltersContext(); + return expressions.interpretAst(filterAST, variables); + } else { + const filterType = initialize.types.filter; + return filterType?.from(null, {}); + } }; + + return buildFiltersFunction(fn); } diff --git a/x-pack/plugins/canvas/public/lib/create_handlers.ts b/x-pack/plugins/canvas/public/lib/create_handlers.ts index 3734b1bf53051d..3536bed0f92b3c 100644 --- a/x-pack/plugins/canvas/public/lib/create_handlers.ts +++ b/x-pack/plugins/canvas/public/lib/create_handlers.ts @@ -10,10 +10,9 @@ import { ExpressionRendererEvent, IInterpreterRenderHandlers, } from 'src/plugins/expressions/public'; -// @ts-expect-error untyped local -import { setFilter } from '../state/actions/elements'; import { updateEmbeddableExpression, fetchEmbeddableRenderable } from '../state/actions/embeddable'; import { RendererHandlers, CanvasElement } from '../../types'; +import { pluginServices } from '../services'; import { clearValue } from '../state/actions/resolved_args'; // This class creates stub handlers to ensure every element and renderer fulfills the contract. @@ -58,7 +57,6 @@ export const createHandlers = (baseHandlers = createBaseHandlers()): RendererHan }, resize(_size: { height: number; width: number }) {}, - setFilter() {}, }); export const assignHandlers = (handlers: Partial = {}): RendererHandlers => @@ -79,6 +77,8 @@ export const createDispatchedHandlerFactory = ( oldElement = element; } + const { filters } = pluginServices.getServices(); + const handlers: RendererHandlers & { event: IInterpreterRenderHandlers['event']; done: IInterpreterRenderHandlers['done']; @@ -89,8 +89,8 @@ export const createDispatchedHandlerFactory = ( case 'embeddableInputChange': this.onEmbeddableInputChange(event.data); break; - case 'setFilter': - this.setFilter(event.data); + case 'applyFilterAction': + filters.updateFilter(element.id, event.data); break; case 'onComplete': this.onComplete(event.data); @@ -106,10 +106,6 @@ export const createDispatchedHandlerFactory = ( break; } }, - setFilter(text: string) { - dispatch(setFilter(text, element.id, true)); - }, - getFilter() { return element.filter || ''; }, diff --git a/x-pack/plugins/canvas/public/lib/filter.test.ts b/x-pack/plugins/canvas/public/lib/filter.test.ts index bf19bd6ecf4b86..9aef71f33f6093 100644 --- a/x-pack/plugins/canvas/public/lib/filter.test.ts +++ b/x-pack/plugins/canvas/public/lib/filter.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { fromExpression } from '@kbn/interpreter'; import { FC } from 'react'; import { Filter as FilterType, @@ -18,9 +19,8 @@ import { flattenFilterView, createFilledFilterView, groupFiltersBy, - getFiltersByGroups, - extractGroupsFromElementsFilters, - extractUngroupedFromElementsFilters, + getFiltersExprsFromExpression, + getFiltersByFilterExpressions, isExpressionWithFilters, } from './filter'; @@ -285,7 +285,7 @@ describe('groupFiltersBy', () => { }); }); -describe('getFiltersByGroups', () => { +describe('getFiltersByFilterExpressions', () => { const group1 = 'Group 1'; const group2 = 'Group 2'; @@ -296,66 +296,106 @@ describe('getFiltersByGroups', () => { `exactly value="kibana" column="project2" filterGroup="${group2}"`, ]; - it('returns all filters related to a specified groups', () => { - expect(getFiltersByGroups(filters, [group1, group2])).toEqual([ - filters[0], - filters[1], - filters[3], - ]); + const filtersExprWithGroup = `filters group="${group2}"`; + + const kibanaExpr = 'kibana'; + const selectFilterExprEmpty = 'selectFilter'; + const selectFilterExprWithGroup = `${selectFilterExprEmpty} group="${group2}"`; + const selectFilterExprWithGroups = `${selectFilterExprEmpty} group="${group2}" group="${group1}"`; + const selectFilterExprWithUngrouped = `${selectFilterExprEmpty} ungrouped=true`; + const selectFilterExprWithGroupAndUngrouped = `${selectFilterExprEmpty} group="${group2}" ungrouped=true`; + + const removeFilterExprEmpty = 'removeFilter'; + const removeFilterExprWithGroup = `${removeFilterExprEmpty} group="${group2}"`; + const removeFilterExprWithUngrouped = `${removeFilterExprEmpty} ungrouped=true`; + const removeFilterExprWithGroupAndUngrouped = `${removeFilterExprEmpty} group="${group2}" ungrouped=true`; + + const getFiltersAsts = (filtersExprs: string[]) => { + const ast = fromExpression(filtersExprs.join(' | ')); + return ast.chain; + }; - expect(getFiltersByGroups(filters, [group2])).toEqual([filters[1], filters[3]]); + it('returns all filters if no arguments specified to selectFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, selectFilterExprEmpty]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual(filters); }); - it('returns filters without group if ungrouped is true', () => { - expect(getFiltersByGroups(filters, [], true)).toEqual([filters[2]]); + it('returns filters with group, specified to selectFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, selectFilterExprWithGroups]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[0], filters[1], filters[3]]); }); - it('returns filters with group if ungrouped is true and groups are not empty', () => { - expect(getFiltersByGroups(filters, [group1], true)).toEqual([filters[0]]); + it('returns filters without group if ungrouped is true at selectFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, selectFilterExprWithUngrouped]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[2]]); }); - it('returns empty array if not found any filter with a specified group', () => { - expect(getFiltersByGroups(filters, ['absent group'])).toEqual([]); + it('returns filters with group if ungrouped is true and groups are not empty at selectFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, selectFilterExprWithGroupAndUngrouped]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[1], filters[2], filters[3]]); }); - it('returns empty array if not groups specified', () => { - expect(getFiltersByGroups(filters, [])).toEqual(filters); + it('returns no filters if no arguments, specified to removeFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, removeFilterExprEmpty]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([]); + }); + + it('returns filters without group, specified to removeFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, removeFilterExprWithGroup]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[0], filters[2]]); }); -}); -describe('extractGroupsFromElementsFilters', () => { - const exprFilters = 'filters'; - const exprRest = 'demodata | plot | render'; - - it('returns groups which are specified at filters expression', () => { - const groups = ['group 1', 'group 2', 'group 3', 'group 4']; - const additionalGroups = [...groups, 'group 5']; - const groupsExpr = groups.map((group) => `group="${group}"`).join(' '); - const additionalGroupsExpr = additionalGroups.map((group) => `group="${group}"`).join(' '); - - expect( - extractGroupsFromElementsFilters( - `${exprFilters} ${groupsExpr} | ${exprFilters} ${additionalGroupsExpr} | ${exprRest}` - ) - ).toEqual(additionalGroups); + it('returns filters without group if ungrouped is true at removeFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, removeFilterExprWithUngrouped]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[0], filters[1], filters[3]]); }); - it('returns empty array if no groups were specified at filters expression', () => { - expect(extractGroupsFromElementsFilters(`${exprFilters} | ${exprRest}`)).toEqual([]); + it('remove filters without group and with specified group if ungrouped is true and groups are not empty at removeFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, removeFilterExprWithGroupAndUngrouped]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[0]]); + }); + + it('should include/exclude filters iteratively', () => { + const filtersExprs = getFiltersAsts([ + kibanaExpr, + selectFilterExprWithGroup, + removeFilterExprWithGroup, + selectFilterExprEmpty, + ]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([]); + }); + + it('should include/exclude filters from global filters if `filters` expression is specified', () => { + const filtersExprs = getFiltersAsts([ + kibanaExpr, + selectFilterExprWithGroup, + removeFilterExprWithGroup, + selectFilterExprEmpty, + filtersExprWithGroup, + ]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[1], filters[3]]); }); }); -describe('extractUngroupedFromElementsFilters', () => { - it('checks if ungrouped filters expression exist at the element', () => { - const expression = - 'filters group="10" group="11" | filters group="15" ungrouped=true | demodata | plot | render'; - const isUngrouped = extractUngroupedFromElementsFilters(expression); - expect(isUngrouped).toBeTruthy(); +describe('getFiltersExprsFromExpression', () => { + it('returns list of filters expressions asts', () => { + const filter1 = 'selectFilter'; + const filter2 = 'filters group="15" ungrouped=true'; + const filter3 = 'removeFilter'; + const expression = `kibana | ${filter1} | ${filter2} | ${filter3} | demodata | plot | render`; + const filtersAsts = getFiltersExprsFromExpression(expression); - const nextExpression = - 'filters group="10" group="11" | filters group="15" | demodata | plot | render'; - const nextIsUngrouped = extractUngroupedFromElementsFilters(nextExpression); - expect(nextIsUngrouped).toBeFalsy(); + expect(filtersAsts).toEqual([filter1, filter2, filter3].map((f) => fromExpression(f).chain[0])); }); }); diff --git a/x-pack/plugins/canvas/public/lib/filter.ts b/x-pack/plugins/canvas/public/lib/filter.ts index 6e9db1757ccc7e..2554ae11220eb7 100644 --- a/x-pack/plugins/canvas/public/lib/filter.ts +++ b/x-pack/plugins/canvas/public/lib/filter.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { fromExpression } from '@kbn/interpreter'; +import { Ast, ExpressionFunctionAST, fromExpression, toExpression } from '@kbn/interpreter'; import { flowRight, get, groupBy } from 'lodash'; import { Filter as FilterType, @@ -14,6 +14,14 @@ import { FlattenFilterViewInstance, } from '../../types/filters'; +const SELECT_FILTER = 'selectFilter'; +const FILTERS = 'filters'; +const REMOVE_FILTER = 'removeFilter'; + +const includeFiltersExpressions = [FILTERS, SELECT_FILTER]; +const excludeFiltersExpressions = [REMOVE_FILTER]; +const filtersExpressions = [...includeFiltersExpressions, ...excludeFiltersExpressions]; + export const defaultFormatter = (value: unknown) => (value || null ? `${value}` : '-'); export const formatFilterView = @@ -55,41 +63,73 @@ export const groupFiltersBy = (filters: FilterType[], groupByField: FilterField) })); }; -export const getFiltersByGroups = ( - filters: string[], - groups: string[], - ungrouped: boolean = false -) => - filters.filter((filter: string) => { - const ast = fromExpression(filter); - const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []); - if (!groups?.length && ungrouped) { - return expGroups.length === 0; - } +const excludeFiltersByGroups = (filters: Ast[], filterExprAst: ExpressionFunctionAST) => { + const groupsToExclude = filterExprAst.arguments.group ?? []; + const removeUngrouped = filterExprAst.arguments.ungrouped?.[0] ?? false; + return filters.filter((filter) => { + const groups: string[] = get(filter, 'chain[0].arguments.filterGroup', []).filter( + (group: string) => group !== '' + ); + const noNeedToExcludeByGroup = !( + groups.length && + groupsToExclude.length && + groupsToExclude.includes(groups[0]) + ); + + const noNeedToExcludeByUngrouped = (removeUngrouped && groups.length) || !removeUngrouped; + const excludeAllFilters = !groupsToExclude.length && !removeUngrouped; - return ( - !groups.length || - (expGroups.length > 0 && expGroups.every((expGroup) => groups.includes(expGroup))) + return !excludeAllFilters && noNeedToExcludeByUngrouped && noNeedToExcludeByGroup; + }); +}; + +const includeFiltersByGroups = ( + filters: Ast[], + filterExprAst: ExpressionFunctionAST, + ignoreUngroupedIfGroups: boolean = false +) => { + const groupsToInclude = filterExprAst.arguments.group ?? []; + const includeOnlyUngrouped = filterExprAst.arguments.ungrouped?.[0] ?? false; + return filters.filter((filter) => { + const groups: string[] = get(filter, 'chain[0].arguments.filterGroup', []).filter( + (group: string) => group !== '' ); + const needToIncludeByGroup = + groups.length && groupsToInclude.length && groupsToInclude.includes(groups[0]); + + const needToIncludeByUngrouped = + includeOnlyUngrouped && + !groups.length && + (ignoreUngroupedIfGroups ? !groupsToInclude.length : true); + + const allowAll = !groupsToInclude.length && !includeOnlyUngrouped; + return needToIncludeByUngrouped || needToIncludeByGroup || allowAll; }); +}; -export const extractGroupsFromElementsFilters = (expr: string) => { - const ast = fromExpression(expr); - const filtersFns = ast.chain.filter((expression) => expression.function === 'filters'); - const groups = filtersFns.reduce((foundGroups, filterFn) => { - const filterGroups = filterFn?.arguments.group?.map((g) => g.toString()) ?? []; - return [...foundGroups, ...filterGroups]; - }, []); - return [...new Set(groups)]; +export const getFiltersByFilterExpressions = ( + filters: string[], + filterExprsAsts: ExpressionFunctionAST[] +) => { + const filtersAst = filters.map((filter) => fromExpression(filter)); + const matchedFiltersAst = filterExprsAsts.reduce((includedFilters, filter) => { + if (excludeFiltersExpressions.includes(filter.function)) { + return excludeFiltersByGroups(includedFilters, filter); + } + const isFiltersExpr = filter.function === FILTERS; + const filtersToInclude = isFiltersExpr ? filtersAst : includedFilters; + return includeFiltersByGroups(filtersToInclude, filter, isFiltersExpr); + }, filtersAst); + + return matchedFiltersAst.map((ast) => toExpression(ast)); }; -export const extractUngroupedFromElementsFilters = (expr: string) => { +export const getFiltersExprsFromExpression = (expr: string) => { const ast = fromExpression(expr); - const filtersFns = ast.chain.filter((expression) => expression.function === 'filters'); - return filtersFns.some((filterFn) => filterFn?.arguments.ungrouped?.[0]); + return ast.chain.filter((expression) => filtersExpressions.includes(expression.function)); }; export const isExpressionWithFilters = (expr: string) => { const ast = fromExpression(expr); - return ast.chain.some((expression) => expression.function === 'filters'); + return ast.chain.some((expression) => filtersExpressions.includes(expression.function)); }; diff --git a/x-pack/plugins/canvas/public/lib/run_interpreter.ts b/x-pack/plugins/canvas/public/lib/run_interpreter.ts deleted file mode 100644 index 77c31b11924c04..00000000000000 --- a/x-pack/plugins/canvas/public/lib/run_interpreter.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 { fromExpression, getType } from '@kbn/interpreter'; -import { pluck } from 'rxjs/operators'; -import { ExpressionValue, ExpressionAstExpression } from 'src/plugins/expressions/public'; -import { pluginServices } from '../services'; - -interface Options { - castToRender?: boolean; -} - -/** - * Meant to be a replacement for plugins/interpreter/interpretAST - */ -export async function interpretAst( - ast: ExpressionAstExpression, - variables: Record, - input: ExpressionValue = null -): Promise { - const context = { variables }; - const { execute } = pluginServices.getServices().expressions; - - return await execute(ast, input, context).getData().pipe(pluck('result')).toPromise(); -} - -/** - * Runs interpreter, usually in the browser - * - * @param {object} ast - Executable AST - * @param {any} input - Initial input for AST execution - * @param {object} variables - Variables to pass in to the intrepreter context - * @param {object} options - * @param {boolean} options.castToRender - try to cast to a type: render object? - * @returns {promise} - */ -export async function runInterpreter( - ast: ExpressionAstExpression, - input: ExpressionValue, - variables: Record, - options: Options = {} -): Promise { - const context = { variables }; - try { - const { execute } = pluginServices.getServices().expressions; - - const renderable = await execute(ast, input, context) - .getData() - .pipe(pluck('result')) - .toPromise(); - - if (getType(renderable) === 'render') { - return renderable; - } - - if (options.castToRender) { - return runInterpreter(fromExpression('render'), renderable, variables, { - castToRender: false, - }); - } - - throw new Error(`Ack! I don't know how to render a '${getType(renderable)}'`); - } catch (err) { - const { error: displayError } = pluginServices.getServices().notify; - displayError(err); - throw err; - } -} diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 8cdc695ebaaba7..1c2ce763f42e24 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -36,7 +36,6 @@ import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public'; import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; import { getPluginApi, CanvasApi } from './plugin_api'; import { setupExpressions } from './setup_expressions'; -import { pluginServiceRegistry } from './services/kibana'; export type { CoreStart, CoreSetup }; @@ -123,6 +122,8 @@ export class CanvasPlugin srcPlugin.start(coreStart, startPlugins); const { pluginServices } = await import('./services'); + const { pluginServiceRegistry } = await import('./services/kibana'); + pluginServices.setRegistry( pluginServiceRegistry.start({ coreStart, diff --git a/x-pack/plugins/canvas/public/services/expressions.ts b/x-pack/plugins/canvas/public/services/expressions.ts index 01bb0adb177117..456a1314bdfffc 100644 --- a/x-pack/plugins/canvas/public/services/expressions.ts +++ b/x-pack/plugins/canvas/public/services/expressions.ts @@ -5,6 +5,4 @@ * 2.0. */ -import { ExpressionsServiceStart } from '../../../../../src/plugins/expressions/public'; - -export type CanvasExpressionsService = ExpressionsServiceStart; +export type { CanvasExpressionsService } from './kibana/expressions'; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/constants.ts b/x-pack/plugins/canvas/public/services/filters.ts similarity index 78% rename from x-pack/plugins/cases/public/components/user_action_tree/constants.ts rename to x-pack/plugins/canvas/public/services/filters.ts index 584194be65f505..1ced3d15f6e100 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/constants.ts +++ b/x-pack/plugins/canvas/public/services/filters.ts @@ -5,4 +5,4 @@ * 2.0. */ -export const DRAFT_COMMENT_STORAGE_ID = 'xpack.cases.commentDraft'; +export type { CanvasFiltersService } from './kibana/filters'; diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index ed55f919e4c767..4bf025c2748594 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -12,6 +12,7 @@ import { PluginServices } from '../../../../../src/plugins/presentation_util/pub import { CanvasCustomElementService } from './custom_element'; import { CanvasEmbeddablesService } from './embeddables'; import { CanvasExpressionsService } from './expressions'; +import { CanvasFiltersService } from './filters'; import { CanvasLabsService } from './labs'; import { CanvasNavLinkService } from './nav_link'; import { CanvasNotifyService } from './notify'; @@ -24,6 +25,7 @@ export interface CanvasPluginServices { customElement: CanvasCustomElementService; embeddables: CanvasEmbeddablesService; expressions: CanvasExpressionsService; + filters: CanvasFiltersService; labs: CanvasLabsService; navLink: CanvasNavLinkService; notify: CanvasNotifyService; @@ -41,6 +43,7 @@ export const useEmbeddablesService = () => (() => pluginServices.getHooks().embeddables.useService())(); export const useExpressionsService = () => (() => pluginServices.getHooks().expressions.useService())(); +export const useFiltersService = () => (() => pluginServices.getHooks().filters.useService())(); export const useLabsService = () => (() => pluginServices.getHooks().labs.useService())(); export const useNavLinkService = () => (() => pluginServices.getHooks().navLink.useService())(); export const useNotifyService = () => (() => pluginServices.getHooks().notify.useService())(); diff --git a/x-pack/plugins/canvas/public/services/kibana/expressions.ts b/x-pack/plugins/canvas/public/services/kibana/expressions.ts index 780de5309d97e1..ea329b63863f8c 100644 --- a/x-pack/plugins/canvas/public/services/kibana/expressions.ts +++ b/x-pack/plugins/canvas/public/services/kibana/expressions.ts @@ -4,16 +4,137 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { fromExpression, getType } from '@kbn/interpreter'; +import { + ExpressionAstExpression, + ExpressionExecutionParams, + ExpressionValue, +} from 'src/plugins/expressions'; +import { pluck } from 'rxjs/operators'; +import { buildEmbeddableFilters } from '../../../common/lib/build_embeddable_filters'; +import { ExpressionsServiceStart } from '../../../../../../src/plugins/expressions/public'; import { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; - import { CanvasStartDeps } from '../../plugin'; -import { CanvasExpressionsService } from '../expressions'; +import { CanvasFiltersService } from './filters'; +import { CanvasNotifyService } from '../notify'; + +interface Options { + castToRender?: boolean; +} + +export class ExpressionsService { + private filters: CanvasFiltersService; + private notify: CanvasNotifyService; + + constructor( + private readonly expressions: ExpressionsServiceStart, + { filters, notify }: CanvasExpressionsServiceRequiredServices + ) { + this.filters = filters; + this.notify = notify; + } + + async interpretAst( + ast: ExpressionAstExpression, + variables: Record, + input: ExpressionValue = null + ) { + const context = await this.getGlobalContext(); + return await this.interpretAstWithContext(ast, input, { + ...(context ?? {}), + variables, + }); + } + + async interpretAstWithContext( + ast: ExpressionAstExpression, + input: ExpressionValue = null, + context?: ExpressionExecutionParams + ): Promise { + return await this.expressions + .execute(ast, input, context) + .getData() + .pipe(pluck('result')) + .toPromise(); + } + + /** + * Runs interpreter, usually in the browser + * + * @param {object} ast - Executable AST + * @param {any} input - Initial input for AST execution + * @param {object} variables - Variables to pass in to the intrepreter context + * @param {object} options + * @param {boolean} options.castToRender - try to cast to a type: render object? + * @returns {Promise} + */ + async runInterpreter( + ast: ExpressionAstExpression, + input: ExpressionValue, + variables: Record, + options: Options = {} + ): Promise { + const context = await this.getGlobalContext(); + const fullContext = { ...(context ?? {}), variables }; + + try { + const renderable = await this.interpretAstWithContext(ast, input, fullContext); + + if (getType(renderable) === 'render') { + return renderable; + } + + if (options.castToRender) { + return this.runInterpreter(fromExpression('render'), renderable, fullContext, { + castToRender: false, + }); + } + + throw new Error(`Ack! I don't know how to render a '${getType(renderable)}'`); + } catch (err) { + this.notify.error(err); + throw err; + } + } + + getRenderer(name: string) { + return this.expressions.getRenderer(name); + } + + getFunctions() { + return this.expressions.getFunctions(); + } + + private async getFilters() { + const filtersList = this.filters.getFilters(); + const context = this.filters.getFiltersContext(); + const filterExpression = filtersList.join(' | '); + const filterAST = fromExpression(filterExpression); + return await this.interpretAstWithContext(filterAST, null, context); + } + + private async getGlobalContext() { + const canvasFilters = await this.getFilters(); + const kibanaFilters = buildEmbeddableFilters(canvasFilters ? canvasFilters.and : []); + return { + searchContext: { ...kibanaFilters }, + }; + } +} + +export type CanvasExpressionsService = ExpressionsService; +export interface CanvasExpressionsServiceRequiredServices { + notify: CanvasNotifyService; + filters: CanvasFiltersService; +} export type CanvasExpressionsServiceFactory = KibanaPluginServiceFactory< CanvasExpressionsService, - CanvasStartDeps + CanvasStartDeps, + CanvasExpressionsServiceRequiredServices >; -export const expressionsServiceFactory: CanvasExpressionsServiceFactory = ({ startPlugins }) => - startPlugins.expressions; +export const expressionsServiceFactory: CanvasExpressionsServiceFactory = ( + { startPlugins }, + requiredServices +) => new ExpressionsService(startPlugins.expressions, requiredServices); diff --git a/x-pack/plugins/canvas/public/services/kibana/filters.ts b/x-pack/plugins/canvas/public/services/kibana/filters.ts new file mode 100644 index 00000000000000..872b6759b389b1 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/kibana/filters.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; +// @ts-expect-error untyped local +import { getState, getStore } from '../../state/store'; +import { State } from '../../../types'; +import { getGlobalFilters, getWorkpadVariablesAsObject } from '../../state/selectors/workpad'; +import { CanvasStartDeps } from '../../plugin'; +// @ts-expect-error untyped local +import { setFilter } from '../../state/actions/elements'; + +export class FiltersService { + constructor() {} + + getFilters(state: State = getState()) { + return getGlobalFilters(state); + } + + updateFilter(filterId: string, filterExpression: string) { + const { dispatch } = getStore(); + dispatch(setFilter(filterExpression, filterId, true)); + } + + getFiltersContext(state: State = getState()) { + const variables = getWorkpadVariablesAsObject(state); + return { variables }; + } +} + +export type CanvasFiltersService = FiltersService; + +export type CanvasFiltersServiceFactory = KibanaPluginServiceFactory< + CanvasFiltersService, + CanvasStartDeps +>; + +export const filtersServiceFactory: CanvasFiltersServiceFactory = () => new FiltersService(); diff --git a/x-pack/plugins/canvas/public/services/kibana/index.ts b/x-pack/plugins/canvas/public/services/kibana/index.ts index 91767947bc0a65..c1ceb531657d00 100644 --- a/x-pack/plugins/canvas/public/services/kibana/index.ts +++ b/x-pack/plugins/canvas/public/services/kibana/index.ts @@ -24,10 +24,12 @@ import { platformServiceFactory } from './platform'; import { reportingServiceFactory } from './reporting'; import { visualizationsServiceFactory } from './visualizations'; import { workpadServiceFactory } from './workpad'; +import { filtersServiceFactory } from './filters'; export { customElementServiceFactory } from './custom_element'; export { embeddablesServiceFactory } from './embeddables'; export { expressionsServiceFactory } from './expressions'; +export { filtersServiceFactory } from './filters'; export { labsServiceFactory } from './labs'; export { notifyServiceFactory } from './notify'; export { platformServiceFactory } from './platform'; @@ -41,7 +43,8 @@ export const pluginServiceProviders: PluginServiceProviders< > = { customElement: new PluginServiceProvider(customElementServiceFactory), embeddables: new PluginServiceProvider(embeddablesServiceFactory), - expressions: new PluginServiceProvider(expressionsServiceFactory), + expressions: new PluginServiceProvider(expressionsServiceFactory, ['filters', 'notify']), + filters: new PluginServiceProvider(filtersServiceFactory), labs: new PluginServiceProvider(labsServiceFactory), navLink: new PluginServiceProvider(navLinkServiceFactory), notify: new PluginServiceProvider(notifyServiceFactory), diff --git a/x-pack/plugins/canvas/public/services/kibana/workpad.ts b/x-pack/plugins/canvas/public/services/kibana/workpad.ts index 9f69d5096237cc..c0ef1097555a63 100644 --- a/x-pack/plugins/canvas/public/services/kibana/workpad.ts +++ b/x-pack/plugins/canvas/public/services/kibana/workpad.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SavedObject } from 'kibana/public'; import { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; import { CanvasStartDeps } from '../../plugin'; @@ -67,6 +68,21 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({ coreStart, return { css: DEFAULT_WORKPAD_CSS, variables: [], ...workpad }; }, + export: async (id: string) => { + const workpad = await coreStart.http.get>( + `${getApiPath()}/export/${id}` + ); + const { attributes } = workpad; + + return { + ...workpad, + attributes: { + ...attributes, + css: attributes.css ?? DEFAULT_WORKPAD_CSS, + variables: attributes.variables ?? [], + }, + }; + }, resolve: async (id: string) => { const { workpad, outcome, aliasId } = await coreStart.http.get( `${getApiPath()}/resolve/${id}` @@ -93,6 +109,14 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({ coreStart, }), }); }, + import: (workpad: CanvasWorkpad) => + coreStart.http.post(`${getApiPath()}/import`, { + body: JSON.stringify({ + ...sanitizeWorkpad({ ...workpad }), + assets: workpad.assets || {}, + variables: workpad.variables || [], + }), + }), createFromTemplate: (templateId: string) => { return coreStart.http.post(getApiPath(), { body: JSON.stringify({ templateId }), diff --git a/x-pack/plugins/canvas/public/services/storybook/workpad.ts b/x-pack/plugins/canvas/public/services/storybook/workpad.ts index 6c77bdb1adeac5..5dd40997900c6f 100644 --- a/x-pack/plugins/canvas/public/services/storybook/workpad.ts +++ b/x-pack/plugins/canvas/public/services/storybook/workpad.ts @@ -31,7 +31,7 @@ type CanvasWorkpadServiceFactory = PluginServiceFactory () => new Promise((resolve) => setTimeout(resolve, time)); -const { findNoTemplates, findNoWorkpads, findSomeTemplates } = stubs; +const { findNoTemplates, findNoWorkpads, findSomeTemplates, importWorkpad } = stubs; const getRandomName = () => { const lorem = @@ -85,6 +85,10 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({ action('workpadService.findTemplates')(); return (hasTemplates ? findSomeTemplates() : findNoTemplates())(); }, + import: (workpad) => { + action('workpadService.import')(workpad); + return importWorkpad(workpad); + }, create: (workpad) => { action('workpadService.create')(workpad); return Promise.resolve(workpad); diff --git a/x-pack/plugins/canvas/public/services/stubs/expressions.ts b/x-pack/plugins/canvas/public/services/stubs/expressions.ts index 6660c1c6efb357..405f2ebe0ba918 100644 --- a/x-pack/plugins/canvas/public/services/stubs/expressions.ts +++ b/x-pack/plugins/canvas/public/services/stubs/expressions.ts @@ -10,11 +10,22 @@ import { plugin } from '../../../../../../src/plugins/expressions/public'; import { functions as functionDefinitions } from '../../../canvas_plugin_src/functions/common'; import { renderFunctions } from '../../../canvas_plugin_src/renderers/core'; import { PluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; -import { CanvasExpressionsService } from '../expressions'; +import { + CanvasExpressionsService, + CanvasExpressionsServiceRequiredServices, + ExpressionsService, +} from '../kibana/expressions'; -type CanvasExpressionsServiceFactory = PluginServiceFactory; +type CanvasExpressionsServiceFactory = PluginServiceFactory< + CanvasExpressionsService, + {}, + CanvasExpressionsServiceRequiredServices +>; -export const expressionsServiceFactory: CanvasExpressionsServiceFactory = () => { +export const expressionsServiceFactory: CanvasExpressionsServiceFactory = ( + params, + requiredServices +) => { const placeholder = {} as any; const expressionsPlugin = plugin(placeholder); const setup = expressionsPlugin.setup(placeholder); @@ -25,5 +36,5 @@ export const expressionsServiceFactory: CanvasExpressionsServiceFactory = () => expressionsService.registerRenderer(fn as unknown as AnyExpressionRenderDefinition); }); - return expressionsService; + return new ExpressionsService(expressionsService, requiredServices); }; diff --git a/x-pack/plugins/canvas/public/services/stubs/filters.ts b/x-pack/plugins/canvas/public/services/stubs/filters.ts new file mode 100644 index 00000000000000..972dbfd6dc0e4f --- /dev/null +++ b/x-pack/plugins/canvas/public/services/stubs/filters.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; +import { CanvasFiltersService } from '../filters'; + +export type CanvasFiltersServiceFactory = PluginServiceFactory; + +const noop = (..._args: any[]): any => {}; + +export const filtersServiceFactory: CanvasFiltersServiceFactory = () => ({ + getFilters: () => [ + 'exactly value="machine-learning" column="project1" filterGroup="Group 1"', + 'exactly value="kibana" column="project2" filterGroup="Group 1"', + 'time column="@timestamp1" from="2021-11-02 17:13:18" to="2021-11-09 17:13:18" filterGroup="Some group"', + ], + updateFilter: noop, + getFiltersContext: () => ({ variables: {} }), +}); diff --git a/x-pack/plugins/canvas/public/services/stubs/index.ts b/x-pack/plugins/canvas/public/services/stubs/index.ts index 2216013a29c129..d90b1a3c92201a 100644 --- a/x-pack/plugins/canvas/public/services/stubs/index.ts +++ b/x-pack/plugins/canvas/public/services/stubs/index.ts @@ -24,9 +24,11 @@ import { platformServiceFactory } from './platform'; import { reportingServiceFactory } from './reporting'; import { visualizationsServiceFactory } from './visualizations'; import { workpadServiceFactory } from './workpad'; +import { filtersServiceFactory } from './filters'; export { customElementServiceFactory } from './custom_element'; export { expressionsServiceFactory } from './expressions'; +export { filtersServiceFactory } from './filters'; export { labsServiceFactory } from './labs'; export { navLinkServiceFactory } from './nav_link'; export { notifyServiceFactory } from './notify'; @@ -38,7 +40,8 @@ export { workpadServiceFactory } from './workpad'; export const pluginServiceProviders: PluginServiceProviders = { customElement: new PluginServiceProvider(customElementServiceFactory), embeddables: new PluginServiceProvider(embeddablesServiceFactory), - expressions: new PluginServiceProvider(expressionsServiceFactory), + expressions: new PluginServiceProvider(expressionsServiceFactory, ['filters', 'notify']), + filters: new PluginServiceProvider(filtersServiceFactory), labs: new PluginServiceProvider(labsServiceFactory), navLink: new PluginServiceProvider(navLinkServiceFactory), notify: new PluginServiceProvider(notifyServiceFactory), diff --git a/x-pack/plugins/canvas/public/services/stubs/workpad.ts b/x-pack/plugins/canvas/public/services/stubs/workpad.ts index c10244038750d6..6268fa128df0fe 100644 --- a/x-pack/plugins/canvas/public/services/stubs/workpad.ts +++ b/x-pack/plugins/canvas/public/services/stubs/workpad.ts @@ -6,13 +6,12 @@ */ import moment from 'moment'; - import { PluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; // @ts-expect-error -import { getDefaultWorkpad } from '../../state/defaults'; +import { getDefaultWorkpad, getExportedWorkpad } from '../../state/defaults'; import { CanvasWorkpadService } from '../workpad'; -import { CanvasTemplate } from '../../../types'; +import { CanvasTemplate, CanvasWorkpad } from '../../../types'; type CanvasWorkpadServiceFactory = PluginServiceFactory; @@ -94,6 +93,7 @@ export const findNoTemplates = .then(() => getNoTemplates()); }; +export const importWorkpad = (workpad: CanvasWorkpad) => Promise.resolve(workpad); export const getNoTemplates = () => ({ templates: [] }); export const getSomeTemplates = () => ({ templates }); @@ -103,6 +103,7 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = () => ({ Promise.resolve({ outcome: 'exactMatch', workpad: { ...getDefaultWorkpad(), id } }), findTemplates: findNoTemplates(), create: (workpad) => Promise.resolve(workpad), + import: (workpad) => importWorkpad(workpad), createFromTemplate: (_templateId: string) => Promise.resolve(getDefaultWorkpad()), find: findNoWorkpads(), remove: (_id: string) => Promise.resolve(), diff --git a/x-pack/plugins/canvas/public/services/workpad.ts b/x-pack/plugins/canvas/public/services/workpad.ts index 8e77ab3f321ef8..233b1a70ff7f66 100644 --- a/x-pack/plugins/canvas/public/services/workpad.ts +++ b/x-pack/plugins/canvas/public/services/workpad.ts @@ -25,10 +25,12 @@ export interface ResolveWorkpadResponse { outcome: SavedObjectsResolveResponse['outcome']; aliasId?: SavedObjectsResolveResponse['alias_target_id']; } + export interface CanvasWorkpadService { get: (id: string) => Promise; resolve: (id: string) => Promise; create: (workpad: CanvasWorkpad) => Promise; + import: (workpad: CanvasWorkpad) => Promise; createFromTemplate: (templateId: string) => Promise; find: (term: string) => Promise; remove: (id: string) => Promise; diff --git a/x-pack/plugins/canvas/public/state/actions/elements.js b/x-pack/plugins/canvas/public/state/actions/elements.js index bcc02c3cbc2cd0..72186abd38c948 100644 --- a/x-pack/plugins/canvas/public/state/actions/elements.js +++ b/x-pack/plugins/canvas/public/state/actions/elements.js @@ -20,7 +20,6 @@ import { import { getValue as getResolvedArgsValue } from '../selectors/resolved_args'; import { getDefaultElement } from '../defaults'; import { ErrorStrings } from '../../../i18n'; -import { runInterpreter, interpretAst } from '../../lib/run_interpreter'; import { subMultitree } from '../../lib/aeroelastic/functional'; import { pluginServices } from '../../services'; import { selectToplevelNodes } from './transient'; @@ -101,11 +100,16 @@ export const fetchContext = createThunk( }); const variables = getWorkpadVariablesAsObject(getState()); + + const { expressions } = pluginServices.getServices(); const elementWithNewAst = set(element, pathToTarget, astChain); + // get context data from a partial AST - return interpretAst(elementWithNewAst.ast, variables, prevContextValue).then((value) => { - dispatch(args.setValue({ path: contextPath, value })); - }); + return expressions + .interpretAst(elementWithNewAst.ast, variables, prevContextValue) + .then((value) => { + dispatch(args.setValue({ path: contextPath, value })); + }); } ); @@ -124,14 +128,14 @@ const fetchRenderableWithContextFn = ({ dispatch, getState }, element, ast, cont }); const variables = getWorkpadVariablesAsObject(getState()); - - return runInterpreter(ast, context, variables, { castToRender: true }) + const { expressions, notify } = pluginServices.getServices(); + return expressions + .runInterpreter(ast, context, variables, { castToRender: true }) .then((renderable) => { dispatch(getAction(renderable)); }) .catch((err) => { - const notifyService = pluginServices.getServices().notify; - notifyService.error(err); + notify.error(err); dispatch(getAction(err)); }); }; @@ -171,12 +175,13 @@ export const fetchAllRenderables = createThunk( const argumentPath = [element.id, 'expressionRenderable']; const variables = getWorkpadVariablesAsObject(getState()); + const { expressions, notify } = pluginServices.getServices(); - return runInterpreter(ast, null, variables, { castToRender: true }) + return expressions + .runInterpreter(ast, null, variables, { castToRender: true }) .then((renderable) => ({ path: argumentPath, value: renderable })) .catch((err) => { - const notifyService = pluginServices.getServices().notify; - notifyService.error(err); + notify.error(err); return { path: argumentPath, value: err }; }); }); diff --git a/x-pack/plugins/canvas/public/state/defaults.js b/x-pack/plugins/canvas/public/state/defaults.js index 40e8425c98ff07..a4a38d50388d56 100644 --- a/x-pack/plugins/canvas/public/state/defaults.js +++ b/x-pack/plugins/canvas/public/state/defaults.js @@ -87,6 +87,14 @@ export const getDefaultWorkpad = () => { }; }; +export const getExportedWorkpad = () => { + const workpad = getDefaultWorkpad(); + return { + id: workpad.id, + attributes: workpad, + }; +}; + export const getDefaultSidebar = () => ({ groupFiltersByOption: DEFAULT_GROUP_BY_FIELD, }); diff --git a/x-pack/plugins/canvas/public/state/selectors/workpad.ts b/x-pack/plugins/canvas/public/state/selectors/workpad.ts index ac94ccc562e889..557a6b8acc4e71 100644 --- a/x-pack/plugins/canvas/public/state/selectors/workpad.ts +++ b/x-pack/plugins/canvas/public/state/selectors/workpad.ts @@ -27,6 +27,7 @@ import { ExpressionAstFunction, ExpressionAstExpression, } from '../../../types'; +import { isExpressionWithFilters } from '../../lib/filter'; type Modify = Pick> & R; type WorkpadInfo = Modify; @@ -248,7 +249,7 @@ function extractFilterGroups( // TODO: we always get a function here, right? const { function: fn, arguments: args } = item; - if (fn === 'filters') { + if (isExpressionWithFilters(fn)) { // we have a filter function, extract groups from args return groups.concat( buildGroupValues(args, (argValue) => { diff --git a/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts b/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts index 216cdc0970dc40..13e4e34b20b669 100644 --- a/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts +++ b/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts @@ -12,6 +12,7 @@ export interface MockWorkpadRouteContext extends CanvasRouteHandlerContext { workpad: { create: jest.Mock; get: jest.Mock; + import: jest.Mock; update: jest.Mock; resolve: jest.Mock; }; @@ -23,6 +24,7 @@ export const workpadRouteContextMock = { workpad: { create: jest.fn(), get: jest.fn(), + import: jest.fn(), update: jest.fn(), resolve: jest.fn(), }, diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index ebe43ba76a46ac..27b6186216b694 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -23,7 +23,8 @@ import { initRoutes } from './routes'; import { registerCanvasUsageCollector } from './collectors'; import { loadSampleData } from './sample_data'; import { setupInterpreter } from './setup_interpreter'; -import { customElementType, workpadType, workpadTemplateType } from './saved_objects'; +import { customElementType, workpadTypeFactory, workpadTemplateType } from './saved_objects'; +import type { CanvasSavedObjectTypeMigrationsDeps } from './saved_objects/migrations'; import { initializeTemplates } from './templates'; import { essqlSearchStrategyProvider } from './lib/essql_strategy'; import { getUISettings } from './ui_settings'; @@ -53,10 +54,18 @@ export class CanvasPlugin implements Plugin { public setup(coreSetup: CoreSetup, plugins: PluginsSetup) { const expressionsFork = plugins.expressions.fork(); + setupInterpreter(expressionsFork, { + embeddablePersistableStateService: { + extract: plugins.embeddable.extract, + inject: plugins.embeddable.inject, + }, + }); + + const deps: CanvasSavedObjectTypeMigrationsDeps = { expressions: expressionsFork }; coreSetup.uiSettings.register(getUISettings()); - coreSetup.savedObjects.registerType(customElementType); - coreSetup.savedObjects.registerType(workpadType); - coreSetup.savedObjects.registerType(workpadTemplateType); + coreSetup.savedObjects.registerType(customElementType(deps)); + coreSetup.savedObjects.registerType(workpadTypeFactory(deps)); + coreSetup.savedObjects.registerType(workpadTemplateType(deps)); plugins.features.registerKibanaFeature(getCanvasFeature(plugins)); @@ -84,13 +93,6 @@ export class CanvasPlugin implements Plugin { const kibanaIndex = coreSetup.savedObjects.getKibanaIndex(); registerCanvasUsageCollector(plugins.usageCollection, kibanaIndex); - setupInterpreter(expressionsFork, { - embeddablePersistableStateService: { - extract: plugins.embeddable.extract, - inject: plugins.embeddable.inject, - }, - }); - coreSetup.getStartServices().then(([_, depsStart]) => { const strategy = essqlSearchStrategyProvider(); plugins.data.search.registerSearchStrategy(ESSQL_SEARCH_STRATEGY, strategy); diff --git a/x-pack/plugins/canvas/server/routes/workpad/import.ts b/x-pack/plugins/canvas/server/routes/workpad/import.ts new file mode 100644 index 00000000000000..35d362f43beccf --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/import.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RouteInitializerDeps } from '../'; +import { API_ROUTE_WORKPAD_IMPORT } from '../../../common/lib/constants'; +import { ImportedCanvasWorkpad } from '../../../types'; +import { ImportedWorkpadSchema } from './workpad_schema'; +import { okResponse } from '../ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +const createRequestBodySchema = ImportedWorkpadSchema; + +export function initializeImportWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.post( + { + path: `${API_ROUTE_WORKPAD_IMPORT}`, + validate: { + body: createRequestBodySchema, + }, + options: { + body: { + maxBytes: 26214400, + accepts: ['application/json'], + }, + }, + }, + catchErrorHandler(async (context, request, response) => { + const workpad = request.body as ImportedCanvasWorkpad; + + const createdObject = await context.canvas.workpad.import(workpad); + + return response.ok({ + body: { ...okResponse, id: createdObject.id }, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/index.ts b/x-pack/plugins/canvas/server/routes/workpad/index.ts index 8483642e59c5aa..b97d58ee232f13 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/index.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/index.ts @@ -9,6 +9,7 @@ import { RouteInitializerDeps } from '../'; import { initializeFindWorkpadsRoute } from './find'; import { initializeGetWorkpadRoute } from './get'; import { initializeCreateWorkpadRoute } from './create'; +import { initializeImportWorkpadRoute } from './import'; import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update'; import { initializeDeleteWorkpadRoute } from './delete'; import { initializeResolveWorkpadRoute } from './resolve'; @@ -18,6 +19,7 @@ export function initWorkpadRoutes(deps: RouteInitializerDeps) { initializeResolveWorkpadRoute(deps); initializeGetWorkpadRoute(deps); initializeCreateWorkpadRoute(deps); + initializeImportWorkpadRoute(deps); initializeUpdateWorkpadRoute(deps); initializeUpdateWorkpadAssetsRoute(deps); initializeDeleteWorkpadRoute(deps); diff --git a/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts index 9bde26298185b0..473b46d470265a 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; +import { TypeOf, schema } from '@kbn/config-schema'; export const PositionSchema = schema.object({ angle: schema.number(), @@ -18,30 +18,30 @@ export const PositionSchema = schema.object({ export const WorkpadElementSchema = schema.object({ expression: schema.string(), - filter: schema.maybe(schema.nullable(schema.string())), + filter: schema.nullable(schema.string({ defaultValue: '' })), id: schema.string(), position: PositionSchema, }); export const WorkpadPageSchema = schema.object({ elements: schema.arrayOf(WorkpadElementSchema), - groups: schema.maybe( - schema.arrayOf( - schema.object({ - id: schema.string(), - position: PositionSchema, - }) - ) + groups: schema.arrayOf( + schema.object({ + id: schema.string(), + position: PositionSchema, + }), + { defaultValue: [] } ), id: schema.string(), style: schema.recordOf(schema.string(), schema.string()), - transition: schema.maybe( - schema.oneOf([ - schema.object({}), + transition: schema.oneOf( + [ + schema.object({}, { defaultValue: {} }), schema.object({ name: schema.string(), }), - ]) + ], + { defaultValue: {} } ), }); @@ -55,44 +55,71 @@ export const WorkpadAssetSchema = schema.object({ export const WorkpadVariable = schema.object({ name: schema.string(), value: schema.oneOf([schema.string(), schema.number(), schema.boolean()]), - type: schema.string(), + type: schema.string({ + validate: (type) => { + const validTypes = ['string', 'number', 'boolean']; + if (type && !validTypes.includes(type)) { + return `${type} is invalid type for a variable. Valid types: ${validTypes.join(', ')}.`; + } + }, + }), }); -export const WorkpadSchema = schema.object( - { - '@created': schema.maybe(schema.string()), - '@timestamp': schema.maybe(schema.string()), - assets: schema.maybe(schema.recordOf(schema.string(), WorkpadAssetSchema)), - colors: schema.arrayOf(schema.string()), - css: schema.string(), - variables: schema.arrayOf(WorkpadVariable), - height: schema.number(), - id: schema.string(), - isWriteable: schema.maybe(schema.boolean()), - name: schema.string(), - page: schema.number(), - pages: schema.arrayOf(WorkpadPageSchema), - width: schema.number(), - }, - { - validate: (workpad) => { - // Validate unique page ids - const pageIdsArray = workpad.pages.map((page) => page.id); - const pageIdsSet = new Set(pageIdsArray); +const commonWorkpadFields = { + '@created': schema.maybe(schema.string()), + '@timestamp': schema.maybe(schema.string()), + colors: schema.arrayOf(schema.string()), + css: schema.string(), + variables: schema.arrayOf(WorkpadVariable), + height: schema.number(), + id: schema.maybe(schema.string()), + isWriteable: schema.maybe(schema.boolean()), + name: schema.string(), + page: schema.number(), + pages: schema.arrayOf(WorkpadPageSchema), + width: schema.number(), +}; - if (pageIdsArray.length !== pageIdsSet.size) { - return 'Page Ids are not unique'; - } +const WorkpadSchemaWithoutValidation = schema.object({ + assets: schema.maybe(schema.recordOf(schema.string(), WorkpadAssetSchema)), + ...commonWorkpadFields, +}); - // Validate unique element ids - const elementIdsArray = workpad.pages - .map((page) => page.elements.map((element) => element.id)) - .flat(); - const elementIdsSet = new Set(elementIdsArray); +const ImportedWorkpadSchemaWithoutValidation = schema.object({ + assets: schema.recordOf(schema.string(), WorkpadAssetSchema), + ...commonWorkpadFields, +}); - if (elementIdsArray.length !== elementIdsSet.size) { - return 'Element Ids are not unique'; - } - }, +const validate = (workpad: TypeOf) => { + // Validate unique page ids + const pageIdsArray = workpad.pages.map((page) => page.id); + const pageIdsSet = new Set(pageIdsArray); + + if (pageIdsArray.length !== pageIdsSet.size) { + return 'Page Ids are not unique'; + } + + // Validate unique element ids + const elementIdsArray = workpad.pages + .map((page) => page.elements.map((element) => element.id)) + .flat(); + const elementIdsSet = new Set(elementIdsArray); + + if (elementIdsArray.length !== elementIdsSet.size) { + return 'Element Ids are not unique'; + } +}; + +export const WorkpadSchema = WorkpadSchemaWithoutValidation.extends( + {}, + { + validate, + } +); + +export const ImportedWorkpadSchema = ImportedWorkpadSchemaWithoutValidation.extends( + {}, + { + validate, } ); diff --git a/x-pack/plugins/canvas/server/saved_objects/custom_element.ts b/x-pack/plugins/canvas/server/saved_objects/custom_element.ts index d62642f5619ea0..82305b2fdd95f1 100644 --- a/x-pack/plugins/canvas/server/saved_objects/custom_element.ts +++ b/x-pack/plugins/canvas/server/saved_objects/custom_element.ts @@ -7,8 +7,9 @@ import { SavedObjectsType } from 'src/core/server'; import { CUSTOM_ELEMENT_TYPE } from '../../common/lib/constants'; +import { customElementMigrationsFactory, CanvasSavedObjectTypeMigrationsDeps } from './migrations'; -export const customElementType: SavedObjectsType = { +export const customElementType = (deps: CanvasSavedObjectTypeMigrationsDeps): SavedObjectsType => ({ name: CUSTOM_ELEMENT_TYPE, hidden: false, namespaceType: 'multiple-isolated', @@ -31,7 +32,7 @@ export const customElementType: SavedObjectsType = { '@created': { type: 'date' }, }, }, - migrations: {}, + migrations: customElementMigrationsFactory(deps), management: { icon: 'canvasApp', defaultSearchField: 'name', @@ -40,4 +41,4 @@ export const customElementType: SavedObjectsType = { return obj.attributes.displayName; }, }, -}; +}); diff --git a/x-pack/plugins/canvas/server/saved_objects/index.ts b/x-pack/plugins/canvas/server/saved_objects/index.ts index dfc27c4b6fa665..9e7cd8644c7c79 100644 --- a/x-pack/plugins/canvas/server/saved_objects/index.ts +++ b/x-pack/plugins/canvas/server/saved_objects/index.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { workpadType } from './workpad'; +import { workpadTypeFactory } from './workpad'; import { customElementType } from './custom_element'; import { workpadTemplateType } from './workpad_template'; -export { customElementType, workpadType, workpadTemplateType }; +export { customElementType, workpadTypeFactory, workpadTemplateType }; diff --git a/x-pack/plugins/canvas/server/saved_objects/migrations/expressions.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/expressions.ts new file mode 100644 index 00000000000000..20eba14c5cff0d --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/expressions.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Ast, fromExpression, toExpression } from '@kbn/interpreter'; +import { Serializable } from '@kbn/utility-types'; +import { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { flowRight, mapValues } from 'lodash'; +import { + CanvasElement, + CanvasTemplateElement, + CanvasTemplate, + CustomElement, + CustomElementContent, + CustomElementNode, +} from '../../../types'; +import { + MigrateFunction, + MigrateFunctionsObject, +} from '../../../../../../src/plugins/kibana_utils/common'; +import { WorkpadAttributes } from '../../routes/workpad/workpad_attributes'; +import { CanvasSavedObjectTypeMigrationsDeps } from './types'; + +type ToSerializable = { + [K in keyof Type]: Type[K] extends unknown[] + ? ToSerializable + : Type[K] extends {} + ? ToSerializable + : Serializable; +}; + +type ExprAst = ToSerializable; + +interface CommonPage { + elements?: T[]; +} +interface CommonWorkpad, U> { + pages?: T[]; +} + +type MigrationFn = ( + migrate: MigrateFunction, + version: string +) => SavedObjectMigrationFn; + +const toAst = (expression: string): ExprAst => fromExpression(expression); +const fromAst = (ast: Ast): string => toExpression(ast); + +const migrateExpr = (expr: string, migrateFn: MigrateFunction) => + flowRight(fromAst, migrateFn, toAst)(expr); + +const migrateWorkpadElement = + (migrate: MigrateFunction) => + ({ filter, expression, ...element }: CanvasElement | CustomElementNode) => ({ + ...element, + filter: filter ? migrateExpr(filter, migrate) : filter, + expression: expression ? migrateExpr(expression, migrate) : expression, + }); + +const migrateTemplateElement = + (migrate: MigrateFunction) => + ({ expression, ...element }: CanvasTemplateElement) => ({ + ...element, + expression: expression ? migrateExpr(expression, migrate) : expression, + }); + +const migrateWorkpadElements = , U>( + doc: SavedObjectUnsanitizedDoc | undefined>, + migrateElementFn: any +) => { + if ( + typeof doc.attributes !== 'object' || + doc.attributes === null || + doc.attributes === undefined + ) { + return doc; + } + + const { pages } = doc.attributes; + + const newPages = pages?.map((page) => { + const { elements } = page; + const newElements = elements?.map(migrateElementFn); + return { ...page, elements: newElements }; + }); + + return { ...doc, attributes: { ...doc.attributes, pages: newPages } }; +}; + +const migrateTemplateWorkpadExpressions: MigrationFn = + (migrate) => (doc) => + migrateWorkpadElements(doc, migrateTemplateElement(migrate)); + +const migrateWorkpadExpressionsAndFilters: MigrationFn = (migrate) => (doc) => + migrateWorkpadElements(doc, migrateWorkpadElement(migrate)); + +const migrateCustomElementExpressionsAndFilters: MigrationFn = + (migrate) => (doc) => { + if ( + typeof doc.attributes !== 'object' || + doc.attributes === null || + doc.attributes === undefined + ) { + return doc; + } + + const { content } = doc.attributes; + const { selectedNodes = [] }: CustomElementContent = content + ? JSON.parse(content) + : { selectedNodes: [] }; + + const newSelectedNodes = selectedNodes.map((element) => { + const newElement = migrateWorkpadElement(migrate)(element); + return { ...element, ...newElement, ast: toAst(newElement.expression) }; + }); + + const newContent = JSON.stringify({ selectedNodes: newSelectedNodes }); + return { ...doc, attributes: { ...doc.attributes, content: newContent } }; + }; + +export const workpadExpressionsMigrationsFactory = ({ + expressions, +}: CanvasSavedObjectTypeMigrationsDeps) => + mapValues>( + expressions.getAllMigrations(), + migrateWorkpadExpressionsAndFilters + ) as MigrateFunctionsObject; + +export const templateWorkpadExpressionsMigrationsFactory = ({ + expressions, +}: CanvasSavedObjectTypeMigrationsDeps) => + mapValues>( + expressions.getAllMigrations(), + migrateTemplateWorkpadExpressions + ) as MigrateFunctionsObject; + +export const customElementExpressionsMigrationsFactory = ({ + expressions, +}: CanvasSavedObjectTypeMigrationsDeps) => + mapValues>( + expressions.getAllMigrations(), + migrateCustomElementExpressionsAndFilters + ) as MigrateFunctionsObject; diff --git a/x-pack/plugins/canvas/server/saved_objects/migrations/index.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/index.ts new file mode 100644 index 00000000000000..88913b50c3c4ca --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + customElementExpressionsMigrationsFactory, + templateWorkpadExpressionsMigrationsFactory, + workpadExpressionsMigrationsFactory, +} from './expressions'; +import { CanvasSavedObjectTypeMigrationsDeps } from './types'; +import { workpadMigrationsFactory as workpadMigrationsFactoryFn } from './workpad'; +import { mergeMigrationFunctionMaps } from '../../../../../../src/plugins/kibana_utils/common'; + +export const workpadMigrationsFactory = (deps: CanvasSavedObjectTypeMigrationsDeps) => + mergeMigrationFunctionMaps( + workpadMigrationsFactoryFn(deps), + workpadExpressionsMigrationsFactory(deps) + ); + +export const templateWorkpadMigrationsFactory = (deps: CanvasSavedObjectTypeMigrationsDeps) => + templateWorkpadExpressionsMigrationsFactory(deps); + +export const customElementMigrationsFactory = (deps: CanvasSavedObjectTypeMigrationsDeps) => + customElementExpressionsMigrationsFactory(deps); + +export type { CanvasSavedObjectTypeMigrationsDeps } from './types'; diff --git a/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.ts index c9c36fd7b26a92..f1dcbd5fe9e7c4 100644 --- a/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.ts +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.ts @@ -7,7 +7,7 @@ import { SavedObjectMigrationFn } from 'src/core/server'; -export const removeAttributesId: SavedObjectMigrationFn = (doc) => { +export const removeAttributesId: SavedObjectMigrationFn = (doc) => { if (typeof doc.attributes === 'object' && doc.attributes !== null) { delete (doc.attributes as any).id; } diff --git a/x-pack/plugins/canvas/server/saved_objects/migrations/types.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/types.ts new file mode 100644 index 00000000000000..18ce0bd88cb696 --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ExpressionsService } from 'src/plugins/expressions/public'; + +export interface CanvasSavedObjectTypeMigrationsDeps { + expressions: ExpressionsService; +} diff --git a/x-pack/plugins/canvas/server/saved_objects/migrations/workpad.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/workpad.ts new file mode 100644 index 00000000000000..d4d7e2b4297114 --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/workpad.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MigrateFunctionsObject } from 'src/plugins/kibana_utils/common'; +import { removeAttributesId } from './remove_attributes_id'; +import { CanvasSavedObjectTypeMigrationsDeps } from './types'; + +export const workpadMigrationsFactory = (deps: CanvasSavedObjectTypeMigrationsDeps) => + ({ + '7.0.0': removeAttributesId, + } as unknown as MigrateFunctionsObject); diff --git a/x-pack/plugins/canvas/server/saved_objects/workpad.ts b/x-pack/plugins/canvas/server/saved_objects/workpad.ts index a8f0f3daf21757..db22025e625e8c 100644 --- a/x-pack/plugins/canvas/server/saved_objects/workpad.ts +++ b/x-pack/plugins/canvas/server/saved_objects/workpad.ts @@ -7,9 +7,12 @@ import { SavedObjectsType } from 'src/core/server'; import { CANVAS_TYPE } from '../../common/lib/constants'; -import { removeAttributesId } from './migrations/remove_attributes_id'; +import { workpadMigrationsFactory } from './migrations'; +import type { CanvasSavedObjectTypeMigrationsDeps } from './migrations'; -export const workpadType: SavedObjectsType = { +export const workpadTypeFactory = ( + deps: CanvasSavedObjectTypeMigrationsDeps +): SavedObjectsType => ({ name: CANVAS_TYPE, hidden: false, namespaceType: 'multiple-isolated', @@ -29,9 +32,7 @@ export const workpadType: SavedObjectsType = { '@created': { type: 'date' }, }, }, - migrations: { - '7.0.0': removeAttributesId, - }, + migrations: workpadMigrationsFactory(deps), management: { importableAndExportable: true, icon: 'canvasApp', @@ -46,4 +47,4 @@ export const workpadType: SavedObjectsType = { }; }, }, -}; +}); diff --git a/x-pack/plugins/canvas/server/saved_objects/workpad_template.ts b/x-pack/plugins/canvas/server/saved_objects/workpad_template.ts index eff7f45dcadae5..a55c7348c62bb9 100644 --- a/x-pack/plugins/canvas/server/saved_objects/workpad_template.ts +++ b/x-pack/plugins/canvas/server/saved_objects/workpad_template.ts @@ -7,8 +7,14 @@ import { SavedObjectsType } from 'src/core/server'; import { TEMPLATE_TYPE } from '../../common/lib/constants'; +import { + CanvasSavedObjectTypeMigrationsDeps, + templateWorkpadMigrationsFactory, +} from './migrations'; -export const workpadTemplateType: SavedObjectsType = { +export const workpadTemplateType = ( + deps: CanvasSavedObjectTypeMigrationsDeps +): SavedObjectsType => ({ name: TEMPLATE_TYPE, hidden: false, namespaceType: 'agnostic', @@ -44,7 +50,7 @@ export const workpadTemplateType: SavedObjectsType = { }, }, }, - migrations: {}, + migrations: templateWorkpadMigrationsFactory(deps), management: { importableAndExportable: false, icon: 'canvasApp', @@ -53,4 +59,4 @@ export const workpadTemplateType: SavedObjectsType = { return obj.attributes.name; }, }, -}; +}); diff --git a/x-pack/plugins/canvas/server/templates/pitch_presentation.ts b/x-pack/plugins/canvas/server/templates/pitch_presentation.ts index a9f09ada989c6e..82ad535852c970 100644 --- a/x-pack/plugins/canvas/server/templates/pitch_presentation.ts +++ b/x-pack/plugins/canvas/server/templates/pitch_presentation.ts @@ -63,7 +63,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "# Sample."\n| render css=".canvasRenderEl h1 {\ntext-align: center;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "# Sample."\n| render css=".canvasRenderEl h1 {\ntext-align: center;\n}"', }, { id: 'element-33286979-7ea0-41ce-9835-b3bf07f09272', @@ -76,7 +76,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### This is a subtitle"\n| render css=".canvasRenderEl h3 {\ntext-align: center;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### This is a subtitle"\n| render css=".canvasRenderEl h3 {\ntext-align: center;\n}"', }, { id: 'element-1e3b3ffe-4ed8-4376-aad3-77e06d29cafe', @@ -89,7 +89,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "Footnote can go here"\n| render \n css=".canvasRenderEl p {\ntext-align: center;\ncolor: #FFFFFF;\nfont-size: 18px;\nopacity: .7;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "Footnote can go here"\n| render \n css=".canvasRenderEl p {\ntext-align: center;\ncolor: #FFFFFF;\nfont-size: 18px;\nopacity: .7;\n}"', }, { id: 'element-5b5035a3-d5b7-4483-a240-2cf80f5e0acf', @@ -150,7 +150,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render', }, { id: 'element-96a390b6-3d0a-4372-89cb-3ff38eec9565', @@ -162,7 +163,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "## Half text, half _image._"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "## Half text, half _image._"\n| render', }, { id: 'element-118b848d-0f89-4d20-868c-21597b7fd5e0', @@ -188,7 +190,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', }, ], groups: [], @@ -223,7 +225,7 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "##### BIOS"\n| render', + expression: 'kibana\n| selectFilter\n| demodata\n| markdown "##### BIOS"\n| render', }, { id: 'element-e2c658ee-7614-4d92-a46e-2b1a81a24485', @@ -236,7 +238,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Jane Doe" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Jane Doe" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-3d16765e-5251-4954-8e2a-6c64ed465b73', @@ -249,7 +251,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Developer" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Developer" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', }, { id: 'element-624675cf-46e9-4545-b86a-5409bbe53ac1', @@ -262,7 +264,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. " \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. " \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-dc841809-d2a9-491b-b44f-be92927b8034', @@ -301,7 +303,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. " \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. " \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-62f241ec-71ce-4edb-a27b-0de990522d20', @@ -314,7 +316,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Designer" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Designer" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', }, { id: 'element-aa6c07e0-937f-4362-9d52-f70738faa0c5', @@ -340,7 +342,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## John Smith" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown "## John Smith" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, ], groups: [], @@ -388,7 +390,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "##### CATEGORY 10"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 10"\n| render', }, { id: 'element-96be0724-0945-4802-8929-1dc456192fb5', @@ -401,7 +404,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Another page style."\n| render css=".canvasRenderEl h2 {\nfont-size: 64px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Another page style."\n| render css=".canvasRenderEl h2 {\nfont-size: 64px;\n}"', }, { id: 'element-3b4ba0ff-7f95-460e-9fa6-0cbb0f8f3df8', @@ -427,7 +430,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', }, { id: 'element-0b9aa82b-fb0c-4000-805b-146cc9280bc5', @@ -440,7 +443,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Introduction"\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Introduction"\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', }, ], groups: [], @@ -489,7 +492,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', }, { id: 'element-1ba728f0-f645-4910-9d32-fa5b5820a94c', @@ -502,7 +505,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-db9051eb-7699-4883-b67f-945979cf5650', @@ -528,7 +531,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', }, { id: 'element-fc11525c-2d9c-4a7b-9d96-d54e7bc6479b', @@ -554,7 +557,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-eb9a8883-de47-4a46-9400-b7569f9e69e6', @@ -567,7 +570,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', }, { id: 'element-20c1c86a-658b-4bd2-8326-f987ef84e730', @@ -580,7 +583,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-335db0c3-f678-4cb8-8b93-a6494f1787f5', @@ -593,7 +596,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', }, { id: 'element-079d3cbf-8b15-4ce2-accb-6ba04481019d', @@ -667,7 +670,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render', }, { id: 'element-0f2b9268-f0bd-41b7-abc8-5593276f26fa', @@ -680,7 +684,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Bold title text goes _here_."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Bold title text goes _here_."\n| render', }, { id: 'element-4f4b503e-f1ef-4ab7-aa1d-5d95b3e2e605', @@ -706,7 +710,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', }, { id: 'element-f3f28541-06fe-47ea-89b7-1c5831e28e71', @@ -719,7 +723,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "Caption text goes here" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="right" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\nfont-size: 18px;\nopacity: .8;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "Caption text goes here" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="right" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\nfont-size: 18px;\nopacity: .8;\n}"', }, ], groups: [], @@ -768,7 +772,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', }, { id: 'element-5afa7019-af44-4919-9e11-24e2348cfae9', @@ -781,7 +785,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Title for live charts."\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Title for live charts."\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', }, { id: 'element-7b856b52-0d8b-492b-a71f-3508a84388a6', @@ -820,7 +824,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## _Charts with live data._"\n| render css=".canvasRenderEl h1 {\n\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## _Charts with live data._"\n| render css=".canvasRenderEl h1 {\n\n}"', }, { id: 'element-317bed0b-f067-4d2d-8cb4-1145f6e0a11c', @@ -833,7 +837,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', }, { id: 'element-34385617-6eb7-4918-b4db-1a0e8dd6eabe', @@ -846,7 +850,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', }, { id: 'element-b22a35eb-b177-4664-800e-57b91436a879', @@ -859,7 +863,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', }, { id: 'element-651f8a4a-6069-49bf-a7b0-484854628a79', @@ -872,7 +876,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', }, { id: 'element-0ee8c529-4155-442f-8c7c-1df86be37051', @@ -885,7 +889,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', }, { id: 'element-3fb61301-3dc2-411f-ac69-ad22bd37c77d', @@ -898,7 +902,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. \n\nDonec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. \n\nDonec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', }, ], groups: [], @@ -960,7 +964,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', }, { id: 'element-8b9d3e2b-1d7b-48f4-897c-bf48f0f363d4', @@ -973,7 +977,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Title on a _dark_ background."\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Title on a _dark_ background."\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', }, { id: 'element-080c3153-45f7-4efc-8b23-ed7735da426f', @@ -999,7 +1003,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', }, ], groups: [], @@ -1021,7 +1025,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "## Bullet point layout style"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "## Bullet point layout style"\n| render', }, { id: 'element-37dc903a-1c6d-4452-8fc0-38d4afa4631a', @@ -1034,7 +1039,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "- Dolor sit amet, consectetur adipiscing elit\n- Cras dapibus urna non feugiat imperdiet\n- Donec vel sollicitudin mauris, ut scelerisque urna\n- Sed vel neque quis metus vulputate luctus\n- Dolor sit amet, consectetur adipiscing elit\n- Cras dapibus urna non feugiat imperdiet\n- Donec vel sollicitudin mauris, ut scelerisque urna\n- Sed vel neque quis metus vulputate luctus"\n| render css=".canvasRenderEl li {\nfont-size: 24px;\nline-height: 30px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "- Dolor sit amet, consectetur adipiscing elit\n- Cras dapibus urna non feugiat imperdiet\n- Donec vel sollicitudin mauris, ut scelerisque urna\n- Sed vel neque quis metus vulputate luctus\n- Dolor sit amet, consectetur adipiscing elit\n- Cras dapibus urna non feugiat imperdiet\n- Donec vel sollicitudin mauris, ut scelerisque urna\n- Sed vel neque quis metus vulputate luctus"\n| render css=".canvasRenderEl li {\nfont-size: 24px;\nline-height: 30px;\n}"', }, { id: 'element-e506de9d-bda1-4018-89bf-f8d02ee5738e', @@ -1047,7 +1052,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Vel sollicitudin mauris, ut scelerisque urna." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=true}\n| render css=".canvasRenderEl p {\nfont-size: 18px;\nopacity: .8;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Vel sollicitudin mauris, ut scelerisque urna." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=true}\n| render css=".canvasRenderEl p {\nfont-size: 18px;\nopacity: .8;\n}"', }, { id: 'element-ea5319f5-d204-48c5-a9a0-0724676869a6', @@ -1073,7 +1078,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', }, ], groups: [], @@ -1095,7 +1100,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "## Paragraph layout style"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "## Paragraph layout style"\n| render', }, { id: 'element-92b05ab1-c504-4110-a8ad-73d547136024', @@ -1108,7 +1114,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Proin ipsum orci, consectetur a lacus vel, varius rutrum neque. Mauris quis gravida tellus. Integer quis tellus non lectus vestibulum fermentum. Quisque tortor justo, vulputate quis mollis eu, molestie eu ex. Nam eu arcu ac dui mattis facilisis aliquam venenatis est. Quisque tempor risus quis arcu viverra, quis consequat dolor molestie. Sed sed arcu dictum, sollicitudin dui id, iaculis elit. Nunc odio ex, placerat sed hendrerit vitae, finibus eu felis. Sed vulputate mi diam, at dictum mi tempus eu.\n\nClass aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus."\n| render css=".canvasRenderEl p {\nfont-size: 24px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Proin ipsum orci, consectetur a lacus vel, varius rutrum neque. Mauris quis gravida tellus. Integer quis tellus non lectus vestibulum fermentum. Quisque tortor justo, vulputate quis mollis eu, molestie eu ex. Nam eu arcu ac dui mattis facilisis aliquam venenatis est. Quisque tempor risus quis arcu viverra, quis consequat dolor molestie. Sed sed arcu dictum, sollicitudin dui id, iaculis elit. Nunc odio ex, placerat sed hendrerit vitae, finibus eu felis. Sed vulputate mi diam, at dictum mi tempus eu.\n\nClass aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus."\n| render css=".canvasRenderEl p {\nfont-size: 24px;\n}"', }, { id: 'element-e49141ec-3034-4bec-88ca-f9606d12a60a', @@ -1134,7 +1140,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', }, ], groups: [], @@ -1170,7 +1176,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Title text can also go _here_ on multiple lines." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Title text can also go _here_ on multiple lines." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', }, { id: 'element-a8e0d4b3-864d-4dae-b0dc-64caad06c106', @@ -1196,7 +1202,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus. Nam est nulla, venenatis at mi et, sodales convallis eros. Aliquam a convallis justo, eu viverra augue. Donec mollis ipsum sed orci posuere, vel posuere neque tempus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus. Nam est nulla, venenatis at mi et, sodales convallis eros. Aliquam a convallis justo, eu viverra augue. Donec mollis ipsum sed orci posuere, vel posuere neque tempus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', }, { id: 'element-b54e2908-6908-4dd6-90f1-3ca489807016', @@ -1222,7 +1228,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus. Nam est nulla, venenatis at mi et, sodales convallis eros. Aliquam a convallis justo, eu viverra augue. Donec mollis ipsum sed orci posuere, vel posuere neque tempus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus. Nam est nulla, venenatis at mi et, sodales convallis eros. Aliquam a convallis justo, eu viverra augue. Donec mollis ipsum sed orci posuere, vel posuere neque tempus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', }, { id: 'element-aa54f47c-fecf-4bdb-ac1d-b815d4a8d71d', @@ -1235,7 +1241,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## This title is a _centered_ layout." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## This title is a _centered_ layout." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', }, { id: 'element-6ae072e7-213c-4de9-af22-7fb3e254cf52', @@ -1284,7 +1290,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "## \\"Aliquam mollis auctor nisl vitae varius. Donec nunc turpis, condimentum non sagittis tristique, sollicitudin blandit sem.\\"" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=true}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "## \\"Aliquam mollis auctor nisl vitae varius. Donec nunc turpis, condimentum non sagittis tristique, sollicitudin blandit sem.\\"" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=true}\n| render', }, { id: 'element-989daff8-3571-4e02-b5fc-26657b2d9aaf', @@ -1310,7 +1316,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Lorem Ipsum" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Lorem Ipsum" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', }, { id: 'element-cf931bd0-e3b6-4ae3-9164-8fe9ba14873d', @@ -1372,7 +1378,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #FFFFFF;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #FFFFFF;\n}"', }, { id: 'element-dc4336d5-9752-421f-8196-9f4a6f8150f0', @@ -1385,7 +1391,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', }, { id: 'element-b8325cb3-2856-4fd6-8c5a-cba2430dda3e', @@ -1411,7 +1417,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| math "unique(project)"\n| metric "Projects" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "unique(project)"\n| metric "Projects" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', }, { id: 'element-07f73884-13e9-4a75-8a23-4eb137e75817', @@ -1424,7 +1430,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Vel sollicitudin mauris, ut scelerisque urna." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#FFFFFF" weight="normal" underline=false italic=true}\n| render css=".canvasRenderEl p {\nfont-size: 16px;\nopacity: .7;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Vel sollicitudin mauris, ut scelerisque urna." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#FFFFFF" weight="normal" underline=false italic=true}\n| render css=".canvasRenderEl p {\nfont-size: 16px;\nopacity: .7;\n}"', }, { id: 'element-201b8f78-045e-4457-9ada-5166965e64cf', @@ -1437,7 +1443,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', }, { id: 'element-9b667060-18ba-4f4d-84a2-48adff57efac', @@ -1450,7 +1456,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| math "unique(country)"\n| metric "Countries" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "unique(country)"\n| metric "Countries" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', }, { id: 'element-23fcecca-1f6a-44f6-b441-0f65e03d8210', @@ -1463,7 +1469,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| math "unique(username)"\n| metric "Customers" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "unique(username)"\n| metric "Customers" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', }, { id: 'element-19f1db84-7a46-4ccb-a6b9-afd6ddd68523', @@ -1476,7 +1482,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', }, ], groups: [], @@ -1499,7 +1505,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## An alternative opening title slide."\n| render css=".canvasRenderEl h2 {\nfont-size: 64px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## An alternative opening title slide."\n| render css=".canvasRenderEl h2 {\nfont-size: 64px;\n}"', }, { id: 'element-433586c1-4d44-40cf-988e-cf51871248fb', @@ -1525,7 +1531,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', }, ], groups: [], diff --git a/x-pack/plugins/canvas/server/workpad_route_context.ts b/x-pack/plugins/canvas/server/workpad_route_context.ts index 9727327fcbd793..d7c818b786e323 100644 --- a/x-pack/plugins/canvas/server/workpad_route_context.ts +++ b/x-pack/plugins/canvas/server/workpad_route_context.ts @@ -16,12 +16,13 @@ import { WorkpadAttributes } from './routes/workpad/workpad_attributes'; import { CANVAS_TYPE } from '../common/lib/constants'; import { injectReferences, extractReferences } from './saved_objects/workpad_references'; import { getId } from '../common/lib/get_id'; -import { CanvasWorkpad } from '../types'; +import { CanvasWorkpad, ImportedCanvasWorkpad } from '../types'; export interface CanvasRouteHandlerContext extends RequestHandlerContext { canvas: { workpad: { create: (attributes: CanvasWorkpad) => Promise>; + import: (workpad: ImportedCanvasWorkpad) => Promise>; get: (id: string) => Promise>; resolve: (id: string) => Promise>; update: ( @@ -62,6 +63,33 @@ export const createWorkpadRouteContext: ( { id, references } ); }, + import: async (workpad: ImportedCanvasWorkpad) => { + const now = new Date().toISOString(); + const { id: maybeId, ...workpadWithoutId } = workpad; + + // Functionality of running migrations on import of workpads was implemented in v8.1.0. + // As only attributes of the saved object workpad are exported, to run migrations it is necessary + // to specify the minimal version of possible migrations to execute them. It is v8.0.0 in the current case. + const DEFAULT_MIGRATION_VERSION = { [CANVAS_TYPE]: '8.0.0' }; + const DEFAULT_CORE_MIGRATION_VERSION = '8.0.0'; + + const id = maybeId ? maybeId : getId('workpad'); + + return await context.core.savedObjects.client.create( + CANVAS_TYPE, + { + isWriteable: true, + ...workpadWithoutId, + '@timestamp': now, + '@created': now, + }, + { + migrationVersion: DEFAULT_MIGRATION_VERSION, + coreMigrationVersion: DEFAULT_CORE_MIGRATION_VERSION, + id, + } + ); + }, get: async (id: string) => { const workpad = await context.core.savedObjects.client.get( CANVAS_TYPE, diff --git a/x-pack/plugins/canvas/types/canvas.ts b/x-pack/plugins/canvas/types/canvas.ts index efb121b2948af2..09add343aeac47 100644 --- a/x-pack/plugins/canvas/types/canvas.ts +++ b/x-pack/plugins/canvas/types/canvas.ts @@ -66,15 +66,30 @@ export interface CanvasWorkpad { width: number; } -type CanvasTemplateElement = Omit; -type CanvasTemplatePage = Omit & { elements: CanvasTemplateElement[] }; +export type ImportedCanvasWorkpad = Omit< + CanvasWorkpad, + '@created' | '@timestamp' | 'id' | 'isWriteable' +> & { + id?: CanvasWorkpad['id']; + isWriteable?: CanvasWorkpad['isWriteable']; + '@created'?: CanvasWorkpad['@created']; + '@timestamp'?: CanvasWorkpad['@timestamp']; +}; + +export type CanvasTemplateElement = Omit; +export type CanvasTemplatePage = Omit & { + elements: CanvasTemplateElement[]; +}; + export interface CanvasTemplate { id: string; name: string; help: string; tags: string[]; template_key: string; - template?: Omit & { pages: CanvasTemplatePage[] }; + template?: Omit & { + pages: CanvasTemplatePage[] | undefined; + }; } export interface CanvasWorkpadBoundingBox { diff --git a/x-pack/plugins/canvas/types/elements.ts b/x-pack/plugins/canvas/types/elements.ts index 0baf1e086d1557..0119c0a842f50b 100644 --- a/x-pack/plugins/canvas/types/elements.ts +++ b/x-pack/plugins/canvas/types/elements.ts @@ -49,6 +49,12 @@ export interface CustomElement { content: string; } +export type CustomElementNode = Omit; + +export interface CustomElementContent { + selectedNodes: CustomElementNode[]; +} + export interface ElementPosition { /** * distance from the left edge of the page diff --git a/x-pack/plugins/canvas/types/renderers.ts b/x-pack/plugins/canvas/types/renderers.ts index 2c3931485757de..7c9b785dbb2bbc 100644 --- a/x-pack/plugins/canvas/types/renderers.ts +++ b/x-pack/plugins/canvas/types/renderers.ts @@ -26,8 +26,6 @@ export interface CanvasSpecificRendererHandlers { onResize: GenericRendererCallback; /** Handler to invoke when an element should be resized. */ resize: (size: { height: number; width: number }) => void; - /** Sets the value of the filter property on the element object persisted on the workpad */ - setFilter: (filter: string) => void; } export type RendererHandlers = IInterpreterRenderHandlers & CanvasSpecificRendererHandlers; diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts index e4e2a0793f7d43..19ad15286db6ab 100644 --- a/x-pack/plugins/cases/common/api/cases/comment.ts +++ b/x-pack/plugins/cases/common/api/cases/comment.ts @@ -110,6 +110,14 @@ export const CommentResponseRt = rt.intersection([ }), ]); +export const CommentResponseTypeUserRt = rt.intersection([ + AttributesTypeUserRt, + rt.type({ + id: rt.string, + version: rt.string, + }), +]); + export const CommentResponseTypeAlertsRt = rt.intersection([ AttributesTypeAlertsRt, rt.type({ @@ -172,6 +180,7 @@ export type AttributesTypeUser = rt.TypeOf; export type CommentAttributes = rt.TypeOf; export type CommentRequest = rt.TypeOf; export type CommentResponse = rt.TypeOf; +export type CommentResponseUserType = rt.TypeOf; export type CommentResponseAlertsType = rt.TypeOf; export type CommentResponseActionsType = rt.TypeOf; export type AllCommentsResponse = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 65981e6aebd0f8..cfa91a9c57cab3 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -12,12 +12,12 @@ import { CasePatchRequest, CaseStatuses, CaseType, - CommentRequest, User, ActionConnector, CaseExternalServiceBasic, CaseUserActionResponse, CaseMetricsResponse, + CommentResponse, } from '../api'; import { SnakeToCamelCase } from '../types'; @@ -62,18 +62,7 @@ export type CaseViewRefreshPropInterface = null | { refreshCase: () => Promise; }; -export type Comment = CommentRequest & { - associationType: AssociationType; - id: string; - createdAt: string; - createdBy: ElasticUser; - pushedAt: string | null; - pushedBy: string | null; - updatedAt: string | null; - updatedBy: ElasticUser | null; - version: string; -}; - +export type Comment = SnakeToCamelCase; export type CaseUserActions = SnakeToCamelCase; export type CaseExternalService = SnakeToCamelCase; diff --git a/x-pack/plugins/cases/kibana.json b/x-pack/plugins/cases/kibana.json index ebac6295166df7..bf3cc0ee320bd6 100644 --- a/x-pack/plugins/cases/kibana.json +++ b/x-pack/plugins/cases/kibana.json @@ -14,8 +14,8 @@ "spaces" ], "owner":{ - "githubTeam":"security-threat-hunting", - "name":"Security Solution Threat Hunting" + "githubTeam":"response-ops", + "name":"ResponseOps" }, "requiredPlugins":[ "actions", diff --git a/x-pack/plugins/cases/public/common/test_utils.ts b/x-pack/plugins/cases/public/common/test_utils.ts index 0ebff7693eed88..5965ccbcf504eb 100644 --- a/x-pack/plugins/cases/public/common/test_utils.ts +++ b/x-pack/plugins/cases/public/common/test_utils.ts @@ -7,6 +7,7 @@ import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; +import { MatcherFunction } from '@testing-library/react'; /** * Convenience utility to remove text appended to links by EUI @@ -25,3 +26,16 @@ export const waitForComponentToUpdate = async () => act(async () => { return Promise.resolve(); }); + +type Query = (f: MatcherFunction) => HTMLElement; + +export const createQueryWithMarkup = + (query: Query) => + (text: string): HTMLElement => + query((content: string, node: Parameters[1]) => { + const hasText = (el: Parameters[1]) => el?.textContent === text; + const childrenDontHaveText = Array.from(node?.children ?? []).every( + (child) => !hasText(child as HTMLElement) + ); + return hasText(node) && childrenDontHaveText; + }); diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx index 7950f962a9cc1e..5897a757b5bdf5 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx @@ -792,8 +792,10 @@ describe('AllCasesListGeneric', () => { ); - const solutionHeader = wrapper.find({ children: 'Solution' }); - expect(solutionHeader.exists()).toBeTruthy(); + await waitFor(() => { + const solutionHeader = wrapper.find({ children: 'Solution' }); + expect(solutionHeader.exists()).toBeTruthy(); + }); }); it('hides Solution column if there is a set owner', async () => { @@ -805,8 +807,10 @@ describe('AllCasesListGeneric', () => { ); - const solutionHeader = wrapper.find({ children: 'Solution' }); - expect(solutionHeader.exists()).toBeFalsy(); + await waitFor(() => { + const solutionHeader = wrapper.find({ children: 'Solution' }); + expect(solutionHeader.exists()).toBeFalsy(); + }); }); it('should deselect cases when refreshing', async () => { diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx index ac10068b88b3ef..9c0b7831893fa0 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx @@ -35,7 +35,7 @@ jest.mock('../../containers/use_get_case_user_actions'); jest.mock('../../containers/use_get_case'); jest.mock('../../containers/configure/use_connectors'); jest.mock('../../containers/use_post_push_to_service'); -jest.mock('../user_action_tree/user_action_timestamp'); +jest.mock('../user_actions/timestamp'); jest.mock('../../common/lib/kibana'); jest.mock('../../common/navigation/hooks'); @@ -506,7 +506,7 @@ describe('CaseViewPage', () => { expect( wrapper .find( - '[data-test-subj="comment-create-action-alert-action-id"] .euiCommentEvent__headerEvent' + '[data-test-subj="user-action-alert-comment-create-action-alert-action-id"] .euiCommentEvent__headerEvent' ) .first() .text() diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx index 3ff8845f1e3a54..f1f4193ad346d6 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx @@ -18,7 +18,7 @@ import { CaseStatuses, CaseAttributes, CaseType, CaseConnector } from '../../../ import { Case, UpdateKey, UpdateByKey } from '../../../common/ui'; import { EditableTitle } from '../header_page/editable_title'; import { TagList } from '../tag_list'; -import { UserActionTree } from '../user_action_tree'; +import { UserActions } from '../user_actions'; import { UserList } from '../user_list'; import { useUpdateCase } from '../../containers/use_update_case'; import { getTypedPayload } from '../../containers/utils'; @@ -363,12 +363,11 @@ export const CaseViewPage = React.memo( )} - { @@ -31,7 +32,7 @@ describe('Description', () => { }; beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); it('it renders', async () => { diff --git a/x-pack/plugins/cases/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx index 65fcc479979f14..dd0759ce723aea 100644 --- a/x-pack/plugins/cases/public/components/create/form.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -68,7 +68,7 @@ describe('CreateCaseForm', () => { }; beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); useGetTagsMock.mockReturnValue({ tags: ['test'] }); useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock }); useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/__mocks__/use_lens_draft_comment.ts b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/__mocks__/use_lens_draft_comment.ts index a0f0d49b211fb1..677ef694894ec3 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/__mocks__/use_lens_draft_comment.ts +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/__mocks__/use_lens_draft_comment.ts @@ -5,4 +5,9 @@ * 2.0. */ -export const useLensDraftComment = () => ({}); +export const useLensDraftComment = jest.fn().mockReturnValue({ + draftComment: null, + hasIncomingLensState: false, + clearDraftComment: jest.fn(), + openLensModal: jest.fn(), +}); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx b/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx index c57283106c6c80..b0f167628496b2 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx @@ -40,6 +40,7 @@ const MarkdownRendererComponent: React.FC = ({ children, disableLinks }) {children} diff --git a/x-pack/plugins/cases/public/components/user_action_tree/helpers.test.tsx b/x-pack/plugins/cases/public/components/user_action_tree/helpers.test.tsx deleted file mode 100644 index 7f3ae4a490264d..00000000000000 --- a/x-pack/plugins/cases/public/components/user_action_tree/helpers.test.tsx +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount } from 'enzyme'; - -import { - Actions, - CaseStatuses, - CommentType, - ConnectorTypes, - ConnectorUserAction, - PushedUserAction, - TagsUserAction, - TitleUserAction, -} from '../../../common/api'; -import { basicPush, getUserAction } from '../../containers/mock'; -import { getLabelTitle, getPushedServiceLabelTitle, getConnectorLabelTitle } from './helpers'; -import { connectorsMock } from '../../containers/configure/mock'; -import * as i18n from './translations'; -import { SnakeToCamelCase } from '../../../common/types'; -import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; - -describe('User action tree helpers', () => { - const connectors = connectorsMock; - it('label title generated for update tags', () => { - const action = getUserAction('tags', Actions.update, { payload: { tags: ['test'] } }); - const result: string | JSX.Element = getLabelTitle({ - action, - }); - - const tags = (action as unknown as TagsUserAction).payload.tags; - - const wrapper = mount(<>{result}); - expect(wrapper.find(`[data-test-subj="ua-tags-label"]`).first().text()).toEqual( - ` ${i18n.TAGS.toLowerCase()}` - ); - - expect(wrapper.find(`[data-test-subj="tag-${tags[0]}"]`).first().text()).toEqual(tags[0]); - }); - - it('label title generated for update title', () => { - const action = getUserAction('title', Actions.update, { payload: { title: 'test' } }); - const result: string | JSX.Element = getLabelTitle({ - action, - }); - - const title = (action as unknown as TitleUserAction).payload.title; - - expect(result).toEqual( - `${i18n.CHANGED_FIELD.toLowerCase()} ${i18n.CASE_NAME.toLowerCase()} ${i18n.TO} "${title}"` - ); - }); - - it('label title generated for update description', () => { - const action = getUserAction('description', Actions.update, { - payload: { description: 'test' }, - }); - const result: string | JSX.Element = getLabelTitle({ - action, - }); - - expect(result).toEqual(`${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`); - }); - - it('label title generated for update status to open', () => { - const action = { - ...getUserAction('status', Actions.update, { payload: { status: CaseStatuses.open } }), - }; - const result: string | JSX.Element = getLabelTitle({ - action, - }); - - const wrapper = mount(<>{result}); - expect(wrapper.find(`[data-test-subj="status-badge-open"]`).first().text()).toEqual('Open'); - }); - - it('label title generated for update status to in-progress', () => { - const action = { - ...getUserAction('status', Actions.update, { - payload: { status: CaseStatuses['in-progress'] }, - }), - }; - const result: string | JSX.Element = getLabelTitle({ - action, - }); - - const wrapper = mount(<>{result}); - expect(wrapper.find(`[data-test-subj="status-badge-in-progress"]`).first().text()).toEqual( - 'In progress' - ); - }); - - it('label title generated for update status to closed', () => { - const action = { - ...getUserAction('status', Actions.update, { - payload: { status: CaseStatuses.closed }, - }), - }; - const result: string | JSX.Element = getLabelTitle({ - action, - }); - - const wrapper = mount(<>{result}); - expect(wrapper.find(`[data-test-subj="status-badge-closed"]`).first().text()).toEqual('Closed'); - }); - - it('label title is empty when status is not valid', () => { - const action = { - ...getUserAction('status', Actions.update, { - payload: { status: '' }, - }), - }; - - const result: string | JSX.Element = getLabelTitle({ - action, - }); - - expect(result).toEqual(''); - }); - - it('label title generated for update comment', () => { - const action = getUserAction('comment', Actions.update, { - payload: { - comment: { comment: 'a comment', type: CommentType.user, owner: SECURITY_SOLUTION_OWNER }, - }, - }); - const result: string | JSX.Element = getLabelTitle({ - action, - }); - - expect(result).toEqual(`${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`); - }); - - it('label title generated for pushed incident', () => { - const action = getUserAction('pushed', 'push_to_service', { - payload: { externalService: basicPush }, - }) as SnakeToCamelCase; - const result: string | JSX.Element = getPushedServiceLabelTitle(action, true); - const externalService = (action as SnakeToCamelCase).payload.externalService; - - const wrapper = mount(<>{result}); - expect(wrapper.find(`[data-test-subj="pushed-label"]`).first().text()).toEqual( - `${i18n.PUSHED_NEW_INCIDENT} ${basicPush.connectorName}` - ); - expect(wrapper.find(`[data-test-subj="pushed-value"]`).first().prop('href')).toEqual( - externalService.externalUrl - ); - }); - - it('label title generated for needs update incident', () => { - const action = getUserAction('pushed', 'push_to_service') as SnakeToCamelCase; - const result: string | JSX.Element = getPushedServiceLabelTitle(action, false); - const externalService = (action as SnakeToCamelCase).payload.externalService; - - const wrapper = mount(<>{result}); - expect(wrapper.find(`[data-test-subj="pushed-label"]`).first().text()).toEqual( - `${i18n.UPDATE_INCIDENT} ${basicPush.connectorName}` - ); - expect(wrapper.find(`[data-test-subj="pushed-value"]`).first().prop('href')).toEqual( - externalService.externalUrl - ); - }); - - describe('getConnectorLabelTitle', () => { - it('returns an empty string when the encoded value is null', () => { - const result = getConnectorLabelTitle({ - // @ts-expect-error - action: getUserAction(['connector'], Actions.update, { payload: { connector: null } }), - connectors, - }); - - expect(result).toEqual(''); - }); - - it('returns the change connector label', () => { - const result: string | JSX.Element = getConnectorLabelTitle({ - action: getUserAction('connector', Actions.update, { - payload: { - connector: { - id: 'resilient-2', - type: ConnectorTypes.resilient, - name: 'a', - fields: null, - }, - }, - }) as unknown as ConnectorUserAction, - connectors, - }); - - expect(result).toEqual('selected My Connector 2 as incident management system'); - }); - - it('returns the removed connector label', () => { - const result: string | JSX.Element = getConnectorLabelTitle({ - action: getUserAction('connector', Actions.update, { - payload: { - connector: { id: 'none', type: ConnectorTypes.none, name: 'test', fields: null }, - }, - }) as unknown as ConnectorUserAction, - connectors, - }); - - expect(result).toEqual('removed external incident management system'); - }); - }); -}); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx b/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx deleted file mode 100644 index ccdc0f8ce888ef..00000000000000 --- a/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx +++ /dev/null @@ -1,411 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiLink, - EuiCommentProps, - EuiToken, -} from '@elastic/eui'; -import React, { useContext } from 'react'; -import classNames from 'classnames'; -import { ThemeContext } from 'styled-components'; -import { CaseExternalService, Comment } from '../../../common/ui/types'; -import { - ActionConnector, - CaseStatuses, - CommentType, - CommentRequestActionsType, - NONE_CONNECTOR_ID, - Actions, - ConnectorUserAction, - PushedUserAction, - TagsUserAction, -} from '../../../common/api'; -import { CaseUserActions } from '../../containers/types'; -import { CaseServices } from '../../containers/use_get_case_user_actions'; -import { Tags } from '../tag_list/tags'; -import { UserActionUsernameWithAvatar } from './user_action_username_with_avatar'; -import { UserActionTimestamp } from './user_action_timestamp'; -import { UserActionCopyLink } from './user_action_copy_link'; -import { ContentWrapper } from './user_action_markdown'; -import { UserActionMoveToReference } from './user_action_move_to_reference'; -import { Status, statuses } from '../status'; -import { UserActionShowAlert } from './user_action_show_alert'; -import * as i18n from './translations'; -import { AlertCommentEvent } from './user_action_alert_comment_event'; -import { CasesNavigation } from '../links'; -import { HostIsolationCommentEvent } from './user_action_host_isolation_comment_event'; -import { MarkdownRenderer } from '../markdown_editor'; -import { - isCommentUserAction, - isDescriptionUserAction, - isStatusUserAction, - isTagsUserAction, - isTitleUserAction, -} from '../../../common/utils/user_actions'; -import { SnakeToCamelCase } from '../../../common/types'; - -interface LabelTitle { - action: CaseUserActions; -} - -export type RuleDetailsNavigation = CasesNavigation; - -export type ActionsNavigation = CasesNavigation; - -const getStatusTitle = (id: string, status: CaseStatuses) => ( - - {i18n.MARKED_CASE_AS} - - - - -); - -const isStatusValid = (status: string): status is CaseStatuses => - Object.prototype.hasOwnProperty.call(statuses, status); - -export const getLabelTitle = ({ action }: LabelTitle) => { - if (isTagsUserAction(action)) { - return getTagsLabelTitle(action); - } else if (isTitleUserAction(action)) { - return `${i18n.CHANGED_FIELD.toLowerCase()} ${i18n.CASE_NAME.toLowerCase()} ${i18n.TO} "${ - action.payload.title - }"`; - } else if (isDescriptionUserAction(action) && action.action === Actions.update) { - return `${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`; - } else if (isStatusUserAction(action)) { - const status = action.payload.status ?? ''; - if (isStatusValid(status)) { - return getStatusTitle(action.actionId, status); - } - - return ''; - } else if (isCommentUserAction(action) && action.action === Actions.update) { - return `${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`; - } - - return ''; -}; - -export const getConnectorLabelTitle = ({ - action, - connectors, -}: { - action: ConnectorUserAction; - connectors: ActionConnector[]; -}) => { - const connector = action.payload.connector; - - if (connector == null) { - return ''; - } - - // ids are not the same so check and see if the id is a valid connector and then return its name - // if the connector id is the none connector value then it must have been removed - const newConnectorActionInfo = connectors.find((c) => c.id === connector.id); - if (connector.id !== NONE_CONNECTOR_ID && newConnectorActionInfo != null) { - return i18n.SELECTED_THIRD_PARTY(newConnectorActionInfo.name); - } - - // it wasn't a valid connector or it was the none connector, so it must have been removed - return i18n.REMOVED_THIRD_PARTY; -}; - -const getTagsLabelTitle = (action: TagsUserAction) => { - const tags = action.payload.tags ?? []; - - return ( - - - {action.action === Actions.add && i18n.ADDED_FIELD} - {action.action === Actions.delete && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()} - - - - - - ); -}; - -export const getPushedServiceLabelTitle = ( - action: SnakeToCamelCase, - firstPush: boolean -) => { - const externalService = action.payload.externalService; - - return ( - - - {`${firstPush ? i18n.PUSHED_NEW_INCIDENT : i18n.UPDATE_INCIDENT} ${ - externalService?.connectorName - }`} - - - - {externalService?.externalTitle} - - - - ); -}; - -export const getPushInfo = ( - caseServices: CaseServices, - externalService: CaseExternalService | undefined, - index: number -) => - externalService != null && externalService.connectorId !== NONE_CONNECTOR_ID - ? { - firstPush: caseServices[externalService.connectorId]?.firstPushIndex === index, - parsedConnectorId: externalService.connectorId, - parsedConnectorName: externalService.connectorName, - } - : { - firstPush: false, - parsedConnectorId: NONE_CONNECTOR_ID, - parsedConnectorName: NONE_CONNECTOR_ID, - }; - -const getUpdateActionIcon = (fields: string): string => { - if (fields === 'tags') { - return 'tag'; - } else if (fields === 'status') { - return 'folderClosed'; - } - - return 'dot'; -}; - -export const getUpdateAction = ({ - action, - label, - handleOutlineComment, -}: { - action: CaseUserActions; - label: string | JSX.Element; - handleOutlineComment: (id: string) => void; -}): EuiCommentProps => ({ - username: ( - - ), - type: 'update', - event: label, - 'data-test-subj': `${action.type}-${action.action}-action-${action.actionId}`, - timestamp: , - timelineIcon: getUpdateActionIcon(action.type), - actions: ( - - - - - {action.action === Actions.update && action.commentId != null && ( - - - - )} - - ), -}); - -export const getAlertAttachment = ({ - action, - alertId, - getRuleDetailsHref, - index, - loadingAlertData, - onRuleDetailsClick, - onShowAlertDetails, - ruleId, - ruleName, -}: { - action: CaseUserActions; - alertId: string; - getRuleDetailsHref: RuleDetailsNavigation['href']; - index: string; - loadingAlertData: boolean; - onRuleDetailsClick?: RuleDetailsNavigation['onClick']; - onShowAlertDetails: (alertId: string, index: string) => void; - ruleId?: string | null; - ruleName?: string | null; -}): EuiCommentProps => ({ - username: ( - - ), - className: 'comment-alert', - type: 'update', - event: ( - - ), - 'data-test-subj': `${action.type}-${action.action}-action-${action.actionId}`, - timestamp: , - timelineIcon: 'bell', - actions: ( - - - - - - - - - ), -}); - -export const getGeneratedAlertsAttachment = ({ - action, - alertIds, - getRuleDetailsHref, - onRuleDetailsClick, - renderInvestigateInTimelineActionComponent, - ruleId, - ruleName, -}: { - action: CaseUserActions; - alertIds: string[]; - getRuleDetailsHref: RuleDetailsNavigation['href']; - onRuleDetailsClick?: RuleDetailsNavigation['onClick']; - renderInvestigateInTimelineActionComponent?: (alertIds: string[]) => JSX.Element; - ruleId: string; - ruleName: string; -}): EuiCommentProps => ({ - username: , - className: 'comment-alert', - type: 'update', - event: ( - - ), - 'data-test-subj': `${action.type}-${action.action}-action-${action.actionId}`, - timestamp: , - timelineIcon: 'bell', - actions: ( - - - - - {renderInvestigateInTimelineActionComponent ? ( - - {renderInvestigateInTimelineActionComponent(alertIds)} - - ) : null} - - ), -}); - -const ActionIcon = React.memo<{ - actionType: string; -}>(({ actionType }) => { - const theme = useContext(ThemeContext); - return ( - - ); -}); - -ActionIcon.displayName = 'ActionIcon'; - -export const getActionAttachment = ({ - comment, - userCanCrud, - isLoadingIds, - actionsNavigation, - action, -}: { - comment: Comment & CommentRequestActionsType; - userCanCrud: boolean; - isLoadingIds: string[]; - actionsNavigation?: ActionsNavigation; - action: CaseUserActions; -}): EuiCommentProps => ({ - username: ( - - ), - className: classNames('comment-action', { 'empty-comment': comment.comment.trim().length === 0 }), - event: ( - - ), - 'data-test-subj': 'endpoint-action', - timestamp: , - timelineIcon: , - actions: , - children: comment.comment.trim().length > 0 && ( - - {comment.comment} - - ), -}); - -interface Signal { - rule: { - id: string; - name: string; - to: string; - from: string; - }; -} - -export interface Alert { - _id: string; - _index: string; - '@timestamp': string; - signal: Signal; - [key: string]: unknown; -} diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx deleted file mode 100644 index d4270b464773c0..00000000000000 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx +++ /dev/null @@ -1,689 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiCommentList, - EuiCommentProps, -} from '@elastic/eui'; -import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; - -import classNames from 'classnames'; -import { get, isEmpty } from 'lodash'; -import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react'; -import styled from 'styled-components'; -import { isRight } from 'fp-ts/Either'; - -import * as i18n from './translations'; - -import { useUpdateComment } from '../../containers/use_update_comment'; -import { useCurrentUser } from '../../common/lib/kibana'; -import { AddComment, AddCommentRefObject } from '../add_comment'; -import { Case, CaseUserActions, Ecs } from '../../../common/ui/types'; -import { - ActionConnector, - Actions, - ActionsCommentRequestRt, - AlertCommentRequestRt, - CommentType, - ContextTypeUserRt, -} from '../../../common/api'; -import { CaseServices } from '../../containers/use_get_case_user_actions'; -import { - getConnectorLabelTitle, - getLabelTitle, - getPushedServiceLabelTitle, - getPushInfo, - getUpdateAction, - getAlertAttachment, - getGeneratedAlertsAttachment, - RuleDetailsNavigation, - ActionsNavigation, - getActionAttachment, -} from './helpers'; -import { UserActionAvatar } from './user_action_avatar'; -import { UserActionMarkdown, UserActionMarkdownRefObject } from './user_action_markdown'; -import { UserActionTimestamp } from './user_action_timestamp'; -import { UserActionUsername } from './user_action_username'; -import { UserActionContentToolbar } from './user_action_content_toolbar'; -import { getManualAlertIdsWithNoRuleId } from '../case_view/helpers'; -import { useLensDraftComment } from '../markdown_editor/plugins/lens/use_lens_draft_comment'; -import { useCaseViewParams } from '../../common/navigation'; -import { isConnectorUserAction, isPushedUserAction } from '../../../common/utils/user_actions'; -import type { OnUpdateFields } from '../case_view/types'; - -export interface UserActionTreeProps { - caseServices: CaseServices; - caseUserActions: CaseUserActions[]; - connectors: ActionConnector[]; - data: Case; - fetchUserActions: () => void; - getRuleDetailsHref?: RuleDetailsNavigation['href']; - actionsNavigation?: ActionsNavigation; - isLoadingDescription: boolean; - isLoadingUserActions: boolean; - onRuleDetailsClick?: RuleDetailsNavigation['onClick']; - onShowAlertDetails: (alertId: string, index: string) => void; - onUpdateField: ({ key, value, onSuccess, onError }: OnUpdateFields) => void; - renderInvestigateInTimelineActionComponent?: (alertIds: string[]) => JSX.Element; - statusActionButton: JSX.Element | null; - updateCase: (newCase: Case) => void; - useFetchAlertData: (alertIds: string[]) => [boolean, Record]; - userCanCrud: boolean; -} - -const MyEuiFlexGroup = styled(EuiFlexGroup)` - margin-bottom: 8px; -`; - -const MyEuiCommentList = styled(EuiCommentList)` - ${({ theme }) => ` - & .userAction__comment.outlined .euiCommentEvent { - outline: solid 5px ${theme.eui.euiColorVis1_behindText}; - margin: 0.5em; - transition: 0.8s; - } - - & .euiComment.isEdit { - & .euiCommentEvent { - border: none; - box-shadow: none; - } - - & .euiCommentEvent__body { - padding: 0; - } - - & .euiCommentEvent__header { - display: none; - } - } - - & .comment-alert .euiCommentEvent { - background-color: ${theme.eui.euiColorLightestShade}; - border: ${theme.eui.euiFlyoutBorder}; - padding: ${theme.eui.paddingSizes.s}; - border-radius: ${theme.eui.paddingSizes.xs}; - } - - & .comment-alert .euiCommentEvent__headerData { - flex-grow: 1; - } - - & .comment-action.empty-comment .euiCommentEvent--regular { - box-shadow: none; - .euiCommentEvent__header { - padding: ${theme.eui.euiSizeM} ${theme.eui.paddingSizes.s}; - border-bottom: 0; - } - } - `} -`; - -const DESCRIPTION_ID = 'description'; -const NEW_ID = 'newComment'; - -const isAddCommentRef = ( - ref: AddCommentRefObject | UserActionMarkdownRefObject | null | undefined -): ref is AddCommentRefObject => { - const commentRef = ref as AddCommentRefObject; - if (commentRef?.addQuote != null) { - return true; - } - - return false; -}; - -export const UserActionTree = React.memo( - ({ - caseServices, - caseUserActions, - connectors, - data: caseData, - fetchUserActions, - getRuleDetailsHref, - actionsNavigation, - isLoadingDescription, - isLoadingUserActions, - onRuleDetailsClick, - onShowAlertDetails, - onUpdateField, - renderInvestigateInTimelineActionComponent, - statusActionButton, - updateCase, - useFetchAlertData, - userCanCrud, - }: UserActionTreeProps) => { - const { detailName: caseId, subCaseId, commentId } = useCaseViewParams(); - const handlerTimeoutId = useRef(0); - const [initLoading, setInitLoading] = useState(true); - const [selectedOutlineCommentId, setSelectedOutlineCommentId] = useState(''); - const { isLoadingIds, patchComment } = useUpdateComment(); - const currentUser = useCurrentUser(); - const [manageMarkdownEditIds, setManageMarkdownEditIds] = useState([]); - const commentRefs = useRef< - Record - >({}); - const { clearDraftComment, draftComment, hasIncomingLensState, openLensModal } = - useLensDraftComment(); - - const [loadingAlertData, manualAlertsData] = useFetchAlertData( - getManualAlertIdsWithNoRuleId(caseData.comments) - ); - - const handleManageMarkdownEditId = useCallback( - (id: string) => { - clearDraftComment(); - setManageMarkdownEditIds((prevManageMarkdownEditIds) => - !prevManageMarkdownEditIds.includes(id) - ? prevManageMarkdownEditIds.concat(id) - : prevManageMarkdownEditIds.filter((myId) => id !== myId) - ); - }, - [clearDraftComment] - ); - - const handleSaveComment = useCallback( - ({ id, version }: { id: string; version: string }, content: string) => { - patchComment({ - caseId, - commentId: id, - commentUpdate: content, - fetchUserActions, - version, - updateCase, - subCaseId, - }); - }, - [caseId, fetchUserActions, patchComment, subCaseId, updateCase] - ); - - const handleOutlineComment = useCallback( - (id: string) => { - const moveToTarget = document.getElementById(`${id}-permLink`); - if (moveToTarget != null) { - const yOffset = -60; - const y = moveToTarget.getBoundingClientRect().top + window.pageYOffset + yOffset; - window.scrollTo({ - top: y, - behavior: 'smooth', - }); - if (id === 'add-comment') { - moveToTarget.getElementsByTagName('textarea')[0].focus(); - } - } - window.clearTimeout(handlerTimeoutId.current); - setSelectedOutlineCommentId(id); - handlerTimeoutId.current = window.setTimeout(() => { - setSelectedOutlineCommentId(''); - window.clearTimeout(handlerTimeoutId.current); - }, 2400); - }, - [handlerTimeoutId] - ); - - const handleManageQuote = useCallback( - (quote: string) => { - const ref = commentRefs?.current[NEW_ID]; - if (isAddCommentRef(ref)) { - ref.addQuote(quote); - } - - handleOutlineComment('add-comment'); - }, - [handleOutlineComment] - ); - - const handleUpdate = useCallback( - (newCase: Case) => { - updateCase(newCase); - fetchUserActions(); - }, - [fetchUserActions, updateCase] - ); - - const MarkdownDescription = useMemo( - () => ( - (commentRefs.current[DESCRIPTION_ID] = element)} - id={DESCRIPTION_ID} - content={caseData.description} - isEditable={manageMarkdownEditIds.includes(DESCRIPTION_ID)} - onSaveContent={(content: string) => { - onUpdateField({ key: DESCRIPTION_ID, value: content }); - }} - onChangeEditable={handleManageMarkdownEditId} - /> - ), - [caseData.description, handleManageMarkdownEditId, manageMarkdownEditIds, onUpdateField] - ); - - const MarkdownNewComment = useMemo( - () => ( - (commentRefs.current[NEW_ID] = element)} - onCommentPosted={handleUpdate} - onCommentSaving={handleManageMarkdownEditId.bind(null, NEW_ID)} - showLoading={false} - statusActionButton={statusActionButton} - subCaseId={subCaseId} - /> - ), - [caseId, userCanCrud, handleUpdate, handleManageMarkdownEditId, statusActionButton, subCaseId] - ); - - useEffect(() => { - if (initLoading && !isLoadingUserActions && isLoadingIds.length === 0) { - setInitLoading(false); - if (commentId != null) { - handleOutlineComment(commentId); - } - } - }, [commentId, initLoading, isLoadingUserActions, isLoadingIds, handleOutlineComment]); - - const descriptionCommentListObj: EuiCommentProps = useMemo( - () => ({ - username: ( - - ), - event: i18n.ADDED_DESCRIPTION, - 'data-test-subj': 'description-action', - timestamp: , - children: MarkdownDescription, - timelineIcon: ( - - ), - className: classNames({ - isEdit: manageMarkdownEditIds.includes(DESCRIPTION_ID), - }), - actions: ( - - ), - }), - [ - MarkdownDescription, - caseData, - handleManageMarkdownEditId, - handleManageQuote, - isLoadingDescription, - userCanCrud, - manageMarkdownEditIds, - ] - ); - - const userActions: EuiCommentProps[] = useMemo( - () => - caseUserActions.reduce( - // TODO: Decrease complexity. https://github.com/elastic/kibana/issues/115730 - // eslint-disable-next-line complexity - (comments, action, index) => { - // Comment creation - if (action.commentId != null && action.action === Actions.create) { - const comment = caseData.comments.find((c) => c.id === action.commentId); - if ( - comment != null && - isRight(ContextTypeUserRt.decode(comment)) && - comment.type === CommentType.user - ) { - return [ - ...comments, - { - username: ( - - ), - 'data-test-subj': `comment-create-action-${comment.id}`, - timestamp: ( - - ), - className: classNames('userAction__comment', { - outlined: comment.id === selectedOutlineCommentId, - isEdit: manageMarkdownEditIds.includes(comment.id), - }), - children: ( - (commentRefs.current[comment.id] = element)} - id={comment.id} - content={comment.comment} - isEditable={manageMarkdownEditIds.includes(comment.id)} - onChangeEditable={handleManageMarkdownEditId} - onSaveContent={handleSaveComment.bind(null, { - id: comment.id, - version: comment.version, - })} - /> - ), - timelineIcon: ( - - ), - actions: ( - - ), - }, - ]; - } else if ( - comment != null && - isRight(AlertCommentRequestRt.decode(comment)) && - comment.type === CommentType.alert - ) { - // TODO: clean this up - const alertId = Array.isArray(comment.alertId) - ? comment.alertId.length > 0 - ? comment.alertId[0] - : '' - : comment.alertId; - - const alertIndex = Array.isArray(comment.index) - ? comment.index.length > 0 - ? comment.index[0] - : '' - : comment.index; - - if (isEmpty(alertId)) { - return comments; - } - - const ruleId = - comment?.rule?.id ?? - manualAlertsData[alertId]?.signal?.rule?.id?.[0] ?? - get(manualAlertsData[alertId], ALERT_RULE_UUID)[0] ?? - null; - const ruleName = - comment?.rule?.name ?? - manualAlertsData[alertId]?.signal?.rule?.name?.[0] ?? - get(manualAlertsData[alertId], ALERT_RULE_NAME)[0] ?? - null; - - return [ - ...comments, - ...(getRuleDetailsHref != null - ? [ - getAlertAttachment({ - action, - alertId, - getRuleDetailsHref, - index: alertIndex, - loadingAlertData, - onRuleDetailsClick, - ruleId, - ruleName, - onShowAlertDetails, - }), - ] - : []), - ]; - } else if (comment != null && comment.type === CommentType.generatedAlert) { - // TODO: clean this up - const alertIds = Array.isArray(comment.alertId) - ? comment.alertId - : [comment.alertId]; - - if (isEmpty(alertIds)) { - return comments; - } - - return [ - ...comments, - ...(getRuleDetailsHref != null - ? [ - getGeneratedAlertsAttachment({ - action, - alertIds, - getRuleDetailsHref, - onRuleDetailsClick, - renderInvestigateInTimelineActionComponent, - ruleId: comment.rule?.id ?? '', - ruleName: comment.rule?.name ?? i18n.UNKNOWN_RULE, - }), - ] - : []), - ]; - } else if ( - comment != null && - isRight(ActionsCommentRequestRt.decode(comment)) && - comment.type === CommentType.actions - ) { - return [ - ...comments, - ...(comment.actions !== null - ? [ - getActionAttachment({ - comment, - userCanCrud, - isLoadingIds, - actionsNavigation, - action, - }), - ] - : []), - ]; - } - } - - // Connectors - if (isConnectorUserAction(action)) { - const label = getConnectorLabelTitle({ action, connectors }); - return [ - ...comments, - getUpdateAction({ - action, - label, - handleOutlineComment, - }), - ]; - } - - // Pushed information - if (isPushedUserAction<'camelCase'>(action)) { - const parsedExternalService = action.payload.externalService; - - const { firstPush, parsedConnectorId, parsedConnectorName } = getPushInfo( - caseServices, - parsedExternalService, - index - ); - - const label = getPushedServiceLabelTitle(action, firstPush); - - const showTopFooter = - action.action === Actions.push_to_service && - index === caseServices[parsedConnectorId]?.lastPushIndex; - - const showBottomFooter = - action.action === Actions.push_to_service && - index === caseServices[parsedConnectorId]?.lastPushIndex && - caseServices[parsedConnectorId].hasDataToPush; - - let footers: EuiCommentProps[] = []; - - if (showTopFooter) { - footers = [ - ...footers, - { - username: '', - type: 'update', - event: i18n.ALREADY_PUSHED_TO_SERVICE(`${parsedConnectorName}`), - timelineIcon: 'sortUp', - 'data-test-subj': 'top-footer', - }, - ]; - } - - if (showBottomFooter) { - footers = [ - ...footers, - { - username: '', - type: 'update', - event: i18n.REQUIRED_UPDATE_TO_SERVICE(`${parsedConnectorName}`), - timelineIcon: 'sortDown', - 'data-test-subj': 'bottom-footer', - }, - ]; - } - - return [ - ...comments, - getUpdateAction({ - action, - label, - handleOutlineComment, - }), - ...footers, - ]; - } - - // title, description, comment updates, tags - if (['title', 'description', 'comment', 'tags', 'status'].includes(action.type)) { - const label: string | JSX.Element = getLabelTitle({ - action, - }); - - return [ - ...comments, - getUpdateAction({ - action, - label, - handleOutlineComment, - }), - ]; - } - - return comments; - }, - [descriptionCommentListObj] - ), - [ - caseUserActions, - descriptionCommentListObj, - caseData.comments, - selectedOutlineCommentId, - manageMarkdownEditIds, - handleManageMarkdownEditId, - handleSaveComment, - actionsNavigation, - userCanCrud, - isLoadingIds, - handleManageQuote, - manualAlertsData, - getRuleDetailsHref, - loadingAlertData, - onRuleDetailsClick, - onShowAlertDetails, - renderInvestigateInTimelineActionComponent, - connectors, - handleOutlineComment, - caseServices, - ] - ); - - const bottomActions = userCanCrud - ? [ - { - username: ( - - ), - 'data-test-subj': 'add-comment', - timelineIcon: ( - - ), - className: 'isEdit', - children: MarkdownNewComment, - }, - ] - : []; - - const comments = [...userActions, ...bottomActions]; - - useEffect(() => { - if (draftComment?.commentId) { - setManageMarkdownEditIds((prevManageMarkdownEditIds) => { - if ( - ![NEW_ID].includes(draftComment?.commentId) && - !prevManageMarkdownEditIds.includes(draftComment?.commentId) - ) { - return [draftComment?.commentId]; - } - return prevManageMarkdownEditIds; - }); - - const ref = commentRefs?.current?.[draftComment.commentId]; - - if (isAddCommentRef(ref) && ref.editor?.textarea) { - ref.setComment(draftComment.comment); - if (hasIncomingLensState) { - openLensModal({ editorRef: ref.editor }); - } else { - clearDraftComment(); - } - } - } - }, [ - draftComment, - openLensModal, - commentRefs, - hasIncomingLensState, - clearDraftComment, - manageMarkdownEditIds, - ]); - - return ( - <> - - {(isLoadingUserActions || isLoadingIds.includes(NEW_ID)) && ( - - - - - - )} - - ); - } -); - -UserActionTree.displayName = 'UserActionTree'; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx deleted file mode 100644 index 692fbbb318b22b..00000000000000 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui'; -import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react'; -import styled from 'styled-components'; - -import * as i18n from '../case_view/translations'; -import { Form, useForm, UseField } from '../../common/shared_imports'; -import { schema, Content } from './schema'; -import { MarkdownRenderer, MarkdownEditorForm } from '../markdown_editor'; - -export const ContentWrapper = styled.div` - padding: ${({ theme }) => `${theme.eui.euiSizeM} ${theme.eui.euiSizeL}`}; -`; - -interface UserActionMarkdownProps { - content: string; - id: string; - isEditable: boolean; - onChangeEditable: (id: string) => void; - onSaveContent: (content: string) => void; -} - -export interface UserActionMarkdownRefObject { - setComment: (newComment: string) => void; -} - -export const UserActionMarkdown = forwardRef( - ({ id, content, isEditable, onChangeEditable, onSaveContent }, ref) => { - const editorRef = useRef(); - const initialState = { content }; - const { form } = useForm({ - defaultValue: initialState, - options: { stripEmptyFields: false }, - schema, - }); - - const fieldName = 'content'; - const { setFieldValue, submit } = form; - - const handleCancelAction = useCallback(() => { - onChangeEditable(id); - }, [id, onChangeEditable]); - - const handleSaveAction = useCallback(async () => { - const { isValid, data } = await submit(); - - if (isValid && data.content !== content) { - onSaveContent(data.content); - } - onChangeEditable(id); - }, [content, id, onChangeEditable, onSaveContent, submit]); - - const setComment = useCallback( - (newComment) => { - setFieldValue(fieldName, newComment); - }, - [setFieldValue] - ); - - const EditorButtons = useMemo( - () => ( - - - - {i18n.CANCEL} - - - - - {i18n.SAVE} - - - - ), - [handleCancelAction, handleSaveAction] - ); - - useImperativeHandle(ref, () => ({ - setComment, - editor: editorRef.current, - })); - - return isEditable ? ( -
- - - ) : ( - - {content} - - ); - } -); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_avatar.test.tsx b/x-pack/plugins/cases/public/components/user_actions/avatar.test.tsx similarity index 96% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_avatar.test.tsx rename to x-pack/plugins/cases/public/components/user_actions/avatar.test.tsx index 1df780db3bdaa1..aeda9196cc58ae 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_avatar.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/avatar.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; -import { UserActionAvatar } from './user_action_avatar'; +import { UserActionAvatar } from './avatar'; const props = { username: 'elastic', diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_avatar.tsx b/x-pack/plugins/cases/public/components/user_actions/avatar.tsx similarity index 74% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_avatar.tsx rename to x-pack/plugins/cases/public/components/user_actions/avatar.tsx index 80b048618bfc40..da43a122f38686 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_avatar.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/avatar.tsx @@ -6,19 +6,19 @@ */ import React, { memo } from 'react'; -import { EuiAvatar } from '@elastic/eui'; +import { EuiAvatar, EuiAvatarProps } from '@elastic/eui'; import * as i18n from './translations'; interface UserActionAvatarProps { username?: string | null; fullName?: string | null; + size?: EuiAvatarProps['size']; } -const UserActionAvatarComponent = ({ username, fullName }: UserActionAvatarProps) => { +const UserActionAvatarComponent = ({ username, fullName, size = 'm' }: UserActionAvatarProps) => { const avatarName = fullName && fullName.length > 0 ? fullName : username ?? i18n.UNKNOWN; - - return ; + return ; }; export const UserActionAvatar = memo(UserActionAvatarComponent); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_username_with_avatar.test.tsx b/x-pack/plugins/cases/public/components/user_actions/avatar_username.test.tsx similarity index 69% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_username_with_avatar.test.tsx rename to x-pack/plugins/cases/public/components/user_actions/avatar_username.test.tsx index 3b6c9560171201..776fa3325478af 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_username_with_avatar.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/avatar_username.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; -import { UserActionUsernameWithAvatar } from './user_action_username_with_avatar'; +import { UserActionUsernameWithAvatar } from './avatar_username'; const props = { username: 'elastic', @@ -25,19 +25,15 @@ describe('UserActionUsernameWithAvatar ', () => { expect( wrapper.find('[data-test-subj="user-action-username-with-avatar"]').first().exists() ).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="user-action-username-avatar"]').first().exists() - ).toBeTruthy(); + expect(wrapper.find('[data-test-subj="user-action-avatar"]').first().exists()).toBeTruthy(); }); it('it shows the avatar', async () => { - expect(wrapper.find('[data-test-subj="user-action-username-avatar"]').first().text()).toBe('E'); + expect(wrapper.find('[data-test-subj="user-action-avatar"]').first().text()).toBe('E'); }); it('it shows the avatar without fullName', async () => { const newWrapper = mount(); - expect(newWrapper.find('[data-test-subj="user-action-username-avatar"]').first().text()).toBe( - 'e' - ); + expect(newWrapper.find('[data-test-subj="user-action-avatar"]').first().text()).toBe('e'); }); }); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_username_with_avatar.tsx b/x-pack/plugins/cases/public/components/user_actions/avatar_username.tsx similarity index 71% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_username_with_avatar.tsx rename to x-pack/plugins/cases/public/components/user_actions/avatar_username.tsx index 685adc8724e87a..581ebb7272d347 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_username_with_avatar.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/avatar_username.tsx @@ -6,11 +6,10 @@ */ import React, { memo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiAvatar } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { UserActionUsername } from './user_action_username'; -import * as i18n from './translations'; +import { UserActionAvatar } from './avatar'; +import { UserActionUsername } from './username'; interface UserActionUsernameWithAvatarProps { username?: string | null; @@ -28,11 +27,7 @@ const UserActionUsernameWithAvatarComponent = ({ data-test-subj="user-action-username-with-avatar" > - + diff --git a/x-pack/plugins/cases/public/components/user_actions/builder.tsx b/x-pack/plugins/cases/public/components/user_actions/builder.tsx new file mode 100644 index 00000000000000..92f1dd6c123da6 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/builder.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createCommentUserActionBuilder } from './comment/comment'; +import { createConnectorUserActionBuilder } from './connector'; +import { createDescriptionUserActionBuilder } from './description'; +import { createPushedUserActionBuilder } from './pushed'; +import { createStatusUserActionBuilder } from './status'; +import { createTagsUserActionBuilder } from './tags'; +import { createTitleUserActionBuilder } from './title'; +import { UserActionBuilderMap } from './types'; + +export const builderMap: UserActionBuilderMap = { + connector: createConnectorUserActionBuilder, + tags: createTagsUserActionBuilder, + title: createTitleUserActionBuilder, + status: createStatusUserActionBuilder, + pushed: createPushedUserActionBuilder, + comment: createCommentUserActionBuilder, + description: createDescriptionUserActionBuilder, +}; diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/actions.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/actions.tsx new file mode 100644 index 00000000000000..4dc189c14c74fa --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/comment/actions.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext } from 'react'; +import classNames from 'classnames'; +import { ThemeContext } from 'styled-components'; + +import { EuiToken } from '@elastic/eui'; +import { CommentResponseActionsType } from '../../../../common/api'; +import { UserActionBuilder, UserActionBuilderArgs } from '../types'; +import { UserActionTimestamp } from '../timestamp'; +import { SnakeToCamelCase } from '../../../../common/types'; +import { UserActionUsernameWithAvatar } from '../avatar_username'; +import { UserActionCopyLink } from '../copy_link'; +import { MarkdownRenderer } from '../../markdown_editor'; +import { ContentWrapper } from '../markdown_form'; +import { HostIsolationCommentEvent } from './host_isolation_event'; + +type BuilderArgs = Pick & { + comment: SnakeToCamelCase; +}; + +export const createActionAttachmentUserActionBuilder = ({ + userAction, + comment, + actionsNavigation, +}: BuilderArgs): ReturnType => ({ + build: () => { + return [ + { + username: ( + + ), + className: classNames('comment-action', { + 'empty-comment': comment.comment.trim().length === 0, + }), + event: ( + + ), + 'data-test-subj': 'endpoint-action', + timestamp: , + timelineIcon: , + actions: , + children: comment.comment.trim().length > 0 && ( + + {comment.comment} + + ), + }, + ]; + }, +}); + +const ActionIcon = React.memo<{ + actionType: string; +}>(({ actionType }) => { + const theme = useContext(ThemeContext); + return ( + + ); +}); + +ActionIcon.displayName = 'ActionIcon'; diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/alert.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/alert.tsx new file mode 100644 index 00000000000000..0341af259e4022 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/comment/alert.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { get, isEmpty } from 'lodash'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; + +import { CommentType, CommentResponseAlertsType } from '../../../../common/api'; +import { UserActionBuilder, UserActionBuilderArgs } from '../types'; +import { UserActionTimestamp } from '../timestamp'; +import { SnakeToCamelCase } from '../../../../common/types'; +import { UserActionUsernameWithAvatar } from '../avatar_username'; +import { AlertCommentEvent } from './alert_event'; +import { UserActionCopyLink } from '../copy_link'; +import { UserActionShowAlert } from './show_alert'; + +type BuilderArgs = Pick< + UserActionBuilderArgs, + | 'userAction' + | 'alertData' + | 'getRuleDetailsHref' + | 'onRuleDetailsClick' + | 'loadingAlertData' + | 'onShowAlertDetails' +> & { comment: SnakeToCamelCase }; + +const getFirstItem = (items: string | string[]) => + Array.isArray(items) ? (items.length > 0 ? items[0] : '') : items; + +export const createAlertAttachmentUserActionBuilder = ({ + userAction, + comment, + alertData, + getRuleDetailsHref, + loadingAlertData, + onRuleDetailsClick, + onShowAlertDetails, +}: BuilderArgs): ReturnType => ({ + build: () => { + const alertId = getFirstItem(comment.alertId); + const alertIndex = getFirstItem(comment.index); + + if (isEmpty(alertId)) { + return []; + } + + const ruleId = + comment?.rule?.id ?? + alertData[alertId]?.signal?.rule?.id?.[0] ?? + get(alertData[alertId], ALERT_RULE_UUID)[0] ?? + null; + + const ruleName = + comment?.rule?.name ?? + alertData[alertId]?.signal?.rule?.name?.[0] ?? + get(alertData[alertId], ALERT_RULE_NAME)[0] ?? + null; + + return [ + { + username: ( + + ), + className: 'comment-alert', + type: 'update', + event: ( + + ), + 'data-test-subj': `user-action-alert-${userAction.type}-${userAction.action}-action-${userAction.actionId}`, + timestamp: , + timelineIcon: 'bell', + actions: ( + + + + + + + + + ), + }, + ]; + }, +}); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_alert_comment_event.test.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/alert_event.test.tsx similarity index 62% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_alert_comment_event.test.tsx rename to x-pack/plugins/cases/public/components/user_actions/comment/alert_event.test.tsx index 858b54038286d5..948a15eeba88ec 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_alert_comment_event.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/alert_event.test.tsx @@ -8,14 +8,14 @@ import React from 'react'; import { mount } from 'enzyme'; -import { TestProviders } from '../../common/mock'; -import { useKibana } from '../../common/lib/kibana'; -import { AlertCommentEvent } from './user_action_alert_comment_event'; -import { CommentType } from '../../../common/api'; +import { TestProviders } from '../../../common/mock'; +import { useKibana } from '../../../common/lib/kibana'; +import { AlertCommentEvent } from './alert_event'; +import { CommentType } from '../../../../common/api'; const props = { alertId: 'alert-id-1', - getRuleDetailsHref: jest.fn().mockReturnValue('some-detection-rule-link'), + getRuleDetailsHref: jest.fn().mockReturnValue('https://example.com'), onRuleDetailsClick: jest.fn(), ruleId: 'rule-id-1', ruleName: 'Awesome rule', @@ -23,7 +23,7 @@ const props = { commentType: CommentType.alert, }; -jest.mock('../../common/lib/kibana'); +jest.mock('../../../common/lib/kibana'); const useKibanaMock = useKibana as jest.Mocked; describe('UserActionAvatar ', () => { @@ -59,6 +59,33 @@ describe('UserActionAvatar ', () => { wrapper.find(`[data-test-subj="alert-rule-link-alert-id-1"]`).first().exists() ).toBeFalsy(); + expect(wrapper.text()).toBe('added an alert from Awesome rule'); + }); + + it('does NOT render the link when the href is invalid but it shows the rule name', async () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find(`[data-test-subj="alert-rule-link-alert-id-1"]`).first().exists() + ).toBeFalsy(); + + expect(wrapper.text()).toBe('added an alert from Awesome rule'); + }); + + it('show Unknown rule if the rule name is invalid', async () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find(`[data-test-subj="alert-rule-link-alert-id-1"]`).first().exists() + ).toBeTruthy(); expect(wrapper.text()).toBe('added an alert from Unknown rule'); }); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_alert_comment_event.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/alert_event.tsx similarity index 54% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_alert_comment_event.tsx rename to x-pack/plugins/cases/public/components/user_actions/comment/alert_event.tsx index 4236691a16bb2c..b4b4b3b75fe7e9 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_alert_comment_event.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/alert_event.tsx @@ -7,17 +7,17 @@ import React, { memo, useCallback } from 'react'; import { isEmpty } from 'lodash'; -import { EuiText, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiLoadingSpinner } from '@elastic/eui'; -import * as i18n from './translations'; -import { CommentType } from '../../../common/api'; -import { LinkAnchor } from '../links'; -import { RuleDetailsNavigation } from './helpers'; +import { CommentType } from '../../../../common/api'; +import * as i18n from '../translations'; +import { LinkAnchor } from '../../links'; +import { RuleDetailsNavigation } from '../types'; interface Props { alertId: string; commentType: CommentType; - getRuleDetailsHref: RuleDetailsNavigation['href']; + getRuleDetailsHref?: RuleDetailsNavigation['href']; onRuleDetailsClick?: RuleDetailsNavigation['onClick']; ruleId?: string | null; ruleName?: string | null; @@ -42,38 +42,24 @@ const AlertCommentEventComponent: React.FC = ({ }, [ruleId, onRuleDetailsClick] ); - const detectionsRuleDetailsHref = getRuleDetailsHref(ruleId); + const detectionsRuleDetailsHref = getRuleDetailsHref?.(ruleId); + const finalRuleName = ruleName ?? i18n.UNKNOWN_RULE; - return commentType !== CommentType.generatedAlert ? ( + return ( <> {`${i18n.ALERT_COMMENT_LABEL_TITLE} `} {loadingAlertData && } - {!loadingAlertData && !isEmpty(ruleId) && ( + {!loadingAlertData && !isEmpty(ruleId) && detectionsRuleDetailsHref != null && ( - {ruleName ?? i18n.UNKNOWN_RULE} + {finalRuleName} )} - {!loadingAlertData && isEmpty(ruleId) && i18n.UNKNOWN_RULE} - - ) : ( - <> - {i18n.GENERATED_ALERT_COUNT_COMMENT_LABEL_TITLE(alertsCount ?? 0)}{' '} - {i18n.GENERATED_ALERT_COMMENT_LABEL_TITLE}{' '} - {loadingAlertData && } - {!loadingAlertData && ruleId !== '' && ( - - {ruleName} - - )} - {!loadingAlertData && ruleId === '' && {ruleName}} + {!loadingAlertData && !isEmpty(ruleId) && detectionsRuleDetailsHref == null && finalRuleName} + {!loadingAlertData && isEmpty(ruleId) && finalRuleName} ); }; diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx new file mode 100644 index 00000000000000..ca45191dd4cb16 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { EuiCommentList } from '@elastic/eui'; +import { render, screen } from '@testing-library/react'; + +import { Actions } from '../../../../common/api'; +import { + alertComment, + basicCase, + getAlertUserAction, + getHostIsolationUserAction, + getUserAction, + hostIsolationComment, +} from '../../../containers/mock'; +import { TestProviders } from '../../../common/mock'; +import { createCommentUserActionBuilder } from './comment'; +import { getMockBuilderArgs } from '../mock'; + +jest.mock('../../../common/lib/kibana'); +jest.mock('../../../common/navigation/hooks'); + +describe('createCommentUserActionBuilder', () => { + const builderArgs = getMockBuilderArgs(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly when editing a comment', async () => { + const userAction = getUserAction('comment', Actions.update); + const builder = createCommentUserActionBuilder({ + ...builderArgs, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByText('edited comment')).toBeInTheDocument(); + }); + + it('renders correctly a user comment', async () => { + const userAction = getUserAction('comment', Actions.create, { + commentId: basicCase.comments[0].id, + }); + + const builder = createCommentUserActionBuilder({ + ...builderArgs, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByText('Solve this fast!')).toBeInTheDocument(); + }); + + it('renders correctly an alert', async () => { + const userAction = getAlertUserAction(); + + const builder = createCommentUserActionBuilder({ + ...builderArgs, + caseData: { + ...builderArgs.caseData, + comments: [alertComment], + }, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByText('added an alert from')).toBeInTheDocument(); + expect(screen.getByText('Awesome rule')).toBeInTheDocument(); + }); + + it('renders correctly an action', async () => { + const userAction = getHostIsolationUserAction(); + + const builder = createCommentUserActionBuilder({ + ...builderArgs, + caseData: { + ...builderArgs.caseData, + comments: [hostIsolationComment()], + }, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByText('submitted isolate request on host')).toBeInTheDocument(); + expect(screen.getByText('host1')).toBeInTheDocument(); + expect(screen.getByText('I just isolated the host!')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/comment.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/comment.tsx new file mode 100644 index 00000000000000..79df2aaca99785 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/comment/comment.tsx @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiCommentProps } from '@elastic/eui'; + +import { CommentUserAction, Actions, CommentType } from '../../../../common/api'; +import { UserActionBuilder, UserActionBuilderArgs, UserActionResponse } from '../types'; +import { createCommonUpdateUserActionBuilder } from '../common'; +import { Comment } from '../../../containers/types'; +import * as i18n from '../translations'; +import { createUserAttachmentUserActionBuilder } from './user'; +import { createAlertAttachmentUserActionBuilder } from './alert'; +import { createActionAttachmentUserActionBuilder } from './actions'; + +const getUpdateLabelTitle = () => `${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`; + +const getCreateCommentUserAction = ({ + userAction, + comment, + userCanCrud, + commentRefs, + manageMarkdownEditIds, + selectedOutlineCommentId, + loadingCommentIds, + handleManageMarkdownEditId, + handleSaveComment, + handleManageQuote, + getRuleDetailsHref, + loadingAlertData, + onRuleDetailsClick, + alertData, + onShowAlertDetails, + actionsNavigation, +}: { + userAction: UserActionResponse; + comment: Comment; +} & Omit< + UserActionBuilderArgs, + 'caseData' | 'caseServices' | 'comments' | 'index' | 'handleOutlineComment' +>): EuiCommentProps[] => { + switch (comment.type) { + case CommentType.user: + const userBuilder = createUserAttachmentUserActionBuilder({ + comment, + userCanCrud, + outlined: comment.id === selectedOutlineCommentId, + isEdit: manageMarkdownEditIds.includes(comment.id), + commentRefs, + isLoading: loadingCommentIds.includes(comment.id), + handleManageMarkdownEditId, + handleSaveComment, + handleManageQuote, + }); + + return userBuilder.build(); + + case CommentType.alert: + const alertBuilder = createAlertAttachmentUserActionBuilder({ + alertData, + comment, + userAction, + getRuleDetailsHref, + loadingAlertData, + onRuleDetailsClick, + onShowAlertDetails, + }); + return alertBuilder.build(); + case CommentType.actions: + const actionBuilder = createActionAttachmentUserActionBuilder({ + userAction, + comment, + actionsNavigation, + }); + return actionBuilder.build(); + default: + return []; + } +}; + +export const createCommentUserActionBuilder: UserActionBuilder = ({ + caseData, + userAction, + userCanCrud, + commentRefs, + manageMarkdownEditIds, + selectedOutlineCommentId, + loadingCommentIds, + loadingAlertData, + alertData, + getRuleDetailsHref, + onRuleDetailsClick, + onShowAlertDetails, + handleManageMarkdownEditId, + handleSaveComment, + handleManageQuote, + handleOutlineComment, +}) => ({ + build: () => { + const commentUserAction = userAction as UserActionResponse; + const comment = caseData.comments.find((c) => c.id === commentUserAction.commentId); + + if (comment == null) { + return []; + } + + if (commentUserAction.action === Actions.create) { + const commentAction = getCreateCommentUserAction({ + userAction: commentUserAction, + comment, + userCanCrud, + commentRefs, + manageMarkdownEditIds, + selectedOutlineCommentId, + loadingCommentIds, + loadingAlertData, + alertData, + getRuleDetailsHref, + onRuleDetailsClick, + onShowAlertDetails, + handleManageMarkdownEditId, + handleSaveComment, + handleManageQuote, + }); + + return commentAction; + } + + const label = getUpdateLabelTitle(); + const commonBuilder = createCommonUpdateUserActionBuilder({ + userAction, + handleOutlineComment, + label, + icon: 'dot', + }); + + return commonBuilder.build(); + }, +}); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.test.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/host_isolation_event.test.tsx similarity index 93% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.test.tsx rename to x-pack/plugins/cases/public/components/user_actions/comment/host_isolation_event.test.tsx index 80f9985ef15c15..64619ad1379507 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/host_isolation_event.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { HostIsolationCommentEvent } from './user_action_host_isolation_comment_event'; +import { HostIsolationCommentEvent } from './host_isolation_event'; const defaultProps = () => { return { diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/host_isolation_event.tsx similarity index 91% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.tsx rename to x-pack/plugins/cases/public/components/user_actions/comment/host_isolation_event.tsx index 2381d31b3ada88..531323e548dd1c 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/host_isolation_event.tsx @@ -6,9 +6,9 @@ */ import React, { memo, useCallback } from 'react'; -import * as i18n from './translations'; -import { LinkAnchor } from '../links'; -import { ActionsNavigation } from './helpers'; +import * as i18n from '../translations'; +import { LinkAnchor } from '../../links'; +import { ActionsNavigation } from '../types'; interface EndpointInfo { endpointId: string; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_show_alert.test.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/show_alert.test.tsx similarity index 94% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_show_alert.test.tsx rename to x-pack/plugins/cases/public/components/user_actions/comment/show_alert.test.tsx index d6005a8bd521e9..cc570b245ec90a 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_show_alert.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/show_alert.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; -import { UserActionShowAlert } from './user_action_show_alert'; +import { UserActionShowAlert } from './show_alert'; const props = { id: 'action-id', diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_show_alert.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/show_alert.tsx similarity index 96% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_show_alert.tsx rename to x-pack/plugins/cases/public/components/user_actions/comment/show_alert.tsx index c16382a96bb984..dd874b029dc9c3 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_show_alert.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/show_alert.tsx @@ -7,7 +7,7 @@ import React, { memo, useCallback } from 'react'; import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; -import * as i18n from './translations'; +import * as i18n from '../translations'; interface UserActionShowAlertProps { id: string; diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/user.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/user.tsx new file mode 100644 index 00000000000000..e48246a3754674 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/comment/user.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 classNames from 'classnames'; + +import { CommentResponseUserType } from '../../../../common/api'; +import { UserActionTimestamp } from '../timestamp'; +import { SnakeToCamelCase } from '../../../../common/types'; +import { UserActionMarkdown } from '../markdown_form'; +import { UserActionAvatar } from '../avatar'; +import { UserActionContentToolbar } from '../content_toolbar'; +import { UserActionUsername } from '../username'; +import * as i18n from '../translations'; +import { UserActionBuilderArgs, UserActionBuilder } from '../types'; + +type BuilderArgs = Pick< + UserActionBuilderArgs, + | 'userCanCrud' + | 'handleManageMarkdownEditId' + | 'handleSaveComment' + | 'handleManageQuote' + | 'commentRefs' +> & { + comment: SnakeToCamelCase; + outlined: boolean; + isEdit: boolean; + isLoading: boolean; +}; + +export const createUserAttachmentUserActionBuilder = ({ + comment, + userCanCrud, + outlined, + isEdit, + isLoading, + commentRefs, + handleManageMarkdownEditId, + handleSaveComment, + handleManageQuote, +}: BuilderArgs): ReturnType => ({ + build: () => [ + { + username: ( + + ), + 'data-test-subj': `comment-create-action-${comment.id}`, + timestamp: ( + + ), + className: classNames('userAction__comment', { + outlined, + isEdit, + }), + children: ( + (commentRefs.current[comment.id] = element)} + id={comment.id} + content={comment.comment} + isEditable={isEdit} + onChangeEditable={handleManageMarkdownEditId} + onSaveContent={handleSaveComment.bind(null, { + id: comment.id, + version: comment.version, + })} + /> + ), + timelineIcon: ( + + ), + actions: ( + + ), + }, + ], +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/common.test.tsx b/x-pack/plugins/cases/public/components/user_actions/common.test.tsx new file mode 100644 index 00000000000000..ffb99757b45ac4 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/common.test.tsx @@ -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 React from 'react'; +import { EuiCommentList } from '@elastic/eui'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import copy from 'copy-to-clipboard'; + +import { Actions } from '../../../common/api'; +import { createCommonUpdateUserActionBuilder } from './common'; +import { getUserAction } from '../../containers/mock'; +import { TestProviders } from '../../common/mock'; + +jest.mock('../../common/lib/kibana'); +jest.mock('../../common/navigation/hooks'); +jest.mock('copy-to-clipboard', () => jest.fn()); + +describe('createCommonUpdateUserActionBuilder ', () => { + const label = <>{'A label'}; + const handleOutlineComment = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly', async () => { + const userAction = getUserAction('title', Actions.update); + const builder = createCommonUpdateUserActionBuilder({ + userAction, + label, + icon: 'dot', + handleOutlineComment, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + // The avatar + expect(screen.getByText('LK')).toBeInTheDocument(); + // The username + expect(screen.getByText(userAction.createdBy.username!)).toBeInTheDocument(); + // The label of the event + expect(screen.getByText('A label')).toBeInTheDocument(); + // The copy link button + expect(screen.getByLabelText('Copy reference link')).toBeInTheDocument(); + }); + + it('renders shows the move to comment button if the user action is an edit comment', async () => { + const userAction = getUserAction('comment', Actions.update); + const builder = createCommonUpdateUserActionBuilder({ + userAction, + label, + icon: 'dot', + handleOutlineComment, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByLabelText('Highlight the referenced comment')).toBeInTheDocument(); + }); + + it('it copies the reference link when clicking the reference button', async () => { + const userAction = getUserAction('comment', Actions.update); + const builder = createCommonUpdateUserActionBuilder({ + userAction, + label, + icon: 'dot', + handleOutlineComment, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + userEvent.click(screen.getByLabelText('Copy reference link')); + expect(copy).toHaveBeenCalled(); + }); + + it('calls the handleOutlineComment when clicking the reference button', async () => { + const userAction = getUserAction('comment', Actions.update); + const builder = createCommonUpdateUserActionBuilder({ + userAction, + label, + icon: 'dot', + handleOutlineComment, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + userEvent.click(screen.getByLabelText('Highlight the referenced comment')); + expect(handleOutlineComment).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/common.tsx b/x-pack/plugins/cases/public/components/user_actions/common.tsx new file mode 100644 index 00000000000000..407fface85cebc --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/common.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiCommentProps, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { Actions, ConnectorUserAction, UserAction } from '../../../common/api'; +import { UserActionTimestamp } from './timestamp'; +import { UserActionBuilder, UserActionBuilderArgs, UserActionResponse } from './types'; +import { UserActionUsernameWithAvatar } from './avatar_username'; +import { UserActionCopyLink } from './copy_link'; +import { UserActionMoveToReference } from './move_to_reference'; + +interface Props { + userAction: UserActionResponse; + handleOutlineComment: (id: string) => void; +} + +const showMoveToReference = (action: UserAction, commentId: string | null): commentId is string => + action === Actions.update && commentId != null; + +const CommentListActions: React.FC = React.memo(({ userAction, handleOutlineComment }) => ( + + + + + {showMoveToReference(userAction.action, userAction.commentId) && ( + + + + )} + +)); + +CommentListActions.displayName = 'CommentListActions'; + +type BuilderArgs = Pick & { + label: EuiCommentProps['event']; + icon: EuiCommentProps['timelineIcon']; +}; + +export const createCommonUpdateUserActionBuilder = ({ + userAction, + label, + icon, + handleOutlineComment, +}: BuilderArgs): ReturnType => ({ + build: () => [ + { + username: ( + + ), + type: 'update' as const, + event: label, + 'data-test-subj': `${userAction.type}-${userAction.action}-action-${userAction.actionId}`, + timestamp: , + timelineIcon: icon, + actions: ( + + + + + {showMoveToReference(userAction.action, userAction.commentId) && ( + + + + )} + + ), + }, + ], +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/connector.test.tsx b/x-pack/plugins/cases/public/components/user_actions/connector.test.tsx new file mode 100644 index 00000000000000..51abe66a27fdaf --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/connector.test.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { EuiCommentList } from '@elastic/eui'; +import { render, screen } from '@testing-library/react'; + +import { Actions, NONE_CONNECTOR_ID } from '../../../common/api'; +import { getUserAction, getJiraConnector } from '../../containers/mock'; +import { TestProviders } from '../../common/mock'; +import { createConnectorUserActionBuilder } from './connector'; +import { getMockBuilderArgs } from './mock'; + +jest.mock('../../common/lib/kibana'); +jest.mock('../../common/navigation/hooks'); + +describe('createConnectorUserActionBuilder ', () => { + const builderArgs = getMockBuilderArgs(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly', async () => { + const userAction = getUserAction('connector', Actions.update, { + payload: { connector: getJiraConnector() }, + }); + + const builder = createConnectorUserActionBuilder({ + ...builderArgs, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByText('selected jira1 as incident management system')).toBeInTheDocument(); + }); + + it('renders the removed connector label if the connector is none', async () => { + const userAction = getUserAction('connector', Actions.update, { + payload: { connector: { ...getJiraConnector(), id: NONE_CONNECTOR_ID } }, + }); + + const builder = createConnectorUserActionBuilder({ + ...builderArgs, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByText('removed external incident management system')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/connector.tsx b/x-pack/plugins/cases/public/components/user_actions/connector.tsx new file mode 100644 index 00000000000000..70c1ecf274736a --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/connector.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ConnectorUserAction, NONE_CONNECTOR_ID } from '../../../common/api'; +import { UserActionBuilder, UserActionResponse } from './types'; +import { createCommonUpdateUserActionBuilder } from './common'; +import * as i18n from './translations'; + +const getLabelTitle = (userAction: UserActionResponse) => { + const connector = userAction.payload.connector; + + if (connector == null) { + return ''; + } + + if (connector.id === NONE_CONNECTOR_ID) { + return i18n.REMOVED_THIRD_PARTY; + } + + return i18n.SELECTED_THIRD_PARTY(connector.name); +}; + +export const createConnectorUserActionBuilder: UserActionBuilder = ({ + userAction, + handleOutlineComment, +}) => ({ + build: () => { + const connectorUserAction = userAction as UserActionResponse; + const label = getLabelTitle(connectorUserAction); + const commonBuilder = createCommonUpdateUserActionBuilder({ + userAction, + handleOutlineComment, + label, + icon: 'dot', + }); + + return commonBuilder.build(); + }, +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/constants.ts b/x-pack/plugins/cases/public/components/user_actions/constants.ts new file mode 100644 index 00000000000000..4cdc0f4fb5edb6 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/constants.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { omit } from 'lodash'; +import { ActionTypes } from '../../../common/api'; +import { SupportedUserActionTypes } from './types'; + +export const DRAFT_COMMENT_STORAGE_ID = 'xpack.cases.commentDraft'; + +export const UNSUPPORTED_ACTION_TYPES = ['create_case', 'delete_case', 'settings'] as const; +export const SUPPORTED_ACTION_TYPES: SupportedUserActionTypes[] = Object.keys( + omit(ActionTypes, UNSUPPORTED_ACTION_TYPES) +) as SupportedUserActionTypes[]; + +export const NEW_COMMENT_ID = 'newComment'; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.test.tsx b/x-pack/plugins/cases/public/components/user_actions/content_toolbar.test.tsx similarity index 90% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.test.tsx rename to x-pack/plugins/cases/public/components/user_actions/content_toolbar.test.tsx index c2edfe27397159..74f5205578a1d2 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/content_toolbar.test.tsx @@ -7,10 +7,7 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; -import { - UserActionContentToolbar, - UserActionContentToolbarProps, -} from './user_action_content_toolbar'; +import { UserActionContentToolbar, UserActionContentToolbarProps } from './content_toolbar'; jest.mock('../../common/navigation/hooks'); jest.mock('../../common/lib/kibana'); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.tsx b/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx similarity index 90% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.tsx rename to x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx index ab030348595d1d..dee1a25c3b79c9 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx @@ -8,8 +8,8 @@ import React, { memo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { UserActionCopyLink } from './user_action_copy_link'; -import { UserActionPropertyActions } from './user_action_property_actions'; +import { UserActionCopyLink } from './copy_link'; +import { UserActionPropertyActions } from './property_actions'; export interface UserActionContentToolbarProps { commentMarkdown: string; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_copy_link.test.tsx b/x-pack/plugins/cases/public/components/user_actions/copy_link.test.tsx similarity index 96% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_copy_link.test.tsx rename to x-pack/plugins/cases/public/components/user_actions/copy_link.test.tsx index 4e3496a06bb72a..d4b093eed12f7c 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_copy_link.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/copy_link.test.tsx @@ -11,7 +11,7 @@ import copy from 'copy-to-clipboard'; import { useKibana } from '../../common/lib/kibana'; import { TestProviders } from '../../common/mock'; -import { UserActionCopyLink } from './user_action_copy_link'; +import { UserActionCopyLink } from './copy_link'; const useKibanaMock = useKibana as jest.Mocked; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_copy_link.tsx b/x-pack/plugins/cases/public/components/user_actions/copy_link.tsx similarity index 100% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_copy_link.tsx rename to x-pack/plugins/cases/public/components/user_actions/copy_link.tsx diff --git a/x-pack/plugins/cases/public/components/user_actions/description.test.tsx b/x-pack/plugins/cases/public/components/user_actions/description.test.tsx new file mode 100644 index 00000000000000..d76829add5a638 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/description.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiCommentList } from '@elastic/eui'; +import { render, screen } from '@testing-library/react'; + +import { Actions } from '../../../common/api'; +import { getUserAction } from '../../containers/mock'; +import { TestProviders } from '../../common/mock'; +import { createDescriptionUserActionBuilder } from './description'; +import { getMockBuilderArgs } from './mock'; + +jest.mock('../../common/lib/kibana'); +jest.mock('../../common/navigation/hooks'); + +describe('createDescriptionUserActionBuilder ', () => { + const builderArgs = getMockBuilderArgs(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly when editing a description', async () => { + const userAction = getUserAction('description', Actions.update); + const builder = createDescriptionUserActionBuilder({ + ...builderArgs, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByText('edited description')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/description.tsx b/x-pack/plugins/cases/public/components/user_actions/description.tsx new file mode 100644 index 00000000000000..01b0e105ecd965 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/description.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 classNames from 'classnames'; +import { EuiCommentProps } from '@elastic/eui'; + +import type { UserActionBuilder, UserActionBuilderArgs, UserActionTreeProps } from './types'; +import { createCommonUpdateUserActionBuilder } from './common'; +import { UserActionUsername } from './username'; +import { UserActionAvatar } from './avatar'; +import { UserActionContentToolbar } from './content_toolbar'; +import { UserActionTimestamp } from './timestamp'; +import { UserActionMarkdown } from './markdown_form'; +import * as i18n from './translations'; + +const DESCRIPTION_ID = 'description'; + +const getLabelTitle = () => `${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`; + +type GetDescriptionUserActionArgs = Pick< + UserActionBuilderArgs, + | 'caseData' + | 'commentRefs' + | 'manageMarkdownEditIds' + | 'userCanCrud' + | 'handleManageMarkdownEditId' + | 'handleManageQuote' +> & + Pick; + +export const getDescriptionUserAction = ({ + caseData, + commentRefs, + manageMarkdownEditIds, + isLoadingDescription, + userCanCrud, + onUpdateField, + handleManageMarkdownEditId, + handleManageQuote, +}: GetDescriptionUserActionArgs): EuiCommentProps => { + return { + username: ( + + ), + event: i18n.ADDED_DESCRIPTION, + 'data-test-subj': 'description-action', + timestamp: , + children: ( + (commentRefs.current[DESCRIPTION_ID] = element)} + id={DESCRIPTION_ID} + content={caseData.description} + isEditable={manageMarkdownEditIds.includes(DESCRIPTION_ID)} + onSaveContent={(content: string) => { + onUpdateField({ key: DESCRIPTION_ID, value: content }); + }} + onChangeEditable={handleManageMarkdownEditId} + /> + ), + timelineIcon: ( + + ), + className: classNames({ + isEdit: manageMarkdownEditIds.includes(DESCRIPTION_ID), + }), + actions: ( + + ), + }; +}; + +export const createDescriptionUserActionBuilder: UserActionBuilder = ({ + userAction, + handleOutlineComment, +}) => ({ + build: () => { + const label = getLabelTitle(); + const commonBuilder = createCommonUpdateUserActionBuilder({ + userAction, + handleOutlineComment, + label, + icon: 'dot', + }); + + return commonBuilder.build(); + }, +}); diff --git a/x-pack/plugins/cases/public/components/case_view/helpers.test.tsx b/x-pack/plugins/cases/public/components/user_actions/helpers.test.ts similarity index 63% rename from x-pack/plugins/cases/public/components/case_view/helpers.test.tsx rename to x-pack/plugins/cases/public/components/user_actions/helpers.test.ts index e398c5edad145d..dd75ed8c6fcd88 100644 --- a/x-pack/plugins/cases/public/components/case_view/helpers.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/helpers.test.ts @@ -8,8 +8,7 @@ import { AssociationType, CommentType } from '../../../common/api'; import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { Comment } from '../../containers/types'; - -import { getManualAlertIdsWithNoRuleId } from './helpers'; +import { isUserActionTypeSupported, getManualAlertIdsWithNoRuleId } from './helpers'; const comments: Comment[] = [ { @@ -19,7 +18,7 @@ const comments: Comment[] = [ index: 'alert-index-1', id: 'comment-id', createdAt: '2020-02-19T23:06:33.798Z', - createdBy: { username: 'elastic' }, + createdBy: { username: 'elastic', email: 'elastic@elastic.co', fullName: 'Elastic' }, rule: { id: null, name: null, @@ -38,7 +37,7 @@ const comments: Comment[] = [ index: 'alert-index-2', id: 'comment-id', createdAt: '2020-02-19T23:06:33.798Z', - createdBy: { username: 'elastic' }, + createdBy: { username: 'elastic', email: 'elastic@elastic.co', fullName: 'Elastic' }, pushedAt: null, pushedBy: null, rule: { @@ -52,7 +51,28 @@ const comments: Comment[] = [ }, ]; -describe('Case view helpers', () => { +describe('Case view helpers', () => {}); + +describe('helpers', () => { + describe('isUserActionTypeSupported', () => { + const types: Array<[string, boolean]> = [ + ['comment', true], + ['connector', true], + ['description', true], + ['pushed', true], + ['tags', true], + ['title', true], + ['status', true], + ['settings', false], + ['create_case', false], + ['delete_case', false], + ]; + + it.each(types)('determines if the type is support %s', (type, supported) => { + expect(isUserActionTypeSupported(type)).toBe(supported); + }); + }); + describe('getAlertIdsFromComments', () => { it('it returns the alert id from the comments where rule is not defined', () => { expect(getManualAlertIdsWithNoRuleId(comments)).toEqual(['alert-id-1']); diff --git a/x-pack/plugins/cases/public/components/case_view/helpers.ts b/x-pack/plugins/cases/public/components/user_actions/helpers.ts similarity index 76% rename from x-pack/plugins/cases/public/components/case_view/helpers.ts rename to x-pack/plugins/cases/public/components/user_actions/helpers.ts index 04052d1eedea55..673af99ed77720 100644 --- a/x-pack/plugins/cases/public/components/case_view/helpers.ts +++ b/x-pack/plugins/cases/public/components/user_actions/helpers.ts @@ -8,6 +8,11 @@ import { isEmpty } from 'lodash'; import { CommentType } from '../../../common/api'; import type { Comment } from '../../containers/types'; +import { SUPPORTED_ACTION_TYPES } from './constants'; +import { SupportedUserActionTypes } from './types'; + +export const isUserActionTypeSupported = (type: string): type is SupportedUserActionTypes => + SUPPORTED_ACTION_TYPES.includes(type as SupportedUserActionTypes); export const getManualAlertIdsWithNoRuleId = (comments: Comment[]): string[] => { const dedupeAlerts = comments.reduce((alertIds, comment: Comment) => { diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx similarity index 94% rename from x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx rename to x-pack/plugins/cases/public/components/user_actions/index.test.tsx index 1e7b0dca172ca0..67e9b4505ae620 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx @@ -20,7 +20,7 @@ import { hostIsolationComment, hostReleaseComment, } from '../../containers/mock'; -import { UserActionTree } from '.'; +import { UserActions } from '.'; import { TestProviders } from '../../common/mock'; import { Ecs } from '../../../common/ui/types'; import { Actions } from '../../../common/api'; @@ -53,23 +53,25 @@ const defaultProps = { alerts: {}, onShowAlertDetails, }; -const useUpdateCommentMock = useUpdateComment as jest.Mock; + jest.mock('../../containers/use_update_comment'); -jest.mock('./user_action_timestamp'); +jest.mock('./timestamp'); jest.mock('../../common/lib/kibana'); +const useUpdateCommentMock = useUpdateComment as jest.Mock; const patchComment = jest.fn(); -describe(`UserActionTree`, () => { +describe(`UserActions`, () => { const sampleData = { content: 'what a great comment update', }; + beforeEach(() => { jest.clearAllMocks(); - useUpdateCommentMock.mockImplementation(() => ({ + useUpdateCommentMock.mockReturnValue({ isLoadingIds: [], patchComment, - })); + }); jest .spyOn(routeData, 'useParams') @@ -79,15 +81,14 @@ describe(`UserActionTree`, () => { it('Loading spinner when user actions loading and displays fullName/username', () => { const wrapper = mount( - + ); - expect(wrapper.find(`[data-test-subj="user-actions-loading"]`).exists()).toEqual(true); + expect(wrapper.find(`[data-test-subj="user-actions-loading"]`).exists()).toEqual(true); expect(wrapper.find(`[data-test-subj="user-action-avatar"]`).first().prop('name')).toEqual( defaultProps.data.createdBy.fullName ); - expect( wrapper.find(`[data-test-subj="description-action"] figcaption strong`).first().text() ).toEqual(defaultProps.data.createdBy.username); @@ -114,7 +115,7 @@ describe(`UserActionTree`, () => { }; const wrapper = mount( - + ); await waitFor(() => { @@ -141,7 +142,7 @@ describe(`UserActionTree`, () => { const wrapper = mount( - + ); await waitFor(() => { @@ -161,7 +162,7 @@ describe(`UserActionTree`, () => { const wrapper = mount( - + ); expect( @@ -196,7 +197,7 @@ describe(`UserActionTree`, () => { const wrapper = mount( - + ); @@ -240,7 +241,7 @@ describe(`UserActionTree`, () => { const wrapper = mount( - + ); @@ -296,7 +297,7 @@ describe(`UserActionTree`, () => { it('calls update description when description markdown is saved', async () => { const wrapper = mount( - + ); @@ -340,7 +341,7 @@ describe(`UserActionTree`, () => { const wrapper = mount( - + ); @@ -373,7 +374,7 @@ describe(`UserActionTree`, () => { const wrapper = mount( - + ); await waitFor(() => { @@ -397,7 +398,7 @@ describe(`UserActionTree`, () => { const wrapper = mount( - + ); await waitFor(() => { @@ -415,7 +416,7 @@ describe(`UserActionTree`, () => { const wrapper = mount( - + ); await waitFor(() => { @@ -435,7 +436,7 @@ describe(`UserActionTree`, () => { const wrapper = mount( - + ); await waitFor(() => { @@ -454,7 +455,7 @@ describe(`UserActionTree`, () => { const wrapper = mount( - + ); await waitFor(() => { diff --git a/x-pack/plugins/cases/public/components/user_actions/index.tsx b/x-pack/plugins/cases/public/components/user_actions/index.tsx new file mode 100644 index 00000000000000..084d1c548f9031 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/index.tsx @@ -0,0 +1,275 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiCommentList, + EuiCommentProps, +} from '@elastic/eui'; + +import React, { useMemo, useState, useEffect } from 'react'; +import styled from 'styled-components'; + +import { useCurrentUser } from '../../common/lib/kibana'; +import { AddComment } from '../add_comment'; +import { UserActionAvatar } from './avatar'; +import { UserActionUsername } from './username'; +import { useCaseViewParams } from '../../common/navigation'; +import { builderMap } from './builder'; +import { isUserActionTypeSupported, getManualAlertIdsWithNoRuleId } from './helpers'; +import type { UserActionTreeProps } from './types'; +import { getDescriptionUserAction } from './description'; +import { useUserActionsHandler } from './use_user_actions_handler'; +import { NEW_COMMENT_ID } from './constants'; + +const MyEuiFlexGroup = styled(EuiFlexGroup)` + margin-bottom: 8px; +`; + +const MyEuiCommentList = styled(EuiCommentList)` + ${({ theme }) => ` + & .userAction__comment.outlined .euiCommentEvent { + outline: solid 5px ${theme.eui.euiColorVis1_behindText}; + margin: 0.5em; + transition: 0.8s; + } + + & .euiComment.isEdit { + & .euiCommentEvent { + border: none; + box-shadow: none; + } + + & .euiCommentEvent__body { + padding: 0; + } + + & .euiCommentEvent__header { + display: none; + } + } + + & .comment-alert .euiCommentEvent { + background-color: ${theme.eui.euiColorLightestShade}; + border: ${theme.eui.euiFlyoutBorder}; + padding: ${theme.eui.paddingSizes.s}; + border-radius: ${theme.eui.paddingSizes.xs}; + } + + & .comment-alert .euiCommentEvent__headerData { + flex-grow: 1; + } + + & .comment-action.empty-comment .euiCommentEvent--regular { + box-shadow: none; + .euiCommentEvent__header { + padding: ${theme.eui.euiSizeM} ${theme.eui.paddingSizes.s}; + border-bottom: 0; + } + } + `} +`; + +export const UserActions = React.memo( + ({ + caseServices, + caseUserActions, + data: caseData, + fetchUserActions, + getRuleDetailsHref, + actionsNavigation, + isLoadingDescription, + isLoadingUserActions, + onRuleDetailsClick, + onShowAlertDetails, + onUpdateField, + renderInvestigateInTimelineActionComponent, + statusActionButton, + updateCase, + useFetchAlertData, + userCanCrud, + }: UserActionTreeProps) => { + const { detailName: caseId, subCaseId, commentId } = useCaseViewParams(); + const [initLoading, setInitLoading] = useState(true); + const currentUser = useCurrentUser(); + + const [loadingAlertData, manualAlertsData] = useFetchAlertData( + getManualAlertIdsWithNoRuleId(caseData.comments) + ); + + const { + loadingCommentIds, + commentRefs, + selectedOutlineCommentId, + manageMarkdownEditIds, + handleManageMarkdownEditId, + handleOutlineComment, + handleSaveComment, + handleManageQuote, + handleUpdate, + } = useUserActionsHandler({ fetchUserActions, updateCase }); + + const MarkdownNewComment = useMemo( + () => ( + (commentRefs.current[NEW_COMMENT_ID] = element)} + onCommentPosted={handleUpdate} + onCommentSaving={handleManageMarkdownEditId.bind(null, NEW_COMMENT_ID)} + showLoading={false} + statusActionButton={statusActionButton} + subCaseId={subCaseId} + /> + ), + [ + caseId, + userCanCrud, + handleUpdate, + handleManageMarkdownEditId, + statusActionButton, + subCaseId, + commentRefs, + ] + ); + + useEffect(() => { + if (initLoading && !isLoadingUserActions && loadingCommentIds.length === 0) { + setInitLoading(false); + if (commentId != null) { + handleOutlineComment(commentId); + } + } + }, [commentId, initLoading, isLoadingUserActions, loadingCommentIds, handleOutlineComment]); + + const descriptionCommentListObj: EuiCommentProps = useMemo( + () => + getDescriptionUserAction({ + caseData, + commentRefs, + manageMarkdownEditIds, + isLoadingDescription, + userCanCrud, + onUpdateField, + handleManageMarkdownEditId, + handleManageQuote, + }), + [ + caseData, + commentRefs, + manageMarkdownEditIds, + isLoadingDescription, + userCanCrud, + onUpdateField, + handleManageMarkdownEditId, + handleManageQuote, + ] + ); + + const userActions: EuiCommentProps[] = useMemo( + () => + caseUserActions.reduce( + (comments, userAction, index) => { + if (!isUserActionTypeSupported(userAction.type)) { + return comments; + } + + const builder = builderMap[userAction.type]; + + if (builder == null) { + return comments; + } + + const userActionBuilder = builder({ + caseData, + userAction, + caseServices, + comments: caseData.comments, + index, + userCanCrud, + commentRefs, + manageMarkdownEditIds, + selectedOutlineCommentId, + loadingCommentIds, + loadingAlertData, + alertData: manualAlertsData, + handleOutlineComment, + handleManageMarkdownEditId, + handleSaveComment, + handleManageQuote, + onShowAlertDetails, + actionsNavigation, + getRuleDetailsHref, + onRuleDetailsClick, + }); + return [...comments, ...userActionBuilder.build()]; + }, + [descriptionCommentListObj] + ), + [ + caseUserActions, + descriptionCommentListObj, + caseData, + caseServices, + userCanCrud, + commentRefs, + manageMarkdownEditIds, + selectedOutlineCommentId, + loadingCommentIds, + loadingAlertData, + manualAlertsData, + handleOutlineComment, + handleManageMarkdownEditId, + handleSaveComment, + handleManageQuote, + onShowAlertDetails, + actionsNavigation, + getRuleDetailsHref, + onRuleDetailsClick, + ] + ); + + const bottomActions = userCanCrud + ? [ + { + username: ( + + ), + 'data-test-subj': 'add-comment', + timelineIcon: ( + + ), + className: 'isEdit', + children: MarkdownNewComment, + }, + ] + : []; + + const comments = [...userActions, ...bottomActions]; + + return ( + <> + + {(isLoadingUserActions || loadingCommentIds.includes(NEW_COMMENT_ID)) && ( + + + + + + )} + + ); + } +); + +UserActions.displayName = 'UserActions'; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.test.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx similarity index 97% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.test.tsx rename to x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx index 0695f9d5a2c44e..19f60d7cb8c72c 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { UserActionMarkdown } from './user_action_markdown'; +import { UserActionMarkdown } from './markdown_form'; import { TestProviders } from '../../common/mock'; import { waitFor } from '@testing-library/react'; const onChangeEditable = jest.fn(); diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx new file mode 100644 index 00000000000000..f63ce9b3fce880 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui'; +import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react'; +import styled from 'styled-components'; + +import * as i18n from '../case_view/translations'; +import { Form, useForm, UseField } from '../../common/shared_imports'; +import { schema, Content } from './schema'; +import { MarkdownRenderer, MarkdownEditorForm } from '../markdown_editor'; + +export const ContentWrapper = styled.div` + padding: ${({ theme }) => `${theme.eui.euiSizeM} ${theme.eui.euiSizeL}`}; +`; + +interface UserActionMarkdownProps { + content: string; + id: string; + isEditable: boolean; + onChangeEditable: (id: string) => void; + onSaveContent: (content: string) => void; +} + +export interface UserActionMarkdownRefObject { + setComment: (newComment: string) => void; +} + +const UserActionMarkdownComponent = forwardRef< + UserActionMarkdownRefObject, + UserActionMarkdownProps +>(({ id, content, isEditable, onChangeEditable, onSaveContent }, ref) => { + const editorRef = useRef(); + const initialState = { content }; + const { form } = useForm({ + defaultValue: initialState, + options: { stripEmptyFields: false }, + schema, + }); + + const fieldName = 'content'; + const { setFieldValue, submit } = form; + + const handleCancelAction = useCallback(() => { + onChangeEditable(id); + }, [id, onChangeEditable]); + + const handleSaveAction = useCallback(async () => { + const { isValid, data } = await submit(); + + if (isValid && data.content !== content) { + onSaveContent(data.content); + } + onChangeEditable(id); + }, [content, id, onChangeEditable, onSaveContent, submit]); + + const setComment = useCallback( + (newComment) => { + setFieldValue(fieldName, newComment); + }, + [setFieldValue] + ); + + const EditorButtons = useMemo( + () => ( + + + + {i18n.CANCEL} + + + + + {i18n.SAVE} + + + + ), + [handleCancelAction, handleSaveAction] + ); + + useImperativeHandle(ref, () => ({ + setComment, + editor: editorRef.current, + })); + + return isEditable ? ( +
+ + + ) : ( + + {content} + + ); +}); + +UserActionMarkdownComponent.displayName = 'UserActionMarkdownComponent'; + +export const UserActionMarkdown = React.memo(UserActionMarkdownComponent); diff --git a/x-pack/plugins/cases/public/components/user_actions/mock.ts b/x-pack/plugins/cases/public/components/user_actions/mock.ts new file mode 100644 index 00000000000000..7000f407f97e53 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/mock.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Actions } from '../../../common/api'; +import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; +import { basicCase, basicPush, getUserAction } from '../../containers/mock'; +import { UserActionBuilderArgs } from './types'; + +export const getMockBuilderArgs = (): UserActionBuilderArgs => { + const userAction = getUserAction('title', Actions.update); + const commentRefs = { current: {} }; + const caseServices = { + '123': { + ...basicPush, + firstPushIndex: 0, + lastPushIndex: 0, + commentsToUpdate: [], + hasDataToPush: true, + }, + }; + + const alertData = { + 'alert-id-1': { + _id: 'alert-id-1', + _index: 'alert-index-1', + signal: { + rule: { + id: ['rule-id-1'], + name: ['Awesome rule'], + false_positives: [], + }, + }, + kibana: { + alert: { + rule: { + uuid: ['rule-id-1'], + name: ['Awesome rule'], + false_positives: [], + parameters: {}, + }, + }, + }, + owner: SECURITY_SOLUTION_OWNER, + }, + }; + + const getRuleDetailsHref = jest.fn().mockReturnValue('https://example.com'); + const onRuleDetailsClick = jest.fn(); + const onShowAlertDetails = jest.fn(); + const handleManageMarkdownEditId = jest.fn(); + const handleSaveComment = jest.fn(); + const handleManageQuote = jest.fn(); + const handleOutlineComment = jest.fn(); + + return { + userAction, + caseData: basicCase, + comments: basicCase.comments, + caseServices, + index: 0, + alertData, + userCanCrud: true, + commentRefs, + manageMarkdownEditIds: [], + selectedOutlineCommentId: '', + loadingCommentIds: [], + loadingAlertData: false, + getRuleDetailsHref, + onRuleDetailsClick, + onShowAlertDetails, + handleManageMarkdownEditId, + handleSaveComment, + handleManageQuote, + handleOutlineComment, + }; +}; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_move_to_reference.test.tsx b/x-pack/plugins/cases/public/components/user_actions/move_to_reference.test.tsx similarity index 92% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_move_to_reference.test.tsx rename to x-pack/plugins/cases/public/components/user_actions/move_to_reference.test.tsx index acd3814786a343..cd207c635e9d4b 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_move_to_reference.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/move_to_reference.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; -import { UserActionMoveToReference } from './user_action_move_to_reference'; +import { UserActionMoveToReference } from './move_to_reference'; const outlineComment = jest.fn(); const props = { diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_move_to_reference.tsx b/x-pack/plugins/cases/public/components/user_actions/move_to_reference.tsx similarity index 100% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_move_to_reference.tsx rename to x-pack/plugins/cases/public/components/user_actions/move_to_reference.tsx diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_property_actions.test.tsx b/x-pack/plugins/cases/public/components/user_actions/property_actions.test.tsx similarity index 96% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_property_actions.test.tsx rename to x-pack/plugins/cases/public/components/user_actions/property_actions.test.tsx index 999a3380f57973..167a7fb9779294 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_property_actions.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/property_actions.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; -import { UserActionPropertyActions } from './user_action_property_actions'; +import { UserActionPropertyActions } from './property_actions'; jest.mock('../../common/lib/kibana'); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_property_actions.tsx b/x-pack/plugins/cases/public/components/user_actions/property_actions.tsx similarity index 100% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_property_actions.tsx rename to x-pack/plugins/cases/public/components/user_actions/property_actions.tsx diff --git a/x-pack/plugins/cases/public/components/user_actions/pushed.test.tsx b/x-pack/plugins/cases/public/components/user_actions/pushed.test.tsx new file mode 100644 index 00000000000000..219a7a6d2c7c8e --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/pushed.test.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { EuiCommentList } from '@elastic/eui'; +import { render, screen } from '@testing-library/react'; + +import { Actions, NONE_CONNECTOR_ID } from '../../../common/api'; +import { getUserAction } from '../../containers/mock'; +import { TestProviders } from '../../common/mock'; +import { createPushedUserActionBuilder } from './pushed'; +import { getMockBuilderArgs } from './mock'; + +jest.mock('../../common/lib/kibana'); +jest.mock('../../common/navigation/hooks'); + +describe('createPushedUserActionBuilder ', () => { + const builderArgs = getMockBuilderArgs(); + const caseServices = builderArgs.caseServices; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly pushing for the first time', async () => { + const userAction = getUserAction('pushed', Actions.push_to_service); + const builder = createPushedUserActionBuilder({ + ...builderArgs, + userAction, + caseServices, + index: 0, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByText('pushed as new incident connector name')).toBeInTheDocument(); + expect(screen.getByText('external title').closest('a')).toHaveAttribute( + 'href', + 'basicPush.com' + ); + }); + + it('renders correctly when updating an external service', async () => { + const userAction = getUserAction('pushed', Actions.push_to_service); + const builder = createPushedUserActionBuilder({ + ...builderArgs, + userAction, + caseServices, + index: 1, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByText('updated incident connector name')).toBeInTheDocument(); + }); + + it('renders the pushing indicators correctly', async () => { + const userAction = getUserAction('pushed', Actions.push_to_service); + const builder = createPushedUserActionBuilder({ + ...builderArgs, + userAction, + caseServices: { + ...caseServices, + '123': { + ...caseServices['123'], + lastPushIndex: 1, + }, + }, + index: 1, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByText('Already pushed to connector name incident')).toBeInTheDocument(); + expect(screen.getByText('Requires update to connector name incident')).toBeInTheDocument(); + }); + + it('shows only the already pushed indicator if has no data to push', async () => { + const userAction = getUserAction('pushed', Actions.push_to_service); + const builder = createPushedUserActionBuilder({ + ...builderArgs, + userAction, + caseServices: { + ...caseServices, + '123': { + ...caseServices['123'], + lastPushIndex: 1, + hasDataToPush: false, + }, + }, + index: 1, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByText('Already pushed to connector name incident')).toBeInTheDocument(); + expect( + screen.queryByText('Requires update to connector name incident') + ).not.toBeInTheDocument(); + }); + + it('does not show the push information if the connector is none', async () => { + const userAction = getUserAction('pushed', Actions.push_to_service, { + payload: { + externalService: { connectorId: NONE_CONNECTOR_ID, connectorName: 'none connector' }, + }, + }); + + const builder = createPushedUserActionBuilder({ + ...builderArgs, + userAction, + caseServices: { + ...caseServices, + '123': { + ...caseServices['123'], + lastPushIndex: 1, + }, + }, + index: 1, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.queryByText('pushed as new incident none connector')).not.toBeInTheDocument(); + expect(screen.queryByText('updated incident none connector')).not.toBeInTheDocument(); + expect(screen.queryByText('Already pushed to connector name incident')).not.toBeInTheDocument(); + expect( + screen.queryByText('Requires update to connector name incident') + ).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/pushed.tsx b/x-pack/plugins/cases/public/components/user_actions/pushed.tsx new file mode 100644 index 00000000000000..e02bde992b6517 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/pushed.tsx @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { EuiCommentProps, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; + +import { Actions, NONE_CONNECTOR_ID, PushedUserAction } from '../../../common/api'; +import { UserActionBuilder, UserActionResponse } from './types'; +import { createCommonUpdateUserActionBuilder } from './common'; +import * as i18n from './translations'; +import { CaseServices } from '../../containers/use_get_case_user_actions'; +import { CaseExternalService } from '../../containers/types'; + +const getPushInfo = ( + caseServices: CaseServices, + externalService: CaseExternalService | undefined, + index: number +) => + externalService != null && externalService.connectorId !== NONE_CONNECTOR_ID + ? { + firstPush: caseServices[externalService.connectorId]?.firstPushIndex === index, + parsedConnectorId: externalService.connectorId, + parsedConnectorName: externalService.connectorName, + } + : { + firstPush: false, + parsedConnectorId: NONE_CONNECTOR_ID, + parsedConnectorName: NONE_CONNECTOR_ID, + }; + +const getLabelTitle = (action: UserActionResponse, firstPush: boolean) => { + const externalService = action.payload.externalService; + + return ( + + + {`${firstPush ? i18n.PUSHED_NEW_INCIDENT : i18n.UPDATE_INCIDENT} ${ + externalService?.connectorName + }`} + + + + {externalService?.externalTitle} + + + + ); +}; + +const getFooters = ({ + userAction, + caseServices, + connectorId, + connectorName, + index, +}: { + userAction: UserActionResponse; + caseServices: CaseServices; + connectorId: string; + connectorName: string; + index: number; +}): EuiCommentProps[] => { + const showTopFooter = + userAction.action === Actions.push_to_service && + index === caseServices[connectorId]?.lastPushIndex; + + const showBottomFooter = + userAction.action === Actions.push_to_service && + index === caseServices[connectorId]?.lastPushIndex && + caseServices[connectorId].hasDataToPush; + + let footers: EuiCommentProps[] = []; + + if (showTopFooter) { + footers = [ + ...footers, + { + username: '', + type: 'update', + event: i18n.ALREADY_PUSHED_TO_SERVICE(`${connectorName}`), + timelineIcon: 'sortUp', + 'data-test-subj': 'top-footer', + }, + ]; + } + + if (showBottomFooter) { + footers = [ + ...footers, + { + username: '', + type: 'update', + event: i18n.REQUIRED_UPDATE_TO_SERVICE(`${connectorName}`), + timelineIcon: 'sortDown', + 'data-test-subj': 'bottom-footer', + }, + ]; + } + + return footers; +}; + +export const createPushedUserActionBuilder: UserActionBuilder = ({ + userAction, + caseServices, + index, + handleOutlineComment, +}) => ({ + build: () => { + const pushedUserAction = userAction as UserActionResponse; + const { firstPush, parsedConnectorId, parsedConnectorName } = getPushInfo( + caseServices, + pushedUserAction.payload.externalService, + index + ); + + if (parsedConnectorId === NONE_CONNECTOR_ID) { + return []; + } + + const footers = getFooters({ + userAction: pushedUserAction, + caseServices, + connectorId: parsedConnectorId, + connectorName: parsedConnectorName, + index, + }); + + const label = getLabelTitle(pushedUserAction, firstPush); + const commonBuilder = createCommonUpdateUserActionBuilder({ + userAction, + handleOutlineComment, + label, + icon: 'dot', + }); + + return [...commonBuilder.build(), ...footers]; + }, +}); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/schema.ts b/x-pack/plugins/cases/public/components/user_actions/schema.ts similarity index 100% rename from x-pack/plugins/cases/public/components/user_action_tree/schema.ts rename to x-pack/plugins/cases/public/components/user_actions/schema.ts diff --git a/x-pack/plugins/cases/public/components/user_actions/status.test.tsx b/x-pack/plugins/cases/public/components/user_actions/status.test.tsx new file mode 100644 index 00000000000000..037a2e17564190 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/status.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiCommentList } from '@elastic/eui'; +import { render, screen } from '@testing-library/react'; + +import { Actions, CaseStatuses } from '../../../common/api'; +import { getUserAction } from '../../containers/mock'; +import { TestProviders } from '../../common/mock'; +import { createStatusUserActionBuilder } from './status'; +import { getMockBuilderArgs } from './mock'; + +jest.mock('../../common/lib/kibana'); +jest.mock('../../common/navigation/hooks'); + +describe('createStatusUserActionBuilder ', () => { + const builderArgs = getMockBuilderArgs(); + const tests = [ + [CaseStatuses.open, 'Open'], + [CaseStatuses['in-progress'], 'In progress'], + [CaseStatuses.closed, 'Closed'], + ]; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it.each(tests)('renders correctly when changed to %s status', async (status, label) => { + const userAction = getUserAction('status', Actions.update, { payload: { status } }); + const builder = createStatusUserActionBuilder({ + ...builderArgs, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByText('marked case as')).toBeInTheDocument(); + expect(screen.getByText(label)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/status.tsx b/x-pack/plugins/cases/public/components/user_actions/status.tsx new file mode 100644 index 00000000000000..6300bee347c4b3 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/status.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { CaseStatuses, StatusUserAction } from '../../../common/api'; +import { UserActionBuilder, UserActionResponse } from './types'; +import { createCommonUpdateUserActionBuilder } from './common'; +import { Status, statuses } from '../status'; +import * as i18n from './translations'; + +const isStatusValid = (status: string): status is CaseStatuses => + Object.prototype.hasOwnProperty.call(statuses, status); + +const getLabelTitle = (userAction: UserActionResponse) => { + const status = userAction.payload.status ?? ''; + if (isStatusValid(status)) { + return ( + + {i18n.MARKED_CASE_AS} + + + + + ); + } + + return <>; +}; + +export const createStatusUserActionBuilder: UserActionBuilder = ({ + userAction, + handleOutlineComment, +}) => ({ + build: () => { + const statusUserAction = userAction as UserActionResponse; + const label = getLabelTitle(statusUserAction); + const commonBuilder = createCommonUpdateUserActionBuilder({ + userAction, + handleOutlineComment, + label, + icon: 'folderClosed', + }); + + return commonBuilder.build(); + }, +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/tags.test.tsx b/x-pack/plugins/cases/public/components/user_actions/tags.test.tsx new file mode 100644 index 00000000000000..058d2232e666d2 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/tags.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiCommentList } from '@elastic/eui'; +import { render, screen } from '@testing-library/react'; + +import { Actions } from '../../../common/api'; +import { getUserAction } from '../../containers/mock'; +import { TestProviders } from '../../common/mock'; +import { createTagsUserActionBuilder } from './tags'; +import { getMockBuilderArgs } from './mock'; + +jest.mock('../../common/lib/kibana'); +jest.mock('../../common/navigation/hooks'); + +describe('createTagsUserActionBuilder ', () => { + const builderArgs = getMockBuilderArgs(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly when adding a tag', async () => { + const userAction = getUserAction('tags', Actions.add); + const builder = createTagsUserActionBuilder({ + ...builderArgs, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByText('added tags')).toBeInTheDocument(); + expect(screen.getByText('a tag')).toBeInTheDocument(); + }); + + it('renders correctly when deleting a tag', async () => { + const userAction = getUserAction('tags', Actions.delete); + const builder = createTagsUserActionBuilder({ + ...builderArgs, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByText('removed tags')).toBeInTheDocument(); + expect(screen.getByText('a tag')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/tags.tsx b/x-pack/plugins/cases/public/components/user_actions/tags.tsx new file mode 100644 index 00000000000000..d5553a3f6f13d4 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/tags.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { Actions, TagsUserAction } from '../../../common/api'; +import { UserActionBuilder, UserActionResponse } from './types'; +import { createCommonUpdateUserActionBuilder } from './common'; +import { Tags } from '../tag_list/tags'; +import * as i18n from './translations'; + +const getLabelTitle = (userAction: UserActionResponse) => { + const tags = userAction.payload.tags ?? []; + + return ( + + + {userAction.action === Actions.add && i18n.ADDED_FIELD} + {userAction.action === Actions.delete && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()} + + + + + + ); +}; + +export const createTagsUserActionBuilder: UserActionBuilder = ({ + userAction, + handleOutlineComment, +}) => ({ + build: () => { + const tagsUserAction = userAction as UserActionResponse; + const label = getLabelTitle(tagsUserAction); + const commonBuilder = createCommonUpdateUserActionBuilder({ + userAction, + handleOutlineComment, + label, + icon: 'tag', + }); + + return commonBuilder.build(); + }, +}); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_timestamp.test.tsx b/x-pack/plugins/cases/public/components/user_actions/timestamp.test.tsx similarity index 97% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_timestamp.test.tsx rename to x-pack/plugins/cases/public/components/user_actions/timestamp.test.tsx index f2e5d9793f3a81..d380f246566e9a 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_timestamp.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/timestamp.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; import { TestProviders } from '../../common/mock'; -import { UserActionTimestamp } from './user_action_timestamp'; +import { UserActionTimestamp } from './timestamp'; jest.mock('@kbn/i18n-react', () => { const originalModule = jest.requireActual('@kbn/i18n-react'); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_timestamp.tsx b/x-pack/plugins/cases/public/components/user_actions/timestamp.tsx similarity index 94% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_timestamp.tsx rename to x-pack/plugins/cases/public/components/user_actions/timestamp.tsx index 45ad44932831d6..98e25fe265dbb4 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_timestamp.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/timestamp.tsx @@ -9,7 +9,7 @@ import React, { memo } from 'react'; import { EuiTextColor } from '@elastic/eui'; import { FormattedRelative } from '@kbn/i18n-react'; -import { LocalizedDateTooltip } from '../../components/localized_date_tooltip'; +import { LocalizedDateTooltip } from '../localized_date_tooltip'; import * as i18n from './translations'; interface UserActionAvatarProps { diff --git a/x-pack/plugins/cases/public/components/user_actions/title.test.tsx b/x-pack/plugins/cases/public/components/user_actions/title.test.tsx new file mode 100644 index 00000000000000..c8d063e9a53432 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/title.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiCommentList } from '@elastic/eui'; +import { render, screen } from '@testing-library/react'; + +import { Actions } from '../../../common/api'; +import { getUserAction } from '../../containers/mock'; +import { TestProviders } from '../../common/mock'; +import { createTitleUserActionBuilder } from './title'; +import { getMockBuilderArgs } from './mock'; + +jest.mock('../../common/lib/kibana'); +jest.mock('../../common/navigation/hooks'); + +describe('createTitleUserActionBuilder ', () => { + const builderArgs = getMockBuilderArgs(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly', async () => { + const userAction = getUserAction('title', Actions.update); + // @ts-ignore no need to pass all the arguments + const builder = createTitleUserActionBuilder({ + ...builderArgs, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByText(`changed case name to "a title"`)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/title.tsx b/x-pack/plugins/cases/public/components/user_actions/title.tsx new file mode 100644 index 00000000000000..203b0e9882f641 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/title.tsx @@ -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 { TitleUserAction } from '../../../common/api'; +import { UserActionBuilder, UserActionResponse } from './types'; +import { createCommonUpdateUserActionBuilder } from './common'; +import * as i18n from './translations'; + +const getLabelTitle = (userAction: UserActionResponse) => + `${i18n.CHANGED_FIELD.toLowerCase()} ${i18n.CASE_NAME.toLowerCase()} ${i18n.TO} "${ + userAction.payload.title + }"`; + +export const createTitleUserActionBuilder: UserActionBuilder = ({ + userAction, + handleOutlineComment, +}) => ({ + build: () => { + const titleUserAction = userAction as UserActionResponse; + const label = getLabelTitle(titleUserAction); + const commonBuilder = createCommonUpdateUserActionBuilder({ + userAction, + handleOutlineComment, + label, + icon: 'dot', + }); + + return commonBuilder.build(); + }, +}); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/translations.ts b/x-pack/plugins/cases/public/components/user_actions/translations.ts similarity index 100% rename from x-pack/plugins/cases/public/components/user_action_tree/translations.ts rename to x-pack/plugins/cases/public/components/user_actions/translations.ts diff --git a/x-pack/plugins/cases/public/components/user_actions/types.ts b/x-pack/plugins/cases/public/components/user_actions/types.ts new file mode 100644 index 00000000000000..80657cc90cba93 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/types.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiCommentProps } from '@elastic/eui'; +import { SnakeToCamelCase } from '../../../common/types'; +import { ActionTypes, UserActionWithResponse } from '../../../common/api'; +import { Case, CaseUserActions, Ecs, Comment } from '../../containers/types'; +import { CaseServices } from '../../containers/use_get_case_user_actions'; +import { AddCommentRefObject } from '../add_comment'; +import { UserActionMarkdownRefObject } from './markdown_form'; +import { CasesNavigation } from '../links'; +import { UNSUPPORTED_ACTION_TYPES } from './constants'; +import type { OnUpdateFields } from '../case_view/types'; + +export interface UserActionTreeProps { + caseServices: CaseServices; + caseUserActions: CaseUserActions[]; + data: Case; + fetchUserActions: () => void; + getRuleDetailsHref?: RuleDetailsNavigation['href']; + actionsNavigation?: ActionsNavigation; + isLoadingDescription: boolean; + isLoadingUserActions: boolean; + onRuleDetailsClick?: RuleDetailsNavigation['onClick']; + onShowAlertDetails: (alertId: string, index: string) => void; + onUpdateField: ({ key, value, onSuccess, onError }: OnUpdateFields) => void; + renderInvestigateInTimelineActionComponent?: (alertIds: string[]) => JSX.Element; + statusActionButton: JSX.Element | null; + updateCase: (newCase: Case) => void; + useFetchAlertData: (alertIds: string[]) => [boolean, Record]; + userCanCrud: boolean; +} + +type UnsupportedUserActionTypes = typeof UNSUPPORTED_ACTION_TYPES[number]; +export type SupportedUserActionTypes = keyof Omit; + +export interface UserActionBuilderArgs { + caseData: Case; + userAction: CaseUserActions; + caseServices: CaseServices; + comments: Comment[]; + index: number; + userCanCrud: boolean; + commentRefs: React.MutableRefObject< + Record + >; + manageMarkdownEditIds: string[]; + selectedOutlineCommentId: string; + loadingCommentIds: string[]; + loadingAlertData: boolean; + alertData: Record; + handleOutlineComment: (id: string) => void; + handleManageMarkdownEditId: (id: string) => void; + handleSaveComment: ({ id, version }: { id: string; version: string }, content: string) => void; + handleManageQuote: (quote: string) => void; + onShowAlertDetails: (alertId: string, index: string) => void; + actionsNavigation?: ActionsNavigation; + getRuleDetailsHref?: RuleDetailsNavigation['href']; + onRuleDetailsClick?: RuleDetailsNavigation['onClick']; +} + +export type UserActionResponse = SnakeToCamelCase>; +export type UserActionBuilder = (args: UserActionBuilderArgs) => { + build: () => EuiCommentProps[]; +}; + +export type UserActionBuilderMap = Record; + +export type RuleDetailsNavigation = CasesNavigation; +export type ActionsNavigation = CasesNavigation; + +interface Signal { + rule: { + id: string; + name: string; + to: string; + from: string; + }; +} + +export interface Alert { + _id: string; + _index: string; + '@timestamp': string; + signal: Signal; + [key: string]: unknown; +} diff --git a/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.test.tsx b/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.test.tsx new file mode 100644 index 00000000000000..574109b4381674 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.test.tsx @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { basicCase } from '../../containers/mock'; + +import { useUpdateComment } from '../../containers/use_update_comment'; +import { useLensDraftComment } from '../markdown_editor/plugins/lens/use_lens_draft_comment'; +import { NEW_COMMENT_ID } from './constants'; +import { + useUserActionsHandler, + UseUserActionsHandlerArgs, + UseUserActionsHandler, +} from './use_user_actions_handler'; + +jest.mock('../../common/lib/kibana'); +jest.mock('../../common/navigation/hooks'); +jest.mock('../markdown_editor/plugins/lens/use_lens_draft_comment'); +jest.mock('../../containers/use_update_comment'); + +const useUpdateCommentMock = useUpdateComment as jest.Mock; +const useLensDraftCommentMock = useLensDraftComment as jest.Mock; +const patchComment = jest.fn(); +const clearDraftComment = jest.fn(); +const openLensModal = jest.fn(); + +describe('useUserActionsHandler', () => { + const fetchUserActions = jest.fn(); + const updateCase = jest.fn(); + + beforeAll(() => { + jest.useFakeTimers(); + jest.spyOn(global, 'setTimeout'); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + useUpdateCommentMock.mockReturnValue({ + isLoadingIds: [], + patchComment, + }); + + useLensDraftCommentMock.mockReturnValue({ + clearDraftComment, + openLensModal, + draftComment: null, + hasIncomingLensState: false, + }); + }); + + it('init', async () => { + const { result } = renderHook(() => + useUserActionsHandler({ fetchUserActions, updateCase }) + ); + + expect(result.current).toEqual({ + loadingCommentIds: [], + selectedOutlineCommentId: '', + manageMarkdownEditIds: [], + commentRefs: result.current.commentRefs, + handleManageMarkdownEditId: result.current.handleManageMarkdownEditId, + handleOutlineComment: result.current.handleOutlineComment, + handleSaveComment: result.current.handleSaveComment, + handleManageQuote: result.current.handleManageQuote, + handleUpdate: result.current.handleUpdate, + }); + }); + + it('should saves a comment', async () => { + const { result } = renderHook(() => + useUserActionsHandler({ fetchUserActions, updateCase }) + ); + + result.current.handleSaveComment({ id: 'test-id', version: 'test-version' }, 'a comment'); + expect(patchComment).toHaveBeenCalledWith({ + caseId: 'basic-case-id', + commentId: 'test-id', + commentUpdate: 'a comment', + fetchUserActions, + subCaseId: undefined, + updateCase, + version: 'test-version', + }); + }); + + it('should update a case', async () => { + const { result } = renderHook(() => + useUserActionsHandler({ fetchUserActions, updateCase }) + ); + + result.current.handleUpdate(basicCase); + expect(fetchUserActions).toHaveBeenCalled(); + expect(updateCase).toHaveBeenCalledWith(basicCase); + }); + + it('should handle markdown edit', async () => { + const { result } = renderHook(() => + useUserActionsHandler({ fetchUserActions, updateCase }) + ); + + act(() => { + result.current.handleManageMarkdownEditId('test-id'); + }); + + expect(clearDraftComment).toHaveBeenCalled(); + expect(result.current.manageMarkdownEditIds).toEqual(['test-id']); + }); + + it('should remove id from the markdown edit ids', async () => { + const { result } = renderHook(() => + useUserActionsHandler({ fetchUserActions, updateCase }) + ); + + act(() => { + result.current.handleManageMarkdownEditId('test-id'); + }); + + expect(result.current.manageMarkdownEditIds).toEqual(['test-id']); + + act(() => { + result.current.handleManageMarkdownEditId('test-id'); + }); + + expect(result.current.manageMarkdownEditIds).toEqual([]); + }); + + it('should outline a comment', async () => { + const { result } = renderHook(() => + useUserActionsHandler({ fetchUserActions, updateCase }) + ); + + act(() => { + result.current.handleOutlineComment('test-id'); + }); + + expect(result.current.selectedOutlineCommentId).toBe('test-id'); + + act(() => { + jest.runAllTimers(); + }); + + expect(result.current.selectedOutlineCommentId).toBe(''); + }); + + it('should quote', async () => { + const addQuote = jest.fn(); + const { result } = renderHook(() => + useUserActionsHandler({ fetchUserActions, updateCase }) + ); + + result.current.commentRefs.current[NEW_COMMENT_ID] = { + addQuote, + setComment: jest.fn(), + }; + + act(() => { + result.current.handleManageQuote('my quote'); + }); + + expect(addQuote).toHaveBeenCalledWith('my quote'); + expect(result.current.selectedOutlineCommentId).toBe('add-comment'); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.tsx b/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.tsx new file mode 100644 index 00000000000000..b9943a89603920 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.tsx @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCaseViewParams } from '../../common/navigation'; +import { Case } from '../../containers/types'; +import { useLensDraftComment } from '../markdown_editor/plugins/lens/use_lens_draft_comment'; +import { useUpdateComment } from '../../containers/use_update_comment'; +import { AddCommentRefObject } from '../add_comment'; +import { UserActionMarkdownRefObject } from './markdown_form'; +import { UserActionBuilderArgs, UserActionTreeProps } from './types'; +import { NEW_COMMENT_ID } from './constants'; + +export type UseUserActionsHandlerArgs = Pick< + UserActionTreeProps, + 'fetchUserActions' | 'updateCase' +>; + +export type UseUserActionsHandler = Pick< + UserActionBuilderArgs, + | 'loadingCommentIds' + | 'selectedOutlineCommentId' + | 'manageMarkdownEditIds' + | 'commentRefs' + | 'handleManageMarkdownEditId' + | 'handleOutlineComment' + | 'handleSaveComment' + | 'handleManageQuote' +> & { handleUpdate: (updatedCase: Case) => void }; + +const isAddCommentRef = ( + ref: AddCommentRefObject | UserActionMarkdownRefObject | null | undefined +): ref is AddCommentRefObject => { + const commentRef = ref as AddCommentRefObject; + return commentRef?.addQuote != null; +}; + +export const useUserActionsHandler = ({ + fetchUserActions, + updateCase, +}: UseUserActionsHandlerArgs): UseUserActionsHandler => { + const { detailName: caseId, subCaseId } = useCaseViewParams(); + const { clearDraftComment, draftComment, hasIncomingLensState, openLensModal } = + useLensDraftComment(); + const handlerTimeoutId = useRef(0); + const { isLoadingIds, patchComment } = useUpdateComment(); + const [selectedOutlineCommentId, setSelectedOutlineCommentId] = useState(''); + const [manageMarkdownEditIds, setManageMarkdownEditIds] = useState([]); + const commentRefs = useRef< + Record + >({}); + + const handleManageMarkdownEditId = useCallback( + (id: string) => { + clearDraftComment(); + setManageMarkdownEditIds((prevManageMarkdownEditIds) => + !prevManageMarkdownEditIds.includes(id) + ? prevManageMarkdownEditIds.concat(id) + : prevManageMarkdownEditIds.filter((myId) => id !== myId) + ); + }, + [clearDraftComment] + ); + + const handleSaveComment = useCallback( + ({ id, version }: { id: string; version: string }, content: string) => { + patchComment({ + caseId, + commentId: id, + commentUpdate: content, + fetchUserActions, + version, + updateCase, + subCaseId, + }); + }, + [caseId, fetchUserActions, patchComment, subCaseId, updateCase] + ); + + const handleOutlineComment = useCallback( + (id: string) => { + const moveToTarget = document.getElementById(`${id}-permLink`); + if (moveToTarget != null) { + const yOffset = -120; + const y = moveToTarget.getBoundingClientRect().top + window.pageYOffset + yOffset; + window.scrollTo({ + top: y, + behavior: 'smooth', + }); + + if (id === 'add-comment') { + moveToTarget.getElementsByTagName('textarea')[0].focus(); + } + } + + window.clearTimeout(handlerTimeoutId.current); + setSelectedOutlineCommentId(id); + + handlerTimeoutId.current = window.setTimeout(() => { + setSelectedOutlineCommentId(''); + window.clearTimeout(handlerTimeoutId.current); + }, 2400); + }, + [handlerTimeoutId] + ); + + const handleManageQuote = useCallback( + (quote: string) => { + const ref = commentRefs?.current[NEW_COMMENT_ID]; + if (isAddCommentRef(ref)) { + ref.addQuote(quote); + } + + handleOutlineComment('add-comment'); + }, + [handleOutlineComment] + ); + + const handleUpdate = useCallback( + (newCase: Case) => { + updateCase(newCase); + fetchUserActions(); + }, + [fetchUserActions, updateCase] + ); + + useEffect(() => { + if (draftComment?.commentId) { + setManageMarkdownEditIds((prevManageMarkdownEditIds) => { + if ( + NEW_COMMENT_ID !== draftComment?.commentId && + !prevManageMarkdownEditIds.includes(draftComment?.commentId) + ) { + return [draftComment?.commentId]; + } + return prevManageMarkdownEditIds; + }); + + const ref = commentRefs?.current?.[draftComment.commentId]; + + if (isAddCommentRef(ref) && ref.editor?.textarea) { + ref.setComment(draftComment.comment); + if (hasIncomingLensState) { + openLensModal({ editorRef: ref.editor }); + } else { + clearDraftComment(); + } + } + } + }, [clearDraftComment, draftComment, hasIncomingLensState, openLensModal]); + + return { + loadingCommentIds: isLoadingIds, + selectedOutlineCommentId, + manageMarkdownEditIds, + commentRefs, + handleManageMarkdownEditId, + handleOutlineComment, + handleSaveComment, + handleManageQuote, + handleUpdate, + }; +}; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_username.test.tsx b/x-pack/plugins/cases/public/components/user_actions/username.test.tsx similarity index 97% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_username.test.tsx rename to x-pack/plugins/cases/public/components/user_actions/username.test.tsx index f664da71fc1f66..f8bfd7a54005d7 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_username.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/username.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; -import { UserActionUsername } from './user_action_username'; +import { UserActionUsername } from './username'; const props = { username: 'elastic', diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_username.tsx b/x-pack/plugins/cases/public/components/user_actions/username.tsx similarity index 100% rename from x-pack/plugins/cases/public/components/user_action_tree/user_action_username.tsx rename to x-pack/plugins/cases/public/components/user_actions/username.tsx diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index 7fb4dab915c2d9..fb69ca6f227936 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -32,6 +32,7 @@ import { import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { UseGetCasesState, DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; import { SnakeToCamelCase } from '../../common/types'; +import { covertToSnakeCase } from './utils'; export { connectorsMock } from './configure/mock'; export const basicCaseId = 'basic-case-id'; @@ -258,6 +259,8 @@ export const basicCommentPatch: Comment = { updatedAt: basicUpdatedAt, updatedBy: { username: 'elastic', + email: 'elastic@elastic.co', + fullName: 'Elastic', }, }; @@ -428,55 +431,138 @@ export const allCasesSnake: CasesFindResponse = { ...casesStatusSnake, }; -const basicActionSnake = { - created_at: basicCreatedAt, - created_by: elasticUserSnake, - case_id: basicCaseId, - comment_id: null, - owner: SECURITY_SOLUTION_OWNER, +export const getUserAction = ( + type: UserActionTypes, + action: UserAction, + overrides?: Record +): CaseUserActions => { + const commonProperties = { + ...basicAction, + actionId: `${type}-${action}`, + action, + }; + + const externalService = { + connectorId: pushConnectorId, + connectorName: 'connector name', + externalId: 'external_id', + externalTitle: 'external title', + externalUrl: 'basicPush.com', + pushedAt: basicUpdatedAt, + pushedBy: elasticUser, + }; + + switch (type) { + case ActionTypes.comment: + return { + ...commonProperties, + type: ActionTypes.comment, + payload: { + comment: { comment: 'a comment', type: CommentType.user, owner: SECURITY_SOLUTION_OWNER }, + }, + commentId: basicCommentId, + ...overrides, + }; + case ActionTypes.connector: + return { + ...commonProperties, + type: ActionTypes.connector, + payload: { + connector: { ...getJiraConnector() }, + }, + ...overrides, + }; + case ActionTypes.create_case: + return { + ...commonProperties, + type: ActionTypes.create_case, + payload: { + description: 'a desc', + connector: { ...getJiraConnector() }, + status: CaseStatuses.open, + title: 'a title', + tags: ['a tag'], + settings: { syncAlerts: true }, + owner: SECURITY_SOLUTION_OWNER, + }, + ...overrides, + }; + case ActionTypes.delete_case: + return { + ...commonProperties, + type: ActionTypes.delete_case, + payload: {}, + ...overrides, + }; + case ActionTypes.description: + return { + ...commonProperties, + type: ActionTypes.description, + payload: { description: 'a desc' }, + ...overrides, + }; + case ActionTypes.pushed: + return { + ...commonProperties, + type: ActionTypes.pushed, + payload: { + externalService, + }, + ...overrides, + }; + case ActionTypes.settings: + return { + ...commonProperties, + type: ActionTypes.settings, + payload: { settings: { syncAlerts: true } }, + ...overrides, + }; + case ActionTypes.status: + return { + ...commonProperties, + type: ActionTypes.status, + payload: { status: CaseStatuses.open }, + ...overrides, + }; + case ActionTypes.tags: + return { + ...commonProperties, + type: ActionTypes.tags, + payload: { tags: ['a tag'] }, + ...overrides, + }; + case ActionTypes.title: + return { + ...commonProperties, + type: ActionTypes.title, + payload: { title: 'a title' }, + ...overrides, + }; + + default: + return { + ...commonProperties, + ...overrides, + } as CaseUserActions; + } }; export const getUserActionSnake = ( type: UserActionTypes, action: UserAction, - payload?: Record + overrides?: Record ): CaseUserActionResponse => { - const isPushToService = type === ActionTypes.pushed; - return { - ...basicActionSnake, - action_id: `${type}-${action}`, - type, - action, - comment_id: type === 'comment' ? basicCommentId : null, - payload: isPushToService ? { externalService: basicPushSnake } : payload ?? basicAction.payload, + ...covertToSnakeCase(getUserAction(type, action, overrides)), } as unknown as CaseUserActionResponse; }; export const caseUserActionsSnake: CaseUserActionsResponse = [ - getUserActionSnake('description', Actions.create, { description: 'a desc' }), - getUserActionSnake('comment', Actions.create, { - comment: { comment: 'a comment', type: CommentType.user, owner: SECURITY_SOLUTION_OWNER }, - }), - getUserActionSnake('description', Actions.update, { description: 'a desc updated' }), + getUserActionSnake('description', Actions.create), + getUserActionSnake('comment', Actions.create), + getUserActionSnake('description', Actions.update), ]; -export const getUserAction = ( - type: UserActionTypes, - action: UserAction, - overrides?: Record -): CaseUserActions => { - return { - ...basicAction, - actionId: `${type}-${action}`, - type, - action, - commentId: type === 'comment' ? basicCommentId : null, - payload: type === 'pushed' ? { externalService: basicPush } : basicAction.payload, - ...overrides, - } as CaseUserActions; -}; - export const getJiraConnector = (overrides?: Partial): CaseConnector => { return { id: '123', @@ -492,9 +578,8 @@ export const jiraFields = { fields: { issueType: '10006', priority: null, parent export const getAlertUserAction = (): SnakeToCamelCase< UserActionWithResponse > => ({ - ...basicAction, + ...getUserAction(ActionTypes.comment, Actions.create), actionId: 'alert-action-id', - action: Actions.create, commentId: 'alert-comment-id', type: ActionTypes.comment, payload: { @@ -514,10 +599,9 @@ export const getAlertUserAction = (): SnakeToCamelCase< export const getHostIsolationUserAction = (): SnakeToCamelCase< UserActionWithResponse > => ({ - ...basicAction, + ...getUserAction(ActionTypes.comment, Actions.create), actionId: 'isolate-action-id', type: ActionTypes.comment, - action: Actions.create, commentId: 'isolate-comment-id', payload: { comment: { @@ -530,13 +614,9 @@ export const getHostIsolationUserAction = (): SnakeToCamelCase< }); export const caseUserActions: CaseUserActions[] = [ - getUserAction('description', Actions.create, { payload: { description: 'a desc' } }), - getUserAction('comment', Actions.create, { - payload: { - comment: { comment: 'a comment', type: CommentType.user, owner: SECURITY_SOLUTION_OWNER }, - }, - }), - getUserAction('description', Actions.update, { payload: { description: 'a desc updated' } }), + getUserAction('description', Actions.create), + getUserAction('comment', Actions.create), + getUserAction('description', Actions.update), ]; // components tests diff --git a/x-pack/plugins/cases/public/containers/utils.ts b/x-pack/plugins/cases/public/containers/utils.ts index 069c883b993925..303600fdb33983 100644 --- a/x-pack/plugins/cases/public/containers/utils.ts +++ b/x-pack/plugins/cases/public/containers/utils.ts @@ -6,7 +6,7 @@ */ import { set } from '@elastic/safer-lodash-set'; -import { camelCase, isArray, isObject } from 'lodash'; +import { camelCase, isArray, isObject, transform, snakeCase } from 'lodash'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -40,6 +40,12 @@ import * as i18n from './translations'; export const getTypedPayload = (a: unknown): T => a as T; +export const covertToSnakeCase = (obj: Record) => + transform(obj, (acc: Record, value, key, target) => { + const camelKey = Array.isArray(target) ? key : snakeCase(key); + acc[camelKey] = isObject(value) ? covertToSnakeCase(value as Record) : value; + }); + export const convertArrayToCamelCase = (arrayOfSnakes: unknown[]): unknown[] => arrayOfSnakes.reduce((acc: unknown[], value) => { if (isArray(value)) { @@ -51,8 +57,8 @@ export const convertArrayToCamelCase = (arrayOfSnakes: unknown[]): unknown[] => } }, []); -export const convertToCamelCase = (snakeCase: T): U => - Object.entries(snakeCase).reduce((acc, [key, value]) => { +export const convertToCamelCase = (obj: T): U => + Object.entries(obj).reduce((acc, [key, value]) => { if (isArray(value)) { set(acc, camelCase(key), convertArrayToCamelCase(value)); } else if (isObject(value)) { @@ -64,7 +70,7 @@ export const convertToCamelCase = (snakeCase: T): U => }, {} as U); export const convertAllCasesToCamel = (snakeCases: CasesFindResponse): AllCases => ({ - cases: snakeCases.cases.map((snakeCase) => convertToCamelCase(snakeCase)), + cases: snakeCases.cases.map((theCase) => convertToCamelCase(theCase)), countOpenCases: snakeCases.count_open_cases, countInProgressCases: snakeCases.count_in_progress_cases, countClosedCases: snakeCases.count_closed_cases, diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx index 4d08994ceb0454..5dbcb1cfb9990a 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx @@ -117,8 +117,9 @@ export function SearchSessionsMgmtTable({ {...props} id={SEARCH_SESSIONS_TABLE_ID} data-test-subj={SEARCH_SESSIONS_TABLE_ID} - rowProps={() => ({ - 'data-test-subj': 'searchSessionsRow', + rowProps={(searchSession: UISession) => ({ + 'data-test-subj': `searchSessionsRow`, + 'data-test-search-session-id': `id-${searchSession.id}`, })} columns={getColumns(core, plugins, api, config, timezone, onActionComplete, kibanaVersion)} items={tableData} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx index 084dcd139f9bf4..21fcac7ea6c75b 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx @@ -5,22 +5,45 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { Query, IndexPattern, TimefilterContract } from 'src/plugins/data/public'; -import { EuiButton } from '@elastic/eui'; +import { TimefilterContract } from 'src/plugins/data/public'; +import { DataView } from 'src/plugins/data/common'; + +import { + EuiButton, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiPopover, + EuiRadioGroup, + EuiRadioGroupOption, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { setFullTimeRange } from './full_time_range_selector_service'; import { useDataVisualizerKibana } from '../../../kibana_context'; +import { DV_FROZEN_TIER_PREFERENCE, useStorage } from '../../hooks/use_storage'; + +export const ML_FROZEN_TIER_PREFERENCE = 'ml.frozenDataTierPreference'; interface Props { timefilter: TimefilterContract; - indexPattern: IndexPattern; + indexPattern: DataView; disabled: boolean; - query?: Query; + query?: QueryDslQueryContainer; callback?: (a: any) => void; } +const FROZEN_TIER_PREFERENCE = { + EXCLUDE: 'exclude-frozen', + INCLUDE: 'include-frozen', +} as const; + +type FrozenTierPreference = typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE]; + // Component for rendering a button which automatically sets the range of the time filter // to the time range of data in the index(es) mapped to the supplied Kibana data view or query. export const FullTimeRangeSelector: FC = ({ @@ -37,36 +60,144 @@ export const FullTimeRangeSelector: FC = ({ } = useDataVisualizerKibana(); // wrapper around setFullTimeRange to allow for the calling of the optional callBack prop - async function setRange(i: IndexPattern, q?: Query) { - try { - const fullTimeRange = await setFullTimeRange(timefilter, i, q); - if (typeof callback === 'function') { - callback(fullTimeRange); + const setRange = useCallback( + async (i: DataView, q?: QueryDslQueryContainer, excludeFrozenData?: boolean) => { + try { + const fullTimeRange = await setFullTimeRange(timefilter, i, q, excludeFrozenData); + if (typeof callback === 'function') { + callback(fullTimeRange); + } + } catch (e) { + toasts.addDanger( + i18n.translate( + 'xpack.dataVisualizer.index.fullTimeRangeSelector.errorSettingTimeRangeNotification', + { + defaultMessage: 'An error occurred setting the time range.', + } + ) + ); } - } catch (e) { - toasts.addDanger( - i18n.translate( - 'xpack.dataVisualizer.index.fullTimeRangeSelector.errorSettingTimeRangeNotification', + }, + [callback, timefilter, toasts] + ); + + const [isPopoverOpen, setPopover] = useState(false); + + const [frozenDataPreference, setFrozenDataPreference] = useStorage( + DV_FROZEN_TIER_PREFERENCE, + // By default we will exclude frozen data tier + FROZEN_TIER_PREFERENCE.EXCLUDE + ); + + const setPreference = useCallback( + (id: string) => { + setFrozenDataPreference(id as FrozenTierPreference); + setRange(indexPattern, query, id === FROZEN_TIER_PREFERENCE.EXCLUDE); + closePopover(); + }, + [indexPattern, query, setFrozenDataPreference, setRange] + ); + + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + const sortOptions: EuiRadioGroupOption[] = useMemo(() => { + return [ + { + id: FROZEN_TIER_PREFERENCE.EXCLUDE, + label: i18n.translate( + 'xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataExcludingFrozenMenuLabel', { - defaultMessage: 'An error occurred setting the time range.', + defaultMessage: 'Exclude frozen data tier', } - ) - ); - } - } + ), + }, + { + id: FROZEN_TIER_PREFERENCE.INCLUDE, + label: i18n.translate( + 'xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataIncludingFrozenMenuLabel', + { + defaultMessage: 'Include frozen data tier', + } + ), + }, + ]; + }, []); + + const popoverContent = useMemo( + () => ( + + + + ), + [sortOptions, frozenDataPreference, setPreference] + ); + + const buttonTooltip = useMemo( + () => + frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE ? ( + + ) : ( + + ), + [frozenDataPreference] + ); + return ( - setRange(indexPattern, query)} - data-test-subj="dataVisualizerButtonUseFullData" - > - - + + + setRange(indexPattern, query, true)} + data-test-subj="dataVisualizerButtonUseFullData" + > + + + + + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downRight" + > + {popoverContent} + + + ); }; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts index f2d14de9812caf..303d54c9d45ccf 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts @@ -7,12 +7,14 @@ import moment from 'moment'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { Query, TimefilterContract } from 'src/plugins/data/public'; +import { TimefilterContract } from 'src/plugins/data/public'; import dateMath from '@elastic/datemath'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; import { isPopulatedObject } from '../../../../../common/utils/object_utils'; import { getTimeFieldRange } from '../../services/time_field_range'; import { GetTimeFieldRangeResponse } from '../../../../../common/types/time_field_request'; +import { addExcludeFrozenToQuery } from '../../utils/query_utils'; export interface TimeRange { from: number; @@ -22,14 +24,15 @@ export interface TimeRange { export async function setFullTimeRange( timefilter: TimefilterContract, indexPattern: IndexPattern, - query?: Query + query?: QueryDslQueryContainer, + excludeFrozenData?: boolean ): Promise { const runtimeMappings = indexPattern.getComputedFields() .runtimeFields as estypes.MappingRuntimeFields; const resp = await getTimeFieldRange({ index: indexPattern.title, timeFieldName: indexPattern.timeFieldName, - query, + query: excludeFrozenData ? addExcludeFrozenToQuery(query) : query, ...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}), }); timefilter.setTime({ diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_storage.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_storage.ts new file mode 100644 index 00000000000000..d6b0bb3322c037 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_storage.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useState } from 'react'; +import { useDataVisualizerKibana } from '../../kibana_context'; + +export const DV_FROZEN_TIER_PREFERENCE = 'dataVisualizer.frozenDataTierPreference'; + +export type DV = Partial<{ + [DV_FROZEN_TIER_PREFERENCE]: 'exclude_frozen' | 'include_frozen'; +}> | null; + +export type DVKey = keyof Exclude; + +/** + * Hook for accessing and changing a value in the storage. + * @param key - Storage key + * @param initValue + */ +export function useStorage(key: DVKey, initValue?: T): [T, (value: T) => void] { + const { + services: { storage }, + } = useDataVisualizerKibana(); + + const [val, setVal] = useState(storage.get(key) ?? initValue); + + const setStorage = useCallback( + (value: T): void => { + try { + storage.set(key, value); + setVal(value); + } catch (e) { + throw new Error('Unable to update storage with provided value'); + } + }, + [key, storage] + ); + + return [val, setStorage]; +} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_field_range.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_field_range.ts index 58a4bd4520829e..bcf32a7f62bd7f 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_field_range.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_field_range.ts @@ -6,9 +6,9 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { lazyLoadModules } from '../../../lazy_load_bundle'; import { GetTimeFieldRangeResponse } from '../../../../common/types/time_field_request'; -import { Query } from '../../../../../../../src/plugins/data/common/query'; export async function getTimeFieldRange({ index, @@ -18,7 +18,7 @@ export async function getTimeFieldRange({ }: { index: string; timeFieldName?: string; - query?: Query; + query?: QueryDslQueryContainer; runtimeMappings?: estypes.MappingRuntimeFields; }) { const body = JSON.stringify({ index, timeFieldName, query, runtimeMappings }); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.test.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.test.ts new file mode 100644 index 00000000000000..947b87e9976d58 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { addExcludeFrozenToQuery } from './query_utils'; + +describe('Util: addExcludeFrozenToQuery()', () => { + test('Validation checks.', () => { + expect( + addExcludeFrozenToQuery({ + match_all: {}, + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }) + ).toMatchObject({ + bool: { + must: [{ match_all: {} }], + must_not: [{ term: { _tier: { value: 'data_frozen' } } }], + }, + }); + + expect( + addExcludeFrozenToQuery({ + bool: { + must: [], + must_not: { + term: { + category: { + value: 'clothing', + }, + }, + }, + }, + }) + ).toMatchObject({ + bool: { + must: [], + must_not: [ + { term: { category: { value: 'clothing' } } }, + { term: { _tier: { value: 'data_frozen' } } }, + ], + }, + }); + + expect( + addExcludeFrozenToQuery({ + bool: { + must: [], + must_not: [{ term: { category: { value: 'clothing' } } }], + }, + }) + ).toMatchObject({ + bool: { + must: [], + must_not: [ + { term: { category: { value: 'clothing' } } }, + { term: { _tier: { value: 'data_frozen' } } }, + ], + }, + }); + + expect(addExcludeFrozenToQuery(undefined)).toMatchObject({ + bool: { + must_not: [{ term: { _tier: { value: 'data_frozen' } } }], + }, + }); + }); +}); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.ts new file mode 100644 index 00000000000000..43c5d49d1986f0 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { cloneDeep } from 'lodash'; +import { isPopulatedObject } from '../../../../common/utils/object_utils'; + +export const addExcludeFrozenToQuery = (originalQuery: QueryDslQueryContainer | undefined) => { + const FROZEN_TIER_TERM = { + term: { + _tier: { + value: 'data_frozen', + }, + }, + }; + + if (!originalQuery) { + return { + bool: { + must_not: [FROZEN_TIER_TERM], + }, + }; + } + + const query = cloneDeep(originalQuery); + + delete query.match_all; + + if (isPopulatedObject(query.bool)) { + // Must_not can be both arrays or singular object + if (Array.isArray(query.bool.must_not)) { + query.bool.must_not.push(FROZEN_TIER_TERM); + } else { + // If there's already a must_not condition + if (isPopulatedObject(query.bool.must_not)) { + query.bool.must_not = [query.bool.must_not, FROZEN_TIER_TERM]; + } + if (query.bool.must_not === undefined) { + query.bool.must_not = [FROZEN_TIER_TERM]; + } + } + } else { + query.bool = { + must_not: [FROZEN_TIER_TERM], + }; + } + + return query; +}; diff --git a/x-pack/plugins/data_visualizer/public/application/kibana_context.ts b/x-pack/plugins/data_visualizer/public/application/kibana_context.ts index 58d0ac021ff224..83fcc104fbe4b8 100644 --- a/x-pack/plugins/data_visualizer/public/application/kibana_context.ts +++ b/x-pack/plugins/data_visualizer/public/application/kibana_context.ts @@ -8,7 +8,11 @@ import { CoreStart } from 'kibana/public'; import { KibanaReactContextValue, useKibana } from '../../../../../src/plugins/kibana_react/public'; import type { DataVisualizerStartDependencies } from '../plugin'; +import type { IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public'; -export type StartServices = CoreStart & DataVisualizerStartDependencies; +export type StartServices = CoreStart & + DataVisualizerStartDependencies & { + storage: IStorageWrapper; + }; export type DataVisualizerKibanaReactContextValue = KibanaReactContextValue; export const useDataVisualizerKibana = () => useKibana(); diff --git a/x-pack/plugins/file_upload/server/get_time_field_range.ts b/x-pack/plugins/file_upload/server/get_time_field_range.ts index 126269e22dd3a9..84fc6ac002008a 100644 --- a/x-pack/plugins/file_upload/server/get_time_field_range.ts +++ b/x-pack/plugins/file_upload/server/get_time_field_range.ts @@ -6,13 +6,14 @@ */ import { IScopedClusterClient } from 'kibana/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { isPopulatedObject } from '../common/utils'; export async function getTimeFieldRange( client: IScopedClusterClient, index: string[] | string, timeFieldName: string, - query: any, + query: QueryDslQueryContainer, runtimeMappings?: estypes.MappingRuntimeFields ): Promise<{ success: boolean; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts b/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts index 9eb20383d57bdf..740f0401dca6e3 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts @@ -56,6 +56,7 @@ export type DeletePackagePoliciesResponse = Array<{ name?: string; success: boolean; package?: PackagePolicyPackage; + policy_id?: string; }>; export interface UpgradePackagePolicyBaseResponse { diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 5858ac8900c111..5591c165bd7063 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -489,6 +489,7 @@ class PackagePolicyService { title: packagePolicy.package?.title || '', version: packagePolicy.package?.version || '', }, + policy_id: packagePolicy.policy_id, }); } catch (error) { result.push({ diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 7a0601f9673cc7..23804a8a6d6184 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -24,12 +24,8 @@ import { I18nProvider } from '@kbn/i18n-react'; import { SavedObjectSaveModal } from '../../../../../src/plugins/saved_objects/public'; import { checkForDuplicateTitle } from '../persistence'; import { createMemoryHistory } from 'history'; -import { - esFilters, - FilterManager, - IndexPattern, - Query, -} from '../../../../../src/plugins/data/public'; +import { FilterManager, IndexPattern, Query } from '../../../../../src/plugins/data/public'; +import { buildExistsFilter, FilterStateStore } from '@kbn/es-query'; import type { FieldSpec } from '../../../../../src/plugins/data/common'; import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; import { LensByValueInput } from '../embeddable/embeddable'; @@ -147,7 +143,7 @@ describe('Lens App', () => { const services = makeDefaultServicesForApp(); const indexPattern = { id: 'index1' } as unknown as IndexPattern; const pinnedField = { name: 'pinnedField' } as unknown as FieldSpec; - const pinnedFilter = esFilters.buildExistsFilter(pinnedField, indexPattern); + const pinnedFilter = buildExistsFilter(pinnedField, indexPattern); services.data.query.filterManager.getFilters = jest.fn().mockImplementation(() => { return []; }); @@ -668,10 +664,10 @@ describe('Lens App', () => { const indexPattern = { id: 'index1' } as unknown as IndexPattern; const field = { name: 'myfield' } as unknown as FieldSpec; const pinnedField = { name: 'pinnedField' } as unknown as FieldSpec; - const unpinned = esFilters.buildExistsFilter(field, indexPattern); - const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); + const unpinned = buildExistsFilter(field, indexPattern); + const pinned = buildExistsFilter(pinnedField, indexPattern); await act(async () => { - FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); + FilterManager.setFiltersStore([pinned], FilterStateStore.GLOBAL_STATE); }); const { services } = await save({ initialSavedObjectId: defaultSavedObjectId, @@ -882,14 +878,12 @@ describe('Lens App', () => { }), }); act(() => - services.data.query.filterManager.setFilters([ - esFilters.buildExistsFilter(field, indexPattern), - ]) + services.data.query.filterManager.setFilters([buildExistsFilter(field, indexPattern)]) ); instance.update(); expect(lensStore.getState()).toEqual({ lens: expect.objectContaining({ - filters: [esFilters.buildExistsFilter(field, indexPattern)], + filters: [buildExistsFilter(field, indexPattern)], }), }); }); @@ -932,9 +926,7 @@ describe('Lens App', () => { const indexPattern = { id: 'index1' } as unknown as IndexPattern; const field = { name: 'myfield' } as unknown as FieldSpec; act(() => - services.data.query.filterManager.setFilters([ - esFilters.buildExistsFilter(field, indexPattern), - ]) + services.data.query.filterManager.setFilters([buildExistsFilter(field, indexPattern)]) ); instance.update(); expect(lensStore.getState()).toEqual({ @@ -1067,9 +1059,9 @@ describe('Lens App', () => { const indexPattern = { id: 'index1' } as unknown as IndexPattern; const field = { name: 'myfield' } as unknown as FieldSpec; const pinnedField = { name: 'pinnedField' } as unknown as FieldSpec; - const unpinned = esFilters.buildExistsFilter(field, indexPattern); - const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); - FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); + const unpinned = buildExistsFilter(field, indexPattern); + const pinned = buildExistsFilter(pinnedField, indexPattern); + FilterManager.setFiltersStore([pinned], FilterStateStore.GLOBAL_STATE); act(() => services.data.query.filterManager.setFilters([pinned, unpinned])); instance.update(); act(() => instance.find(services.navigation.ui.TopNavMenu).prop('onClearSavedQuery')!()); @@ -1124,9 +1116,9 @@ describe('Lens App', () => { const indexPattern = { id: 'index1' } as unknown as IndexPattern; const field = { name: 'myfield' } as unknown as FieldSpec; const pinnedField = { name: 'pinnedField' } as unknown as FieldSpec; - const unpinned = esFilters.buildExistsFilter(field, indexPattern); - const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); - FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); + const unpinned = buildExistsFilter(field, indexPattern); + const pinned = buildExistsFilter(pinnedField, indexPattern); + FilterManager.setFiltersStore([pinned], FilterStateStore.GLOBAL_STATE); act(() => services.data.query.filterManager.setFilters([pinned, unpinned])); instance.update(); act(() => instance.find(services.navigation.ui.TopNavMenu).prop('onClearSavedQuery')!()); diff --git a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx index ae144cdeb10681..e3098904a4b851 100644 --- a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx +++ b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx @@ -8,6 +8,7 @@ import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; +import { isFilterPinned } from '@kbn/es-query'; import type { SavedObjectReference } from 'kibana/public'; import { SaveModal } from './save_modal'; @@ -15,7 +16,7 @@ import type { LensAppProps, LensAppServices } from './types'; import type { SaveProps } from './app'; import { Document, checkForDuplicateTitle } from '../persistence'; import type { LensByReferenceInput, LensEmbeddableInput } from '../embeddable'; -import { esFilters, FilterManager } from '../../../../../src/plugins/data/public'; +import { FilterManager } from '../../../../../src/plugins/data/public'; import { APP_ID, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../common'; import { trackUiEvent } from '../lens_ui_telemetry'; import type { LensAppState } from '../state_management'; @@ -377,7 +378,7 @@ export function removePinnedFilters(doc?: Document) { ...doc, state: { ...doc.state, - filters: (doc.state?.filters || []).filter((filter) => !esFilters.isFilterPinned(filter)), + filters: (doc.state?.filters || []).filter((filter) => !isFilterPinned(filter)), }, }; } diff --git a/x-pack/plugins/lens/public/async_services.ts b/x-pack/plugins/lens/public/async_services.ts index bbb4faf55e1e9d..09b434c6484186 100644 --- a/x-pack/plugins/lens/public/async_services.ts +++ b/x-pack/plugins/lens/public/async_services.ts @@ -28,6 +28,8 @@ export * from './visualizations/gauge/gauge_visualization'; export * from './visualizations/gauge'; export * from './indexpattern_datasource/indexpattern'; +export { createFormulaPublicApi } from './indexpattern_datasource/operations/definitions/formula/formula_public_api'; + export * from './indexpattern_datasource'; export * from './editor_frame_service/editor_frame'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index fef71e92c5f2c2..5d475be7bb83fe 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -27,8 +27,9 @@ import { WorkspacePanel } from './workspace_panel'; import { ReactWrapper } from 'enzyme'; import { DragDrop, ChildDragDropProvider } from '../../../drag_drop'; import { fromExpression } from '@kbn/interpreter'; +import { buildExistsFilter } from '@kbn/es-query'; import { coreMock } from 'src/core/public/mocks'; -import { esFilters, IndexPattern } from '../../../../../../../src/plugins/data/public'; +import { IndexPattern } from '../../../../../../../src/plugins/data/public'; import type { FieldSpec } from '../../../../../../../src/plugins/data/common'; import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/public'; import { uiActionsPluginMock } from '../../../../../../../src/plugins/ui_actions/public/mocks'; @@ -415,7 +416,7 @@ describe('workspace_panel', () => { instance.setProps({ framePublicAPI: { ...framePublicAPI, - filters: [esFilters.buildExistsFilter(field, indexPattern)], + filters: [buildExistsFilter(field, indexPattern)], }, }); }); diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index 29dce6f0d10900..3e622d8ac93124 100644 --- a/x-pack/plugins/lens/public/index.ts +++ b/x-pack/plugins/lens/public/index.ts @@ -55,6 +55,7 @@ export type { FormulaIndexPatternColumn, MathIndexPatternColumn, OverallSumIndexPatternColumn, + FormulaPublicApi, } from './indexpattern_datasource/types'; export type { LensEmbeddableInput } from './embeddable'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx index dd3185b3c79906..efe79668705313 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiComboBox, EuiSpacer, EuiRange } from '@elastic/eui'; import { GenericIndexPatternColumn } from '../indexpattern'; +import { isColumnFormatted } from '../operations/definitions/helpers'; const supportedFormats: Record = { number: { @@ -55,11 +56,9 @@ const RANGE_MAX = 15; export function FormatSelector(props: FormatSelectorProps) { const { selectedColumn, onChange } = props; - - const currentFormat = - 'params' in selectedColumn && selectedColumn.params && 'format' in selectedColumn.params - ? selectedColumn.params.format - : undefined; + const currentFormat = isColumnFormatted(selectedColumn) + ? selectedColumn.params?.format + : undefined; const [decimals, setDecimals] = useState(currentFormat?.params?.decimals ?? 2); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index 386cd7a58ae019..4301540e5bf7e0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -21,6 +21,8 @@ import type { FieldFormatsSetup, } from '../../../../../src/plugins/field_formats/public'; +export type { PersistedIndexPatternLayer, IndexPattern, FormulaPublicApi } from './types'; + export interface IndexPatternDatasourceSetupPlugins { expressions: ExpressionsSetup; fieldFormats: FieldFormatsSetup; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index e1a15b87e5f5cb..c61569539bec83 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -6,9 +6,11 @@ */ import { uniq, mapValues, difference } from 'lodash'; -import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { HttpSetup, SavedObjectReference } from 'kibana/public'; -import { InitializationOptions, StateSetter } from '../types'; +import type { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import type { DataView } from 'src/plugins/data_views/public'; +import type { HttpSetup, SavedObjectReference } from 'kibana/public'; +import type { InitializationOptions, StateSetter } from '../types'; + import { IndexPattern, IndexPatternRef, @@ -17,6 +19,7 @@ import { IndexPatternField, IndexPatternLayer, } from './types'; + import { updateLayerIndexPattern, translateToOperationName } from './operations'; import { DateRange, ExistingFields } from '../../common/types'; import { BASE_API_URL } from '../../common'; @@ -35,6 +38,72 @@ type SetState = StateSetter; type IndexPatternsService = Pick; type ErrorHandler = (err: Error) => void; +export function convertDataViewIntoLensIndexPattern(dataView: DataView): IndexPattern { + const newFields = dataView.fields + .filter( + (field) => + !indexPatternsUtils.isNestedField(field) && (!!field.aggregatable || !!field.scripted) + ) + .map((field): IndexPatternField => { + // Convert the getters on the index pattern service into plain JSON + const base = { + name: field.name, + displayName: field.displayName, + type: field.type, + aggregatable: field.aggregatable, + searchable: field.searchable, + meta: dataView.metaFields.includes(field.name), + esTypes: field.esTypes, + scripted: field.scripted, + runtime: Boolean(field.runtimeField), + }; + + // Simplifies tests by hiding optional properties instead of undefined + return base.scripted + ? { + ...base, + lang: field.lang, + script: field.script, + } + : base; + }) + .concat(documentField); + + const { typeMeta, title, timeFieldName, fieldFormatMap } = dataView; + if (typeMeta?.aggs) { + const aggs = Object.keys(typeMeta.aggs); + newFields.forEach((field, index) => { + const restrictionsObj: IndexPatternField['aggregationRestrictions'] = {}; + aggs.forEach((agg) => { + const restriction = typeMeta.aggs && typeMeta.aggs[agg] && typeMeta.aggs[agg][field.name]; + if (restriction) { + restrictionsObj[translateToOperationName(agg)] = restriction; + } + }); + if (Object.keys(restrictionsObj).length) { + newFields[index] = { ...field, aggregationRestrictions: restrictionsObj }; + } + }); + } + + return { + id: dataView.id!, // id exists for sure because we got index patterns by id + title, + timeFieldName, + fieldFormatMap: + fieldFormatMap && + Object.fromEntries( + Object.entries(fieldFormatMap).map(([id, format]) => [ + id, + 'toJSON' in format ? format.toJSON() : format, + ]) + ), + fields: newFields, + getFieldByName: getFieldByNameFactory(newFields), + hasRestrictions: !!typeMeta?.aggs, + }; +} + export async function loadIndexPatterns({ indexPatternsService, patterns, @@ -79,77 +148,10 @@ export async function loadIndexPatterns({ } const indexPatternsObject = indexPatterns.reduce( - (acc, indexPattern) => { - const newFields = indexPattern.fields - .filter( - (field) => - !indexPatternsUtils.isNestedField(field) && (!!field.aggregatable || !!field.scripted) - ) - .map((field): IndexPatternField => { - // Convert the getters on the index pattern service into plain JSON - const base = { - name: field.name, - displayName: field.displayName, - type: field.type, - aggregatable: field.aggregatable, - searchable: field.searchable, - meta: indexPattern.metaFields.includes(field.name), - esTypes: field.esTypes, - scripted: field.scripted, - runtime: Boolean(field.runtimeField), - }; - - // Simplifies tests by hiding optional properties instead of undefined - return base.scripted - ? { - ...base, - lang: field.lang, - script: field.script, - } - : base; - }) - .concat(documentField); - - const { typeMeta, title, timeFieldName, fieldFormatMap } = indexPattern; - if (typeMeta?.aggs) { - const aggs = Object.keys(typeMeta.aggs); - newFields.forEach((field, index) => { - const restrictionsObj: IndexPatternField['aggregationRestrictions'] = {}; - aggs.forEach((agg) => { - const restriction = - typeMeta.aggs && typeMeta.aggs[agg] && typeMeta.aggs[agg][field.name]; - if (restriction) { - restrictionsObj[translateToOperationName(agg)] = restriction; - } - }); - if (Object.keys(restrictionsObj).length) { - newFields[index] = { ...field, aggregationRestrictions: restrictionsObj }; - } - }); - } - - const currentIndexPattern: IndexPattern = { - id: indexPattern.id!, // id exists for sure because we got index patterns by id - title, - timeFieldName, - fieldFormatMap: - fieldFormatMap && - Object.fromEntries( - Object.entries(fieldFormatMap).map(([id, format]) => [ - id, - 'toJSON' in format ? format.toJSON() : format, - ]) - ), - fields: newFields, - getFieldByName: getFieldByNameFactory(newFields), - hasRestrictions: !!typeMeta?.aggs, - }; - - return { - [currentIndexPattern.id]: currentIndexPattern, - ...acc, - }; - }, + (acc, indexPattern) => ({ + [indexPattern.id!]: convertDataViewIntoLensIndexPattern(indexPattern), + ...acc, + }), { ...cache } ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts index bd5b816cd8917a..2b11d182eeed06 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts @@ -20,8 +20,7 @@ export interface BaseIndexPatternColumn extends Operation { } // Formatting can optionally be added to any column -// export interface FormattedIndexPatternColumn extends BaseIndexPatternColumn { -export type FormattedIndexPatternColumn = BaseIndexPatternColumn & { +export interface FormattedIndexPatternColumn extends BaseIndexPatternColumn { params?: { format?: { id: string; @@ -30,15 +29,13 @@ export type FormattedIndexPatternColumn = BaseIndexPatternColumn & { }; }; }; -}; +} export interface FieldBasedIndexPatternColumn extends BaseIndexPatternColumn { sourceField: string; } -export interface ReferenceBasedIndexPatternColumn - extends BaseIndexPatternColumn, - FormattedIndexPatternColumn { +export interface ReferenceBasedIndexPatternColumn extends FormattedIndexPatternColumn { references: string[]; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx index fc69ea1d869f18..62a681ac3d6040 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx @@ -45,7 +45,7 @@ import { trackUiEvent } from '../../../../../lens_ui_telemetry'; import './formula.scss'; import { FormulaIndexPatternColumn } from '../formula'; -import { regenerateLayerFromAst } from '../parse'; +import { insertOrReplaceFormulaColumn } from '../parse'; import { filterByVisibleOperation } from '../util'; import { getColumnTimeShiftWarnings, getDateHistogramInterval } from '../../../../time_shift_utils'; @@ -151,16 +151,24 @@ export function FormulaEditor({ setIsCloseable(true); // If the text is not synced, update the column. if (text !== currentColumn.params.formula) { - updateLayer((prevLayer) => { - return regenerateLayerFromAst( - text || '', - prevLayer, - columnId, - currentColumn, - indexPattern, - operationDefinitionMap - ).newLayer; - }); + updateLayer( + (prevLayer) => + insertOrReplaceFormulaColumn( + columnId, + { + ...currentColumn, + params: { + ...currentColumn.params, + formula: text || '', + }, + }, + prevLayer, + { + indexPattern, + operations: operationDefinitionMap, + } + ).layer + ); } }); @@ -173,15 +181,23 @@ export function FormulaEditor({ monaco.editor.setModelMarkers(editorModel.current, 'LENS', []); if (currentColumn.params.formula) { // Only submit if valid - const { newLayer } = regenerateLayerFromAst( - text || '', - layer, - columnId, - currentColumn, - indexPattern, - operationDefinitionMap + updateLayer( + insertOrReplaceFormulaColumn( + columnId, + { + ...currentColumn, + params: { + ...currentColumn.params, + formula: text || '', + }, + }, + layer, + { + indexPattern, + operations: operationDefinitionMap, + } + ).layer ); - updateLayer(newLayer); } return; @@ -215,14 +231,21 @@ export function FormulaEditor({ // If the formula is already broken, show the latest error message in the workspace if (currentColumn.params.formula !== text) { updateLayer( - regenerateLayerFromAst( - text || '', - layer, + insertOrReplaceFormulaColumn( columnId, - currentColumn, - indexPattern, - visibleOperationsMap - ).newLayer + { + ...currentColumn, + params: { + ...currentColumn.params, + formula: text || '', + }, + }, + layer, + { + indexPattern, + operations: operationDefinitionMap, + } + ).layer ); } } @@ -270,14 +293,25 @@ export function FormulaEditor({ monaco.editor.setModelMarkers(editorModel.current, 'LENS', []); // Only submit if valid - const { newLayer, locations } = regenerateLayerFromAst( - text || '', - layer, + const { + layer: newLayer, + meta: { locations }, + } = insertOrReplaceFormulaColumn( columnId, - currentColumn, - indexPattern, - visibleOperationsMap + { + ...currentColumn, + params: { + ...currentColumn.params, + formula: text || '', + }, + }, + layer, + { + indexPattern, + operations: operationDefinitionMap, + } ); + updateLayer(newLayer); const managedColumns = getManagedColumnsFrom(columnId, newLayer.columns); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx index 2babd87768e32d..d1561e93aa8079 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx @@ -8,7 +8,7 @@ import { createMockedIndexPattern } from '../../../mocks'; import { formulaOperation, GenericOperationDefinition, GenericIndexPatternColumn } from '../index'; import { FormulaIndexPatternColumn } from './formula'; -import { regenerateLayerFromAst } from './parse'; +import { insertOrReplaceFormulaColumn } from './parse'; import type { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../../types'; import { tinymathFunctions } from './util'; import { TermsIndexPatternColumn } from '../terms'; @@ -424,25 +424,36 @@ describe('formula', () => { }); }); - describe('regenerateLayerFromAst()', () => { + describe('insertOrReplaceFormulaColumn()', () => { let indexPattern: IndexPattern; let currentColumn: FormulaIndexPatternColumn; function testIsBrokenFormula( formula: string, - columnParams: Partial> = {} + partialColumn: Partial> = {} ) { - const mergedColumn = { ...currentColumn, ...columnParams }; + const mergedColumn = { + ...currentColumn, + ...partialColumn, + }; const mergedLayer = { ...layer, columns: { ...layer.columns, col1: mergedColumn } }; + expect( - regenerateLayerFromAst( - formula, - mergedLayer, + insertOrReplaceFormulaColumn( 'col1', - mergedColumn, - indexPattern, - operationDefinitionMap - ).newLayer + { + ...mergedColumn, + params: { + ...mergedColumn.params, + formula, + }, + }, + mergedLayer, + { + indexPattern, + operations: operationDefinitionMap, + } + ).layer ).toEqual({ ...mergedLayer, columns: { @@ -475,14 +486,21 @@ describe('formula', () => { it('should mutate the layer with new columns for valid formula expressions', () => { expect( - regenerateLayerFromAst( - 'average(bytes)', - layer, + insertOrReplaceFormulaColumn( 'col1', - currentColumn, - indexPattern, - operationDefinitionMap - ).newLayer + { + ...currentColumn, + params: { + ...currentColumn.params, + formula: 'average(bytes)', + }, + }, + layer, + { + indexPattern, + operations: operationDefinitionMap, + } + ).layer ).toEqual({ ...layer, columnOrder: ['col1X0', 'col1'], @@ -514,14 +532,21 @@ describe('formula', () => { it('should create a valid formula expression for numeric literals', () => { expect( - regenerateLayerFromAst( - '0', - layer, + insertOrReplaceFormulaColumn( 'col1', - currentColumn, - indexPattern, - operationDefinitionMap - ).newLayer + { + ...currentColumn, + params: { + ...currentColumn.params, + formula: '0', + }, + }, + layer, + { + indexPattern, + operations: operationDefinitionMap, + } + ).layer ).toEqual({ ...layer, columnOrder: ['col1X0', 'col1'], @@ -672,14 +697,21 @@ describe('formula', () => { it('returns the locations of each function', () => { expect( - regenerateLayerFromAst( - 'moving_average(average(bytes), window=7) + count()', - layer, + insertOrReplaceFormulaColumn( 'col1', - currentColumn, - indexPattern, - operationDefinitionMap - ).locations + { + ...currentColumn, + params: { + ...currentColumn.params, + formula: 'moving_average(average(bytes), window=7) + count()', + }, + }, + layer, + { + indexPattern, + operations: operationDefinitionMap, + } + ).meta.locations ).toEqual({ col1X0: { min: 15, max: 29 }, col1X1: { min: 0, max: 41 }, @@ -693,14 +725,22 @@ describe('formula', () => { const mergedLayer = { ...layer, columns: { ...layer.columns, col1: mergedColumn } }; const formula = 'moving_average(average(bytes), window=7) + count()'; - const { newLayer } = regenerateLayerFromAst( - formula, - mergedLayer, + const { layer: newLayer } = insertOrReplaceFormulaColumn( 'col1', - mergedColumn, - indexPattern, - operationDefinitionMap + { + ...mergedColumn, + params: { + ...mergedColumn.params, + formula, + }, + }, + mergedLayer, + { + indexPattern, + operations: operationDefinitionMap, + } ); + // average and math are not filterable in the mocks expect(newLayer.columns).toEqual( expect.objectContaining({ @@ -737,14 +777,22 @@ describe('formula', () => { const mergedLayer = { ...layer, columns: { ...layer.columns, col1: mergedColumn } }; const formula = `moving_average(average(bytes), window=7, kql='${innerFilter}') + count(kql='${innerFilter}')`; - const { newLayer } = regenerateLayerFromAst( - formula, - mergedLayer, + const { layer: newLayer } = insertOrReplaceFormulaColumn( 'col1', - mergedColumn, - indexPattern, - operationDefinitionMap + { + ...mergedColumn, + params: { + ...mergedColumn.params, + formula, + }, + }, + mergedLayer, + { + indexPattern, + operations: operationDefinitionMap, + } ); + // average and math are not filterable in the mocks expect(newLayer.columns).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx index 15c49a7336c7e2..ce0d03a232e28b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx @@ -6,12 +6,12 @@ */ import { i18n } from '@kbn/i18n'; -import type { OperationDefinition } from '../index'; +import type { BaseIndexPatternColumn, OperationDefinition } from '../index'; import type { ReferenceBasedIndexPatternColumn } from '../column_types'; import type { IndexPattern } from '../../../types'; import { runASTValidation, tryToParse } from './validation'; import { WrappedFormulaEditor } from './editor'; -import { regenerateLayerFromAst } from './parse'; +import { insertOrReplaceFormulaColumn } from './parse'; import { generateFormula } from './generate'; import { filterByVisibleOperation } from './util'; import { getManagedColumnsFrom } from '../../layer_helpers'; @@ -36,6 +36,12 @@ export interface FormulaIndexPatternColumn extends ReferenceBasedIndexPatternCol }; } +export function isFormulaIndexPatternColumn( + column: BaseIndexPatternColumn +): column is FormulaIndexPatternColumn { + return 'params' in column && 'formula' in (column as FormulaIndexPatternColumn).params; +} + export const formulaOperation: OperationDefinition = { type: 'formula', @@ -150,22 +156,11 @@ export const formulaOperation: OperationDefinition ({ + insertOrReplaceFormulaColumn: jest.fn().mockReturnValue({}), +})); + +jest.mock('../../../loader', () => ({ + convertDataViewIntoLensIndexPattern: jest.fn((v) => v), +})); + +const getBaseLayer = (): PersistedIndexPatternLayer => ({ + columnOrder: ['col1'], + columns: { + col1: { + dataType: 'date', + isBucketed: true, + label: '@timestamp', + operationType: 'date_histogram', + params: { interval: 'auto' }, + scale: 'interval', + } as DateHistogramIndexPatternColumn, + }, +}); + +describe('createFormulaPublicApi', () => { + let publicApiHelper: FormulaPublicApi; + let dataView: DataView; + + beforeEach(() => { + publicApiHelper = createFormulaPublicApi(); + dataView = {} as DataView; + + jest.clearAllMocks(); + }); + + test('should use cache for caching lens index patterns', () => { + const baseLayer = getBaseLayer(); + + publicApiHelper.insertOrReplaceFormulaColumn( + 'col', + { formula: 'count()' }, + baseLayer, + dataView + ); + + publicApiHelper.insertOrReplaceFormulaColumn( + 'col', + { formula: 'count()' }, + baseLayer, + dataView + ); + + expect(convertDataViewIntoLensIndexPattern).toHaveBeenCalledTimes(1); + }); + + test('should execute insertOrReplaceFormulaColumn with valid arguments', () => { + const baseLayer = getBaseLayer(); + + publicApiHelper.insertOrReplaceFormulaColumn( + 'col', + { formula: 'count()' }, + baseLayer, + dataView + ); + + expect(insertOrReplaceFormulaColumn).toHaveBeenCalledWith( + 'col', + { + customLabel: false, + dataType: 'number', + isBucketed: false, + label: 'count()', + operationType: 'formula', + params: { formula: 'count()' }, + references: [], + }, + { + columnOrder: ['col1'], + columns: { + col1: { + dataType: 'date', + isBucketed: true, + label: '@timestamp', + operationType: 'date_histogram', + params: { interval: 'auto' }, + scale: 'interval', + }, + }, + indexPatternId: undefined, + }, + { indexPattern: {} } + ); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.ts new file mode 100644 index 00000000000000..63255ad2bf9dc7 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IndexPattern, PersistedIndexPatternLayer } from '../../../types'; +import type { DataView } from '../../../../../../../../src/plugins/data_views/public'; + +import { insertOrReplaceFormulaColumn } from './parse'; +import { convertDataViewIntoLensIndexPattern } from '../../../loader'; + +/** @public **/ +export interface FormulaPublicApi { + /** + * Method which Lens consumer can import and given a formula string, + * return a parsed result as list of columns to use as Embeddable attributes. + * + * @param id - Formula column id + * @param column.formula - String representation of a formula + * @param [column.label] - Custom formula label + * @param layer - The layer to which the formula columns will be added + * @param dataView - The dataView instance + * + * See `x-pack/examples/embedded_lens_example` for exemplary usage. + */ + insertOrReplaceFormulaColumn: ( + id: string, + column: { + formula: string; + label?: string; + }, + layer: PersistedIndexPatternLayer, + dataView: DataView + ) => PersistedIndexPatternLayer | undefined; +} + +/** @public **/ +export const createFormulaPublicApi = (): FormulaPublicApi => { + const cache: WeakMap = new WeakMap(); + + const getCachedLensIndexPattern = (dataView: DataView): IndexPattern => { + const cachedIndexPattern = cache.get(dataView); + if (cachedIndexPattern) { + return cachedIndexPattern; + } + const indexPattern = convertDataViewIntoLensIndexPattern(dataView); + cache.set(dataView, indexPattern); + return indexPattern; + }; + + return { + insertOrReplaceFormulaColumn: (id, { formula, label }, layer, dataView) => { + const indexPattern = getCachedLensIndexPattern(dataView); + + return insertOrReplaceFormulaColumn( + id, + { + label: label ?? formula, + customLabel: Boolean(label), + operationType: 'formula', + dataType: 'number', + references: [], + isBucketed: false, + params: { + formula, + }, + }, + { ...layer, indexPatternId: indexPattern.id }, + { indexPattern } + ).layer; + }, + }; +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/index.ts index 5ff0c4e2d4bd7d..cbe6efba1b8598 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/index.ts @@ -7,6 +7,8 @@ export type { FormulaIndexPatternColumn } from './formula'; export { formulaOperation } from './formula'; -export { regenerateLayerFromAst } from './parse'; + +export { insertOrReplaceFormulaColumn } from './parse'; + export type { MathIndexPatternColumn } from './math'; export { mathOperation } from './math'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts index ee245cc06bff96..a3b61429fb0bf4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts @@ -8,10 +8,11 @@ import { i18n } from '@kbn/i18n'; import { isObject } from 'lodash'; import type { TinymathAST, TinymathVariable, TinymathLocation } from '@kbn/tinymath'; -import type { +import { OperationDefinition, GenericOperationDefinition, GenericIndexPatternColumn, + operationDefinitionMap, } from '../index'; import type { IndexPattern, IndexPatternLayer } from '../../../types'; import { mathOperation } from './math'; @@ -24,10 +25,11 @@ import { groupArgsByType, mergeWithGlobalFilter, } from './util'; -import type { FormulaIndexPatternColumn } from './formula'; +import { FormulaIndexPatternColumn, isFormulaIndexPatternColumn } from './formula'; import { getColumnOrder } from '../../layer_helpers'; -function getManagedId(mainId: string, index: number) { +/** @internal **/ +export function getManagedId(mainId: string, index: number) { return `${mainId}X${index}`; } @@ -36,21 +38,15 @@ function parseAndExtract( layer: IndexPatternLayer, columnId: string, indexPattern: IndexPattern, - operationDefinitionMap: Record, + operations: Record, label?: string ) { - const { root, error } = tryToParse(text, operationDefinitionMap); + const { root, error } = tryToParse(text, operations); if (error || root == null) { return { extracted: [], isValid: false }; } // before extracting the data run the validation task and throw if invalid - const errors = runASTValidation( - root, - layer, - indexPattern, - operationDefinitionMap, - layer.columns[columnId] - ); + const errors = runASTValidation(root, layer, indexPattern, operations, layer.columns[columnId]); if (errors.length) { return { extracted: [], isValid: false }; } @@ -59,7 +55,7 @@ function parseAndExtract( */ const extracted = extractColumns( columnId, - operationDefinitionMap, + operations, root, layer, indexPattern, @@ -201,63 +197,116 @@ function extractColumns( return columns; } -export function regenerateLayerFromAst( - text: string, +interface ExpandColumnProperties { + indexPattern: IndexPattern; + operations?: Record; +} + +const getEmptyColumnsWithFormulaMeta = (): { + columns: Record; + meta: { + locations: Record; + }; +} => ({ + columns: {}, + meta: { + locations: {}, + }, +}); + +function generateFormulaColumns( + id: string, + column: FormulaIndexPatternColumn, layer: IndexPatternLayer, - columnId: string, - currentColumn: FormulaIndexPatternColumn, - indexPattern: IndexPattern, - operationDefinitionMap: Record + { indexPattern, operations = operationDefinitionMap }: ExpandColumnProperties ) { + const { columns, meta } = getEmptyColumnsWithFormulaMeta(); + const formula = column.params.formula || ''; + const { extracted, isValid } = parseAndExtract( - text, + formula, layer, - columnId, + id, indexPattern, - filterByVisibleOperation(operationDefinitionMap), - currentColumn.customLabel ? currentColumn.label : undefined + filterByVisibleOperation(operations), + column.customLabel ? column.label : undefined ); - const columns = { ...layer.columns }; - - const locations: Record = {}; + extracted.forEach(({ column: extractedColumn, location }, index) => { + const managedId = getManagedId(id, index); + columns[managedId] = extractedColumn; - Object.keys(columns).forEach((k) => { - if (k.startsWith(columnId)) { - delete columns[k]; + if (location) { + meta.locations[managedId] = location; } }); - extracted.forEach(({ column, location }, index) => { - columns[getManagedId(columnId, index)] = column; - if (location) locations[getManagedId(columnId, index)] = location; - }); - - columns[columnId] = { - ...currentColumn, - label: !currentColumn.customLabel - ? text ?? + columns[id] = { + ...column, + label: !column.customLabel + ? formula ?? i18n.translate('xpack.lens.indexPattern.formulaLabel', { defaultMessage: 'Formula', }) - : currentColumn.label, + : column.label, + references: !isValid ? [] : [getManagedId(id, extracted.length - 1)], params: { - ...currentColumn.params, - formula: text, + ...column.params, + formula, isFormulaBroken: !isValid, }, - references: !isValid ? [] : [getManagedId(columnId, extracted.length - 1)], } as FormulaIndexPatternColumn; + return { columns, meta }; +} + +/** @internal **/ +export function insertOrReplaceFormulaColumn( + id: string, + column: FormulaIndexPatternColumn, + baseLayer: IndexPatternLayer, + params: ExpandColumnProperties +) { + const layer = { + ...baseLayer, + columns: { + ...baseLayer.columns, + [id]: { + ...column, + }, + }, + }; + + const { columns: updatedColumns, meta } = Object.entries(layer.columns).reduce( + (acc, [currentColumnId, currentColumn]) => { + if (currentColumnId.startsWith(id)) { + if (currentColumnId === id && isFormulaIndexPatternColumn(currentColumn)) { + const formulaColumns = generateFormulaColumns( + currentColumnId, + currentColumn, + layer, + params + ); + acc.columns = { ...acc.columns, ...formulaColumns.columns }; + acc.meta = { ...acc.meta, ...formulaColumns.meta }; + } + } else { + acc.columns[currentColumnId] = { ...currentColumn }; + } + return acc; + }, + getEmptyColumnsWithFormulaMeta() + ); + return { - newLayer: { + layer: { ...layer, - columns, + columns: updatedColumns, columnOrder: getColumnOrder({ ...layer, - columns, + columns: updatedColumns, }), }, - locations, + meta, }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts index b9d675716c788b..4474effc8c8c86 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts @@ -8,6 +8,7 @@ export * from './operations'; export * from './layer_helpers'; export * from './time_scale_utils'; + export type { OperationType, BaseIndexPatternColumn, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 289161c9d3e377..dda1b16bc6c7b8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -36,7 +36,7 @@ import { ReferenceBasedIndexPatternColumn, BaseIndexPatternColumn, } from './definitions/column_types'; -import { FormulaIndexPatternColumn, regenerateLayerFromAst } from './definitions/formula'; +import { FormulaIndexPatternColumn, insertOrReplaceFormulaColumn } from './definitions/formula'; import type { TimeScaleUnit } from '../../../common/expressions'; import { isColumnOfType } from './definitions/helpers'; @@ -533,14 +533,9 @@ export function replaceColumn({ try { newLayer = newColumn.params.formula - ? regenerateLayerFromAst( - newColumn.params.formula, - basicLayer, - columnId, - newColumn, + ? insertOrReplaceFormulaColumn(columnId, newColumn, basicLayer, { indexPattern, - operationDefinitionMap - ).newLayer + }).layer : basicLayer; } catch (e) { newLayer = basicLayer; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index a0d43c5523c5be..08786b181f3e79 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -38,10 +38,13 @@ export type { OverallSumIndexPatternColumn, } from './operations'; +export type { FormulaPublicApi } from './operations/definitions/formula/formula_public_api'; + export type DraggedField = DragDropIdentifier & { field: IndexPatternField; indexPatternId: string; }; + export interface IndexPattern { id: string; fields: IndexPatternField[]; @@ -79,6 +82,7 @@ export interface IndexPatternPersistedState { } export type PersistedIndexPatternLayer = Omit; + export interface IndexPatternPrivateState { currentIndexPatternId: string; layers: Record; diff --git a/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts b/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts index 865e21f5bb6131..e740c789a48741 100644 --- a/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts +++ b/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts @@ -7,7 +7,8 @@ import { Observable, Subject } from 'rxjs'; import moment from 'moment'; -import { DataPublicPluginStart, esFilters, Filter } from '../../../../../src/plugins/data/public'; +import { isFilterPinned, Filter } from '@kbn/es-query'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; function createMockTimefilter() { const unsubscribe = jest.fn(); @@ -84,7 +85,7 @@ export function mockDataPlugin( getFilters: () => filters, getGlobalFilters: () => { // @ts-ignore - return filters.filter(esFilters.isFilterPinned); + return filters.filter(isFilterPinned); }, removeAll: () => { filters = []; diff --git a/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx b/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx index a92533a89ba678..4e713872c5a67b 100644 --- a/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx +++ b/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx @@ -25,6 +25,12 @@ export const lensPluginMock = { getXyVisTypes: jest .fn() .mockReturnValue(new Promise((resolve) => resolve(visualizationTypes))), + + stateHelperApi: jest.fn().mockResolvedValue({ + formula: { + insertOrReplaceFormulaColumn: jest.fn(), + }, + }), }; return startContract; }, diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 6b26753b41880c..decd9d8c695109 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -39,6 +39,7 @@ import { IndexPatternFieldEditorStart } from '../../../../src/plugins/data_view_ import type { IndexPatternDatasource as IndexPatternDatasourceType, IndexPatternDatasourceSetupPlugins, + FormulaPublicApi, } from './indexpattern_datasource'; import type { XyVisualization as XyVisualizationType, @@ -160,6 +161,13 @@ export interface LensPublicStart { * Method which returns xy VisualizationTypes array keeping this async as to not impact page load bundle */ getXyVisTypes: () => Promise; + + /** + * API which returns state helpers keeping this async as to not impact page load bundle + */ + stateHelperApi: () => Promise<{ + formula: FormulaPublicApi; + }>; } export class LensPlugin { @@ -387,6 +395,14 @@ export class LensPlugin { const { visualizationTypes } = await import('./xy_visualization/types'); return visualizationTypes; }, + + stateHelperApi: async () => { + const { createFormulaPublicApi } = await import('./async_services'); + + return { + formula: createFormulaPublicApi(), + }; + }, }; } diff --git a/x-pack/plugins/maps/common/index.ts b/x-pack/plugins/maps/common/index.ts index 31f0d67969b5de..f3b8595efc8d19 100644 --- a/x-pack/plugins/maps/common/index.ts +++ b/x-pack/plugins/maps/common/index.ts @@ -18,8 +18,14 @@ export { SOURCE_TYPES, STYLE_TYPE, SYMBOLIZE_AS_TYPES, + LAYER_WIZARD_CATEGORY, + MAX_ZOOM, + MIN_ZOOM, + VECTOR_SHAPE_TYPE, } from './constants'; +export type { FieldFormatter } from './constants'; + export type { EMSFileSourceDescriptor, ESTermSourceDescriptor, diff --git a/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts b/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts index e0e12b7d51a567..47482cd4c4f92b 100644 --- a/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts @@ -23,8 +23,13 @@ export class TopTermPercentageField implements IESAggField { } supportsFieldMetaFromLocalData(): boolean { - // Elasticsearch vector tile search API does not support top term metric - return false; + if (this.getSource().isMvt()) { + // Elasticsearch vector tile search API does not support top term metric so meta tile does not contain any values + return false; + } else { + // field meta can be extracted from local data when field is geojson source + return true; + } } getSource(): IVectorSource { diff --git a/x-pack/plugins/maps/public/classes/layers/index.ts b/x-pack/plugins/maps/public/classes/layers/index.ts index 688cbd409c4508..b068d4d234170f 100644 --- a/x-pack/plugins/maps/public/classes/layers/index.ts +++ b/x-pack/plugins/maps/public/classes/layers/index.ts @@ -6,4 +6,4 @@ */ export type { LayerWizard, LayerWizardWithMeta, RenderWizardArguments } from './wizards'; -export { getLayerWizards, registerLayerWizard } from './wizards'; +export { getLayerWizards, registerLayerWizardExternal } from './wizards'; diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/choropleth_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/choropleth_layer_wizard.tsx index 2670a6ac82c788..4334a347854338 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/choropleth_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/choropleth_layer_wizard.tsx @@ -13,6 +13,7 @@ import { LayerTemplate } from './layer_template'; import { ChoroplethLayerIcon } from '../icons/cloropleth_layer_icon'; export const choroplethLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.choropleth.desc', { defaultMessage: 'Shaded areas to compare statistics across boundaries', diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/file_upload_wizard/config.tsx b/x-pack/plugins/maps/public/classes/layers/wizards/file_upload_wizard/config.tsx index 586667b9a1ecb7..9be1639f7cb398 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/file_upload_wizard/config.tsx +++ b/x-pack/plugins/maps/public/classes/layers/wizards/file_upload_wizard/config.tsx @@ -12,6 +12,7 @@ import { ClientFileCreateSourceEditor, UPLOAD_STEPS } from './wizard'; import { getFileUpload } from '../../../../kibana_services'; export const uploadLayerWizardConfig: LayerWizard = { + order: 10, categories: [], description: i18n.translate('xpack.maps.fileUploadWizard.description', { defaultMessage: 'Index GeoJSON data in Elasticsearch', diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/index.ts b/x-pack/plugins/maps/public/classes/layers/wizards/index.ts index d925fb50eb60dd..814a2ec8e5c2fa 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/index.ts +++ b/x-pack/plugins/maps/public/classes/layers/wizards/index.ts @@ -10,4 +10,4 @@ export type { LayerWizardWithMeta, RenderWizardArguments, } from './layer_wizard_registry'; -export { getLayerWizards, registerLayerWizard } from './layer_wizard_registry'; +export { getLayerWizards, registerLayerWizardExternal } from './layer_wizard_registry'; diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/layer_wizard_registry.test.tsx b/x-pack/plugins/maps/public/classes/layers/wizards/layer_wizard_registry.test.tsx new file mode 100644 index 00000000000000..aa54ea4286f1a7 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/wizards/layer_wizard_registry.test.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + getLayerWizards, + registerLayerWizardInternal, + registerLayerWizardExternal, +} from './layer_wizard_registry'; +import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; + +describe('LayerWizardRegistryTest', () => { + it('should enforce ordering', async () => { + registerLayerWizardExternal({ + categories: [LAYER_WIZARD_CATEGORY.REFERENCE], + description: '', + icon: '', + title: 'foo', + renderWizard(): React.ReactElement { + return <>; + }, + order: 100, + }); + + registerLayerWizardInternal({ + order: 1, + categories: [LAYER_WIZARD_CATEGORY.REFERENCE], + description: '', + icon: '', + title: 'foobar', + renderWizard(): React.ReactElement { + return <>; + }, + }); + + registerLayerWizardInternal({ + order: 1, + categories: [LAYER_WIZARD_CATEGORY.REFERENCE], + description: '', + icon: '', + title: 'bar', + renderWizard(): React.ReactElement { + return <>; + }, + }); + + const wizards = await getLayerWizards(); + + expect(wizards[0].title).toBe('foobar'); + expect(wizards[1].title).toBe('bar'); + expect(wizards[2].title).toBe('foo'); + }); + + it('external users must add order higher than 99 ', async () => { + expect(() => { + registerLayerWizardExternal({ + order: 99, + categories: [LAYER_WIZARD_CATEGORY.REFERENCE], + description: '', + icon: '', + title: 'bar', + renderWizard(): React.ReactElement { + return <>; + }, + }); + }).toThrow(`layerWizard.order should be greater than or equal to '100'`); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/layer_wizard_registry.ts b/x-pack/plugins/maps/public/classes/layers/wizards/layer_wizard_registry.ts index 251af230d52786..6ab8a3d9a2f56b 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/layer_wizard_registry.ts +++ b/x-pack/plugins/maps/public/classes/layers/wizards/layer_wizard_registry.ts @@ -11,6 +11,26 @@ import { ReactElement, FunctionComponent } from 'react'; import type { LayerDescriptor } from '../../../../common/descriptor_types'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; +export type LayerWizard = { + title: string; + categories: LAYER_WIZARD_CATEGORY[]; + /* + * Sets display order. + * Lower numbers are displayed before higher numbers. + * 0-99 reserved for Maps-plugin wizards. + */ + order: number; + description: string; + icon: string | FunctionComponent; + renderWizard(renderWizardArguments: RenderWizardArguments): ReactElement; + prerequisiteSteps?: Array<{ id: string; label: string }>; + disabledReason?: string; + getIsDisabled?: () => Promise | boolean; + isBeta?: boolean; + checkVisibility?: () => Promise; + showFeatureEditTools?: boolean; +}; + export type RenderWizardArguments = { previewLayers: (layerDescriptors: LayerDescriptor[]) => void; mapColors: string[]; @@ -27,20 +47,6 @@ export type RenderWizardArguments = { advanceToNextStep: () => void; }; -export type LayerWizard = { - categories: LAYER_WIZARD_CATEGORY[]; - checkVisibility?: () => Promise; - description: string; - disabledReason?: string; - getIsDisabled?: () => Promise | boolean; - isBeta?: boolean; - icon: string | FunctionComponent; - prerequisiteSteps?: Array<{ id: string; label: string }>; - renderWizard(renderWizardArguments: RenderWizardArguments): ReactElement; - title: string; - showFeatureEditTools?: boolean; -}; - export type LayerWizardWithMeta = LayerWizard & { isVisible: boolean; isDisabled: boolean; @@ -48,7 +54,7 @@ export type LayerWizardWithMeta = LayerWizard & { const registry: LayerWizard[] = []; -export function registerLayerWizard(layerWizard: LayerWizard) { +export function registerLayerWizardInternal(layerWizard: LayerWizard) { registry.push({ checkVisibility: async () => { return true; @@ -61,6 +67,13 @@ export function registerLayerWizard(layerWizard: LayerWizard) { }); } +export function registerLayerWizardExternal(layerWizard: LayerWizard) { + if (layerWizard.order < 100) { + throw new Error(`layerWizard.order should be greater than or equal to '100'`); + } + registerLayerWizardInternal(layerWizard); +} + export async function getLayerWizards(): Promise { const promises = registry.map(async (layerWizard: LayerWizard) => { return { @@ -69,7 +82,11 @@ export async function getLayerWizards(): Promise { isDisabled: await layerWizard.getIsDisabled!(), }; }); - return (await Promise.all(promises)).filter(({ isVisible }) => { - return isVisible; - }); + return (await Promise.all(promises)) + .filter(({ isVisible }) => { + return isVisible; + }) + .sort((wizard1: LayerWizardWithMeta, wizard2: LayerWizardWithMeta) => { + return wizard1.order - wizard2.order; + }); } diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/load_layer_wizards.ts b/x-pack/plugins/maps/public/classes/layers/wizards/load_layer_wizards.ts index 0b9558a393e78e..3bf64d08fc8451 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/load_layer_wizards.ts +++ b/x-pack/plugins/maps/public/classes/layers/wizards/load_layer_wizards.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { registerLayerWizard } from './layer_wizard_registry'; +import { registerLayerWizardInternal } from './layer_wizard_registry'; import { uploadLayerWizardConfig } from './file_upload_wizard'; import { esDocumentsLayerWizardConfig, @@ -16,17 +16,12 @@ import { heatmapLayerWizardConfig, } from '../../sources/es_geo_grid_source'; import { geoLineLayerWizardConfig } from '../../sources/es_geo_line_source'; -// @ts-ignore -import { point2PointLayerWizardConfig } from '../../sources/es_pew_pew_source'; -// @ts-ignore +import { point2PointLayerWizardConfig } from '../../sources/es_pew_pew_source/point_2_point_layer_wizard'; import { emsBoundariesLayerWizardConfig } from '../../sources/ems_file_source'; -// @ts-ignore import { emsBaseMapLayerWizardConfig } from '../../sources/ems_tms_source'; -// @ts-ignore -import { kibanaBasemapLayerWizardConfig } from '../../sources/kibana_tilemap_source'; +import { kibanaBasemapLayerWizardConfig } from '../../sources/kibana_tilemap_source/kibana_base_map_layer_wizard'; import { tmsLayerWizardConfig } from '../../sources/xyz_tms_source'; -// @ts-ignore -import { wmsLayerWizardConfig } from '../../sources/wms_source'; +import { wmsLayerWizardConfig } from '../../sources/wms_source/wms_layer_wizard'; import { mvtVectorSourceWizardConfig } from '../../sources/mvt_single_layer_vector_source'; import { ObservabilityLayerWizardConfig } from './solution_layers/observability'; import { SecurityLayerWizardConfig } from './solution_layers/security'; @@ -39,31 +34,23 @@ export function registerLayerWizards() { return; } - // Registration order determines display order - registerLayerWizard(uploadLayerWizardConfig); - registerLayerWizard(esDocumentsLayerWizardConfig); - // @ts-ignore - registerLayerWizard(choroplethLayerWizardConfig); - registerLayerWizard(clustersLayerWizardConfig); - // @ts-ignore - registerLayerWizard(heatmapLayerWizardConfig); - registerLayerWizard(esTopHitsLayerWizardConfig); - registerLayerWizard(geoLineLayerWizardConfig); - // @ts-ignore - registerLayerWizard(point2PointLayerWizardConfig); - // @ts-ignore - registerLayerWizard(emsBoundariesLayerWizardConfig); - registerLayerWizard(newVectorLayerWizardConfig); - // @ts-ignore - registerLayerWizard(emsBaseMapLayerWizardConfig); - // @ts-ignore - registerLayerWizard(kibanaBasemapLayerWizardConfig); - registerLayerWizard(tmsLayerWizardConfig); - // @ts-ignore - registerLayerWizard(wmsLayerWizardConfig); + registerLayerWizardInternal(uploadLayerWizardConfig); + registerLayerWizardInternal(esDocumentsLayerWizardConfig); + registerLayerWizardInternal(choroplethLayerWizardConfig); + registerLayerWizardInternal(clustersLayerWizardConfig); + registerLayerWizardInternal(heatmapLayerWizardConfig); + registerLayerWizardInternal(esTopHitsLayerWizardConfig); + registerLayerWizardInternal(geoLineLayerWizardConfig); + registerLayerWizardInternal(point2PointLayerWizardConfig); + registerLayerWizardInternal(emsBoundariesLayerWizardConfig); + registerLayerWizardInternal(newVectorLayerWizardConfig); + registerLayerWizardInternal(emsBaseMapLayerWizardConfig); + registerLayerWizardInternal(kibanaBasemapLayerWizardConfig); + registerLayerWizardInternal(tmsLayerWizardConfig); + registerLayerWizardInternal(wmsLayerWizardConfig); - registerLayerWizard(mvtVectorSourceWizardConfig); - registerLayerWizard(ObservabilityLayerWizardConfig); - registerLayerWizard(SecurityLayerWizardConfig); + registerLayerWizardInternal(mvtVectorSourceWizardConfig); + registerLayerWizardInternal(ObservabilityLayerWizardConfig); + registerLayerWizardInternal(SecurityLayerWizardConfig); registered = true; } diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/new_vector_layer_wizard/config.tsx b/x-pack/plugins/maps/public/classes/layers/wizards/new_vector_layer_wizard/config.tsx index d6376470ae71fb..85fae39a05910e 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/new_vector_layer_wizard/config.tsx +++ b/x-pack/plugins/maps/public/classes/layers/wizards/new_vector_layer_wizard/config.tsx @@ -16,6 +16,7 @@ import { LAYER_WIZARD_CATEGORY } from '../../../../../common/constants'; const ADD_VECTOR_DRAWING_LAYER = 'ADD_VECTOR_DRAWING_LAYER'; export const newVectorLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.newVectorLayerWizard.description', { defaultMessage: 'Draw shapes on the map and index in Elasticsearch', diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/observability/observability_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/observability/observability_layer_wizard.tsx index a69b09fffd9eec..2e023f7c588d31 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/observability/observability_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/observability/observability_layer_wizard.tsx @@ -14,6 +14,7 @@ import { APM_INDEX_PATTERN_ID } from './create_layer_descriptor'; import { getIndexPatternService } from '../../../../../kibana_services'; export const ObservabilityLayerWizardConfig: LayerWizard = { + order: 20, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH, LAYER_WIZARD_CATEGORY.SOLUTIONS], getIsDisabled: async () => { try { diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/security_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/security_layer_wizard.tsx index f055683722debb..79575ea8151240 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/security_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/security_layer_wizard.tsx @@ -13,6 +13,7 @@ import { getSecurityIndexPatterns } from './security_index_pattern_utils'; import { SecurityLayerTemplate } from './security_layer_template'; export const SecurityLayerWizardConfig: LayerWizard = { + order: 20, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH, LAYER_WIZARD_CATEGORY.SOLUTIONS], getIsDisabled: async () => { const indexPatterns = await getSecurityIndexPatterns(); diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx index ff899101ced496..8fe8f1b3a155fb 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -29,6 +29,7 @@ function getDescription() { } export const emsBoundariesLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.REFERENCE], checkVisibility: async () => { const emsSettings = getEMSSettings(); diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx index 26afa65b9527cb..27d911cc8feb94 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -27,6 +27,7 @@ function getDescription() { } export const emsBaseMapLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.REFERENCE], checkVisibility: async () => { const emsSettings = getEMSSettings(); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx index a184ae7b7ce569..e075a615d5867d 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx @@ -33,6 +33,7 @@ import { NUMERICAL_COLOR_PALETTES } from '../../styles/color_palettes'; import { ClustersLayerIcon } from '../../layers/wizards/icons/clusters_layer_icon'; export const clustersLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.source.esGridClustersDescription', { defaultMessage: 'Geospatial data grouped in grids with metrics for each gridded cell', diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx index b415b7a167c5a0..5e67a83811561b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx @@ -17,6 +17,7 @@ import { GRID_RESOLUTION, LAYER_WIZARD_CATEGORY, RENDER_AS } from '../../../../c import { HeatmapLayerIcon } from '../../layers/wizards/icons/heatmap_layer_icon'; export const heatmapLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.source.esGridHeatmapDescription', { defaultMessage: 'Geospatial data grouped in grids to show density', diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx index 85932658383de7..18d459ddbcb78b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx @@ -17,6 +17,7 @@ import { getIsGoldPlus } from '../../../licensed_features'; import { TracksLayerIcon } from '../../layers/wizards/icons/tracks_layer_icon'; export const geoLineLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.source.esGeoLineDescription', { defaultMessage: 'Create lines from points', diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx index c35b8677c10931..e3522d39e892dd 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx @@ -27,6 +27,7 @@ import { ColorDynamicOptions, SizeDynamicOptions } from '../../../../common/desc import { Point2PointLayerIcon } from '../../layers/wizards/icons/point_2_point_layer_icon'; export const point2PointLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.source.pewPewDescription', { defaultMessage: 'Aggregated data paths between the source and destination', diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx index b5aeb28715aefa..82fb1c502ef6aa 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx @@ -35,6 +35,7 @@ export function createDefaultLayerDescriptor( } export const esDocumentsLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.source.esSearchDescription', { defaultMessage: 'Points, lines, and polygons from Elasticsearch', diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/wizard.tsx index 3e5af637dc3369..7c01fed158b0dc 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/wizard.tsx @@ -16,6 +16,7 @@ import { ESSearchSourceDescriptor } from '../../../../../common/descriptor_types import { ESSearchSource } from '../es_search_source'; export const esTopHitsLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.source.topHitsDescription', { defaultMessage: diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx index ec69989a8313d1..0f3475eeae9ee0 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx @@ -17,6 +17,7 @@ import { getKibanaTileMap } from '../../../util'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; export const kibanaBasemapLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.REFERENCE], checkVisibility: async () => { const tilemap = getKibanaTileMap(); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx index bf6ff368594cce..f123ed7c78054b 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx @@ -16,6 +16,7 @@ import { TiledSingleLayerVectorSourceSettings } from '../../../../common/descrip import { VectorTileLayerIcon } from '../../layers/wizards/icons/vector_tile_layer_icon'; export const mvtVectorSourceWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.REFERENCE], description: i18n.translate('xpack.maps.source.mvtVectorSourceWizard', { defaultMessage: 'Data service implementing the Mapbox vector tile specification', diff --git a/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx index 19f31d481f58ed..2f79b8d0984d0c 100644 --- a/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx @@ -17,6 +17,7 @@ import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; import { WebMapServiceLayerIcon } from '../../layers/wizards/icons/web_map_service_layer_icon'; export const wmsLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.REFERENCE], description: i18n.translate('xpack.maps.source.wmsDescription', { defaultMessage: 'Maps from OGC Standard WMS', diff --git a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx index 82aab592a13443..7c137419f4a192 100644 --- a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx @@ -15,6 +15,7 @@ import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; import { WorldMapLayerIcon } from '../../layers/wizards/icons/world_map_layer_icon'; export const tmsLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.REFERENCE], description: i18n.translate('xpack.maps.source.ems_xyzDescription', { defaultMessage: 'Raster image tile map service using {z}/{x}/{y} url pattern.', diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts index a264ae36d88afc..6b0e2123574366 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts @@ -31,18 +31,19 @@ export const DEFAULT_ICON_SIZE = 6; export const DEFAULT_COLOR_RAMP = NUMERICAL_COLOR_PALETTES[0].value; export const DEFAULT_COLOR_PALETTE = CATEGORICAL_COLOR_PALETTES[0].value; -export const LINE_STYLES = [VECTOR_STYLES.LINE_COLOR, VECTOR_STYLES.LINE_WIDTH]; -export const POLYGON_STYLES = [ - VECTOR_STYLES.FILL_COLOR, - VECTOR_STYLES.LINE_COLOR, - VECTOR_STYLES.LINE_WIDTH, -]; export const LABEL_STYLES = [ VECTOR_STYLES.LABEL_SIZE, VECTOR_STYLES.LABEL_COLOR, VECTOR_STYLES.LABEL_BORDER_COLOR, VECTOR_STYLES.LABEL_BORDER_SIZE, ]; +export const LINE_STYLES = [VECTOR_STYLES.LINE_COLOR, VECTOR_STYLES.LINE_WIDTH, ...LABEL_STYLES]; +export const POLYGON_STYLES = [ + VECTOR_STYLES.FILL_COLOR, + VECTOR_STYLES.LINE_COLOR, + VECTOR_STYLES.LINE_WIDTH, + ...LABEL_STYLES, +]; export function getDefaultStaticProperties( mapColors: string[] = [] diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 40ee17d176706f..a3b6638d5ee8bb 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -15,6 +15,7 @@ import { Subscription } from 'rxjs'; import { Unsubscribe } from 'redux'; import { EuiEmptyPrompt } from '@elastic/eui'; import { Filter } from '@kbn/es-query'; +import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import { Embeddable, IContainer, @@ -72,6 +73,7 @@ import { getChartsPaletteServiceGetColor, getSpacesApi, getSearchService, + getTheme, } from '../kibana_services'; import { LayerDescriptor, MapExtent } from '../../common/descriptor_types'; import { MapContainer } from '../connected_components/map_container'; @@ -400,7 +402,9 @@ export class MapEmbeddable const I18nContext = getCoreI18n().Context; render( - {content} + + {content}; + , this._domNode ); diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 31066204cd318a..b11d7270fe13e6 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -46,7 +46,7 @@ import { MapsStartApi, suggestEMSTermJoinConfig, } from './api'; -import { registerLayerWizard } from './classes/layers'; +import { registerLayerWizardExternal } from './classes/layers'; import { registerSource } from './classes/sources/source_registry'; import type { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; import type { MapsEmsPluginPublicStart } from '../../../../src/plugins/maps_ems/public'; @@ -182,7 +182,7 @@ export class MapsPlugin plugins.visualizations.createBaseVisualization(tileMapVisType); return { - registerLayerWizard, + registerLayerWizard: registerLayerWizardExternal, registerSource, }; } diff --git a/x-pack/plugins/ml/common/types/storage.ts b/x-pack/plugins/ml/common/types/storage.ts index 6da8076e223320..22374a5533fab0 100644 --- a/x-pack/plugins/ml/common/types/storage.ts +++ b/x-pack/plugins/ml/common/types/storage.ts @@ -13,6 +13,8 @@ export const ML_APPLY_TIME_RANGE_CONFIG = 'ml.jobSelectorFlyout.applyTimeRange'; export const ML_GETTING_STARTED_CALLOUT_DISMISSED = 'ml.gettingStarted.isDismissed'; +export const ML_FROZEN_TIER_PREFERENCE = 'ml.frozenDataTierPreference'; + export type PartitionFieldConfig = | { /** @@ -44,6 +46,7 @@ export type MlStorage = Partial<{ [ML_ENTITY_FIELDS_CONFIG]: PartitionFieldsConfig; [ML_APPLY_TIME_RANGE_CONFIG]: ApplyTimeRangeConfig; [ML_GETTING_STARTED_CALLOUT_DISMISSED]: boolean | undefined; + [ML_FROZEN_TIER_PREFERENCE]: 'exclude_frozen' | 'include_frozen'; }> | null; export type MlStorageKey = keyof Exclude; diff --git a/x-pack/plugins/ml/common/util/query_utils.test.ts b/x-pack/plugins/ml/common/util/query_utils.test.ts new file mode 100644 index 00000000000000..947b87e9976d58 --- /dev/null +++ b/x-pack/plugins/ml/common/util/query_utils.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { addExcludeFrozenToQuery } from './query_utils'; + +describe('Util: addExcludeFrozenToQuery()', () => { + test('Validation checks.', () => { + expect( + addExcludeFrozenToQuery({ + match_all: {}, + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }) + ).toMatchObject({ + bool: { + must: [{ match_all: {} }], + must_not: [{ term: { _tier: { value: 'data_frozen' } } }], + }, + }); + + expect( + addExcludeFrozenToQuery({ + bool: { + must: [], + must_not: { + term: { + category: { + value: 'clothing', + }, + }, + }, + }, + }) + ).toMatchObject({ + bool: { + must: [], + must_not: [ + { term: { category: { value: 'clothing' } } }, + { term: { _tier: { value: 'data_frozen' } } }, + ], + }, + }); + + expect( + addExcludeFrozenToQuery({ + bool: { + must: [], + must_not: [{ term: { category: { value: 'clothing' } } }], + }, + }) + ).toMatchObject({ + bool: { + must: [], + must_not: [ + { term: { category: { value: 'clothing' } } }, + { term: { _tier: { value: 'data_frozen' } } }, + ], + }, + }); + + expect(addExcludeFrozenToQuery(undefined)).toMatchObject({ + bool: { + must_not: [{ term: { _tier: { value: 'data_frozen' } } }], + }, + }); + }); +}); diff --git a/x-pack/plugins/ml/common/util/query_utils.ts b/x-pack/plugins/ml/common/util/query_utils.ts new file mode 100644 index 00000000000000..22c0f45f2f239b --- /dev/null +++ b/x-pack/plugins/ml/common/util/query_utils.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { cloneDeep } from 'lodash'; +import { isPopulatedObject } from './object_utils'; + +export const addExcludeFrozenToQuery = (originalQuery: QueryDslQueryContainer | undefined) => { + const FROZEN_TIER_TERM = { + term: { + _tier: { + value: 'data_frozen', + }, + }, + }; + + if (!originalQuery) { + return { + bool: { + must_not: [FROZEN_TIER_TERM], + }, + }; + } + + const query = cloneDeep(originalQuery); + + delete query.match_all; + + if (isPopulatedObject(query.bool)) { + // Must_not can be both arrays or singular object + if (Array.isArray(query.bool.must_not)) { + query.bool.must_not.push(FROZEN_TIER_TERM); + } else { + // If there's already a must_not condition + if (isPopulatedObject(query.bool.must_not)) { + query.bool.must_not = [query.bool.must_not, FROZEN_TIER_TERM]; + } + if (query.bool.must_not === undefined) { + query.bool.must_not = [FROZEN_TIER_TERM]; + } + } + } else { + query.bool = { + must_not: [FROZEN_TIER_TERM], + }; + } + + return query; +}; diff --git a/x-pack/plugins/ml/public/application/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap b/x-pack/plugins/ml/public/application/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap index eb9705f3438aa0..9a3fb9b29d09b0 100644 --- a/x-pack/plugins/ml/public/application/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap +++ b/x-pack/plugins/ml/public/application/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap @@ -1,19 +1,77 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`FullTimeRangeSelector renders the selector 1`] = ` - - } - /> - + delay="regular" + display="inlineBlock" + position="top" + > + + + + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="mlFullTimeRangeSelectorOption" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + + + + `; diff --git a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx index d04f8f7b648f5d..3f64ff794d9abb 100644 --- a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx +++ b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx @@ -20,6 +20,12 @@ jest.mock('./full_time_range_selector_service', () => ({ mockSetFullTimeRange(indexPattern, query), })); +jest.mock('../../contexts/ml/use_storage', () => { + return { + useStorage: jest.fn(() => 'exclude-frozen'), + }; +}); + describe('FullTimeRangeSelector', () => { const dataView = { id: '0844fc70-5ab5-11e9-935e-836737467b0f', diff --git a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx index f0af666e07dbc0..44f6fc5e604cb9 100644 --- a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx @@ -5,44 +5,160 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { Query } from 'src/plugins/data/public'; -import { EuiButton } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiButton, + EuiFlexItem, + EuiButtonIcon, + EuiRadioGroup, + EuiPanel, + EuiToolTip, + EuiPopover, + EuiRadioGroupOption, +} from '@elastic/eui'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { i18n } from '@kbn/i18n'; import type { DataView } from '../../../../../../../src/plugins/data_views/public'; import { setFullTimeRange } from './full_time_range_selector_service'; +import { useStorage } from '../../contexts/ml/use_storage'; +import { ML_FROZEN_TIER_PREFERENCE } from '../../../../common/types/storage'; interface Props { dataView: DataView; - query: Query; + query: QueryDslQueryContainer; disabled: boolean; callback?: (a: any) => void; } +const FROZEN_TIER_PREFERENCE = { + EXCLUDE: 'exclude-frozen', + INCLUDE: 'include-frozen', +} as const; + +type FrozenTierPreference = typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE]; + // Component for rendering a button which automatically sets the range of the time filter // to the time range of data in the index(es) mapped to the supplied Kibana index pattern or query. export const FullTimeRangeSelector: FC = ({ dataView, query, disabled, callback }) => { // wrapper around setFullTimeRange to allow for the calling of the optional callBack prop - async function setRange(i: DataView, q: Query) { - const fullTimeRange = await setFullTimeRange(i, q); + async function setRange(i: DataView, q: QueryDslQueryContainer, excludeFrozenData = true) { + const fullTimeRange = await setFullTimeRange(i, q, excludeFrozenData); if (typeof callback === 'function') { callback(fullTimeRange); } } + + const [isPopoverOpen, setPopover] = useState(false); + const [frozenDataPreference, setFrozenDataPreference] = useStorage( + ML_FROZEN_TIER_PREFERENCE, + FROZEN_TIER_PREFERENCE.EXCLUDE + ); + + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + const sortOptions: EuiRadioGroupOption[] = useMemo(() => { + return [ + { + id: FROZEN_TIER_PREFERENCE.EXCLUDE, + label: i18n.translate( + 'xpack.ml.fullTimeRangeSelector.useFullDataExcludingFrozenMenuLabel', + { + defaultMessage: 'Exclude frozen data tier', + } + ), + }, + { + id: FROZEN_TIER_PREFERENCE.INCLUDE, + label: i18n.translate( + 'xpack.ml.fullTimeRangeSelector.useFullDataIncludingFrozenMenuLabel', + { + defaultMessage: 'Include frozen data tier', + } + ), + }, + ]; + }, []); + + const setPreference = useCallback((id: string) => { + setFrozenDataPreference(id as FrozenTierPreference); + setRange(dataView, query, id === FROZEN_TIER_PREFERENCE.EXCLUDE); + closePopover(); + }, []); + + const popoverContent = useMemo( + () => ( + + + + ), + [frozenDataPreference, sortOptions] + ); + + const buttonTooltip = useMemo( + () => + frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE ? ( + + ) : ( + + ), + [frozenDataPreference] + ); + return ( - setRange(dataView, query)} - data-test-subj="mlButtonUseFullData" - > - - + + + setRange(dataView, query, true)} + data-test-subj="mlButtonUseFullData" + > + + + + + + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downRight" + > + {popoverContent} + + + ); }; diff --git a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts index 8f0d344a36f36e..7e14639f1b8b43 100644 --- a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts +++ b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts @@ -8,13 +8,14 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; -import type { Query } from 'src/plugins/data/public'; import dateMath from '@elastic/datemath'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { getTimefilter, getToastNotifications } from '../../util/dependency_cache'; import { ml, GetTimeFieldRangeResponse } from '../../services/ml_api_service'; import type { DataView } from '../../../../../../../src/plugins/data_views/public'; import { isPopulatedObject } from '../../../../common/util/object_utils'; -import { RuntimeMappings } from '../../../../common/types/fields'; +import type { RuntimeMappings } from '../../../../common/types/fields'; +import { addExcludeFrozenToQuery } from '../../../../common/util/query_utils'; export interface TimeRange { from: number; @@ -23,7 +24,8 @@ export interface TimeRange { export async function setFullTimeRange( indexPattern: DataView, - query: Query + query: QueryDslQueryContainer, + excludeFrozenData: boolean ): Promise { try { const timefilter = getTimefilter(); @@ -31,7 +33,8 @@ export async function setFullTimeRange( const resp = await ml.getTimeFieldRange({ index: indexPattern.title, timeFieldName: indexPattern.timeFieldName, - query, + // By default we want to use full non-frozen time range + query: excludeFrozenData ? addExcludeFrozenToQuery(query) : query, ...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}), }); timefilter.setTime({ diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx index a6a707634811d2..c370778b178c80 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx @@ -42,6 +42,7 @@ import { TIME_FORMAT } from '../../../../../common/constants/time_format'; import { JobsAwaitingNodeWarning } from '../../../components/jobs_awaiting_node_warning'; import { isPopulatedObject } from '../../../../../common/util/object_utils'; import { RuntimeMappings } from '../../../../../common/types/fields'; +import { addExcludeFrozenToQuery } from '../../../../../common/util/query_utils'; import { MlPageHeader } from '../../../components/page_header'; export interface ModuleJobUI extends ModuleJob { @@ -136,7 +137,8 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { const { start, end } = await ml.getTimeFieldRange({ index: dataView.title, timeFieldName: dataView.timeFieldName, - query: combinedQuery, + // By default we want to use full non-frozen time range + query: addExcludeFrozenToQuery(combinedQuery), ...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}), }); return { diff --git a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts index e608bfeb622d8c..128517777bb463 100644 --- a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts +++ b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts @@ -16,7 +16,6 @@ import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; import { Datafeed, IndicesOptions } from '../../../common/types/anomaly_detection_jobs'; import { RuntimeMappings } from '../../../common/types/fields'; import { isPopulatedObject } from '../../../common/util/object_utils'; - /** * Service for carrying out queries to obtain data * specific to fields in Elasticsearch indices. diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/statement_section.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/statement_section.test.js.snap index 59af548f267fd8..1fe8d3e1f4d7bc 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/statement_section.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/statement_section.test.js.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`StatementSection component renders heading text, correct icon type, and elements for StatementSection 1`] = ` -
+
+
diff --git a/x-pack/plugins/monitoring/public/components/sparkline/__snapshots__/index.test.js.snap b/x-pack/plugins/monitoring/public/components/sparkline/__snapshots__/index.test.js.snap index 13fbd618a0a09b..f52e00b4432c0f 100644 --- a/x-pack/plugins/monitoring/public/components/sparkline/__snapshots__/index.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/sparkline/__snapshots__/index.test.js.snap @@ -18,7 +18,7 @@ exports[`Sparkline component shows tooltip on hover 1`] = ` style={ Object { "left": 210, - "top": 27, + "top": 17, } } > @@ -35,7 +35,7 @@ exports[`Sparkline component shows tooltip on hover 1`] = ` className="monSparklineTooltip" style={ Object { - "height": 36, + "height": 56, "width": 220, } } diff --git a/x-pack/plugins/monitoring/public/components/sparkline/index.js b/x-pack/plugins/monitoring/public/components/sparkline/index.js index fe399545cf6e01..ee250624432a60 100644 --- a/x-pack/plugins/monitoring/public/components/sparkline/index.js +++ b/x-pack/plugins/monitoring/public/components/sparkline/index.js @@ -60,7 +60,7 @@ export class Sparkline extends React.Component { return; } - const tooltipHeightInPx = 36; + const tooltipHeightInPx = 56; const tooltipWidthInPx = 220; const caretWidthInPx = 6; const marginBetweenPointAndCaretInPx = 10; diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.ts index 4185d1a4c985f4..1e8eb94df4836b 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.ts @@ -76,6 +76,17 @@ export function _enrichStateWithStatsAggregation( statsAggregation: any, timeseriesIntervalInSeconds: number ) { + // we could have data in both legacy and metricbeat collection, we pick the bucket most filled + const bucketCount = (aggregationKey: string) => + get( + statsAggregation.aggregations, + `${aggregationKey}.scoped.total_processor_duration_stats.count` + ); + + const pipelineBucket = + bucketCount('pipelines_mb') > bucketCount('pipelines') + ? statsAggregation.aggregations.pipelines_mb + : statsAggregation.aggregations.pipelines; const logstashState = stateDocument.logstash_state || stateDocument.logstash?.node?.state; const vertices = logstashState?.pipeline?.representation?.graph?.vertices ?? []; @@ -85,14 +96,10 @@ export function _enrichStateWithStatsAggregation( vertex.stats = {}; }); - const totalDurationStats = - statsAggregation.aggregations.pipelines.scoped.total_processor_duration_stats; + const totalDurationStats = pipelineBucket.scoped.total_processor_duration_stats; const totalProcessorsDurationInMillis = totalDurationStats.max - totalDurationStats.min; - const verticesWithStatsBuckets = - statsAggregation.aggregations?.pipelines.scoped.vertices?.vertex_id.buckets ?? - statsAggregation.aggregations?.pipelines_mb.scoped.vertices?.vertex_id.buckets ?? - []; + const verticesWithStatsBuckets = pipelineBucket.scoped.vertices?.vertex_id.buckets ?? []; verticesWithStatsBuckets.forEach((vertexStatsBucket: any) => { // Each vertexStats bucket contains a list of stats for a single vertex within a single timeseries interval const vertexId = vertexStatsBucket.key; diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index fcc3fdd64c36cf..03860fd3cd1227 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -24,3 +24,7 @@ export const observabilityFeatureId = 'observability'; // Used by Cases to install routes export const casesPath = '/cases'; + +// Name of a locator created by the uptime plugin. Intended for use +// by other plugins as well, so defined here to prevent cross-references. +export const uptimeOverviewLocatorID = 'uptime-overview-locator'; diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 9a45dbcbdbd64c..e502cf7fb37e0c 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -24,6 +24,7 @@ export type { ObservabilityPublicPluginsStart, }; export { enableInspectEsQueries } from '../common/ui_settings_keys'; +export { uptimeOverviewLocatorID } from '../common'; export interface ConfigSchema { unsafe: { diff --git a/x-pack/plugins/osquery/cypress/fixtures/saved_objects/hardware_monitoring.conf b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/hardware_monitoring.conf new file mode 100644 index 00000000000000..82e2e3f9b8f7a6 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/hardware_monitoring.conf @@ -0,0 +1,124 @@ +{ + "queries": { + "acpi_tables": { + "query": "select * from acpi_tables;", + "interval": 86400, + "platform": "posix", + "version": "1.3.0", + "description": "General reporting and heuristics monitoring." + }, + "cpuid": { + "query": "select feature, value, output_register, output_bit, input_eax from cpuid;", + "interval": 86400, + "version": "1.0.4", + "description": "General reporting and heuristics monitoring." + }, + "smbios_tables": { + "query": "select * from smbios_tables;", + "interval": 86400, + "platform": "posix", + "version": "1.3.0", + "description": "General reporting and heuristics monitoring." + }, + "nvram": { + "query": "select * from nvram where name not in ('backlight-level', 'SystemAudioVolumeDB', 'SystemAudioVolume');", + "interval": 7200, + "platform": "darwin", + "version": "1.0.2", + "description": "Report on crashes, alternate boots, and boot arguments." + }, + "kernel_info": { + "query": "select * from kernel_info join hash using (path);", + "interval": 7200, + "version": "1.4.0", + "description": "Report the booted kernel, potential arguments, and the device." + }, + "pci_devices": { + "query": "select * from pci_devices;", + "interval": 7200, + "platform": "posix", + "version": "1.0.4", + "description": "Report an inventory of PCI devices. Attaches and detaches will show up in hardware_events." + }, + "fan_speeds": { + "query": "select * from fan_speed_sensors;", + "interval": 7200, + "platform": "darwin", + "version": "1.7.1", + "description": "Report current fan speeds in the target OSX system." + }, + "temperatures": { + "query": "select * from temperature_sensors;", + "interval": 7200, + "platform": "darwin", + "version": "1.7.1", + "description": "Report current machine temperatures in the target OSX system." + }, + "usb_devices": { + "query": "select * from usb_devices;", + "interval": 7200, + "platform": "posix", + "version": "1.2.0", + "description": "Report an inventory of USB devices. Attaches and detaches will show up in hardware_events." + }, + "hardware_events": { + "query" : "select * from hardware_events where path <> '' or model <> '';", + "interval" : 7200, + "platform": "posix", + "removed": false, + "version" : "1.4.5", + "description" : "Retrieves all the hardware related events in the target OSX system.", + "value" : "Determine if a third party device was attached to the system." + }, + "darwin_kernel_system_controls": { + "query": "select * from system_controls where subsystem = 'kern' and (name like '%boot%' or name like '%secure%' or name like '%single%');", + "interval": 7200, + "platform": "darwin", + "version": "1.4.3", + "description": "Double check the information reported in kernel_info and report the kernel signature." + }, + "iokit_devicetree": { + "query": "select * from iokit_devicetree;", + "interval": 86400, + "platform": "darwin", + "version": "1.3.0", + "description": "General inventory of IOKit's devices on OS X." + }, + "efi_file_hashes": { + "query": "select file.path, uid, gid, mode, 0 as atime, mtime, ctime, md5, sha1, sha256 from (select * from file where path like '/System/Library/CoreServices/%.efi' union select * from file where path like '/System/Library/LaunchDaemons/com.apple%efi%') file join hash using (path);", + "interval": 7200, + "removed": false, + "version": "1.6.1", + "platform": "darwin", + "description": "Hash files related to EFI platform updates and EFI bootloaders on primary boot partition. This does not hash bootloaders on the EFI/boot partition." + }, + "kernel_extensions": { + "query" : "select * from kernel_extensions;", + "interval" : "7200", + "platform" : "darwin", + "version" : "1.4.5", + "description" : "Retrieves all the information about the current kernel extensions for the target OSX system." + }, + "kernel_modules": { + "query" : "select * from kernel_modules;", + "interval" : "7200", + "platform" : "linux", + "version" : "1.4.5", + "description" : "Retrieves all the information for the current kernel modules in the target Linux system." + }, + "windows_drivers": { + "query" : "select * from drivers;", + "interval" : "7200", + "platform" : "windows", + "version" : "2.2.0", + "description" : "Retrieves all the information for the current windows drivers in the target Windows system." + }, + "device_nodes": { + "query": "select file.path, uid, gid, mode, 0 as atime, mtime, ctime, block_size, type from file where directory = '/dev/';", + "interval": "7200", + "platform": "posix", + "version": "1.6.0", + "description": "Inventory all 'device' nodes in /dev/." + } + } +} diff --git a/x-pack/plugins/osquery/cypress/fixtures/saved_objects/hardware_monitoring.ndjson b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/hardware_monitoring.ndjson new file mode 100644 index 00000000000000..1d420cf9482087 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/hardware_monitoring.ndjson @@ -0,0 +1 @@ +{"attributes":{"created_at":"2021-12-29T09:23:21.137Z","created_by":"elastic","enabled":true,"name":"hardware-monitoring","queries":[{"id":"acpi_tables","interval":86400,"platform":"darwin,linux","query":"select * from acpi_tables;","version":"1.3.0"},{"id":"cpuid","interval":86400,"query":"select feature, value, output_register, output_bit, input_eax from cpuid;","version":"1.0.4"},{"id":"smbios_tables","interval":86400,"platform":"darwin,linux","query":"select * from smbios_tables;","version":"1.3.0"},{"id":"nvram","interval":7200,"platform":"darwin","query":"select * from nvram where name not in ('backlight-level', 'SystemAudioVolumeDB', 'SystemAudioVolume');","version":"1.0.2"},{"id":"kernel_info","interval":7200,"query":"select * from kernel_info join hash using (path);","version":"1.4.0"},{"id":"pci_devices","interval":7200,"platform":"darwin,linux","query":"select * from pci_devices;","version":"1.0.4"},{"id":"fan_speeds","interval":7200,"platform":"darwin","query":"select * from fan_speed_sensors;","version":"1.7.1"},{"id":"temperatures","interval":7200,"platform":"darwin","query":"select * from temperature_sensors;","version":"1.7.1"},{"id":"usb_devices","interval":7200,"platform":"darwin,linux","query":"select * from usb_devices;","version":"1.2.0"},{"id":"hardware_events","interval":7200,"platform":"darwin,linux","query":"select * from hardware_events where path <> '' or model <> '';","version":"1.4.5"},{"id":"darwin_kernel_system_controls","interval":7200,"platform":"darwin","query":"select * from system_controls where subsystem = 'kern' and (name like '%boot%' or name like '%secure%' or name like '%single%');","version":"1.4.3"},{"id":"iokit_devicetree","interval":86400,"platform":"darwin","query":"select * from iokit_devicetree;","version":"1.3.0"},{"id":"efi_file_hashes","interval":7200,"platform":"darwin","query":"select file.path, uid, gid, mode, 0 as atime, mtime, ctime, md5, sha1, sha256 from (select * from file where path like '/System/Library/CoreServices/%.efi' union select * from file where path like '/System/Library/LaunchDaemons/com.apple%efi%') file join hash using (path);","version":"1.6.1"},{"id":"kernel_extensions","interval":7200,"platform":"darwin","query":"select * from kernel_extensions;","version":"1.4.5"},{"id":"kernel_modules","interval":7200,"platform":"linux","query":"select * from kernel_modules;","version":"1.4.5"},{"id":"windows_drivers","interval":7200,"platform":"windows","query":"select * from drivers;","version":"2.2.0"},{"id":"device_nodes","interval":7200,"platform":"darwin,linux","query":"select file.path, uid, gid, mode, 0 as atime, mtime, ctime, block_size, type from file where directory = '/dev/';","version":"1.6.0"}],"updated_at":"2021-12-29T09:23:21.137Z","updated_by":"elastic"},"coreMigrationVersion":"8.1.0","id":"f70e1920-6888-11ec-9276-97ce5eb54433","references":[],"type":"osquery-pack","updated_at":"2021-12-29T09:23:21.147Z","version":"WzI4NDMxLDJd"} diff --git a/x-pack/plugins/osquery/cypress/fixtures/saved_objects/saved_query.ndjson b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/saved_query.ndjson index 2f9dd45dae6201..b29c4e28e731d6 100644 --- a/x-pack/plugins/osquery/cypress/fixtures/saved_objects/saved_query.ndjson +++ b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/saved_query.ndjson @@ -9,12 +9,6 @@ "value": { "field": "hours" } - }, - { - "key": "message", - "value": { - "field": "seconds" - } } ], "id": "Saved-Query-Id", diff --git a/x-pack/plugins/osquery/cypress/integration/superuser/add_integration.spec.ts b/x-pack/plugins/osquery/cypress/integration/superuser/add_integration.spec.ts index 7b117b7cd5ff3b..4f9fb4304fd284 100644 --- a/x-pack/plugins/osquery/cypress/integration/superuser/add_integration.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/superuser/add_integration.spec.ts @@ -9,14 +9,61 @@ import { FLEET_AGENT_POLICIES } from '../../tasks/navigation'; import { addIntegration } from '../../tasks/integrations'; import { login } from '../../tasks/login'; +// import { findAndClickButton, findFormFieldByRowsLabelAndType } from '../../tasks/live_query'; +import { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver'; describe('Super User - Add Integration', () => { const integration = 'Osquery Manager'; + before(() => { + runKbnArchiverScript(ArchiverMethod.LOAD, 'saved_query'); + }); beforeEach(() => { login(); }); - it('should display Osquery integration in the Policies list once installed ', () => { + after(() => { + runKbnArchiverScript(ArchiverMethod.UNLOAD, 'saved_query'); + }); + + // it('should add the old integration and be able to upgrade it', () => { + // cy.visit(OLD_OSQUERY_MANAGER); + // cy.contains(integration).click(); + // addIntegration(); + // cy.contains('osquery_manager-1'); + // cy.visit('app/fleet/policies'); + // cy.contains(/^Default Fleet Server policy$/).click(); + // cy.contains('Actions').click(); + // cy.contains('View policy').click(); + // cy.contains('name: osquery_manager-1'); + // cy.contains(`version: 0.7.4`); + // cy.contains('Close').click(); + // cy.contains(/^Osquery Manager$/).click(); + // cy.contains(/^Settings$/).click(); + // cy.contains(/^Upgrade to latest version$/).click(); + // closeModalIfVisible(); + // cy.contains('Updated Osquery Manager and upgraded policies', { timeout: 60000 }); + // cy.visit('app/fleet/policies'); + // cy.contains(/^Default Fleet Server policy$/).click(); + // cy.contains('Actions').click(); + // cy.contains('View policy').click(); + // cy.contains('name: osquery_manager-1'); + // cy.contains(`version: 0.8.1`); + // cy.visit('app/integrations/detail/osquery_manager/policies'); + // cy.contains('Loading integration policies').should('exist'); + // cy.contains('Loading integration policies').should('not.exist'); + // cy.getBySel('integrationPolicyTable') + // .get('.euiTableRow', { timeout: 60000 }) + // .should('have.lengthOf.above', 0); + // cy.get('.euiTableCellContent').get('.euiPopover__anchor').get(`[aria-label="Open"]`).click(); + // cy.contains(/^Delete integration$/).click(); + // closeModalIfVisible(); + // cy.contains(/^Settings$/).click(); + // cy.contains(/^Uninstall Osquery Manager$/).click(); + // closeModalIfVisible(); + // cy.contains(/^Successfully uninstalled Osquery Manager$/); + // }); + + it('add integration', () => { cy.visit(FLEET_AGENT_POLICIES); cy.contains('Default Fleet Server policy').click(); cy.contains('Add integration').click(); @@ -24,4 +71,53 @@ describe('Super User - Add Integration', () => { addIntegration(); cy.contains('osquery_manager-'); }); + // it('should have integration and packs copied when upgrading integration', () => { + // const packageName = 'osquery_manager'; + // const oldVersion = '0.7.4'; + // const newVersion = '0.8.1'; + // + // cy.visit(`app/integrations/detail/${packageName}-${oldVersion}/overview`); + // cy.contains('Add Osquery Manager').click(); + // cy.contains('Save and continue').click(); + // cy.contains('Add Elastic Agent later').click(); + // cy.contains('Upgrade'); + // cy.contains('Default policy').click(); + // cy.get('tr') + // .should('contain', 'osquery_manager-2') + // .and('contain', 'Osquery Manager') + // .and('contain', `v${oldVersion}`); + // cy.contains('Actions').click(); + // cy.contains('View policy').click(); + // cy.contains('name: osquery_manager-2'); + // cy.contains(`version: ${oldVersion}`); + // cy.contains('Close').click(); + // navigateTo('app/osquery/packs'); + // findAndClickButton('Add pack'); + // findFormFieldByRowsLabelAndType('Name', 'Integration'); + // findFormFieldByRowsLabelAndType('Scheduled agent policies (optional)', '{downArrow} {enter}'); + // findAndClickButton('Add query'); + // cy.react('EuiComboBox', { props: { placeholder: 'Search for saved queries' } }) + // .click() + // .type('{downArrow} {enter}'); + // cy.contains(/^Save$/).click(); + // cy.contains(/^Save pack$/).click(); + // cy.visit('app/fleet/policies'); + // cy.contains('Default policy').click(); + // cy.contains('Upgrade').click(); + // cy.contains(/^Advanced$/).click(); + // cy.contains('"Integration":'); + // cy.contains(/^Upgrade integration$/).click(); + // cy.contains(/^osquery_manager-2$/).click(); + // cy.contains(/^Advanced$/).click(); + // cy.contains('"Integration":'); + // cy.contains('Cancel').click(); + // cy.get('tr') + // .should('contain', 'osquery_manager-2') + // .and('contain', 'Osquery Manager') + // .and('contain', `v${newVersion}`); + // cy.contains('Actions').click(); + // cy.contains('View policy').click(); + // cy.contains('name: osquery_manager-2'); + // cy.contains(`version: ${newVersion}`); + // }); }); diff --git a/x-pack/plugins/osquery/cypress/integration/superuser/delete_all_ecs_mappings.spec.ts b/x-pack/plugins/osquery/cypress/integration/superuser/delete_all_ecs_mappings.spec.ts new file mode 100644 index 00000000000000..689450d8838eee --- /dev/null +++ b/x-pack/plugins/osquery/cypress/integration/superuser/delete_all_ecs_mappings.spec.ts @@ -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 { navigateTo } from '../../tasks/navigation'; +import { login } from '../../tasks/login'; +import { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver'; + +describe('SuperUser - Delete ECS Mappings', () => { + const SAVED_QUERY_ID = 'Saved-Query-Id'; + + before(() => { + runKbnArchiverScript(ArchiverMethod.LOAD, 'saved_query'); + }); + beforeEach(() => { + login(); + navigateTo('/app/osquery/saved_queries'); + }); + + after(() => { + runKbnArchiverScript(ArchiverMethod.UNLOAD, 'saved_query'); + }); + + it('to click the edit button and edit pack', () => { + cy.react('CustomItemAction', { + props: { index: 1, item: { attributes: { id: SAVED_QUERY_ID } } }, + }).click(); + cy.contains('Custom key/value pairs. e.g. {"application":"foo-bar","env":"production"}').should( + 'exist' + ); + cy.contains('Hours of uptime').should('exist'); + cy.react('EuiButtonIcon', { props: { id: 'labels-trash' } }).click(); + cy.react('EuiButton').contains('Update query').click(); + cy.wait(1000); + + cy.react('CustomItemAction', { + props: { index: 1, item: { attributes: { id: SAVED_QUERY_ID } } }, + }).click(); + cy.contains('Custom key/value pairs. e.g. {"application":"foo-bar","env":"production"}').should( + 'not.exist' + ); + cy.contains('Hours of uptime').should('not.exist'); + }); +}); diff --git a/x-pack/plugins/osquery/cypress/integration/superuser/live_query.spec.ts b/x-pack/plugins/osquery/cypress/integration/superuser/live_query.spec.ts index dde93b391d12b6..7006e0a0b76277 100644 --- a/x-pack/plugins/osquery/cypress/integration/superuser/live_query.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/superuser/live_query.spec.ts @@ -30,10 +30,10 @@ describe('Super User - Live Query', () => { checkResults(); cy.react('EuiDataGridHeaderCellWrapper', { - props: { id: 'osquery.days', index: 1 }, + props: { id: 'osquery.days.number', index: 1 }, }); cy.react('EuiDataGridHeaderCellWrapper', { - props: { id: 'osquery.hours', index: 2 }, + props: { id: 'osquery.hours.number', index: 2 }, }); cy.react('EuiAccordion', { props: { buttonContent: 'Advanced' } }).click(); @@ -46,7 +46,7 @@ describe('Super User - Live Query', () => { props: { id: 'message', index: 1 }, }); cy.react('EuiDataGridHeaderCellWrapper', { - props: { id: 'osquery.days', index: 2 }, + props: { id: 'osquery.days.number', index: 2 }, }).react('EuiIconIndexMapping'); }); }); diff --git a/x-pack/plugins/osquery/cypress/integration/superuser/metrics.spec.ts b/x-pack/plugins/osquery/cypress/integration/superuser/metrics.spec.ts index e1b0eec6985931..a9524a509c0a19 100644 --- a/x-pack/plugins/osquery/cypress/integration/superuser/metrics.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/superuser/metrics.spec.ts @@ -23,12 +23,12 @@ describe('Super User - Metrics', () => { }); it('should be able to run the query', () => { - cy.get('[data-test-subj="toggleNavButton"]').click(); + cy.getBySel('toggleNavButton').click(); cy.contains('Metrics').click(); cy.wait(1000); - cy.get('[data-test-subj="nodeContainer"]').click(); + cy.getBySel('nodeContainer').click(); cy.contains('Osquery').click(); inputQuery('select * from uptime;'); @@ -36,17 +36,17 @@ describe('Super User - Metrics', () => { checkResults(); }); it('should be able to run the previously saved query', () => { - cy.get('[data-test-subj="toggleNavButton"]').click(); - cy.get('[data-test-subj="collapsibleNavAppLink"').contains('Metrics').click(); + cy.getBySel('toggleNavButton').click(); + cy.getBySel('collapsibleNavAppLink').contains('Metrics').click(); cy.wait(500); - cy.get('[data-test-subj="nodeContainer"]').click(); + cy.getBySel('nodeContainer').click(); cy.contains('Osquery').click(); - cy.get('[data-test-subj="comboBoxInput"]').first().click(); + cy.getBySel('comboBoxInput').first().click(); cy.wait(500); cy.get('div[role=listBox]').should('have.lengthOf.above', 0); - cy.get('[data-test-subj="comboBoxInput"]').first().type('{downArrow}{enter}'); + cy.getBySel('comboBoxInput').first().type('{downArrow}{enter}'); submitQuery(); checkResults(); diff --git a/x-pack/plugins/osquery/cypress/integration/superuser/packs.spec.ts b/x-pack/plugins/osquery/cypress/integration/superuser/packs.spec.ts index 02af440ba0e6a1..99f1dac6208ee2 100644 --- a/x-pack/plugins/osquery/cypress/integration/superuser/packs.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/superuser/packs.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { navigateTo } from '../../tasks/navigation'; +import { FLEET_AGENT_POLICIES, navigateTo } from '../../tasks/navigation'; import { deleteAndConfirm, findAndClickButton, @@ -15,93 +15,204 @@ import { import { login } from '../../tasks/login'; import { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver'; import { preparePack } from '../../tasks/packs'; +import { addIntegration, closeModalIfVisible } from '../../tasks/integrations'; describe('SuperUser - Packs', () => { + const integration = 'Osquery Manager'; const SAVED_QUERY_ID = 'Saved-Query-Id'; const PACK_NAME = 'Pack-name'; const NEW_QUERY_NAME = 'new-query-name'; - before(() => { - runKbnArchiverScript(ArchiverMethod.LOAD, 'saved_query'); - }); - beforeEach(() => { - login(); - navigateTo('/app/osquery'); - }); + describe('Create and edit a pack', () => { + before(() => { + runKbnArchiverScript(ArchiverMethod.LOAD, 'saved_query'); + }); + beforeEach(() => { + login(); + navigateTo('/app/osquery'); + }); - after(() => { - runKbnArchiverScript(ArchiverMethod.UNLOAD, 'saved_query'); - }); + after(() => { + runKbnArchiverScript(ArchiverMethod.UNLOAD, 'saved_query'); + }); + + it('should add a pack from a saved query', () => { + cy.contains('Packs').click(); + findAndClickButton('Add pack'); + findFormFieldByRowsLabelAndType('Name', PACK_NAME); + findFormFieldByRowsLabelAndType('Description (optional)', 'Pack description'); + findFormFieldByRowsLabelAndType( + 'Scheduled agent policies (optional)', + 'Default Fleet Server policy' + ); + cy.react('List').first().click(); + findAndClickButton('Add query'); + cy.contains('Attach next query'); + cy.react('EuiComboBox', { props: { placeholder: 'Search for saved queries' } }) + .click() + .type(SAVED_QUERY_ID); + cy.react('List').first().click(); + cy.react('EuiFormRow', { props: { label: 'Interval (s)' } }) + .click() + .clear() + .type('10'); + cy.react('EuiFlyoutFooter').react('EuiButton').contains('Save').click(); + cy.react('EuiTableRow').contains(SAVED_QUERY_ID); + findAndClickButton('Save pack'); + cy.contains('Save and deploy changes'); + findAndClickButton('Save and deploy changes'); + cy.contains(PACK_NAME); + }); + + it('to click the edit button and edit pack', () => { + preparePack(PACK_NAME, SAVED_QUERY_ID); + findAndClickButton('Edit'); + cy.contains(`Edit ${PACK_NAME}`); + findAndClickButton('Add query'); + cy.contains('Attach next query'); + inputQuery('select * from uptime'); + findFormFieldByRowsLabelAndType('ID', NEW_QUERY_NAME); + cy.react('EuiFlyoutFooter').react('EuiButton').contains('Save').click(); + cy.react('EuiTableRow').contains(NEW_QUERY_NAME); + findAndClickButton('Update pack'); + cy.contains('Save and deploy changes'); + findAndClickButton('Save and deploy changes'); + }); + // THIS TESTS TAKES TOO LONG FOR NOW - LET ME THINK IT THROUGH + it.skip('to click the icon and visit discover', () => { + preparePack(PACK_NAME, SAVED_QUERY_ID); + cy.react('CustomItemAction', { + props: { index: 0, item: { id: SAVED_QUERY_ID } }, + }).click(); + cy.getBySel('superDatePickerToggleQuickMenuButton').click(); + cy.getBySel('superDatePickerToggleRefreshButton').click(); + cy.getBySel('superDatePickerRefreshIntervalInput').clear().type('10'); + cy.get('button').contains('Apply').click(); + cy.getBySel('discoverDocTable', { timeout: 60000 }).contains( + `pack_${PACK_NAME}_${SAVED_QUERY_ID}` + ); + }); + it('by clicking in Lens button', () => { + let lensUrl = ''; + cy.window().then((win) => { + cy.stub(win, 'open') + .as('windowOpen') + .callsFake((url) => { + lensUrl = url; + }); + }); + preparePack(PACK_NAME, SAVED_QUERY_ID); + cy.react('CustomItemAction', { + props: { index: 1, item: { id: SAVED_QUERY_ID } }, + }).click(); + cy.window() + .its('open') + .then(() => { + cy.visit(lensUrl); + }); + cy.getBySel('lnsWorkspace'); + cy.getBySel('breadcrumbs').contains(`Action pack_${PACK_NAME}_${SAVED_QUERY_ID} results`); + }); + + // strange behaviour with modal + it('activate and deactive pack', () => { + cy.contains('Packs').click(); + cy.react('ActiveStateSwitchComponent', { + props: { item: { attributes: { name: PACK_NAME } } }, + }).click(); + cy.contains(`Successfully deactivated "${PACK_NAME}" pack`).should('not.exist'); + cy.contains(`Successfully deactivated "${PACK_NAME}" pack`).should('exist'); + cy.react('ActiveStateSwitchComponent', { + props: { item: { attributes: { name: PACK_NAME } } }, + }).click(); + cy.getBySel('confirmModalConfirmButton').click(); + cy.contains(`Successfully activated "${PACK_NAME}" pack`).should('not.exist'); + cy.contains(`Successfully activated "${PACK_NAME}" pack`).should('exist'); + }); - it('should add a pack from a saved query', () => { - cy.contains('Packs').click(); - findAndClickButton('Add pack'); - findFormFieldByRowsLabelAndType('Name', PACK_NAME); - findFormFieldByRowsLabelAndType('Description (optional)', 'Pack description'); - findFormFieldByRowsLabelAndType( - 'Scheduled agent policies (optional)', - 'Default Fleet Server policy' - ); - cy.react('List').first().click(); - findAndClickButton('Add query'); - cy.contains('Attach next query'); - cy.react('EuiComboBox', { props: { placeholder: 'Search for saved queries' } }) - .click() - .type(SAVED_QUERY_ID); - cy.react('List').first().click(); - cy.react('EuiFormRow', { props: { label: 'Interval (s)' } }) - .click() - .clear() - .type('10'); - cy.react('EuiFlyoutFooter').react('EuiButton').contains('Save').click(); - cy.react('EuiTableRow').contains(SAVED_QUERY_ID); - findAndClickButton('Save pack'); - cy.contains('Save and deploy changes'); - findAndClickButton('Save and deploy changes'); - cy.contains(PACK_NAME); + it('delete all queries in the pack', () => { + preparePack(PACK_NAME, SAVED_QUERY_ID); + cy.contains(/^Edit$/).click(); + + cy.getBySel('checkboxSelectAll').click(); + + cy.contains(/^Delete \d+ quer(y|ies)/).click(); + cy.contains(/^Update pack$/).click(); + cy.react('EuiButtonDisplay') + .contains(/^Save and deploy changes$/) + .click(); + cy.contains(`${PACK_NAME}`).click(); + cy.contains(`${PACK_NAME} details`); + cy.contains(/^No items found/); + }); + + it('to click delete button', () => { + preparePack(PACK_NAME, SAVED_QUERY_ID); + findAndClickButton('Edit'); + deleteAndConfirm('pack'); + }); }); + describe('Validate that agent is getting removed from pack if we remove agent', () => { + beforeEach(() => { + login(); + }); + const AGENT_NAME = 'PackTest'; + const REMOVING_PACK = 'removing-pack'; + it('add integration', () => { + cy.visit(FLEET_AGENT_POLICIES); + cy.contains('Create agent policy').click(); + cy.get('input[placeholder*="Choose a name"]').type(AGENT_NAME); + cy.get('.euiFlyoutFooter').contains('Create agent policy').click(); + cy.contains(`Agent policy '${AGENT_NAME}' created`); + cy.visit(FLEET_AGENT_POLICIES); + cy.contains('Default Fleet Server policy').click(); + cy.contains('Add integration').click(); + cy.contains(integration).click(); + addIntegration(AGENT_NAME); + cy.contains('Add Elastic Agent later').click(); + navigateTo('app/osquery/packs'); + findAndClickButton('Add pack'); + findFormFieldByRowsLabelAndType('Name', REMOVING_PACK); + findFormFieldByRowsLabelAndType('Scheduled agent policies (optional)', AGENT_NAME); + findAndClickButton('Save pack'); - it('to click the edit button and edit pack', () => { - preparePack(PACK_NAME, SAVED_QUERY_ID); - findAndClickButton('Edit'); - cy.contains(`Edit ${PACK_NAME}`); - findAndClickButton('Add query'); - cy.contains('Attach next query'); - inputQuery('select * from uptime'); - findFormFieldByRowsLabelAndType('ID', NEW_QUERY_NAME); - cy.react('EuiFlyoutFooter').react('EuiButton').contains('Save').click(); - cy.react('EuiTableRow').contains(NEW_QUERY_NAME); - findAndClickButton('Update pack'); - cy.contains('Save and deploy changes'); - findAndClickButton('Save and deploy changes'); + cy.getBySel('toastCloseButton').click(); + cy.contains(REMOVING_PACK).click(); + cy.contains(`${REMOVING_PACK} details`); + findAndClickButton('Edit'); + cy.react('EuiComboBoxInput', { props: { value: AGENT_NAME } }).should('exist'); + + cy.visit(FLEET_AGENT_POLICIES); + cy.contains(AGENT_NAME).click(); + cy.get('.euiTableCellContent') + .get('.euiPopover__anchor') + .get(`[aria-label="Open"]`) + .first() + .click(); + cy.contains(/^Delete integration$/).click(); + closeModalIfVisible(); + navigateTo('app/osquery/packs'); + cy.contains(REMOVING_PACK).click(); + cy.contains(`${REMOVING_PACK} details`); + findAndClickButton('Edit'); + cy.react('EuiComboBoxInput', { props: { value: '' } }).should('exist'); + }); }); - // THIS TESTS TAKES TOO LONG FOR NOW - LET ME THINK IT THROUGH - // it('to click the icon and visit discover', () => { - // preparePack(PACK_NAME, SAVED_QUERY_ID); - // cy.react('CustomItemAction', { - // props: { index: 0, item: { id: SAVED_QUERY_ID } }, - // }).click(); - // cy.get('[data-test-subj="superDatePickerToggleQuickMenuButton"').click(); - // cy.get('[data-test-subj="superDatePickerToggleRefreshButton"').click(); - // cy.get('[data-test-subj="superDatePickerRefreshIntervalInput"').clear().type('10'); - // cy.get('button').contains('Apply').click(); - // cy.get('[data-test-subj="discoverDocTable"]', { timeout: 60000 }).contains( - // `pack_${PACK_NAME}_${SAVED_QUERY_ID}` - // ); - // }); - // it('by clicking in Lens button', () => { - // preparePack(PACK_NAME, SAVED_QUERY_ID); - // cy.react('CustomItemAction', { - // props: { index: 1, item: { id: SAVED_QUERY_ID } }, - // }).click(); - // cy.get('[data-test-subj="lnsWorkspace"]'); - // cy.get('[data-test-subj="breadcrumbs"]').contains( - // `Action pack_${PACK_NAME}_${SAVED_QUERY_ID} results` - // ); - // }); - it('to click delete button', () => { - preparePack(PACK_NAME, SAVED_QUERY_ID); - findAndClickButton('Edit'); - deleteAndConfirm('pack'); + describe.skip('Remove queries from pack', () => { + const TEST_PACK = 'Test-pack'; + before(() => { + runKbnArchiverScript(ArchiverMethod.LOAD, 'hardware_monitoring'); + }); + beforeEach(() => { + login(); + navigateTo('/app/osquery'); + }); + after(() => { + runKbnArchiverScript(ArchiverMethod.UNLOAD, 'hardware_monitoring'); + }); + + it('should remove ALL queries', () => { + preparePack(TEST_PACK, SAVED_QUERY_ID); + }); }); }); diff --git a/x-pack/plugins/osquery/cypress/integration/superuser/saved_queries.spec.ts b/x-pack/plugins/osquery/cypress/integration/superuser/saved_queries.spec.ts index 146083e279d6a6..bfeb5adc11f6e0 100644 --- a/x-pack/plugins/osquery/cypress/integration/superuser/saved_queries.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/superuser/saved_queries.spec.ts @@ -26,59 +26,93 @@ describe('Super User - Saved queries', () => { navigateTo('/app/osquery'); }); - it('should save the query', () => { - cy.contains('New live query').click(); - selectAllAgents(); - inputQuery(DEFAULT_QUERY); - submitQuery(); - checkResults(); - cy.contains('Save for later').click(); - cy.contains('Save query'); - findFormFieldByRowsLabelAndType('ID', SAVED_QUERY_ID); - findFormFieldByRowsLabelAndType('Description', SAVED_QUERY_DESCRIPTION); - cy.react('EuiButtonDisplay').contains('Save').click(); - }); + it( + 'should create a new query and verify: \n ' + + '- hidden columns, full screen and sorting \n' + + '- pagination \n' + + '- query can viewed (status), edited and deleted ', + () => { + cy.contains('New live query').click(); + selectAllAgents(); + inputQuery(DEFAULT_QUERY); + submitQuery(); + checkResults(); + // enter fullscreen + cy.getBySel('dataGridFullScreenButton').trigger('mouseover'); + cy.contains(/Full screen$/).should('exist'); + cy.contains('Exit full screen').should('not.exist'); + cy.getBySel('dataGridFullScreenButton').click(); - it('should view query details in status', () => { - cy.contains('New live query'); - cy.react('ActionTableResultsButton').first().click(); - cy.wait(1000); - cy.contains(DEFAULT_QUERY); - checkResults(); - cy.react('EuiTab', { props: { id: 'status' } }).click(); - cy.wait(1000); - cy.react('EuiTableRow').should('have.lengthOf', 1); - cy.contains('Successful').siblings().contains(1); - }); + cy.getBySel('dataGridFullScreenButton').trigger('mouseover'); + cy.contains(/Full screen$/).should('not.exist'); + cy.contains('Exit full screen').should('exist'); - it('should display a previously saved query and run it', () => { - cy.contains('Saved queries').click(); - cy.contains(SAVED_QUERY_ID); - cy.react('PlayButtonComponent', { - props: { savedQuery: { attributes: { id: SAVED_QUERY_ID } } }, - }).click(); - selectAllAgents(); - submitQuery(); - }); + // hidden columns + cy.react('EuiDataGridHeaderCellWrapper', { props: { id: 'osquery.cmdline' } }).click(); + cy.contains(/Hide column$/).click(); + cy.react('EuiDataGridHeaderCellWrapper', { + props: { id: 'osquery.disk_bytes_written.number' }, + }).click(); + cy.contains(/Hide column$/).click(); + cy.contains('2 columns hidden').should('exist'); + // change pagination + cy.getBySel('pagination-button-next').click().wait(500).click(); + cy.contains('2 columns hidden').should('exist'); - it('should edit the saved query', () => { - cy.contains('Saved queries').click(); - cy.contains(SAVED_QUERY_ID); - cy.react('CustomItemAction', { - props: { index: 1, item: { attributes: { id: SAVED_QUERY_ID } } }, - }).click(); - findFormFieldByRowsLabelAndType('Description', ' Edited'); - cy.react('EuiButton').contains('Update query').click(); - cy.contains(`${SAVED_QUERY_DESCRIPTION} Edited`); - }); + cy.getBySel('dataGridFullScreenButton').trigger('mouseover'); + cy.contains(/Full screen$/).should('not.exist'); + cy.contains('Exit full screen').should('exist'); + cy.getBySel('dataGridFullScreenButton').click(); - it('should delete the saved query', () => { - cy.contains('Saved queries').click(); - cy.contains(SAVED_QUERY_ID); - cy.react('CustomItemAction', { - props: { index: 1, item: { attributes: { id: SAVED_QUERY_ID } } }, - }).click(); - deleteAndConfirm('query'); - cy.contains(SAVED_QUERY_ID); - }); + // sorting + cy.react('EuiDataGridHeaderCellWrapper', { + props: { id: 'osquery.egid' }, + }).click(); + cy.contains(/Sort A-Z$/).click(); + cy.contains('2 columns hidden').should('exist'); + cy.getBySel('dataGridFullScreenButton').trigger('mouseover'); + cy.contains(/Full screen$/).should('exist'); + + // save new query + cy.contains('Exit full screen').should('not.exist'); + cy.contains('Save for later').click(); + cy.contains('Save query'); + findFormFieldByRowsLabelAndType('ID', SAVED_QUERY_ID); + findFormFieldByRowsLabelAndType('Description (optional)', SAVED_QUERY_DESCRIPTION); + cy.react('EuiButtonDisplay').contains('Save').click(); + + // visit Status results + cy.react('EuiTab', { props: { id: 'status' } }).click(); + cy.react('EuiTableRow').should('have.lengthOf', 1); + cy.contains('Successful').siblings().contains(1); + + // play saved query + cy.contains('Saved queries').click(); + cy.contains(SAVED_QUERY_ID); + cy.react('PlayButtonComponent', { + props: { savedQuery: { attributes: { id: SAVED_QUERY_ID } } }, + }).click(); + selectAllAgents(); + submitQuery(); + + // edit saved query + cy.contains('Saved queries').click(); + cy.contains(SAVED_QUERY_ID); + cy.react('CustomItemAction', { + props: { index: 1, item: { attributes: { id: SAVED_QUERY_ID } } }, + }).click(); + findFormFieldByRowsLabelAndType('Description (optional)', ' Edited'); + cy.react('EuiButton').contains('Update query').click(); + cy.contains(`${SAVED_QUERY_DESCRIPTION} Edited`); + + // delete saved query + cy.contains(SAVED_QUERY_ID); + cy.react('CustomItemAction', { + props: { index: 1, item: { attributes: { id: SAVED_QUERY_ID } } }, + }).click(); + deleteAndConfirm('query'); + cy.contains(SAVED_QUERY_ID); + cy.contains(/^No items found/); + } + ); }); diff --git a/x-pack/plugins/osquery/cypress/support/commands.ts b/x-pack/plugins/osquery/cypress/support/commands.ts index 66f94350355712..a0f3744f992b87 100644 --- a/x-pack/plugins/osquery/cypress/support/commands.ts +++ b/x-pack/plugins/osquery/cypress/support/commands.ts @@ -30,3 +30,7 @@ // // -- This will overwrite an existing command -- // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) + +Cypress.Commands.add('getBySel', (selector, ...args) => + cy.get(`[data-test-subj=${selector}]`, ...args) +); diff --git a/x-pack/plugins/osquery/cypress/tasks/integrations.ts b/x-pack/plugins/osquery/cypress/tasks/integrations.ts index e47b4c792b1e80..673f2091760a65 100644 --- a/x-pack/plugins/osquery/cypress/tasks/integrations.ts +++ b/x-pack/plugins/osquery/cypress/tasks/integrations.ts @@ -13,17 +13,16 @@ import { DATA_COLLECTION_SETUP_STEP, } from '../screens/integrations'; -export const addIntegration = () => { +export const addIntegration = (agent = 'Default fleet') => { cy.getBySel(ADD_POLICY_BTN).click(); cy.getBySel(DATA_COLLECTION_SETUP_STEP).find('.euiLoadingSpinner').should('not.exist'); + cy.getBySel('comboBoxInput').click().type(`${agent} {downArrow} {enter}`); cy.getBySel(CREATE_PACKAGE_POLICY_SAVE_BTN).click(); // sometimes agent is assigned to default policy, sometimes not closeModalIfVisible(); - - cy.getBySel(CREATE_PACKAGE_POLICY_SAVE_BTN, { timeout: 60000 }).should('not.exist'); }; -function closeModalIfVisible() { +export function closeModalIfVisible() { cy.get('body').then(($body) => { if ($body.find(CONFIRM_MODAL_BTN_SEL).length) { cy.getBySel(CONFIRM_MODAL_BTN).click(); diff --git a/x-pack/plugins/osquery/cypress/tasks/live_query.ts b/x-pack/plugins/osquery/cypress/tasks/live_query.ts index 213f949ee84ed6..4e7bfc63c35ac5 100644 --- a/x-pack/plugins/osquery/cypress/tasks/live_query.ts +++ b/x-pack/plugins/osquery/cypress/tasks/live_query.ts @@ -7,7 +7,7 @@ import { LIVE_QUERY_EDITOR } from '../screens/live_query'; -export const DEFAULT_QUERY = 'select * from processes;'; +export const DEFAULT_QUERY = 'select * from processes, users;'; export const selectAllAgents = () => { cy.react('EuiComboBox', { props: { placeholder: 'Select agents or groups' } }).type('All agents'); @@ -22,10 +22,10 @@ export const inputQuery = (query: string) => cy.get(LIVE_QUERY_EDITOR).type(quer export const submitQuery = () => cy.contains('Submit').click(); export const checkResults = () => - cy.get('[data-test-subj="dataGridRowCell"]', { timeout: 60000 }).should('have.lengthOf.above', 0); + cy.getBySel('dataGridRowCell', { timeout: 60000 }).should('have.lengthOf.above', 0); export const typeInECSFieldInput = (text: string) => - cy.get('[data-test-subj="ECS-field-input"]').click().type(text); + cy.getBySel('ECS-field-input').click().type(text); export const typeInOsqueryFieldInput = (text: string) => cy.react('OsqueryColumnFieldComponent').first().react('ResultComboBox').click().type(text); diff --git a/x-pack/plugins/osquery/cypress/tasks/navigation.ts b/x-pack/plugins/osquery/cypress/tasks/navigation.ts index f1da34cd0fbad6..7b1505eecd698a 100644 --- a/x-pack/plugins/osquery/cypress/tasks/navigation.ts +++ b/x-pack/plugins/osquery/cypress/tasks/navigation.ts @@ -11,6 +11,7 @@ export const INTEGRATIONS = 'app/integrations#/'; export const FLEET = 'app/fleet/'; export const FLEET_AGENT_POLICIES = 'app/fleet/policies'; export const OSQUERY = 'app/osquery'; +export const OLD_OSQUERY_MANAGER = 'app/integrations/detail/osquery_manager-0.7.4/settings'; export const NEW_LIVE_QUERY = 'app/osquery/live_queries/new'; export const OSQUERY_INTEGRATION_PAGE = '/app/fleet/integrations/osquery_manager/add-integration'; export const navigateTo = (page: string, opts?: Partial) => { diff --git a/x-pack/plugins/osquery/cypress/tasks/packs.ts b/x-pack/plugins/osquery/cypress/tasks/packs.ts index 8fa680a5899a2c..3218c792772baa 100644 --- a/x-pack/plugins/osquery/cypress/tasks/packs.ts +++ b/x-pack/plugins/osquery/cypress/tasks/packs.ts @@ -9,6 +9,4 @@ export const preparePack = (packName: string, savedQueryId: string) => { cy.contains('Packs').click(); const createdPack = cy.contains(packName); createdPack.click(); - cy.waitForReact(1000); - cy.react('EuiTableRow').contains(savedQueryId); }; diff --git a/x-pack/plugins/osquery/public/action_results/action_agents_status_badges.tsx b/x-pack/plugins/osquery/public/action_results/action_agents_status_badges.tsx index 95b96ca4546109..7244c2417151ba 100644 --- a/x-pack/plugins/osquery/public/action_results/action_agents_status_badges.tsx +++ b/x-pack/plugins/osquery/public/action_results/action_agents_status_badges.tsx @@ -13,7 +13,7 @@ import { getColorForAgentStatus, getLabelForAgentStatus, } from './services/agent_status'; -import type { ActionAgentStatus } from './types'; +import { ActionAgentStatus } from './types'; export const ActionAgentsStatusBadges = memo<{ agentStatus: { [k in ActionAgentStatus]: number }; diff --git a/x-pack/plugins/osquery/public/action_results/action_agents_status_bar.tsx b/x-pack/plugins/osquery/public/action_results/action_agents_status_bar.tsx index 21866566cb7e3a..def52bf5112154 100644 --- a/x-pack/plugins/osquery/public/action_results/action_agents_status_bar.tsx +++ b/x-pack/plugins/osquery/public/action_results/action_agents_status_bar.tsx @@ -10,7 +10,7 @@ import { EuiColorPaletteDisplay } from '@elastic/eui'; import React, { useMemo } from 'react'; import { AGENT_STATUSES, getColorForAgentStatus } from './services/agent_status'; -import type { ActionAgentStatus } from './types'; +import { ActionAgentStatus } from './types'; const StyledEuiColorPaletteDisplay = styled(EuiColorPaletteDisplay)` &.osquery-action-agent-status-bar { diff --git a/x-pack/plugins/osquery/public/action_results/helpers.ts b/x-pack/plugins/osquery/public/action_results/helpers.ts deleted file mode 100644 index 171530a77299f9..00000000000000 --- a/x-pack/plugins/osquery/public/action_results/helpers.ts +++ /dev/null @@ -1,37 +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 { - PaginationInputPaginated, - FactoryQueryTypes, - StrategyResponseType, - Inspect, -} from '../../common/search_strategy'; - -export type InspectResponse = Inspect & { response: string[] }; - -export const generateTablePaginationOptions = ( - activePage: number, - limit: number -): PaginationInputPaginated => { - const cursorStart = activePage * limit; - return { - activePage, - cursorStart, - fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5, - querySize: limit, - }; -}; - -export const getInspectResponse = ( - response: StrategyResponseType, - prevResponse: InspectResponse -): InspectResponse => ({ - dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], - response: - response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response, -}); diff --git a/x-pack/plugins/osquery/public/action_results/services/agent_status.tsx b/x-pack/plugins/osquery/public/action_results/services/agent_status.tsx index 1a2c9f370bc31a..0f586d10fb6a08 100644 --- a/x-pack/plugins/osquery/public/action_results/services/agent_status.tsx +++ b/x-pack/plugins/osquery/public/action_results/services/agent_status.tsx @@ -8,7 +8,7 @@ import { euiPaletteColorBlindBehindText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import type { ActionAgentStatus } from '../types'; +import { ActionAgentStatus } from '../types'; const visColors = euiPaletteColorBlindBehindText(); const colorToHexMap = { @@ -20,15 +20,19 @@ const colorToHexMap = { danger: visColors[9], }; -export const AGENT_STATUSES: ActionAgentStatus[] = ['success', 'pending', 'failed']; +export const AGENT_STATUSES: ActionAgentStatus[] = [ + ActionAgentStatus.SUCCESS, + ActionAgentStatus.PENDING, + ActionAgentStatus.FAILED, +]; export function getColorForAgentStatus(agentStatus: ActionAgentStatus): string { switch (agentStatus) { - case 'success': + case ActionAgentStatus.SUCCESS: return colorToHexMap.success; - case 'pending': + case ActionAgentStatus.PENDING: return colorToHexMap.default; - case 'failed': + case ActionAgentStatus.FAILED: return colorToHexMap.danger; default: throw new Error(`Unsupported action agent status ${agentStatus}`); @@ -37,11 +41,11 @@ export function getColorForAgentStatus(agentStatus: ActionAgentStatus): string { export function getLabelForAgentStatus(agentStatus: ActionAgentStatus, expired: boolean): string { switch (agentStatus) { - case 'success': + case ActionAgentStatus.SUCCESS: return i18n.translate('xpack.osquery.liveQueryActionResults.summary.successfulLabelText', { defaultMessage: 'Successful', }); - case 'pending': + case ActionAgentStatus.PENDING: return expired ? i18n.translate('xpack.osquery.liveQueryActionResults.summary.expiredLabelText', { defaultMessage: 'Expired', @@ -49,7 +53,7 @@ export function getLabelForAgentStatus(agentStatus: ActionAgentStatus, expired: : i18n.translate('xpack.osquery.liveQueryActionResults.summary.pendingLabelText', { defaultMessage: 'Not yet responded', }); - case 'failed': + case ActionAgentStatus.FAILED: return i18n.translate('xpack.osquery.liveQueryActionResults.summary.failedLabelText', { defaultMessage: 'Failed', }); diff --git a/x-pack/plugins/osquery/public/action_results/types.ts b/x-pack/plugins/osquery/public/action_results/types.ts index ce9415986ba022..504626445450db 100644 --- a/x-pack/plugins/osquery/public/action_results/types.ts +++ b/x-pack/plugins/osquery/public/action_results/types.ts @@ -5,4 +5,8 @@ * 2.0. */ -export type ActionAgentStatus = 'success' | 'pending' | 'failed'; +export enum ActionAgentStatus { + SUCCESS = 'success', + PENDING = 'pending', + FAILED = 'failed', +} diff --git a/x-pack/plugins/osquery/public/action_results/use_action_results.ts b/x-pack/plugins/osquery/public/action_results/use_action_results.ts index e4b6ef14eb1e99..0d3396d7331a19 100644 --- a/x-pack/plugins/osquery/public/action_results/use_action_results.ts +++ b/x-pack/plugins/osquery/public/action_results/use_action_results.ts @@ -9,7 +9,12 @@ import { flatten, reverse, uniqBy } from 'lodash/fp'; import { useQuery } from 'react-query'; import { i18n } from '@kbn/i18n'; -import { createFilter } from '../common/helpers'; +import { + createFilter, + getInspectResponse, + InspectResponse, + generateTablePaginationOptions, +} from '../common/helpers'; import { useKibana } from '../common/lib/kibana'; import { ResultEdges, @@ -22,7 +27,6 @@ import { import { ESTermQuery } from '../../common/typed_json'; import { queryClient } from '../query_client'; -import { generateTablePaginationOptions, getInspectResponse, InspectResponse } from './helpers'; import { useErrorToast } from '../common/hooks/use_error_toast'; export interface ResultsArgs { diff --git a/x-pack/plugins/osquery/public/actions/helpers.ts b/x-pack/plugins/osquery/public/actions/helpers.ts deleted file mode 100644 index 171530a77299f9..00000000000000 --- a/x-pack/plugins/osquery/public/actions/helpers.ts +++ /dev/null @@ -1,37 +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 { - PaginationInputPaginated, - FactoryQueryTypes, - StrategyResponseType, - Inspect, -} from '../../common/search_strategy'; - -export type InspectResponse = Inspect & { response: string[] }; - -export const generateTablePaginationOptions = ( - activePage: number, - limit: number -): PaginationInputPaginated => { - const cursorStart = activePage * limit; - return { - activePage, - cursorStart, - fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5, - querySize: limit, - }; -}; - -export const getInspectResponse = ( - response: StrategyResponseType, - prevResponse: InspectResponse -): InspectResponse => ({ - dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], - response: - response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response, -}); diff --git a/x-pack/plugins/osquery/public/actions/use_action_details.ts b/x-pack/plugins/osquery/public/actions/use_action_details.ts index dfa23247045ef3..61ba6a3340bdbf 100644 --- a/x-pack/plugins/osquery/public/actions/use_action_details.ts +++ b/x-pack/plugins/osquery/public/actions/use_action_details.ts @@ -62,6 +62,7 @@ export const useActionDetails = ({ actionId, filterQuery, skip = false }: UseAct defaultMessage: 'Error while fetching action details', }), }), + refetchOnWindowFocus: false, retryDelay: 1000, } ); diff --git a/x-pack/plugins/osquery/public/actions/use_all_actions.ts b/x-pack/plugins/osquery/public/actions/use_all_actions.ts index ae872d3c1ed523..4951b3c9d8fd1c 100644 --- a/x-pack/plugins/osquery/public/actions/use_all_actions.ts +++ b/x-pack/plugins/osquery/public/actions/use_all_actions.ts @@ -8,7 +8,12 @@ import { useQuery } from 'react-query'; import { i18n } from '@kbn/i18n'; -import { createFilter } from '../common/helpers'; +import { + createFilter, + generateTablePaginationOptions, + getInspectResponse, + InspectResponse, +} from '../common/helpers'; import { useKibana } from '../common/lib/kibana'; import { ActionEdges, @@ -20,7 +25,6 @@ import { } from '../../common/search_strategy'; import { ESTermQuery } from '../../common/typed_json'; -import { generateTablePaginationOptions, getInspectResponse, InspectResponse } from './helpers'; import { useErrorToast } from '../common/hooks/use_error_toast'; export interface ActionsArgs { diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx index a4fee25dfcd9af..105518537384f1 100644 --- a/x-pack/plugins/osquery/public/agents/agents_table.tsx +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -84,6 +84,22 @@ const AgentsTableComponent: React.FC = ({ agentSelection, onCh const defaultValueInitialized = useRef(false); useEffect(() => { + const handleSelectedOptions = (selection: string[], label: string) => { + const agentOptions = find(['label', label], options); + + if (agentOptions) { + const defaultOptions = agentOptions.options?.filter((option) => { + if (option.key) { + return selection.includes(option.key); + } + }); + + if (defaultOptions?.length) { + setSelectedOptions(defaultOptions); + defaultValueInitialized.current = true; + } + } + }; if (agentSelection && !defaultValueInitialized.current && options.length) { if (agentSelection.allAgentsSelected) { const allAgentsOptions = find(['label', ALL_AGENTS_LABEL], options); @@ -95,35 +111,11 @@ const AgentsTableComponent: React.FC = ({ agentSelection, onCh } if (agentSelection.policiesSelected.length) { - const policyOptions = find(['label', AGENT_POLICY_LABEL], options); - - if (policyOptions) { - const defaultOptions = policyOptions.options?.filter((option) => - // @ts-expect-error update types - agentSelection.policiesSelected.includes(option.key) - ); - - if (defaultOptions?.length) { - setSelectedOptions(defaultOptions); - defaultValueInitialized.current = true; - } - } + handleSelectedOptions(agentSelection.policiesSelected, AGENT_POLICY_LABEL); } if (agentSelection.agents.length) { - const agentOptions = find(['label', AGENT_SELECTION_LABEL], options); - - if (agentOptions) { - const defaultOptions = agentOptions.options?.filter((option) => - // @ts-expect-error update types - agentSelection.agents.includes(option.key) - ); - - if (defaultOptions?.length) { - setSelectedOptions(defaultOptions); - defaultValueInitialized.current = true; - } - } + handleSelectedOptions(agentSelection.agents, AGENT_SELECTION_LABEL); } } }, [agentSelection, options, selectedOptions]); diff --git a/x-pack/plugins/osquery/public/agents/helpers.ts b/x-pack/plugins/osquery/public/agents/helpers.ts index 39cbbcc8907774..71a67ef1f623a3 100644 --- a/x-pack/plugins/osquery/public/agents/helpers.ts +++ b/x-pack/plugins/osquery/public/agents/helpers.ts @@ -7,12 +7,6 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { euiPaletteColorBlindBehindText } from '@elastic/eui'; -import { - PaginationInputPaginated, - FactoryQueryTypes, - StrategyResponseType, - Inspect, -} from '../../common/search_strategy'; import { AGENT_GROUP_KEY, SelectedGroups, @@ -25,8 +19,6 @@ import { GroupOption, } from './types'; -export type InspectResponse = Inspect & { response: string[] }; - export const getNumOverlapped = ( { policy = {}, platform = {} }: SelectedGroups, overlap: Overlap @@ -158,26 +150,3 @@ export const generateAgentSelection = (selection: GroupOption[]) => { } return { newAgentSelection, selectedGroups, selectedAgents }; }; - -export const generateTablePaginationOptions = ( - activePage: number, - limit: number -): PaginationInputPaginated => { - const cursorStart = activePage * limit; - return { - activePage, - cursorStart, - fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5, - querySize: limit, - }; -}; - -export const getInspectResponse = ( - response: StrategyResponseType, - prevResponse?: InspectResponse -): InspectResponse => ({ - dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], - // @ts-expect-error update types - response: - response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response, -}); diff --git a/x-pack/plugins/osquery/public/agents/use_agent_groups.ts b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts index 4163861166acf8..6821217c30cbfd 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_groups.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts @@ -16,7 +16,8 @@ import { AgentsStrategyResponse, } from '../../common/search_strategy'; -import { generateTablePaginationOptions, processAggregations } from './helpers'; +import { processAggregations } from './helpers'; +import { generateTablePaginationOptions } from '../common/helpers'; import { Overlap, Group } from './types'; import { useErrorToast } from '../common/hooks/use_error_toast'; diff --git a/x-pack/plugins/osquery/public/common/helpers.ts b/x-pack/plugins/osquery/public/common/helpers.ts index adac59211dee34..4f9efbe839ffd1 100644 --- a/x-pack/plugins/osquery/public/common/helpers.ts +++ b/x-pack/plugins/osquery/public/common/helpers.ts @@ -7,7 +7,38 @@ import { isString } from 'lodash/fp'; +import { + PaginationInputPaginated, + FactoryQueryTypes, + StrategyResponseType, + Inspect, +} from '../../common/search_strategy'; + import { ESQuery } from '../../common/typed_json'; export const createFilter = (filterQuery: ESQuery | string | undefined) => isString(filterQuery) ? filterQuery : JSON.stringify(filterQuery); + +export type InspectResponse = Inspect & { response: string[] }; + +export const generateTablePaginationOptions = ( + activePage: number, + limit: number +): PaginationInputPaginated => { + const cursorStart = activePage * limit; + return { + activePage, + cursorStart, + fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5, + querySize: limit, + }; +}; + +export const getInspectResponse = ( + response: StrategyResponseType, + prevResponse: InspectResponse +): InspectResponse => ({ + dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], + response: + response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response, +}); diff --git a/x-pack/plugins/osquery/public/common/hooks/use_breadcrumbs.tsx b/x-pack/plugins/osquery/public/common/hooks/use_breadcrumbs.tsx index 92660943b11707..6bef0ee38c24eb 100644 --- a/x-pack/plugins/osquery/public/common/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/osquery/public/common/hooks/use_breadcrumbs.tsx @@ -156,12 +156,27 @@ const breadcrumbGetters: { }; export function useBreadcrumbs(page: Page, values: DynamicPagePathValues = {}) { - const { chrome, http } = useKibana().services; + const { chrome, http, application } = useKibana().services; + const breadcrumbs: ChromeBreadcrumb[] = - breadcrumbGetters[page]?.(values).map((breadcrumb) => ({ - ...breadcrumb, - href: breadcrumb.href ? http.basePath.prepend(`${BASE_PATH}${breadcrumb.href}`) : undefined, - })) || []; + breadcrumbGetters[page]?.(values).map((breadcrumb) => { + const href = breadcrumb.href + ? http.basePath.prepend(`${BASE_PATH}${breadcrumb.href}`) + : undefined; + return { + ...breadcrumb, + href, + onClick: href + ? (ev: React.MouseEvent) => { + if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) { + return; + } + ev.preventDefault(); + application.navigateToUrl(href); + } + : undefined, + }; + }) || []; const docTitle: string[] = [...breadcrumbs] .reverse() .map((breadcrumb) => breadcrumb.text as string); diff --git a/x-pack/plugins/osquery/public/components/app.tsx b/x-pack/plugins/osquery/public/components/app.tsx index 9be63b33394ada..0b44739a7f2ed6 100644 --- a/x-pack/plugins/osquery/public/components/app.tsx +++ b/x-pack/plugins/osquery/public/components/app.tsx @@ -5,33 +5,16 @@ * 2.0. */ -/* eslint-disable react-hooks/rules-of-hooks */ +import React from 'react'; +import { EuiLoadingElastic, EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiTabs, - EuiTab, - EuiLoadingElastic, - EuiPage, - EuiPageBody, - EuiPageContent, -} from '@elastic/eui'; -import { useLocation } from 'react-router-dom'; - -import { Container, Nav, Wrapper } from './layouts'; +import { Container, Wrapper } from './layouts'; import { OsqueryAppRoutes } from '../routes'; -import { useRouterNavigate } from '../common/lib/kibana'; -import { ManageIntegrationLink } from './manage_integration_link'; import { useOsqueryIntegrationStatus } from '../common/hooks'; import { OsqueryAppEmptyState } from './empty_state'; +import { MainNavigation } from './main_navigation'; const OsqueryAppComponent = () => { - const location = useLocation(); - const section = useMemo(() => location.pathname.split('/')[1] ?? 'overview', [location.pathname]); const { data: osqueryIntegration, isFetched } = useOsqueryIntegrationStatus(); if (!isFetched) { @@ -59,55 +42,7 @@ const OsqueryAppComponent = () => { return ( - + diff --git a/x-pack/plugins/osquery/public/components/main_navigation.tsx b/x-pack/plugins/osquery/public/components/main_navigation.tsx new file mode 100644 index 00000000000000..73b6435fd8f338 --- /dev/null +++ b/x-pack/plugins/osquery/public/components/main_navigation.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, { useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiTab, EuiTabs } from '@elastic/eui'; +import { useLocation } from 'react-router-dom'; +import { useRouterNavigate } from '../common/lib/kibana'; +import { ManageIntegrationLink } from './manage_integration_link'; +import { Nav } from './layouts'; + +enum Section { + LiveQueries = 'live_queries', + Packs = 'packs', + SavedQueries = 'saved_queries', +} + +export const MainNavigation = () => { + const location = useLocation(); + const section = useMemo(() => location.pathname.split('/')[1] ?? 'overview', [location.pathname]); + return ( + + ); +}; diff --git a/x-pack/plugins/osquery/public/fleet_integration/navigation_buttons.tsx b/x-pack/plugins/osquery/public/fleet_integration/navigation_buttons.tsx index 4bcc9d9ebf2a10..4da470270de76b 100644 --- a/x-pack/plugins/osquery/public/fleet_integration/navigation_buttons.tsx +++ b/x-pack/plugins/osquery/public/fleet_integration/navigation_buttons.tsx @@ -30,7 +30,7 @@ const NavigationButtonsComponent: React.FC = ({ getUrlForApp(PLUGIN_ID, { path: agentPolicyId ? `/live_queries/new?agentPolicyId=${agentPolicyId}` - : ' `/live_queries/new', + : '/live_queries/new', }), [agentPolicyId, getUrlForApp] ); @@ -42,7 +42,7 @@ const NavigationButtonsComponent: React.FC = ({ navigateToApp(PLUGIN_ID, { path: agentPolicyId ? `/live_queries/new?agentPolicyId=${agentPolicyId}` - : ' `/live_queries/new', + : '/live_queries/new', }); } }, 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 39975cb65ce2b1..1b7b87fe180bff 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 @@ -311,25 +311,26 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo< /* From 0.6.0 we don't provide an input template, so we have to set it here */ if (satisfies(newPolicy?.package?.version, '>=0.6.0')) { const updatedPolicy = produce(newPolicy, (draft) => { - if (!draft.inputs.length) { + if (editMode && policy?.inputs.length) { + set(draft, 'inputs', policy.inputs); + } else { set(draft, 'inputs[0]', { type: 'osquery', enabled: true, streams: [], policy_template: 'osquery_manager', }); - } else { - if (!draft.inputs[0].type) { - set(draft, 'inputs[0].type', 'osquery'); - } - if (!draft.inputs[0].policy_template) { - set(draft, 'inputs[0].policy_template', 'osquery_manager'); - } - if (!draft.inputs[0].enabled) { - set(draft, 'inputs[0].enabled', true); - } } + return draft; }); + + if (updatedPolicy?.inputs[0].config) { + setFieldValue( + 'config', + JSON.stringify(updatedPolicy?.inputs[0].config.osquery.value, null, 2) + ); + } + onChange({ isValid: true, updatedPolicy, diff --git a/x-pack/plugins/osquery/public/live_queries/index.tsx b/x-pack/plugins/osquery/public/live_queries/index.tsx index 2336a1de1d4a05..bf2186c1a3e509 100644 --- a/x-pack/plugins/osquery/public/live_queries/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/index.tsx @@ -45,7 +45,7 @@ const LiveQueryComponent: React.FC = ({ formType, enabled, }) => { - const { data: hasActionResultsPrivileges, isFetched } = useActionResultsPrivileges(); + const { data: hasActionResultsPrivileges, isLoading } = useActionResultsPrivileges(); const defaultValue = useMemo(() => { if (agentId || agentPolicyIds?.length || query?.length) { @@ -70,7 +70,7 @@ const LiveQueryComponent: React.FC = ({ return undefined; }, [agentId, agentIds, agentPolicyIds, ecs_mapping, query, savedQueryId]); - if (!isFetched) { + if (isLoading) { return ; } diff --git a/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx b/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx index 631fa62e12bfbe..2bfe75e2833aaf 100644 --- a/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx +++ b/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx @@ -391,13 +391,13 @@ const ScheduledQueryLastResults: React.FC = ({ toggleErrors, expanded, }) => { - const { data: lastResultsData, isFetched } = usePackQueryLastResults({ + const { data: lastResultsData, isLoading } = usePackQueryLastResults({ actionId, interval, logsDataView, }); - const { data: errorsData, isFetched: errorsFetched } = usePackQueryErrors({ + const { data: errorsData, isLoading: errorsLoading } = usePackQueryErrors({ actionId, interval, logsDataView, @@ -408,7 +408,7 @@ const ScheduledQueryLastResults: React.FC = ({ [queryId, interval, toggleErrors] ); - if (!isFetched || !errorsFetched) { + if (isLoading || errorsLoading) { return ; } diff --git a/x-pack/plugins/osquery/public/packs/packs_table.tsx b/x-pack/plugins/osquery/public/packs/packs_table.tsx index 9bea07b7c234ca..f8599cc1fc51e2 100644 --- a/x-pack/plugins/osquery/public/packs/packs_table.tsx +++ b/x-pack/plugins/osquery/public/packs/packs_table.tsx @@ -13,17 +13,18 @@ import { EuiBasicTableColumn, EuiLink, EuiToolTip, + EuiLoadingContent, } from '@elastic/eui'; import moment from 'moment-timezone'; import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; -import { PackagePolicy } from '../../../fleet/common'; import { useRouterNavigate } from '../common/lib/kibana'; import { usePacks } from './use_packs'; import { ActiveStateSwitch } from './active_state_switch'; import { AgentsPolicyLink } from '../agent_policies/agents_policy_link'; +import { PackSavedObject } from './types'; const UpdatedBy = styled.span` white-space: nowrap; @@ -82,7 +83,7 @@ export const AgentPoliciesPopover = ({ agentPolicyIds }: { agentPolicyIds: strin }; const PacksTableComponent = () => { - const { data } = usePacks({}); + const { data, isLoading } = usePacks({}); const renderAgentPolicy = useCallback( (agentPolicyIds) => , @@ -112,15 +113,14 @@ const PacksTableComponent = () => { ); }, []); - // @ts-expect-error update types - const columns: Array> = useMemo( + const columns: Array> = useMemo( () => [ { field: 'attributes.name', name: i18n.translate('xpack.osquery.packs.table.nameColumnTitle', { defaultMessage: 'Name', }), - sortable: true, + sortable: (item) => item.attributes.name.toLowerCase(), render: renderName, }, { @@ -178,8 +178,12 @@ const PacksTableComponent = () => { [] ); + if (isLoading) { + return ; + } + return ( - + // eslint-disable-next-line react-perf/jsx-no-new-array-as-prop items={data?.saved_objects ?? []} columns={columns} diff --git a/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx b/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx index 77acd3d0239cf7..6cbf4dc84635eb 100644 --- a/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx +++ b/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx @@ -763,6 +763,7 @@ export const ECSMappingEditorForm = forwardRef) => ({ description: { type: FIELD_TYPES.TEXT, label: i18n.translate('xpack.osquery.pack.queryFlyoutForm.descriptionFieldLabel', { - defaultMessage: 'Description', + defaultMessage: 'Description (optional)', }), validations: [], }, diff --git a/x-pack/plugins/osquery/public/packs/types.ts b/x-pack/plugins/osquery/public/packs/types.ts index fce37ec495faa1..30cae97b006bb2 100644 --- a/x-pack/plugins/osquery/public/packs/types.ts +++ b/x-pack/plugins/osquery/public/packs/types.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { SavedObject } from 'kibana/server'; export interface IQueryPayload { attributes?: { @@ -11,3 +12,14 @@ export interface IQueryPayload { id: string; }; } + +export type PackSavedObject = SavedObject<{ + name: string; + description: string | undefined; + queries: Array>; + enabled: boolean | undefined; + created_at: string; + created_by: string | undefined; + updated_at: string; + updated_by: string | undefined; +}>; diff --git a/x-pack/plugins/osquery/public/results/helpers.ts b/x-pack/plugins/osquery/public/results/helpers.ts deleted file mode 100644 index 171530a77299f9..00000000000000 --- a/x-pack/plugins/osquery/public/results/helpers.ts +++ /dev/null @@ -1,37 +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 { - PaginationInputPaginated, - FactoryQueryTypes, - StrategyResponseType, - Inspect, -} from '../../common/search_strategy'; - -export type InspectResponse = Inspect & { response: string[] }; - -export const generateTablePaginationOptions = ( - activePage: number, - limit: number -): PaginationInputPaginated => { - const cursorStart = activePage * limit; - return { - activePage, - cursorStart, - fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5, - querySize: limit, - }; -}; - -export const getInspectResponse = ( - response: StrategyResponseType, - prevResponse: InspectResponse -): InspectResponse => ({ - dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], - response: - response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response, -}); diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx index 461255533c3805..87d71bc3d17c8b 100644 --- a/x-pack/plugins/osquery/public/results/results_table.tsx +++ b/x-pack/plugins/osquery/public/results/results_table.tsx @@ -105,7 +105,11 @@ const ResultsTableComponent: React.FC = ({ ]); const [columns, setColumns] = useState([]); - const { data: allResultsData, isFetched } = useAllResults({ + const { + data: allResultsData, + isFetched, + isLoading, + } = useAllResults({ actionId, activePage: pagination.pageIndex, limit: pagination.pageSize, @@ -232,15 +236,11 @@ const ResultsTableComponent: React.FC = ({ ); useEffect(() => { - if (!allResultsData?.edges?.length) { + if (!allResultsData?.columns.length) { return; } - const fields = [ - 'agent.name', - ...ecsMappingColumns.sort(), - ...keys(allResultsData?.edges[0]?.fields || {}).sort(), - ]; + const fields = ['agent.name', ...ecsMappingColumns.sort(), ...allResultsData?.columns]; const newColumns = fields.reduce( (acc, fieldName) => { @@ -277,12 +277,15 @@ const ResultsTableComponent: React.FC = ({ if (fieldName.startsWith('osquery.')) { const displayAsText = fieldName.split('.')[1]; + const hasNumberType = fields.includes(`${fieldName}.number`); if (!seen.has(displayAsText)) { + const id = hasNumberType ? fieldName + '.number' : fieldName; data.push({ - id: fieldName, + id, displayAsText, display: getHeaderDisplay(displayAsText), defaultSortDirection: Direction.asc, + ...(hasNumberType ? { schema: 'numeric' } : {}), }); seen.add(displayAsText); } @@ -298,7 +301,8 @@ const ResultsTableComponent: React.FC = ({ !isEqual(map('id', currentColumns), map('id', newColumns)) ? newColumns : currentColumns ); setVisibleColumns(map('id', newColumns)); - }, [allResultsData?.edges, ecsMappingColumns, getHeaderDisplay]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [allResultsData?.columns.length, ecsMappingColumns, getHeaderDisplay]); const toolbarVisibility = useMemo( () => ({ @@ -347,7 +351,7 @@ const ResultsTableComponent: React.FC = ({ ); } - if (!isFetched) { + if (isLoading) { return ; } diff --git a/x-pack/plugins/osquery/public/results/use_all_results.ts b/x-pack/plugins/osquery/public/results/use_all_results.ts index bc7673dd0ffbd3..00c27f11c12aad 100644 --- a/x-pack/plugins/osquery/public/results/use_all_results.ts +++ b/x-pack/plugins/osquery/public/results/use_all_results.ts @@ -8,7 +8,12 @@ import { useQuery } from 'react-query'; import { i18n } from '@kbn/i18n'; -import { createFilter } from '../common/helpers'; +import { + createFilter, + generateTablePaginationOptions, + getInspectResponse, + InspectResponse, +} from '../common/helpers'; import { useKibana } from '../common/lib/kibana'; import { ResultEdges, @@ -20,7 +25,6 @@ import { } from '../../common/search_strategy'; import { ESTermQuery } from '../../common/typed_json'; -import { generateTablePaginationOptions, getInspectResponse, InspectResponse } from './helpers'; import { useErrorToast } from '../common/hooks/use_error_toast'; export interface ResultsArgs { @@ -78,10 +82,12 @@ export const useAllResults = ({ return { ...responseData, + columns: Object.keys(responseData.edges[0].fields || {}).sort(), inspect: getInspectResponse(responseData, {} as InspectResponse), }; }, { + keepPreviousData: true, refetchInterval: isLive ? 5000 : false, enabled: !skip, onSuccess: () => setErrorToast(), diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx index 0bb162173adc19..f16e32a62cb4f0 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx @@ -13,6 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiText, + EuiBasicTableColumn, } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; @@ -28,10 +29,12 @@ import { useSavedQueries } from '../../../saved_queries/use_saved_queries'; type SavedQuerySO = SavedObject<{ name: string; + id: string; query: string; ecs_mapping: ECSMapping; updated_at: string; }>; + interface PlayButtonProps { disabled: boolean; savedQuery: SavedQuerySO; @@ -141,14 +144,14 @@ const SavedQueriesPageComponent = () => { return updatedAt ? `${moment(updatedAt).fromNow()}${updatedBy}` : '-'; }, []); - const columns = useMemo( + const columns: Array> = useMemo( () => [ { field: 'attributes.id', name: i18n.translate('xpack.osquery.savedQueries.table.queryIdColumnTitle', { defaultMessage: 'Query ID', }), - sortable: true, + sortable: (item) => item.attributes.id.toLowerCase(), truncateText: true, }, { @@ -156,7 +159,6 @@ const SavedQueriesPageComponent = () => { name: i18n.translate('xpack.osquery.savedQueries.table.descriptionColumnTitle', { defaultMessage: 'Description', }), - sortable: true, truncateText: true, }, { @@ -172,7 +174,7 @@ const SavedQueriesPageComponent = () => { name: i18n.translate('xpack.osquery.savedQueries.table.updatedAtColumnTitle', { defaultMessage: 'Last updated at', }), - sortable: (item: SavedQuerySO) => + sortable: (item) => item.attributes.updated_at ? Date.parse(item.attributes.updated_at) : 0, truncateText: true, render: renderUpdatedAt, diff --git a/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx b/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx index 3fd2275477ebfa..b3e0cab60851ea 100644 --- a/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx +++ b/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx @@ -54,7 +54,7 @@ export const useSavedQueryForm = ({ try { await handleSubmit({ ...formData, - ...(isEmpty(ecsFieldValue) ? {} : { ecs_mapping: ecsFieldValue }), + ecs_mapping: ecsFieldValue, }); // eslint-disable-next-line no-empty } catch (e) {} diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx index 7bc54b44de7759..3861784120e0c4 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx @@ -30,7 +30,11 @@ interface OsqueryActionProps { const OsqueryActionComponent: React.FC = ({ metadata }) => { const permissions = useKibana().services.application.capabilities.osquery; const agentId = metadata?.info?.agent?.id ?? undefined; - const { data: agentData, isFetched: agentFetched } = useAgentDetails({ + const { + data: agentData, + isFetched: agentFetched, + isLoading, + } = useAgentDetails({ agentId, silent: true, skip: !agentId, @@ -72,7 +76,7 @@ const OsqueryActionComponent: React.FC = ({ metadata }) => { ); } - if (!agentFetched) { + if (isLoading) { return ; } diff --git a/x-pack/plugins/osquery/server/lib/fleet_integration.ts b/x-pack/plugins/osquery/server/lib/fleet_integration.ts new file mode 100644 index 00000000000000..87d48c95648bbe --- /dev/null +++ b/x-pack/plugins/osquery/server/lib/fleet_integration.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 { SavedObjectReference, SavedObjectsClient } from 'kibana/server'; +import { filter, map } from 'lodash'; +import { packSavedObjectType } from '../../common/types'; +import { PostPackagePolicyDeleteCallback } from '../../../fleet/server'; +import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common'; +import { OSQUERY_INTEGRATION_NAME } from '../../common'; + +export const getPackagePolicyDeleteCallback = + (packsClient: SavedObjectsClient): PostPackagePolicyDeleteCallback => + async (deletedPackagePolicy) => { + const deletedOsqueryManagerPolicies = filter(deletedPackagePolicy, [ + 'package.name', + OSQUERY_INTEGRATION_NAME, + ]); + await Promise.all( + map(deletedOsqueryManagerPolicies, async (deletedOsqueryManagerPolicy) => { + if (deletedOsqueryManagerPolicy.policy_id) { + const foundPacks = await packsClient.find({ + type: packSavedObjectType, + hasReference: { + type: AGENT_POLICY_SAVED_OBJECT_TYPE, + id: deletedOsqueryManagerPolicy.policy_id, + }, + perPage: 1000, + }); + + await Promise.all( + map( + foundPacks.saved_objects, + (pack: { id: string; references: SavedObjectReference[] }) => + packsClient.update( + packSavedObjectType, + pack.id, + {}, + { + references: filter( + pack.references, + (reference) => reference.id !== deletedOsqueryManagerPolicy.policy_id + ), + } + ) + ) + ); + } + }) + ); + }; diff --git a/x-pack/plugins/osquery/server/plugin.ts b/x-pack/plugins/osquery/server/plugin.ts index 77dfde5800c1e4..8e887a012dab39 100644 --- a/x-pack/plugins/osquery/server/plugin.ts +++ b/x-pack/plugins/osquery/server/plugin.ts @@ -18,8 +18,10 @@ import { CoreStart, Plugin, Logger, + SavedObjectsClient, DEFAULT_APP_CATEGORIES, } from '../../../../src/core/server'; + import { createConfig } from './create_config'; import { OsqueryPluginSetup, OsqueryPluginStart, SetupPlugins, StartPlugins } from './types'; import { defineRoutes } from './routes'; @@ -30,6 +32,7 @@ import { OsqueryAppContext, OsqueryAppContextService } from './lib/osquery_app_c import { ConfigType } from './config'; import { packSavedObjectType, savedQuerySavedObjectType } from '../common/types'; import { PLUGIN_ID } from '../common'; +import { getPackagePolicyDeleteCallback } from './lib/fleet_integration'; const registerFeatures = (features: SetupPlugins['features']) => { features.registerKibanaFeature({ @@ -257,6 +260,11 @@ export class OsqueryPlugin implements Plugin !isEmpty(value) - ), + name, + description: description || '', + queries: queries && convertPackQueriesToSO(queries), + updated_at: moment().toISOString(), + updated_by: currentUser, }, policy_ids ? { diff --git a/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts index 21cfd0bd437724..7431050996deb5 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isEmpty, filter, pickBy } from 'lodash'; +import { filter } from 'lodash'; import { schema } from '@kbn/config-schema'; import { PLUGIN_ID } from '../../../common'; @@ -77,20 +77,17 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp const updatedSavedQuerySO = await savedObjectsClient.update( savedQuerySavedObjectType, request.params.id, - pickBy( - { - id, - description, - platform, - query, - version, - interval, - ecs_mapping: convertECSMappingToArray(ecs_mapping), - updated_by: currentUser, - updated_at: new Date().toISOString(), - }, - (value) => !isEmpty(value) - ), + { + id, + description: description || '', + platform, + query, + version, + interval, + ecs_mapping: convertECSMappingToArray(ecs_mapping), + updated_by: currentUser, + updated_at: new Date().toISOString(), + }, { refresh: 'wait_for', } diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts index a4fe1835ce5f70..268ac144d2b589 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts @@ -10,26 +10,34 @@ import { first } from 'rxjs/operators'; import { CoreStart } from 'src/core/public'; import type { SearchSource } from 'src/plugins/data/common'; import type { SavedSearch } from 'src/plugins/discover/public'; +import { LicenseCheckState } from '../../../licensing/public'; import { coreMock } from '../../../../../src/core/public/mocks'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; -import type { ILicense, LicensingPluginSetup } from '../../../licensing/public'; +import { licensingMock } from '../../../licensing/public/mocks'; import { ReportingAPIClient } from '../lib/reporting_api_client'; import type { ReportingPublicPluginStartDendencies } from '../plugin'; import type { ActionContext } from './get_csv_panel_action'; import { ReportingCsvPanelAction } from './get_csv_panel_action'; -type LicenseResults = 'valid' | 'invalid' | 'unavailable' | 'expired'; - const core = coreMock.createSetup(); let apiClient: ReportingAPIClient; describe('GetCsvReportPanelAction', () => { let context: ActionContext; - let mockLicense$: (state?: LicenseResults) => Rx.Observable; + let mockLicenseState: LicenseCheckState; let mockSearchSource: SearchSource; let mockStartServicesPayload: [CoreStart, ReportingPublicPluginStartDendencies, unknown]; let mockStartServices$: Rx.Observable; + const mockLicense$ = () => { + const license = licensingMock.createLicense(); + license.check = jest.fn(() => ({ + message: `check-foo state: ${mockLicenseState}`, + state: mockLicenseState, + })); + return new Rx.BehaviorSubject(license); + }; + beforeAll(() => { if (typeof window.URL.revokeObjectURL === 'undefined') { Object.defineProperty(window.URL, 'revokeObjectURL', { @@ -44,11 +52,7 @@ describe('GetCsvReportPanelAction', () => { apiClient = new ReportingAPIClient(core.http, core.uiSettings, '7.15.0'); jest.spyOn(apiClient, 'createImmediateReport'); - mockLicense$ = (state: LicenseResults = 'valid') => { - return Rx.of({ - check: jest.fn().mockImplementation(() => ({ state })), - }) as unknown as LicensingPluginSetup['license$']; - }; + mockLicenseState = 'valid'; mockStartServicesPayload = [ { @@ -57,7 +61,8 @@ describe('GetCsvReportPanelAction', () => { } as unknown as CoreStart, { data: dataPluginMock.createStartContract(), - } as ReportingPublicPluginStartDendencies, + licensing: { ...licensingMock.createStart(), license$: mockLicense$() }, + } as unknown as ReportingPublicPluginStartDendencies, null, ]; mockStartServices$ = Rx.from(Promise.resolve(mockStartServicesPayload)); @@ -93,7 +98,6 @@ describe('GetCsvReportPanelAction', () => { const panel = new ReportingCsvPanelAction({ core, apiClient, - license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, }); @@ -130,7 +134,6 @@ describe('GetCsvReportPanelAction', () => { const panel = new ReportingCsvPanelAction({ core, apiClient, - license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, }); @@ -153,7 +156,6 @@ describe('GetCsvReportPanelAction', () => { const panel = new ReportingCsvPanelAction({ core, apiClient, - license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, }); @@ -169,7 +171,6 @@ describe('GetCsvReportPanelAction', () => { const panel = new ReportingCsvPanelAction({ core, apiClient, - license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, }); @@ -187,7 +188,6 @@ describe('GetCsvReportPanelAction', () => { const panel = new ReportingCsvPanelAction({ core, apiClient, - license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, }); @@ -200,18 +200,16 @@ describe('GetCsvReportPanelAction', () => { }); it(`doesn't allow downloads with bad licenses`, async () => { - const licenseMock$ = mockLicense$('invalid'); + mockLicenseState = 'invalid'; + const plugin = new ReportingCsvPanelAction({ core, apiClient, - license$: licenseMock$, startServices$: mockStartServices$, usesUiCapabilities: true, }); await mockStartServices$.pipe(first()).toPromise(); - await licenseMock$.pipe(first()).toPromise(); - expect(await plugin.isCompatible(context)).toEqual(false); }); @@ -219,7 +217,6 @@ describe('GetCsvReportPanelAction', () => { const panel = new ReportingCsvPanelAction({ core, apiClient, - license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, }); @@ -232,23 +229,15 @@ describe('GetCsvReportPanelAction', () => { describe('Application UI Capabilities', () => { it(`doesn't allow downloads when UI capability is not enabled`, async () => { - mockStartServicesPayload = [ - { application: { capabilities: {} } } as unknown as CoreStart, - { - data: dataPluginMock.createStartContract(), - } as ReportingPublicPluginStartDendencies, - null, - ]; - const startServices$ = Rx.from(Promise.resolve(mockStartServicesPayload)); + mockStartServicesPayload[0].application = { capabilities: {} } as CoreStart['application']; const plugin = new ReportingCsvPanelAction({ core, apiClient, - license$: mockLicense$(), - startServices$, + startServices$: mockStartServices$, usesUiCapabilities: true, }); - await startServices$.pipe(first()).toPromise(); + await mockStartServices$.pipe(first()).toPromise(); expect(await plugin.isCompatible(context)).toEqual(false); }); @@ -257,7 +246,6 @@ describe('GetCsvReportPanelAction', () => { const plugin = new ReportingCsvPanelAction({ core, apiClient, - license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, }); @@ -271,7 +259,6 @@ describe('GetCsvReportPanelAction', () => { const plugin = new ReportingCsvPanelAction({ core, apiClient, - license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: false, }); diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 380857f1bffd27..49e693fc8e87eb 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -19,7 +19,6 @@ import type { IEmbeddable } from '../../../../../src/plugins/embeddable/public'; import { ViewMode } from '../../../../../src/plugins/embeddable/public'; import type { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public'; import { IncompatibleActionError } from '../../../../../src/plugins/ui_actions/public'; -import type { LicensingPluginSetup } from '../../../licensing/public'; import { CSV_REPORTING_ACTION } from '../../common/constants'; import { checkLicense } from '../lib/license_check'; import { ReportingAPIClient } from '../lib/reporting_api_client'; @@ -39,7 +38,6 @@ interface Params { apiClient: ReportingAPIClient; core: CoreSetup; startServices$: Rx.Observable<[CoreStart, ReportingPublicPluginStartDendencies, unknown]>; - license$: LicensingPluginSetup['license$']; usesUiCapabilities: boolean; } @@ -52,27 +50,16 @@ export class ReportingCsvPanelAction implements ActionDefinition private notifications: NotificationsSetup; private apiClient: ReportingAPIClient; private startServices$: Params['startServices$']; + private usesUiCapabilities: any; - constructor({ core, startServices$, license$, usesUiCapabilities, apiClient }: Params) { + constructor({ core, apiClient, startServices$, usesUiCapabilities }: Params) { this.isDownloading = false; this.notifications = core.notifications; this.apiClient = apiClient; - this.startServices$ = startServices$; - - license$.subscribe((license) => { - const results = license.check('reporting', 'basic'); - const { showLinks } = checkLicense(results); - this.licenseHasDownloadCsv = showLinks; - }); - if (usesUiCapabilities) { - this.startServices$.subscribe(([{ application }]) => { - this.capabilityHasDownloadCsv = application.capabilities.dashboard?.downloadCsv === true; - }); - } else { - this.capabilityHasDownloadCsv = true; // deprecated - } + this.startServices$ = startServices$; + this.usesUiCapabilities = usesUiCapabilities; } public getIconType() { @@ -85,7 +72,7 @@ export class ReportingCsvPanelAction implements ActionDefinition }); } - public async getSearchSource(savedSearch: SavedSearch, embeddable: ISearchEmbeddable) { + public async getSearchSource(savedSearch: SavedSearch, _embeddable: ISearchEmbeddable) { const [{ uiSettings }, { data }] = await this.startServices$.pipe(first()).toPromise(); const { getSharingData } = await loadSharingDataHelpers(); return await getSharingData( @@ -96,12 +83,29 @@ export class ReportingCsvPanelAction implements ActionDefinition } public isCompatible = async (context: ActionContext) => { + await new Promise((resolve) => { + this.startServices$.subscribe(([{ application }, { licensing }]) => { + licensing.license$.subscribe((license) => { + const results = license.check('reporting', 'basic'); + const { showLinks } = checkLicense(results); + this.licenseHasDownloadCsv = showLinks; + }); + + if (this.usesUiCapabilities) { + this.capabilityHasDownloadCsv = application.capabilities.dashboard?.downloadCsv === true; + } else { + this.capabilityHasDownloadCsv = true; // deprecated + } + + resolve(); + }); + }); + if (!this.licenseHasDownloadCsv || !this.capabilityHasDownloadCsv) { return false; } const { embeddable } = context; - return embeddable.getInput().viewMode !== ViewMode.EDIT && embeddable.type === 'search'; }; @@ -166,7 +170,7 @@ export class ReportingCsvPanelAction implements ActionDefinition .catch(this.onGenerationFail.bind(this)); }; - private onGenerationFail(error: Error) { + private onGenerationFail(_error: Error) { this.isDownloading = false; this.notifications.toasts.addDanger({ title: i18n.translate('xpack.reporting.dashboard.failedCsvDownloadTitle', { diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index 77c8489bb89926..466ac2decefa17 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -27,7 +27,7 @@ import { HomePublicPluginStart, } from '../../../../src/plugins/home/public'; import { ManagementSetup, ManagementStart } from '../../../../src/plugins/management/public'; -import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; +import { LicensingPluginStart } from '../../licensing/public'; import { durationToNumber } from '../common/schema_utils'; import { JobId, JobSummarySet } from '../common/types'; import { ReportingSetup, ReportingStart } from './'; @@ -43,7 +43,7 @@ import type { UiActionsStart, } from './shared_imports'; import { AppNavLinkStatus } from './shared_imports'; -import { ReportingCsvShareProvider } from './share_context_menu/register_csv_reporting'; +import { reportingCsvShareProvider } from './share_context_menu/register_csv_reporting'; import { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting'; import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../common/constants'; @@ -78,7 +78,6 @@ function handleError( export interface ReportingPublicPluginSetupDendencies { home: HomePublicPluginSetup; management: ManagementSetup; - licensing: LicensingPluginSetup; uiActions: UiActionsSetup; screenshotting: ScreenshottingSetup; share: SharePluginSetup; @@ -153,14 +152,7 @@ export class ReportingPublicPlugin setupDeps: ReportingPublicPluginSetupDendencies ) { const { getStartServices, uiSettings } = core; - const { - home, - management, - licensing: { license$ }, // FIXME: 'license$' is deprecated - screenshotting, - share, - uiActions, - } = setupDeps; + const { home, management, screenshotting, share, uiActions } = setupDeps; const startServices$ = Rx.from(getStartServices()); const usesUiCapabilities = !this.config.roles.enabled; @@ -187,14 +179,15 @@ export class ReportingPublicPlugin order: 1, mount: async (params) => { params.setBreadcrumbs([{ text: this.breadcrumbText }]); - const [[start], { mountManagementSection }] = await Promise.all([ + const [[start, startDeps], { mountManagementSection }] = await Promise.all([ getStartServices(), import('./management/mount_management_section'), ]); - const { - chrome: { docTitle }, - } = start; + + const { docTitle } = start.chrome; docTitle.change(this.title); + + const { license$ } = startDeps.licensing; const umountAppCallback = await mountManagementSection( core, start, @@ -227,35 +220,39 @@ export class ReportingPublicPlugin uiActions.addTriggerAction( CONTEXT_MENU_TRIGGER, - new ReportingCsvPanelAction({ core, apiClient, startServices$, license$, usesUiCapabilities }) + new ReportingCsvPanelAction({ core, apiClient, startServices$, usesUiCapabilities }) ); const reportingStart = this.getContract(core); const { toasts } = core.notifications; - share.register( - ReportingCsvShareProvider({ - apiClient, - toasts, - license$, - startServices$, - uiSettings, - usesUiCapabilities, - theme: core.theme, - }) - ); + startServices$.subscribe(([{ application }, { licensing }]) => { + licensing.license$.subscribe((license) => { + share.register( + reportingCsvShareProvider({ + apiClient, + toasts, + uiSettings, + license, + application, + usesUiCapabilities, + theme: core.theme, + }) + ); - share.register( - reportingScreenshotShareProvider({ - apiClient, - toasts, - license$, - startServices$, - uiSettings, - usesUiCapabilities, - theme: core.theme, - }) - ); + share.register( + reportingScreenshotShareProvider({ + apiClient, + toasts, + uiSettings, + license, + application, + usesUiCapabilities, + theme: core.theme, + }) + ); + }); + }); return reportingStart; } diff --git a/x-pack/plugins/reporting/public/share_context_menu/index.ts b/x-pack/plugins/reporting/public/share_context_menu/index.ts index 6a5dbf970e0b4d..1a16804411c442 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/index.ts +++ b/x-pack/plugins/reporting/public/share_context_menu/index.ts @@ -5,20 +5,23 @@ * 2.0. */ -import * as Rx from 'rxjs'; -import type { IUiSettingsClient, ThemeServiceSetup, ToastsSetup } from 'src/core/public'; -import { CoreStart } from 'src/core/public'; +import type { + ApplicationStart, + IUiSettingsClient, + ThemeServiceSetup, + ToastsSetup, +} from 'src/core/public'; +import { ILicense } from '../../../licensing/public'; import type { LayoutParams } from '../../../screenshotting/common'; -import type { LicensingPluginSetup } from '../../../licensing/public'; import type { ReportingAPIClient } from '../lib/reporting_api_client'; export interface ExportPanelShareOpts { apiClient: ReportingAPIClient; toasts: ToastsSetup; uiSettings: IUiSettingsClient; - license$: LicensingPluginSetup['license$']; // FIXME: 'license$' is deprecated - startServices$: Rx.Observable<[CoreStart, object, unknown]>; usesUiCapabilities: boolean; + license: ILicense; + application: ApplicationStart; theme: ThemeServiceSetup; } diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index b264c963611222..23ecb01eddcf9c 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -8,42 +8,21 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import type { SearchSourceFields } from 'src/plugins/data/common'; -import { ExportPanelShareOpts } from '.'; -import type { ShareContext } from '../../../../../src/plugins/share/public'; +import { ShareContext, ShareMenuProvider } from 'src/plugins/share/public'; import { CSV_JOB_TYPE } from '../../common/constants'; import { checkLicense } from '../lib/license_check'; +import { ExportPanelShareOpts } from './'; import { ReportingPanelContent } from './reporting_panel_content_lazy'; -export const ReportingCsvShareProvider = ({ +export const reportingCsvShareProvider = ({ apiClient, toasts, uiSettings, - license$, - startServices$, + application, + license, usesUiCapabilities, theme, -}: ExportPanelShareOpts) => { - let licenseToolTipContent = ''; - let licenseHasCsvReporting = false; - let licenseDisabled = true; - let capabilityHasCsvReporting = false; - - license$.subscribe((license) => { - const licenseCheck = checkLicense(license.check('reporting', 'basic')); - licenseToolTipContent = licenseCheck.message; - licenseHasCsvReporting = licenseCheck.showLinks; - licenseDisabled = !licenseCheck.enableLinks; - }); - - if (usesUiCapabilities) { - startServices$.subscribe(([{ application }]) => { - // TODO: add abstractions in ExportTypeRegistry to use here? - capabilityHasCsvReporting = application.capabilities.discover?.generateCsv === true; - }); - } else { - capabilityHasCsvReporting = true; // deprecated - } - +}: ExportPanelShareOpts): ShareMenuProvider => { const getShareMenuItems = ({ objectType, objectId, sharingData, onClose }: ShareContext) => { if ('search' !== objectType) { return []; @@ -69,6 +48,19 @@ export const ReportingCsvShareProvider = ({ const shareActions = []; + const licenseCheck = checkLicense(license.check('reporting', 'basic')); + const licenseToolTipContent = licenseCheck.message; + const licenseHasCsvReporting = licenseCheck.showLinks; + const licenseDisabled = !licenseCheck.enableLinks; + + // TODO: add abstractions in ExportTypeRegistry to use here? + let capabilityHasCsvReporting = false; + if (usesUiCapabilities) { + capabilityHasCsvReporting = application.capabilities.discover?.generateCsv === true; + } else { + capabilityHasCsvReporting = true; // deprecated + } + if (licenseHasCsvReporting && capabilityHasCsvReporting) { const panelTitle = i18n.translate('xpack.reporting.shareContextMenu.csvReportsButtonLabel', { defaultMessage: 'CSV Reports', diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx index 3cc8cbacc7921b..b898a93c28880f 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx @@ -7,11 +7,11 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { ShareContext } from 'src/plugins/share/public'; -import { ExportPanelShareOpts, JobParamsProviderOptions, ReportingSharingData } from '.'; +import { ShareContext, ShareMenuProvider } from 'src/plugins/share/public'; import { isJobV2Params } from '../../common/job_utils'; import { checkLicense } from '../lib/license_check'; import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { ExportPanelShareOpts, JobParamsProviderOptions, ReportingSharingData } from './'; import { ScreenCapturePanelContent } from './screen_capture_panel_content_lazy'; const getJobParams = @@ -60,38 +60,11 @@ export const reportingScreenshotShareProvider = ({ apiClient, toasts, uiSettings, - license$, - startServices$, + license, + application, usesUiCapabilities, theme, -}: ExportPanelShareOpts) => { - let licenseToolTipContent = ''; - let licenseDisabled = true; - let licenseHasScreenshotReporting = false; - let capabilityHasDashboardScreenshotReporting = false; - let capabilityHasVisualizeScreenshotReporting = false; - - license$.subscribe((license) => { - const { enableLinks, showLinks, message } = checkLicense(license.check('reporting', 'gold')); - licenseToolTipContent = message; - licenseHasScreenshotReporting = showLinks; - licenseDisabled = !enableLinks; - }); - - if (usesUiCapabilities) { - startServices$.subscribe(([{ application }]) => { - // TODO: add abstractions in ExportTypeRegistry to use here? - capabilityHasDashboardScreenshotReporting = - application.capabilities.dashboard?.generateScreenshot === true; - capabilityHasVisualizeScreenshotReporting = - application.capabilities.visualize?.generateScreenshot === true; - }); - } else { - // deprecated - capabilityHasDashboardScreenshotReporting = true; - capabilityHasVisualizeScreenshotReporting = true; - } - +}: ExportPanelShareOpts): ShareMenuProvider => { const getShareMenuItems = ({ objectType, objectId, @@ -100,6 +73,25 @@ export const reportingScreenshotShareProvider = ({ shareableUrl, ...shareOpts }: ShareContext) => { + const { enableLinks, showLinks, message } = checkLicense(license.check('reporting', 'gold')); + const licenseToolTipContent = message; + const licenseHasScreenshotReporting = showLinks; + const licenseDisabled = !enableLinks; + + let capabilityHasDashboardScreenshotReporting = false; + let capabilityHasVisualizeScreenshotReporting = false; + if (usesUiCapabilities) { + // TODO: add abstractions in ExportTypeRegistry to use here? + capabilityHasDashboardScreenshotReporting = + application.capabilities.dashboard?.generateScreenshot === true; + capabilityHasVisualizeScreenshotReporting = + application.capabilities.visualize?.generateScreenshot === true; + } else { + // deprecated + capabilityHasDashboardScreenshotReporting = true; + capabilityHasVisualizeScreenshotReporting = true; + } + if (!licenseHasScreenshotReporting) { return []; } @@ -204,5 +196,8 @@ export const reportingScreenshotShareProvider = ({ return shareActions; }; - return { id: 'screenCaptureReports', getShareMenuItems }; + return { + id: 'screenCaptureReports', + getShareMenuItems, + }; }; diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index e48983634efd88..a29e709b4442df 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -23,7 +23,7 @@ import { import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; import { IEventLogService } from '../../event_log/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; -import { LicensingPluginSetup } from '../../licensing/server'; +import { LicensingPluginStart } from '../../licensing/server'; import type { ScreenshotResult, ScreenshottingStart } from '../../screenshotting/server'; import { SecurityPluginSetup } from '../../security/server'; import { DEFAULT_SPACE_ID } from '../../spaces/common/constants'; @@ -44,7 +44,6 @@ export interface ReportingInternalSetup { basePath: Pick; router: ReportingPluginRouter; features: FeaturesPluginSetup; - licensing: LicensingPluginSetup; security?: SecurityPluginSetup; spaces?: SpacesPluginSetup; taskManager: TaskManagerSetupContract; @@ -58,9 +57,10 @@ export interface ReportingInternalStart { uiSettings: UiSettingsServiceStart; esClient: IClusterClient; data: DataPluginStart; - taskManager: TaskManagerStartContract; + licensing: LicensingPluginStart; logger: LevelLogger; screenshotting: ScreenshottingStart; + taskManager: TaskManagerStartContract; } /** @@ -250,10 +250,12 @@ export class ReportingCore { } public async getLicenseInfo() { - const { licensing } = this.getPluginSetupDeps(); - return await licensing.license$ + const { license$ } = (await this.getPluginStartDeps()).licensing; + const registry = this.getExportTypesRegistry(); + + return await license$ .pipe( - map((license) => checkLicense(this.getExportTypesRegistry(), license)), + map((license) => checkLicense(registry, license)), first() ) .toPromise(); diff --git a/x-pack/plugins/reporting/server/plugin.test.ts b/x-pack/plugins/reporting/server/plugin.test.ts index 7afeedd3d2832a..e179d847d95260 100644 --- a/x-pack/plugins/reporting/server/plugin.test.ts +++ b/x-pack/plugins/reporting/server/plugin.test.ts @@ -5,16 +5,18 @@ * 2.0. */ -import { CoreSetup, CoreStart } from 'kibana/server'; +import type { CoreSetup, CoreStart } from 'kibana/server'; import { coreMock } from 'src/core/server/mocks'; -import { ReportingInternalStart } from './core'; +import type { ReportingCore, ReportingInternalStart } from './core'; +import { LevelLogger } from './lib'; import { ReportingPlugin } from './plugin'; -import { createMockConfigSchema, createMockPluginSetup } from './test_helpers'; import { + createMockConfigSchema, + createMockLevelLogger, + createMockPluginSetup, createMockPluginStart, - createMockReportingCore, -} from './test_helpers/create_mock_reportingplugin'; -import { ReportingSetupDeps } from './types'; +} from './test_helpers'; +import type { ReportingSetupDeps } from './types'; const sleep = (time: number) => new Promise((r) => setTimeout(r, time)); @@ -25,32 +27,33 @@ describe('Reporting Plugin', () => { let coreStart: CoreStart; let pluginSetup: ReportingSetupDeps; let pluginStart: ReportingInternalStart; + let logger: jest.Mocked; + let plugin: ReportingPlugin; beforeEach(async () => { - const reportingCore = await createMockReportingCore(createMockConfigSchema()); configSchema = createMockConfigSchema(); initContext = coreMock.createPluginInitializerContext(configSchema); coreSetup = coreMock.createSetup(configSchema); coreStart = coreMock.createStart(); pluginSetup = createMockPluginSetup({}) as unknown as ReportingSetupDeps; - pluginStart = createMockPluginStart(reportingCore, {}); + pluginStart = await createMockPluginStart(coreStart, configSchema); + + logger = createMockLevelLogger(); + plugin = new ReportingPlugin(initContext); + (plugin as unknown as { logger: LevelLogger }).logger = logger; }); it('has a sync setup process', () => { - const plugin = new ReportingPlugin(initContext); - expect(plugin.setup(coreSetup, pluginSetup)).not.toHaveProperty('then'); }); it('has a sync startup process', async () => { - const plugin = new ReportingPlugin(initContext); plugin.setup(coreSetup, pluginSetup); await sleep(5); expect(plugin.start(coreStart, pluginStart)).not.toHaveProperty('then'); }); it('registers an advanced setting for PDF logos', async () => { - const plugin = new ReportingPlugin(initContext); plugin.setup(coreSetup, pluginSetup); expect(coreSetup.uiSettings.register).toHaveBeenCalled(); expect((coreSetup.uiSettings.register as jest.Mock).mock.calls[0][0]).toHaveProperty( @@ -59,17 +62,24 @@ describe('Reporting Plugin', () => { }); it('logs start issues', async () => { - const plugin = new ReportingPlugin(initContext); - (plugin as unknown as { logger: { error: jest.Mock } }).logger.error = jest.fn(); + // wait for the setup phase background work plugin.setup(coreSetup, pluginSetup); - await sleep(5); - plugin.start(null as any, pluginStart); - await sleep(10); - // @ts-ignore overloading error logger - expect(plugin.logger.error.mock.calls[0][0]).toMatch( - /Error in Reporting start, reporting may not function properly/ - ); - // @ts-ignore overloading error logger - expect(plugin.logger.error).toHaveBeenCalledTimes(2); + await new Promise(setImmediate); + + // create a way for an error to happen + const reportingCore = (plugin as unknown as { reportingCore: ReportingCore }).reportingCore; + reportingCore.pluginStart = jest.fn().mockRejectedValueOnce('silly'); + + // wait for the startup phase background work + plugin.start(coreStart, pluginStart); + await new Promise(setImmediate); + + expect(logger.error.mock.calls.map(([message]) => message)).toMatchInlineSnapshot(` + Array [ + "Error in Reporting start, reporting may not function properly", + "silly", + ] + `); + expect(logger.error).toHaveBeenCalledTimes(2); }); }); diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index 942ebbea47881d..32285772d0e233 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -37,8 +37,8 @@ export class ReportingPlugin } public setup(core: CoreSetup, plugins: ReportingSetupDeps) { - const { http } = core; - const { features, licensing, eventLog, security, spaces, taskManager } = plugins; + const { http, status } = core; + const { features, eventLog, security, spaces, taskManager } = plugins; const reportingCore = new ReportingCore(this.logger, this.initContext); @@ -53,28 +53,25 @@ export class ReportingPlugin } }); - const router = http.createRouter(); const basePath = http.basePath; + const router = http.createRouter(); + reportingCore.pluginSetup({ + status, features, - licensing, eventLog, security, spaces, taskManager, - logger: this.logger, - status: core.status, basePath, router, + logger: this.logger, }); registerEventLogProviderActions(eventLog); registerUiSettings(core); - registerDeprecations({ - core, - reportingCore, - }); - registerReportingUsageCollector(reportingCore, plugins); + registerDeprecations({ core, reportingCore }); + registerReportingUsageCollector(reportingCore, plugins.usageCollection); registerRoutes(reportingCore, this.logger); // async background setup @@ -94,8 +91,10 @@ export class ReportingPlugin } public start(core: CoreStart, plugins: ReportingStartDeps) { + const { elasticsearch, savedObjects, uiSettings } = core; + const { data, licensing, screenshotting, taskManager } = plugins; // use data plugin for csv formats - setFieldFormats(plugins.data.fieldFormats); + setFieldFormats(data.fieldFormats); // FIXME: 'fieldFormats' is deprecated. const reportingCore = this.reportingCore!; // async background start @@ -105,14 +104,15 @@ export class ReportingPlugin const store = new ReportingStore(reportingCore, this.logger); await reportingCore.pluginStart({ - savedObjects: core.savedObjects, - uiSettings: core.uiSettings, - store, - esClient: core.elasticsearch.client, - data: plugins.data, - taskManager: plugins.taskManager, logger: this.logger, - screenshotting: plugins.screenshotting, + esClient: elasticsearch.client, + savedObjects, + uiSettings, + store, + data, + licensing, + screenshotting, + taskManager, }); // Note: this must be called after ReportingCore.pluginStart diff --git a/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts b/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts index 1917f3f68b5a34..d283dd2a5e800e 100644 --- a/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts +++ b/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts @@ -50,6 +50,7 @@ export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Log return res.notFound(); } } catch (e) { + logger.error(e); return res.customError({ statusCode: e.statusCode, body: e.message }); } @@ -86,6 +87,7 @@ export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Log }; return res.ok({ body: response }); } catch (e) { + logger.error(e); return res.customError({ statusCode: e?.statusCode ?? 500, body: { message: e.message }, diff --git a/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts b/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts index 1cbdfa36e342d8..67d7d0c4a0c080 100644 --- a/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts +++ b/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts @@ -5,15 +5,16 @@ * 2.0. */ -import { of } from 'rxjs'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; +import { licensingMock } from '../../../../../licensing/server/mocks'; import { securityMock } from '../../../../../security/server/mocks'; import { API_GET_ILM_POLICY_STATUS } from '../../../../common/constants'; import { createMockConfigSchema, createMockLevelLogger, createMockPluginSetup, + createMockPluginStart, createMockReportingCore, } from '../../../test_helpers'; import { registerDeprecationsRoutes } from '../deprecations'; @@ -26,24 +27,18 @@ describe(`GET ${API_GET_ILM_POLICY_STATUS}`, () => { let server: SetupServerReturn['server']; let httpSetup: SetupServerReturn['httpSetup']; - const createReportingCore = ({ + const mockConfig = createMockConfigSchema({ + queue: { indexInterval: 'year', timeout: 10000, pollEnabled: true }, + }); + const createReportingCore = async ({ security, }: { security?: ReturnType; }) => createMockReportingCore( - createMockConfigSchema({ - queue: { - indexInterval: 'year', - timeout: 10000, - pollEnabled: true, - }, - }), - createMockPluginSetup({ - security, - router: httpSetup.createRouter(''), - licensing: { license$: of({ isActive: true, isAvailable: true, type: 'gold' }) }, - }) + mockConfig, + createMockPluginSetup({ security, router: httpSetup.createRouter('') }), + await createMockPluginStart({ licensing: licensingMock.createStart() }, mockConfig) ); beforeEach(async () => { diff --git a/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts b/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts index f80c7d9b2accd1..3ec735083d7cc3 100644 --- a/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts +++ b/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts @@ -5,19 +5,22 @@ * 2.0. */ -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; -import { ElasticsearchClient } from 'kibana/server'; import rison from 'rison-node'; -import { of } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { ReportingCore } from '../../../'; +import { licensingMock } from '../../../../../licensing/server/mocks'; +import { ReportingStore } from '../../../lib'; import { ExportTypesRegistry } from '../../../lib/export_types_registry'; -import { createMockLevelLogger, createMockReportingCore } from '../../../test_helpers'; +import { Report } from '../../../lib/store'; import { createMockConfigSchema, + createMockLevelLogger, createMockPluginSetup, -} from '../../../test_helpers/create_mock_reportingplugin'; + createMockPluginStart, + createMockReportingCore, +} from '../../../test_helpers'; import type { ReportingRequestHandlerContext } from '../../../types'; import { registerJobGenerationRoutes } from '../generate_from_jobparams'; @@ -28,15 +31,11 @@ describe('POST /api/reporting/generate', () => { let server: SetupServerReturn['server']; let httpSetup: SetupServerReturn['httpSetup']; let mockExportTypesRegistry: ExportTypesRegistry; - let core: ReportingCore; - let mockEsClient: DeeplyMockedKeys; - - const config = createMockConfigSchema({ - queue: { - indexInterval: 'year', - timeout: 10000, - pollEnabled: true, - }, + let mockReportingCore: ReportingCore; + let store: ReportingStore; + + const mockConfigSchema = createMockConfigSchema({ + queue: { indexInterval: 'year', timeout: 10000, pollEnabled: true }, }); const mockLogger = createMockLevelLogger(); @@ -57,10 +56,23 @@ describe('POST /api/reporting/generate', () => { }, }, router: httpSetup.createRouter(''), - licensing: { license$: of({ isActive: true, isAvailable: true, type: 'gold' }) }, }); - core = await createMockReportingCore(config, mockSetupDeps); + const mockStartDeps = await createMockPluginStart( + { + licensing: { + ...licensingMock.createStart(), + license$: new BehaviorSubject({ isActive: true, isAvailable: true, type: 'gold' }), + }, + }, + mockConfigSchema + ); + + mockReportingCore = await createMockReportingCore( + mockConfigSchema, + mockSetupDeps, + mockStartDeps + ); mockExportTypesRegistry = new ExportTypesRegistry(); mockExportTypesRegistry.register({ @@ -73,10 +85,16 @@ describe('POST /api/reporting/generate', () => { createJobFnFactory: () => async () => ({ createJobTest: { test1: 'yes' } } as any), runTaskFnFactory: () => async () => ({ runParamsTest: { test2: 'yes' } } as any), }); - core.getExportTypesRegistry = () => mockExportTypesRegistry; - - mockEsClient = (await core.getEsClient()).asInternalUser as typeof mockEsClient; - mockEsClient.index.mockResolvedValue({ body: {} } as any); + mockReportingCore.getExportTypesRegistry = () => mockExportTypesRegistry; + + store = await mockReportingCore.getStore(); + store.addReport = jest.fn().mockImplementation(async (opts) => { + return new Report({ + ...opts, + _id: 'foo', + _index: 'foo-index', + }); + }); }); afterEach(async () => { @@ -84,7 +102,7 @@ describe('POST /api/reporting/generate', () => { }); it('returns 400 if there are no job params', async () => { - registerJobGenerationRoutes(core, mockLogger); + registerJobGenerationRoutes(mockReportingCore, mockLogger); await server.start(); @@ -99,7 +117,7 @@ describe('POST /api/reporting/generate', () => { }); it('returns 400 if job params query is invalid', async () => { - registerJobGenerationRoutes(core, mockLogger); + registerJobGenerationRoutes(mockReportingCore, mockLogger); await server.start(); @@ -110,7 +128,7 @@ describe('POST /api/reporting/generate', () => { }); it('returns 400 if job params body is invalid', async () => { - registerJobGenerationRoutes(core, mockLogger); + registerJobGenerationRoutes(mockReportingCore, mockLogger); await server.start(); @@ -122,7 +140,7 @@ describe('POST /api/reporting/generate', () => { }); it('returns 400 export type is invalid', async () => { - registerJobGenerationRoutes(core, mockLogger); + registerJobGenerationRoutes(mockReportingCore, mockLogger); await server.start(); @@ -136,9 +154,9 @@ describe('POST /api/reporting/generate', () => { }); it('returns 500 if job handler throws an error', async () => { - mockEsClient.index.mockRejectedValueOnce('silly'); + store.addReport = jest.fn().mockRejectedValue('silly'); - registerJobGenerationRoutes(core, mockLogger); + registerJobGenerationRoutes(mockReportingCore, mockLogger); await server.start(); @@ -149,8 +167,7 @@ describe('POST /api/reporting/generate', () => { }); it(`returns 200 if job handler doesn't error`, async () => { - mockEsClient.index.mockResolvedValueOnce({ body: { _id: 'foo', _index: 'foo-index' } } as any); - registerJobGenerationRoutes(core, mockLogger); + registerJobGenerationRoutes(mockReportingCore, mockLogger); await server.start(); diff --git a/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts b/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts index b1e4a398cfd092..6cbe7f27fa2792 100644 --- a/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts +++ b/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts @@ -8,19 +8,20 @@ jest.mock('../../../lib/content_stream', () => ({ getContentStream: jest.fn(), })); - import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { ElasticsearchClient } from 'kibana/server'; -import { of } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; import { setupServer } from 'src/core/server/test_utils'; import { Readable } from 'stream'; import supertest from 'supertest'; import { ReportingCore } from '../../../'; -import { ReportingInternalSetup } from '../../../core'; +import { licensingMock } from '../../../../../licensing/server/mocks'; +import { ReportingInternalSetup, ReportingInternalStart } from '../../../core'; import { ContentStream, ExportTypesRegistry, getContentStream } from '../../../lib'; import { createMockConfigSchema, createMockPluginSetup, + createMockPluginStart, createMockReportingCore, } from '../../../test_helpers'; import { ExportTypeDefinition, ReportingRequestHandlerContext } from '../../../types'; @@ -35,6 +36,7 @@ describe('GET /api/reporting/jobs/download', () => { let exportTypesRegistry: ExportTypesRegistry; let core: ReportingCore; let mockSetupDeps: ReportingInternalSetup; + let mockStartDeps: ReportingInternalStart; let mockEsClient: DeeplyMockedKeys; let stream: jest.Mocked; @@ -53,34 +55,31 @@ describe('GET /api/reporting/jobs/download', () => { 'reporting', () => ({ usesUiCapabilities: jest.fn() }) ); + + const mockConfigSchema = createMockConfigSchema({ roles: { enabled: false } }); + mockSetupDeps = createMockPluginSetup({ security: { - license: { - isEnabled: () => true, - }, + license: { isEnabled: () => true }, authc: { - getCurrentUser: () => ({ - id: '123', - roles: ['superuser'], - username: 'Tom Riddle', - }), + getCurrentUser: () => ({ id: '123', roles: ['superuser'], username: 'Tom Riddle' }), }, }, router: httpSetup.createRouter(''), - licensing: { - license$: of({ - isActive: true, - isAvailable: true, - type: 'gold', - }), - }, }); - core = await createMockReportingCore( - createMockConfigSchema({ roles: { enabled: false } }), - mockSetupDeps + mockStartDeps = await createMockPluginStart( + { + licensing: { + ...licensingMock.createStart(), + license$: new BehaviorSubject({ isActive: true, isAvailable: true, type: 'gold' }), + }, + }, + mockConfigSchema ); - // @ts-ignore + + core = await createMockReportingCore(mockConfigSchema, mockSetupDeps, mockStartDeps); + exportTypesRegistry = new ExportTypesRegistry(); exportTypesRegistry.register({ id: 'unencoded', diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index 9570c82f23a8a2..1f6d7bcff5176d 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -9,7 +9,7 @@ jest.mock('../routes'); jest.mock('../usage'); import _ from 'lodash'; -import * as Rx from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; import { coreMock, elasticsearchServiceMock, statusServiceMock } from 'src/core/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { dataPluginMock } from 'src/plugins/data/server/mocks'; @@ -17,11 +17,12 @@ import { FieldFormatsRegistry } from 'src/plugins/field_formats/common'; import { DeepPartial } from 'utility-types'; import { ReportingConfig, ReportingCore } from '../'; import { featuresPluginMock } from '../../../features/server/mocks'; +import { licensingMock } from '../../../licensing/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { createMockScreenshottingStart } from '../../../screenshotting/server/mock'; import { securityMock } from '../../../security/server/mocks'; import { taskManagerMock } from '../../../task_manager/server/mocks'; -import { ReportingConfigType } from '../config'; +import { buildConfig, ReportingConfigType } from '../config'; import { ReportingInternalSetup, ReportingInternalStart } from '../core'; import { ReportingStore } from '../lib'; import { setFieldFormats } from '../services'; @@ -35,7 +36,6 @@ export const createMockPluginSetup = ( basePath: { set: jest.fn() }, router: setupMock.router, security: securityMock.createSetup(), - licensing: { license$: Rx.of({ isAvailable: true, isActive: true, type: 'basic' }) }, taskManager: taskManagerMock.createSetup(), logger: createMockLevelLogger(), status: statusServiceMock.createSetupContract(), @@ -49,27 +49,33 @@ export const createMockPluginSetup = ( const logger = createMockLevelLogger(); -const createMockReportingStore = () => ({} as ReportingStore); - -export const createMockPluginStart = ( - mockReportingCore: ReportingCore | undefined, - startMock: Partial> -): ReportingInternalStart => { - const store = mockReportingCore - ? new ReportingStore(mockReportingCore, logger) - : createMockReportingStore(); +const createMockReportingStore = async (config: ReportingConfigType) => { + const mockConfigSchema = createMockConfigSchema(config); + const mockContext = coreMock.createPluginInitializerContext(mockConfigSchema); + const mockCore = new ReportingCore(logger, mockContext); + mockCore.setConfig(await buildConfig(mockContext, coreMock.createSetup(), logger)); + return new ReportingStore(mockCore, logger); +}; +export const createMockPluginStart = async ( + startMock: Partial>, + config: ReportingConfigType +): Promise => { return { esClient: elasticsearchServiceMock.createClusterClient(), savedObjects: startMock.savedObjects || { getScopedClient: jest.fn() }, uiSettings: startMock.uiSettings || { asScopedToClient: () => ({ get: jest.fn() }) }, data: startMock.data || dataPluginMock.createStartContract(), - store, + store: await createMockReportingStore(config), taskManager: { schedule: jest.fn().mockImplementation(() => ({ id: 'taskId' })), ensureScheduled: jest.fn(), }, - logger: createMockLevelLogger(), + licensing: { + ...licensingMock.createStart(), + license$: new BehaviorSubject({ isAvailable: true, isActive: true, type: 'basic' }), + }, + logger, screenshotting: startMock.screenshotting || createMockScreenshottingStart(), ...startMock, }; @@ -131,18 +137,9 @@ export const createMockReportingCore = async ( setupDepsMock: ReportingInternalSetup | undefined = undefined, startDepsMock: ReportingInternalStart | undefined = undefined ) => { - const mockReportingCore = { - getConfig: () => createMockConfig(config), - getEsClient: () => startDepsMock?.esClient, - getDataService: () => startDepsMock?.data, - } as unknown as ReportingCore; - if (!setupDepsMock) { setupDepsMock = createMockPluginSetup({}); } - if (!startDepsMock) { - startDepsMock = createMockPluginStart(mockReportingCore, {}); - } const context = coreMock.createPluginInitializerContext(createMockConfigSchema()); context.config = { get: () => config } as any; @@ -154,7 +151,7 @@ export const createMockReportingCore = async ( await core.pluginSetsUp(); if (!startDepsMock) { - startDepsMock = createMockPluginStart(core, context); + startDepsMock = await createMockPluginStart(context, config); } await core.pluginStart(startDepsMock); await core.pluginStartsUp(); diff --git a/x-pack/plugins/reporting/server/test_helpers/index.ts b/x-pack/plugins/reporting/server/test_helpers/index.ts index 667c85c24a35df..df0a182075341a 100644 --- a/x-pack/plugins/reporting/server/test_helpers/index.ts +++ b/x-pack/plugins/reporting/server/test_helpers/index.ts @@ -10,5 +10,6 @@ export { createMockConfig, createMockConfigSchema, createMockPluginSetup, + createMockPluginStart, createMockReportingCore, } from './create_mock_reportingplugin'; diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index e23a1d555fdbae..e558448950c79b 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -13,7 +13,7 @@ import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import type { Writable } from 'stream'; import { IEventLogService } from '../../event_log/server'; import type { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; -import type { LicensingPluginSetup } from '../../licensing/server'; +import type { LicensingPluginStart } from '../../licensing/server'; import type { ScreenshotOptions as BaseScreenshotOptions, ScreenshottingStart, @@ -91,7 +91,6 @@ export interface ExportTypeDefinition< * @internal */ export interface ReportingSetupDeps { - licensing: LicensingPluginSetup; eventLog: IEventLogService; features: FeaturesPluginSetup; screenshotMode: ScreenshotModePluginSetup; @@ -106,6 +105,7 @@ export interface ReportingSetupDeps { */ export interface ReportingStartDeps { data: DataPluginStart; + licensing: LicensingPluginStart; screenshotting: ScreenshottingStart; taskManager: TaskManagerStartContract; } diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index 4ffdaa80577be5..9543039ab576a5 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -5,14 +5,16 @@ * 2.0. */ -import * as Rx from 'rxjs'; -import sinon from 'sinon'; -import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; +import { loggerMock } from '@kbn/logging/mocks'; +import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; +import { + Collector, + createCollectorFetchContextMock, + usageCollectionPluginMock, +} from 'src/plugins/usage_collection/server/mocks'; import { ReportingCore } from '../'; import { getExportTypesRegistry } from '../lib/export_types_registry'; import { createMockConfigSchema, createMockReportingCore } from '../test_helpers'; -import { ReportingSetupDeps } from '../types'; import { FeaturesAvailability } from './'; import { getReportingUsageCollector, @@ -21,22 +23,6 @@ import { const exportTypesRegistry = getExportTypesRegistry(); -function getMockUsageCollection() { - class MockUsageCollector { - // @ts-ignore fetch is not used - private fetch: any; - constructor(_server: any, { fetch }: any) { - this.fetch = fetch; - } - } - return { - makeUsageCollector: (options: any) => { - return new MockUsageCollector(null, options); - }, - registerCollector: sinon.stub(), - }; -} - const getLicenseMock = (licenseType = 'platinum') => () => { @@ -46,17 +32,6 @@ const getLicenseMock = } as FeaturesAvailability); }; -function getPluginsMock( - { license, usageCollection = getMockUsageCollection() } = { license: 'platinum' } -) { - return { - licensing: { license$: Rx.of(getLicenseMock(license)) }, - usageCollection, - elasticsearch: {}, - security: {}, - } as unknown as ReportingSetupDeps & { usageCollection: UsageCollectionSetup }; -} - const getResponseMock = (base = {}) => base; const getMockFetchClients = (resp: any) => { @@ -64,6 +39,9 @@ const getMockFetchClients = (resp: any) => { fetchParamsMock.esClient.search = jest.fn().mockResolvedValue({ body: resp }); return fetchParamsMock; }; + +const usageCollectionSetup = usageCollectionPluginMock.createSetupContract(); + describe('license checks', () => { let mockCore: ReportingCore; beforeAll(async () => { @@ -73,10 +51,9 @@ describe('license checks', () => { describe('with a basic license', () => { let usageStats: any; beforeAll(async () => { - const plugins = getPluginsMock({ license: 'basic' }); const collector = getReportingUsageCollector( mockCore, - plugins.usageCollection, + usageCollectionSetup, getLicenseMock('basic'), exportTypesRegistry, function isReady() { @@ -102,10 +79,9 @@ describe('license checks', () => { describe('with no license', () => { let usageStats: any; beforeAll(async () => { - const plugins = getPluginsMock({ license: 'none' }); const collector = getReportingUsageCollector( mockCore, - plugins.usageCollection, + usageCollectionSetup, getLicenseMock('none'), exportTypesRegistry, function isReady() { @@ -131,10 +107,9 @@ describe('license checks', () => { describe('with platinum license', () => { let usageStats: any; beforeAll(async () => { - const plugins = getPluginsMock({ license: 'platinum' }); const collector = getReportingUsageCollector( mockCore, - plugins.usageCollection, + usageCollectionSetup, getLicenseMock('platinum'), exportTypesRegistry, function isReady() { @@ -160,10 +135,9 @@ describe('license checks', () => { describe('with no usage data', () => { let usageStats: any; beforeAll(async () => { - const plugins = getPluginsMock({ license: 'basic' }); const collector = getReportingUsageCollector( mockCore, - plugins.usageCollection, + usageCollectionSetup, getLicenseMock('basic'), exportTypesRegistry, function isReady() { @@ -190,10 +164,9 @@ describe('data modeling', () => { mockCore = await createMockReportingCore(createMockConfigSchema()); }); test('with usage data from the reporting/archived_reports es archive', async () => { - const plugins = getPluginsMock(); const collector = getReportingUsageCollector( mockCore, - plugins.usageCollection, + usageCollectionSetup, getLicenseMock(), exportTypesRegistry, function isReady() { @@ -238,10 +211,9 @@ describe('data modeling', () => { }); test('usage data with meta.isDeprecated jobTypes', async () => { - const plugins = getPluginsMock(); const collector = getReportingUsageCollector( mockCore, - plugins.usageCollection, + usageCollectionSetup, getLicenseMock(), exportTypesRegistry, function isReady() { @@ -279,10 +251,9 @@ describe('data modeling', () => { }); test('with sparse data', async () => { - const plugins = getPluginsMock(); const collector = getReportingUsageCollector( mockCore, - plugins.usageCollection, + usageCollectionSetup, getLicenseMock(), exportTypesRegistry, function isReady() { @@ -320,10 +291,9 @@ describe('data modeling', () => { }); test('with empty data', async () => { - const plugins = getPluginsMock(); const collector = getReportingUsageCollector( mockCore, - plugins.usageCollection, + usageCollectionSetup, getLicenseMock(), exportTypesRegistry, function isReady() { @@ -372,15 +342,12 @@ describe('data modeling', () => { describe('Ready for collection observable', () => { test('converts observable to promise', async () => { const mockReporting = await createMockReportingCore(createMockConfigSchema()); + const makeCollectorSpy = jest.fn((options: any) => new Collector(loggerMock.create(), options)); + usageCollectionSetup.makeUsageCollector.mockImplementation(makeCollectorSpy); - const usageCollection = getMockUsageCollection(); - const makeCollectorSpy = sinon.spy(); - usageCollection.makeUsageCollector = makeCollectorSpy; - - const plugins = getPluginsMock({ usageCollection, license: 'platinum' }); - registerReportingUsageCollector(mockReporting, plugins); + registerReportingUsageCollector(mockReporting, usageCollectionSetup); - const [args] = makeCollectorSpy.firstCall.args; + const [args] = makeCollectorSpy.mock.calls[0]; expect(args).toMatchSnapshot(); await expect(args.isReady()).resolves.toBe(true); diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts index 06eb0d064b89ed..a36d7caab2ecfb 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts @@ -9,7 +9,6 @@ import { first, map } from 'rxjs/operators'; import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ReportingCore } from '../'; import { ExportTypesRegistry } from '../lib/export_types_registry'; -import { ReportingSetupDeps } from '../types'; import { GetLicense } from './'; import { getReportingUsage } from './get_reporting_usage'; import { ReportingUsageType } from './types'; @@ -38,7 +37,7 @@ export function getReportingUsageCollector( export function registerReportingUsageCollector( reporting: ReportingCore, - { licensing, usageCollection }: ReportingSetupDeps + usageCollection?: UsageCollectionSetup ) { if (!usageCollection) { return; @@ -46,6 +45,7 @@ export function registerReportingUsageCollector( const exportTypesRegistry = reporting.getExportTypesRegistry(); const getLicense = async () => { + const { licensing } = await reporting.getPluginStartDeps(); return await licensing.license$ .pipe( map(({ isAvailable, type }) => ({ diff --git a/x-pack/plugins/security_solution/common/ecs/index.ts b/x-pack/plugins/security_solution/common/ecs/index.ts index 4de1160e53936d..c3e589447313a5 100644 --- a/x-pack/plugins/security_solution/common/ecs/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/index.ts @@ -74,4 +74,5 @@ export interface Ecs { Target?: Target; dll?: DllEcs; 'kibana.alert.workflow_status'?: 'open' | 'acknowledged' | 'in-progress' | 'closed'; + 'kibana.alert.rule.parameters'?: { index: string[] }; } diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts index e1208c7c54a3b3..d764a12f951d8d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts @@ -165,6 +165,8 @@ describe('Detection rules, EQL', () => { .invoke('text') .then((text) => { expect(text).contains(this.rule.name); + expect(text).contains(this.rule.severity.toLowerCase()); + expect(text).contains(this.rule.riskScore); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_modal.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_modal.spec.ts index 7eedc99652f80b..829f98b4a537db 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_modal.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_modal.spec.ts @@ -29,6 +29,7 @@ import { LOADING_SPINNER, EXCEPTION_ITEM_CONTAINER, ADD_EXCEPTIONS_BTN, + EXCEPTION_FIELD_LIST, } from '../../screens/exceptions'; import { ALERTS_URL } from '../../urls/navigation'; @@ -196,4 +197,13 @@ describe('Exceptions modal', () => { closeExceptionBuilderModal(); }); + + it('Contains custom index fields', () => { + cy.get(ADD_EXCEPTIONS_BTN).click({ force: true }); + + cy.get(FIELD_INPUT).eq(0).click({ force: true }); + cy.get(EXCEPTION_FIELD_LIST).contains('unique_value.test'); + + closeExceptionBuilderModal(); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts index e5027ee8b4f3ae..9bba5e4b555dc4 100644 --- a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts @@ -56,3 +56,6 @@ export const EXCEPTIONS_TABLE_MODAL = '[data-test-subj="referenceErrorModal"]'; export const EXCEPTIONS_TABLE_MODAL_CONFIRM_BTN = '[data-test-subj="confirmModalConfirmButton"]'; export const EXCEPTION_ITEM_CONTAINER = '[data-test-subj="exceptionEntriesContainer"]'; + +export const EXCEPTION_FIELD_LIST = + '[data-test-subj="comboBoxOptionsList fieldAutocompleteComboBox-optionsList"]'; diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx index cfcf5307de8d43..2d6ca63c82bdb6 100644 --- a/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx @@ -41,6 +41,10 @@ jest.mock('../../../common/containers/source', () => ({ useFetchIndex: () => [false, { indicesExist: true, indexPatterns: mockIndexPattern }], })); +jest.mock('../../../common/containers/sourcerer/use_signal_helpers', () => ({ + useSignalHelpers: () => ({ signalIndexNeedsInit: false }), +})); + jest.mock('react-reverse-portal', () => ({ InPortal: ({ children }: { children: React.ReactNode }) => <>{children}, OutPortal: ({ children }: { children: React.ReactNode }) => <>{children}, diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx index 7d3dc9641929ad..f4707272d2c244 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx @@ -21,10 +21,12 @@ import { createStore } from '../../store'; import { EuiSuperSelectOption } from '@elastic/eui/src/components/form/super_select/super_select_control'; import { waitFor } from '@testing-library/dom'; import { useSourcererDataView } from '../../containers/sourcerer'; +import { useSignalHelpers } from '../../containers/sourcerer/use_signal_helpers'; const mockDispatch = jest.fn(); jest.mock('../../containers/sourcerer'); +jest.mock('../../containers/sourcerer/use_signal_helpers'); const mockUseUpdateDataView = jest.fn().mockReturnValue(() => true); jest.mock('./use_update_data_view', () => ({ useUpdateDataView: () => mockUseUpdateDataView, @@ -81,10 +83,12 @@ const sourcererDataView = { describe('Sourcerer component', () => { const { storage } = createSecuritySolutionStorageMock(); - + const pollForSignalIndexMock = jest.fn(); beforeEach(() => { store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); (useSourcererDataView as jest.Mock).mockReturnValue(sourcererDataView); + (useSignalHelpers as jest.Mock).mockReturnValue({ signalIndexNeedsInit: false }); + jest.clearAllMocks(); }); @@ -570,6 +574,63 @@ describe('Sourcerer component', () => { .exists() ).toBeFalsy(); }); + + it('does not poll for signals index if pollForSignalIndex is not defined', () => { + (useSignalHelpers as jest.Mock).mockReturnValue({ + signalIndexNeedsInit: false, + }); + + mount( + + + + ); + + expect(pollForSignalIndexMock).toHaveBeenCalledTimes(0); + }); + + it('does not poll for signals index if it does not exist and scope is default', () => { + (useSignalHelpers as jest.Mock).mockReturnValue({ + pollForSignalIndex: pollForSignalIndexMock, + signalIndexNeedsInit: false, + }); + + mount( + + + + ); + + expect(pollForSignalIndexMock).toHaveBeenCalledTimes(0); + }); + + it('polls for signals index if it does not exist and scope is timeline', () => { + (useSignalHelpers as jest.Mock).mockReturnValue({ + pollForSignalIndex: pollForSignalIndexMock, + signalIndexNeedsInit: false, + }); + + mount( + + + + ); + expect(pollForSignalIndexMock).toHaveBeenCalledTimes(1); + }); + + it('polls for signals index if it does not exist and scope is detections', () => { + (useSignalHelpers as jest.Mock).mockReturnValue({ + pollForSignalIndex: pollForSignalIndexMock, + signalIndexNeedsInit: false, + }); + + mount( + + + + ); + expect(pollForSignalIndexMock).toHaveBeenCalledTimes(1); + }); }); describe('sourcerer on alerts page or rules details page', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx index 924601758c7307..ad3b11a74e81d1 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx @@ -28,6 +28,7 @@ import { useSourcererDataView } from '../../containers/sourcerer'; import { useUpdateDataView } from './use_update_data_view'; import { Trigger } from './trigger'; import { AlertsCheckbox, SaveButtons, SourcererCallout } from './sub_components'; +import { useSignalHelpers } from '../../containers/sourcerer/use_signal_helpers'; export interface SourcererComponentProps { scope: sourcererModel.SourcererScopeName; @@ -50,6 +51,14 @@ export const Sourcerer = React.memo(({ scope: scopeId } }, } = useDeepEqualSelector((state) => sourcererScopeSelector(state, scopeId)); + const { pollForSignalIndex } = useSignalHelpers(); + + useEffect(() => { + if (pollForSignalIndex != null && (isTimelineSourcerer || isDetectionsSourcerer)) { + pollForSignalIndex(); + } + }, [isDetectionsSourcerer, isTimelineSourcerer, pollForSignalIndex]); + const { activePatterns, indicesExist, loading } = useSourcererDataView(scopeId); const [missingPatterns, setMissingPatterns] = useState( activePatterns && activePatterns.length > 0 diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 1ba95cc2a2951b..d64864a699a60e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -121,6 +121,9 @@ const AlertContextMenuComponent: React.FC ), diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index 7c8b2ddf636c07..de9da5c293fc14 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -14,6 +14,8 @@ import { EuiProgress, EuiSearchBarProps, EuiSpacer, + EuiPageHeader, + EuiHorizontalRule, } from '@elastic/eui'; import type { NamespaceType, ExceptionListFilter } from '@kbn/securitysolution-io-ts-list-types'; @@ -24,8 +26,6 @@ import { useKibana } from '../../../../../../common/lib/kibana'; import { useFormatUrl } from '../../../../../../common/components/link_to'; import { Loader } from '../../../../../../common/components/loader'; -import { DetectionEngineHeaderPage } from '../../../../../components/detection_engine_header_page'; - import * as i18n from './translations'; import { AllRulesUtilityBar } from '../utility_bar'; import { AllExceptionListsColumns, getAllExceptionListsColumns } from './columns'; @@ -341,13 +341,14 @@ export const ExceptionListsTable = React.memo(() => { return ( <> - {timelines.getLastUpdated({ showUpdating: loading, updatedAt: lastUpdated })}

, + ]} /> - +
{loadingTableInfo && ( (); + useEffect(() => { + // At some point we would like to check if the path has changed or not to keep this consistent across different pages + if (routeState && routeState.onBackButtonNavigateTo) { + setMemoizedRouteState(routeState); + } + }, [routeState]); + + return memoizedRouteState; +} diff --git a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx index 685d893bf4a73b..05912e764af2cf 100644 --- a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx @@ -63,15 +63,9 @@ export const AdministrationListPage: FC - hideHeader ? ( - - - {headerBackComponent && <>{headerBackComponent}} - - - ) : ( + return ( +
+ {!hideHeader && ( <> - ), - [ - actions, - description, - getTestId, - hasBottomBorder, - header, - headerBackComponent, - hideHeader, - restrictWidth, - ] - ); - - return ( -
- {pageHeader} + )} ( + ({ backButtonLabel, backButtonUrl, onBackButtonNavigateTo, ...commonProps }) => { + const handleBackOnClick = useNavigateToAppEventHandler(...onBackButtonNavigateTo); + + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + + {backButtonLabel || ( + + )} + + ); + } +); + +BackToExternalAppSecondaryButton.displayName = 'BackToExternalAppSecondaryButton'; diff --git a/x-pack/plugins/security_solution/public/management/components/back_to_external_app_secondary_button/index.ts b/x-pack/plugins/security_solution/public/management/components/back_to_external_app_secondary_button/index.ts new file mode 100644 index 00000000000000..7c59e53dbaeae8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/back_to_external_app_secondary_button/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { BackToExternalAppSecondaryButton } from './back_to_external_app_secondary_button'; diff --git a/x-pack/plugins/security_solution/public/management/components/management_empty_state_wraper.tsx b/x-pack/plugins/security_solution/public/management/components/management_empty_state_wrapper.tsx similarity index 82% rename from x-pack/plugins/security_solution/public/management/components/management_empty_state_wraper.tsx rename to x-pack/plugins/security_solution/public/management/components/management_empty_state_wrapper.tsx index 6283fa86b1026a..0510e6d0b331c0 100644 --- a/x-pack/plugins/security_solution/public/management/components/management_empty_state_wraper.tsx +++ b/x-pack/plugins/security_solution/public/management/components/management_empty_state_wrapper.tsx @@ -13,7 +13,7 @@ export const StyledEuiFlexGroup = styled(EuiFlexGroup)` min-height: calc(100vh - 140px); `; -export const ManagementEmptyStateWraper = memo(({ children }) => { +export const ManagementEmptyStateWrapper = memo(({ children }) => { return ( {children} @@ -21,4 +21,4 @@ export const ManagementEmptyStateWraper = memo(({ children }) => { ); }); -ManagementEmptyStateWraper.displayName = 'ManagementEmptyStateWraper'; +ManagementEmptyStateWrapper.displayName = 'ManagementEmptyStateWrapper'; diff --git a/x-pack/plugins/security_solution/public/management/components/management_page_loader.tsx b/x-pack/plugins/security_solution/public/management/components/management_page_loader.tsx index 047b4ca1efd50e..cc1104127871fa 100644 --- a/x-pack/plugins/security_solution/public/management/components/management_page_loader.tsx +++ b/x-pack/plugins/security_solution/public/management/components/management_page_loader.tsx @@ -7,14 +7,14 @@ import React, { memo } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; -import { ManagementEmptyStateWraper } from './management_empty_state_wraper'; +import { ManagementEmptyStateWrapper } from './management_empty_state_wrapper'; export const ManagementPageLoader = memo<{ 'data-test-subj': string }>( ({ 'data-test-subj': dataTestSubj }) => { return ( - + - + ); } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx index 9b22afc300466a..032ea7bf190bf6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx @@ -71,9 +71,16 @@ export const EndpointActivityLog = memo( [hasActiveDateRange, isPagingDisabled, activityLogLoading, activityLogSize] ); + const doesNotHaveDataAlsoOnRefetch = useMemo( + () => !activityLastLogData?.data.length && !activityLogData.length, + [activityLastLogData, activityLogData] + ); + const showCallout = useMemo( - () => !isPagingDisabled && activityLogLoaded && !activityLogData.length, - [isPagingDisabled, activityLogLoaded, activityLogData] + () => + (!isPagingDisabled && activityLogLoaded && !activityLogData.length) || + doesNotHaveDataAlsoOnRefetch, + [isPagingDisabled, activityLogLoaded, activityLogData, doesNotHaveDataAlsoOnRefetch] ); const loadMoreTrigger = useRef(null); @@ -153,7 +160,7 @@ export const EndpointActivityLog = memo( ref={loadMoreTrigger} /> )} - {isPagingDisabled && !activityLogLoading && ( + {isPagingDisabled && !activityLogLoading && !showCallout && (

{i18.ACTIVITY_LOG.LogEntry.endOfLog}

diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 4732e28c7f8287..ddd7349fadc511 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -1028,6 +1028,57 @@ describe('when on the endpoint list page', () => { expect(activityLogCallout).not.toBeNull(); }); + it('should display a callout message if no log data also on refetch', async () => { + const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); + reactTestingLibrary.act(() => { + history.push( + getEndpointDetailsPath({ + page_index: '0', + page_size: '10', + name: 'endpointActivityLog', + selected_endpoint: '1', + }) + ); + }); + const changedUrlAction = await userChangedUrlChecker; + expect(changedUrlAction.payload.search).toEqual( + '?page_index=0&page_size=10&selected_endpoint=1&show=activity_log' + ); + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged('success', { + page: 1, + pageSize: 50, + startDate: 'now-1d', + endDate: 'now', + data: [], + }); + }); + + const activityLogCallout = await renderResult.findByTestId('activityLogNoDataCallout'); + expect(activityLogCallout).not.toBeNull(); + + // click refresh button + const refreshLogButton = await renderResult.findByTestId('superDatePickerApplyTimeButton'); + userEvent.click(refreshLogButton); + + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged('success', { + page: 1, + pageSize: 50, + startDate: 'now-1d', + endDate: 'now', + data: [], + }); + }); + + const activityLogNoDataCallout = await renderResult.findByTestId( + 'activityLogNoDataCallout' + ); + expect(activityLogNoDataCallout).not.toBeNull(); + }); + it('should not display scroll trigger when showing callout message', async () => { const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx index 11cc9eef7caf1f..e48d4f8fb4d21b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx @@ -9,7 +9,7 @@ import React, { memo } from 'react'; import styled, { css } from 'styled-components'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { ManagementEmptyStateWraper } from '../../../../../components/management_empty_state_wraper'; +import { ManagementEmptyStateWrapper } from '../../../../../components/management_empty_state_wrapper'; const EmptyPrompt = styled(EuiEmptyPrompt)` ${() => css` @@ -21,9 +21,10 @@ export const EventFiltersListEmptyState = memo<{ onAdd: () => void; /** Should the Add button be disabled */ isAddDisabled?: boolean; -}>(({ onAdd, isAddDisabled = false }) => { + backComponent?: React.ReactNode; +}>(({ onAdd, isAddDisabled = false, backComponent }) => { return ( - + } - actions={ + actions={[ - - } + , + ...(backComponent ? [backComponent] : []), + ]} /> - + ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx index 094c964576f7e6..5833c12be879fe 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx @@ -221,11 +221,27 @@ describe('When on the Event Filters List Page', () => { expect(button).toHaveAttribute('href', '/fleet'); }); - it('back button is not present', () => { + it('back button is still present after push history', () => { act(() => { history.push('/administration/event_filters'); }); - expect(renderResult.queryByTestId('backToOrigin')).toBeNull(); + const button = renderResult.queryByTestId('backToOrigin'); + expect(button).not.toBeNull(); + expect(button).toHaveAttribute('href', '/fleet'); + }); + }); + + describe('and the back button is not present', () => { + beforeEach(async () => { + renderResult = render(); + act(() => { + history.push('/administration/event_filters'); + }); + }); + + it('back button is not present when missing history params', () => { + const button = renderResult.queryByTestId('backToOrigin'); + expect(button).toBeNull(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx index 2cc4bc7b8bd719..899968a8277db5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx @@ -45,6 +45,7 @@ import { import { EventFilterDeleteModal } from './components/event_filter_delete_modal'; import { SearchExceptions } from '../../../components/search_exceptions'; +import { BackToExternalAppSecondaryButton } from '../../../components/back_to_external_app_secondary_button'; import { BackToExternalAppButton } from '../../../components/back_to_external_app_button'; import { ABOUT_EVENT_FILTERS } from './translations'; import { useGetEndpointSpecificPolicies } from '../../../services/policies/hooks'; @@ -52,6 +53,7 @@ import { useToasts } from '../../../../common/lib/kibana'; import { getLoadPoliciesError } from '../../../common/translations'; import { useEndpointPoliciesToArtifactPolicies } from '../../../components/artifact_entry_card/hooks/use_endpoint_policies_to_artifact_policies'; import { ManagementPageLoader } from '../../../components/management_page_loader'; +import { useMemoizedRouteState } from '../../../common/hooks'; type ArtifactEntryCardType = typeof ArtifactEntryCard; @@ -103,6 +105,20 @@ export const EventFiltersListPage = memo(() => { const navigateCallback = useEventFiltersNavigateCallback(); const showFlyout = !!location.show; + const memoizedRouteState = useMemoizedRouteState(routeState); + + const backButtonEmptyComponent = useMemo(() => { + if (memoizedRouteState && memoizedRouteState.onBackButtonNavigateTo) { + return ; + } + }, [memoizedRouteState]); + + const backButtonHeaderComponent = useMemo(() => { + if (memoizedRouteState && memoizedRouteState.onBackButtonNavigateTo) { + return ; + } + }, [memoizedRouteState]); + // load the list of policies const policiesRequest = useGetEndpointSpecificPolicies({ onError: (err) => { @@ -141,13 +157,6 @@ export const EventFiltersListPage = memo(() => { } }, [dispatch, formEntry, history, isActionError, location, navigateCallback]); - const backButton = useMemo(() => { - if (routeState && routeState.onBackButtonNavigateTo) { - return ; - } - return null; - }, [routeState]); - const handleAddButtonClick = useCallback( () => navigateCallback({ @@ -240,7 +249,7 @@ export const EventFiltersListPage = memo(() => { return ( { data-test-subj="eventFiltersContent" noItemsMessage={ !doesDataExist && ( - + ) } /> 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 082ec3c6fa7651..f4b43880860127 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 @@ -9,7 +9,7 @@ import React, { memo } from 'react'; import styled, { css } from 'styled-components'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { ManagementEmptyStateWraper } from '../../../../components/management_empty_state_wraper'; +import { ManagementEmptyStateWrapper } from '../../../../components/management_empty_state_wrapper'; const EmptyPrompt = styled(EuiEmptyPrompt)` ${() => css` @@ -17,9 +17,12 @@ const EmptyPrompt = styled(EuiEmptyPrompt)` `} `; -export const HostIsolationExceptionsEmptyState = memo<{ onAdd: () => void }>(({ onAdd }) => { +export const HostIsolationExceptionsEmptyState = memo<{ + onAdd: () => void; + backComponent?: React.ReactNode; +}>(({ onAdd, backComponent }) => { return ( - + void }>(({ defaultMessage="Add a Host isolation exception to allow isolated hosts to communicate with specific IPs." /> } - actions={ + actions={[ void }>(({ id="xpack.securitySolution.hostIsolationExceptions.listEmpty.addButton" defaultMessage="Add Host isolation exception" /> - - } + , + + ...(backComponent ? [backComponent] : []), + ]} /> - + ); }); 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 b8b82737ea58ca..b206dd708329e1 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 @@ -265,5 +265,47 @@ describe('When on the host isolation exceptions page', () => { expect(renderResult.queryByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeFalsy(); }); }); + + describe('and the back button is present', () => { + beforeEach(async () => { + renderResult = render(); + act(() => { + history.push(HOST_ISOLATION_EXCEPTIONS_PATH, { + onBackButtonNavigateTo: [{ appId: 'appId' }], + backButtonLabel: 'back to fleet', + backButtonUrl: '/fleet', + }); + }); + }); + + it('back button is present', () => { + const button = renderResult.queryByTestId('backToOrigin'); + expect(button).not.toBeNull(); + expect(button).toHaveAttribute('href', '/fleet'); + }); + + it('back button is still present after push history', () => { + act(() => { + history.push(HOST_ISOLATION_EXCEPTIONS_PATH); + }); + const button = renderResult.queryByTestId('backToOrigin'); + expect(button).not.toBeNull(); + expect(button).toHaveAttribute('href', '/fleet'); + }); + }); + + describe('and the back button is not present', () => { + beforeEach(async () => { + renderResult = render(); + act(() => { + history.push(HOST_ISOLATION_EXCEPTIONS_PATH); + }); + }); + + it('back button is not present when missing history params', () => { + const button = renderResult.queryByTestId('backToOrigin'); + expect(button).toBeNull(); + }); + }); }); }); 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 18a77172ab48a0..8a1bc3fa2128f6 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 @@ -24,6 +24,7 @@ import { getLoadPoliciesError } from '../../../common/translations'; import { AdministrationListPage } from '../../../components/administration_list_page'; import { ArtifactEntryCard, ArtifactEntryCardProps } from '../../../components/artifact_entry_card'; import { useEndpointPoliciesToArtifactPolicies } from '../../../components/artifact_entry_card/hooks/use_endpoint_policies_to_artifact_policies'; +import { BackToExternalAppSecondaryButton } from '../../../components/back_to_external_app_secondary_button'; import { BackToExternalAppButton } from '../../../components/back_to_external_app_button'; import { ManagementPageLoader } from '../../../components/management_page_loader'; import { PaginatedContent, PaginatedContentProps } from '../../../components/paginated_content'; @@ -42,6 +43,7 @@ import { useHostIsolationExceptionsNavigateCallback, useHostIsolationExceptionsSelector, } from './hooks'; +import { useMemoizedRouteState } from '../../../common/hooks'; type HostIsolationExceptionPaginatedContent = PaginatedContentProps< Immutable, @@ -56,6 +58,20 @@ export const HostIsolationExceptionsList = () => { const location = useHostIsolationExceptionsSelector(getCurrentLocation); const navigateCallback = useHostIsolationExceptionsNavigateCallback(); + const memoizedRouteState = useMemoizedRouteState(routeState); + + const backButtonEmptyComponent = useMemo(() => { + if (memoizedRouteState && memoizedRouteState.onBackButtonNavigateTo) { + return ; + } + }, [memoizedRouteState]); + + const backButtonHeaderComponent = useMemo(() => { + if (memoizedRouteState && memoizedRouteState.onBackButtonNavigateTo) { + return ; + } + }, [memoizedRouteState]); + const [itemToDelete, setItemToDelete] = useState(null); const includedPoliciesParam = location.included_policies; @@ -155,13 +171,6 @@ export const HostIsolationExceptionsList = () => { [navigateCallback] ); - const backButton = useMemo(() => { - if (routeState && routeState.onBackButtonNavigateTo) { - return ; - } - return null; - }, [routeState]); - const handleAddButtonClick = useCallback( () => navigateCallback({ @@ -193,7 +202,7 @@ export const HostIsolationExceptionsList = () => { return ( { contentClassName="host-isolation-exceptions-container" data-test-subj="hostIsolationExceptionsContent" noItemsMessage={ - !hasDataToShow && + !hasDataToShow && ( + + ) } /> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unassigned.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unassigned.tsx index 4d53682d2d669d..ac944371acdda6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unassigned.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unassigned.tsx @@ -8,7 +8,7 @@ import React, { memo, useCallback } from 'react'; import { EuiButton, EuiEmptyPrompt, EuiPageTemplate, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { usePolicyDetailsNavigateCallback } from '../../policy_hooks'; +import { usePolicyDetailsEventFiltersNavigateCallback } from '../../policy_hooks'; import { useGetLinkTo } from './use_policy_event_filters_empty_hooks'; import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; @@ -21,7 +21,7 @@ export const PolicyEventFiltersEmptyUnassigned = memo(({ policyId, const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; const { onClickHandler, toRouteUrl } = useGetLinkTo(policyId, policyName); - const navigateCallback = usePolicyDetailsNavigateCallback(); + const navigateCallback = usePolicyDetailsEventFiltersNavigateCallback(); const onClickPrimaryButtonHandler = useCallback( () => navigateCallback({ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx index 5b4138480fdf3b..850a303654c521 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx @@ -114,10 +114,23 @@ export const PolicyEventFiltersLayout = React.memo - ) : ( - + return ( + <> + {canCreateArtifactsByPolicy && urlParams.show === 'list' && ( + + )} + {allEventFilters && allEventFilters.total !== 0 ? ( + + ) : ( + + )} + ); } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx index d93ebc47adc6de..a470d4b63e7bda 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx @@ -72,7 +72,7 @@ export const FleetEventFiltersCard = memo( return { backButtonLabel: i18n.translate( 'xpack.securitySolution.endpoint.fleetCustomExtension.backButtonLabel', - { defaultMessage: 'Back to Endpoint Integration' } + { defaultMessage: 'Return to Endpoint Security integrations' } ), onBackButtonNavigateTo: [ INTEGRATIONS_PLUGIN_ID, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_host_isolation_exceptions_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_host_isolation_exceptions_card.tsx index 711df5b82079a3..286047d804ebfc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_host_isolation_exceptions_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_host_isolation_exceptions_card.tsx @@ -73,7 +73,7 @@ export const FleetHostIsolationExceptionsCard = memo void; /** Should the Add button be disabled */ isAddDisabled?: boolean; -}>(({ onAdd, isAddDisabled = false }) => { + backComponent?: React.ReactNode; +}>(({ onAdd, isAddDisabled = false, backComponent }) => { return ( - + } - actions={ + actions={[ - - } + , + ...(backComponent ? [backComponent] : []), + ]} /> - + ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index ab0bbaa875a394..7443e4b0d12a94 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -858,11 +858,31 @@ describe('When on the Trusted Apps Page', () => { expect(button).toHaveAttribute('href', '/fleet'); }); - it('back button is not present', () => { + it('back button is present after push history', () => { reactTestingLibrary.act(() => { history.push('/administration/trusted_apps'); }); - expect(renderResult.queryByTestId('backToOrigin')).toBeNull(); + const button = renderResult.queryByTestId('backToOrigin'); + expect(button).not.toBeNull(); + expect(button).toHaveAttribute('href', '/fleet'); + }); + }); + + describe('and the back button is not present', () => { + let renderResult: ReturnType; + beforeEach(async () => { + renderResult = render(); + await act(async () => { + await waitForAction('trustedAppsListResourceStateChanged'); + }); + reactTestingLibrary.act(() => { + history.push('/administration/trusted_apps'); + }); + }); + + it('back button is not present when missing history params', () => { + const button = renderResult.queryByTestId('backToOrigin'); + expect(button).toBeNull(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index 7990fb66cd7836..6b3f59f44ce122 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -31,9 +31,11 @@ import { AppAction } from '../../../../common/store/actions'; import { ABOUT_TRUSTED_APPS, SEARCH_TRUSTED_APP_PLACEHOLDER } from './translations'; import { EmptyState } from './components/empty_state'; import { SearchExceptions } from '../../../components/search_exceptions'; +import { BackToExternalAppSecondaryButton } from '../../../components/back_to_external_app_secondary_button'; import { BackToExternalAppButton } from '../../../components/back_to_external_app_button'; import { ListPageRouteState } from '../../../../../common/endpoint/types'; import { ManagementPageLoader } from '../../../components/management_page_loader'; +import { useMemoizedRouteState } from '../../../common/hooks'; export const TrustedAppsPage = memo(() => { const dispatch = useDispatch>(); @@ -51,6 +53,20 @@ export const TrustedAppsPage = memo(() => { }) ); + const memoizedRouteState = useMemoizedRouteState(routeState); + + const backButtonEmptyComponent = useMemo(() => { + if (memoizedRouteState && memoizedRouteState.onBackButtonNavigateTo) { + return ; + } + }, [memoizedRouteState]); + + const backButtonHeaderComponent = useMemo(() => { + if (memoizedRouteState && memoizedRouteState.onBackButtonNavigateTo) { + return ; + } + }, [memoizedRouteState]); + const handleAddButtonClick = useTrustedAppsNavigateCallback(() => ({ show: 'create', id: undefined, @@ -75,13 +91,6 @@ export const TrustedAppsPage = memo(() => { [didEntriesExist, doEntriesExist, isCheckingIfEntriesExists] ); - const backButton = useMemo(() => { - if (routeState && routeState.onBackButtonNavigateTo) { - return ; - } - return null; - }, [routeState]); - const addButton = ( { ) : ( - + )} ); @@ -150,13 +163,13 @@ export const TrustedAppsPage = memo(() => { return ( } - headerBackComponent={backButton} subtitle={ABOUT_TRUSTED_APPS} actions={addButton} hideHeader={!canDisplayContent()} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx index 0096b152b32368..dba9f99681a0c2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx @@ -35,6 +35,9 @@ jest.mock('../body/events/index', () => ({ })); jest.mock('../../../../common/containers/sourcerer'); +jest.mock('../../../../common/containers/sourcerer/use_signal_helpers', () => ({ + useSignalHelpers: () => ({ signalIndexNeedsInit: false }), +})); const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx index d1741c399dbabb..a32e77a107a43f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx @@ -38,6 +38,9 @@ jest.mock('../body/events/index', () => ({ })); jest.mock('../../../../common/containers/sourcerer'); +jest.mock('../../../../common/containers/sourcerer/use_signal_helpers', () => ({ + useSignalHelpers: () => ({ signalIndexNeedsInit: false }), +})); const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index ac889ebc55cdf6..50d553045b5d5a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -184,7 +184,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = } } catch (exc) { const errorMessage = buildRuleMessage(`Check privileges failed to execute ${exc}`); - logger.error(errorMessage); + logger.warn(errorMessage); await ruleStatusClient.logStatusChange({ ...basicLogArguments, message: errorMessage, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 13106ec3012be4..6842cc5bda2304 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -661,7 +661,7 @@ describe('utils', () => { }, }, }; - mockLogger.error.mockClear(); + mockLogger.warn.mockClear(); const res = await hasTimestampFields({ timestampField, ruleName: 'myfakerulename', @@ -677,11 +677,12 @@ describe('utils', () => { logger: mockLogger, buildRuleMessage, }); - expect(mockLogger.error).toHaveBeenCalledWith( + expect(mockLogger.warn).toHaveBeenCalledWith( 'The following indices are missing the timestamp override field "event.ingested": ["myfakeindex-1","myfakeindex-2"] name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); expect(res).toBeTruthy(); }); + test('returns true when missing timestamp field', async () => { const timestampField = '@timestamp'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -706,7 +707,7 @@ describe('utils', () => { }, }, }; - mockLogger.error.mockClear(); + mockLogger.warn.mockClear(); const res = await hasTimestampFields({ timestampField, ruleName: 'myfakerulename', @@ -722,7 +723,7 @@ describe('utils', () => { logger: mockLogger, buildRuleMessage, }); - expect(mockLogger.error).toHaveBeenCalledWith( + expect(mockLogger.warn).toHaveBeenCalledWith( 'The following indices are missing the timestamp field "@timestamp": ["myfakeindex-1","myfakeindex-2"] name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); expect(res).toBeTruthy(); @@ -737,7 +738,7 @@ describe('utils', () => { fields: {}, }, }; - mockLogger.error.mockClear(); + mockLogger.warn.mockClear(); const res = await hasTimestampFields({ timestampField, ruleName: 'Endpoint Security', @@ -753,7 +754,7 @@ describe('utils', () => { logger: mockLogger, buildRuleMessage, }); - expect(mockLogger.error).toHaveBeenCalledWith( + expect(mockLogger.warn).toHaveBeenCalledWith( 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated. If you have recently enrolled agents enabled with Endpoint Security through Fleet, this warning should stop once an alert is sent from an agent. name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); expect(res).toBeTruthy(); @@ -768,7 +769,7 @@ describe('utils', () => { fields: {}, }, }; - mockLogger.error.mockClear(); + mockLogger.warn.mockClear(); const res = await hasTimestampFields({ timestampField, ruleName: 'NOT Endpoint Security', @@ -784,7 +785,7 @@ describe('utils', () => { logger: mockLogger, buildRuleMessage, }); - expect(mockLogger.error).toHaveBeenCalledWith( + expect(mockLogger.warn).toHaveBeenCalledWith( 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated. name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); expect(res).toBeTruthy(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 60df18847939b6..4efe356525c1e5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -118,7 +118,7 @@ export const hasReadIndexPrivileges = async (args: { const errorString = `This rule may not have the required read privileges to the following indices/index patterns: ${JSON.stringify( indexesWithNoReadPrivileges )}`; - logger.error(buildRuleMessage(errorString)); + logger.warn(buildRuleMessage(errorString)); await ruleStatusClient.logStatusChange({ message: errorString, ruleId, @@ -168,7 +168,7 @@ export const hasTimestampFields = async (args: { ? 'If you have recently enrolled agents enabled with Endpoint Security through Fleet, this warning should stop once an alert is sent from an agent.' : '' }`; - logger.error(buildRuleMessage(errorString.trimEnd())); + logger.warn(buildRuleMessage(errorString.trimEnd())); await ruleStatusClient.logStatusChange({ message: errorString.trimEnd(), ruleId, @@ -195,7 +195,7 @@ export const hasTimestampFields = async (args: { ? timestampFieldCapsResponse.body.indices : timestampFieldCapsResponse.body.fields[timestampField]?.unmapped?.indices )}`; - logger.error(buildRuleMessage(errorString)); + logger.warn(buildRuleMessage(errorString)); await ruleStatusClient.logStatusChange({ message: errorString, ruleId, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 901fbbef6714b6..36f16182b17e9a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -235,7 +235,6 @@ "console.settingsPage.indicesAndAliasesLabelText": "インデックスとエイリアス", "console.settingsPage.jsonSyntaxLabel": "JSON構文", "console.settingsPage.pageTitle": "コンソール設定", - "console.settingsPage.pollingLabelText": "自動入力候補を自動的に更新", "console.settingsPage.refreshButtonLabel": "自動入力候補の更新", "console.settingsPage.refreshingDataDescription": "コンソールは、Elasticsearchをクエリして自動入力候補を更新します。クラスターが大きい場合や、ネットワークの制限がある場合には、自動更新で問題が発生する可能性があります。", "console.settingsPage.refreshingDataLabel": "自動入力候補を更新しています", @@ -6565,7 +6564,7 @@ "xpack.canvas.error.esService.indicesFetchErrorMessage": "Elasticsearch インデックスを取得できませんでした", "xpack.canvas.error.RenderWithFn.renderErrorMessage": "「{functionName}」のレンダリングが失敗しました", "xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage": "ワークパッドのクローンを作成できませんでした", - "xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage": "ワークパッドをアップロードできませんでした", + "xpack.canvas.error.useUploadWorkpad.uploadFailureErrorMessage": "ワークパッドをアップロードできませんでした", "xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage": "すべてのワークパッドを削除できませんでした", "xpack.canvas.error.useFindWorkpads.findFailureErrorMessage": "ワークパッドが見つかりませんでした", "xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage": "{JSON} 個のファイルしか受け付けられませんでした", @@ -8388,7 +8387,6 @@ "xpack.dataVisualizer.index.fieldNameSelect": "フィールド名", "xpack.dataVisualizer.index.fieldTypeSelect": "フィールド型", "xpack.dataVisualizer.index.fullTimeRangeSelector.errorSettingTimeRangeNotification": "時間範囲の設定中にエラーが発生しました。", - "xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataButtonLabel": "完全な {indexPatternTitle} データを使用", "xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationDescription": "異常検知は時間ベースのインデックスでのみ実行されます", "xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationTitle": "インデックスパターン {indexPatternTitle} は時系列に基づくものではありません", "xpack.dataVisualizer.index.lensChart.averageOfLabel": "{fieldName}の平均", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 76eac37d763c90..4ec36c767a5d35 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -238,7 +238,6 @@ "console.settingsPage.indicesAndAliasesLabelText": "索引和别名", "console.settingsPage.jsonSyntaxLabel": "JSON 语法", "console.settingsPage.pageTitle": "控制台设置", - "console.settingsPage.pollingLabelText": "自动刷新自动完成建议", "console.settingsPage.refreshButtonLabel": "刷新自动完成建议", "console.settingsPage.refreshingDataDescription": "控制台通过查询 Elasticsearch 来刷新自动完成建议。如果您的集群较大或您的网络有限制,则自动刷新可能会造成问题。", "console.settingsPage.refreshingDataLabel": "正在刷新自动完成建议", @@ -6610,7 +6609,7 @@ "xpack.canvas.error.esService.indicesFetchErrorMessage": "无法提取 Elasticsearch 索引", "xpack.canvas.error.RenderWithFn.renderErrorMessage": "呈现“{functionName}”失败。", "xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage": "无法克隆 Workpad", - "xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage": "无法上传 Workpad", + "xpack.canvas.error.useUploadWorkpad.uploadFailureErrorMessage": "无法上传 Workpad", "xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage": "无法删除所有 Workpad", "xpack.canvas.error.useFindWorkpads.findFailureErrorMessage": "无法查找 Workpad", "xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage": "仅接受 {JSON} 文件", @@ -8461,7 +8460,6 @@ "xpack.dataVisualizer.index.fieldNameSelect": "字段名称", "xpack.dataVisualizer.index.fieldTypeSelect": "字段类型", "xpack.dataVisualizer.index.fullTimeRangeSelector.errorSettingTimeRangeNotification": "设置时间范围时出错。", - "xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataButtonLabel": "使用完整的 {indexPatternTitle} 数据", "xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationDescription": "仅针对基于时间的索引运行异常检测", "xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationTitle": "索引模式 {indexPatternTitle} 不基于时间序列", "xpack.dataVisualizer.index.lensChart.averageOfLabel": "{fieldName} 的平均值", diff --git a/x-pack/plugins/uptime/common/config.ts b/x-pack/plugins/uptime/common/config.ts index 38ba7b7b3fd481..e9ea061fcf1600 100644 --- a/x-pack/plugins/uptime/common/config.ts +++ b/x-pack/plugins/uptime/common/config.ts @@ -7,46 +7,51 @@ import { PluginConfigDescriptor } from 'kibana/server'; import { schema, TypeOf } from '@kbn/config-schema'; +import { sslSchema } from '@kbn/server-http-tools'; -export const config: PluginConfigDescriptor = { - exposeToBrowser: { - ui: true, - }, - schema: schema.maybe( +const serviceConfig = schema.object({ + enabled: schema.boolean(), + username: schema.maybe(schema.string()), + password: schema.maybe(schema.string()), + manifestUrl: schema.string(), + hosts: schema.maybe(schema.arrayOf(schema.string())), + syncInterval: schema.maybe(schema.string()), + tls: schema.maybe(sslSchema), + devUrl: schema.maybe(schema.string()), +}); + +const uptimeConfig = schema.object({ + index: schema.maybe(schema.string()), + ui: schema.maybe( schema.object({ - index: schema.maybe(schema.string()), - ui: schema.maybe( - schema.object({ - unsafe: schema.maybe( - schema.object({ - monitorManagement: schema.maybe( - schema.object({ - enabled: schema.boolean(), - }) - ), - }) - ), - }) - ), unsafe: schema.maybe( schema.object({ - service: schema.maybe( + monitorManagement: schema.maybe( schema.object({ enabled: schema.boolean(), - username: schema.string(), - password: schema.string(), - manifestUrl: schema.string(), - hosts: schema.maybe(schema.arrayOf(schema.string())), - syncInterval: schema.maybe(schema.string()), }) ), }) ), }) ), + unsafe: schema.maybe( + schema.object({ + service: serviceConfig, + }) + ), +}); + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: uptimeConfig, }; -export type UptimeConfig = TypeOf; +export type UptimeConfig = TypeOf; +export type ServiceConfig = TypeOf; + export interface UptimeUiConfig { ui?: TypeOf['ui']; } diff --git a/x-pack/plugins/uptime/e2e/journeys/alerts/index.ts b/x-pack/plugins/uptime/e2e/journeys/alerts/index.ts new file mode 100644 index 00000000000000..d8746d715581d6 --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/alerts/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './tls_alert_flyouts_in_alerting_app'; +export * from './status_alert_flyouts_in_alerting_app'; diff --git a/x-pack/plugins/uptime/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts b/x-pack/plugins/uptime/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts new file mode 100644 index 00000000000000..ba973a7aa8a616 --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { journey, step, expect, before } from '@elastic/synthetics'; +import { assertText, byTestId, loginToKibana, waitForLoadingToFinish } from '../utils'; + +journey('StatusFlyoutInAlertingApp', async ({ page, params }) => { + before(async () => { + await waitForLoadingToFinish({ page }); + }); + + const baseUrl = `${params.kibanaUrl}/app/management/insightsAndAlerting/triggersActions/rules`; + + step('Go to Alerting app', async () => { + await page.goto(`${baseUrl}`, { + waitUntil: 'networkidle', + }); + await loginToKibana({ page }); + }); + + step('Open monitor status flyout', async () => { + await page.click(byTestId('createFirstAlertButton')); + await waitForLoadingToFinish({ page }); + await page.click(byTestId('"xpack.uptime.alerts.monitorStatus-SelectOption"')); + await waitForLoadingToFinish({ page }); + await assertText({ page, text: 'This alert will apply to approximately 0 monitors.' }); + }); + + step('can add filters', async () => { + await page.click('text=Add filter'); + await page.click(byTestId('"uptimeAlertAddFilter.monitor.type"')); + await page.click(byTestId('"uptimeCreateStatusAlert.filter_scheme"')); + }); + + step('can open query bar', async () => { + await page.click(byTestId('"xpack.uptime.alerts.monitorStatus.filterBar"')); + + await page.fill(byTestId('"xpack.uptime.alerts.monitorStatus.filterBar"'), 'monitor.type : '); + + await waitForLoadingToFinish({ page }); + + await assertText({ page, text: 'browser' }); + await assertText({ page, text: 'http' }); + + const suggestionItem = await page.$(byTestId('autoCompleteSuggestionText')); + expect(await suggestionItem?.textContent()).toBe('"browser" '); + + await page.click(byTestId('euiFlyoutCloseButton')); + await page.click(byTestId('confirmModalConfirmButton')); + }); + + step('Open tls alert flyout', async () => { + await page.click(byTestId('createFirstAlertButton')); + await waitForLoadingToFinish({ page }); + await page.click(byTestId('"xpack.uptime.alerts.tlsCertificate-SelectOption"')); + await waitForLoadingToFinish({ page }); + await assertText({ page, text: 'has a certificate expiring within' }); + }); + + step('Tls alert flyout has setting values', async () => { + await assertText({ page, text: '30 days' }); + await assertText({ page, text: '730 days' }); + }); +}); diff --git a/x-pack/plugins/uptime/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts b/x-pack/plugins/uptime/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts new file mode 100644 index 00000000000000..024e8e53c3b2a1 --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { journey, step, before } from '@elastic/synthetics'; +import { assertText, byTestId, loginToKibana, waitForLoadingToFinish } from '../utils'; + +journey('TlsFlyoutInAlertingApp', async ({ page, params }) => { + before(async () => { + await waitForLoadingToFinish({ page }); + }); + + const baseUrl = `${params.kibanaUrl}/app/management/insightsAndAlerting/triggersActions/rules`; + + step('Go to Alerting app', async () => { + await page.goto(`${baseUrl}`, { + waitUntil: 'networkidle', + }); + await loginToKibana({ page }); + }); + + step('Open tls alert flyout', async () => { + await page.click(byTestId('createFirstAlertButton')); + await waitForLoadingToFinish({ page }); + await page.click(byTestId('"xpack.uptime.alerts.tlsCertificate-SelectOption"')); + await waitForLoadingToFinish({ page }); + await assertText({ page, text: 'has a certificate expiring within' }); + }); + + step('Tls alert flyout has setting values', async () => { + await assertText({ page, text: '30 days' }); + await assertText({ page, text: '730 days' }); + }); +}); diff --git a/x-pack/plugins/uptime/e2e/journeys/index.ts b/x-pack/plugins/uptime/e2e/journeys/index.ts index 89abed5ce8f298..6bdea1beb016b9 100644 --- a/x-pack/plugins/uptime/e2e/journeys/index.ts +++ b/x-pack/plugins/uptime/e2e/journeys/index.ts @@ -7,3 +7,4 @@ export * from './uptime.journey'; export * from './step_duration.journey'; +export * from './alerts'; diff --git a/x-pack/plugins/uptime/e2e/journeys/utils.ts b/x-pack/plugins/uptime/e2e/journeys/utils.ts index 3188c86f820498..6d2f1dd5541086 100644 --- a/x-pack/plugins/uptime/e2e/journeys/utils.ts +++ b/x-pack/plugins/uptime/e2e/journeys/utils.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { Page } from '@elastic/synthetics'; -import { byTestId } from './uptime.journey'; +import { expect, Page } from '@elastic/synthetics'; export async function waitForLoadingToFinish({ page }: { page: Page }) { while (true) { @@ -25,3 +24,12 @@ export async function loginToKibana({ page }: { page: Page }) { await waitForLoadingToFinish({ page }); } + +export const byTestId = (testId: string) => { + return `[data-test-subj=${testId}]`; +}; + +export const assertText = async ({ page, text }: { page: Page; text: string }) => { + await page.waitForSelector(`text=${text}`); + expect(await page.$(`text=${text}`)).toBeTruthy(); +}; diff --git a/x-pack/plugins/uptime/e2e/playwright_start.ts b/x-pack/plugins/uptime/e2e/playwright_start.ts index fe4d3ff804bf92..0581692e0e278f 100644 --- a/x-pack/plugins/uptime/e2e/playwright_start.ts +++ b/x-pack/plugins/uptime/e2e/playwright_start.ts @@ -13,18 +13,22 @@ import { esArchiverLoad, esArchiverUnload } from './tasks/es_archiver'; import './journeys'; +const listOfJourneys = [ + 'uptime', + 'StepsDuration', + 'TlsFlyoutInAlertingApp', + 'StatusFlyoutInAlertingApp', +] as const; + export function playwrightRunTests({ headless, match }: { headless: boolean; match?: string }) { return async ({ getService }: any) => { const result = await playwrightStart(getService, headless, match); - if ( - result?.uptime && - result.uptime.status !== 'succeeded' && - result.StepsDuration && - result.StepsDuration.status !== 'succeeded' - ) { - throw new Error('Tests failed'); - } + listOfJourneys.forEach((journey) => { + if (result?.[journey] && result[journey].status !== 'succeeded') { + throw new Error('Tests failed'); + } + }); }; } diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index f3971b6bd4bf3c..35be0b19d45219 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -1,43 +1,27 @@ { - "configPath": [ - "xpack", - "uptime" - ], + "configPath": ["xpack", "uptime"], "id": "uptime", "kibanaVersion": "kibana", - "optionalPlugins": [ - "cloud", - "data", - "fleet", - "home", - "ml" - ], + "optionalPlugins": ["cloud", "data", "fleet", "home", "ml"], "requiredPlugins": [ "alerting", "embeddable", "encryptedSavedObjects", - "inspector", "features", + "inspector", "licensing", "observability", "ruleRegistry", "security", + "share", + "taskManager", "triggersActionsUi", - "usageCollection", - "taskManager" + "usageCollection" ], "server": true, "ui": true, "version": "8.0.0", - "requiredBundles": [ - "observability", - "kibanaReact", - "kibanaUtils", - "home", - "data", - "ml", - "fleet" - ], + "requiredBundles": ["data", "fleet", "home", "kibanaReact", "kibanaUtils", "ml", "observability"], "owner": { "name": "Uptime", "githubTeam": "uptime" diff --git a/x-pack/plugins/uptime/public/apps/locators/overview.test.ts b/x-pack/plugins/uptime/public/apps/locators/overview.test.ts new file mode 100644 index 00000000000000..c414778f7769cc --- /dev/null +++ b/x-pack/plugins/uptime/public/apps/locators/overview.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { OVERVIEW_ROUTE } from '../../../common/constants'; +import { uptimeOverviewNavigatorParams } from './overview'; + +describe('uptimeOverviewNavigatorParams', () => { + it('supplies the correct app name', async () => { + const location = await uptimeOverviewNavigatorParams.getLocation({}); + expect(location.app).toEqual('uptime'); + }); + + it('creates the expected path when no params specified', async () => { + const location = await uptimeOverviewNavigatorParams.getLocation({}); + expect(location.path).toEqual(OVERVIEW_ROUTE); + }); + + it('creates a path with expected search when ip is specified', async () => { + const location = await uptimeOverviewNavigatorParams.getLocation({ ip: '127.0.0.1' }); + expect(location.path).toEqual(`${OVERVIEW_ROUTE}?search=monitor.ip: "127.0.0.1"`); + }); + + it('creates a path with expected search when hostname is specified', async () => { + const location = await uptimeOverviewNavigatorParams.getLocation({ hostname: 'elastic.co' }); + expect(location.path).toEqual(`${OVERVIEW_ROUTE}?search=url.domain: "elastic.co"`); + }); + + it('creates a path with expected search when multiple keys are specified', async () => { + const location = await uptimeOverviewNavigatorParams.getLocation({ + hostname: 'elastic.co', + ip: '127.0.0.1', + }); + expect(location.path).toEqual( + `${OVERVIEW_ROUTE}?search=monitor.ip: "127.0.0.1" OR url.domain: "elastic.co"` + ); + }); +}); diff --git a/x-pack/plugins/uptime/public/apps/locators/overview.ts b/x-pack/plugins/uptime/public/apps/locators/overview.ts new file mode 100644 index 00000000000000..d7faf7b78f7973 --- /dev/null +++ b/x-pack/plugins/uptime/public/apps/locators/overview.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 { uptimeOverviewLocatorID } from '../../../../observability/public'; +import { OVERVIEW_ROUTE } from '../../../common/constants'; + +const formatSearchKey = (key: string, value: string) => `${key}: "${value}"`; + +async function navigate({ ip, hostname }: { ip?: string; hostname?: string }) { + const searchParams: string[] = []; + + if (ip) searchParams.push(formatSearchKey('monitor.ip', ip)); + if (hostname) searchParams.push(formatSearchKey('url.domain', hostname)); + + const searchString = searchParams.join(' OR '); + + const path = + searchParams.length === 0 ? OVERVIEW_ROUTE : OVERVIEW_ROUTE + `?search=${searchString}`; + + return { + app: 'uptime', + path, + state: {}, + }; +} + +export const uptimeOverviewNavigatorParams = { + id: uptimeOverviewLocatorID, + getLocation: navigate, +}; diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index ec6deef429ca9a..dd2287b3b16422 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { CoreSetup, CoreStart, @@ -14,6 +15,7 @@ import { import { from } from 'rxjs'; import { map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; +import { SharePluginSetup, SharePluginStart } from '../../../../../src/plugins/share/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../../src/core/public'; import { @@ -29,6 +31,7 @@ import { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../../src/plugins/data/public'; + import { alertTypeInitializers, legacyAlertTypeInitializers } from '../lib/alert_types'; import { FleetStart } from '../../../fleet/public'; import { @@ -47,19 +50,21 @@ import { Start as InspectorPluginStart } from '../../../../../src/plugins/inspec import { UptimeUiConfig } from '../../common/config'; export interface ClientPluginsSetup { - data: DataPublicPluginSetup; home?: HomePublicPluginSetup; + data: DataPublicPluginSetup; observability: ObservabilityPublicSetup; + share: SharePluginSetup; triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; } export interface ClientPluginsStart { - embeddable: EmbeddableStart; - data: DataPublicPluginStart; - triggersActionsUi: TriggersAndActionsUIPublicPluginStart; fleet?: FleetStart; - observability: ObservabilityPublicStart; + data: DataPublicPluginStart; inspector: InspectorPluginStart; + embeddable: EmbeddableStart; + observability: ObservabilityPublicStart; + share: SharePluginStart; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; } export interface UptimePluginServices extends Partial { diff --git a/x-pack/plugins/uptime/public/apps/render_app.tsx b/x-pack/plugins/uptime/public/apps/render_app.tsx index cc831680dbf09c..23f8fc9a8e58ce 100644 --- a/x-pack/plugins/uptime/public/apps/render_app.tsx +++ b/x-pack/plugins/uptime/public/apps/render_app.tsx @@ -18,6 +18,7 @@ import { import { UptimeApp, UptimeAppProps } from './uptime_app'; import { ClientPluginsSetup, ClientPluginsStart } from './plugin'; import { UptimeUiConfig } from '../../common/config'; +import { uptimeOverviewNavigatorParams } from './locators/overview'; export function renderApp( core: CoreStart, @@ -41,6 +42,8 @@ export function renderApp( const canSave = (capabilities.uptime.save ?? false) as boolean; + plugins.share.url.locators.create(uptimeOverviewNavigatorParams); + const props: UptimeAppProps = { plugins, canSave, 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 9ff6701d20f7a8..b0fe387613f40e 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 @@ -9,14 +9,14 @@ import React from 'react'; import { EuiHeaderLinks, EuiToolTip, EuiHeaderLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useRouteMatch } from 'react-router-dom'; import { useSelector } from 'react-redux'; import { createExploratoryViewUrl } from '../../../../../observability/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context'; import { useGetUrlParams } from '../../../hooks'; import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers'; -import { MONITOR_MANAGEMENT, SETTINGS_ROUTE } from '../../../../common/constants'; +import { MONITOR_MANAGEMENT, MONITOR_ROUTE, SETTINGS_ROUTE } from '../../../../common/constants'; import { stringifyUrlParams } from '../../../lib/helper/stringify_url_params'; import { InspectorHeaderLink } from './inspector_header_link'; import { monitorStatusSelector } from '../../../state/selectors'; @@ -44,6 +44,7 @@ export function ActionMenuContent({ config }: { config: UptimeConfig }): React.R const selectedMonitor = useSelector(monitorStatusSelector); + const detailRouteMatch = useRouteMatch(MONITOR_ROUTE); const monitorId = selectedMonitor?.monitor?.id; const syntheticExploratoryViewLink = createExploratoryViewUrl( @@ -57,7 +58,10 @@ export function ActionMenuContent({ config }: { config: UptimeConfig }): React.R time: { from: dateRangeStart, to: dateRangeEnd }, breakdown: monitorId ? 'observer.geo.name' : 'monitor.type', reportDefinitions: { - 'monitor.name': selectedMonitor?.monitor?.name ? [selectedMonitor?.monitor?.name] : [], + 'monitor.name': + selectedMonitor?.monitor?.name && detailRouteMatch?.isExact === true + ? [selectedMonitor?.monitor?.name] + : [], 'url.full': ['ALL_VALUES'], }, name: monitorId ? `${monitorId}-response-duration` : 'All monitors response duration', diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.test.tsx index d352ccef51a944..14d3495cb96e24 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.test.tsx @@ -9,11 +9,11 @@ import React from 'react'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { render } from '../../../lib/helper/rtl_helpers'; -import { DataStream, HTTPFields, ScheduleUnit } from '../../../../common/runtime_types'; +import { ConfigKey, DataStream, HTTPFields, ScheduleUnit } from '../../../../common/runtime_types'; import { MonitorManagementList } from './monitor_list'; import { MonitorManagementList as MonitorManagementListState } from '../../../state/reducers/monitor_management'; -describe('', () => { +describe('', () => { const setRefresh = jest.fn(); const setPageSize = jest.fn(); const setPageIndex = jest.fn(); @@ -110,4 +110,64 @@ describe('', () => { expect(setPageIndex).toBeCalledWith(2); expect(setRefresh).toBeCalledWith(true); }); + + it.each([ + [DataStream.BROWSER, ConfigKey.SOURCE_INLINE], + [DataStream.HTTP, ConfigKey.URLS], + [DataStream.TCP, ConfigKey.HOSTS], + [DataStream.ICMP, ConfigKey.HOSTS], + ])( + 'appends inline to the monitor id for browser monitors and omits for lightweight checks', + (type, configKey) => { + const id = '123456'; + const name = 'sample monitor'; + const browserState = { + monitorManagementList: { + ...state.monitorManagementList, + list: { + ...state.monitorManagementList.list, + monitors: [ + { + id, + attributes: { + name, + schedule: { + unit: ScheduleUnit.MINUTES, + number: '1', + }, + [configKey]: 'test', + type, + tags: [`tag-1`], + }, + }, + ], + }, + }, + }; + + render( + , + { state: browserState } + ); + + const link = screen.getByText(name) as HTMLAnchorElement; + + expect(link.href).toEqual( + expect.stringContaining( + `/app/uptime/monitor/${Buffer.from( + `${id}${type === DataStream.BROWSER ? `-inline` : ''}`, + 'utf8' + ).toString('base64')}` + ) + ); + + expect(setPageIndex).toBeCalledWith(2); + expect(setRefresh).toBeCalledWith(true); + } + ); }); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx index 75c94c2d07d1e0..a0785df79bd756 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { EuiBasicTable, EuiPanel, EuiSpacer, EuiLink } from '@elastic/eui'; import { SyntheticsMonitorSavedObject } from '../../../../common/types'; import { MonitorManagementList as MonitorManagementListState } from '../../../state/reducers/monitor_management'; -import { MonitorFields, SyntheticsMonitor } from '../../../../common/runtime_types'; +import { DataStream, MonitorFields, SyntheticsMonitor } from '../../../../common/runtime_types'; import { UptimeSettingsContext } from '../../../contexts'; import { Actions } from './actions'; import { MonitorLocations } from './monitor_locations'; @@ -66,14 +66,19 @@ export const MonitorManagementList = ({ defaultMessage: 'Monitor name', }), render: ({ - attributes: { name }, + attributes: { name, type }, id, }: { attributes: Partial; id: string; }) => ( {name} diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_tls.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_tls.tsx index 82917fc4e17580..9f3da1674ca09b 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_tls.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_tls.tsx @@ -6,10 +6,11 @@ */ import { useDispatch, useSelector } from 'react-redux'; -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { AlertTlsComponent } from '../alert_tls'; import { setAlertFlyoutVisible } from '../../../../state/actions'; import { selectDynamicSettings } from '../../../../state/selectors'; +import { getDynamicSettings } from '../../../../state/actions/dynamic_settings'; export const AlertTls: React.FC<{}> = () => { const dispatch = useDispatch(); @@ -18,6 +19,13 @@ export const AlertTls: React.FC<{}> = () => { [dispatch] ); const { settings } = useSelector(selectDynamicSettings); + + useEffect(() => { + if (typeof settings === 'undefined') { + dispatch(getDynamicSettings()); + } + }, [dispatch, settings]); + return ( { disabled={false} flush="left" iconType="plusInCircleFilled" + isLoading={false} onClick={[Function]} size="s" > @@ -90,6 +91,7 @@ describe('AddFilterButton component', () => { disabled={false} flush="left" iconType="plusInCircleFilled" + isLoading={false} onClick={[Function]} size="s" > @@ -143,6 +145,7 @@ describe('AddFilterButton component', () => { disabled={true} flush="left" iconType="plusInCircleFilled" + isLoading={false} onClick={[Function]} size="s" > diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx index 66f0f296b12482..58b8e7bb085da5 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx @@ -8,6 +8,7 @@ import React, { useState } from 'react'; import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import * as labels from '../translations'; +import { useIndexPattern } from '../../../../contexts/uptime_index_pattern_context'; interface Props { newFilters: string[]; @@ -20,6 +21,8 @@ export const AddFilterButton: React.FC = ({ newFilters, onNewFilter, aler const getSelectedItems = (fieldName: string) => alertFilters?.[fieldName] ?? []; + const indexPattern = useIndexPattern(); + const onButtonClick = () => { setPopover(!isPopoverOpen); }; @@ -62,6 +65,7 @@ export const AddFilterButton: React.FC = ({ newFilters, onNewFilter, aler onClick={onButtonClick} size="s" flush="left" + isLoading={!indexPattern} > {labels.ADD_FILTER} diff --git a/x-pack/plugins/uptime/public/contexts/uptime_index_pattern_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_index_pattern_context.tsx index 8171f7e19865f4..6c658ec7f5d408 100644 --- a/x-pack/plugins/uptime/public/contexts/uptime_index_pattern_context.tsx +++ b/x-pack/plugins/uptime/public/contexts/uptime_index_pattern_context.tsx @@ -5,12 +5,10 @@ * 2.0. */ -import React, { createContext, useContext, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import React, { createContext, useContext } from 'react'; import { useFetcher } from '../../../observability/public'; import { DataPublicPluginStart, IndexPattern } from '../../../../../src/plugins/data/public'; -import { indexStatusSelector, selectDynamicSettings } from '../state/selectors'; -import { getDynamicSettings } from '../state/actions/dynamic_settings'; +import { useHasData } from '../components/overview/empty_state/use_has_data'; export const UptimeIndexPatternContext = createContext({} as IndexPattern); @@ -18,16 +16,7 @@ export const UptimeIndexPatternContextProvider: React.FC<{ data: DataPublicPlugi children, data: { indexPatterns }, }) => { - const { settings } = useSelector(selectDynamicSettings); - const { data: indexStatus } = useSelector(indexStatusSelector); - - const dispatch = useDispatch(); - - useEffect(() => { - if (typeof settings === 'undefined') { - dispatch(getDynamicSettings()); - } - }, [dispatch, settings]); + const { settings, data: indexStatus } = useHasData(); const heartbeatIndices = settings?.heartbeatIndices || ''; diff --git a/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts b/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts index 024e387d235477..ffe7c61c7a4e38 100644 --- a/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts +++ b/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { takeLatest, put, call } from 'redux-saga/effects'; +import { takeLeading, put, call, takeLatest } from 'redux-saga/effects'; import { Action } from 'redux-actions'; import { i18n } from '@kbn/i18n'; import { fetchEffectFactory } from './fetch_effect'; @@ -25,7 +25,7 @@ import { DynamicSettings } from '../../../common/runtime_types'; import { kibanaService } from '../kibana_service'; export function* fetchDynamicSettingsEffect() { - yield takeLatest( + yield takeLeading( String(getDynamicSettings), fetchEffectFactory(getDynamicSettingsAPI, getDynamicSettingsSuccess, getDynamicSettingsFail) ); diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index 988cb3ddb9447b..18c72d7c35cb16 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -47,6 +47,7 @@ export interface UptimeServerSetup { fleet: FleetStartContract; security: SecurityPluginStart; savedObjectsClient?: SavedObjectsClientContract; + authSavedObjectsClient?: SavedObjectsClientContract; encryptedSavedObjects: EncryptedSavedObjectsPluginStart; syntheticsService: SyntheticsService; } diff --git a/x-pack/plugins/uptime/server/lib/saved_objects/saved_objects.ts b/x-pack/plugins/uptime/server/lib/saved_objects/saved_objects.ts index 5aa6b7ea7c5a9e..5fc99816df006a 100644 --- a/x-pack/plugins/uptime/server/lib/saved_objects/saved_objects.ts +++ b/x-pack/plugins/uptime/server/lib/saved_objects/saved_objects.ts @@ -38,7 +38,7 @@ export const registerUptimeSavedObjects = ( }; export interface UMSavedObjectsAdapter { - config: UptimeConfig; + config: UptimeConfig | null; getUptimeDynamicSettings: UMSavedObjectsQueryFn; setUptimeDynamicSettings: UMSavedObjectsQueryFn; } diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.ts index 3cf37758b7cec2..cd90828f93ccfd 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.ts @@ -22,7 +22,7 @@ export const getAPIKeyForSyntheticsService = async ({ server: UptimeServerSetup; request?: KibanaRequest; }): Promise => { - const { security, encryptedSavedObjects, savedObjectsClient } = server; + const { security, encryptedSavedObjects, authSavedObjectsClient } = server; const encryptedClient = encryptedSavedObjects.getClient({ includedHiddenTypes: [syntheticsServiceApiKey.name], @@ -37,17 +37,22 @@ export const getAPIKeyForSyntheticsService = async ({ // TODO: figure out how to handle decryption errors } - return await generateAndSaveAPIKey({ request, security, savedObjectsClient }); + return await generateAndSaveAPIKey({ + request, + security, + authSavedObjectsClient, + }); }; export const generateAndSaveAPIKey = async ({ security, request, - savedObjectsClient, + authSavedObjectsClient, }: { request?: KibanaRequest; security: SecurityPluginStart; - savedObjectsClient?: SavedObjectsClientContract; + // authSavedObject is needed for write operations + authSavedObjectsClient?: SavedObjectsClientContract; }) => { const isApiKeysEnabled = await security.authc.apiKeys?.areAPIKeysEnabled(); @@ -81,9 +86,9 @@ export const generateAndSaveAPIKey = async ({ if (apiKeyResult) { const { id, name, api_key: apiKey } = apiKeyResult; const apiKeyObject = { id, name, apiKey }; - if (savedObjectsClient) { + if (authSavedObjectsClient) { // discard decoded key and rest of the keys - await setSyntheticsServiceApiKey(savedObjectsClient, apiKeyObject); + await setSyntheticsServiceApiKey(authSavedObjectsClient, apiKeyObject); } return apiKeyObject; } diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.test.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.test.ts index f028d5e154a567..496f39557adb19 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.test.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.test.ts @@ -25,7 +25,10 @@ describe('getEsHostsTest', () => { it('should return expected host in cloud', function () { const esHosts = getEsHosts({ cloud: cloudSetup, - config: {}, + config: { + enabled: true, + manifestUrl: 'https://testing.com', + }, }); expect(esHosts).toEqual([ @@ -36,11 +39,9 @@ describe('getEsHostsTest', () => { it('should return expected host from config', function () { const esHosts = getEsHosts({ config: { - unsafe: { - service: { - hosts: ['http://localhost:9200'], - }, - }, + enabled: true, + manifestUrl: 'https://testing.com', + hosts: ['http://localhost:9200'], }, }); @@ -50,11 +51,9 @@ describe('getEsHostsTest', () => { const esHosts = getEsHosts({ cloud: cloudSetup, config: { - unsafe: { - service: { - hosts: ['http://localhost:9200'], - }, - }, + enabled: true, + manifestUrl: 'https://testing.com', + hosts: ['http://localhost:9200'], }, }); diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.ts index d0de73b73e23e5..847fcfa9db834c 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.ts @@ -14,14 +14,14 @@ import { CloudSetup } from '../../../../cloud/server'; import { decodeCloudId } from '../../../../fleet/common'; -import { UptimeConfig } from '../../../common/config'; +import { ServiceConfig } from '../../../common/config'; export function getEsHosts({ cloud, config, }: { cloud?: CloudSetup; - config: UptimeConfig; + config: ServiceConfig; }): string[] { const cloudId = cloud?.isCloudEnabled && cloud.cloudId; const cloudUrl = cloudId && decodeCloudId(cloudId)?.elasticsearchUrl; @@ -30,7 +30,7 @@ export function getEsHosts({ return cloudHosts; } - const flagHosts = config?.unsafe?.service?.hosts; + const flagHosts = config.hosts; if (flagHosts && flagHosts.length > 0) { return flagHosts; diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts index 736e73da711346..1c55b8812d64fd 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts @@ -8,10 +8,13 @@ import axios from 'axios'; import { forkJoin, from as rxjsFrom, Observable, of } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; +import * as https from 'https'; +import { SslConfig } from '@kbn/server-http-tools'; import { getServiceLocations } from './get_service_locations'; import { Logger } from '../../../../../../src/core/server'; import { MonitorFields, ServiceLocations } from '../../../common/runtime_types'; import { convertToDataStreamFormat } from './formatters/convert_to_data_stream'; +import { ServiceConfig } from '../../../common/config'; const TEST_SERVICE_USERNAME = 'localKibanaIntegrationTestsUser'; @@ -24,14 +27,25 @@ export interface ServiceData { } export class ServiceAPIClient { - private readonly username: string; + private readonly username?: string; + private readonly devUrl?: string; private readonly authorization: string; private locations: ServiceLocations; private logger: Logger; + private readonly config: ServiceConfig; - constructor(manifestUrl: string, username: string, password: string, logger: Logger) { + constructor(logger: Logger, config: ServiceConfig) { + this.config = config; + const { username, password, manifestUrl, devUrl } = config; this.username = username; - this.authorization = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'); + this.devUrl = devUrl; + + if (username && password) { + this.authorization = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'); + } else { + this.authorization = ''; + } + this.logger = logger; this.locations = []; @@ -40,6 +54,19 @@ export class ServiceAPIClient { }); } + getHttpsAgent() { + const config = this.config; + if (config.tls && config.tls.certificate && config.tls.key) { + const tlsConfig = new SslConfig(config.tls); + + return new https.Agent({ + rejectUnauthorized: true, // (NOTE: this will disable client verification) + cert: tlsConfig.certificate, + key: tlsConfig.key, + }); + } + } + async post(data: ServiceData) { return this.callAPI('POST', data); } @@ -66,11 +93,14 @@ export class ServiceAPIClient { return axios({ method, - url: url + '/monitors', + url: (this.devUrl ?? url) + '/monitors', data: { monitors: monitorsStreams, output }, - headers: { - Authorization: this.authorization, - }, + headers: this.authorization + ? { + Authorization: this.authorization, + } + : undefined, + httpsAgent: this.getHttpsAgent(), }); }; @@ -88,6 +118,9 @@ export class ServiceAPIClient { rxjsFrom(callServiceEndpoint(locMonitors, url)).pipe( tap((result) => { this.logger.debug(result.data); + this.logger.debug( + `Successfully called service with method ${method} with ${allMonitors.length} monitors ` + ); }), catchError((err) => { pushErrors.push({ locationId: id, error: err }); diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts index 82a901192b0eeb..d6fe86453a1c00 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts @@ -20,7 +20,7 @@ import { SyntheticsServiceApiKey } from '../../../common/runtime_types/synthetic import { getAPIKeyForSyntheticsService } from './get_api_key'; import { syntheticsMonitorType } from '../saved_objects/synthetics_monitor'; import { getEsHosts } from './get_es_hosts'; -import { UptimeConfig } from '../../../common/config'; +import { ServiceConfig } from '../../../common/config'; import { ServiceAPIClient } from './service_api_client'; import { formatMonitorConfig } from './formatters/format_configs'; import { @@ -40,19 +40,17 @@ export class SyntheticsService { private readonly server: UptimeServerSetup; private apiClient: ServiceAPIClient; - private readonly config: UptimeConfig; + private readonly config: ServiceConfig; private readonly esHosts: string[]; private apiKey: SyntheticsServiceApiKey | undefined; - constructor(logger: Logger, server: UptimeServerSetup) { + constructor(logger: Logger, server: UptimeServerSetup, config: ServiceConfig) { this.logger = logger; this.server = server; - this.config = server.config; + this.config = config; - const { manifestUrl, username, password } = this.config.unsafe.service; - - this.apiClient = new ServiceAPIClient(manifestUrl, username, password, logger); + this.apiClient = new ServiceAPIClient(logger, this.config); this.esHosts = getEsHosts({ config: this.config, cloud: server.cloud }); } @@ -116,8 +114,7 @@ export class SyntheticsService { public async scheduleSyncTask( taskManager: TaskManagerStartContract ): Promise { - const interval = - this.config.unsafe.service.syncInterval ?? SYNTHETICS_SERVICE_SYNC_INTERVAL_DEFAULT; + const interval = this.config.syncInterval ?? SYNTHETICS_SERVICE_SYNC_INTERVAL_DEFAULT; try { await taskManager.removeIfExists(SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_ID); @@ -152,6 +149,7 @@ export class SyntheticsService { try { this.apiKey = await getAPIKeyForSyntheticsService({ server: this.server, request }); } catch (err) { + this.logger.error(err); throw err; } } @@ -162,6 +160,8 @@ export class SyntheticsService { throw error; } + this.logger.debug('Found api key and esHosts for service.'); + return { hosts: this.esHosts, api_key: `${this.apiKey.id}:${this.apiKey.apiKey}`, @@ -171,6 +171,7 @@ export class SyntheticsService { async pushConfigs(request?: KibanaRequest, configs?: SyntheticsMonitorWithId[]) { const monitors = this.formatConfigs(configs || (await this.getMonitorConfigs())); if (monitors.length === 0) { + this.logger.debug('No monitor found which can be pushed to service.'); return; } const data = { @@ -178,6 +179,8 @@ export class SyntheticsService { output: await this.getOutput(request), }; + this.logger.debug(`${monitors.length} monitors will be pushed to synthetics service.`); + try { return await this.apiClient.post(data); } catch (e) { diff --git a/x-pack/plugins/uptime/server/plugin.ts b/x-pack/plugins/uptime/server/plugin.ts index 692607041ea807..4c076db0255ef2 100644 --- a/x-pack/plugins/uptime/server/plugin.ts +++ b/x-pack/plugins/uptime/server/plugin.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { PluginInitializerContext, CoreStart, @@ -75,7 +74,12 @@ export class Plugin implements PluginType { } as UptimeServerSetup; if (this.server?.config?.unsafe?.service.enabled) { - this.syntheticService = new SyntheticsService(this.logger, this.server); + this.syntheticService = new SyntheticsService( + this.logger, + this.server, + this.server.config.unsafe.service + ); + this.syntheticService.registerSyncTask(plugins.taskManager); } @@ -111,7 +115,7 @@ export class Plugin implements PluginType { this.server.savedObjectsClient = this.savedObjectsClient; } - if (this.server?.config?.unsafe?.service.enabled) { + if (this.server?.config?.unsafe?.service?.enabled) { this.syntheticService?.init(); this.syntheticService?.scheduleSyncTask(plugins.taskManager); if (this.server && this.syntheticService) { diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts index ecf95c7e9175a2..dfd0dcd1a9107c 100644 --- a/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts @@ -14,5 +14,5 @@ export const getServiceLocationsRoute: UMRestApiRouteFactory = () => ({ path: API_URLS.SERVICE_LOCATIONS, validate: {}, handler: async ({ server }): Promise => - getServiceLocations({ manifestUrl: server.config.unsafe.service.manifestUrl }), + getServiceLocations({ manifestUrl: server.config.unsafe!.service.manifestUrl }), }); 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 faefb71e34f661..47c25bca6f900b 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 @@ -31,7 +31,7 @@ export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute, server) => } // specifically needed for the synthetics service api key generation - server.savedObjectsClient = savedObjectsClient; + server.authSavedObjectsClient = savedObjectsClient; const isInspectorEnabled = await context.core.uiSettings.client.get( enableInspectEsQueries diff --git a/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.spec.ts b/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.spec.ts index beaaae24fb2209..def2e2a4c48e5c 100644 --- a/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.spec.ts +++ b/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.spec.ts @@ -73,13 +73,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('has correct series overall values', () => { expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` - Array [ - 0.714, - 0.3877, - 0.75, - 0.2543, - ] - `); + Array [ + 0.714, + 0.3877, + 0.75, + 0.2543, + ] + `); }); }); @@ -105,11 +105,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('has correct series overall values', () => { expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` - Array [ - 0.722093920925555, - 0.718173546796348, - ] - `); + Array [ + 0.722093920925555, + 0.718173546796348, + ] + `); }); }); }); @@ -375,11 +375,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('has correct series overall values', () => { expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` - Array [ - 0, - 3, - ] - `); + Array [ + 0, + 3, + ] + `); }); }); diff --git a/x-pack/test/cases_api_integration/common/fixtures/plugins/cases_client_user/kibana.json b/x-pack/test/cases_api_integration/common/fixtures/plugins/cases_client_user/kibana.json index c8ccea36bd6c3e..950e3b23f6a347 100644 --- a/x-pack/test/cases_api_integration/common/fixtures/plugins/cases_client_user/kibana.json +++ b/x-pack/test/cases_api_integration/common/fixtures/plugins/cases_client_user/kibana.json @@ -1,8 +1,8 @@ { "id": "casesClientUserFixture", "owner": { - "githubTeam": "security-threat-hunting", - "name": "Security Solution Threat Hunting" + "githubTeam": "response-ops", + "name": "ResponseOps" }, "version": "1.0.0", "kibanaVersion": "kibana", diff --git a/x-pack/test/cases_api_integration/common/fixtures/plugins/observability/kibana.json b/x-pack/test/cases_api_integration/common/fixtures/plugins/observability/kibana.json index 783a9b60e22a67..eb95b32242e2bf 100644 --- a/x-pack/test/cases_api_integration/common/fixtures/plugins/observability/kibana.json +++ b/x-pack/test/cases_api_integration/common/fixtures/plugins/observability/kibana.json @@ -1,8 +1,8 @@ { "id": "observabilityFixtures", "owner": { - "githubTeam": "security-threat-hunting", - "name": "Security Solution Threat Hunting" + "githubTeam": "response-ops", + "name": "ResponseOps" }, "version": "1.0.0", "kibanaVersion": "kibana", diff --git a/x-pack/test/cases_api_integration/common/fixtures/plugins/security_solution/kibana.json b/x-pack/test/cases_api_integration/common/fixtures/plugins/security_solution/kibana.json index a0d33c9ec09e88..885b3cbfbf0002 100644 --- a/x-pack/test/cases_api_integration/common/fixtures/plugins/security_solution/kibana.json +++ b/x-pack/test/cases_api_integration/common/fixtures/plugins/security_solution/kibana.json @@ -1,8 +1,8 @@ { "id": "securitySolutionFixtures", "owner": { - "githubTeam": "security-threat-hunting", - "name": "Security Solution Threat Hunting" + "githubTeam": "response-ops", + "name": "ResponseOps" }, "version": "1.0.0", "kibanaVersion": "kibana", diff --git a/x-pack/test/functional/apps/canvas/filters.ts b/x-pack/test/functional/apps/canvas/filters.ts index e5b97fa2350f12..ce8b319b9d53f6 100644 --- a/x-pack/test/functional/apps/canvas/filters.ts +++ b/x-pack/test/functional/apps/canvas/filters.ts @@ -43,16 +43,18 @@ export default function canvasFiltersTest({ getService, getPageObjects }: FtrPro // Double check that the filter has the correct time range and default filter value const startingMatchFilters = await PageObjects.canvas.getMatchFiltersFromDebug(); - expect(startingMatchFilters[0].value).to.equal('apm'); - expect(startingMatchFilters[0].column).to.equal('project'); + const projectQuery = startingMatchFilters[0].query.term.project; + expect(projectQuery !== null && typeof projectQuery === 'object').to.equal(true); + expect(projectQuery?.value).to.equal('apm'); // Change dropdown value await testSubjects.selectValue('canvasDropdownFilter__select', 'beats'); await retry.try(async () => { const matchFilters = await PageObjects.canvas.getMatchFiltersFromDebug(); - expect(matchFilters[0].value).to.equal('beats'); - expect(matchFilters[0].column).to.equal('project'); + const newProjectQuery = matchFilters[0].query.term.project; + expect(newProjectQuery !== null && typeof newProjectQuery === 'object').to.equal(true); + expect(newProjectQuery?.value).to.equal('beats'); }); }); @@ -66,18 +68,20 @@ export default function canvasFiltersTest({ getService, getPageObjects }: FtrPro }); const startingTimeFilters = await PageObjects.canvas.getTimeFiltersFromDebug(); - expect(startingTimeFilters[0].column).to.equal('@timestamp'); - expect(new Date(startingTimeFilters[0].from).toDateString()).to.equal('Sun Oct 18 2020'); - expect(new Date(startingTimeFilters[0].to).toDateString()).to.equal('Sat Oct 24 2020'); + const timestampQuery = startingTimeFilters[0].query.range['@timestamp']; + expect(timestampQuery !== null && typeof timestampQuery === 'object').to.equal(true); + expect(new Date(timestampQuery.gte).toDateString()).to.equal('Sun Oct 18 2020'); + expect(new Date(timestampQuery.lte).toDateString()).to.equal('Sat Oct 24 2020'); await testSubjects.click('superDatePickerstartDatePopoverButton'); await find.clickByCssSelector('.react-datepicker [aria-label="day-19"]', 20000); await retry.try(async () => { const timeFilters = await PageObjects.canvas.getTimeFiltersFromDebug(); - expect(timeFilters[0].column).to.equal('@timestamp'); - expect(new Date(timeFilters[0].from).toDateString()).to.equal('Mon Oct 19 2020'); - expect(new Date(timeFilters[0].to).toDateString()).to.equal('Sat Oct 24 2020'); + const newTimestampQuery = timeFilters[0].query.range['@timestamp']; + expect(newTimestampQuery !== null && typeof newTimestampQuery === 'object').to.equal(true); + expect(new Date(newTimestampQuery.gte).toDateString()).to.equal('Mon Oct 19 2020'); + expect(new Date(newTimestampQuery.lte).toDateString()).to.equal('Sat Oct 24 2020'); }); await testSubjects.click('superDatePickerendDatePopoverButton'); @@ -85,9 +89,10 @@ export default function canvasFiltersTest({ getService, getPageObjects }: FtrPro await retry.try(async () => { const timeFilters = await PageObjects.canvas.getTimeFiltersFromDebug(); - expect(timeFilters[0].column).to.equal('@timestamp'); - expect(new Date(timeFilters[0].from).toDateString()).to.equal('Mon Oct 19 2020'); - expect(new Date(timeFilters[0].to).toDateString()).to.equal('Fri Oct 23 2020'); + const newTimestampQuery = timeFilters[0].query.range['@timestamp']; + expect(newTimestampQuery !== null && typeof newTimestampQuery === 'object').to.equal(true); + expect(new Date(newTimestampQuery.gte).toDateString()).to.equal('Mon Oct 19 2020'); + expect(new Date(newTimestampQuery.lte).toDateString()).to.equal('Fri Oct 23 2020'); }); }); }); diff --git a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts deleted file mode 100644 index bc15ba2f65ca73..00000000000000 --- a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts +++ /dev/null @@ -1,60 +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 { promises as fs } from 'fs'; -import path from 'path'; -import { comparePngs } from '../../../../../../../test/functional/services/lib/compare_pngs'; - -export async function checkIfPngsMatch( - actualpngPath: string, - baselinepngPath: string, - screenshotsDirectory: string, - log: any -) { - log.debug(`checkIfpngsMatch: ${actualpngPath} vs ${baselinepngPath}`); - // Copy the pngs into the screenshot session directory, as that's where the generated pngs will automatically be - // stored. - const sessionDirectoryPath = path.resolve(screenshotsDirectory, 'session'); - const failureDirectoryPath = path.resolve(screenshotsDirectory, 'failure'); - - await fs.mkdir(sessionDirectoryPath, { recursive: true }); - await fs.mkdir(failureDirectoryPath, { recursive: true }); - - const actualpngFileName = path.basename(actualpngPath, '.png'); - const baselinepngFileName = path.basename(baselinepngPath, '.png'); - - const baselineCopyPath = path.resolve( - sessionDirectoryPath, - `${baselinepngFileName}_baseline.png` - ); - const actualCopyPath = path.resolve(sessionDirectoryPath, `${actualpngFileName}_actual.png`); - - // Don't cause a test failure if the baseline snapshot doesn't exist - we don't have all OS's covered and we - // don't want to start causing failures for other devs working on OS's which are lacking snapshots. We have - // mac and linux covered which is better than nothing for now. - try { - log.debug(`writeFile: ${baselineCopyPath}`); - await fs.writeFile(baselineCopyPath, await fs.readFile(baselinepngPath)); - } catch (error) { - throw new Error(`No baseline png found at ${baselinepngPath}`); - } - log.debug(`writeFile: ${actualCopyPath}`); - await fs.writeFile(actualCopyPath, await fs.readFile(actualpngPath)); - - let diffTotal = 0; - - const diffPngPath = path.resolve(failureDirectoryPath, `${baselinepngFileName}-${1}.png`); - diffTotal += await comparePngs( - actualCopyPath, - baselineCopyPath, - diffPngPath, - sessionDirectoryPath, - log - ); - - return diffTotal; -} diff --git a/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts b/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts index a2523c6d44244c..d42d23b7578a5c 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts @@ -6,14 +6,8 @@ */ import expect from '@kbn/expect'; -import fs from 'fs'; import path from 'path'; -import { promisify } from 'util'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { checkIfPngsMatch } from './lib/compare_pngs'; - -const writeFileAsync = promisify(fs.writeFile); -const mkdirAsync = promisify(fs.mkdir); const REPORTS_FOLDER = path.resolve(__dirname, 'reports'); @@ -27,6 +21,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const es = getService('es'); const testSubjects = getService('testSubjects'); const kibanaServer = getService('kibanaServer'); + const reporting = getService('reporting'); const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; describe('Dashboard Reporting Screenshots', () => { @@ -128,20 +123,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('PNG Layout', () => { - const writeSessionReport = async (name: string, rawPdf: Buffer, reportExt: string) => { - const sessionDirectory = path.resolve(REPORTS_FOLDER, 'session'); - await mkdirAsync(sessionDirectory, { recursive: true }); - const sessionReportPath = path.resolve(sessionDirectory, `${name}.${reportExt}`); - await writeFileAsync(sessionReportPath, rawPdf); - return sessionReportPath; - }; - const getBaselineReportPath = (fileName: string, reportExt: string) => { - const baselineFolder = path.resolve(REPORTS_FOLDER, 'baseline'); - const fullPath = path.resolve(baselineFolder, `${fileName}.${reportExt}`); - log.debug(`getBaselineReportPath (${fullPath})`); - return fullPath; - }; - it('downloads a PNG file: small dashboard', async function () { this.timeout(300000); @@ -155,10 +136,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const url = await PageObjects.reporting.getReportURL(60000); const reportData = await PageObjects.reporting.getRawPdfReportData(url); const reportFileName = 'small_dashboard_preserve_layout'; - const sessionReportPath = await writeSessionReport(reportFileName, reportData, 'png'); - const percentDiff = await checkIfPngsMatch( + const sessionReportPath = await PageObjects.reporting.writeSessionReport( + reportFileName, + 'png', + reportData, + REPORTS_FOLDER + ); + const percentDiff = await reporting.checkIfPngsMatch( sessionReportPath, - getBaselineReportPath(reportFileName, 'png'), + PageObjects.reporting.getBaselineReportPath(reportFileName, 'png', REPORTS_FOLDER), config.get('screenshots.directory'), log ); @@ -179,10 +165,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const url = await PageObjects.reporting.getReportURL(200000); const reportData = await PageObjects.reporting.getRawPdfReportData(url); const reportFileName = 'large_dashboard_preserve_layout'; - const sessionReportPath = await writeSessionReport(reportFileName, reportData, 'png'); - const percentDiff = await checkIfPngsMatch( + const sessionReportPath = await PageObjects.reporting.writeSessionReport( + reportFileName, + 'png', + reportData, + REPORTS_FOLDER + ); + const percentDiff = await reporting.checkIfPngsMatch( sessionReportPath, - getBaselineReportPath(reportFileName, 'png'), + PageObjects.reporting.getBaselineReportPath(reportFileName, 'png', REPORTS_FOLDER), config.get('screenshots.directory'), log ); diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index b85859bf2d5d3d..2e8bf8f4be7254 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -78,6 +78,11 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./embeddable')); }); + describe('', function () { + this.tags('ciGroup2'); // same group used in x-pack/test/reporting_functional + loadTestFile(require.resolve('./reports')); + }); + describe('', function () { this.tags('ciGroup10'); loadTestFile(require.resolve('./es_pew_pew_source')); diff --git a/x-pack/test/functional/apps/maps/reports/baseline/example_map_report.png b/x-pack/test/functional/apps/maps/reports/baseline/example_map_report.png new file mode 100644 index 00000000000000..11185fd271579a Binary files /dev/null and b/x-pack/test/functional/apps/maps/reports/baseline/example_map_report.png differ diff --git a/x-pack/test/functional/apps/maps/reports/baseline/geo_map_report.png b/x-pack/test/functional/apps/maps/reports/baseline/geo_map_report.png new file mode 100644 index 00000000000000..d0ddd1c4074d9c Binary files /dev/null and b/x-pack/test/functional/apps/maps/reports/baseline/geo_map_report.png differ diff --git a/x-pack/test/functional/apps/maps/reports/index.ts b/x-pack/test/functional/apps/maps/reports/index.ts new file mode 100644 index 00000000000000..4e942b1e150ef0 --- /dev/null +++ b/x-pack/test/functional/apps/maps/reports/index.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const REPORTS_FOLDER = __dirname; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const PageObjects = getPageObjects(['reporting', 'common', 'dashboard']); + const config = getService('config'); + const log = getService('log'); + const reporting = getService('reporting'); + + describe('dashboard reporting', () => { + // helper function to check the difference between the new image and the baseline + const measurePngDifference = async (fileName: string) => { + const url = await PageObjects.reporting.getReportURL(60000); + const reportData = await PageObjects.reporting.getRawPdfReportData(url); + + const sessionReportPath = await PageObjects.reporting.writeSessionReport( + fileName, + 'png', + reportData, + REPORTS_FOLDER + ); + log.debug(`session report path: ${sessionReportPath}`); + + expect(sessionReportPath).not.to.be(null); + return await reporting.checkIfPngsMatch( + sessionReportPath, + PageObjects.reporting.getBaselineReportPath(fileName, 'png', REPORTS_FOLDER), + config.get('screenshots.directory'), + log + ); + }; + + after(async () => { + await reporting.deleteAllReports(); + }); + + it('creates a map report using sample geo data', async function () { + await reporting.initEcommerce(); + + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Ecommerce Map'); + await PageObjects.reporting.openPngReportingPanel(); + await PageObjects.reporting.clickGenerateReportButton(); + + const percentDiff = await measurePngDifference('geo_map_report'); + expect(percentDiff).to.be.lessThan(0.09); + + await reporting.teardownEcommerce(); + }); + + it('creates a map report using embeddable example', async function () { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('map embeddable example'); + await PageObjects.reporting.openPngReportingPanel(); + await PageObjects.reporting.clickGenerateReportButton(); + + const percentDiff = await measurePngDifference('example_map_report'); + expect(percentDiff).to.be.lessThan(0.09); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index 1d5c6d1bf84a36..7a84c41aa4a661 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -13,8 +13,7 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); const editedDescription = 'Edited description'; - // FLAKY: https://github.com/elastic/kibana/issues/122927 - describe.skip('regression creation', function () { + describe('regression creation', function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/egs_regression'); await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp'); diff --git a/x-pack/test/functional/apps/monitoring/index.js b/x-pack/test/functional/apps/monitoring/index.js index 3b4251d20e3689..b62e4e750518f3 100644 --- a/x-pack/test/functional/apps/monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/index.js @@ -41,6 +41,8 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./logstash/nodes_mb')); loadTestFile(require.resolve('./logstash/pipelines')); loadTestFile(require.resolve('./logstash/pipelines_mb')); + loadTestFile(require.resolve('./logstash/pipeline_viewer')); + loadTestFile(require.resolve('./logstash/pipeline_viewer_mb')); loadTestFile(require.resolve('./logstash/node_detail')); loadTestFile(require.resolve('./logstash/node_detail_mb')); loadTestFile(require.resolve('./beats/cluster')); diff --git a/x-pack/test/functional/apps/monitoring/logstash/pipeline_viewer.js b/x-pack/test/functional/apps/monitoring/logstash/pipeline_viewer.js new file mode 100644 index 00000000000000..57136884e81295 --- /dev/null +++ b/x-pack/test/functional/apps/monitoring/logstash/pipeline_viewer.js @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { getLifecycleMethods } from '../_get_lifecycle_methods'; + +export default function ({ getService, getPageObjects }) { + const overview = getService('monitoringClusterOverview'); + const pipelinesList = getService('monitoringLogstashPipelines'); + const pipelineViewer = getService('monitoringLogstashPipelineViewer'); + + describe('Logstash pipeline viewer', () => { + const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); + + before(async () => { + await setup('x-pack/test/functional/es_archives/monitoring/logstash_pipelines', { + from: 'Jan 22, 2018 @ 09:10:00.000', + to: 'Jan 22, 2018 @ 09:41:00.000', + }); + + await overview.closeAlertsModal(); + + // go to nginx_logs pipeline view + await overview.clickLsPipelines(); + expect(await pipelinesList.isOnListing()).to.be(true); + await pipelinesList.clickPipeline('nginx_logs'); + expect(await pipelineViewer.isOnPipelineViewer()).to.be(true); + }); + + after(async () => { + await tearDown(); + }); + + it('displays pipelines inputs, filters and ouputs', async () => { + const { inputs, filters, outputs } = await pipelineViewer.getPipelineDefinition(); + + expect(inputs).to.eql([{ name: 'generator', metrics: ['mygen01', '62.5 e/s emitted'] }]); + expect(filters).to.eql([ + { name: 'sleep', metrics: ['1%', '94.86 ms/e', '62.5 e/s received'] }, + ]); + expect(outputs).to.eql([{ name: 'stdout', metrics: ['0%', '0 ms/e', '62.5 e/s received'] }]); + }); + }); +} diff --git a/x-pack/test/functional/apps/monitoring/logstash/pipeline_viewer_mb.js b/x-pack/test/functional/apps/monitoring/logstash/pipeline_viewer_mb.js new file mode 100644 index 00000000000000..bb94e49e34b110 --- /dev/null +++ b/x-pack/test/functional/apps/monitoring/logstash/pipeline_viewer_mb.js @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { getLifecycleMethods } from '../_get_lifecycle_methods'; + +export default function ({ getService, getPageObjects }) { + const overview = getService('monitoringClusterOverview'); + const pipelinesList = getService('monitoringLogstashPipelines'); + const pipelineViewer = getService('monitoringLogstashPipelineViewer'); + + describe('Logstash pipeline viewer mb', () => { + const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); + + before(async () => { + await setup('x-pack/test/functional/es_archives/monitoring/logstash_pipelines_mb', { + from: 'Jan 22, 2018 @ 09:10:00.000', + to: 'Jan 22, 2018 @ 09:41:00.000', + useCreate: true, + }); + + await overview.closeAlertsModal(); + + // go to nginx_logs pipeline view + await overview.clickLsPipelines(); + expect(await pipelinesList.isOnListing()).to.be(true); + await pipelinesList.clickPipeline('nginx_logs'); + expect(await pipelineViewer.isOnPipelineViewer()).to.be(true); + }); + + after(async () => { + await tearDown(); + }); + + it('displays pipelines inputs and ouputs', async () => { + const { inputs, filters, outputs } = await pipelineViewer.getPipelineDefinition(); + + expect(inputs).to.eql([{ name: 'generator', metrics: ['mygen01', '62.5 e/s emitted'] }]); + expect(filters).to.eql([ + { name: 'sleep', metrics: ['1%', '94.86 ms/e', '62.5 e/s received'] }, + ]); + expect(outputs).to.eql([{ name: 'stdout', metrics: ['0%', '0 ms/e', '62.5 e/s received'] }]); + }); + }); +} diff --git a/x-pack/test/functional/apps/security/users.ts b/x-pack/test/functional/apps/security/users.ts index b2bef848b18e23..634c7ace52735a 100644 --- a/x-pack/test/functional/apps/security/users.ts +++ b/x-pack/test/functional/apps/security/users.ts @@ -201,7 +201,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('Deactivate/Activate user', () => { + // FLAKY: https://github.com/elastic/kibana/issues/118728 + describe.skip('Deactivate/Activate user', () => { it('deactivates user when confirming', async () => { await PageObjects.security.deactivatesUser(optionalUser); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); diff --git a/x-pack/test/functional/es_archives/monitoring/logstash_pipelines_mb/data.json.gz b/x-pack/test/functional/es_archives/monitoring/logstash_pipelines_mb/data.json.gz index a9889a81d5e4f8..1f51df6b841d39 100644 Binary files a/x-pack/test/functional/es_archives/monitoring/logstash_pipelines_mb/data.json.gz and b/x-pack/test/functional/es_archives/monitoring/logstash_pipelines_mb/data.json.gz differ diff --git a/x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json b/x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json index 0e9f375ee8be16..3d6b78d0ae10a0 100644 --- a/x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json +++ b/x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json @@ -807,3 +807,89 @@ "updated_at": "2021-12-22T22:39:18.507Z", "version": "WzEzMDQsMV0=" } + +{ + "attributes": { + "description": "", + "layerListJSON": "[{\"id\":\"0hmz5\",\"alpha\":1,\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true,\"lightModeDefault\":\"road_map_desaturated\"},\"visible\":true,\"style\":{},\"type\":\"EMS_VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"7ameq\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"world_countries\",\"tooltipProperties\":[\"name\",\"iso2\"]},\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"name\":\"__kbnjoin__count__741db9c6-8ebb-4ea9-9885-b6b4ac019d14\",\"origin\":\"join\"},\"color\":\"Green to Red\",\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}}},\"type\":\"GEOJSON_VECTOR\",\"joins\":[{\"leftField\":\"iso2\",\"right\":{\"type\":\"ES_TERM_SOURCE\",\"id\":\"741db9c6-8ebb-4ea9-9885-b6b4ac019d14\",\"indexPatternTitle\":\"kibana_sample_data_ecommerce\",\"term\":\"geoip.country_iso_code\",\"indexPatternRefName\":\"layer_1_join_0_index_pattern\",\"metrics\":[{\"type\":\"count\",\"label\":\"sales count\"}],\"applyGlobalQuery\":true}}]},{\"id\":\"jmtgf\",\"label\":\"United States\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"usa_states\",\"tooltipProperties\":[\"name\"]},\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"name\":\"__kbnjoin__count__30a0ec24-49b6-476a-b4ed-6c1636333695\",\"origin\":\"join\"},\"color\":\"Blues\",\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}}},\"type\":\"GEOJSON_VECTOR\",\"joins\":[{\"leftField\":\"name\",\"right\":{\"type\":\"ES_TERM_SOURCE\",\"id\":\"30a0ec24-49b6-476a-b4ed-6c1636333695\",\"indexPatternTitle\":\"kibana_sample_data_ecommerce\",\"term\":\"geoip.region_name\",\"indexPatternRefName\":\"layer_2_join_0_index_pattern\",\"metrics\":[{\"type\":\"count\",\"label\":\"sales count\"}],\"applyGlobalQuery\":true}}]},{\"id\":\"ui5f8\",\"label\":\"France\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"france_departments\",\"tooltipProperties\":[\"label_en\"]},\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"name\":\"__kbnjoin__count__e325c9da-73fa-4b3b-8b59-364b99370826\",\"origin\":\"join\"},\"color\":\"Blues\",\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}}},\"type\":\"GEOJSON_VECTOR\",\"joins\":[{\"leftField\":\"label_en\",\"right\":{\"type\":\"ES_TERM_SOURCE\",\"id\":\"e325c9da-73fa-4b3b-8b59-364b99370826\",\"indexPatternTitle\":\"kibana_sample_data_ecommerce\",\"term\":\"geoip.region_name\",\"indexPatternRefName\":\"layer_3_join_0_index_pattern\",\"metrics\":[{\"type\":\"count\",\"label\":\"sales count\"}],\"applyGlobalQuery\":true}}]},{\"id\":\"y3fjb\",\"label\":\"United Kingdom\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"uk_subdivisions\",\"tooltipProperties\":[\"label_en\"]},\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"name\":\"__kbnjoin__count__612d805d-8533-43a9-ac0e-cbf51fe63dcd\",\"origin\":\"join\"},\"color\":\"Blues\",\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}}},\"type\":\"GEOJSON_VECTOR\",\"joins\":[{\"leftField\":\"label_en\",\"right\":{\"type\":\"ES_TERM_SOURCE\",\"id\":\"612d805d-8533-43a9-ac0e-cbf51fe63dcd\",\"indexPatternTitle\":\"kibana_sample_data_ecommerce\",\"term\":\"geoip.region_name\",\"indexPatternRefName\":\"layer_4_join_0_index_pattern\",\"metrics\":[{\"type\":\"count\",\"label\":\"sales count\"}],\"applyGlobalQuery\":true}}]},{\"id\":\"c54wk\",\"label\":\"Sales\",\"minZoom\":9,\"maxZoom\":24,\"alpha\":1,\"sourceDescriptor\":{\"id\":\"04c983b0-8cfa-4e6a-a64b-52c10b7008fe\",\"type\":\"ES_SEARCH\",\"geoField\":\"geoip.location\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[\"category\",\"customer_gender\",\"manufacturer\",\"order_id\",\"total_quantity\",\"total_unique_products\",\"taxful_total_price\",\"order_date\",\"geoip.region_name\",\"geoip.country_iso_code\"],\"indexPatternRefName\":\"layer_5_source_index_pattern\",\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\"},\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"name\":\"taxful_total_price\",\"origin\":\"source\"},\"color\":\"Greens\",\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}}},\"type\":\"GEOJSON_VECTOR\"},{\"id\":\"qvhh3\",\"label\":\"Total Sales Revenue\",\"minZoom\":0,\"maxZoom\":9,\"alpha\":1,\"sourceDescriptor\":{\"type\":\"ES_GEO_GRID\",\"resolution\":\"COARSE\",\"id\":\"aa7f87b8-9dc5-42be-b19e-1a2fa09b6cad\",\"geoField\":\"geoip.location\",\"requestType\":\"point\",\"metrics\":[{\"type\":\"count\",\"label\":\"sales count\"},{\"type\":\"sum\",\"field\":\"taxful_total_price\",\"label\":\"total sales price\"}],\"indexPatternRefName\":\"layer_6_source_index_pattern\",\"applyGlobalQuery\":true},\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"name\":\"doc_count\",\"origin\":\"source\"},\"color\":\"Greens\",\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#cccccc\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"name\":\"sum_of_taxful_total_price\",\"origin\":\"source\"},\"minSize\":1,\"maxSize\":20,\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"labelText\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"name\":\"sum_of_taxful_total_price\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"labelSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"name\":\"sum_of_taxful_total_price\",\"origin\":\"source\"},\"minSize\":12,\"maxSize\":24,\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"labelBorderSize\":{\"options\":{\"size\":\"MEDIUM\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}}},\"type\":\"GEOJSON_VECTOR\"}]", + "mapStateJSON": "{\"zoom\":2.11,\"center\":{\"lon\":-15.07605,\"lat\":45.88578},\"timeFilters\":{\"from\":\"now-7d\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"settings\":{\"autoFitToDataBounds\":false}}", + "title": "[eCommerce] Orders by Country", + "uiStateJSON": "{\"isDarkMode\":false}" + }, + "coreMigrationVersion": "8.0.0", + "id": "2c9c1f60-1909-11e9-919b-ffe5949a18d2", + "migrationVersion": { + "map": "8.0.0" + }, + "references": [ + { + "id": "aac3e500-f2c7-11ea-8250-fb138aa491e7", + "name": "layer_1_join_0_index_pattern", + "type": "index-pattern" + }, + { + "id": "aac3e500-f2c7-11ea-8250-fb138aa491e7", + "name": "layer_2_join_0_index_pattern", + "type": "index-pattern" + }, + { + "id": "aac3e500-f2c7-11ea-8250-fb138aa491e7", + "name": "layer_3_join_0_index_pattern", + "type": "index-pattern" + }, + { + "id": "aac3e500-f2c7-11ea-8250-fb138aa491e7", + "name": "layer_4_join_0_index_pattern", + "type": "index-pattern" + }, + { + "id": "aac3e500-f2c7-11ea-8250-fb138aa491e7", + "name": "layer_5_source_index_pattern", + "type": "index-pattern" + }, + { + "id": "aac3e500-f2c7-11ea-8250-fb138aa491e7", + "name": "layer_6_source_index_pattern", + "type": "index-pattern" + } + ], + "type": "map", + "updated_at": "2022-01-12T21:54:15.577Z", + "version": "Wzk2NywxXQ==" +} + +{ + "attributes": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "optionsJSON": "{\"useMargins\":true,\"syncColors\":false,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"8.0.0\",\"type\":\"map\",\"gridData\":{\"x\":0,\"y\":0,\"w\":48,\"h\":34,\"i\":\"2eb614a8-f5f6-4ca1-b572-ee990f62d5f8\"},\"panelIndex\":\"2eb614a8-f5f6-4ca1-b572-ee990f62d5f8\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":51.97522,\"lon\":0,\"zoom\":2.09},\"mapBuffer\":{\"minLon\":-225,\"minLat\":-40.9799,\"maxLon\":225,\"maxLat\":85.05113},\"isLayerTOCOpen\":false,\"openTOCDetails\":[],\"hiddenLayers\":[],\"enhancements\":{}},\"panelRefName\":\"panel_2eb614a8-f5f6-4ca1-b572-ee990f62d5f8\"}]", + "refreshInterval": { + "pause": true, + "value": 0 + }, + "timeFrom": "2019-06-09T21:40:39.634Z", + "timeRestore": true, + "timeTo": "2019-07-15T23:28:25.262Z", + "title": "Ecommerce Map", + "version": 1 + }, + "coreMigrationVersion": "8.0.0", + "id": "20699400-73f3-11ec-9525-57f8836282e5", + "migrationVersion": { + "dashboard": "8.0.0" + }, + "references": [ + { + "id": "2c9c1f60-1909-11e9-919b-ffe5949a18d2", + "name": "2eb614a8-f5f6-4ca1-b572-ee990f62d5f8:panel_2eb614a8-f5f6-4ca1-b572-ee990f62d5f8", + "type": "map" + } + ], + "type": "dashboard", + "updated_at": "2022-01-12T22:04:44.061Z", + "version": "WzcxMCwxXQ==" +} diff --git a/x-pack/test/functional/page_objects/canvas_page.ts b/x-pack/test/functional/page_objects/canvas_page.ts index 2b570a4d7dae6f..a51b878b6af309 100644 --- a/x-pack/test/functional/page_objects/canvas_page.ts +++ b/x-pack/test/functional/page_objects/canvas_page.ts @@ -108,7 +108,7 @@ export function CanvasPageProvider({ getService, getPageObjects }: FtrProviderCo const filters = JSON.parse(content); - return filters.and.filter((f: any) => f.filterType === 'time'); + return filters.filters.filter((f: any) => f.query?.range); }, async getMatchFiltersFromDebug() { @@ -119,7 +119,7 @@ export function CanvasPageProvider({ getService, getPageObjects }: FtrProviderCo const filters = JSON.parse(content); - return filters.and.filter((f: any) => f.filterType === 'exactly'); + return filters.filters.filter((f: any) => f.query?.term); }, async clickAddFromLibrary() { diff --git a/x-pack/test/functional/page_objects/reporting_page.ts b/x-pack/test/functional/page_objects/reporting_page.ts index 039f4ff0fbc571..234e19bf90af0d 100644 --- a/x-pack/test/functional/page_objects/reporting_page.ts +++ b/x-pack/test/functional/page_objects/reporting_page.ts @@ -6,11 +6,16 @@ */ import expect from '@kbn/expect'; -import { format as formatUrl } from 'url'; +import fs from 'fs'; +import path from 'path'; import type SuperTest from 'supertest'; - -import { FtrService } from '../ftr_provider_context'; +import { format as formatUrl } from 'url'; +import { promisify } from 'util'; import { REPORT_TABLE_ID, REPORT_TABLE_ROW_ID } from '../../../plugins/reporting/common/constants'; +import { FtrService } from '../ftr_provider_context'; + +const writeFileAsync = promisify(fs.writeFile); +const mkdirAsync = promisify(fs.mkdir); export class ReportingPageObject extends FtrService { private readonly browser = this.ctx.getService('browser'); @@ -186,4 +191,20 @@ export class ReportingPageObject extends FtrService { }) ); } + + async writeSessionReport(name: string, reportExt: string, rawPdf: Buffer, folder: string) { + const sessionDirectory = path.resolve(folder, 'session'); + await mkdirAsync(sessionDirectory, { recursive: true }); + const sessionReportPath = path.resolve(sessionDirectory, `${name}.${reportExt}`); + await writeFileAsync(sessionReportPath, rawPdf); + this.log.debug(`sessionReportPath (${sessionReportPath})`); + return sessionReportPath; + } + + getBaselineReportPath(fileName: string, reportExt: string, folder: string) { + const baselineFolder = path.resolve(folder, 'baseline'); + const fullPath = path.resolve(baselineFolder, `${fileName}.${reportExt}`); + this.log.debug(`getBaselineReportPath (${fullPath})`); + return fullPath; + } } diff --git a/x-pack/test/functional/page_objects/search_sessions_management_page.ts b/x-pack/test/functional/page_objects/search_sessions_management_page.ts index 15c87ea4504252..29faf3cee3b518 100644 --- a/x-pack/test/functional/page_objects/search_sessions_management_page.ts +++ b/x-pack/test/functional/page_objects/search_sessions_management_page.ts @@ -32,7 +32,9 @@ export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrPr const $ = await row.parseDomContent(); const viewCell = await row.findByTestSubject('sessionManagementNameCol'); const actionsCell = await row.findByTestSubject('sessionManagementActionsCol'); + return { + id: (await row.getAttribute('data-test-search-session-id')).split('id-')[1], name: $.findTestSubject('sessionManagementNameCol').text().trim(), status: $.findTestSubject('sessionManagementStatusLabel').attr('data-test-status'), mainUrl: $.findTestSubject('sessionManagementNameCol').text(), diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 6f204608df13ad..96a6a88f112697 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -31,6 +31,7 @@ import { MonitoringLogstashNodesProvider, MonitoringLogstashNodeDetailProvider, MonitoringLogstashPipelinesProvider, + MonitoringLogstashPipelineViewerProvider, MonitoringLogstashSummaryStatusProvider, MonitoringKibanaOverviewProvider, MonitoringKibanaInstancesProvider, @@ -98,6 +99,7 @@ export const services = { monitoringLogstashNodes: MonitoringLogstashNodesProvider, monitoringLogstashNodeDetail: MonitoringLogstashNodeDetailProvider, monitoringLogstashPipelines: MonitoringLogstashPipelinesProvider, + monitoringLogstashPipelineViewer: MonitoringLogstashPipelineViewerProvider, monitoringLogstashSummaryStatus: MonitoringLogstashSummaryStatusProvider, monitoringKibanaOverview: MonitoringKibanaOverviewProvider, monitoringKibanaInstances: MonitoringKibanaInstancesProvider, diff --git a/x-pack/test/functional/services/ml/data_frame_analytics.ts b/x-pack/test/functional/services/ml/data_frame_analytics.ts index aafe96c2c49676..97834bc57c4abd 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics.ts @@ -51,13 +51,15 @@ export function MachineLearningDataFrameAnalyticsProvider( }, async startAnalyticsCreation() { - await retry.tryForTime(20 * 1000, async () => { - if (await testSubjects.exists('mlNoDataFrameAnalyticsFound', { timeout: 1000 })) { + await retry.tryForTime(30 * 1000, async () => { + if (await testSubjects.exists('mlAnalyticsCreateFirstButton', { timeout: 1000 })) { await testSubjects.click('mlAnalyticsCreateFirstButton'); - } else { + } else if (await testSubjects.exists('mlAnalyticsButtonCreate', { timeout: 1000 })) { await testSubjects.click('mlAnalyticsButtonCreate'); + } else { + throw new Error('No Analytics create button found'); } - await testSubjects.existOrFail('analyticsCreateSourceIndexModal'); + await testSubjects.existOrFail('analyticsCreateSourceIndexModal', { timeout: 5000 }); }); }, diff --git a/x-pack/test/functional/services/monitoring/index.js b/x-pack/test/functional/services/monitoring/index.js index 3e9584904c5f87..d776d07f35a75e 100644 --- a/x-pack/test/functional/services/monitoring/index.js +++ b/x-pack/test/functional/services/monitoring/index.js @@ -24,6 +24,7 @@ export { MonitoringLogstashOverviewProvider } from './logstash_overview'; export { MonitoringLogstashNodesProvider } from './logstash_nodes'; export { MonitoringLogstashNodeDetailProvider } from './logstash_node_detail'; export { MonitoringLogstashPipelinesProvider } from './logstash_pipelines'; +export { MonitoringLogstashPipelineViewerProvider } from './logstash_pipeline_viewer'; export { MonitoringLogstashSummaryStatusProvider } from './logstash_summary_status'; export { MonitoringKibanaOverviewProvider } from './kibana_overview'; export { MonitoringKibanaInstancesProvider } from './kibana_instances'; diff --git a/x-pack/test/functional/services/monitoring/logstash_pipeline_viewer.js b/x-pack/test/functional/services/monitoring/logstash_pipeline_viewer.js new file mode 100644 index 00000000000000..c7ff4f79f3e36d --- /dev/null +++ b/x-pack/test/functional/services/monitoring/logstash_pipeline_viewer.js @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 function MonitoringLogstashPipelineViewerProvider({ getService }) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const find = getService('find'); + + const PIPELINE_VIEWER_SELECTOR = '.monPipelineViewer'; + const SUBJ_PIPELINE_SECTION_PREFIX = 'pipelineViewerSection_'; + const PIPELINE_SECTION_ITEM_CLS = 'monPipelineViewer__listItem'; + + return new (class LogstashPipelineViewer { + isOnPipelineViewer() { + return retry.try(() => find.existsByCssSelector(PIPELINE_VIEWER_SELECTOR)); + } + + async getPipelineDefinition() { + const getSectionItems = async (section) => { + const items = await section.findAllByClassName(PIPELINE_SECTION_ITEM_CLS); + + return Promise.all( + items.map(async (item) => { + const [name, ...metrics] = await item.getVisibleText().then((text) => text.split('\n')); + return { name, metrics }; + }) + ); + }; + + const [inputs, filters, outputs] = await Promise.all([ + testSubjects.find(SUBJ_PIPELINE_SECTION_PREFIX + 'Inputs').then(getSectionItems), + testSubjects.find(SUBJ_PIPELINE_SECTION_PREFIX + 'Filters').then(getSectionItems), + testSubjects.find(SUBJ_PIPELINE_SECTION_PREFIX + 'Outputs').then(getSectionItems), + ]); + + return { inputs, filters, outputs }; + } + })(); +} diff --git a/x-pack/test/functional/services/monitoring/logstash_pipelines.js b/x-pack/test/functional/services/monitoring/logstash_pipelines.js index db256cc1f23ab4..54d5f44d10545a 100644 --- a/x-pack/test/functional/services/monitoring/logstash_pipelines.js +++ b/x-pack/test/functional/services/monitoring/logstash_pipelines.js @@ -64,6 +64,18 @@ export function MonitoringLogstashPipelinesProvider({ getService, getPageObjects }, []); } + async clickPipeline(id) { + const anchors = await testSubjects.findAll(SUBJ_PIPELINES_IDS); + for (let i = 0; i < anchors.length; i++) { + const anchor = anchors[i]; + if ((await anchor.getVisibleText()) === id) { + return anchor.click(); + } + } + + throw new Error(`pipeline with id ${id} not found`); + } + async clickIdCol() { const headerCell = await testSubjects.find(SUBJ_TABLE_SORT_ID_COL); const button = await headerCell.findByTagName('button'); diff --git a/x-pack/test/functional/services/transform/wizard.ts b/x-pack/test/functional/services/transform/wizard.ts index 434653be49c14f..f876e2432931a1 100644 --- a/x-pack/test/functional/services/transform/wizard.ts +++ b/x-pack/test/functional/services/transform/wizard.ts @@ -26,6 +26,7 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi const testSubjects = getService('testSubjects'); const comboBox = getService('comboBox'); const retry = getService('retry'); + const ml = getService('ml'); const PageObjects = getPageObjects(['discover', 'timePicker']); return { @@ -679,7 +680,9 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi }, async setTransformId(transformId: string) { - await testSubjects.setValue('transformIdInput', transformId, { clearWithKeyboard: true }); + await ml.commonUI.setValueWithChecks('transformIdInput', transformId, { + clearWithKeyboard: true, + }); await this.assertTransformIdValue(transformId); }, @@ -699,7 +702,7 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi }, async setTransformDescription(transformDescription: string) { - await testSubjects.setValue('transformDescriptionInput', transformDescription, { + await ml.commonUI.setValueWithChecks('transformDescriptionInput', transformDescription, { clearWithKeyboard: true, }); await this.assertTransformDescriptionValue(transformDescription); @@ -721,7 +724,7 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi }, async setDestinationIndex(destinationIndex: string) { - await testSubjects.setValue('transformDestinationIndexInput', destinationIndex, { + await ml.commonUI.setValueWithChecks('transformDestinationIndexInput', destinationIndex, { clearWithKeyboard: true, }); await this.assertDestinationIndexValue(destinationIndex); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts index e40c821d988517..b2e27b30f00794 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts @@ -18,7 +18,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const comboBox = getService('comboBox'); const supertest = getService('supertest'); - describe('Connectors', function () { + // FLAKY: https://github.com/elastic/kibana/issues/88796 + describe.skip('Connectors', function () { const objectRemover = new ObjectRemover(supertest); before(async () => { diff --git a/x-pack/test/licensing_plugin/server/updates.ts b/x-pack/test/licensing_plugin/server/updates.ts index 87132dd28ddfb0..55b2b68ff8f821 100644 --- a/x-pack/test/licensing_plugin/server/updates.ts +++ b/x-pack/test/licensing_plugin/server/updates.ts @@ -17,7 +17,8 @@ export default function (ftrContext: FtrProviderContext) { const scenario = createScenario(ftrContext); - describe('changes in license types', () => { + // FLAKY: https://github.com/elastic/kibana/issues/110938 + describe.skip('changes in license types', () => { after(async () => { await scenario.teardown(); }); diff --git a/x-pack/test/reporting_functional/services/scenarios.ts b/x-pack/test/reporting_functional/services/scenarios.ts index a1387127ffc0ac..79a0c59cc5a3da 100644 --- a/x-pack/test/reporting_functional/services/scenarios.ts +++ b/x-pack/test/reporting_functional/services/scenarios.ts @@ -6,8 +6,9 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { checkIfPngsMatch } from '../../../../test/functional/services/lib/compare_pngs'; import { createScenarios as createAPIScenarios } from '../../reporting_api_integration/services/scenarios'; +import { FtrProviderContext } from '../ftr_provider_context'; export function createScenarios( context: Pick @@ -161,5 +162,6 @@ export function createScenarios( tryReportsNotAvailable, loginDataAnalyst, loginReportingUser, + checkIfPngsMatch, }; } diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/sessions_in_space.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/sessions_in_space.ts index 8561094890474f..308dc472c29fe3 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/sessions_in_space.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/sessions_in_space.ts @@ -5,6 +5,7 @@ * 2.0. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -18,17 +19,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'visChart', 'security', 'timePicker', + 'searchSessionsManagement', ]); const dashboardPanelActions = getService('dashboardPanelActions'); const browser = getService('browser'); const searchSessions = getService('searchSessions'); + const kibanaServer = getService('kibanaServer'); + const toasts = getService('toasts'); - // Failing: See https://github.com/elastic/kibana/issues/112732 - describe.skip('dashboard in space', () => { + describe('dashboard in space', () => { describe('Storing search sessions in space', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/dashboard/session_in_space'); + await kibanaServer.uiSettings.replace( + { + 'timepicker:timeDefaults': + '{ "from": "2015-09-01T00:00:00.000Z", "to": "2015-10-01T00:00:00.000Z"}', + defaultIndex: 'd1bd6c84-d9d0-56fb-8a72-63fe60020920', + }, + { space: 'another-space' } + ); + await security.role.create('data_analyst', { elasticsearch: { indices: [{ names: ['logstash-*'], privileges: ['all'] }], @@ -63,6 +75,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.role.delete('data_analyst'); await security.user.delete('analyst'); + await kibanaServer.uiSettings.unset('timepicker:timeDefaults', { space: 'another-space' }); + await kibanaServer.uiSettings.unset('defaultIndex', { space: 'another-space' }); await esArchiver.unload('x-pack/test/functional/es_archives/dashboard/session_in_space'); }); @@ -70,11 +84,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('dashboard', { basePath: 's/another-space' }); await PageObjects.dashboard.loadSavedDashboard('A Dashboard in another space'); - await PageObjects.timePicker.setAbsoluteRange( - 'Sep 1, 2015 @ 00:00:00.000', - 'Oct 1, 2015 @ 00:00:00.000' - ); - await PageObjects.dashboard.waitForRenderComplete(); await searchSessions.expectState('completed'); @@ -84,16 +93,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'A Pie in another space' ); - // load URL to restore a saved session - const url = await browser.getCurrentUrl(); - const savedSessionURL = `${url}&searchSessionId=${savedSessionId}`; - await browser.get(savedSessionURL); + await searchSessions.openPopover(); + await searchSessions.viewSearchSessions(); + + // purge client side search cache + // https://github.com/elastic/kibana/issues/106074#issuecomment-920462094 + await browser.refresh(); + + const searchSessionList = await PageObjects.searchSessionsManagement.getList(); + const searchSessionItem = searchSessionList.find( + (session) => session.id === savedSessionId + ); + + if (!searchSessionItem) throw new Error(`Can\'t find session with id = ${savedSessionId}`); + + // navigate to discover + await searchSessionItem.view(); + await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.dashboard.waitForRenderComplete(); // Check that session is restored await searchSessions.expectState('restored'); await testSubjects.missingOrFail('embeddableErrorLabel'); + expect(await toasts.getToastCount()).to.be(0); // no session restoration related warnings }); }); @@ -101,6 +124,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/dashboard/session_in_space'); + await kibanaServer.uiSettings.replace( + { + 'timepicker:timeDefaults': + '{ "from": "2015-09-01T00:00:00.000Z", "to": "2015-10-01T00:00:00.000Z"}', + defaultIndex: 'd1bd6c84-d9d0-56fb-8a72-63fe60020920', + }, + { space: 'another-space' } + ); + await security.role.create('data_analyst', { elasticsearch: { indices: [{ names: ['logstash-*'], privileges: ['all'] }], @@ -135,6 +167,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.role.delete('data_analyst'); await security.user.delete('analyst'); + await kibanaServer.uiSettings.unset('timepicker:timeDefaults', { space: 'another-space' }); + await kibanaServer.uiSettings.unset('defaultIndex', { space: 'another-space' }); await esArchiver.unload('x-pack/test/functional/es_archives/dashboard/session_in_space'); }); @@ -142,11 +176,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('dashboard', { basePath: 's/another-space' }); await PageObjects.dashboard.loadSavedDashboard('A Dashboard in another space'); - await PageObjects.timePicker.setAbsoluteRange( - 'Sep 1, 2015 @ 00:00:00.000', - 'Oct 1, 2015 @ 00:00:00.000' - ); - await PageObjects.dashboard.waitForRenderComplete(); await searchSessions.expectState('completed'); diff --git a/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts b/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts index b989ad11273061..922ecfc12dc4f7 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts @@ -5,6 +5,7 @@ * 2.0. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -19,16 +20,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'visChart', 'security', 'timePicker', + 'searchSessionsManagement', ]); const browser = getService('browser'); const searchSessions = getService('searchSessions'); + const kibanaServer = getService('kibanaServer'); + const toasts = getService('toasts'); - // FLAKY https://github.com/elastic/kibana/issues/112913 - describe.skip('discover in space', () => { + describe('discover in space', () => { describe('Storing search sessions in space', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/dashboard/session_in_space'); + await kibanaServer.uiSettings.replace( + { + 'timepicker:timeDefaults': + '{ "from": "2015-09-01T00:00:00.000Z", "to": "2015-10-01T00:00:00.000Z"}', + defaultIndex: 'd1bd6c84-d9d0-56fb-8a72-63fe60020920', + }, + { space: 'another-space' } + ); + await security.role.create('data_analyst', { elasticsearch: { indices: [{ names: ['logstash-*'], privileges: ['all'] }], @@ -63,6 +75,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.role.delete('data_analyst'); await security.user.delete('analyst'); + await kibanaServer.uiSettings.unset('timepicker:timeDefaults', { space: 'another-space' }); + await kibanaServer.uiSettings.unset('defaultIndex', { space: 'another-space' }); await esArchiver.unload('x-pack/test/functional/es_archives/dashboard/session_in_space'); }); @@ -71,11 +85,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.selectIndexPattern('logstash-*'); - await PageObjects.timePicker.setAbsoluteRange( - 'Sep 1, 2015 @ 00:00:00.000', - 'Oct 1, 2015 @ 00:00:00.000' - ); - await PageObjects.discover.waitForDocTableLoadingComplete(); await searchSessions.expectState('completed'); @@ -88,22 +97,45 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ).getAttribute('data-search-session-id'); await inspector.close(); - // load URL to restore a saved session - const url = await browser.getCurrentUrl(); - const savedSessionURL = `${url}&searchSessionId=${savedSessionId}`; - await browser.get(savedSessionURL); + await searchSessions.openPopover(); + await searchSessions.viewSearchSessions(); + + // purge client side search cache + // https://github.com/elastic/kibana/issues/106074#issuecomment-920462094 + await browser.refresh(); + + const searchSessionList = await PageObjects.searchSessionsManagement.getList(); + const searchSessionItem = searchSessionList.find( + (session) => session.id === savedSessionId + ); + + if (!searchSessionItem) throw new Error(`Can\'t find session with id = ${savedSessionId}`); + + // navigate to discover + await searchSessionItem.view(); + await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.discover.waitForDocTableLoadingComplete(); // Check that session is restored await searchSessions.expectState('restored'); - await testSubjects.missingOrFail('discoverNoResultsError'); // expect error because of fake searchSessionId + await testSubjects.missingOrFail('discoverNoResultsError'); + expect(await toasts.getToastCount()).to.be(0); // no session restoration related warnings }); }); describe('Disabled storing search sessions in space', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/dashboard/session_in_space'); + await kibanaServer.uiSettings.replace( + { + 'timepicker:timeDefaults': + '{ "from": "2015-09-01T00:00:00.000Z", "to": "2015-10-01T00:00:00.000Z"}', + defaultIndex: 'd1bd6c84-d9d0-56fb-8a72-63fe60020920', + }, + { space: 'another-space' } + ); + await security.role.create('data_analyst', { elasticsearch: { indices: [{ names: ['logstash-*'], privileges: ['all'] }], @@ -138,6 +170,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.role.delete('data_analyst'); await security.user.delete('analyst'); + await kibanaServer.uiSettings.unset('timepicker:timeDefaults', { space: 'another-space' }); + await kibanaServer.uiSettings.unset('defaultIndex', { space: 'another-space' }); await esArchiver.unload('x-pack/test/functional/es_archives/dashboard/session_in_space'); }); diff --git a/x-pack/test/security_solution_cypress/es_archives/exceptions/data.json b/x-pack/test/security_solution_cypress/es_archives/exceptions/data.json index b7de2dba02d196..bc3c1c302c685e 100644 --- a/x-pack/test/security_solution_cypress/es_archives/exceptions/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/exceptions/data.json @@ -8,6 +8,9 @@ "agent": { "name": "bond" }, + "unique_value": { + "test": "test field" + }, "user" : [ { "name" : "john", diff --git a/x-pack/test/security_solution_cypress/es_archives/exceptions/mappings.json b/x-pack/test/security_solution_cypress/es_archives/exceptions/mappings.json index e63b86392756fb..3b5cc2dae545c9 100644 --- a/x-pack/test/security_solution_cypress/es_archives/exceptions/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/exceptions/mappings.json @@ -25,6 +25,14 @@ } } }, + "unique_value": { + "properties": { + "test": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, "user": { "type": "nested", "properties": { diff --git a/yarn.lock b/yarn.lock index cc181351a3d96d..745ce5bc6d6baf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9805,7 +9805,7 @@ cli-spinners@^2.2.0, cli-spinners@^2.5.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.5.0.tgz#12763e47251bf951cb75c201dfa58ff1bcb2d047" integrity sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ== -cli-table3@0.6.0, cli-table3@~0.6.0: +cli-table3@0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ== @@ -9815,6 +9815,15 @@ cli-table3@0.6.0, cli-table3@~0.6.0: optionalDependencies: colors "^1.1.2" +cli-table3@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8" + integrity sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA== + dependencies: + string-width "^4.2.0" + optionalDependencies: + colors "1.4.0" + cli-table@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" @@ -10136,7 +10145,7 @@ colors@1.0.3: resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= -colors@^1.1.2, colors@^1.2.1, colors@^1.3.2: +colors@1.4.0, colors@^1.1.2, colors@^1.2.1, colors@^1.3.2: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== @@ -11170,10 +11179,10 @@ cypress-recurse@^1.13.1: resolved "https://registry.yarnpkg.com/cypress-recurse/-/cypress-recurse-1.13.1.tgz#1d026d3381e4de7cf867a5ef592c4161da325fed" integrity sha512-re0djeUInv0JwxhFBSIiZmrJfvUaLTjK9jWsD0oqpnvG1UXGWR69rkXMtMK5HZhxkL7GSk9JiIpm49aWpOnsFA== -cypress@^9.2.0: - version "9.2.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.2.0.tgz#727c20b4662167890db81d5f6ba615231835b17d" - integrity sha512-Jn26Tprhfzh/a66Sdj9SoaYlnNX6Mjfmj5PHu2a7l3YHXhrgmavM368wjCmgrxC6KHTOv9SpMQGhAJn+upDViA== +cypress@^9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.2.1.tgz#47f2457e5ca7ede48be9a4176f20f30ccf3b3902" + integrity sha512-LVEe4yWCo4xO0Vd8iYjFHRyd5ulRvM56XqMgAdn05Qb9kJ6iJdO/MmjKD8gNd768698cp1FDuSmFQZHVZGk+Og== dependencies: "@cypress/request" "^2.88.10" "@cypress/xvfb" "^1.2.4" @@ -11187,7 +11196,7 @@ cypress@^9.2.0: chalk "^4.1.0" check-more-types "^2.24.0" cli-cursor "^3.1.0" - cli-table3 "~0.6.0" + cli-table3 "~0.6.1" commander "^5.1.0" common-tags "^1.8.0" dayjs "^1.10.4"