diff --git a/.buildkite/pipelines/performance/nightly.yml b/.buildkite/pipelines/performance/daily.yml similarity index 79% rename from .buildkite/pipelines/performance/nightly.yml rename to .buildkite/pipelines/performance/daily.yml index dfee1061815c3..208456f9c67a5 100644 --- a/.buildkite/pipelines/performance/nightly.yml +++ b/.buildkite/pipelines/performance/daily.yml @@ -6,7 +6,7 @@ steps: key: "performance-test-iteration-count" hint: "How many times you want to run tests? " required: true - if: build.env('ITERATION_COUNT_ENV') == null + if: build.env('PERF_TEST_COUNT') == null - label: ":male-mechanic::skin-tone-2: Pre-Build" command: .buildkite/scripts/lifecycle/pre_build.sh @@ -19,10 +19,10 @@ steps: queue: c2-16 key: build - - label: ":muscle: Performance Tests" - command: .buildkite/scripts/steps/functional/performance.sh + - label: ":muscle: Performance Tests with Playwright config" + command: .buildkite/scripts/steps/functional/performance_playwright.sh agents: - queue: ci-group-6 + queue: c2-16 depends_on: build - wait: ~ diff --git a/.buildkite/scripts/steps/functional/performance.sh b/.buildkite/scripts/steps/functional/performance_playwright.sh similarity index 52% rename from .buildkite/scripts/steps/functional/performance.sh rename to .buildkite/scripts/steps/functional/performance_playwright.sh index 8e3793733a6e8..c38ef5e56dbe4 100644 --- a/.buildkite/scripts/steps/functional/performance.sh +++ b/.buildkite/scripts/steps/functional/performance_playwright.sh @@ -2,20 +2,22 @@ set -uo pipefail -if [ -z "${ITERATION_COUNT_ENV+x}" ]; then - ITERATION_COUNT="$(buildkite-agent meta-data get performance-test-iteration-count)" +if [ -z "${PERF_TEST_COUNT+x}" ]; then + TEST_COUNT="$(buildkite-agent meta-data get performance-test-iteration-count)" else - ITERATION_COUNT=$ITERATION_COUNT_ENV + TEST_COUNT=$PERF_TEST_COUNT fi -tput setab 2; tput setaf 0; echo "Performance test will be run at ${BUILDKITE_BRANCH} ${ITERATION_COUNT} times" +tput setab 2; tput setaf 0; echo "Performance test will be run at ${BUILDKITE_BRANCH} ${TEST_COUNT} times" cat << EOF | buildkite-agent pipeline upload steps: - - command: .buildkite/scripts/steps/functional/performance_sub.sh - parallelism: "$ITERATION_COUNT" + - command: .buildkite/scripts/steps/functional/performance_sub_playwright.sh + parallelism: "$TEST_COUNT" concurrency: 20 concurrency_group: 'performance-test-group' + agents: + queue: c2-16 EOF diff --git a/.buildkite/scripts/steps/functional/performance_sub.sh b/.buildkite/scripts/steps/functional/performance_sub.sh deleted file mode 100644 index d3e6c0ba7304e..0000000000000 --- a/.buildkite/scripts/steps/functional/performance_sub.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/common/util.sh - -.buildkite/scripts/bootstrap.sh -.buildkite/scripts/download_build_artifacts.sh - -cd "$XPACK_DIR" - -echo --- Run Performance Tests -checks-reporter-with-killswitch "Run Performance Tests" \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --config test/performance/config.ts; diff --git a/.buildkite/scripts/steps/functional/performance_sub_playwright.sh b/.buildkite/scripts/steps/functional/performance_sub_playwright.sh new file mode 100644 index 0000000000000..fee171aef9a48 --- /dev/null +++ b/.buildkite/scripts/steps/functional/performance_sub_playwright.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/common/util.sh + +.buildkite/scripts/bootstrap.sh +.buildkite/scripts/download_build_artifacts.sh + +echo --- Run Performance Tests with Playwright config + +node scripts/es snapshot& + +esPid=$! + +export TEST_PERFORMANCE_PHASE=WARMUP +export TEST_ES_URL=http://elastic:changeme@localhost:9200 +export TEST_ES_DISABLE_STARTUP=true +export ELASTIC_APM_ACTIVE=false + +sleep 120 + +cd "$XPACK_DIR" + +# warmup round 1 +checks-reporter-with-killswitch "Run Performance Tests with Playwright Config (Phase: WARMUP)" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ + --config "test/performance/config.playwright.ts"; + +export TEST_PERFORMANCE_PHASE=TEST +export ELASTIC_APM_ACTIVE=true + +checks-reporter-with-killswitch "Run Performance Tests with Playwright Config (Phase: TEST)" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ + --config "test/performance/config.playwright.ts"; + +kill "$esPid" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 36e1e3813275e..7052a19806d52 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 ac90d149a9967..c2f0322f6d680 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 5f5fb28f74d7a..d038fa59ee9a3 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 8861681827ef3..b406ced798c0c 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 d44de3c2efe2f..814a7d374506f 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 5523201dce36f..c080c412f0f6b 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/osquery/images/live-query-check-results.png b/docs/osquery/images/live-query-check-results.png index df292309e0853..33e31f0ce54f0 100644 Binary files a/docs/osquery/images/live-query-check-results.png and b/docs/osquery/images/live-query-check-results.png differ diff --git a/docs/osquery/osquery.asciidoc b/docs/osquery/osquery.asciidoc index 500dc6959fc00..70effbc2b3c96 100644 --- a/docs/osquery/osquery.asciidoc +++ b/docs/osquery/osquery.asciidoc @@ -121,11 +121,18 @@ image::images/scheduled-pack.png[Shows queries in the pack and details about eac [float] [[osquery-manage-query]] -== Edit saved queries +== Save queries -Add or edit saved queries from the *Saved queries* tab. +You can save queries in two ways: -. Go to the saved queries, then click **Add saved query** or the edit icon. +* After running a live query, click the *Save for later* link. +* From the *Saved queries* tab, click the **Add saved query** button. + +Once you save a query, you can only edit it from the *Saved queries* tab. + +To add or edit saved queries from the *Saved queries* tab: + +. Go to *Saved queries*, and then click **Add saved query** or the edit icon. . Provide the following fields: * The unique identifier. @@ -148,7 +155,7 @@ Add or edit saved queries from the *Saved queries* tab. * From the *Test query* panel, select agents or groups to test the query, then click *Submit* to run a live query. Result columns with the image:images/mapped-icon.png[mapping] icon are mapped. Hover over the icon to see the mapped ECS field. -. Click **Save query**. +. Click *Save* or *Update*. [float] [[osquery-map-fields]] @@ -175,11 +182,7 @@ and the mapped ECS fields. For example, if you update a query to map `osquery.na ** **Static value**: Enter a static value. When the query runs, the ECS field is set to the value entered. For example, static fields can be used to apply `tags` or your preferred `event.category` to the query results. -. Map more fields, as needed. - -** To add a new row for additional fields to map, click the plus icon. - -** To remove any mapped rows, click the trash icon. +. Map more fields, as needed. To remove any mapped rows, click the delete icon. . Save your changes. @@ -314,7 +317,7 @@ While this allows you to use advanced Osquery functionality like pack discovery . Edit the *Osquery config* JSON field to apply your preferred Osquery configuration. Note the following: -* The field may already have content if you have scheduled packs for this agent policy. To keep these packs scheduled, do not edit the `packs` section. +* The field may already have content if you have scheduled packs for this agent policy. To keep these packs scheduled, do not remove the `packs` section. * Refer to the https://osquery.readthedocs.io/en/stable/[Osquery documentation] for configuration options. @@ -344,14 +347,12 @@ https://www.elastic.co/guide/en/fleet/master/upgrade-elastic-agent.html[upgrade [float] === Debug issues -If you encounter issues with *Osquery Manager*, find the relevant logs for the {elastic-agent} -and Osquerybeat in the installed agent directory, then adjust the agent path for your setup. - -The relevant logs look similar to the following example paths: +If you encounter issues with *Osquery Manager*, find the relevant logs for {elastic-agent} +and Osquerybeat in the agent directory. Refer to the {fleet-guide}/installation-layout.html[Fleet Installation layout] to find the log file location for your OS. ```ts -`/data/elastic-agent-054e22/logs/elastic-agent-json.log-*` -`/data/elastic-agent-054e22/logs/default/osquerybeat-json.log` +../data/elastic-agent-*/logs/elastic-agent-json.log-* +../data/elastic-agent-*/logs/default/osquerybeat-json.log ``` To get more details in the logs, change the agent logging level to debug: diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index f0c22ebf8b730..1b8d467713ab8 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 ed36f3277ee64..53eb65b4455fe 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", @@ -831,6 +831,7 @@ "pbf": "3.2.1", "pirates": "^4.0.1", "pixelmatch": "^5.1.0", + "playwright": "^1.17.1", "postcss": "^7.0.32", "postcss-loader": "^3.0.0", "postcss-prefix-selector": "^1.7.2", 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 559c636cfaeec..4ea1af15f43ef 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 4078c848aa480..96bef3e958bdc 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 5c081707bb75c..47359bd07aa8a 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 dd848d9f66c63..ee462085ef79c 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 3610ffae3c7e6..ab4eee4f255b9 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 c25fc7ca9f1c2..973cbc2266cbe 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 69f10efd97d66..e3143a318a16e 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-test/src/functional_tests/tasks.ts b/packages/kbn-test/src/functional_tests/tasks.ts index 6a6c7edb98c79..cace061be64a9 100644 --- a/packages/kbn-test/src/functional_tests/tasks.ts +++ b/packages/kbn-test/src/functional_tests/tasks.ts @@ -109,7 +109,9 @@ export async function runTests(options: RunTestsParams) { let es; try { - es = await runElasticsearch({ config, options: { ...options, log } }); + if (process.env.TEST_ES_DISABLE_STARTUP !== 'true') { + es = await runElasticsearch({ config, options: { ...options, log } }); + } await runKibanaServer({ procs, config, options }); await runFtr({ configPath, options: { ...options, log } }); } finally { 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 25e6c32705f4e..97b58ce5a700f 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 f96f39349887e..b9fb8a21f0a8b 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 1f3118c77aa0f..1744d7a41841b 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/elasticsearch/client/get_ecs_response_log.test.ts b/src/core/server/elasticsearch/client/get_ecs_response_log.test.ts new file mode 100644 index 0000000000000..547aae3224756 --- /dev/null +++ b/src/core/server/elasticsearch/client/get_ecs_response_log.test.ts @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { type DiagnosticResult, type ConnectionRequestParams } from '@elastic/elasticsearch'; +import { getEcsResponseLog } from './get_ecs_response_log'; + +interface ResponseFixtureOptions { + requestParams?: Partial; + + response?: { + body?: any; + headers?: Record; + statusCode?: number; + }; +} + +function createResponseEvent({ + requestParams = {}, + response = {}, +}: ResponseFixtureOptions = {}): DiagnosticResult { + return { + body: response.body ?? {}, + statusCode: response.statusCode ?? 200, + headers: response.headers ?? {}, + meta: { + request: { + params: { + headers: requestParams.headers ?? { 'content-length': '123' }, + method: requestParams.method ?? 'get', + path: requestParams.path ?? '/path', + querystring: requestParams.querystring ?? '?wait_for_completion=true', + }, + options: { + id: '42', + }, + } as DiagnosticResult['meta']['request'], + } as DiagnosticResult['meta'], + warnings: null, + }; +} + +describe('getEcsResponseLog', () => { + describe('filters sensitive headers', () => { + test('redacts Authorization and Cookie headers by default', () => { + const event = createResponseEvent({ + requestParams: { headers: { authorization: 'a', cookie: 'b', 'user-agent': 'hi' } }, + response: { headers: { 'content-length': '123', 'set-cookie': 'c' } }, + }); + const log = getEcsResponseLog(event); + // @ts-expect-error ECS custom field + expect(log.http.request.headers).toMatchInlineSnapshot(` + Object { + "authorization": "[REDACTED]", + "cookie": "[REDACTED]", + "user-agent": "hi", + } + `); + // @ts-expect-error ECS custom field + expect(log.http.response.headers).toMatchInlineSnapshot(` + Object { + "content-length": "123", + "set-cookie": "[REDACTED]", + } + `); + }); + + test('does not mutate original headers', () => { + const reqHeaders = { a: 'foo', b: ['hello', 'world'] }; + const resHeaders = { c: 'bar' }; + const event = createResponseEvent({ + requestParams: { headers: reqHeaders }, + response: { headers: resHeaders }, + }); + + const log = getEcsResponseLog(event); + expect(reqHeaders).toMatchInlineSnapshot(` + Object { + "a": "foo", + "b": Array [ + "hello", + "world", + ], + } + `); + expect(resHeaders).toMatchInlineSnapshot(` + Object { + "c": "bar", + } + `); + + // @ts-expect-error ECS custom field + log.http.request.headers.a = 'testA'; + // @ts-expect-error ECS custom field + log.http.request.headers.b[1] = 'testB'; + // @ts-expect-error ECS custom field + log.http.request.headers.c = 'testC'; + expect(reqHeaders).toMatchInlineSnapshot(` + Object { + "a": "foo", + "b": Array [ + "hello", + "testB", + ], + } + `); + expect(resHeaders).toMatchInlineSnapshot(` + Object { + "c": "bar", + } + `); + }); + + test('does not mutate original headers when redacting sensitive data', () => { + const reqHeaders = { authorization: 'a', cookie: 'b', 'user-agent': 'hi' }; + const resHeaders = { 'content-length': '123', 'set-cookie': 'c' }; + const event = createResponseEvent({ + requestParams: { headers: reqHeaders }, + response: { headers: resHeaders }, + }); + getEcsResponseLog(event); + + expect(reqHeaders).toMatchInlineSnapshot(` + Object { + "authorization": "a", + "cookie": "b", + "user-agent": "hi", + } + `); + expect(resHeaders).toMatchInlineSnapshot(` + Object { + "content-length": "123", + "set-cookie": "c", + } + `); + }); + }); + + describe('ecs', () => { + test('provides an ECS-compatible response', () => { + const event = createResponseEvent(); + const result = getEcsResponseLog(event, 123); + expect(result).toMatchInlineSnapshot(` + Object { + "http": Object { + "request": Object { + "headers": Object { + "content-length": "123", + }, + "id": undefined, + "method": "GET", + }, + "response": Object { + "body": Object { + "bytes": 123, + }, + "headers": Object {}, + "status_code": 200, + }, + }, + "url": Object { + "path": "/path", + "query": "?wait_for_completion=true", + }, + } + `); + }); + }); +}); diff --git a/src/core/server/elasticsearch/client/get_ecs_response_log.ts b/src/core/server/elasticsearch/client/get_ecs_response_log.ts new file mode 100644 index 0000000000000..1a75967cf66d7 --- /dev/null +++ b/src/core/server/elasticsearch/client/get_ecs_response_log.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { type IncomingHttpHeaders } from 'http'; +import { type DiagnosticResult } from '@elastic/elasticsearch'; +import { type LogMeta } from '@kbn/logging'; + +const FORBIDDEN_HEADERS = ['authorization', 'cookie', 'set-cookie']; +const REDACTED_HEADER_TEXT = '[REDACTED]'; + +// We are excluding sensitive headers by default, until we have a log filtering mechanism. +function redactSensitiveHeaders(key: string, value: string | string[]): string | string[] { + return FORBIDDEN_HEADERS.includes(key) ? REDACTED_HEADER_TEXT : value; +} + +// Shallow clone the headers so they are not mutated if filtered by a RewriteAppender. +function cloneAndFilterHeaders(headers?: IncomingHttpHeaders) { + const result = {} as IncomingHttpHeaders; + if (headers) { + for (const key of Object.keys(headers)) { + const value = headers[key]; + if (value) { + result[key] = redactSensitiveHeaders(key, value); + } + } + } + return result; +} + +/** + * Retruns ECS-compliant `LogMeta` for logging. + * + * @internal + */ +export function getEcsResponseLog(event: DiagnosticResult, bytes?: number): LogMeta { + const meta: LogMeta = { + http: { + request: { + id: event.meta.request.options.opaqueId, + method: event.meta.request.params.method.toUpperCase(), + // @ts-expect-error ECS custom field: https://github.com/elastic/ecs/issues/232. + headers: cloneAndFilterHeaders(event.meta.request.params.headers), + }, + response: { + body: { + bytes, + }, + status_code: event.statusCode, + // @ts-expect-error ECS custom field: https://github.com/elastic/ecs/issues/232. + headers: cloneAndFilterHeaders(event.headers), + }, + }, + url: { + path: event.meta.request.params.path, + query: event.meta.request.params.querystring, + }, + }; + + return meta; +} diff --git a/src/core/server/elasticsearch/client/log_query_and_deprecation.test.ts b/src/core/server/elasticsearch/client/log_query_and_deprecation.test.ts index 30d5d8b87ed1c..c4c426cdf6c87 100644 --- a/src/core/server/elasticsearch/client/log_query_and_deprecation.test.ts +++ b/src/core/server/elasticsearch/client/log_query_and_deprecation.test.ts @@ -10,13 +10,14 @@ import { Buffer } from 'buffer'; import { Readable } from 'stream'; import { - Client, - ConnectionRequestParams, errors, - TransportRequestOptions, - TransportRequestParams, + type Client, + type ConnectionRequestParams, + type TransportRequestOptions, + type TransportRequestParams, + type DiagnosticResult, + type RequestBody, } from '@elastic/elasticsearch'; -import type { DiagnosticResult, RequestBody } from '@elastic/elasticsearch'; import { parseClientOptionsMock, ClientMock } from './configure_client.test.mocks'; import { loggingSystemMock } from '../../logging/logging_system.mock'; @@ -27,7 +28,7 @@ const createApiResponse = ({ statusCode = 200, headers = {}, warnings = null, - params, + params = { method: 'GET', path: '/path', querystring: '?wait_for_completion=true' }, requestOptions = {}, }: { body: T; @@ -77,10 +78,14 @@ describe('instrumentQueryAndDeprecationLogger', () => { jest.clearAllMocks(); }); - function createResponseWithBody(body?: RequestBody) { + function createResponseWithBody( + body?: RequestBody, + params?: { headers?: Record } + ) { return createApiResponse({ body: {}, statusCode: 200, + headers: params?.headers ?? {}, params: { method: 'GET', path: '/foo', @@ -107,15 +112,10 @@ describe('instrumentQueryAndDeprecationLogger', () => { }); client.diagnostic.emit('response', null, response); - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "200 + expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(` + "200 GET /foo?hello=dolly - {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}}", - undefined, - ], - ] + {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}}" `); }); @@ -132,15 +132,10 @@ describe('instrumentQueryAndDeprecationLogger', () => { ); client.diagnostic.emit('response', null, response); - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "200 + expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(` + "200 GET /foo?hello=dolly - {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}}", - undefined, - ], - ] + {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}}" `); }); @@ -159,15 +154,10 @@ describe('instrumentQueryAndDeprecationLogger', () => { ); client.diagnostic.emit('response', null, response); - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "200 + expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(` + "200 GET /foo?hello=dolly - [buffer]", - undefined, - ], - ] + [buffer]" `); }); @@ -186,15 +176,10 @@ describe('instrumentQueryAndDeprecationLogger', () => { ); client.diagnostic.emit('response', null, response); - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "200 + expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(` + "200 GET /foo?hello=dolly - [stream]", - undefined, - ], - ] + [stream]" `); }); @@ -204,14 +189,9 @@ describe('instrumentQueryAndDeprecationLogger', () => { const response = createResponseWithBody(); client.diagnostic.emit('response', null, response); - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "200 - GET /foo?hello=dolly", - undefined, - ], - ] + expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(` + "200 + GET /foo?hello=dolly" `); }); @@ -230,14 +210,9 @@ describe('instrumentQueryAndDeprecationLogger', () => { client.diagnostic.emit('response', null, response); - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "200 - GET /foo?city=M%C3%BCnich", - undefined, - ], - ] + expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(` + "200 + GET /foo?city=M%C3%BCnich" `); }); @@ -265,15 +240,10 @@ describe('instrumentQueryAndDeprecationLogger', () => { }); client.diagnostic.emit('response', new errors.ResponseError(response), response); - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "500 + expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(` + "500 GET /foo?hello=dolly - {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}} [internal server error]: internal server error", - undefined, - ], - ] + {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}} [internal server error]: internal server error" `); }); @@ -283,14 +253,9 @@ describe('instrumentQueryAndDeprecationLogger', () => { const response = createApiResponse({ body: {} }); client.diagnostic.emit('response', new errors.TimeoutError('message', response), response); - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "[TimeoutError]: message", - undefined, - ], - ] - `); + expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot( + `"[TimeoutError]: message"` + ); }); it('logs debug when the client emits an ResponseError returned by elasticsearch', () => { @@ -313,14 +278,9 @@ describe('instrumentQueryAndDeprecationLogger', () => { }); client.diagnostic.emit('response', new errors.ResponseError(response), response); - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "400 - GET /_path?hello=dolly [illegal_argument_exception]: request [/_path] contains unrecognized parameter: [name]", - undefined, - ], - ] + expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(` + "400 + GET /_path?hello=dolly [illegal_argument_exception]: request [/_path] contains unrecognized parameter: [name]" `); }); @@ -340,14 +300,9 @@ describe('instrumentQueryAndDeprecationLogger', () => { }); client.diagnostic.emit('response', new errors.ResponseError(response), response); - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "400 - GET /_path [undefined]: {\\"error\\":{}}", - undefined, - ], - ] + expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(` + "400 + GET /_path [undefined]: {\\"error\\":{}}" `); logger.debug.mockClear(); @@ -363,14 +318,9 @@ describe('instrumentQueryAndDeprecationLogger', () => { }); client.diagnostic.emit('response', new errors.ResponseError(response), response); - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "400 - GET /_path [undefined]: Response Error", - undefined, - ], - ] + expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(` + "400 + GET /_path [undefined]: Response Error" `); }); @@ -397,8 +347,21 @@ describe('instrumentQueryAndDeprecationLogger', () => { Object { "http": Object { "request": Object { + "headers": Object {}, "id": "opaque-id", + "method": "GET", }, + "response": Object { + "body": Object { + "bytes": undefined, + }, + "headers": Object {}, + "status_code": 400, + }, + }, + "url": Object { + "path": "/_path", + "query": undefined, }, } `); @@ -423,12 +386,48 @@ describe('instrumentQueryAndDeprecationLogger', () => { Object { "http": Object { "request": Object { + "headers": Object {}, "id": "opaque-id", + "method": "GET", }, + "response": Object { + "body": Object { + "bytes": undefined, + }, + "headers": Object {}, + "status_code": 400, + }, + }, + "url": Object { + "path": "/_path", + "query": undefined, }, } `); }); + + it('logs response size', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + const response = createResponseWithBody( + { + seq_no_primary_term: true, + query: { + term: { user: 'kimchy' }, + }, + }, + { + headers: { 'content-length': '12345678' }, + } + ); + + client.diagnostic.emit('response', null, response); + expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(` + "200 - 11.8MB + GET /foo?hello=dolly + {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}}" + `); + }); }); describe('deprecation warnings from response headers', () => { diff --git a/src/core/server/elasticsearch/client/log_query_and_deprecation.ts b/src/core/server/elasticsearch/client/log_query_and_deprecation.ts index fc5a0fa6e1111..b017fce0e342d 100644 --- a/src/core/server/elasticsearch/client/log_query_and_deprecation.ts +++ b/src/core/server/elasticsearch/client/log_query_and_deprecation.ts @@ -5,11 +5,13 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import type { IncomingHttpHeaders } from 'http'; import { Buffer } from 'buffer'; import { stringify } from 'querystring'; import { errors, DiagnosticResult, RequestBody, Client } from '@elastic/elasticsearch'; +import numeral from '@elastic/numeral'; import type { ElasticsearchErrorDetails } from './types'; +import { getEcsResponseLog } from './get_ecs_response_log'; import { Logger } from '../../logging'; const convertQueryString = (qs: string | Record | undefined): string => { @@ -38,6 +40,14 @@ export function getErrorMessage(error: errors.ElasticsearchClientError): string return `[${error.name}]: ${error.message}`; } +function getContentLength(headers?: IncomingHttpHeaders): number | undefined { + const contentLength = headers && headers['content-length']; + if (contentLength) { + const val = parseInt(contentLength, 10); + return !isNaN(val) ? val : undefined; + } +} + /** * returns a string in format: * @@ -47,10 +57,10 @@ export function getErrorMessage(error: errors.ElasticsearchClientError): string * * so it could be copy-pasted into the Dev console */ -function getResponseMessage(event: DiagnosticResult): string { +function getResponseMessage(event: DiagnosticResult, bytesMsg: string): string { const errorMeta = getRequestDebugMeta(event); const body = errorMeta.body ? `\n${errorMeta.body}` : ''; - return `${errorMeta.statusCode}\n${errorMeta.method} ${errorMeta.url}${body}`; + return `${errorMeta.statusCode}${bytesMsg}\n${errorMeta.method} ${errorMeta.url}${body}`; } /** @@ -93,21 +103,19 @@ export const instrumentEsQueryAndDeprecationLogger = ({ const deprecationLogger = logger.get('deprecation'); client.diagnostic.on('response', (error, event) => { if (event) { - const opaqueId = event.meta.request.options.opaqueId; - const meta = opaqueId - ? { - http: { request: { id: event.meta.request.options.opaqueId } }, - } - : undefined; // do not clutter logs if opaqueId is not present + const bytes = getContentLength(event.headers); + const bytesMsg = bytes ? ` - ${numeral(bytes).format('0.0b')}` : ''; + const meta = getEcsResponseLog(event, bytes); + let queryMsg = ''; if (error) { if (error instanceof errors.ResponseError) { - queryMsg = `${getResponseMessage(event)} ${getErrorMessage(error)}`; + queryMsg = `${getResponseMessage(event, bytesMsg)} ${getErrorMessage(error)}`; } else { queryMsg = getErrorMessage(error); } } else { - queryMsg = getResponseMessage(event); + queryMsg = getResponseMessage(event, bytesMsg); } queryLogger.debug(queryMsg, meta); diff --git a/src/core/server/http/router/headers.ts b/src/core/server/http/router/headers.ts index 89a32d6dc5d2f..68a1993565209 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 3d8dcad08149c..b1c421ec9168a 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 33f00248a110a..0352e655937da 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 967122e414731..6c11fa1f245c7 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 aad45b6fc0bcf..c4be329dabcb8 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)} - /> - - - - - + { // Controls Services Context @@ -108,8 +109,8 @@ export const ControlGroup = () => { return null; } - let panelBg: 'subdued' | 'primary' | 'success' = 'subdued'; - if (emptyState) panelBg = 'primary'; + let panelBg: 'subdued' | 'plain' | 'success' = 'subdued'; + if (emptyState) panelBg = 'plain'; if (draggingId) panelBg = 'success'; return ( @@ -201,10 +202,18 @@ export const ControlGroup = () => { ) : ( <> - - -

{ControlGroupStrings.emptyState.getCallToAction()}

-
+ + + + + + + {' '} + +

{ControlGroupStrings.emptyState.getCallToAction()}

+
+
+
diff --git a/src/plugins/controls/public/control_group/component/controls_illustration.scss b/src/plugins/controls/public/control_group/component/controls_illustration.scss new file mode 100644 index 0000000000000..589a584add493 --- /dev/null +++ b/src/plugins/controls/public/control_group/component/controls_illustration.scss @@ -0,0 +1,6 @@ +@include euiBreakpoint('xs', 's') { + .controlsIllustration { + width: $euiSize * 6; + height: $euiSize * 6; + } +} diff --git a/src/plugins/controls/public/control_group/component/controls_illustration.tsx b/src/plugins/controls/public/control_group/component/controls_illustration.tsx new file mode 100644 index 0000000000000..4b285ffcf17a8 --- /dev/null +++ b/src/plugins/controls/public/control_group/component/controls_illustration.tsx @@ -0,0 +1,255 @@ +/* + * Copyright 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 './controls_illustration.scss'; +import React from 'react'; + +export const ControlsIllustration = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/src/plugins/controls/public/control_group/control_group.scss b/src/plugins/controls/public/control_group/control_group.scss index e595bc245e31d..29e45e7fd1e69 100644 --- a/src/plugins/controls/public/control_group/control_group.scss +++ b/src/plugins/controls/public/control_group/control_group.scss @@ -11,22 +11,26 @@ $controlMinWidth: $euiSize * 14; &--empty { display: flex; @include euiBreakpoint('m', 'l', 'xl') { - background: url(opt_a.svg); - background-position: left top; - background-repeat: no-repeat; .addControlButton { text-align: center; } .emptyStateText { padding-left: $euiSize * 2; } + height: $euiSize * 4; + overflow: hidden; } @include euiBreakpoint('xs', 's') { .addControlButton { text-align: center; } + .emptyStateText { + text-align: center; + } + .controlsIllustration__container { + margin-bottom: 0 !important; + } } - min-height: $euiSize * 4; } &--twoLine { diff --git a/src/plugins/controls/public/control_group/opt_a.svg b/src/plugins/controls/public/control_group/opt_a.svg deleted file mode 100644 index 6722db6f26a55..0000000000000 --- a/src/plugins/controls/public/control_group/opt_a.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index d9733d1a35586..ef2355ef14bc6 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -263,12 +263,19 @@ export class DashboardContainer extends Container - + diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/_dashboard_viewport.scss b/src/plugins/dashboard/public/application/embeddable/viewport/_dashboard_viewport.scss index f71868b059159..5dad462803b3a 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/_dashboard_viewport.scss +++ b/src/plugins/dashboard/public/application/embeddable/viewport/_dashboard_viewport.scss @@ -8,6 +8,7 @@ .dshDashboardViewport-controlGroup { margin: 0 $euiSizeS 0 $euiSizeS; + padding-bottom: $euiSizeXS; } .dshDashboardEmptyScreen { diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx index a862c084de400..43cc00f928c6c 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx +++ b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx @@ -18,6 +18,7 @@ import { ControlGroupContainer } from '../../../../../controls/public'; export interface DashboardViewportProps { container: DashboardContainer; controlGroup?: ControlGroupContainer; + controlsEnabled?: boolean; } interface State { @@ -93,13 +94,15 @@ export class DashboardViewport extends React.Component -
+ {controlsEnabled ? ( +
+ ) : null}
{ + 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 a2515dbcb171d..8ef2b77b1fcc6 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 3e76f3a6426c2..600da4b16d274 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 0000000000000..304150ad94813 --- /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 a6ea8da6ac6e9..b678bd8781d93 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/data/public/index.ts b/src/plugins/data/public/index.ts index 567a0b1d8c6d9..2e746e4ecec93 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -83,6 +83,7 @@ export type { GetFieldsOptions, AggregationRestrictions, IndexPatternListItem, + DataViewListItem, } from '../common'; export { ES_FIELD_TYPES, diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 74b4edde21ae0..51dd883211620 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -20,6 +20,7 @@ import { UsageCollectionSetup } from '../../usage_collection/server'; import { AutocompleteService } from './autocomplete'; import { FieldFormatsSetup, FieldFormatsStart } from '../../field_formats/server'; import { getUiSettings } from './ui_settings'; +import { QuerySetup } from './query'; interface DataEnhancements { search: SearchEnhancements; @@ -27,6 +28,7 @@ interface DataEnhancements { export interface DataPluginSetup { search: ISearchSetup; + query: QuerySetup; /** * @deprecated - use "fieldFormats" plugin directly instead */ @@ -88,7 +90,7 @@ export class DataServerPlugin { bfetch, expressions, usageCollection, fieldFormats }: DataPluginSetupDependencies ) { this.scriptsService.setup(core); - this.queryService.setup(core); + const querySetup = this.queryService.setup(core); this.autocompleteService.setup(core); this.kqlTelemetryService.setup(core, { usageCollection }); @@ -105,6 +107,7 @@ export class DataServerPlugin searchSetup.__enhance(enhancements.search); }, search: searchSetup, + query: querySetup, fieldFormats, }; } diff --git a/src/plugins/data/server/query/index.ts b/src/plugins/data/server/query/index.ts index 1f394e9fc89f9..4439c17704642 100644 --- a/src/plugins/data/server/query/index.ts +++ b/src/plugins/data/server/query/index.ts @@ -7,3 +7,4 @@ */ export { QueryService } from './query_service'; +export type { QuerySetup } from './query_service'; diff --git a/src/plugins/data/server/query/query_service.ts b/src/plugins/data/server/query/query_service.ts index 173abeda0c951..33aeecbc11df1 100644 --- a/src/plugins/data/server/query/query_service.ts +++ b/src/plugins/data/server/query/query_service.ts @@ -36,3 +36,6 @@ export class QueryService implements Plugin { public start() {} } + +/** @public */ +export type QuerySetup = ReturnType; diff --git a/src/plugins/data_view_management/kibana.json b/src/plugins/data_view_management/kibana.json index 707f68d0eb8da..29f305d0ad17a 100644 --- a/src/plugins/data_view_management/kibana.json +++ b/src/plugins/data_view_management/kibana.json @@ -3,7 +3,7 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["management", "data", "urlForwarding", "dataViewFieldEditor", "dataViewEditor"], + "requiredPlugins": ["management", "data", "urlForwarding", "dataViewFieldEditor", "dataViewEditor", "dataViews", "fieldFormats"], "requiredBundles": ["kibanaReact", "kibanaUtils"], "owner": { "name": "App Services", diff --git a/src/plugins/data_view_management/public/components/breadcrumbs.ts b/src/plugins/data_view_management/public/components/breadcrumbs.ts index 43f9ca4374658..244bce41f8fd4 100644 --- a/src/plugins/data_view_management/public/components/breadcrumbs.ts +++ b/src/plugins/data_view_management/public/components/breadcrumbs.ts @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; -import { IndexPattern } from '../../../data/public'; +import { DataView } from '../../../data_views/public'; export function getListBreadcrumbs() { return [ @@ -32,7 +32,7 @@ export function getCreateBreadcrumbs() { ]; } -export function getEditBreadcrumbs(indexPattern: IndexPattern) { +export function getEditBreadcrumbs(indexPattern: DataView) { return [ ...getListBreadcrumbs(), { @@ -42,7 +42,7 @@ export function getEditBreadcrumbs(indexPattern: IndexPattern) { ]; } -export function getEditFieldBreadcrumbs(indexPattern: IndexPattern, fieldName: string) { +export function getEditFieldBreadcrumbs(indexPattern: DataView, fieldName: string) { return [ ...getEditBreadcrumbs(indexPattern), { @@ -51,7 +51,7 @@ export function getEditFieldBreadcrumbs(indexPattern: IndexPattern, fieldName: s ]; } -export function getCreateFieldBreadcrumbs(indexPattern: IndexPattern) { +export function getCreateFieldBreadcrumbs(indexPattern: DataView) { return [ ...getEditBreadcrumbs(indexPattern), { diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx index 843838d9e0fbc..0f41c08fbc6fe 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx @@ -11,7 +11,7 @@ import { RouteComponentProps, withRouter } from 'react-router-dom'; import { EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IndexPattern, IndexPatternField } from '../../../../../../plugins/data/public'; +import { DataView, DataViewField } from '../../../../../../plugins/data_views/public'; import { useKibana } from '../../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../../types'; import { IndexHeader } from '../index_header'; @@ -20,7 +20,7 @@ import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS } from '../constants'; import { FieldEditor } from '../../field_editor'; interface CreateEditFieldProps extends RouteComponentProps { - indexPattern: IndexPattern; + indexPattern: DataView; mode?: string; fieldName?: string; } @@ -34,7 +34,7 @@ const newFieldPlaceholder = i18n.translate( export const CreateEditField = withRouter( ({ indexPattern, mode, fieldName, history }: CreateEditFieldProps) => { - const { uiSettings, chrome, notifications, data } = + const { uiSettings, chrome, notifications, dataViews } = useKibana().services; const spec = mode === 'edit' && fieldName @@ -43,7 +43,7 @@ export const CreateEditField = withRouter( scripted: true, type: 'number', name: undefined, - } as unknown as IndexPatternField); + } as unknown as DataViewField); const url = `/dataView/${indexPattern.id}`; @@ -76,7 +76,7 @@ export const CreateEditField = withRouter( indexPattern={indexPattern} spec={spec} services={{ - indexPatternService: data.indexPatterns, + indexPatternService: dataViews, redirectAway, }} /> diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/create_edit_field/create_edit_field_container.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/create_edit_field/create_edit_field_container.tsx index c35ddb556e18a..f9cb95f67eabc 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/create_edit_field/create_edit_field_container.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/create_edit_field/create_edit_field_container.tsx @@ -9,7 +9,7 @@ import React, { useEffect, useState } from 'react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { IndexPattern } from '../../../../../../plugins/data/public'; +import type { DataView } from '../../../../../../plugins/data_views/public'; import { getEditFieldBreadcrumbs, getCreateFieldBreadcrumbs } from '../../breadcrumbs'; import { useKibana } from '../../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../../types'; @@ -18,13 +18,13 @@ import { CreateEditField } from './create_edit_field'; export type CreateEditFieldContainerProps = RouteComponentProps<{ id: string; fieldName?: string }>; const CreateEditFieldCont: React.FC = ({ ...props }) => { - const { setBreadcrumbs, data } = useKibana().services; - const [indexPattern, setIndexPattern] = useState(); + const { setBreadcrumbs, dataViews } = useKibana().services; + const [indexPattern, setIndexPattern] = useState(); const fieldName = props.match.params.fieldName && decodeURIComponent(props.match.params.fieldName); useEffect(() => { - data.indexPatterns.get(props.match.params.id).then((ip: IndexPattern) => { + dataViews.get(props.match.params.id).then((ip: DataView) => { setIndexPattern(ip); if (ip) { setBreadcrumbs( @@ -32,7 +32,7 @@ const CreateEditFieldCont: React.FC = ({ ...props ); } }); - }, [props.match.params.id, fieldName, setBreadcrumbs, data.indexPatterns]); + }, [props.match.params.id, fieldName, setBreadcrumbs, dataViews]); if (indexPattern) { return ( diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx index 2255b4ff2eb47..6b0d7912ce598 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { IndexPattern, IndexPatternField } from '../../../../../plugins/data/public'; +import { DataView, DataViewField } from '../../../../../plugins/data_views/public'; import { useKibana } from '../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../types'; import { Tabs } from './tabs'; @@ -28,7 +28,7 @@ import { IndexHeader } from './index_header'; import { getTags } from '../utils'; export interface EditIndexPatternProps extends RouteComponentProps { - indexPattern: IndexPattern; + indexPattern: DataView; } const mappingAPILink = i18n.translate( @@ -65,10 +65,10 @@ const securitySolution = 'security-solution'; export const EditIndexPattern = withRouter( ({ indexPattern, history, location }: EditIndexPatternProps) => { - const { application, uiSettings, overlays, chrome, data } = + const { application, uiSettings, overlays, chrome, dataViews } = useKibana().services; - const [fields, setFields] = useState(indexPattern.getNonScriptedFields()); - const [conflictedFields, setConflictedFields] = useState( + const [fields, setFields] = useState(indexPattern.getNonScriptedFields()); + const [conflictedFields, setConflictedFields] = useState( indexPattern.fields.getAll().filter((field) => field.type === 'conflict') ); const [defaultIndex, setDefaultIndex] = useState(uiSettings.get('defaultIndex')); @@ -93,7 +93,7 @@ export const EditIndexPattern = withRouter( const removePattern = () => { async function doRemove() { if (indexPattern.id === defaultIndex) { - const indexPatterns = await data.dataViews.getIdsWithTitle(); + const indexPatterns = await dataViews.getIdsWithTitle(); uiSettings.remove('defaultIndex'); const otherPatterns = filter(indexPatterns, (pattern) => { return pattern.id !== indexPattern.id; @@ -104,7 +104,7 @@ export const EditIndexPattern = withRouter( } } if (indexPattern.id) { - Promise.resolve(data.dataViews.delete(indexPattern.id)).then(function () { + Promise.resolve(dataViews.delete(indexPattern.id)).then(function () { history.push(''); }); } @@ -201,7 +201,7 @@ export const EditIndexPattern = withRouter( > = ({ ...props }) => { - const { data, setBreadcrumbs } = useKibana().services; - const [indexPattern, setIndexPattern] = useState(); + const { dataViews, setBreadcrumbs } = useKibana().services; + const [indexPattern, setIndexPattern] = useState(); useEffect(() => { - data.indexPatterns.get(props.match.params.id).then((ip: IndexPattern) => { + dataViews.get(props.match.params.id).then((ip: DataView) => { setIndexPattern(ip); setBreadcrumbs(getEditBreadcrumbs(ip)); }); - }, [data.indexPatterns, props.match.params.id, setBreadcrumbs]); + }, [dataViews, props.match.params.id, setBreadcrumbs]); if (indexPattern) { return ; diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/index_header/index_header.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/index_header/index_header.tsx index 3352fa194759f..b64aed5c0811c 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/index_header/index_header.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/index_header/index_header.tsx @@ -9,10 +9,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiPageHeader, EuiToolTip } from '@elastic/eui'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { DataView } from 'src/plugins/data_views/public'; interface IndexHeaderProps { - indexPattern: IIndexPattern; + indexPattern: DataView; defaultIndex?: string; setDefault?: () => void; deleteIndexPatternClick?: () => void; diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx index f85f7bb254826..b2197a6dcb203 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { IndexPattern } from 'src/plugins/data/public'; +import { DataView } from 'src/plugins/data_views/public'; import { IndexedFieldItem } from '../../types'; import { Table, renderFieldName, getConflictModalContent } from './table'; import { overlayServiceMock, themeServiceMock } from 'src/core/public/mocks'; @@ -17,7 +17,7 @@ const theme = themeServiceMock.createStartContract(); const indexPattern = { timeFieldName: 'timestamp', -} as IndexPattern; +} as DataView; const items: IndexedFieldItem[] = [ { diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx index 7e915e3c930a5..f860d9fafb1c0 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx @@ -30,7 +30,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { toMountPoint } from '../../../../../../../kibana_react/public'; -import { IIndexPattern } from '../../../../../../../data/public'; +import { DataView } from '../../../../../../../data_views/public'; import { IndexedFieldItem } from '../../types'; // localized labels @@ -174,7 +174,7 @@ const conflictType = i18n.translate( ); interface IndexedFieldProps { - indexPattern: IIndexPattern; + indexPattern: DataView; items: IndexedFieldItem[]; editField: (field: IndexedFieldItem) => void; deleteField: (fieldName: string) => void; diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx index 0427bfba7105d..4773dbff38a28 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { IndexPatternField, IndexPattern, IndexPatternType } from 'src/plugins/data/public'; +import { DataViewField, DataView, DataViewType } from 'src/plugins/data_views/public'; import { IndexedFieldsTable } from './indexed_fields_table'; import { getFieldInfo } from '../../utils'; @@ -36,10 +36,10 @@ const helpers = { const indexPattern = { getNonScriptedFields: () => fields, getFormatterForFieldNoDefault: () => ({ params: () => ({}) }), -} as unknown as IndexPattern; +} as unknown as DataView; const rollupIndexPattern = { - type: IndexPatternType.ROLLUP, + type: DataViewType.ROLLUP, typeMeta: { params: { 'rollup-index': 'rollup', @@ -64,12 +64,12 @@ const rollupIndexPattern = { }, getNonScriptedFields: () => fields, getFormatterForFieldNoDefault: () => ({ params: () => ({}) }), -} as unknown as IndexPattern; +} as unknown as DataView; const mockFieldToIndexPatternField = ( spec: Record ) => { - return new IndexPatternField(spec as unknown as IndexPatternField['spec']); + return new DataViewField(spec as unknown as DataViewField['spec']); }; const fields = [ diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 29b8d82a99704..667a4e029e02b 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -9,21 +9,21 @@ import React, { Component } from 'react'; import { createSelector } from 'reselect'; import { OverlayStart, ThemeServiceStart } from 'src/core/public'; -import { IndexPatternField, IndexPattern } from '../../../../../../plugins/data/public'; +import { DataViewField, DataView } from '../../../../../../plugins/data_views/public'; import { useKibana } from '../../../../../../plugins/kibana_react/public'; import { Table } from './components/table'; import { IndexedFieldItem } from './types'; import { IndexPatternManagmentContext } from '../../../types'; interface IndexedFieldsTableProps { - fields: IndexPatternField[]; - indexPattern: IndexPattern; + fields: DataViewField[]; + indexPattern: DataView; fieldFilter?: string; indexedFieldTypeFilter?: string; helpers: { editField: (fieldName: string) => void; deleteField: (fieldName: string) => void; - getFieldInfo: (indexPattern: IndexPattern, field: IndexPatternField) => string[]; + getFieldInfo: (indexPattern: DataView, field: DataViewField) => string[]; }; fieldWildcardMatcher: (filters: any[]) => (val: any) => boolean; userEditPermission: boolean; @@ -61,7 +61,7 @@ class IndexedFields extends Component & DataViewFieldBase; +type IndexedFieldItemBase = Partial & DataViewFieldBase; export interface IndexedFieldItem extends IndexedFieldItemBase { info: string[]; diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx index 7b53b9a6c20c5..c37399e1d02a9 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx @@ -11,9 +11,9 @@ import { shallow } from 'enzyme'; import { Table } from '../table'; import { ScriptedFieldItem } from '../../types'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { DataView } from 'src/plugins/data_views/public'; -const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as IIndexPattern); +const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as DataView); const items: ScriptedFieldItem[] = [ { name: '1', lang: 'painless', script: '', isUserEditable: true }, @@ -21,7 +21,7 @@ const items: ScriptedFieldItem[] = [ ]; describe('Table', () => { - let indexPattern: IIndexPattern; + let indexPattern: DataView; beforeEach(() => { indexPattern = getIndexPatternMock({ diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx index 5064f40f43297..1e2226ac4ab38 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx @@ -12,10 +12,10 @@ import { i18n } from '@kbn/i18n'; import { EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui'; import { ScriptedFieldItem } from '../../types'; -import { IIndexPattern } from '../../../../../../../data/public'; +import { DataView } from '../../../../../../../data_views/public'; interface TableProps { - indexPattern: IIndexPattern; + indexPattern: DataView; items: ScriptedFieldItem[]; editField: (field: ScriptedFieldItem) => void; deleteField: (field: ScriptedFieldItem) => void; diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx index 76433a7b49e27..2e3657b23c331 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx @@ -17,11 +17,11 @@ import { Table, Header, CallOuts, DeleteScritpedFieldConfirmationModal } from '. import { ScriptedFieldItem } from './types'; import { IndexPatternManagmentContext } from '../../../types'; -import { IndexPattern, DataPublicPluginStart } from '../../../../../../plugins/data/public'; +import { DataView, DataViewsPublicPluginStart } from '../../../../../../plugins/data_views/public'; import { useKibana } from '../../../../../../plugins/kibana_react/public'; interface ScriptedFieldsTableProps { - indexPattern: IndexPattern; + indexPattern: DataView; fieldFilter?: string; scriptedFieldLanguageFilter?: string; helpers: { @@ -30,7 +30,7 @@ interface ScriptedFieldsTableProps { }; onRemoveField?: () => void; painlessDocLink: string; - saveIndexPattern: DataPublicPluginStart['indexPatterns']['updateSavedObject']; + saveIndexPattern: DataViewsPublicPluginStart['updateSavedObject']; userEditPermission: boolean; } diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx index 23255086559b4..f9c7f41030837 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx @@ -11,13 +11,13 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { Table, TableProps, TableState } from './table'; import { EuiTableFieldDataColumnType, keys } from '@elastic/eui'; -import { IndexPattern } from 'src/plugins/data/public'; +import { DataView } from 'src/plugins/data_views/public'; import { SourceFiltersTableFilter } from '../../types'; -const indexPattern = {} as IndexPattern; +const indexPattern = {} as DataView; const items: SourceFiltersTableFilter[] = [{ value: 'tim*', clientId: '' }]; -const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as IndexPattern); +const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as DataView); const getTableColumnRender = ( component: ShallowWrapper, diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx index 237a876688c5d..144f6d19599bd 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx @@ -19,7 +19,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { IndexPattern } from 'src/plugins/data/public'; +import { DataView } from 'src/plugins/data_views/public'; import { SourceFiltersTableFilter } from '../../types'; const filterHeader = i18n.translate( @@ -69,7 +69,7 @@ const cancelAria = i18n.translate( ); export interface TableProps { - indexPattern: IndexPattern; + indexPattern: DataView; items: SourceFiltersTableFilter[]; deleteFilter: Function; fieldWildcardMatcher: Function; diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.test.tsx index 5b332473e41a8..13d1bd72c96ef 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { SourceFiltersTable } from './source_filters_table'; -import { IndexPattern } from 'src/plugins/data/public'; +import { DataView } from 'src/plugins/data_views/public'; jest.mock('@elastic/eui', () => ({ EuiButton: 'eui-button', @@ -41,7 +41,7 @@ const getIndexPatternMock = (mockedFields: any = {}) => ({ sourceFilters: [{ value: 'time*' }, { value: 'nam*' }, { value: 'age*' }], ...mockedFields, - } as IndexPattern); + } as DataView); describe('SourceFiltersTable', () => { test('should render normally', () => { @@ -93,7 +93,7 @@ describe('SourceFiltersTable', () => { indexPattern={ getIndexPatternMock({ sourceFilters: [{ value: 'tim*' }], - }) as IndexPattern + }) as DataView } filterFilter={''} fieldWildcardMatcher={() => {}} @@ -113,7 +113,7 @@ describe('SourceFiltersTable', () => { indexPattern={ getIndexPatternMock({ sourceFilters: [{ value: 'tim*' }, { value: 'na*' }], - }) as IndexPattern + }) as DataView } filterFilter={''} fieldWildcardMatcher={() => {}} @@ -157,7 +157,7 @@ describe('SourceFiltersTable', () => { indexPattern={ getIndexPatternMock({ sourceFilters: [{ value: 'tim*' }], - }) as IndexPattern + }) as DataView } filterFilter={''} fieldWildcardMatcher={() => {}} diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.tsx index 9a0c7390d2c4d..80451e95fb5ed 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.tsx @@ -11,15 +11,15 @@ import { createSelector } from 'reselect'; import { EuiSpacer } from '@elastic/eui'; import { AddFilter, Table, Header, DeleteFilterConfirmationModal } from './components'; -import { IndexPattern, DataPublicPluginStart } from '../../../../../../plugins/data/public'; +import { DataView, DataViewsPublicPluginStart } from '../../../../../../plugins/data_views/public'; import { SourceFiltersTableFilter } from './types'; export interface SourceFiltersTableProps { - indexPattern: IndexPattern; + indexPattern: DataView; filterFilter: string; fieldWildcardMatcher: Function; onAddOrRemoveFilter?: Function; - saveIndexPattern: DataPublicPluginStart['indexPatterns']['updateSavedObject']; + saveIndexPattern: DataViewsPublicPluginStart['updateSavedObject']; } export interface SourceFiltersTableState { diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx index 58b064fa79893..f24374a85a9ae 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx @@ -22,11 +22,11 @@ import { import { i18n } from '@kbn/i18n'; import { fieldWildcardMatcher } from '../../../../../kibana_utils/public'; import { - IndexPattern, - IndexPatternField, - UI_SETTINGS, - DataPublicPluginStart, -} from '../../../../../../plugins/data/public'; + DataView, + DataViewField, + DataViewsPublicPluginStart, + META_FIELDS, +} from '../../../../../../plugins/data_views/public'; import { useKibana } from '../../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../../types'; import { createEditIndexPatternPageStateContainer } from '../edit_index_pattern_state_container'; @@ -38,9 +38,9 @@ import { getTabs, getPath, convertToEuiSelectOption } from './utils'; import { getFieldInfo } from '../../utils'; interface TabsProps extends Pick { - indexPattern: IndexPattern; - fields: IndexPatternField[]; - saveIndexPattern: DataPublicPluginStart['indexPatterns']['updateSavedObject']; + indexPattern: DataView; + fields: DataViewField[]; + saveIndexPattern: DataViewsPublicPluginStart['updateSavedObject']; refreshFields: () => void; } @@ -150,7 +150,7 @@ export function Tabs({ }, [closeFieldEditor]); const fieldWildcardMatcherDecorated = useCallback( - (filters: string[]) => fieldWildcardMatcher(filters, uiSettings.get(UI_SETTINGS.META_FIELDS)), + (filters: string[]) => fieldWildcardMatcher(filters, uiSettings.get(META_FIELDS)), [uiSettings] ); @@ -254,7 +254,7 @@ export function Tabs({ fieldFilter={fieldFilter} scriptedFieldLanguageFilter={scriptedFieldLanguageFilter} helpers={{ - redirectToRoute: (field: IndexPatternField) => { + redirectToRoute: (field: DataViewField) => { history.push(getPath(field, indexPattern)); }, }} diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/utils.test.ts b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/utils.test.ts index 98502776616ae..738f62f647ac2 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/utils.test.ts +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/utils.test.ts @@ -7,13 +7,13 @@ */ import { getPath } from './utils'; -import { IndexPatternField, IndexPattern } from '../../../../../data/public'; +import { DataViewField, DataView } from '../../../../../data_views/public'; test('getPath() should encode "fieldName"', () => { expect( getPath( - { name: 'Memory: Allocated Bytes/sec' } as unknown as IndexPatternField, - { id: 'id' } as unknown as IndexPattern + { name: 'Memory: Allocated Bytes/sec' } as unknown as DataViewField, + { id: 'id' } as unknown as DataView ) ).toMatchInlineSnapshot(`"/dataView/id/field/Memory%3A%20Allocated%20Bytes%2Fsec"`); }); diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/utils.ts b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/utils.ts index 5f7e989cc5753..0ea8d9d9e28f3 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/utils.ts +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/utils.ts @@ -8,17 +8,17 @@ import { Dictionary, countBy, defaults, uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { IndexPattern, IndexPatternField } from '../../../../../../plugins/data/public'; +import { DataView, DataViewField } from '../../../../../../plugins/data_views/public'; import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS, TAB_SOURCE_FILTERS } from '../constants'; import { areScriptedFieldsEnabled } from '../../utils'; -function filterByName(items: IndexPatternField[], filter: string) { +function filterByName(items: DataViewField[], filter: string) { const lowercaseFilter = (filter || '').toLowerCase(); return items.filter((item) => item.name.toLowerCase().includes(lowercaseFilter)); } function getCounts( - fields: IndexPatternField[], + fields: DataViewField[], sourceFilters: { excludes: string[]; }, @@ -68,7 +68,7 @@ function getTitle(type: string, filteredCount: Dictionary, totalCount: D return title + count; } -export function getTabs(indexPattern: IndexPattern, fieldFilter: string) { +export function getTabs(indexPattern: DataView, fieldFilter: string) { const totalCount = getCounts(indexPattern.fields.getAll(), indexPattern.getSourceFiltering()); const filteredCount = getCounts( indexPattern.fields.getAll(), @@ -101,7 +101,7 @@ export function getTabs(indexPattern: IndexPattern, fieldFilter: string) { return tabs; } -export function getPath(field: IndexPatternField, indexPattern: IndexPattern) { +export function getPath(field: DataViewField, indexPattern: DataView) { return `/dataView/${indexPattern?.id}/field/${encodeURIComponent(field.name)}`; } diff --git a/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/help_flyout.test.tsx b/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/help_flyout.test.tsx index 3eeb97351eb39..949032e57b71a 100644 --- a/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/help_flyout.test.tsx +++ b/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/help_flyout.test.tsx @@ -11,7 +11,7 @@ import { shallow } from 'enzyme'; import { ScriptingHelpFlyout } from './help_flyout'; -import { IndexPattern } from '../../../../../../data/public'; +import { DataView } from '../../../../../../data_views/public'; import { ExecuteScript } from '../../types'; @@ -21,7 +21,7 @@ jest.mock('./test_script', () => ({ }, })); -const indexPatternMock = {} as IndexPattern; +const indexPatternMock = {} as DataView; describe('ScriptingHelpFlyout', () => { it('should render normally', async () => { diff --git a/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/help_flyout.tsx b/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/help_flyout.tsx index a65f51a2b5d54..74bbb90a3d898 100644 --- a/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/help_flyout.tsx +++ b/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/help_flyout.tsx @@ -13,10 +13,10 @@ import { ScriptingSyntax } from './scripting_syntax'; import { TestScript } from './test_script'; import { ExecuteScript } from '../../types'; -import { IndexPattern } from '../../../../../../data/public'; +import { DataView } from '../../../../../../data_views/public'; interface ScriptingHelpFlyoutProps { - indexPattern: IndexPattern; + indexPattern: DataView; lang: string; name?: string; script?: string; diff --git a/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.tsx b/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.tsx index 36e61af6cea33..89f80f8a87e38 100644 --- a/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.tsx +++ b/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.tsx @@ -24,13 +24,15 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { esQuery, IndexPattern, Query } from '../../../../../../../plugins/data/public'; +import { Query, buildEsQuery } from '@kbn/es-query'; +import { getEsQueryConfig } from '../../../../../../../plugins/data/public'; +import { DataView } from '../../../../../../../plugins/data_views/public'; import { context as contextType } from '../../../../../../kibana_react/public'; import { IndexPatternManagmentContextValue } from '../../../../types'; import { ExecuteScript } from '../../types'; interface TestScriptProps { - indexPattern: IndexPattern; + indexPattern: DataView; lang: string; name?: string; script?: string; @@ -82,13 +84,8 @@ export class TestScript extends Component { let query; if (searchContext) { - const esQueryConfigs = esQuery.getEsQueryConfig(this.context.services.uiSettings); - query = esQuery.buildEsQuery( - this.props.indexPattern, - searchContext.query || [], - [], - esQueryConfigs - ); + const esQueryConfigs = getEsQueryConfig(this.context.services.uiSettings); + query = buildEsQuery(this.props.indexPattern, searchContext.query || [], [], esQueryConfigs); } const scriptResponse = await executeScript({ diff --git a/src/plugins/data_view_management/public/components/field_editor/constants/index.ts b/src/plugins/data_view_management/public/components/field_editor/constants/index.ts index b4fb4481a5b95..a41b1e235fafc 100644 --- a/src/plugins/data_view_management/public/components/field_editor/constants/index.ts +++ b/src/plugins/data_view_management/public/components/field_editor/constants/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { getKbnTypeNames } from '../../../../../data/public'; +import { getKbnTypeNames } from '@kbn/field-types'; export const FIELD_TYPES_BY_LANG = { painless: ['number', 'string', 'date', 'boolean'], diff --git a/src/plugins/data_view_management/public/components/field_editor/field_editor.test.tsx b/src/plugins/data_view_management/public/components/field_editor/field_editor.test.tsx index 34a54acb82e86..39f8469dc0989 100644 --- a/src/plugins/data_view_management/public/components/field_editor/field_editor.test.tsx +++ b/src/plugins/data_view_management/public/components/field_editor/field_editor.test.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPattern, IndexPatternField, IndexPatternsService } from 'src/plugins/data/public'; +import { DataView, DataViewField, DataViewsService } from 'src/plugins/data_views/public'; import { FieldFormatInstanceType } from 'src/plugins/field_formats/common'; import { findTestSubject } from '@elastic/eui/lib/test'; @@ -67,7 +67,7 @@ jest.mock('./components/field_format_editor', () => ({ const fieldList = [ { name: 'foobar', - } as IndexPatternField, + } as DataViewField, ]; const fields = { @@ -95,17 +95,17 @@ const field = { const services = { redirectAway: () => {}, saveIndexPattern: async () => {}, - indexPatternService: {} as IndexPatternsService, + indexPatternService: {} as DataViewsService, }; describe('FieldEditor', () => { - let indexPattern: IndexPattern; + let indexPattern: DataView; const mockContext = mockManagementPlugin.createIndexPatternManagmentContext(); - mockContext.data.fieldFormats.getDefaultType = jest.fn( + mockContext.fieldFormats.getDefaultType = jest.fn( () => ({} as unknown as FieldFormatInstanceType) ); - mockContext.data.fieldFormats.getByFieldType = jest.fn((fieldType) => { + mockContext.fieldFormats.getByFieldType = jest.fn((fieldType) => { if (fieldType === 'number') { return [{} as unknown as FieldFormatInstanceType]; } else { @@ -118,7 +118,7 @@ describe('FieldEditor', () => { fields, getFormatterForField: () => ({ params: () => ({}) }), getFormatterForFieldNoDefault: () => ({ params: () => ({}) }), - } as unknown as IndexPattern; + } as unknown as DataView; }); it('should render create new scripted field correctly', async () => { @@ -126,7 +126,7 @@ describe('FieldEditor', () => { FieldEditor, { indexPattern, - spec: field as unknown as IndexPatternField, + spec: field as unknown as DataViewField, services, }, mockContext @@ -143,19 +143,19 @@ describe('FieldEditor', () => { name: 'test', script: 'doc.test.value', }; - fieldList.push(testField as unknown as IndexPatternField); + fieldList.push(testField as unknown as DataViewField); indexPattern.fields.getByName = (name) => { const flds = { [testField.name]: testField, }; - return flds[name] as unknown as IndexPatternField; + return flds[name] as unknown as DataViewField; }; const component = createComponentWithContext( FieldEditor, { indexPattern, - spec: testField as unknown as IndexPatternField, + spec: testField as unknown as DataViewField, services, }, mockContext @@ -173,7 +173,7 @@ describe('FieldEditor', () => { lang: undefined, type: 'string', customLabel: 'Test', - } as unknown as IndexPatternField; + } as unknown as DataViewField; fieldList.push(testField); indexPattern.fields.getByName = (name) => { const flds = { @@ -185,7 +185,7 @@ describe('FieldEditor', () => { ...indexPattern.fields, ...{ update: (fld) => { - testField = fld as unknown as IndexPatternField; + testField = fld as unknown as DataViewField; }, add: jest.fn(), }, @@ -197,12 +197,12 @@ describe('FieldEditor', () => { FieldEditor, { indexPattern, - spec: testField as unknown as IndexPatternField, + spec: testField as unknown as DataViewField, services: { redirectAway: () => {}, indexPatternService: { updateSavedObject: jest.fn(() => Promise.resolve()), - } as unknown as IndexPatternsService, + } as unknown as DataViewsService, }, }, mockContext @@ -227,19 +227,19 @@ describe('FieldEditor', () => { script: 'doc.test.value', lang: 'testlang', }; - fieldList.push(testField as unknown as IndexPatternField); + fieldList.push(testField as unknown as DataViewField); indexPattern.fields.getByName = (name) => { const flds = { [testField.name]: testField, }; - return flds[name] as unknown as IndexPatternField; + return flds[name] as unknown as DataViewField; }; const component = createComponentWithContext( FieldEditor, { indexPattern, - spec: testField as unknown as IndexPatternField, + spec: testField as unknown as DataViewField, services, }, mockContext @@ -256,7 +256,7 @@ describe('FieldEditor', () => { FieldEditor, { indexPattern, - spec: testField as unknown as IndexPatternField, + spec: testField as unknown as DataViewField, services, }, mockContext @@ -281,7 +281,7 @@ describe('FieldEditor', () => { FieldEditor, { indexPattern, - spec: testField as unknown as IndexPatternField, + spec: testField as unknown as DataViewField, services, }, mockContext @@ -302,7 +302,7 @@ describe('FieldEditor', () => { FieldEditor, { indexPattern, - spec: testField as unknown as IndexPatternField, + spec: testField as unknown as DataViewField, services, }, mockContext diff --git a/src/plugins/data_view_management/public/components/field_editor/field_editor.tsx b/src/plugins/data_view_management/public/components/field_editor/field_editor.tsx index ce680e197073c..4455b554eb053 100644 --- a/src/plugins/data_view_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/data_view_management/public/components/field_editor/field_editor.tsx @@ -34,18 +34,18 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { PainlessLang } from '@kbn/monaco'; import type { FieldFormatInstanceType } from 'src/plugins/field_formats/common'; +import type { FieldFormatsStart } from 'src/plugins/field_formats/public'; +import { KBN_FIELD_TYPES, ES_FIELD_TYPES } from '@kbn/field-types'; import { getEnabledScriptingLanguages, getDeprecatedScriptingLanguages, getSupportedScriptingLanguages, } from '../../scripting_languages'; import { - IndexPattern, - IndexPatternField, - KBN_FIELD_TYPES, - ES_FIELD_TYPES, - DataPublicPluginStart, -} from '../../../../../plugins/data/public'; + DataView, + DataViewField, + DataViewsPublicPluginStart, +} from '../../../../../plugins/data_views/public'; import { context as contextType, CodeEditor } from '../../../../kibana_react/public'; import { ScriptingDisabledCallOut, @@ -60,9 +60,9 @@ import { FIELD_TYPES_BY_LANG, DEFAULT_FIELD_TYPES } from './constants'; import { executeScript, isScriptValid } from './lib'; const getFieldTypeFormatsList = ( - field: IndexPatternField['spec'], + field: DataViewField['spec'], defaultFieldFormat: FieldFormatInstanceType, - fieldFormats: DataPublicPluginStart['fieldFormats'] + fieldFormats: FieldFormatsStart ) => { const formatsByType = fieldFormats .getByFieldType(field.type as KBN_FIELD_TYPES) @@ -109,16 +109,16 @@ export interface FieldEditorState { isSaving: boolean; errors?: string[]; format: any; - spec: IndexPatternField['spec']; + spec: DataViewField['spec']; customLabel: string; } export interface FieldEdiorProps { - indexPattern: IndexPattern; - spec: IndexPatternField['spec']; + indexPattern: DataView; + spec: DataViewField['spec']; services: { redirectAway: () => void; - indexPatternService: DataPublicPluginStart['indexPatterns']; + indexPatternService: DataViewsPublicPluginStart; }; } @@ -141,7 +141,7 @@ export class FieldEditor extends PureComponent f.name), + existingFieldNames: indexPattern.fields.getAll().map((f: DataViewField) => f.name), fieldFormatId: undefined, fieldFormatParams: {}, showScriptingHelp: false, @@ -159,7 +159,7 @@ export class FieldEditor extends PureComponent { - const { data } = this.context.services; + const { fieldFormats } = this.context.services; const { spec, format } = this.state; - const DefaultFieldFormat = data.fieldFormats.getDefaultType(type) as FieldFormatInstanceType; + const DefaultFieldFormat = fieldFormats.getDefaultType(type) as FieldFormatInstanceType; spec.type = type; this.setState({ - fieldTypeFormats: getFieldTypeFormatsList(spec, DefaultFieldFormat, data.fieldFormats), + fieldTypeFormats: getFieldTypeFormatsList(spec, DefaultFieldFormat, fieldFormats), fieldFormatId: DefaultFieldFormat.id, fieldFormatParams: format.params(), }); @@ -233,9 +233,9 @@ export class FieldEditor extends PureComponent { const { fieldTypeFormats } = this.state; - const { uiSettings, data } = this.context.services; + const { uiSettings, fieldFormats } = this.context.services; - const FieldFormat = data.fieldFormats.getType( + const FieldFormat = fieldFormats.getType( formatId || (fieldTypeFormats[0] as InitialFieldTypeFormat).defaultFieldFormat.id ) as FieldFormatInstanceType; @@ -819,7 +819,7 @@ export class FieldEditor extends PureComponent; diff --git a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx index fdbd6543b08ba..965fcdb6d8818 100644 --- a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -79,7 +79,7 @@ export const IndexPatternTable = ({ uiSettings, indexPatternManagementStart, chrome, - data, + dataViews, IndexPatternEditor, } = useKibana().services; const [indexPatterns, setIndexPatterns] = useState([]); @@ -91,18 +91,18 @@ export const IndexPatternTable = ({ (async function () { const gettedIndexPatterns: IndexPatternTableItem[] = await getIndexPatterns( uiSettings.get('defaultIndex'), - data.dataViews + dataViews ); setIndexPatterns(gettedIndexPatterns); setIsLoadingIndexPatterns(false); if ( gettedIndexPatterns.length === 0 || - !(await data.dataViews.hasUserDataView().catch(() => false)) + !(await dataViews.hasUserDataView().catch(() => false)) ) { setShowCreateDialog(true); } })(); - }, [indexPatternManagementStart, uiSettings, data]); + }, [indexPatternManagementStart, uiSettings, dataViews]); chrome.docTitle.change(title); diff --git a/src/plugins/data_view_management/public/components/utils.test.ts b/src/plugins/data_view_management/public/components/utils.test.ts index 5e36d1bbc907f..cb318b79e223d 100644 --- a/src/plugins/data_view_management/public/components/utils.test.ts +++ b/src/plugins/data_view_management/public/components/utils.test.ts @@ -5,7 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { IndexPatternsContract } from 'src/plugins/data/public'; + +import type { DataViewsContract } from 'src/plugins/data_views/public'; import { getIndexPatterns } from './utils'; const indexPatternContractMock = { @@ -22,7 +23,7 @@ const indexPatternContractMock = { ]) ), get: jest.fn().mockReturnValue(Promise.resolve({})), -} as unknown as jest.Mocked; +} as unknown as jest.Mocked; test('getting index patterns', async () => { const indexPatterns = await getIndexPatterns('test', indexPatternContractMock); diff --git a/src/plugins/data_view_management/public/components/utils.ts b/src/plugins/data_view_management/public/components/utils.ts index 1273a1073fbbf..3024c172ac441 100644 --- a/src/plugins/data_view_management/public/components/utils.ts +++ b/src/plugins/data_view_management/public/components/utils.ts @@ -6,8 +6,12 @@ * Side Public License, v 1. */ -import { IndexPatternsContract } from 'src/plugins/data/public'; -import { IFieldType, IndexPattern, IndexPatternListItem } from 'src/plugins/data/public'; +import { + DataViewsContract, + DataView, + DataViewField, + DataViewListItem, +} from 'src/plugins/data_views/public'; import { i18n } from '@kbn/i18n'; const defaultIndexPatternListName = i18n.translate( @@ -30,7 +34,7 @@ const isRollup = (indexPatternType: string = '') => { export async function getIndexPatterns( defaultIndex: string, - indexPatternsService: IndexPatternsContract + indexPatternsService: DataViewsContract ) { const existingIndexPatterns = await indexPatternsService.getIdsWithTitle(true); const indexPatternsListItems = existingIndexPatterns.map((idxPattern) => { @@ -63,7 +67,7 @@ export async function getIndexPatterns( ); } -export const getTags = (indexPattern: IndexPatternListItem | IndexPattern, isDefault: boolean) => { +export const getTags = (indexPattern: DataViewListItem | DataView, isDefault: boolean) => { const tags = []; if (isDefault) { tags.push({ @@ -80,14 +84,11 @@ export const getTags = (indexPattern: IndexPatternListItem | IndexPattern, isDef return tags; }; -export const areScriptedFieldsEnabled = (indexPattern: IndexPatternListItem | IndexPattern) => { +export const areScriptedFieldsEnabled = (indexPattern: DataViewListItem | DataView) => { return !isRollup(indexPattern.type); }; -export const getFieldInfo = ( - indexPattern: IndexPatternListItem | IndexPattern, - field: IFieldType -) => { +export const getFieldInfo = (indexPattern: DataViewListItem | DataView, field: DataViewField) => { if (!isRollup(indexPattern.type)) { return []; } diff --git a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx index 4bc0a204f68a1..4f8e98d382d37 100644 --- a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx @@ -40,7 +40,7 @@ export async function mountManagementSection( ) { const [ { chrome, application, uiSettings, notifications, overlays, http, docLinks, theme }, - { data, dataViewFieldEditor, dataViewEditor }, + { data, dataViewFieldEditor, dataViewEditor, dataViews, fieldFormats }, indexPatternManagementStart, ] = await getStartServices(); const canSave = Boolean(application.capabilities.indexPatterns.save); @@ -59,10 +59,12 @@ export async function mountManagementSection( docLinks, data, dataViewFieldEditor, + dataViews, indexPatternManagementStart: indexPatternManagementStart as IndexPatternManagementStart, setBreadcrumbs: params.setBreadcrumbs, fieldFormatEditors: dataViewFieldEditor.fieldFormatEditors, IndexPatternEditor: dataViewEditor.IndexPatternEditorComponent, + fieldFormats, }; ReactDOM.render( diff --git a/src/plugins/data_view_management/public/mocks.ts b/src/plugins/data_view_management/public/mocks.ts index 513de8d7f4404..3404ca4912c88 100644 --- a/src/plugins/data_view_management/public/mocks.ts +++ b/src/plugins/data_view_management/public/mocks.ts @@ -19,6 +19,7 @@ import { IndexPatternManagementPlugin, } from './plugin'; import { IndexPatternManagmentContext } from './types'; +import { fieldFormatsServiceMock } from '../../field_formats/public/mocks'; const createSetupContract = (): IndexPatternManagementSetup => ({}); @@ -57,6 +58,7 @@ const createIndexPatternManagmentContext = (): { const { http } = coreMock.createSetup(); const data = dataPluginMock.createStartContract(); const dataViewFieldEditor = indexPatternFieldEditorPluginMock.createStartContract(); + const dataViews = data.indexPatterns; return { chrome, @@ -67,12 +69,14 @@ const createIndexPatternManagmentContext = (): { http, docLinks, data, + dataViews, dataViewFieldEditor, indexPatternManagementStart: createStartContract(), setBreadcrumbs: () => {}, fieldFormatEditors: dataViewFieldEditor.fieldFormatEditors, IndexPatternEditor: indexPatternEditorPluginMock.createStartContract().IndexPatternEditorComponent, + fieldFormats: fieldFormatsServiceMock.createStartContract(), }; }; diff --git a/src/plugins/data_view_management/public/plugin.ts b/src/plugins/data_view_management/public/plugin.ts index 742a623dcb084..a0c25479ce3e2 100644 --- a/src/plugins/data_view_management/public/plugin.ts +++ b/src/plugins/data_view_management/public/plugin.ts @@ -9,11 +9,13 @@ import { i18n } from '@kbn/i18n'; import { PluginInitializerContext, CoreSetup, Plugin } from 'src/core/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { FieldFormatsStart } from 'src/plugins/field_formats/public'; import { UrlForwardingSetup } from '../../url_forwarding/public'; import { ManagementSetup } from '../../management/public'; import { IndexPatternFieldEditorStart } from '../../data_view_field_editor/public'; import { DataViewEditorStart } from '../../data_view_editor/public'; +import { DataViewsPublicPluginStart } from '../../data_views/public'; export interface IndexPatternManagementSetupDependencies { management: ManagementSetup; @@ -24,6 +26,8 @@ export interface IndexPatternManagementStartDependencies { data: DataPublicPluginStart; dataViewFieldEditor: IndexPatternFieldEditorStart; dataViewEditor: DataViewEditorStart; + dataViews: DataViewsPublicPluginStart; + fieldFormats: FieldFormatsStart; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/src/plugins/data_view_management/public/types.ts b/src/plugins/data_view_management/public/types.ts index 9cf1976937ac3..dc5e0198a64f1 100644 --- a/src/plugins/data_view_management/public/types.ts +++ b/src/plugins/data_view_management/public/types.ts @@ -21,6 +21,8 @@ import { IndexPatternManagementStart } from './index'; import { KibanaReactContextValue } from '../../kibana_react/public'; import { IndexPatternFieldEditorStart } from '../../data_view_field_editor/public'; import { DataViewEditorStart } from '../../data_view_editor/public'; +import { DataViewsPublicPluginStart } from '../../data_views/public'; +import { FieldFormatsStart } from '../../field_formats/public'; export interface IndexPatternManagmentContext { chrome: ChromeStart; @@ -31,11 +33,13 @@ export interface IndexPatternManagmentContext { http: HttpSetup; docLinks: DocLinksStart; data: DataPublicPluginStart; + dataViews: DataViewsPublicPluginStart; dataViewFieldEditor: IndexPatternFieldEditorStart; indexPatternManagementStart: IndexPatternManagementStart; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; fieldFormatEditors: IndexPatternFieldEditorStart['fieldFormatEditors']; IndexPatternEditor: DataViewEditorStart['IndexPatternEditorComponent']; + fieldFormats: FieldFormatsStart; } export type IndexPatternManagmentContextValue = diff --git a/src/plugins/data_views/public/index.ts b/src/plugins/data_views/public/index.ts index 8e42e4c8b6b0f..b15e6da7d940b 100644 --- a/src/plugins/data_views/public/index.ts +++ b/src/plugins/data_views/public/index.ts @@ -16,9 +16,10 @@ export { export { onRedirectNoIndexPattern } from './data_views'; export type { IIndexPatternFieldList, TypeMeta } from '../common'; -export { IndexPatternField, DataViewField } from '../common'; +export { IndexPatternField, DataViewField, DataViewType, META_FIELDS } from '../common'; export type { IndexPatternsContract, DataViewsContract } from './data_views'; +export type { DataViewListItem } from './data_views'; export { IndexPatternsService, IndexPattern, @@ -42,4 +43,4 @@ export function plugin() { export type { DataViewsPublicPluginSetup, DataViewsPublicPluginStart } from './types'; // Export plugin after all other imports -export type { DataViewsPublicPlugin as DataPlugin }; +export type { DataViewsPublicPlugin as DataViewsPlugin }; diff --git a/src/plugins/data_views/public/mocks.ts b/src/plugins/data_views/public/mocks.ts new file mode 100644 index 0000000000000..c9aece61c4e02 --- /dev/null +++ b/src/plugins/data_views/public/mocks.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DataViewsPlugin, DataViewsContract } from '.'; + +export type Setup = jest.Mocked>; +export type Start = jest.Mocked>; + +const createSetupContract = (): Setup => ({}); + +const createStartContract = (): Start => { + return { + find: jest.fn((search) => [{ id: search, title: search }]), + createField: jest.fn(() => {}), + createFieldList: jest.fn(() => []), + ensureDefaultIndexPattern: jest.fn(), + ensureDefaultDataView: jest.fn().mockReturnValue(Promise.resolve({})), + make: () => ({ + fieldsFetcher: { + fetchForWildcard: jest.fn(), + }, + }), + get: jest.fn().mockReturnValue(Promise.resolve({})), + clearCache: jest.fn(), + } as unknown as jest.Mocked; +}; + +export const dataViewPluginMocks = { + createSetupContract, + createStartContract, +}; diff --git a/src/plugins/discover/public/application/context/context_app.tsx b/src/plugins/discover/public/application/context/context_app.tsx index a1a7cd0c0206e..8faabdbd0682d 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/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index 63343eb4a1ae0..d2b767ce5bcd8 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -121,7 +121,7 @@ function DiscoverDocumentsComponent({ } return ( - +

diff --git a/src/plugins/discover/public/components/doc_table/doc_table_infinite.tsx b/src/plugins/discover/public/components/doc_table/doc_table_infinite.tsx index c43e65f405339..c3d86c646d407 100644 --- a/src/plugins/discover/public/components/doc_table/doc_table_infinite.tsx +++ b/src/plugins/discover/public/components/doc_table/doc_table_infinite.tsx @@ -118,7 +118,7 @@ export const DocTableInfinite = (props: DocTableProps) => { const onBackToTop = useCallback(() => { const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0; - const focusElem = document.querySelector('.dscTable') as HTMLElement; + const focusElem = document.querySelector('.dscSkipButton') as HTMLElement; focusElem.focus(); // Only the desktop one needs to target a specific container 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 1d4a8614b2921..2714dbd2265a4 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 7e314788b03fd..be985c2720f8b 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 01b54d13f8a76..86516344031a0 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 dad69f9433a23..915beceb988fd 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 0000000000000..832c520b9ade8 --- /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 0000000000000..4a7d7e9813dcc --- /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 4d203f791da97..b039243b0cc25 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 17d35c6cb4b7e..21c77ec51e5ef 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 b0e8e6f102f1e..52f725da4906b 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 6bab00895440a..36b5534d9f4af 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 0000000000000..c8bdf0cc692b8 --- /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 0000000000000..22079cbcafdb3 --- /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 0000000000000..8e9315719bc16 --- /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/kibana_utils/common/persistable_state/types.ts b/src/plugins/kibana_utils/common/persistable_state/types.ts index f5aa73e9fe205..db757ac662686 100644 --- a/src/plugins/kibana_utils/common/persistable_state/types.ts +++ b/src/plugins/kibana_utils/common/persistable_state/types.ts @@ -92,8 +92,8 @@ export interface PersistableState

}; export type MigrateFunction< - FromVersion extends SerializableRecord = SerializableRecord, - ToVersion extends SerializableRecord = SerializableRecord + FromVersion extends Serializable = SerializableRecord, + ToVersion extends Serializable = SerializableRecord > = (state: FromVersion) => ToVersion; /** diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.tsx index 15edd85d6754a..fa678dbdaae89 100644 --- a/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.tsx +++ b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.tsx @@ -57,9 +57,15 @@ export const SolutionToolbar = ({ isDarkModeEnabled, children }: Props) => { gutterSize="s" > {primaryActionButton} - {quickButtonGroup ? {quickButtonGroup} : null} - {extra} - {addFromLibraryButton ? {addFromLibraryButton} : null} + + + {quickButtonGroup ? {quickButtonGroup} : null} + {extra} + {addFromLibraryButton ? ( + {addFromLibraryButton} + ) : null} + + ); }; 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 0000000000000..29702c3356865 --- /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 0000000000000..de30b180607fe --- /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 ddc2e5845b037..49ed5ef8aaf8d 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 06590bcfbb3d0..3271dc52fd9d0 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 0000000000000..dd5937149850c --- /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 e8f85666bcac4..8369815a042af 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 fd8a5fd7541a6..684d29caeb312 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 cb2ba498a0664..063f60b82927e 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 136544ac068a3..edb250dfe1200 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 3c9fb1b1b268f..6b799d3e34946 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 0e4322a7ba82c..0976d8f9cf47e 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 165f5080af93a..0cc96ba3c74da 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 0ceb2daa831be..6c9cc942b362f 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 b84aee949471b..6ab50858fe98b 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 b52a3f3a6040e..9c328a175c10b 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 59d7c25c6f41d..eae4f704b7c3c 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 e138acf2e9e85..7fe571b25f98c 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 661717f99ed88..c6e8f9efdd035 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 1a324ef844e2e..6636a490118b4 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 0000000000000..83ac63afd915f --- /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/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index f6f60d4fd6393..794204b923b72 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -120,14 +120,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.backToTop(); }); - it('should go the end of the table when using the accessible Skip button', async function () { + it('should go the end and back to top of the classic table when using the accessible buttons', async function () { // click the Skip to the end of the table await PageObjects.discover.skipToEndOfDocTable(); // now check the footer text content const footer = await PageObjects.discover.getDocTableFooter(); - log.debug(await footer.getVisibleText()); expect(await footer.getVisibleText()).to.have.string(rowsHardLimit); await PageObjects.discover.backToTop(); + // check that the skip to end of the table button now has focus + const skipButton = await testSubjects.find('discoverSkipTableButton'); + const activeElement = await find.activeElement(); + const activeElementText = await activeElement.getVisibleText(); + const skipButtonText = await skipButton.getVisibleText(); + expect(skipButtonText === activeElementText).to.be(true); }); describe('expand a document row', function () { diff --git a/test/functional/apps/discover/_runtime_fields_editor.ts b/test/functional/apps/discover/_runtime_fields_editor.ts index 4757807cb7ac1..2e21b2e1f8ec6 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 13658215e9e59..1241b0e892e9c 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 521781c5a6d2b..5fb0c4d6ad1dc 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/functional/services/remote/network_profiles.ts b/test/functional/services/remote/network_profiles.ts index c27bafa4f8dcb..cb4076686270c 100644 --- a/test/functional/services/remote/network_profiles.ts +++ b/test/functional/services/remote/network_profiles.ts @@ -12,23 +12,11 @@ interface NetworkOptions { LATENCY: number; } -const sec = 1_000; -const kB = 1024; +const sec = 10 ** 3; +const MBps = 10 ** 6 / 8; // megabyte per second (MB/s) (can be abbreviated as MBps) -// Download (kb/s) Upload (kb/s) Latency (ms) -// https://gist.github.com/theodorosploumis/fd4086ee58369b68aea6b0782dc96a2e +// Selenium uses B/s (bytes) for network throttling +// Download (B/s) Upload (B/s) Latency (ms) export const NETWORK_PROFILES: { [key: string]: NetworkOptions } = { - DEFAULT: { DOWNLOAD: 5 * kB * sec, UPLOAD: 1 * kB * sec, LATENCY: 0.1 * sec }, - GPRS: { DOWNLOAD: 0.05 * kB * sec, UPLOAD: 0.02 * kB * sec, LATENCY: 0.5 * sec }, - MOBILE_EDGE: { DOWNLOAD: 0.24 * kB * sec, UPLOAD: 0.2 * kB * sec, LATENCY: 0.84 * sec }, - '2G_REGULAR': { DOWNLOAD: 0.25 * kB * sec, UPLOAD: 0.05 * kB * sec, LATENCY: 0.3 * sec }, - '2G_GOOD': { DOWNLOAD: 0.45 * kB * sec, UPLOAD: 0.15 * kB * sec, LATENCY: 0.15 * sec }, - '3G_SLOW': { DOWNLOAD: 0.78 * kB * sec, UPLOAD: 0.33 * kB * sec, LATENCY: 0.2 * sec }, - '3G_REGULAR': { DOWNLOAD: 0.75 * kB * sec, UPLOAD: 0.25 * kB * sec, LATENCY: 0.1 * sec }, - '3G_GOOD': { DOWNLOAD: 1.5 * kB * sec, UPLOAD: 0.75 * kB * sec, LATENCY: 0.04 * sec }, - '4G_REGULAR': { DOWNLOAD: 4 * kB * sec, UPLOAD: 3 * kB * sec, LATENCY: 0.02 * sec }, - DSL: { DOWNLOAD: 2 * kB * sec, UPLOAD: 1 * kB * sec, LATENCY: 0.005 * sec }, - CABLE_5MBPS: { DOWNLOAD: 5 * kB * sec, UPLOAD: 1 * kB * sec, LATENCY: 0.28 * sec }, - CABLE_8MBPS: { DOWNLOAD: 8 * kB * sec, UPLOAD: 2 * kB * sec, LATENCY: 0.1 * sec }, - WIFI: { DOWNLOAD: 30 * kB * sec, UPLOAD: 15 * kB * sec, LATENCY: 0.002 * sec }, + CLOUD_USER: { DOWNLOAD: 6 * MBps, UPLOAD: 6 * MBps, LATENCY: 0.1 * sec }, }; diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index 84dfdf3c845f7..d4e20a617a602 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -39,6 +39,7 @@ const headlessBrowser: string = process.env.TEST_BROWSER_HEADLESS as string; const browserBinaryPath: string = process.env.TEST_BROWSER_BINARY_PATH as string; const remoteDebug: string = process.env.TEST_REMOTE_DEBUG as string; const certValidation: string = process.env.NODE_TLS_REJECT_UNAUTHORIZED as string; +const noCache: string = process.env.TEST_DISABLE_CACHE as string; const SECOND = 1000; const MINUTE = 60 * SECOND; const NO_QUEUE_COMMANDS = ['getLog', 'getStatus', 'newSession', 'quit']; @@ -118,6 +119,11 @@ function initChromiumOptions(browserType: Browsers, acceptInsecureCerts: boolean options.setChromeBinaryPath(browserBinaryPath); } + if (noCache === '1') { + options.addArguments('disk-cache-size', '0'); + options.addArguments('disk-cache-dir', '/dev/null'); + } + const prefs = new logging.Preferences(); prefs.setLevel(logging.Type.BROWSER, logging.Level.ALL); options.setUserPreferences(chromiumUserPrefs); @@ -291,12 +297,12 @@ async function attemptToCreateCommand( const { session, consoleLog$ } = await buildDriverInstance(); if (throttleOption === '1' && browserType === 'chrome') { - const { KBN_NETWORK_TEST_PROFILE = 'DEFAULT' } = process.env; + const { KBN_NETWORK_TEST_PROFILE = 'CLOUD_USER' } = process.env; const profile = KBN_NETWORK_TEST_PROFILE in Object.keys(NETWORK_PROFILES) ? KBN_NETWORK_TEST_PROFILE - : 'DEFAULT'; + : 'CLOUD_USER'; const { DOWNLOAD: downloadThroughput, @@ -306,9 +312,16 @@ async function attemptToCreateCommand( // Only chrome supports this option. log.debug( - `NETWORK THROTTLED with profile ${profile}: ${downloadThroughput}kbps down, ${uploadThroughput}kbps up, ${latency} ms latency.` + `NETWORK THROTTLED with profile ${profile}: ${downloadThroughput} B/s down, ${uploadThroughput} B/s up, ${latency} ms latency.` ); + if (noCache) { + // @ts-expect-error + await session.sendDevToolsCommand('Network.setCacheDisabled', { + cacheDisabled: true, + }); + } + // @ts-expect-error session.setNetworkConditions({ offline: false, 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 8e7adb504ebee..439ece04e615f 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 f1b683f2430f7..510d9469c7878 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 58ec363223270..e438b6946b8b6 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 562af3d01ef77..d5c1d6c630635 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 12170ac20b7df..396e853ca54a7 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 cfb1a5c354c2d..dde7cfe5399d3 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 45c8ddaf2f4df..b45a513bf9d64 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 d71751805ce41..50f4e34a1c6b4 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 cee38e4186454..ccc483182d772 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 a0ca7daf82610..eaf7bb711e54e 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 4804e52b16d4f..15efd28756b0b 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 ccd409f1798a5..e1ced3fabcd77 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 1faab4092361d..15b2d393a97a0 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 6145e9f9ca7da..1df6dc0332b2a 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 b6f344751b59b..5ce98f8b10884 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 1847ea90bd7fa..375a4b5ac1156 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 62b4b242b1b68..6017d16ba2dad 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 eee8ca66dd08f..8741cd7667698 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 d9540115d0d70..4c50b47a4b9a6 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 c642ca7bd577f..2d09abc15f854 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 0656ab045efc2..5bf7e30a3df5d 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 f79e955595717..5b37a14b4e4e5 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 929cc4f7f4cd3..d68c11f981a0f 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 90d976c389c58..c2c5a63a2ab03 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 77d54e0dad698..85ad5c5336c8c 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 5438fce7c4881..af7283af8c526 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 e252eba15ade1..65681a398d8e6 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 0000000000000..c1360ba3b13ad --- /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 51349ff22a962..8b34ad8980774 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 e5ee427e16677..059feb0915fdb 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 149e9b8ee763a..89f49a9669b45 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 7c48531d21990..a017d1b5304e3 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 ded242e2ce558..82dac9cc8f016 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 8045e4947dcb0..5c0bc7fdfd462 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 b696a46f59bd1..640ab0b0daad2 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 96ec397f5f94f..9f7ad5d053fa4 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 ee11c301441b6..d3eadeaf016c0 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 655bf93e4fb93..1c54713125c7e 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 d9f9154c5c100..787bcd88266e2 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 6a9dfd1fddd11..d18381cee77af 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 0daf620dc5009..e8e24c121ea6d 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 a334c1e23892a..2510420d3eac4 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 9dd83fd1c8fd1..1d6a65317122c 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 ff79feaa924f3..04127ec702262 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 ecba89b2651ac..9c5494c1ea533 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 4ed011441c81b..d6b454bd948f7 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 9bc18d772a4a1..4e39ad4397b41 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 54c34121ea0cb..bb0427d462d54 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 cac899665d98b..558092db4a458 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 f6891a5d8fb67..b3560f0ebc97b 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 e34270f963599..9ecb2f31112ea 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 8228ab4c6e83e..e7a025985fca7 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 7d05b188e9bbe..a1e70f4f25d21 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 ca29bb4ce2944..3bb1e8bd923f1 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 ab6843f94ee43..cc1223de7b177 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 e1253b41a45f5..2f92e5efedf42 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 4eaf0dccc3225..3e1c64c484edb 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 7a19690a4582e..822cb0f591bce 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 8263db648cd39..da4aad4102195 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 f286f963b4fa0..572661dc8cea4 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 17a380f4d5e35..32dcf3d5d3439 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 142337ef3e160..253f989b389a8 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 be3b28f07cdaf..6d59b15876d20 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 e923795fad95f..56f1ae2014473 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 254d15f025c1b..7c0cbf694aed7 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 163082cf044cd..549ccc8e69259 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 fe91b14e64e8a..2d83f1f46bd38 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 a48fb77b45585..9236d9c21a6c6 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 1ceb90ff838ad..dd728be30c71a 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 6c7a8718dee58..ce9dbe7ce417f 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 04bb578b0c434..0436c27cdd6b7 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 6f3b5c71af810..9e5508a5810df 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 aba1073bfe9c2..fea48dee5d6c0 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 28824b3b8a399..d9658f9d5e047 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 853ea37112ad8..ae543adf2b852 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 e5e460e3b2812..932f2f5d215e7 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 7e7f30065c958..7a537125203ea 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 40283ae192947..e5f3c7bcbee4e 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 8430e620f59ab..1e4d1816bf84a 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 a2f6fd493313f..7d387a39f9334 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 cf8d69410d26b..45c9d30419e85 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 9f7d2977dfa84..5e9f1f8149d9d 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 4f26a5875347c..c35de9d510951 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 1c421032ac7d3..f528ce17c02f0 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 b146afae53907..dfc89f78e4b3b 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 0e01c44b3fb5a..c5001e25b0801 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 6ef7651a1e404..71e4b6a6f1aad 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 62507efc64401..5331ae1ae3d36 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 1012c3dfa6fd1..25baf42572d06 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 25b306723b2d2..6ca1a78db28b1 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 c81dfb6283c94..20e278000266a 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 50ec934961e1a..c64a121c91882 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 457daef851bcf..57bfc2b61fc53 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 33a0069e19ef1..f87967f762f25 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 9e7a32a6808ec..3067b335f4861 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 6468e6ed1e2c8..5f1e0cacd8483 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 33f1de91b61cc..7f8fbf62130b3 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 c9e6e08ac759f..056a2847c68bf 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 895b83136a097..312412a8cf827 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 aaa9b3e45ee22..30d4b0049d1bd 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 e1d6fb65bbc56..39d522ca088fc 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 a1b24fc516664..ede47a65d7ea5 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 5750146bef722..c7b98743c6bea 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 d8a996d2163bc..713292c633891 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 710b4b6f71a35..fcc69c5055bad 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 0b97301b4e7c5..962fbb4eb6be6 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 fc87b24579695..43a865c0584c9 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 e1bda5475acc4..40335f0cab61d 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 655fc2da2b097..158705970640c 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 9dccddd509387..d203bd8cfe022 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 30bf5908ef376..0e9ad84864863 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 0b4eca8b9373c..d654cb9c0f5d3 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 6c7cb7a067d2e..4a56f1a28e4be 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 0b7e405ae5d95..e421190d6793c 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 947c7a93f38b1..9e97129b199db 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 cef97c46fd2e2..d3b378ab5cb0b 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 a1e3f42cb5684..3f27ddbf78af4 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 f2c8f9e3b318d..698801b5a8d37 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 88318eb0e0c2d..e4e26ce637bfe 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 cf942ebb75776..4dddb4bee226d 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 2dcc969f661b8..6f07efeed7c46 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 76e85b1d9998d..0ae6fc00b2804 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 12cc137d62142..c792c4ae9b426 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 8791075158c95..7c2bc722ac1e6 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 4344ed3a80f64..a7a8c90c31c16 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 e9525728bc3c5..8b5595a67fa78 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 20484e691d50b..a04d3218f9fff 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 f8f51679a5192..d06bfcceab984 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 386eb7e1e0d7d..e6f482e88afc0 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 1d4dd04ce9df4..805e162c67fe6 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 c422b78d661e4..ea10852c7d668 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 c45ab22682ef4..366003c101789 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 75fd8ba1a5f11..4bf744e29c1ee 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 ccd9bdff960b6..b517a39c1004d 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 66fdb7913b5d5..5337f1a2f16b9 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 c3d418b63426b..9d9a013d1c439 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 0a51cb4cacd0b..c4a589dca3ad8 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 2c4dec9f4bcba..84eff7eb444bd 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 aad5756b70e7e..b8f0a0c53cb7d 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 b76c468ae2153..e13a38143ef25 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 ab3b085e4e255..01e23cba48e04 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 982d25802e202..b33f22cd9faf9 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 92682a88efb17..9353de8162b08 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 9df035515b040..af299c6095e59 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 d3e40aebc1daf..bd0ac78b855f0 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/DiscoverErrorButton.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/DiscoverErrorButton.test.tsx rename to x-pack/plugins/apm/public/components/shared/links/discover_links/discover_error_button.test.tsx index 3d70653d58d19..3cbe8d7e31632 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_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/DiscoverErrorLink.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/DiscoverErrorLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/links/discover_links/discover_error_link.test.tsx index 3d70653d58d19..3cbe8d7e31632 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_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 467a6d604ee60..c7d35e7c983e8 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 53ea5735c8b22..4e47a1d4217de 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 631ac6e2129f0..f943e6fb49985 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 b8278f3d285f4..5505036087dbf 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 a73f569827aa5..498a7fd26c138 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 21e7a1e45263f..d776572cc9e32 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 c5a8c3299daa5..8301bd844b5cb 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 44e33e6bf419d..9a8f2bd98106b 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 72a29a079bc67..541b00f8291b0 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 edea1f916eb18..8f7ff4f11ab22 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 cbafbd3fde2fa..ddcd5503853fe 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 2964a8e2578c7..ef92691336c34 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 03ae13c06c613..16ab8cb1d9202 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 5e2c180bcfd55..ab6c132c61e13 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 ed816b1c7a337..1d14d04a2be65 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 03ae237f470c3..4093d8128c7ca 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 926be5ef93cf7..9fa8bb5f04960 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 db30e73c86dc7..d91af5b11c49b 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 1a6e9a803d735..f1d82e2e307e1 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 a43cd26e7f94a..3d92f3c584f45 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 3395b22988e8c..de24482c06d50 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 e0710556096c9..9993fd27b6172 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 d10d15f8240a1..51e574324dfa6 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 41983a9185b8f..4ef9634fe0279 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 5c2b8383ee3b6..a1ce5e99333d2 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 dc1a62e591b17..399121b710ce9 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 8884020f0364d..9d68c5912f513 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 e20a6df12ad46..d3c3369f6a72a 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 2cb4e0964686f..e61ffbbbc5bab 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 ecef563e72948..300b0ec345f42 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 0818c8915d622..935939761a496 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 187f82963ad60..7e377f2a756ee 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 3095c44d54e78..daf5cb0833b61 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 fa2d3700eaf8c..be91fb8adfe6c 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 84f3b1e45d4a1..292aaaed816af 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 c44fbb8b7f87a..b6e02b1b08c3c 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 d2abd9dc4c15a..f943cf4da4b05 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 c37d83983a00b..4311a9c75de85 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 0446b35872045..a0b06c241a28c 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 cc187c6cf619a..d289c29b60b37 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 77c52e1afeec3..2d00080201709 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 e70d9aecf9a9c..9015a28822a75 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 38dfd2261da00..3a91039d81c7a 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 c3f07ae601db9..b1b657bb37ff5 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 d4bf6ef6f569b..c6db5ff4e3309 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 c15ca14572606..9c01259c6d9e8 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 f4aabba4ca216..ef278fbea3411 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 d1d723a176b45..1675c2c78cdcb 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 84a3aee434141..cdcb9bb584b5d 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 6d8edd21c7e73..7bffff4fe95cd 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 76176f6ba2133..aa18e235f5fd9 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 3f01a8ccb3e73..3c5a4c16565c6 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 2094af748ab16..4739e6ca16474 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 3e879b7fb58db..c0ebfa60708d4 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 e07a848263f50..85f853cea759b 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 6c61ab24d13b2..100f5c65eb94a 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 15fec0d3b6390..1d9ffde49ff8b 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 783b17e7d9362..6a064ffd297ec 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 b2b4ea4a942a3..b78e0d1d5cf24 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 710f595ba7179..417fe09fbc586 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 b3543d532b9be..698468ab2e150 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 de573166c8e9a..a90f79aa995c5 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 04ee9c8cb7db2..89ffc18766bcd 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 7bbf3874f175f..b3a977c1d795a 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 c0ed25849ac97..66553b6fda6c0 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 e4a6a102844a9..b61e03319b916 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 0000000000000..75bd97421e58e --- /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 0000000000000..8b46e818209f3 --- /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 ae3778366651c..388db9e6e5960 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 643d7cdedc50d..38d1d502704e2 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 b831c9aa70e49..a31021cba4c10 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 372bcbb5642cb..5e4ea42990e47 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 e81ca2cc1f057..f7e9d333f8683 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 0000000000000..5c48fbd10862a --- /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 0000000000000..08d9391f81c13 --- /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 57fdc7d7309ce..c98d2f080452a 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 6a61ec595acb7..fa938f2c07c74 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 08caded52aa26..f43e2dd3b4606 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 89faef29a3b02..1ca674bfb6f9d 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 6048ac360386c..31db3366ce3b5 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 a8409270752ad..785f183b193f1 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 eb87f4720deec..3290bc8227a29 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 0000000000000..8c8d2e26d8a22 --- /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 caec30e083d40..045ff8b52e259 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 85b195214d44b..21bcc89304b3c 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 610e6e56af350..20ec56706480d 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 9d37873bcae0a..62d070dbf00f5 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 2634d76297b58..a168020b6eef8 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 3734b1bf53051..3536bed0f92b3 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 bf19bd6ecf4b8..9aef71f33f609 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 6e9db1757ccc7..2554ae11220eb 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 77c31b11924c0..0000000000000 --- 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 8cdc695ebaaba..1c2ce763f42e2 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 01bb0adb17711..456a1314bdfff 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 584194be65f50..1ced3d15f6e10 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 ed55f919e4c76..4bf025c274859 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 780de5309d97e..ea329b63863f8 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 0000000000000..872b6759b389b --- /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 91767947bc0a6..c1ceb531657d0 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 9f69d5096237c..c0ef1097555a6 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 6c77bdb1adeac..5dd40997900c6 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 6660c1c6efb35..405f2ebe0ba91 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 0000000000000..972dbfd6dc0e4 --- /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 2216013a29c12..d90b1a3c92201 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 c10244038750d..6268fa128df0f 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 8e77ab3f321ef..233b1a70ff7f6 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 bcc02c3cbc2cd..72186abd38c94 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 40e8425c98ff0..a4a38d50388d5 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 ac94ccc562e88..557a6b8acc4e7 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 216cdc0970dc4..13e4e34b20b66 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 ebe43ba76a46a..27b6186216b69 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 0000000000000..35d362f43becc --- /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 8483642e59c5a..b97d58ee232f1 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 9bde26298185b..473b46d470265 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 d62642f5619ea..82305b2fdd95f 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 dfc27c4b6fa66..9e7cd8644c7c7 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 0000000000000..20eba14c5cff0 --- /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 0000000000000..88913b50c3c4c --- /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 c9c36fd7b26a9..f1dcbd5fe9e7c 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/test/performance/tests/index.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/types.ts similarity index 50% rename from x-pack/test/performance/tests/index.ts rename to x-pack/plugins/canvas/server/saved_objects/migrations/types.ts index d784fa3031739..18ce0bd88cb69 100644 --- a/x-pack/test/performance/tests/index.ts +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/types.ts @@ -4,13 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { ExpressionsService } from 'src/plugins/expressions/public'; -import { FtrProviderContext } from '../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('performance', function () { - this.tags('ciGroup8'); - - loadTestFile(require.resolve('./home')); - }); +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 0000000000000..d4d7e2b429711 --- /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 a8f0f3daf2175..db22025e625e8 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 eff7f45dcadae..a55c7348c62bb 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 a9f09ada989c6..82ad535852c97 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 9727327fcbd79..d7c818b786e32 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 efb121b2948af..09add343aeac4 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 0baf1e086d155..0119c0a842f50 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 2c3931485757d..7c9b785dbb2bb 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 e4e2a0793f7d4..19ad15286db6a 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 65981e6aebd0f..cfa91a9c57cab 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 ebac6295166df..bf3cc0ee320bd 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 0ebff7693eed8..5965ccbcf504e 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 7950f962a9cc1..5897a757b5bdf 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 ac10068b88b3e..9c0b7831893fa 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 3ff8845f1e3a5..f1f4193ad346d 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/description.tsx b/x-pack/plugins/cases/public/components/create/description.tsx index c1545a42df3f5..6c9792684a126 100644 --- a/x-pack/plugins/cases/public/components/create/description.tsx +++ b/x-pack/plugins/cases/public/components/create/description.tsx @@ -9,6 +9,7 @@ import React, { memo, useEffect, useRef } from 'react'; import { MarkdownEditorForm } from '../markdown_editor'; import { UseField, useFormContext, useFormData } from '../../common/shared_imports'; import { useLensDraftComment } from '../markdown_editor/plugins/lens/use_lens_draft_comment'; +import { ID as LensPluginId } from '../markdown_editor/plugins/lens/constants'; interface Props { isLoading: boolean; @@ -22,6 +23,7 @@ const DescriptionComponent: React.FC = ({ isLoading }) => { const { setFieldValue } = useFormContext(); const [{ title, tags }] = useFormData({ watch: ['title', 'tags'] }); const editorRef = useRef>(); + const disabledUiPlugins = [LensPluginId]; useEffect(() => { if (draftComment?.commentId === fieldName && editorRef.current) { @@ -55,6 +57,7 @@ const DescriptionComponent: React.FC = ({ isLoading }) => { isDisabled: isLoading, caseTitle: title, caseTags: tags, + disabledUiPlugins, }} /> ); 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 65fcc479979f1..dd0759ce723ae 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/create/schema.tsx b/x-pack/plugins/cases/public/components/create/schema.tsx index 632256fcbacec..e7d6dbf8e103c 100644 --- a/x-pack/plugins/cases/public/components/create/schema.tsx +++ b/x-pack/plugins/cases/public/components/create/schema.tsx @@ -18,6 +18,8 @@ import * as i18n from './translations'; import { OptionalFieldLabel } from './optional_field_label'; const { emptyField, maxLengthField } = fieldValidators; +const isEmptyString = (value: string) => value.trim() === ''; + export const schemaTags = { type: FIELD_TYPES.COMBO_BOX, label: i18n.TAGS, @@ -25,7 +27,16 @@ export const schemaTags = { labelAppend: OptionalFieldLabel, validations: [ { - validator: emptyField(i18n.TAGS_EMPTY_ERROR), + validator: ({ value }: { value: string | string[] }) => { + if ( + (!Array.isArray(value) && isEmptyString(value)) || + (Array.isArray(value) && value.length > 0 && value.find(isEmptyString)) + ) { + return { + message: i18n.TAGS_EMPTY_ERROR, + }; + } + }, type: VALIDATION_TYPES.ARRAY_ITEM, isBlocking: false, }, diff --git a/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx b/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx index e2067d75e843e..f04444b57de68 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx @@ -15,12 +15,7 @@ import React, { ElementRef, } from 'react'; import { PluggableList } from 'unified'; -import { - EuiMarkdownEditor, - EuiMarkdownEditorProps, - EuiMarkdownAstNode, - EuiMarkdownEditorUiPlugin, -} from '@elastic/eui'; +import { EuiMarkdownEditor, EuiMarkdownEditorProps, EuiMarkdownAstNode } from '@elastic/eui'; import { ContextShape } from '@elastic/eui/src/components/markdown_editor/markdown_context'; import { usePlugins } from './use_plugins'; import { useLensButtonToggle } from './plugins/lens/use_lens_button_toggle'; @@ -33,7 +28,7 @@ interface MarkdownEditorProps { onChange: (content: string) => void; parsingPlugins?: PluggableList; processingPlugins?: PluggableList; - uiPlugins?: EuiMarkdownEditorUiPlugin[] | undefined; + disabledUiPlugins?: string[] | undefined; value: string; } @@ -46,14 +41,15 @@ export interface MarkdownEditorRef { } const MarkdownEditorComponent = forwardRef( - ({ ariaLabel, dataTestSubj, editorId, height, onChange, value }, ref) => { + ({ ariaLabel, dataTestSubj, editorId, height, onChange, value, disabledUiPlugins }, ref) => { const astRef = useRef(undefined); const [markdownErrorMessages, setMarkdownErrorMessages] = useState([]); const onParse: EuiMarkdownEditorProps['onParse'] = useCallback((err, { messages, ast }) => { setMarkdownErrorMessages(err ? [err] : messages); astRef.current = ast; }, []); - const { parsingPlugins, processingPlugins, uiPlugins } = usePlugins(); + + const { parsingPlugins, processingPlugins, uiPlugins } = usePlugins(disabledUiPlugins); const editorRef = useRef(null); useLensButtonToggle({ diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index 1e5186447e8c0..5d84454d038db 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -21,6 +21,7 @@ type MarkdownEditorFormProps = EuiMarkdownEditorProps & { bottomRightContent?: React.ReactNode; caseTitle?: string; caseTags?: string[]; + disabledUiPlugins?: string[]; }; const BottomContentWrapper = styled(EuiFlexGroup)` @@ -31,7 +32,19 @@ const BottomContentWrapper = styled(EuiFlexGroup)` export const MarkdownEditorForm = React.memo( forwardRef( - ({ id, field, dataTestSubj, idAria, bottomRightContent, caseTitle, caseTags }, ref) => { + ( + { + id, + field, + dataTestSubj, + idAria, + bottomRightContent, + caseTitle, + caseTags, + disabledUiPlugins, + }, + ref + ) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); const commentEditorContextValue = useMemo( @@ -62,6 +75,7 @@ export const MarkdownEditorForm = React.memo( editorId={id} onChange={field.setValue} value={field.value} + disabledUiPlugins={disabledUiPlugins} data-test-subj={`${dataTestSubj}-markdown-editor`} /> 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 a0f0d49b211fb..677ef694894ec 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 c57283106c6c8..b0f167628496b 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/markdown_editor/use_plugins.ts b/x-pack/plugins/cases/public/components/markdown_editor/use_plugins.ts index c68baf083c12f..2a262f16e94b2 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/use_plugins.ts +++ b/x-pack/plugins/cases/public/components/markdown_editor/use_plugins.ts @@ -15,8 +15,9 @@ import { useTimelineContext } from '../timeline_context/use_timeline_context'; import { TemporaryProcessingPluginsType } from './types'; import { KibanaServices } from '../../common/lib/kibana'; import * as lensMarkdownPlugin from './plugins/lens'; +import { ID as LensPluginId } from './plugins/lens/constants'; -export const usePlugins = () => { +export const usePlugins = (disabledPlugins?: string[]) => { const kibanaConfig = KibanaServices.getConfig(); const timelinePlugins = useTimelineContext()?.editor_plugins; @@ -35,7 +36,7 @@ export const usePlugins = () => { processingPlugins[1][1].components.timeline = timelinePlugins.processingPluginRenderer; } - if (kibanaConfig?.markdownPlugins?.lens) { + if (kibanaConfig?.markdownPlugins?.lens && !disabledPlugins?.includes(LensPluginId)) { uiPlugins.push(lensMarkdownPlugin.plugin); } @@ -48,5 +49,5 @@ export const usePlugins = () => { parsingPlugins, processingPlugins, }; - }, [kibanaConfig?.markdownPlugins?.lens, timelinePlugins]); + }, [disabledPlugins, kibanaConfig?.markdownPlugins?.lens, timelinePlugins]); }; 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 7f3ae4a490264..0000000000000 --- 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 ccdc0f8ce888e..0000000000000 --- 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 d4270b464773c..0000000000000 --- 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 692fbbb318b22..0000000000000 --- 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 1df780db3bdaa..aeda9196cc58a 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 80b048618bfc4..da43a122f3868 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 3b6c956017120..776fa3325478a 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 685adc8724e87..581ebb7272d34 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 0000000000000..92f1dd6c123da --- /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 0000000000000..4dc189c14c74f --- /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 0000000000000..0341af259e402 --- /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 858b54038286d..948a15eeba88e 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 4236691a16bb2..b4b4b3b75fe7e 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 0000000000000..ca45191dd4cb1 --- /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 0000000000000..79df2aaca9978 --- /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 80f9985ef15c1..64619ad137950 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 2381d31b3ada8..531323e548dd1 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 d6005a8bd521e..cc570b245ec90 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 c16382a96bb98..dd874b029dc9c 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 0000000000000..e48246a375467 --- /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 0000000000000..ffb99757b45ac --- /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 0000000000000..407fface85ceb --- /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 0000000000000..51abe66a27fda --- /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 0000000000000..70c1ecf274736 --- /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 0000000000000..4cdc0f4fb5edb --- /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 c2edfe2739715..74f5205578a1d 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 ab030348595d1..dee1a25c3b79c 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 4e3496a06bb72..d4b093eed12f7 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 0000000000000..d76829add5a63 --- /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 0000000000000..01b0e105ecd96 --- /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 e398c5edad145..dd75ed8c6fcd8 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 04052d1eedea5..673af99ed7772 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 1e7b0dca172ca..67e9b4505ae62 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 0000000000000..084d1c548f903 --- /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 0695f9d5a2c44..19f60d7cb8c72 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 0000000000000..f63ce9b3fce88 --- /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 0000000000000..7000f407f97e5 --- /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 acd3814786a34..cd207c635e9d4 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 999a3380f5797..167a7fb977929 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 0000000000000..219a7a6d2c7c8 --- /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 0000000000000..e02bde992b651 --- /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 0000000000000..037a2e1756419 --- /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 0000000000000..6300bee347c4b --- /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 0000000000000..058d2232e666d --- /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 0000000000000..d5553a3f6f13d --- /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 f2e5d9793f3a8..d380f246566e9 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 45ad44932831d..98e25fe265dbb 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 0000000000000..c8d063e9a5343 --- /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 0000000000000..203b0e9882f64 --- /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 0000000000000..80657cc90cba9 --- /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 0000000000000..574109b438167 --- /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 0000000000000..b9943a8960392 --- /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 f664da71fc1f6..f8bfd7a54005d 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 7fb4dab915c2d..fb69ca6f22793 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 069c883b99392..303600fdb3398 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/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index 841831b70eac5..f13139605afda 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -6,7 +6,7 @@ */ import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; -import { lensEmbeddableFactory } from '../../../lens/server/embeddable/lens_embeddable_factory'; +import { makeLensEmbeddableFactory } from '../../../lens/server/embeddable/make_lens_embeddable_factory'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { AssociationType, @@ -879,7 +879,7 @@ describe('common utils', () => { ].join('\n\n'); const extractedReferences = extractLensReferencesFromCommentString( - lensEmbeddableFactory, + makeLensEmbeddableFactory({}), commentString ); @@ -977,12 +977,16 @@ describe('common utils', () => { )}},"editMode":false}}`, ].join('\n\n'); - const updatedReferences = getOrUpdateLensReferences(lensEmbeddableFactory, newCommentString, { - references: currentCommentReferences, - attributes: { - comment: currentCommentString, - }, - } as SavedObject); + const updatedReferences = getOrUpdateLensReferences( + makeLensEmbeddableFactory({}), + newCommentString, + { + references: currentCommentReferences, + attributes: { + comment: currentCommentString, + }, + } as SavedObject + ); const expectedReferences = [ ...nonLensCurrentCommentReferences, diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/comments.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/comments.test.ts index 385c1c5945a11..1983ae0db51d3 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/comments.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/comments.test.ts @@ -17,7 +17,7 @@ import { } from '../../../common/utils/markdown_plugins/utils'; import { savedObjectsServiceMock } from '../../../../../../src/core/server/mocks'; -import { lensEmbeddableFactory } from '../../../../lens/server/embeddable/lens_embeddable_factory'; +import { makeLensEmbeddableFactory } from '../../../../lens/server/embeddable/make_lens_embeddable_factory'; import { LensDocShape715 } from '../../../../lens/server'; import { SavedObjectReference, @@ -32,7 +32,7 @@ import { SerializableRecord } from '@kbn/utility-types'; describe('comments migrations', () => { const migrations = createCommentsMigrations({ - lensEmbeddableFactory, + lensEmbeddableFactory: makeLensEmbeddableFactory({}), }); const contextMock = savedObjectsServiceMock.createMigrationContext(); 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 4d08994ceb045..5dbcb1cfb9990 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 084dcd139f9bf..21fcac7ea6c75 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 f2d14de9812ca..303d54c9d45cc 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 0000000000000..d6b0bb3322c03 --- /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 58a4bd4520829..bcf32a7f62bd7 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 0000000000000..947b87e9976d5 --- /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 0000000000000..43c5d49d1986f --- /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 58d0ac021ff22..83fcc104fbe4b 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/enterprise_search/public/applications/app_search/components/curations/curation/results/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/utils.ts index b5a5bf1b5a90a..199a6b2ff5040 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Result } from '../../../result/types'; +import { Result, ResultMeta } from '../../../result/types'; import { CurationResult } from '../../types'; /** @@ -19,19 +19,32 @@ import { CurationResult } from '../../types'; * remove this file when that happens */ +const mergeMetas = (partialMeta: ResultMeta, secondPartialMeta: ResultMeta): ResultMeta => { + return { + ...(partialMeta || {}), + ...secondPartialMeta, + }; +}; + export const convertToResultFormat = (document: CurationResult): Result => { const result = {} as Result; // Convert `key: 'value'` into `key: { raw: 'value' }` Object.entries(document).forEach(([key, value]) => { - result[key] = { - raw: value, - snippet: null, // Don't try to provide a snippet, we can't really guesstimate it - }; + // don't convert _meta if exists + if (key === '_meta') { + result[key] = value as ResultMeta; + } else { + result[key] = { + raw: value, + snippet: null, // Don't try to provide a snippet, we can't really guesstimate it + }; + } }); // Add the _meta obj needed by Result - result._meta = convertIdToMeta(document.id); + const convertedMetaObj = convertIdToMeta(document.id); + result._meta = mergeMetas(result._meta, convertedMetaObj); return result; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/types.ts index b67664d8efde2..b69350d0fac2f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/types.ts @@ -6,7 +6,7 @@ */ import { Meta } from '../../../../../common/types'; -import { Result } from '../result/types'; +import { Result, ResultMeta } from '../result/types'; export interface CurationSuggestion { query: string; @@ -44,5 +44,6 @@ export interface CurationsAPIResponse { export interface CurationResult { // TODO: Consider updating our internal API to return more standard Result data in the future id: string; - [key: string]: string | string[]; + _meta?: ResultMeta; + [key: string]: string | string[] | ResultMeta | undefined; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.test.tsx index 77324566e5a1d..f1f176b9a86b0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.test.tsx @@ -51,6 +51,7 @@ describe('CurationResultPanel', () => { resultPosition: 1, isMetaEngine: true, schemaForTypeHighlights: values.engine.schema, + showClick: true, }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.tsx index b8a659118e736..de3160b1d0e2f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.tsx @@ -79,6 +79,7 @@ export const CurationResultPanel: React.FC = ({ variant, results }) => { isMetaEngine={isMetaEngine} schemaForTypeHighlights={engine.schema} resultPosition={index + 1} + showClick />
)) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx index ea73296d66b30..c1b1f66a01aed 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx @@ -33,6 +33,7 @@ interface Props { schemaForTypeHighlights?: Schema; actions?: ResultAction[]; dragHandleProps?: DraggableProvidedDragHandleProps; + showClick?: boolean; } const RESULT_CUTOFF = 5; @@ -46,6 +47,7 @@ export const Result: React.FC = ({ actions = [], dragHandleProps, resultPosition, + showClick = false, }) => { const [isOpen, setIsOpen] = useState(false); @@ -57,7 +59,6 @@ export const Result: React.FC = ({ [result] ); const numResults = resultFields.length; - const typeForField = (fieldName: string) => { if (schemaForTypeHighlights) return schemaForTypeHighlights[fieldName]; }; @@ -103,6 +104,7 @@ export const Result: React.FC = ({ documentLink={documentLink} actions={actions} resultPosition={resultPosition} + showClick={showClick} /> {resultFields .slice(0, isOpen ? resultFields.length : RESULT_CUTOFF) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx index f51087844e888..af1a537a79e16 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx @@ -24,6 +24,7 @@ describe('ResultHeader', () => { }; const props = { showScore: false, + showClick: false, isMetaEngine: false, resultMeta, actions: [], @@ -69,6 +70,18 @@ describe('ResultHeader', () => { }); }); + describe('clicks', () => { + it('renders clicks if showClick is true', () => { + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="ResultClicks"]').exists()).toBe(true); + }); + + it(' does not render clicks if showClick is false', () => { + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="ResultClicks"]').exists()).toBe(false); + }); + }); + describe('engine', () => { it('renders engine name if this is a meta engine', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx index 776b3fb070092..dabaa5db29060 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx @@ -24,6 +24,7 @@ interface Props { actions: ResultAction[]; documentLink?: string; resultPosition?: number; + showClick: boolean; } export const ResultHeader: React.FC = ({ @@ -33,6 +34,7 @@ export const ResultHeader: React.FC = ({ actions, documentLink, resultPosition, + showClick, }) => { return (
@@ -65,6 +67,16 @@ export const ResultHeader: React.FC = ({ type="id" /> + {showClick && ( + + + + )} {showScore && ( = ({ field, type, value, href }) if (typeof value === 'string') { formattedValue = value; } else if (typeof value === 'number') { - formattedValue = parseFloat((value as number).toFixed(2)).toString(); + if (type === 'clicks') { + formattedValue = i18n.translate('xpack.enterpriseSearch.appSearch.result.clicks', { + defaultMessage: '{clicks} Clicks', + values: { clicks: value }, + description: 'This is click count for Adaptive Relevance suggestion results', + }); + } else { + formattedValue = parseFloat((value as number).toFixed(2)).toString(); + } } const HeaderItemContent = () => ( @@ -45,8 +55,16 @@ export const ResultHeaderItem: React.FC = ({ field, type, value, href }) type === 'score' && 'appSearchResultHeaderItem__score' }`} > - -   + {type !== 'clicks' && ( + <> + +   + + )} {href ? ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts index d9f1bb394778e..f8f579fa32f80 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts @@ -22,6 +22,7 @@ export interface ResultMeta { id: string; score?: number; engine: string; + clicks?: number; } // A search result item diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx index d40d3fd1b6637..38b12d8070fa1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { setMockValues } from '../../__mocks__/kea_logic'; +import '../../__mocks__/shallow_useeffect.mock'; +import { setMockValues, mockKibanaValues } from '../../__mocks__/kea_logic'; import React from 'react'; @@ -50,4 +51,20 @@ describe('ErrorState', () => { expect(wrapper.find('[data-test-subj="CloudError"]').exists()).toBe(false); expect(wrapper.find('[data-test-subj="SelfManagedError"]').exists()).toBe(true); }); + + describe('chrome visiblity', () => { + it('sets chrome visibility to true when not on personal dashboard route', () => { + mockKibanaValues.history.location.pathname = '/overview'; + mountWithIntl(); + + expect(mockKibanaValues.setChromeIsVisible).toHaveBeenCalledWith(true); + }); + + it('sets chrome visibility to false when on personal dashboard route', () => { + mockKibanaValues.history.location.pathname = '/p/sources'; + mountWithIntl(); + + expect(mockKibanaValues.setChromeIsVisible).toHaveBeenCalledWith(false); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx index c160ba5b2f837..d807f1d2322ff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { useValues } from 'kea'; @@ -21,10 +21,27 @@ import { EuiButtonTo, EuiLinkTo } from '../react_router_helpers'; import './error_state_prompt.scss'; +/** + * Personal dashboard urls begin with /p/ + * EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources + */ +const WORKPLACE_SEARCH_PERSONAL_DASHBOARD_PATH = '/p/'; + export const ErrorStatePrompt: React.FC = () => { const { errorConnectingMessage } = useValues(HttpLogic); - const { config, cloud } = useValues(KibanaLogic); + const { config, cloud, setChromeIsVisible, history } = useValues(KibanaLogic); const isCloudEnabled = cloud.isCloudEnabled; + const isWorkplaceSearchPersonalDashboardRoute = history.location.pathname.includes( + WORKPLACE_SEARCH_PERSONAL_DASHBOARD_PATH + ); + + useEffect(() => { + // We hide the Kibana chrome for Workplace Search for Personal Dashboard routes. It is reenabled when the user enters the + // Workplace Search organization admin section of the product. If the Enterprise Search API is not working, we never show + // the chrome and this can have adverse effects when the user leaves thispage and returns to Kibana. To get around this, + // we always show the chrome when the error state is shown, unless the user is visiting the Personal Dashboard. + setChromeIsVisible(!isWorkplaceSearchPersonalDashboardRoute); + }, []); return ( { expect(wrapper.find(WorkplaceSearchConfigured)).toHaveLength(1); }); - it('renders ErrorState', () => { + it('renders ErrorState when not on SetupGuide', () => { + mockUseRouteMatch.mockReturnValue(false); setMockValues({ errorConnectingMessage: '502 Bad Gateway' }); const wrapper = shallow(); @@ -56,6 +57,16 @@ describe('WorkplaceSearch', () => { const errorState = wrapper.find(ErrorState); expect(errorState).toHaveLength(1); }); + + it('does not render ErrorState when on SetupGuide', () => { + mockUseRouteMatch.mockReturnValue(true); + setMockValues({ errorConnectingMessage: '502 Bad Gateway' }); + + const wrapper = shallow(); + + const errorState = wrapper.find(ErrorState); + expect(errorState).toHaveLength(0); + }); }); describe('WorkplaceSearchUnconfigured', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 67e8126f6799d..488d9a0daafd5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -53,6 +53,7 @@ export const WorkplaceSearch: React.FC = (props) => { const { errorConnectingMessage } = useValues(HttpLogic); const { enterpriseSearchVersion, kibanaVersion } = props; const incompatibleVersions = isVersionMismatch(enterpriseSearchVersion, kibanaVersion); + const isSetupGuidePath = !!useRouteMatch(SETUP_GUIDE_PATH); if (!config.host) { return ; @@ -63,7 +64,7 @@ export const WorkplaceSearch: React.FC = (props) => { kibanaVersion={kibanaVersion} /> ); - } else if (errorConnectingMessage) { + } else if (errorConnectingMessage && !isSetupGuidePath) { return ; } @@ -79,7 +80,6 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { * Personal dashboard urls begin with /p/ * EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources */ - const isOrganization = !useRouteMatch(PERSONAL_PATH); setContext(isOrganization); 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 126269e22dd3a..84fc6ac002008 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 9eb20383d57bd..740f0401dca6e 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/integration_tests/reset_preconfiguration.test.ts b/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts index 4096035f840e0..9e13ac7024538 100644 --- a/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts @@ -53,6 +53,7 @@ describe('Fleet preconfiguration rest', () => { { xpack: { fleet: { + // Preconfigure two policies test-12345 and test-456789 agentPolicies: [ { name: 'Elastic Cloud agent policy 0001', @@ -84,6 +85,36 @@ describe('Fleet preconfiguration rest', () => { }, ], }, + { + name: 'Second preconfigured policy', + description: 'second policy', + is_default: false, + is_managed: true, + id: 'test-456789', + namespace: 'default', + monitoring_enabled: [], + package_policies: [ + { + name: 'fleet_server987654321', + package: { + name: 'fleet_server', + }, + inputs: [ + { + type: 'fleet-server', + keep_enabled: true, + vars: [ + { + name: 'host', + value: '127.0.0.1', + frozen: true, + }, + ], + }, + ], + }, + ], + }, ], }, }, @@ -139,11 +170,12 @@ describe('Fleet preconfiguration rest', () => { await new Promise((res) => setTimeout(res, 10000)); }; - beforeEach(async () => { + // Share the same servers for all the test to make test a lot faster (but test are not isolated anymore) + beforeAll(async () => { await startServers(); }); - afterEach(async () => { + afterAll(async () => { await stopServers(); }); @@ -162,12 +194,15 @@ describe('Fleet preconfiguration rest', () => { type: 'ingest-agent-policies', perPage: 10000, }); - expect(agentPolicies.saved_objects).toHaveLength(1); + expect(agentPolicies.saved_objects).toHaveLength(2); expect(agentPolicies.saved_objects.map((ap) => ap.attributes)).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'Elastic Cloud agent policy 0001', }), + expect.objectContaining({ + name: 'Second preconfigured policy', + }), ]) ); }); @@ -181,6 +216,13 @@ describe('Fleet preconfiguration rest', () => { await soClient.delete('ingest-agent-policies', POLICY_ID); + const oldAgentPolicies = await soClient.find({ + type: 'ingest-agent-policies', + perPage: 10000, + }); + + const secondAgentPoliciesUpdatedAt = oldAgentPolicies.saved_objects[0].updated_at; + const resetAPI = kbnTestServer.getSupertest( kbnServer.root, 'post', @@ -194,12 +236,18 @@ describe('Fleet preconfiguration rest', () => { type: 'ingest-agent-policies', perPage: 10000, }); - expect(agentPolicies.saved_objects).toHaveLength(1); - expect(agentPolicies.saved_objects.map((ap) => ap.attributes)).toEqual( + expect(agentPolicies.saved_objects).toHaveLength(2); + expect( + agentPolicies.saved_objects.map((ap) => ({ ...ap.attributes, updated_at: ap.updated_at })) + ).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'Elastic Cloud agent policy 0001', }), + expect.objectContaining({ + name: 'Second preconfigured policy', + updated_at: secondAgentPoliciesUpdatedAt, // Check that policy was not updated + }), ]) ); }); @@ -222,13 +270,16 @@ describe('Fleet preconfiguration rest', () => { type: 'ingest-agent-policies', perPage: 10000, }); - expect(agentPolicies.saved_objects).toHaveLength(1); + expect(agentPolicies.saved_objects).toHaveLength(2); expect(agentPolicies.saved_objects.map((ap) => ap.attributes)).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'Elastic Cloud agent policy 0001', package_policies: expect.arrayContaining([expect.stringMatching(/.*/)]), }), + expect.objectContaining({ + name: 'Second preconfigured policy', + }), ]) ); }); diff --git a/x-pack/plugins/fleet/server/routes/preconfiguration/handler.ts b/x-pack/plugins/fleet/server/routes/preconfiguration/handler.ts index 6e2e320db322e..79fbfc7f9a686 100644 --- a/x-pack/plugins/fleet/server/routes/preconfiguration/handler.ts +++ b/x-pack/plugins/fleet/server/routes/preconfiguration/handler.ts @@ -12,7 +12,7 @@ import type { PreconfiguredAgentPolicy } from '../../../common'; import type { FleetRequestHandler } from '../../types'; import type { PutPreconfigurationSchema, - PostResetOnePreconfiguredAgentPolicies, + PostResetOnePreconfiguredAgentPoliciesSchema, } from '../../types'; import { defaultIngestErrorHandler } from '../../errors'; import { ensurePreconfiguredPackagesAndPolicies, outputService } from '../../services'; @@ -44,8 +44,8 @@ export const updatePreconfigurationHandler: FleetRequestHandler< } }; -export const resetPreconfigurationHandler: FleetRequestHandler< - TypeOf, +export const resetOnePreconfigurationHandler: FleetRequestHandler< + TypeOf, undefined, undefined > = async (context, request, response) => { @@ -53,14 +53,14 @@ export const resetPreconfigurationHandler: FleetRequestHandler< const esClient = context.core.elasticsearch.client.asInternalUser; try { - await resetPreconfiguredAgentPolicies(soClient, esClient, request.params.agentPolicyid); + await resetPreconfiguredAgentPolicies(soClient, esClient, request.params.agentPolicyId); return response.ok({}); } catch (error) { return defaultIngestErrorHandler({ error, response }); } }; -export const resetOnePreconfigurationHandler: FleetRequestHandler< +export const resetPreconfigurationHandler: FleetRequestHandler< undefined, undefined, undefined diff --git a/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts b/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts index ec904e64a18de..38651b15f939d 100644 --- a/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts +++ b/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts @@ -6,7 +6,10 @@ */ import { PRECONFIGURATION_API_ROUTES } from '../../constants'; -import { PutPreconfigurationSchema } from '../../types'; +import { + PutPreconfigurationSchema, + PostResetOnePreconfiguredAgentPoliciesSchema, +} from '../../types'; import type { FleetAuthzRouter } from '../security'; import { @@ -29,7 +32,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => { router.post( { path: PRECONFIGURATION_API_ROUTES.RESET_ONE_PATTERN, - validate: false, + validate: PostResetOnePreconfiguredAgentPoliciesSchema, fleetAuthz: { fleet: { all: true }, }, diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 041b0a45643e0..4c8ef5c3a1b87 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -679,7 +679,7 @@ class AgentPolicyService { await this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'deleted', id); if (options?.removeFleetServerDocuments) { - this.deleteFleetServerPoliciesForPolicyId(esClient, id); + await this.deleteFleetServerPoliciesForPolicyId(esClient, id); } return { diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 5858ac8900c11..5591c165bd706 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/fleet/server/services/preconfiguration/reset_agent_policies.ts b/x-pack/plugins/fleet/server/services/preconfiguration/reset_agent_policies.ts index 4285b62899d34..1c4ac5c5a7a9a 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration/reset_agent_policies.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration/reset_agent_policies.ts @@ -84,16 +84,22 @@ async function _deleteExistingData( logger: Logger, agentPolicyId?: string ) { - let existingPolicies: AgentPolicy[]; + let existingPolicies: AgentPolicy[] = []; if (agentPolicyId) { - const policy = await agentPolicyService.get(soClient, agentPolicyId); - if (!policy || !policy.is_preconfigured) { + const policy = await agentPolicyService.get(soClient, agentPolicyId).catch((err) => { + if (err.output?.statusCode === 404) { + return undefined; + } + throw err; + }); + if (policy && !policy.is_preconfigured) { throw new Error('Invalid policy'); } - existingPolicies = [policy]; - } - { + if (policy) { + existingPolicies = [policy]; + } + } else { existingPolicies = ( await agentPolicyService.list(soClient, { perPage: SO_SEARCH_LIMIT, @@ -120,6 +126,7 @@ async function _deleteExistingData( const { items: enrollmentApiKeys } = await listEnrollmentApiKeys(esClient, { perPage: SO_SEARCH_LIMIT, showInactive: true, + kuery: existingPolicies.map((policy) => `policy_id:"${policy.id}"`).join(' or '), }); if (enrollmentApiKeys.length > 0) { diff --git a/x-pack/plugins/fleet/server/types/rest_spec/preconfiguration.ts b/x-pack/plugins/fleet/server/types/rest_spec/preconfiguration.ts index 936469a16100f..9ea5e8ab5e392 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/preconfiguration.ts @@ -16,8 +16,8 @@ export const PutPreconfigurationSchema = { }), }; -export const PostResetOnePreconfiguredAgentPolicies = { +export const PostResetOnePreconfiguredAgentPoliciesSchema = { params: schema.object({ - agentPolicyid: schema.string(), + agentPolicyId: schema.string(), }), }; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx index 3d9618e4b070b..2188454c80b13 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx @@ -47,6 +47,7 @@ import { } from '../../../../../common/http_api/log_alerts/'; import { useChartPreviewData } from './hooks/use_chart_preview_data'; import { decodeOrThrow } from '../../../../../common/runtime_types'; +import { useKibanaTimeZoneSetting } from '../../../../hooks/use_kibana_time_zone_setting'; const GROUP_LIMIT = 5; @@ -126,6 +127,7 @@ const CriterionPreviewChart: React.FC = ({ }) => { const { uiSettings } = useKibana().services; const isDarkMode = uiSettings?.get('theme:darkMode') || false; + const timezone = useKibanaTimeZoneSetting(); const { getChartPreviewData, @@ -242,6 +244,7 @@ const CriterionPreviewChart: React.FC = ({ }, }} color={!isGrouped ? colorTransformer(Color.color0) : undefined} + timeZone={timezone} /> {showThreshold && threshold ? ( (UI_SETTINGS.DATEFORMAT_TZ); + + if (!kibanaTimeZone || kibanaTimeZone === 'Browser') { + return 'local'; + } + + return kibanaTimeZone; +} diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/single_metric_sparkline.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/single_metric_sparkline.tsx index 289497da0626a..38bb7d224570c 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/single_metric_sparkline.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/single_metric_sparkline.tsx @@ -14,6 +14,7 @@ import { } from '@elastic/eui/dist/eui_charts_theme'; import { useKibanaUiSetting } from '../../../../../utils/use_kibana_ui_setting'; +import { useKibanaTimeZoneSetting } from '../../../../../hooks/use_kibana_time_zone_setting'; import { TimeRange } from '../../../../../../common/time'; interface TimeSeriesPoint { @@ -33,6 +34,7 @@ export const SingleMetricSparkline: React.FunctionComponent<{ timeRange: TimeRange; }> = ({ metric, timeRange }) => { const [isDarkMode] = useKibanaUiSetting('theme:darkMode'); + const timeZone = useKibanaTimeZoneSetting(); const theme = useMemo( () => [ @@ -60,6 +62,7 @@ export const SingleMetricSparkline: React.FunctionComponent<{ xAccessor={timestampAccessor} xScaleType={ScaleType.Time} yAccessors={valueAccessor} + timeZone={timeZone} /> ); diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/series_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/series_chart.tsx index d1c98ed97ce18..22079943efb54 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/series_chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/series_chart.tsx @@ -16,6 +16,7 @@ import { } from '@elastic/charts'; import { NodeDetailsDataSeries } from '../../../../../common/http_api/node_details_api'; import { InventoryVisType } from '../../../../../common/inventory_models/types'; +import { useKibanaTimeZoneSetting } from '../../../../hooks/use_kibana_time_zone_setting'; interface Props { id: string; @@ -34,6 +35,7 @@ export const SeriesChart = (props: Props) => { }; export const AreaChart = ({ id, color, series, name, type, stack }: Props) => { + const timezone = useKibanaTimeZoneSetting(); const style: RecursivePartial = { area: { opacity: 1, @@ -56,11 +58,13 @@ export const AreaChart = ({ id, color, series, name, type, stack }: Props) => { areaSeriesStyle={style} color={color ? color : void 0} stackAccessors={stack ? ['timestamp'] : void 0} + timeZone={timezone} /> ); }; export const BarChart = ({ id, color, series, name, stack }: Props) => { + const timezone = useKibanaTimeZoneSetting(); const style: RecursivePartial = { rectBorder: { stroke: color || void 0, @@ -83,6 +87,7 @@ export const BarChart = ({ id, color, series, name, stack }: Props) => { barSeriesStyle={style} color={color ? color : void 0} stackAccessors={stack ? ['timestamp'] : void 0} + timeZone={timezone} /> ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx index 47844543a279a..12db775e243f8 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx @@ -21,6 +21,7 @@ import { MetricsExplorerOptionsMetric, MetricsExplorerChartType, } from '../hooks/use_metrics_explorer_options'; +import { useKibanaTimeZoneSetting } from '../../../../hooks/use_kibana_time_zone_setting'; import { getMetricId } from './helpers/get_metric_id'; type NumberOrString = string | number; @@ -42,6 +43,7 @@ export const MetricExplorerSeriesChart = (props: Props) => { }; export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack, opacity }: Props) => { + const timezone = useKibanaTimeZoneSetting(); const color = (metric.color && colorTransformer(metric.color)) || colorTransformer(Color.color0); const yAccessors = Array.isArray(id) @@ -78,11 +80,13 @@ export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack, opac stackAccessors={stack ? ['timestamp'] : void 0} areaSeriesStyle={seriesAreaStyle} color={color} + timeZone={timezone} /> ); }; export const MetricsExplorerBarChart = ({ metric, id, series, stack }: Props) => { + const timezone = useKibanaTimeZoneSetting(); const color = (metric.color && colorTransformer(metric.color)) || colorTransformer(Color.color0); const yAccessors = Array.isArray(id) @@ -113,6 +117,7 @@ export const MetricsExplorerBarChart = ({ metric, id, series, stack }: Props) => stackAccessors={stack ? ['timestamp'] : void 0} barSeriesStyle={seriesBarStyle} color={color} + timeZone={timezone} /> ); }; 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 08f046925cb46..23804a8a6d618 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'; @@ -126,11 +122,13 @@ describe('Lens App', () => { defaultSavedObjectId = '1234'; defaultDoc = { savedObjectId: defaultSavedObjectId, + visualizationType: 'testVis', + type: 'lens', title: 'An extremely cool default document!', expression: 'definitely a valid expression', state: { query: 'lucene', - filters: [{ query: { match_phrase: { src: 'test' } } }], + filters: [{ query: { match_phrase: { src: 'test' } }, meta: { index: 'index-pattern-0' } }], }, references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], } as unknown as Document; @@ -145,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 []; }); @@ -666,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, @@ -685,7 +683,7 @@ describe('Lens App', () => { savedObjectId: defaultSavedObjectId, title: 'hello there2', state: expect.objectContaining({ - filters: [unpinned], + filters: services.data.query.filterManager.inject([unpinned], []), }), }), true, @@ -880,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)], }), }); }); @@ -930,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({ @@ -1065,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')!()); @@ -1122,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')!()); @@ -1252,24 +1246,28 @@ describe('Lens App', () => { }); it('should not confirm when changes are saved', async () => { - const { props } = await mountWith({ - preloadedState: { - persistedDoc: { - ...defaultDoc, - state: { - ...defaultDoc.state, - datasourceStates: { testDatasource: {} }, - visualization: {}, - }, - }, - isSaveable: true, - ...(defaultDoc.state as Partial), - visualization: { - activeId: 'testVis', - state: {}, + const preloadedState = { + persistedDoc: { + ...defaultDoc, + state: { + ...defaultDoc.state, + datasourceStates: { testDatasource: {} }, + visualization: {}, }, }, - }); + isSaveable: true, + ...(defaultDoc.state as Partial), + visualization: { + activeId: 'testVis', + state: {}, + }, + }; + + const customProps = makeDefaultProps(); + customProps.datasourceMap.testDatasource.isEqual = () => true; // if this returns false, the documents won't be accounted equal + + const { props } = await mountWith({ preloadedState, props: customProps }); + const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(defaultLeave).toHaveBeenCalled(); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 5638a35d1cc6d..78e739a6324ec 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -7,8 +7,7 @@ import './app.scss'; -import { isEqual } from 'lodash'; -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiBreadcrumb } from '@elastic/eui'; import { @@ -32,13 +31,10 @@ import { DispatchSetState, selectSavedObjectFormat, } from '../state_management'; -import { - SaveModalContainer, - getLastKnownDocWithoutPinnedFilters, - runSaveLensVisualization, -} from './save_modal_container'; +import { SaveModalContainer, runSaveLensVisualization } from './save_modal_container'; import { LensInspector } from '../lens_inspector_service'; import { getEditPath } from '../../common'; +import { isLensEqual } from './lens_document_equality'; export type SaveProps = Omit & { returnToOrigin: boolean; @@ -92,8 +88,17 @@ export function App({ isSaveable, } = useLensSelector((state) => state.lens); + const selectorDependencies = useMemo( + () => ({ + datasourceMap, + visualizationMap, + extractFilterReferences: data.query.filterManager.extract, + }), + [datasourceMap, visualizationMap, data.query.filterManager.extract] + ); + const currentDoc = useLensSelector((state) => - selectSavedObjectFormat(state, datasourceMap, visualizationMap) + selectSavedObjectFormat(state, selectorDependencies) ); // Used to show a popover that guides the user towards changing the date range when no data is available. @@ -146,12 +151,9 @@ export function App({ useEffect(() => { onAppLeave((actions) => { - // Confirm when the user has made any changes to an existing doc - // or when the user has configured something without saving - if ( application.capabilities.visualize.save && - !isEqual(persistedDoc?.state, getLastKnownDocWithoutPinnedFilters(lastKnownDoc)?.state) && + !isLensEqual(persistedDoc, lastKnownDoc, data.query.filterManager.inject, datasourceMap) && (isSaveable || persistedDoc) ) { return actions.confirm( @@ -166,7 +168,15 @@ export function App({ return actions.default(); } }); - }, [onAppLeave, lastKnownDoc, isSaveable, persistedDoc, application.capabilities.visualize.save]); + }, [ + onAppLeave, + lastKnownDoc, + isSaveable, + persistedDoc, + application.capabilities.visualize.save, + data.query.filterManager.inject, + datasourceMap, + ]); const getLegacyUrlConflictCallout = useCallback(() => { // This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario diff --git a/x-pack/plugins/lens/public/app_plugin/lens_document_equality.test.ts b/x-pack/plugins/lens/public/app_plugin/lens_document_equality.test.ts new file mode 100644 index 0000000000000..8bd1e6e980bbb --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/lens_document_equality.test.ts @@ -0,0 +1,248 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Filter, FilterStateStore } from '@kbn/es-query'; +import { isLensEqual } from './lens_document_equality'; +import { Document } from '../persistence/saved_object_store'; +import { Datasource, DatasourceMap } from '../types'; + +const defaultDoc: Document = { + title: 'some-title', + visualizationType: 'lnsXY', + state: { + query: { + query: '', + language: 'kuery', + }, + visualization: { + some: 'props', + }, + datasourceStates: { + indexpattern: {}, + }, + filters: [ + { + meta: { + index: 'reference-1', + }, + }, + ], + }, + references: [ + { + name: 'reference-1', + id: 'id-1', + type: 'index-pattern', + }, + ], +}; + +describe('lens document equality', () => { + const mockInjectFilterReferences = jest.fn((filters: Filter[]) => + filters.map((filter) => ({ + ...filter, + meta: { + ...filter.meta, + index: 'injected!', + }, + })) + ); + + let mockDatasourceMap: DatasourceMap; + + beforeEach(() => { + mockDatasourceMap = { + indexpattern: { isEqual: jest.fn(() => true) }, + } as unknown as DatasourceMap; + }); + + it('returns true when documents are equal', () => { + expect( + isLensEqual(defaultDoc, defaultDoc, mockInjectFilterReferences, mockDatasourceMap) + ).toBeTruthy(); + }); + + it('handles undefined documents', () => { + expect(isLensEqual(undefined, undefined, mockInjectFilterReferences, {})).toBeTruthy(); + expect(isLensEqual(undefined, {} as Document, mockInjectFilterReferences, {})).toBeFalsy(); + expect(isLensEqual({} as Document, undefined, mockInjectFilterReferences, {})).toBeFalsy(); + }); + + it('should compare visualization type', () => { + expect( + isLensEqual( + defaultDoc, + { ...defaultDoc, visualizationType: 'other-type' }, + mockInjectFilterReferences, + mockDatasourceMap + ) + ).toBeFalsy(); + }); + + it('should compare the query', () => { + expect( + isLensEqual( + defaultDoc, + { + ...defaultDoc, + state: { + ...defaultDoc.state, + query: { + query: 'foobar', + language: 'kuery', + }, + }, + }, + mockInjectFilterReferences, + mockDatasourceMap + ) + ).toBeFalsy(); + }); + + it('should compare the visualization state', () => { + expect( + isLensEqual( + defaultDoc, + { + ...defaultDoc, + state: { + ...defaultDoc.state, + visualization: { + some: 'other-props', + }, + }, + }, + mockInjectFilterReferences, + mockDatasourceMap + ) + ).toBeFalsy(); + }); + + describe('comparing the datasources', () => { + it('checks available datasources', () => { + // add an extra datasource in one doc + expect( + isLensEqual( + defaultDoc, + { + ...defaultDoc, + state: { + ...defaultDoc.state, + datasourceStates: { + ...defaultDoc.state.datasourceStates, + foodatasource: {}, + }, + }, + }, + mockInjectFilterReferences, + { ...mockDatasourceMap, foodatasource: { isEqual: () => true } as unknown as Datasource } + ) + ).toBeFalsy(); + + // ordering of the datasource states shouldn't matter + expect( + isLensEqual( + { + ...defaultDoc, + state: { + ...defaultDoc.state, + datasourceStates: { + foodatasource: {}, // first + ...defaultDoc.state.datasourceStates, + }, + }, + }, + { + ...defaultDoc, + state: { + ...defaultDoc.state, + datasourceStates: { + ...defaultDoc.state.datasourceStates, + foodatasource: {}, // last + }, + }, + }, + mockInjectFilterReferences, + { ...mockDatasourceMap, foodatasource: { isEqual: () => true } as unknown as Datasource } + ) + ).toBeTruthy(); + }); + + it('delegates internal datasource comparison', () => { + // datasource's isEqual returns false + (mockDatasourceMap.indexpattern.isEqual as jest.Mock).mockReturnValue(false); + expect( + isLensEqual(defaultDoc, defaultDoc, mockInjectFilterReferences, mockDatasourceMap) + ).toBeFalsy(); + }); + }); + + it('should ignore pinned filters', () => { + // ignores pinned filters + const pinnedFilter: Filter = { + $state: { + store: FilterStateStore.GLOBAL_STATE, + }, + meta: {}, + }; + + const filtersWithPinned = [...defaultDoc.state.filters, pinnedFilter]; + + expect( + isLensEqual( + defaultDoc, + { ...defaultDoc, state: { ...defaultDoc.state, filters: filtersWithPinned } }, + mockInjectFilterReferences, + mockDatasourceMap + ) + ).toBeTruthy(); + }); + + it('should inject filter references', () => { + // injects filter references for comparison + expect( + isLensEqual( + defaultDoc, + { + ...defaultDoc, + state: { + ...defaultDoc.state, + filters: [ + { + meta: { + index: 'some-other-reference-name', + }, + }, + ], + }, + }, + mockInjectFilterReferences, + mockDatasourceMap + ) + ).toBeTruthy(); + }); + + it('should consider undefined props equivalent to non-existant props', () => { + expect( + isLensEqual( + defaultDoc, + { + ...defaultDoc, + state: { + ...defaultDoc.state, + visualization: { + ...(defaultDoc.state.visualization as object), + foo: undefined, + }, + }, + }, + mockInjectFilterReferences, + mockDatasourceMap + ) + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts b/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts new file mode 100644 index 0000000000000..3e833502c0592 --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEqual, intersection, union } from 'lodash'; +import { FilterManager } from 'src/plugins/data/public'; +import { Document } from '../persistence/saved_object_store'; +import { DatasourceMap } from '../types'; +import { injectDocFilterReferences, removePinnedFilters } from './save_modal_container'; + +const removeNonSerializable = (obj: Parameters[0]) => + JSON.parse(JSON.stringify(obj)); + +export const isLensEqual = ( + doc1In: Document | undefined, + doc2In: Document | undefined, + injectFilterReferences: FilterManager['inject'], + datasourceMap: DatasourceMap +) => { + if (doc1In === undefined || doc2In === undefined) { + return doc1In === doc2In; + } + + // we do this so that undefined props are the same as non-existant props + const doc1 = removeNonSerializable(doc1In); + const doc2 = removeNonSerializable(doc2In); + + if (doc1?.visualizationType !== doc2?.visualizationType) { + return false; + } + + if (!isEqual(doc1.state.query, doc2.state.query)) { + return false; + } + + if (!isEqual(doc1.state.visualization, doc2.state.visualization)) { + return false; + } + + // data source equality + const availableDatasourceTypes1 = Object.keys(doc1.state.datasourceStates); + const availableDatasourceTypes2 = Object.keys(doc2.state.datasourceStates); + + let datasourcesEqual = + intersection(availableDatasourceTypes1, availableDatasourceTypes2).length === + union(availableDatasourceTypes1, availableDatasourceTypes2).length; + + if (datasourcesEqual) { + // equal so far, so actually check + datasourcesEqual = availableDatasourceTypes1 + .map((type) => + datasourceMap[type].isEqual( + doc1.state.datasourceStates[type], + doc1.references, + doc2.state.datasourceStates[type], + doc2.references + ) + ) + .every((res) => res); + } + + if (!datasourcesEqual) { + return false; + } + + const [filtersInjected1, filtersInjected2] = [doc1, doc2].map((doc) => + removePinnedFilters(injectDocFilterReferences(injectFilterReferences, doc)) + ); + if (!isEqual(filtersInjected1?.state.filters, filtersInjected2?.state.filters)) { + return false; + } + + return true; +}; 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 b3bc40ef5ac19..e3098904a4b85 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,15 +8,15 @@ import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; -import { partition } from 'lodash'; +import { isFilterPinned } from '@kbn/es-query'; import type { SavedObjectReference } from 'kibana/public'; import { SaveModal } from './save_modal'; import type { LensAppProps, LensAppServices } from './types'; import type { SaveProps } from './app'; -import { Document, injectFilterReferences, checkForDuplicateTitle } from '../persistence'; +import { Document, checkForDuplicateTitle } from '../persistence'; import type { LensByReferenceInput, LensEmbeddableInput } from '../embeddable'; -import { esFilters } 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'; @@ -169,10 +169,11 @@ const redirectToDashboard = ({ const getDocToSave = ( lastKnownDoc: Document, saveProps: SaveProps, - references: SavedObjectReference[] + references: SavedObjectReference[], + injectFilterReferences: FilterManager['inject'] ) => { const docToSave = { - ...getLastKnownDocWithoutPinnedFilters(lastKnownDoc)!, + ...injectDocFilterReferences(injectFilterReferences, removePinnedFilters(lastKnownDoc))!, references, }; @@ -200,6 +201,7 @@ export const runSaveLensVisualization = async ( ): Promise | undefined> => { const { chrome, + data, initialInput, originatingApp, lastKnownDoc, @@ -240,7 +242,12 @@ export const runSaveLensVisualization = async ( ); } - const docToSave = getDocToSave(lastKnownDoc, saveProps, references); + const docToSave = getDocToSave( + lastKnownDoc, + saveProps, + references, + data.query.filterManager.inject + ); // Required to serialize filters in by value mode until // https://github.com/elastic/kibana/issues/77588 is fixed @@ -351,21 +358,29 @@ export const runSaveLensVisualization = async ( } }; -export function getLastKnownDocWithoutPinnedFilters(doc?: Document) { +export function injectDocFilterReferences( + injectFilterReferences: FilterManager['inject'], + doc?: Document +) { if (!doc) return undefined; - const [pinnedFilters, appFilters] = partition( - injectFilterReferences(doc.state?.filters || [], doc.references), - esFilters.isFilterPinned - ); - return pinnedFilters?.length - ? { - ...doc, - state: { - ...doc.state, - filters: appFilters, - }, - } - : doc; + return { + ...doc, + state: { + ...doc.state, + filters: injectFilterReferences(doc.state?.filters || [], doc.references), + }, + }; +} + +export function removePinnedFilters(doc?: Document) { + if (!doc) return undefined; + return { + ...doc, + state: { + ...doc.state, + filters: (doc.state?.filters || []).filter((filter) => !isFilterPinned(filter)), + }, + }; } // eslint-disable-next-line import/no-default-export diff --git a/x-pack/plugins/lens/public/async_services.ts b/x-pack/plugins/lens/public/async_services.ts index bbb4faf55e1e9..09b434c648418 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 fef71e92c5f2c..5d475be7bb83f 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/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index ceb9388a561f0..17e18392e83e9 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx @@ -17,7 +17,7 @@ import { import { ReactExpressionRendererProps } from 'src/plugins/expressions/public'; import { spacesPluginMock } from '../../../spaces/public/mocks'; import { Filter } from '@kbn/es-query'; -import { Query, TimeRange, IndexPatternsContract } from 'src/plugins/data/public'; +import { Query, TimeRange, IndexPatternsContract, FilterManager } from 'src/plugins/data/public'; import { Document } from '../persistence'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public/embeddable'; @@ -72,6 +72,16 @@ const options = { checkForDuplicateTitle: defaultCheckForDuplicateTitle, }; +const mockInjectFilterReferences: FilterManager['inject'] = (filters, references) => { + return filters.map((filter) => ({ + ...filter, + meta: { + ...filter.meta, + index: 'injected!', + }, + })); +}; + const attributeServiceMockFromSavedVis = (document: Document): LensAttributeService => { const core = coreMock.createStart(); const service = new AttributeService< @@ -139,6 +149,7 @@ describe('embeddable', () => { getTrigger, theme: themeServiceMock.createStartContract(), visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), documentToExpression: () => Promise.resolve({ ast: { @@ -180,6 +191,7 @@ describe('embeddable', () => { capabilities: { canSaveDashboards: true, canSaveVisualizations: true }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -226,6 +238,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -283,6 +296,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -329,6 +343,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -370,6 +385,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -414,6 +430,7 @@ describe('embeddable', () => { capabilities: { canSaveDashboards: true, canSaveVisualizations: true }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -465,6 +482,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -514,6 +532,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -570,6 +589,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -592,7 +612,7 @@ describe('embeddable', () => { expect.objectContaining({ timeRange, query: [query, savedVis.state.query], - filters, + filters: mockInjectFilterReferences(filters, []), }) ); @@ -627,6 +647,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -663,9 +684,7 @@ describe('embeddable', () => { state: { ...savedVis.state, query: { language: 'kquery', query: 'saved filter' }, - filters: [ - { meta: { alias: 'test', negate: false, disabled: false, indexRefName: 'filter-0' } }, - ], + filters: [{ meta: { alias: 'test', negate: false, disabled: false, index: 'filter-0' } }], }, references: [{ type: 'index-pattern', name: 'filter-0', id: 'my-index-pattern-id' }], }; @@ -687,6 +706,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -708,11 +728,14 @@ describe('embeddable', () => { expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual({ timeRange, query: [query, { language: 'kquery', query: 'saved filter' }], - filters: [ - filters[0], - // actual index pattern id gets injected - { meta: { alias: 'test', negate: false, disabled: false, index: 'my-index-pattern-id' } }, - ], + // actual index pattern id gets injected + filters: mockInjectFilterReferences( + [ + filters[0], + { meta: { alias: 'test', negate: false, disabled: false, index: 'injected!' } }, + ], + [] + ), }); }); @@ -731,6 +754,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -775,6 +799,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -819,6 +844,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -878,6 +904,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -953,6 +980,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -1003,6 +1031,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -1053,6 +1082,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ @@ -1124,6 +1154,7 @@ describe('embeddable', () => { }, getTrigger, theme: themeServiceMock.createStartContract(), + injectFilterReferences: jest.fn(mockInjectFilterReferences), visualizationMap: { [visDocument.visualizationType as string]: { onEditAction: onEditActionMock, diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 22acfb7aa063f..2878a484686d2 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -16,6 +16,7 @@ import type { TimefilterContract, TimeRange, IndexPattern, + FilterManager, } from 'src/plugins/data/public'; import type { PaletteOutput } from 'src/plugins/charts/public'; import type { Start as InspectorStart } from 'src/plugins/inspector/public'; @@ -42,7 +43,7 @@ import { SavedObjectEmbeddableInput, ReferenceOrValueEmbeddable, } from '../../../../../src/plugins/embeddable/public'; -import { Document, injectFilterReferences } from '../persistence'; +import { Document } from '../persistence'; import { ExpressionWrapper, ExpressionWrapperProps } from './expression_wrapper'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { @@ -107,6 +108,7 @@ export interface LensEmbeddableDeps { documentToExpression: ( doc: Document ) => Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }>; + injectFilterReferences: FilterManager['inject']; visualizationMap: VisualizationMap; indexPatternService: IndexPatternsContract; expressionRenderer: ReactExpressionRendererType; @@ -477,7 +479,7 @@ export class Embeddable output.filters = [...this.savedVis.state.filters]; } - output.filters = injectFilterReferences(output.filters, this.savedVis.references); + output.filters = this.deps.injectFilterReferences(output.filters, this.savedVis.references); return output; } diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts index c1bc5aacc3943..fc335bf2f5f87 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts @@ -10,7 +10,11 @@ import { i18n } from '@kbn/i18n'; import { RecursiveReadonly } from '@kbn/utility-types'; import { Ast } from '@kbn/interpreter'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; -import { IndexPatternsContract, TimefilterContract } from '../../../../../src/plugins/data/public'; +import { + FilterManager, + IndexPatternsContract, + TimefilterContract, +} from '../../../../../src/plugins/data/public'; import { ReactExpressionRendererType } from '../../../../../src/plugins/expressions/public'; import { EmbeddableFactoryDefinition, @@ -40,6 +44,7 @@ export interface LensEmbeddableStartServices { documentToExpression: ( doc: Document ) => Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }>; + injectFilterReferences: FilterManager['inject']; visualizationMap: VisualizationMap; spaces?: SpacesPluginStart; theme: ThemeServiceStart; @@ -88,6 +93,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { timefilter, expressionRenderer, documentToExpression, + injectFilterReferences, visualizationMap, uiActions, coreHttp, @@ -113,6 +119,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { getTrigger: uiActions?.getTrigger, getTriggerCompatibleActions: uiActions?.getTriggerCompatibleActions, documentToExpression, + injectFilterReferences, visualizationMap, capabilities: { canSaveDashboards: Boolean(capabilities.dashboard?.showWriteControls), diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index 29dce6f0d1090..3e622d8ac9312 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/__mocks__/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts index f7be239d8fb36..072087e0de65c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts @@ -28,3 +28,5 @@ export function loadInitialState() { const originalLoader = jest.requireActual('../loader'); export const extractReferences = originalLoader.extractReferences; + +export const injectReferences = originalLoader.injectReferences; 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 dd3185b3c7990..efe7966870531 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 386cd7a58ae01..4301540e5bf7e 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/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 0d1954564c6f3..cc88375e4137b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -29,6 +29,8 @@ import { indexPatternFieldEditorPluginMock } from 'src/plugins/data_view_field_e import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; import { TinymathAST } from 'packages/kbn-tinymath'; +import { SavedObjectReference } from 'kibana/server'; +import { cloneDeep } from 'lodash'; jest.mock('./loader'); jest.mock('../id_generator'); @@ -1737,4 +1739,77 @@ describe('IndexPattern Data Source', () => { }); }); }); + + describe('#isEqual', () => { + const layerId = '8bd66b66-aba3-49fb-9ff2-4bf83f2be08e'; + + const persistableState: IndexPatternPersistedState = { + layers: { + [layerId]: { + columns: { + 'fa649155-d7f5-49d9-af26-508287431244': { + label: 'Count of records', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: 'Records', + }, + }, + columnOrder: ['fa649155-d7f5-49d9-af26-508287431244'], + incompleteColumns: {}, + }, + }, + }; + + const currentIndexPatternReference = { + id: 'some-id', + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }; + + const references1: SavedObjectReference[] = [ + currentIndexPatternReference, + { + id: 'some-id', + name: 'indexpattern-datasource-layer-8bd66b66-aba3-49fb-9ff2-4bf83f2be08e', + type: 'index-pattern', + }, + ]; + + const references2: SavedObjectReference[] = [ + currentIndexPatternReference, + { + id: 'some-DIFFERENT-id', + name: 'indexpattern-datasource-layer-8bd66b66-aba3-49fb-9ff2-4bf83f2be08e', + type: 'index-pattern', + }, + ]; + + it('should be false if datasource states are using different data views', () => { + expect( + indexPatternDatasource.isEqual(persistableState, references1, persistableState, references2) + ).toBe(false); + }); + + it('should be false if datasource states differ', () => { + const differentPersistableState = cloneDeep(persistableState); + differentPersistableState.layers[layerId].columnOrder = ['something else']; + + expect( + indexPatternDatasource.isEqual( + persistableState, + references1, + differentPersistableState, + references1 + ) + ).toBe(false); + }); + + it('should be true if datasource states are identical and they refer to the same data view', () => { + expect( + indexPatternDatasource.isEqual(persistableState, references1, persistableState, references1) + ).toBe(true); + }); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 49a85f3f3af79..81acac82355ab 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -12,6 +12,7 @@ import type { CoreStart, SavedObjectReference } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import type { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import type { FieldFormatsStart } from 'src/plugins/field_formats/public'; +import { isEqual } from 'lodash'; import type { IndexPatternFieldEditorStart } from '../../../../../src/plugins/data_view_field_editor/public'; import type { DatasourceDimensionEditorProps, @@ -27,6 +28,7 @@ import { changeIndexPattern, changeLayerIndexPattern, extractReferences, + injectReferences, } from './loader'; import { toExpression } from './to_expression'; import { @@ -545,6 +547,16 @@ export function getIndexPatternDatasource({ }) ); }, + isEqual: ( + persistableState1: IndexPatternPersistedState, + references1: SavedObjectReference[], + persistableState2: IndexPatternPersistedState, + references2: SavedObjectReference[] + ) => + isEqual( + injectReferences(persistableState1, references1), + injectReferences(persistableState2, references2) + ), }; return indexPatternDatasource; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index e1a15b87e5f5c..c61569539bec8 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 bd5b816cd8917..2b11d182eeed0 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 fc69ea1d869f1..62a681ac3d604 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 2babd87768e32..d1561e93aa807 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 15c49a7336c7e..ce0d03a232e28 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 0000000000000..63255ad2bf9dc --- /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 5ff0c4e2d4bd7..cbe6efba1b859 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 ee245cc06bff9..a3b61429fb0bf 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 b9d675716c788..4474effc8c8c8 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 289161c9d3e37..dda1b16bc6c7b 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 a0d43c5523c5b..08786b181f3e7 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 daab2566b28fe..e740c789a4874 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 } 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,12 +85,25 @@ export function mockDataPlugin( getFilters: () => filters, getGlobalFilters: () => { // @ts-ignore - return filters.filter(esFilters.isFilterPinned); + return filters.filter(isFilterPinned); }, removeAll: () => { filters = []; subscriber(); }, + inject: (filtersIn: Filter[]) => { + return filtersIn.map((filter) => ({ + ...filter, + meta: { ...filter.meta, index: 'injected!' }, + })); + }, + extract: (filtersIn: Filter[]) => { + const state = filtersIn.map((filter) => ({ + ...filter, + meta: { ...filter.meta, index: 'extracted!' }, + })); + return { state, references: [] }; + }, }; } function createMockQueryString() { diff --git a/x-pack/plugins/lens/public/mocks/datasource_mock.ts b/x-pack/plugins/lens/public/mocks/datasource_mock.ts index 50df6f07cb5dc..ce36b575b30e3 100644 --- a/x-pack/plugins/lens/public/mocks/datasource_mock.ts +++ b/x-pack/plugins/lens/public/mocks/datasource_mock.ts @@ -51,6 +51,7 @@ export function createMockDatasource(id: string): DatasourceMock { checkIntegrity: jest.fn((_state) => []), isTimeBased: jest.fn(), isValidColumn: jest.fn(), + isEqual: jest.fn(), }; } 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 a92533a89ba67..4e713872c5a67 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/mocks/services_mock.tsx b/x-pack/plugins/lens/public/mocks/services_mock.tsx index 5ec4f8db4a0ed..9fa6d61370a17 100644 --- a/x-pack/plugins/lens/public/mocks/services_mock.tsx +++ b/x-pack/plugins/lens/public/mocks/services_mock.tsx @@ -40,7 +40,7 @@ export const defaultDoc = { visualizationType: 'testVis', state: { query: 'kuery', - filters: [{ query: { match_phrase: { src: 'test' } } }], + filters: [{ query: { match_phrase: { src: 'test' } }, meta: { index: 'index-pattern-0' } }], datasourceStates: { testDatasource: 'datasource', }, diff --git a/x-pack/plugins/lens/public/persistence/filter_references.test.ts b/x-pack/plugins/lens/public/persistence/filter_references.test.ts deleted file mode 100644 index 8a86c6fdc9664..0000000000000 --- a/x-pack/plugins/lens/public/persistence/filter_references.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Filter } from '@kbn/es-query'; -import { extractFilterReferences, injectFilterReferences } from './filter_references'; -import { FilterStateStore } from 'src/plugins/data/common'; - -describe('filter saved object references', () => { - const filters: Filter[] = [ - { - $state: { store: FilterStateStore.APP_STATE }, - meta: { - alias: null, - disabled: false, - index: '90943e30-9a47-11e8-b64d-95841ca0b247', - key: 'geo.src', - negate: true, - params: { query: 'CN' }, - type: 'phrase', - }, - query: { match_phrase: { 'geo.src': 'CN' } }, - }, - { - $state: { store: FilterStateStore.APP_STATE }, - meta: { - alias: null, - disabled: false, - index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - key: 'geoip.country_iso_code', - negate: true, - params: { query: 'US' }, - type: 'phrase', - }, - query: { match_phrase: { 'geoip.country_iso_code': 'US' } }, - }, - ]; - - it('should create two index-pattern references', () => { - const { references } = extractFilterReferences(filters); - expect(references).toMatchInlineSnapshot(` - Array [ - Object { - "id": "90943e30-9a47-11e8-b64d-95841ca0b247", - "name": "filter-index-pattern-0", - "type": "index-pattern", - }, - Object { - "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", - "name": "filter-index-pattern-1", - "type": "index-pattern", - }, - ] - `); - }); - - it('should remove index and value from persistable filter', () => { - const { persistableFilters } = extractFilterReferences([ - { ...filters[0], meta: { ...filters[0].meta, value: 'CN' } }, - { ...filters[1], meta: { ...filters[1].meta, value: 'US' } }, - ]); - expect(persistableFilters.length).toBe(2); - persistableFilters.forEach((filter) => { - expect(filter.meta.hasOwnProperty('index')).toBe(false); - expect(filter.meta.hasOwnProperty('value')).toBe(false); - }); - }); - - it('should restore the same filter after extracting and injecting', () => { - const { persistableFilters, references } = extractFilterReferences(filters); - expect(injectFilterReferences(persistableFilters, references)).toEqual(filters); - }); - - it('should ignore other references', () => { - const { persistableFilters, references } = extractFilterReferences(filters); - expect( - injectFilterReferences(persistableFilters, [ - { type: 'index-pattern', id: '1234', name: 'some other index pattern' }, - ...references, - ]) - ).toEqual(filters); - }); - - it('should inject other ids if references change', () => { - const { persistableFilters, references } = extractFilterReferences(filters); - - expect( - injectFilterReferences( - persistableFilters, - references.map((reference, index) => ({ ...reference, id: `overwritten-id-${index}` })) - ) - ).toEqual([ - { - ...filters[0], - meta: { - ...filters[0].meta, - index: 'overwritten-id-0', - }, - }, - { - ...filters[1], - meta: { - ...filters[1].meta, - index: 'overwritten-id-1', - }, - }, - ]); - }); -}); diff --git a/x-pack/plugins/lens/public/persistence/filter_references.ts b/x-pack/plugins/lens/public/persistence/filter_references.ts deleted file mode 100644 index d080e2157c3d3..0000000000000 --- a/x-pack/plugins/lens/public/persistence/filter_references.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Filter } from '@kbn/es-query'; -import { SavedObjectReference } from 'kibana/public'; -import { PersistableFilter } from '../../common'; - -export function extractFilterReferences(filters: Filter[]): { - persistableFilters: PersistableFilter[]; - references: SavedObjectReference[]; -} { - const references: SavedObjectReference[] = []; - const persistableFilters = filters.map((filterRow, i) => { - if (!filterRow.meta || !filterRow.meta.index) { - return filterRow; - } - const refName = `filter-index-pattern-${i}`; - references.push({ - name: refName, - type: 'index-pattern', - id: filterRow.meta.index, - }); - const newFilter = { - ...filterRow, - meta: { - ...filterRow.meta, - indexRefName: refName, - }, - }; - // remove index because it's specified by indexRefName - delete newFilter.meta.index; - // remove value because it can't be persisted - delete newFilter.meta.value; - return newFilter; - }); - - return { persistableFilters, references }; -} - -export function injectFilterReferences( - filters: PersistableFilter[], - references: SavedObjectReference[] -) { - return filters.map((filterRow) => { - if (!filterRow.meta || !filterRow.meta.indexRefName) { - return filterRow as Filter; - } - const { indexRefName, ...metaRest } = filterRow.meta; - const reference = references.find((ref) => ref.name === indexRefName); - if (!reference) { - throw new Error(`Could not find reference for ${indexRefName}`); - } - return { - ...filterRow, - meta: { ...metaRest, index: reference.id }, - }; - }); -} diff --git a/x-pack/plugins/lens/public/persistence/index.ts b/x-pack/plugins/lens/public/persistence/index.ts index 0fd3388ef416a..3fe987b78fbcc 100644 --- a/x-pack/plugins/lens/public/persistence/index.ts +++ b/x-pack/plugins/lens/public/persistence/index.ts @@ -6,5 +6,4 @@ */ export * from './saved_object_store'; -export * from './filter_references'; export { checkForDuplicateTitle } from './saved_objects_utils'; diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts index bec604a0f7cfa..769f50f901243 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { Filter } from '@kbn/es-query'; import { SavedObjectAttributes, SavedObjectsClientContract, @@ -12,7 +13,7 @@ import { ResolvedSimpleSavedObject, } from 'kibana/public'; import { Query } from '../../../../../src/plugins/data/public'; -import { DOC_TYPE, PersistableFilter } from '../../common'; +import { DOC_TYPE } from '../../common'; import { LensSavedObjectAttributes } from '../async_services'; export interface Document { @@ -29,7 +30,7 @@ export interface Document { activePaletteId: string; state?: unknown; }; - filters: PersistableFilter[]; + filters: Filter[]; }; references: SavedObjectReference[]; } diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index ecf237ac1327d..decd9d8c69510 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 { @@ -212,6 +220,7 @@ export class LensPlugin { timefilter: plugins.data.query.timefilter.timefilter, expressionRenderer: plugins.expressions.ReactExpressionRenderer, documentToExpression: this.editorFrameService!.documentToExpression, + injectFilterReferences: data.query.filterManager.inject, visualizationMap, indexPatternService: plugins.data.indexPatterns, uiActions: plugins.uiActions, @@ -386,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/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap b/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap index efde7184ac731..7f70508dc423f 100644 --- a/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap +++ b/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap @@ -37,6 +37,9 @@ Object { }, "filters": Array [ Object { + "meta": Object { + "index": "index-pattern-0", + }, "query": Object { "match_phrase": Object { "src": "test", diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index 11fd4c9061271..372d08017ee2a 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -16,7 +16,7 @@ import { getInitialDatasourceId } from '../../utils'; import { initializeDatasources } from '../../editor_frame_service/editor_frame'; import { LensAppServices } from '../../app_plugin/types'; import { getEditPath, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../../common/constants'; -import { Document, injectFilterReferences } from '../../persistence'; +import { Document } from '../../persistence'; export const getPersisted = async ({ initialInput, @@ -162,7 +162,7 @@ export function loadInitial( {} ); - const filters = injectFilterReferences(doc.state.filters, doc.references); + const filters = data.query.filterManager.inject(doc.state.filters, doc.references); // Don't overwrite any pinned filters data.query.filterManager.setAppFilters(filters); diff --git a/x-pack/plugins/lens/public/state_management/load_initial.test.tsx b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx index 1143bf8f7561e..1015838e11674 100644 --- a/x-pack/plugins/lens/public/state_management/load_initial.test.tsx +++ b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx @@ -224,7 +224,7 @@ describe('Initializing the store', () => { }); expect(deps.lensServices.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([ - { query: { match_phrase: { src: 'test' } } }, + { query: { match_phrase: { src: 'test' } }, meta: { index: 'injected!' } }, ]); expect(store.getState()).toEqual({ diff --git a/x-pack/plugins/lens/public/state_management/selectors.ts b/x-pack/plugins/lens/public/state_management/selectors.ts index 4b201e35e5cf7..250e9dde31373 100644 --- a/x-pack/plugins/lens/public/state_management/selectors.ts +++ b/x-pack/plugins/lens/public/state_management/selectors.ts @@ -7,8 +7,8 @@ import { createSelector } from '@reduxjs/toolkit'; import { SavedObjectReference } from 'kibana/server'; +import { FilterManager } from 'src/plugins/data/public'; import { LensState } from './types'; -import { extractFilterReferences } from '../persistence'; import { Datasource, DatasourceMap, VisualizationMap } from '../types'; import { getDatasourceLayers } from '../editor_frame_service/editor_frame'; @@ -43,13 +43,10 @@ export const selectExecutionContextSearch = createSelector(selectExecutionContex filters: res.filters, })); -const selectDatasourceMap = (state: LensState, datasourceMap: DatasourceMap) => datasourceMap; +const selectInjectedDependencies = (_state: LensState, dependencies: unknown) => dependencies; -const selectVisualizationMap = ( - state: LensState, - datasourceMap: DatasourceMap, - visualizationMap: VisualizationMap -) => visualizationMap; +// use this type to cast selectInjectedDependencies to require whatever outside dependencies the selector needs +type SelectInjectedDependenciesFunction = (state: LensState, dependencies: T) => T; export const selectSavedObjectFormat = createSelector( [ @@ -59,8 +56,11 @@ export const selectSavedObjectFormat = createSelector( selectQuery, selectFilters, selectActiveDatasourceId, - selectDatasourceMap, - selectVisualizationMap, + selectInjectedDependencies as SelectInjectedDependenciesFunction<{ + datasourceMap: DatasourceMap; + visualizationMap: VisualizationMap; + extractFilterReferences: FilterManager['extract']; + }>, ], ( persistedDoc, @@ -69,8 +69,7 @@ export const selectSavedObjectFormat = createSelector( query, filters, activeDatasourceId, - datasourceMap, - visualizationMap + { datasourceMap, visualizationMap, extractFilterReferences } ) => { const activeVisualization = visualization.state && visualization.activeId && visualizationMap[visualization.activeId]; @@ -101,7 +100,8 @@ export const selectSavedObjectFormat = createSelector( references.push(...savedObjectReferences); }); - const { persistableFilters, references: filterReferences } = extractFilterReferences(filters); + const { state: persistableFilters, references: filterReferences } = + extractFilterReferences(filters); references.push(...filterReferences); @@ -140,12 +140,19 @@ export const selectAreDatasourcesLoaded = createSelector( ); export const selectDatasourceLayers = createSelector( - [selectDatasourceStates, selectDatasourceMap], + [ + selectDatasourceStates, + selectInjectedDependencies as SelectInjectedDependenciesFunction, + ], (datasourceStates, datasourceMap) => getDatasourceLayers(datasourceStates, datasourceMap) ); export const selectFramePublicAPI = createSelector( - [selectDatasourceStates, selectActiveData, selectDatasourceMap], + [ + selectDatasourceStates, + selectActiveData, + selectInjectedDependencies as SelectInjectedDependenciesFunction, + ], (datasourceStates, activeData, datasourceMap) => { return { datasourceLayers: getDatasourceLayers(datasourceStates, datasourceMap), diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 397878675536d..7ea5ff232607a 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -289,6 +289,15 @@ export interface Datasource { * Given the current state layer and a columnId will verify if the column configuration has errors */ isValidColumn: (state: T, layerId: string, columnId: string) => boolean; + /** + * Are these datasources equivalent? + */ + isEqual: ( + persistableState1: P, + references1: SavedObjectReference[], + persistableState2: P, + references2: SavedObjectReference[] + ) => boolean; } export interface DatasourceFixAction { diff --git a/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.test.ts b/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.test.ts deleted file mode 100644 index 9ce405804bde1..0000000000000 --- a/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import semverGte from 'semver/functions/gte'; -import { lensEmbeddableFactory } from './lens_embeddable_factory'; -import { migrations } from '../migrations/saved_object_migrations'; - -describe('saved object migrations and embeddable migrations', () => { - test('should have same versions registered (>7.13.0)', () => { - const savedObjectMigrationVersions = Object.keys(migrations).filter((version) => { - return semverGte(version, '7.13.1'); - }); - const embeddableMigrationVersions = lensEmbeddableFactory()?.migrations; - if (embeddableMigrationVersions) { - expect(savedObjectMigrationVersions.sort()).toEqual( - Object.keys(embeddableMigrationVersions).sort() - ); - } - }); -}); diff --git a/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts b/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts deleted file mode 100644 index 0e79e342d4427..0000000000000 --- a/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EmbeddableRegistryDefinition } from 'src/plugins/embeddable/server'; -import type { SerializableRecord } from '@kbn/utility-types'; -import { DOC_TYPE } from '../../common'; -import { - commonMakeReversePaletteAsCustom, - commonRemoveTimezoneDateHistogramParam, - commonRenameOperationsForFormula, - commonUpdateVisLayerType, -} from '../migrations/common_migrations'; -import { - LensDocShape713, - LensDocShape715, - LensDocShapePre712, - VisState716, - VisStatePre715, -} from '../migrations/types'; -import { extract, inject } from '../../common/embeddable_factory'; - -export const lensEmbeddableFactory = (): EmbeddableRegistryDefinition => { - return { - id: DOC_TYPE, - migrations: { - // This migration is run in 7.13.1 for `by value` panels because the 7.13 release window was missed. - '7.13.1': (state) => { - const lensState = state as unknown as { attributes: LensDocShapePre712 }; - const migratedLensState = commonRenameOperationsForFormula(lensState.attributes); - return { - ...lensState, - attributes: migratedLensState, - } as unknown as SerializableRecord; - }, - '7.14.0': (state) => { - const lensState = state as unknown as { attributes: LensDocShape713 }; - const migratedLensState = commonRemoveTimezoneDateHistogramParam(lensState.attributes); - return { - ...lensState, - attributes: migratedLensState, - } as unknown as SerializableRecord; - }, - '7.15.0': (state) => { - const lensState = state as unknown as { attributes: LensDocShape715 }; - const migratedLensState = commonUpdateVisLayerType(lensState.attributes); - return { - ...lensState, - attributes: migratedLensState, - } as unknown as SerializableRecord; - }, - '7.16.0': (state) => { - const lensState = state as unknown as { attributes: LensDocShape715 }; - const migratedLensState = commonMakeReversePaletteAsCustom(lensState.attributes); - return { - ...lensState, - attributes: migratedLensState, - } as unknown as SerializableRecord; - }, - }, - extract, - inject, - }; -}; diff --git a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.test.ts b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.test.ts new file mode 100644 index 0000000000000..5f4c69593d270 --- /dev/null +++ b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.test.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import semverGte from 'semver/functions/gte'; +import { makeLensEmbeddableFactory } from './make_lens_embeddable_factory'; +import { getAllMigrations } from '../migrations/saved_object_migrations'; +import { Filter } from '@kbn/es-query'; + +describe('embeddable migrations', () => { + test('should have all saved object migrations versions (>7.13.0)', () => { + const savedObjectMigrationVersions = Object.keys(getAllMigrations({})).filter((version) => { + return semverGte(version, '7.13.1'); + }); + const embeddableMigrationVersions = makeLensEmbeddableFactory({})()?.migrations; + if (embeddableMigrationVersions) { + expect(savedObjectMigrationVersions.sort()).toEqual( + Object.keys(embeddableMigrationVersions).sort() + ); + } + }); + + test('should properly apply a filter migration within a lens visualization', () => { + const migrationVersion = 'some-version'; + + const lensVisualizationDoc = { + attributes: { + state: { + filters: [ + { + filter: 1, + migrated: false, + }, + { + filter: 2, + migrated: false, + }, + ], + }, + }, + }; + + const embeddableMigrationVersions = makeLensEmbeddableFactory({ + [migrationVersion]: (filters: Filter[]) => { + return filters.map((filterState) => ({ + ...filterState, + migrated: true, + })); + }, + })()?.migrations; + + const migratedLensDoc = embeddableMigrationVersions?.[migrationVersion](lensVisualizationDoc); + + expect(migratedLensDoc).toEqual({ + attributes: { + state: { + filters: [ + { + filter: 1, + migrated: true, + }, + { + filter: 2, + migrated: true, + }, + ], + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts new file mode 100644 index 0000000000000..f516a99b078f2 --- /dev/null +++ b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts @@ -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 { EmbeddableRegistryDefinition } from 'src/plugins/embeddable/server'; +import type { SerializableRecord } from '@kbn/utility-types'; +import { + mergeMigrationFunctionMaps, + MigrateFunctionsObject, +} from '../../../../../src/plugins/kibana_utils/common'; +import { DOC_TYPE } from '../../common'; +import { + commonMakeReversePaletteAsCustom, + commonRemoveTimezoneDateHistogramParam, + commonRenameFilterReferences, + commonRenameOperationsForFormula, + commonUpdateVisLayerType, + getLensFilterMigrations, +} from '../migrations/common_migrations'; +import { + LensDocShape713, + LensDocShape715, + LensDocShapePre712, + VisState716, + VisStatePre715, +} from '../migrations/types'; +import { extract, inject } from '../../common/embeddable_factory'; + +export const makeLensEmbeddableFactory = + (filterMigrations: MigrateFunctionsObject) => (): EmbeddableRegistryDefinition => { + return { + id: DOC_TYPE, + migrations: mergeMigrationFunctionMaps(getLensFilterMigrations(filterMigrations), { + // This migration is run in 7.13.1 for `by value` panels because the 7.13 release window was missed. + '7.13.1': (state) => { + const lensState = state as unknown as { attributes: LensDocShapePre712 }; + const migratedLensState = commonRenameOperationsForFormula(lensState.attributes); + return { + ...lensState, + attributes: migratedLensState, + } as unknown as SerializableRecord; + }, + '7.14.0': (state) => { + const lensState = state as unknown as { attributes: LensDocShape713 }; + const migratedLensState = commonRemoveTimezoneDateHistogramParam(lensState.attributes); + return { + ...lensState, + attributes: migratedLensState, + } as unknown as SerializableRecord; + }, + '7.15.0': (state) => { + const lensState = state as unknown as { attributes: LensDocShape715 }; + const migratedLensState = commonUpdateVisLayerType(lensState.attributes); + return { + ...lensState, + attributes: migratedLensState, + } as unknown as SerializableRecord; + }, + '7.16.0': (state) => { + const lensState = state as unknown as { attributes: LensDocShape715 }; + const migratedLensState = commonMakeReversePaletteAsCustom(lensState.attributes); + return { + ...lensState, + attributes: migratedLensState, + } as unknown as SerializableRecord; + }, + '8.1.0': (state) => { + const lensState = state as unknown as { attributes: LensDocShape715 }; + const migratedLensState = commonRenameFilterReferences(lensState.attributes); + return { + ...lensState, + attributes: migratedLensState, + } as unknown as SerializableRecord; + }, + }), + extract, + inject, + }; + }; diff --git a/x-pack/plugins/lens/server/migrations/common_migrations.test.ts b/x-pack/plugins/lens/server/migrations/common_migrations.test.ts new file mode 100644 index 0000000000000..55c7bc641a04e --- /dev/null +++ b/x-pack/plugins/lens/server/migrations/common_migrations.test.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 { Filter } from '@kbn/es-query'; +import { getLensFilterMigrations } from './common_migrations'; + +describe('Lens migrations', () => { + describe('applying filter migrations', () => { + it('creates a filter migrations map that works on a lens visualization', () => { + const filterMigrations = { + '1.1': (filters: Filter[]) => filters.map((filter) => ({ ...filter, version: '1.1' })), + '2.2': (filters: Filter[]) => filters.map((filter) => ({ ...filter, version: '2.2' })), + '3.3': (filters: Filter[]) => filters.map((filter) => ({ ...filter, version: '3.3' })), + }; + + const lensVisualizationSavedObject = { + attributes: { + state: { + filters: [{}, {}], + }, + }, + }; + + const migrationMap = getLensFilterMigrations(filterMigrations); + + expect(migrationMap['1.1'](lensVisualizationSavedObject).attributes.state.filters).toEqual([ + { version: '1.1' }, + { version: '1.1' }, + ]); + expect(migrationMap['2.2'](lensVisualizationSavedObject).attributes.state.filters).toEqual([ + { version: '2.2' }, + { version: '2.2' }, + ]); + expect(migrationMap['3.3'](lensVisualizationSavedObject).attributes.state.filters).toEqual([ + { version: '3.3' }, + { version: '3.3' }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/lens/server/migrations/common_migrations.ts b/x-pack/plugins/lens/server/migrations/common_migrations.ts index 290655ec634eb..82cde025e31ed 100644 --- a/x-pack/plugins/lens/server/migrations/common_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/common_migrations.ts @@ -7,6 +7,11 @@ import { cloneDeep } from 'lodash'; import { PaletteOutput } from 'src/plugins/charts/common'; +import { Filter } from '@kbn/es-query'; +import { + MigrateFunction, + MigrateFunctionsObject, +} from '../../../../../src/plugins/kibana_utils/common'; import { LensDocShapePre712, OperationTypePre712, @@ -19,6 +24,7 @@ import { VisState716, } from './types'; import { CustomPaletteParams, layerTypes } from '../../common'; +import { LensDocShape } from './saved_object_migrations'; export const commonRenameOperationsForFormula = ( attributes: LensDocShapePre712 @@ -155,3 +161,40 @@ export const commonMakeReversePaletteAsCustom = ( } return newAttributes; }; + +export const commonRenameFilterReferences = (attributes: LensDocShape715) => { + const newAttributes = cloneDeep(attributes); + for (const filter of newAttributes.state.filters) { + filter.meta.index = filter.meta.indexRefName; + delete filter.meta.indexRefName; + } + return newAttributes; +}; + +const getApplyFilterMigrationToLens = (filterMigration: MigrateFunction) => { + return (savedObject: { attributes: LensDocShape }) => { + return { + ...savedObject, + attributes: { + ...savedObject.attributes, + state: { + ...savedObject.attributes.state, + filters: filterMigration(savedObject.attributes.state.filters as unknown as Filter[]), + }, + }, + }; + }; +}; + +/** + * This creates a migration map that applies filter migrations to Lens visualizations + */ +export const getLensFilterMigrations = (filterMigrations: MigrateFunctionsObject) => { + const migrationMap: MigrateFunctionsObject = {}; + for (const version in filterMigrations) { + if (filterMigrations.hasOwnProperty(version)) { + migrationMap[version] = getApplyFilterMigrationToLens(filterMigrations[version]); + } + } + return migrationMap; +}; diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts index ef67c768a8997..9a6407ae30552 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts @@ -6,7 +6,7 @@ */ import { cloneDeep } from 'lodash'; -import { migrations, LensDocShape } from './saved_object_migrations'; +import { getAllMigrations, LensDocShape } from './saved_object_migrations'; import { SavedObjectMigrationContext, SavedObjectMigrationFn, @@ -15,8 +15,10 @@ import { import { LensDocShape715, VisState716, VisStatePost715, VisStatePre715 } from './types'; import { CustomPaletteParams, layerTypes } from '../../common'; import { PaletteOutput } from 'src/plugins/charts/common'; +import { Filter } from '@kbn/es-query'; describe('Lens migrations', () => { + const migrations = getAllMigrations({}); describe('7.7.0 missing dimensions in XY', () => { const context = {} as SavedObjectMigrationContext; @@ -1404,4 +1406,162 @@ describe('Lens migrations', () => { ]); }); }); + + describe('8.1.0 update filter reference schema', () => { + const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const example = { + type: 'lens', + id: 'mocked-saved-object-id', + attributes: { + savedObjectId: '1', + title: 'MyRenamedOps', + description: '', + visualizationType: null, + state: { + datasourceMetaData: { + filterableIndexPatterns: [], + }, + datasourceStates: { + indexpattern: { + currentIndexPatternId: 'logstash-*', + layers: { + '2': { + columns: { + '3': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto', timeZone: 'Europe/Berlin' }, + }, + '4': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '5': { + label: '@timestamp', + dataType: 'date', + operationType: 'my_unexpected_operation', + isBucketed: true, + scale: 'interval', + params: { timeZone: 'do not delete' }, + }, + }, + columnOrder: ['3', '4', '5'], + }, + }, + }, + }, + visualization: {}, + query: { query: '', language: 'kuery' }, + filters: [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'geo.src', + params: { query: 'US' }, + indexRefName: 'filter-index-pattern-0', + }, + query: { match_phrase: { 'geo.src': 'US' } }, + $state: { store: 'appState' }, + }, + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'client_ip', + params: { query: '1234.5344.2243.3245' }, + indexRefName: 'filter-index-pattern-2', + }, + query: { match_phrase: { client_ip: '1234.5344.2243.3245' } }, + $state: { store: 'appState' }, + }, + ], + }, + }, + } as unknown as SavedObjectUnsanitizedDoc>; + + it('should rename indexRefName to index in filters metadata', () => { + const expectedFilters = example.attributes.state.filters.map((filter) => { + return { + ...filter, + meta: { + ...filter.meta, + index: filter.meta.indexRefName, + indexRefName: undefined, + }, + }; + }); + + const result = migrations['8.1.0'](example, context) as ReturnType< + SavedObjectMigrationFn + >; + + expect(result.attributes.state.filters).toEqual(expectedFilters); + }); + }); + + test('should properly apply a filter migration within a lens visualization', () => { + const migrationVersion = 'some-version'; + + const lensVisualizationDoc = { + attributes: { + state: { + filters: [ + { + filter: 1, + migrated: false, + }, + { + filter: 2, + migrated: false, + }, + ], + }, + }, + }; + + const migrationFunctionsObject = getAllMigrations({ + [migrationVersion]: (filters: Filter[]) => { + return filters.map((filterState) => ({ + ...filterState, + migrated: true, + })); + }, + }); + + const migratedLensDoc = migrationFunctionsObject[migrationVersion]( + lensVisualizationDoc as SavedObjectUnsanitizedDoc, + {} as SavedObjectMigrationContext + ); + + expect(migratedLensDoc).toEqual({ + attributes: { + state: { + filters: [ + { + filter: 1, + migrated: true, + }, + { + filter: 2, + migrated: true, + }, + ], + }, + }, + }); + }); }); diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts index 152ed173f8903..596ddf12ae7c3 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts @@ -5,16 +5,18 @@ * 2.0. */ -import { cloneDeep } from 'lodash'; +import { cloneDeep, mergeWith } from 'lodash'; import { fromExpression, toExpression, Ast, ExpressionFunctionAST } from '@kbn/interpreter'; import { SavedObjectMigrationMap, SavedObjectMigrationFn, SavedObjectReference, SavedObjectUnsanitizedDoc, + SavedObjectMigrationContext, } from 'src/core/server'; import { Filter } from '@kbn/es-query'; import { Query } from 'src/plugins/data/public'; +import { MigrateFunctionsObject } from '../../../../../src/plugins/kibana_utils/common'; import { PersistableFilter } from '../../common'; import { LensDocShapePost712, @@ -31,6 +33,8 @@ import { commonRemoveTimezoneDateHistogramParam, commonUpdateVisLayerType, commonMakeReversePaletteAsCustom, + commonRenameFilterReferences, + getLensFilterMigrations, } from './common_migrations'; interface LensDocShapePre710 { @@ -440,7 +444,15 @@ const moveDefaultReversedPaletteToCustom: SavedObjectMigrationFn< return { ...newDoc, attributes: commonMakeReversePaletteAsCustom(newDoc.attributes) }; }; -export const migrations: SavedObjectMigrationMap = { +const renameFilterReferences: SavedObjectMigrationFn< + LensDocShape715, + LensDocShape715 +> = (doc) => { + const newDoc = cloneDeep(doc); + return { ...newDoc, attributes: commonRenameFilterReferences(newDoc.attributes) }; +}; + +const lensMigrations: SavedObjectMigrationMap = { '7.7.0': removeInvalidAccessors, // The order of these migrations matter, since the timefield migration relies on the aggConfigs // sitting directly on the esaggs as an argument and not a nested function (which lens_auto_date was). @@ -453,4 +465,25 @@ export const migrations: SavedObjectMigrationMap = { '7.14.0': removeTimezoneDateHistogramParam, '7.15.0': addLayerTypeToVisualization, '7.16.0': moveDefaultReversedPaletteToCustom, + '8.1.0': renameFilterReferences, }; + +export const mergeSavedObjectMigrationMaps = ( + obj1: SavedObjectMigrationMap, + obj2: SavedObjectMigrationMap +): SavedObjectMigrationMap => { + const customizer = (objValue: SavedObjectMigrationFn, srcValue: SavedObjectMigrationFn) => { + if (!srcValue || !objValue) { + return srcValue || objValue; + } + return (state: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => + objValue(srcValue(state, context), context); + }; + + return mergeWith({ ...obj1 }, obj2, customizer); +}; + +export const getAllMigrations = ( + filterMigrations: MigrateFunctionsObject +): SavedObjectMigrationMap => + mergeSavedObjectMigrationMaps(lensMigrations, getLensFilterMigrations(filterMigrations)); diff --git a/x-pack/plugins/lens/server/migrations/types.ts b/x-pack/plugins/lens/server/migrations/types.ts index 43a0f5524f619..de643f9234156 100644 --- a/x-pack/plugins/lens/server/migrations/types.ts +++ b/x-pack/plugins/lens/server/migrations/types.ts @@ -8,7 +8,7 @@ import type { PaletteOutput } from 'src/plugins/charts/common'; import { Filter } from '@kbn/es-query'; import { Query } from 'src/plugins/data/public'; -import type { CustomPaletteParams, LayerType } from '../../common'; +import type { CustomPaletteParams, LayerType, PersistableFilter } from '../../common'; export type OperationTypePre712 = | 'avg' @@ -191,10 +191,17 @@ export interface LensDocShape715 { }; visualization: VisualizationState; query: Query; - filters: Filter[]; + filters: PersistableFilter[]; }; } +export type LensDocShape810 = Omit< + LensDocShape715, + 'filters' +> & { + filters: Filter[]; +}; + export type VisState716 = // Datatable | { diff --git a/x-pack/plugins/lens/server/plugin.tsx b/x-pack/plugins/lens/server/plugin.tsx index 42e68c6223b6d..3f0a41efc21c7 100644 --- a/x-pack/plugins/lens/server/plugin.tsx +++ b/x-pack/plugins/lens/server/plugin.tsx @@ -7,7 +7,10 @@ import { Plugin, CoreSetup, CoreStart, PluginInitializerContext, Logger } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { PluginStart as DataPluginStart } from 'src/plugins/data/server'; +import { + PluginStart as DataPluginStart, + PluginSetup as DataPluginSetup, +} from 'src/plugins/data/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { FieldFormatsStart } from 'src/plugins/field_formats/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; @@ -19,14 +22,15 @@ import { } from './usage'; import { setupSavedObjects } from './saved_objects'; import { EmbeddableSetup } from '../../../../src/plugins/embeddable/server'; -import { lensEmbeddableFactory } from './embeddable/lens_embeddable_factory'; import { setupExpressions } from './expressions'; +import { makeLensEmbeddableFactory } from './embeddable/make_lens_embeddable_factory'; export interface PluginSetupContract { usageCollection?: UsageCollectionSetup; taskManager?: TaskManagerSetupContract; embeddable: EmbeddableSetup; expressions: ExpressionsServerSetup; + data: DataPluginSetup; } export interface PluginStartContract { @@ -36,7 +40,7 @@ export interface PluginStartContract { } export interface LensServerPluginSetup { - lensEmbeddableFactory: typeof lensEmbeddableFactory; + lensEmbeddableFactory: ReturnType; } export class LensServerPlugin implements Plugin { @@ -47,7 +51,8 @@ export class LensServerPlugin implements Plugin, plugins: PluginSetupContract) { - setupSavedObjects(core); + const filterMigrations = plugins.data.query.filterManager.getAllMigrations(); + setupSavedObjects(core, filterMigrations); setupRoutes(core, this.initializerContext.logger.get()); setupExpressions(core, plugins.expressions); @@ -61,6 +66,7 @@ export class LensServerPlugin implements Plugin { + 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 251af230d5278..6ab8a3d9a2f56 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 0b9558a393e78..3bf64d08fc845 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 d6376470ae71f..85fae39a05910 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 a69b09fffd9ee..2e023f7c588d3 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 f055683722deb..79575ea815124 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 ff899101ced49..8fe8f1b3a155f 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 26afa65b9527c..27d911cc8feb9 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 a184ae7b7ce56..e075a615d5867 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 b415b7a167c5a..5e67a83811561 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 85932658383de..18d459ddbcb78 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 c35b8677c1093..e3522d39e892d 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 b5aeb28715aef..82fb1c502ef6a 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 3e5af637dc336..7c01fed158b0d 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 ec69989a8313d..0f3475eeae9ee 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 bf6ff368594cc..f123ed7c78054 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 19f31d481f58e..2f79b8d0984d0 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 82aab592a1344..7c137419f4a19 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 a264ae36d88af..6b0e212357436 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 40ee17d176706..a3b6638d5ee8b 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 31066204cd318..b11d7270fe13e 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 6da8076e22332..22374a5533fab 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 0000000000000..947b87e9976d5 --- /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 0000000000000..22c0f45f2f239 --- /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 eb9705f3438aa..9a3fb9b29d09b 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 d04f8f7b648f5..3f64ff794d9ab 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 f0af666e07dbc..44f6fc5e604cb 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 8f0d344a36f36..7e14639f1b8b4 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 a6a707634811d..c370778b178c8 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 e608bfeb622d8..128517777bb46 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 59af548f267fd..1fe8d3e1f4d7b 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 13fbd618a0a09..f52e00b4432c0 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 fe399545cf6e0..ee250624432a6 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 4185d1a4c985f..1e8eb94df4836 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 fcc3fdd64c36c..03860fd3cd122 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 9a45dbcbdbd64..e502cf7fb37e0 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 0000000000000..82e2e3f9b8f7a --- /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 0000000000000..1d420cf948208 --- /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 2f9dd45dae620..b29c4e28e731d 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 7b117b7cd5ff3..4f9fb4304fd28 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 0000000000000..689450d8838ee --- /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 dde93b391d12b..7006e0a0b7627 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 e1b0eec698593..a9524a509c0a1 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 02af440ba0e6a..99f1dac6208ee 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 146083e279d6a..bfeb5adc11f6e 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 66f9435035571..a0f3744f992b8 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 e47b4c792b1e8..673f2091760a6 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 213f949ee84ed..4e7bfc63c35ac 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 f1da34cd0fbad..7b1505eecd698 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 8fa680a5899a2..3218c792772ba 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 95b96ca454610..7244c2417151b 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 21866566cb7e3..def52bf511215 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 171530a77299f..0000000000000 --- 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 1a2c9f370bc31..0f586d10fb6a0 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 ce9415986ba02..504626445450d 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 e4b6ef14eb1e9..0d3396d7331a1 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 171530a77299f..0000000000000 --- 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 dfa23247045ef..61ba6a3340bdb 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 ae872d3c1ed52..4951b3c9d8fd1 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 a4fee25dfcd9a..105518537384f 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 39cbbcc890777..71a67ef1f623a 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 4163861166acf..6821217c30cbf 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 adac59211dee3..4f9efbe839ffd 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 92660943b1170..6bef0ee38c24e 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 9be63b33394ad..0b44739a7f2ed 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 0000000000000..73b6435fd8f33 --- /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 4bcc9d9ebf2a1..4da470270de76 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 39975cb65ce2b..1b7b87fe180bf 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 2336a1de1d4a0..bf2186c1a3e50 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 df9829407043c..2bfe75e2833aa 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 @@ -157,7 +157,7 @@ function getLensAttributes( { $state: { store: FilterStateStore.APP_STATE }, meta: { - indexRefName: 'filter-index-pattern-0', + index: 'filter-index-pattern-0', negate: false, alias: null, disabled: false, @@ -180,7 +180,7 @@ function getLensAttributes( meta: { alias: 'agent IDs', disabled: false, - indexRefName: 'filter-index-pattern-0', + index: 'filter-index-pattern-0', key: 'query', negate: false, type: 'custom', @@ -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 9bea07b7c234c..f8599cc1fc51e 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 77acd3d0239cf..6cbf4dc84635e 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 fce37ec495faa..30cae97b006bb 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 171530a77299f..0000000000000 --- 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 461255533c380..87d71bc3d17c8 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 bc7673dd0ffbd..00c27f11c12aa 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 0bb162173adc1..f16e32a62cb4f 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 3fd2275477ebf..b3e0cab60851e 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 7bc54b44de775..3861784120e0c 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 0000000000000..87d48c95648bb --- /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 77dfde5800c1e..8e887a012dab3 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 21cfd0bd43772..7431050996deb 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 a4fe1835ce5f7..268ac144d2b58 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 380857f1bffd2..49e693fc8e87e 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 77c8489bb8992..466ac2decefa1 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 6a5dbf970e0b4..1a16804411c44 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 b264c96361122..23ecb01eddcf9 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 3cc8cbacc7921..b898a93c28880 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 e48983634efd8..a29e709b4442d 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 7afeedd3d2832..e179d847d9526 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 942ebbea47881..32285772d0e23 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 1917f3f68b5a3..d283dd2a5e800 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 1cbdfa36e342d..67d7d0c4a0c08 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 f80c7d9b2accd..3ec735083d7cc 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 b1e4a398cfd09..6cbe7f27fa279 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 9570c82f23a8a..1f6d7bcff5176 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 667c85c24a35d..df0a182075341 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 e23a1d555fdba..e558448950c79 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 4ffdaa80577be..9543039ab576a 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 06eb0d064b89e..a36d7caab2ecf 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 4de1160e53936..c3e589447313a 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 e1208c7c54a3b..d764a12f951d8 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 7eedc99652f80..829f98b4a537d 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 e5027ee8b4f3a..9bba5e4b555dc 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 cfcf5307de8d4..2d6ca63c82bdb 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 7d3dc9641929a..f4707272d2c24 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 924601758c730..ad3b11a74e81d 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 1ba95cc2a2951..d64864a699a60 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 7c8b2ddf636c0..de9da5c293fc1 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 685d893bf4a73..05912e764af2c 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 0000000000000..7c59e53dbaeae --- /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 6283fa86b1026..0510e6d0b331c 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 047b4ca1efd50..cc1104127871f 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 9b22afc300466..032ea7bf190bf 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 4732e28c7f828..ddd7349fadc51 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 11cc9eef7caf1..e48d4f8fb4d21 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 094c964576f7e..5833c12be879f 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 2cc4bc7b8bd71..899968a8277db 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 082ec3c6fa765..f4b4388086012 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 b8b82737ea58c..b206dd708329e 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 18a77172ab48a..8a1bc3fa2128f 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 4d53682d2d669..ac944371acdda 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 5b4138480fdf3..850a303654c52 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 d93ebc47adc6d..a470d4b63e7bd 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 711df5b82079a..286047d804ebf 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 ab0bbaa875a39..7443e4b0d12a9 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 7990fb66cd783..6b3f59f44ce12 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 0096b152b3236..dba9f99681a0c 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 d1741c399dbab..a32e77a107a43 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/endpoint/lib/metadata/check_metadata_transforms_task.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.test.ts index 91bb5c775b74e..5dcdacd0921a8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.test.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import type { TransportResult } from '@elastic/elasticsearch'; import { TransformGetTransformStatsResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { @@ -22,6 +23,15 @@ import { ElasticsearchClientMock } from '../../../../../../../src/core/server/el import { TRANSFORM_STATES } from '../../../../common/constants'; import { METADATA_TRANSFORMS_PATTERN } from '../../../../common/endpoint/constants'; import { RunResult } from '../../../../../task_manager/server/task'; +import { + ElasticsearchAssetType, + EsAssetReference, + Installation, +} from '../../../../../fleet/common'; + +import type { EndpointAppContext } from '../../types'; +import type { PackagePolicy } from '../../../../../fleet/common/types/models/package_policy'; +import type { PackageClient } from '../../../../../fleet/server'; const MOCK_TASK_INSTANCE = { id: `${TYPE}:${VERSION}`, @@ -46,14 +56,28 @@ describe('check metadata transforms task', () => { let mockTask: CheckMetadataTransformsTask; let mockCore: CoreSetup; let mockTaskManagerSetup: jest.Mocked; - beforeAll(() => { + let mockEndpointAppContext: EndpointAppContext; + beforeEach(() => { mockCore = coreSetupMock(); mockTaskManagerSetup = tmSetupMock(); + mockEndpointAppContext = createMockEndpointAppContext(); mockTask = new CheckMetadataTransformsTask({ - endpointAppContext: createMockEndpointAppContext(), + endpointAppContext: mockEndpointAppContext, core: mockCore, taskManager: mockTaskManagerSetup, }); + jest + .spyOn(mockEndpointAppContext.service.getInternalFleetServices().packages, 'getInstallation') + .mockResolvedValue({ + installed_es: [ + { type: ElasticsearchAssetType.transform } as EsAssetReference, + { type: ElasticsearchAssetType.transform } as EsAssetReference, + ], + } as Installation); + }); + + afterEach(() => { + jest.clearAllMocks(); }); describe('task lifecycle', () => { @@ -104,147 +128,305 @@ describe('check metadata transforms task', () => { }, } as unknown as TransportResult); - it('should stop task if transform stats response fails', async () => { - esClient.transform.getTransformStats.mockRejectedValue({}); - await runTask(); - expect(esClient.transform.getTransformStats).toHaveBeenCalledWith({ - transform_id: METADATA_TRANSFORMS_PATTERN, + describe('transforms restart', () => { + it('should stop task if transform stats response fails', async () => { + esClient.transform.getTransformStats.mockRejectedValue({}); + await runTask(); + expect(esClient.transform.getTransformStats).toHaveBeenCalledWith({ + transform_id: METADATA_TRANSFORMS_PATTERN, + }); + expect(esClient.transform.stopTransform).not.toHaveBeenCalled(); + expect(esClient.transform.startTransform).not.toHaveBeenCalled(); }); - expect(esClient.transform.stopTransform).not.toHaveBeenCalled(); - expect(esClient.transform.startTransform).not.toHaveBeenCalled(); - }); - it('should attempt transform restart if failing state', async () => { - const transformStatsResponseMock = buildFailedStatsResponse(); - esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock); + it('should attempt transform restart if failing state', async () => { + const transformStatsResponseMock = buildFailedStatsResponse(); + esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock); - const taskResponse = (await runTask()) as RunResult; + const taskResponse = (await runTask()) as RunResult; - expect(esClient.transform.getTransformStats).toHaveBeenCalledWith({ - transform_id: METADATA_TRANSFORMS_PATTERN, + expect(esClient.transform.getTransformStats).toHaveBeenCalledWith({ + transform_id: METADATA_TRANSFORMS_PATTERN, + }); + expect(esClient.transform.stopTransform).toHaveBeenCalledWith({ + transform_id: failedTransformId, + allow_no_match: true, + wait_for_completion: true, + force: true, + }); + expect(esClient.transform.startTransform).toHaveBeenCalledWith({ + transform_id: failedTransformId, + }); + expect(taskResponse?.state?.restartAttempts).toEqual({ + [goodTransformId]: 0, + [failedTransformId]: 0, + }); }); - expect(esClient.transform.stopTransform).toHaveBeenCalledWith({ - transform_id: failedTransformId, - allow_no_match: true, - wait_for_completion: true, - force: true, - }); - expect(esClient.transform.startTransform).toHaveBeenCalledWith({ - transform_id: failedTransformId, + + it('should correctly track transform restart attempts', async () => { + const transformStatsResponseMock = buildFailedStatsResponse(); + esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock); + + esClient.transform.stopTransform.mockRejectedValueOnce({}); + let taskResponse = (await runTask()) as RunResult; + expect(taskResponse?.state?.restartAttempts).toEqual({ + [goodTransformId]: 0, + [failedTransformId]: 1, + }); + + esClient.transform.startTransform.mockRejectedValueOnce({}); + taskResponse = (await runTask({ + ...MOCK_TASK_INSTANCE, + state: taskResponse.state, + })) as RunResult; + expect(taskResponse?.state?.restartAttempts).toEqual({ + [goodTransformId]: 0, + [failedTransformId]: 2, + }); + + taskResponse = (await runTask({ + ...MOCK_TASK_INSTANCE, + state: taskResponse.state, + })) as RunResult; + expect(taskResponse?.state?.restartAttempts).toEqual({ + [goodTransformId]: 0, + [failedTransformId]: 0, + }); }); - expect(taskResponse?.state?.attempts).toEqual({ - [goodTransformId]: 0, - [failedTransformId]: 0, + + it('should correctly back off subsequent restart attempts', async () => { + let transformStatsResponseMock = buildFailedStatsResponse(); + esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock); + + esClient.transform.stopTransform.mockRejectedValueOnce({}); + let taskStartedAt = new Date(); + let taskResponse = (await runTask()) as RunResult; + let delay = BASE_NEXT_ATTEMPT_DELAY * 60000; + let expectedRunAt = taskStartedAt.getTime() + delay; + expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt); + // we don't have the exact timestamp it uses so give a buffer + let expectedRunAtUpperBound = expectedRunAt + 1000; + expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound); + + esClient.transform.startTransform.mockRejectedValueOnce({}); + taskStartedAt = new Date(); + taskResponse = (await runTask({ + ...MOCK_TASK_INSTANCE, + state: taskResponse.state, + })) as RunResult; + // should be exponential on second+ attempt + delay = BASE_NEXT_ATTEMPT_DELAY ** 2 * 60000; + expectedRunAt = taskStartedAt.getTime() + delay; + expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt); + // we don't have the exact timestamp it uses so give a buffer + expectedRunAtUpperBound = expectedRunAt + 1000; + expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound); + + esClient.transform.stopTransform.mockRejectedValueOnce({}); + taskStartedAt = new Date(); + taskResponse = (await runTask({ + ...MOCK_TASK_INSTANCE, + state: taskResponse.state, + })) as RunResult; + // should be exponential on second+ attempt + delay = BASE_NEXT_ATTEMPT_DELAY ** 3 * 60000; + expectedRunAt = taskStartedAt.getTime() + delay; + expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt); + // we don't have the exact timestamp it uses so give a buffer + expectedRunAtUpperBound = expectedRunAt + 1000; + expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound); + + taskStartedAt = new Date(); + taskResponse = (await runTask({ + ...MOCK_TASK_INSTANCE, + state: taskResponse.state, + })) as RunResult; + // back to base delay after success + delay = BASE_NEXT_ATTEMPT_DELAY * 60000; + expectedRunAt = taskStartedAt.getTime() + delay; + expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt); + // we don't have the exact timestamp it uses so give a buffer + expectedRunAtUpperBound = expectedRunAt + 1000; + expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound); + + transformStatsResponseMock = { + body: { + transforms: [ + { + id: goodTransformId, + state: TRANSFORM_STATES.STARTED, + }, + { + id: failedTransformId, + state: TRANSFORM_STATES.STARTED, + }, + ], + }, + } as unknown as TransportResult; + esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock); + taskResponse = (await runTask({ + ...MOCK_TASK_INSTANCE, + state: taskResponse.state, + })) as RunResult; + // no more explicit runAt after subsequent success + expect(taskResponse?.runAt).toBeUndefined(); }); }); - it('should correctly track transform restart attempts', async () => { - const transformStatsResponseMock = buildFailedStatsResponse(); - esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock); + describe('transforms reinstall', () => { + let getRegistryPackageSpy: jest.SpyInstance; + let reinstallEsAssetsSpy: jest.SpyInstance; + let mockPackageClient: jest.Mocked; + + beforeEach(() => { + jest + .spyOn( + mockEndpointAppContext.service.getEndpointMetadataService(), + 'getAllEndpointPackagePolicies' + ) + .mockResolvedValue([{} as PackagePolicy]); - esClient.transform.stopTransform.mockRejectedValueOnce({}); - let taskResponse = (await runTask()) as RunResult; - expect(taskResponse?.state?.attempts).toEqual({ - [goodTransformId]: 0, - [failedTransformId]: 1, + mockPackageClient = mockEndpointAppContext.service.getInternalFleetServices() + .packages as jest.Mocked; + getRegistryPackageSpy = jest.spyOn(mockPackageClient, 'getRegistryPackage'); + reinstallEsAssetsSpy = jest.spyOn(mockPackageClient, 'reinstallEsAssets'); + + const transformStatsResponseMock = { + body: { + transforms: [ + { + id: 'transform1', + state: TRANSFORM_STATES.STARTED, + }, + ], + count: 1, + }, + } as unknown as TransportResult; + esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock); }); - esClient.transform.startTransform.mockRejectedValueOnce({}); - taskResponse = (await runTask({ - ...MOCK_TASK_INSTANCE, - state: taskResponse.state, - })) as RunResult; - expect(taskResponse?.state?.attempts).toEqual({ - [goodTransformId]: 0, - [failedTransformId]: 2, + it('should reinstall if missing transforms', async () => { + const expectedArgs = { + packageInfo: { name: 'package name' }, + paths: ['some/test/transform/path'], + }; + getRegistryPackageSpy.mockResolvedValue(expectedArgs); + reinstallEsAssetsSpy.mockResolvedValue([{}]); + await runTask(); + + expect(reinstallEsAssetsSpy).toHaveBeenCalledTimes(1); + expect(reinstallEsAssetsSpy).toHaveBeenCalledWith( + expectedArgs.packageInfo, + expectedArgs.paths + ); }); - taskResponse = (await runTask({ - ...MOCK_TASK_INSTANCE, - state: taskResponse.state, - })) as RunResult; - expect(taskResponse?.state?.attempts).toEqual({ - [goodTransformId]: 0, - [failedTransformId]: 0, + it('should correctly track attempts on reinstall', async () => { + reinstallEsAssetsSpy.mockRejectedValueOnce({}).mockRejectedValueOnce({}); + + let taskResponse = (await runTask()) as RunResult; + expect(taskResponse?.state.reinstallAttempts).toEqual(1); + + taskResponse = (await runTask({ + ...MOCK_TASK_INSTANCE, + state: taskResponse.state, + })) as RunResult; + expect(taskResponse?.state.reinstallAttempts).toEqual(2); }); - }); - it('should correctly back off subsequent restart attempts', async () => { - let transformStatsResponseMock = buildFailedStatsResponse(); - esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock); - - esClient.transform.stopTransform.mockRejectedValueOnce({}); - let taskStartedAt = new Date(); - let taskResponse = (await runTask()) as RunResult; - let delay = BASE_NEXT_ATTEMPT_DELAY * 60000; - let expectedRunAt = taskStartedAt.getTime() + delay; - expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt); - // we don't have the exact timestamp it uses so give a buffer - let expectedRunAtUpperBound = expectedRunAt + 1000; - expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound); - - esClient.transform.startTransform.mockRejectedValueOnce({}); - taskStartedAt = new Date(); - taskResponse = (await runTask({ - ...MOCK_TASK_INSTANCE, - state: taskResponse.state, - })) as RunResult; - // should be exponential on second+ attempt - delay = BASE_NEXT_ATTEMPT_DELAY ** 2 * 60000; - expectedRunAt = taskStartedAt.getTime() + delay; - expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt); - // we don't have the exact timestamp it uses so give a buffer - expectedRunAtUpperBound = expectedRunAt + 1000; - expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound); - - esClient.transform.stopTransform.mockRejectedValueOnce({}); - taskStartedAt = new Date(); - taskResponse = (await runTask({ - ...MOCK_TASK_INSTANCE, - state: taskResponse.state, - })) as RunResult; - // should be exponential on second+ attempt - delay = BASE_NEXT_ATTEMPT_DELAY ** 3 * 60000; - expectedRunAt = taskStartedAt.getTime() + delay; - expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt); - // we don't have the exact timestamp it uses so give a buffer - expectedRunAtUpperBound = expectedRunAt + 1000; - expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound); - - taskStartedAt = new Date(); - taskResponse = (await runTask({ - ...MOCK_TASK_INSTANCE, - state: taskResponse.state, - })) as RunResult; - // back to base delay after success - delay = BASE_NEXT_ATTEMPT_DELAY * 60000; - expectedRunAt = taskStartedAt.getTime() + delay; - expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt); - // we don't have the exact timestamp it uses so give a buffer - expectedRunAtUpperBound = expectedRunAt + 1000; - expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound); - - transformStatsResponseMock = { - body: { - transforms: [ - { - id: goodTransformId, - state: TRANSFORM_STATES.STARTED, - }, - { - id: failedTransformId, - state: TRANSFORM_STATES.STARTED, - }, - ], - }, - } as unknown as TransportResult; - esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock); - taskResponse = (await runTask({ - ...MOCK_TASK_INSTANCE, - state: taskResponse.state, - })) as RunResult; - // no more explicit runAt after subsequent success - expect(taskResponse?.runAt).toBeUndefined(); + it('should return correct runAt', async () => { + getRegistryPackageSpy.mockResolvedValue({ + packageInfo: { name: 'package name' }, + paths: ['some/test/transform/path'], + }); + reinstallEsAssetsSpy + .mockRejectedValueOnce([]) + .mockRejectedValueOnce([]) + .mockRejectedValueOnce([]) + .mockResolvedValueOnce([{}]); + + let taskStartedAt = new Date(); + let taskResponse = (await runTask()) as RunResult; + + expect(reinstallEsAssetsSpy).toHaveBeenCalledTimes(1); + + let delay = BASE_NEXT_ATTEMPT_DELAY * 60000; + let expectedRunAt = taskStartedAt.getTime() + delay; + expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt); + // we don't have the exact timestamp it uses so give a buffer + let expectedRunAtUpperBound = expectedRunAt + 1000; + expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound); + + taskStartedAt = new Date(); + taskResponse = (await runTask({ + ...MOCK_TASK_INSTANCE, + state: taskResponse.state, + })) as RunResult; + expect(reinstallEsAssetsSpy).toHaveBeenCalledTimes(2); + + // should be exponential on second+ attempt + delay = BASE_NEXT_ATTEMPT_DELAY ** 2 * 60000; + expectedRunAt = taskStartedAt.getTime() + delay; + expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt); + // we don't have the exact timestamp it uses so give a buffer + expectedRunAtUpperBound = expectedRunAt + 1000; + expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound); + + taskStartedAt = new Date(); + taskResponse = (await runTask({ + ...MOCK_TASK_INSTANCE, + state: taskResponse.state, + })) as RunResult; + expect(reinstallEsAssetsSpy).toHaveBeenCalledTimes(3); + + // should be exponential on second+ attempt + delay = BASE_NEXT_ATTEMPT_DELAY ** 3 * 60000; + expectedRunAt = taskStartedAt.getTime() + delay; + expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt); + // we don't have the exact timestamp it uses so give a buffer + expectedRunAtUpperBound = expectedRunAt + 1000; + expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound); + + taskStartedAt = new Date(); + taskResponse = (await runTask({ + ...MOCK_TASK_INSTANCE, + state: taskResponse.state, + })) as RunResult; + expect(reinstallEsAssetsSpy).toHaveBeenCalledTimes(4); + + // back to base delay after success + delay = BASE_NEXT_ATTEMPT_DELAY * 60000; + expectedRunAt = taskStartedAt.getTime() + delay; + expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt); + // we don't have the exact timestamp it uses so give a buffer + expectedRunAtUpperBound = expectedRunAt + 1000; + expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound); + + const goodTransformStatsResponseMock = { + body: { + transforms: [ + { + id: 'transform1', + state: TRANSFORM_STATES.STARTED, + }, + { + id: 'transform2', + state: TRANSFORM_STATES.STARTED, + }, + ], + count: 2, + }, + } as unknown as TransportResult; + esClient.transform.getTransformStats.mockResolvedValue(goodTransformStatsResponseMock); + taskResponse = (await runTask({ + ...MOCK_TASK_INSTANCE, + state: taskResponse.state, + })) as RunResult; + // same since shouldn't call it when transforms are good + expect(reinstallEsAssetsSpy).toHaveBeenCalledTimes(4); + // no more explicit runAt after subsequent success + expect(taskResponse?.runAt).toBeUndefined(); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.ts b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.ts index ba3974839af77..3d96cab82779a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import type { TransportResult } from '@elastic/elasticsearch'; import { TransformGetTransformStatsResponse, @@ -20,6 +21,7 @@ import { EndpointAppContext } from '../../types'; import { METADATA_TRANSFORMS_PATTERN } from '../../../../common/endpoint/constants'; import { WARNING_TRANSFORM_STATES } from '../../../../common/constants'; import { wrapErrorIfNeeded } from '../../utils'; +import { ElasticsearchAssetType, FLEET_ENDPOINT_PACKAGE } from '../../../../../fleet/common'; const SCOPE = ['securitySolution']; const INTERVAL = '2h'; @@ -40,11 +42,13 @@ export interface CheckMetadataTransformsTaskStartContract { } export class CheckMetadataTransformsTask { + private endpointAppContext: EndpointAppContext; private logger: Logger; private wasStarted: boolean = false; constructor(setupContract: CheckMetadataTransformsTaskSetupContract) { const { endpointAppContext, core, taskManager } = setupContract; + this.endpointAppContext = endpointAppContext; this.logger = endpointAppContext.logFactory.get(this.getTaskId()); taskManager.registerTaskDefinitions({ [TYPE]: { @@ -117,42 +121,60 @@ export class CheckMetadataTransformsTask { return; } - const { transforms } = transformStatsResponse.body; - if (!transforms.length) { - this.logger.info('no endpoint metadata transforms found'); + const packageClient = this.endpointAppContext.service.getInternalFleetServices().packages; + const installation = await packageClient.getInstallation(FLEET_ENDPOINT_PACKAGE); + if (!installation) { + this.logger.info('no endpoint installation found'); return; } + const expectedTransforms = installation.installed_es.filter( + (asset) => asset.type === ElasticsearchAssetType.transform + ); + + const { transforms } = transformStatsResponse.body; + let { reinstallAttempts } = taskInstance.state; + let runAt: Date | undefined; + if (transforms.length !== expectedTransforms.length) { + const { attempts, didAttemptReinstall } = await this.reinstallTransformsIfNeeded( + installation.version, + reinstallAttempts + ); + reinstallAttempts = attempts; + + // after a reinstall attempt next check sooner with exponential backoff + if (didAttemptReinstall) { + runAt = this.getNextRunAt(reinstallAttempts); + } + + return this.buildNextTask({ reinstallAttempts, runAt }); + } let didAttemptRestart: boolean = false; let highestAttempt: number = 0; - const attempts = { ...taskInstance.state.attempts }; + const restartAttempts: Record = { ...taskInstance.state.restartAttempts }; for (const transform of transforms) { - const restartedTransform = await this.restartTransform( + const restartedTransform = await this.restartTransformIfNeeded( esClient, transform, - attempts[transform.id] + restartAttempts[transform.id] ); if (restartedTransform.didAttemptRestart) { didAttemptRestart = true; } - attempts[transform.id] = restartedTransform.attempts; - highestAttempt = Math.max(attempts[transform.id], highestAttempt); + restartAttempts[transform.id] = restartedTransform.attempts; + highestAttempt = Math.max(restartAttempts[transform.id], highestAttempt); } - // after a restart attempt run next check sooner with exponential backoff - let runAt: Date | undefined; + // after a restart attempt next check sooner with exponential backoff if (didAttemptRestart) { - const delay = BASE_NEXT_ATTEMPT_DELAY ** Math.max(highestAttempt, 1) * 60000; - runAt = new Date(new Date().getTime() + delay); + runAt = this.getNextRunAt(highestAttempt); } - const nextState = { attempts }; - const nextTask = runAt ? { state: nextState, runAt } : { state: nextState }; - return nextTask; + return this.buildNextTask({ restartAttempts, runAt }); }; - private restartTransform = async ( + private restartTransformIfNeeded = async ( esClient: ElasticsearchClient, transform: TransformGetTransformStatsTransformStats, currentAttempts: number = 0 @@ -208,7 +230,85 @@ export class CheckMetadataTransformsTask { }; }; + private reinstallTransformsIfNeeded = async (pkgVersion: string, currentAttempts = 0) => { + let attempts = currentAttempts; + let didAttemptReinstall = false; + const endpointPolicies = await this.endpointAppContext.service + .getEndpointMetadataService() + .getAllEndpointPackagePolicies(); + + // endpoint not being used, no need to reinstall transforms + if (!endpointPolicies.length) { + return { attempts, didAttemptReinstall }; + } + + if (attempts > MAX_ATTEMPTS) { + this.logger.info('missing endpoint metadata transforms found, attempting reinstall'); + return { attempts, didAttemptReinstall }; + } + + try { + // endpoint policy exists but transforms don't exist, attempt to reinstall + this.logger.info('missing endpoint transforms found, attempting reinstall'); + + const packageClient = this.endpointAppContext.service.getInternalFleetServices().packages; + + const { packageInfo, paths } = await packageClient.getRegistryPackage( + FLEET_ENDPOINT_PACKAGE, + pkgVersion + ); + const transformPaths = paths.filter(this.isTransformPath); + const reinstalledTransforms = await packageClient.reinstallEsAssets( + packageInfo, + transformPaths + ); + if (reinstalledTransforms.length !== transformPaths.length) { + throw new Error( + 'number of reinstalled transforms does not match the expected number of transforms' + ); + } + + // reset attempts on successful reinstall + attempts = 0; + } catch (e) { + const err = wrapErrorIfNeeded(e); + const errMessage = `failed to reinstall endpoint transforms with error: ${err}`; + this.logger.error(errMessage); + + // restart failed, increment attempt count + attempts = attempts + 1; + } finally { + didAttemptReinstall = true; + } + + return { attempts, didAttemptReinstall }; + }; + private getTaskId = (): string => { return `${TYPE}:${VERSION}`; }; + + private getNextRunAt(attempt = 0) { + const delay = BASE_NEXT_ATTEMPT_DELAY ** Math.max(attempt, 1) * 60000; + return new Date(new Date().getTime() + delay); + } + + private buildNextTask({ + restartAttempts = {}, + reinstallAttempts = 0, + runAt = undefined, + }: { + restartAttempts?: Record; + reinstallAttempts?: number; + runAt?: Date | undefined; + }) { + const nextState = { restartAttempts, reinstallAttempts }; + const nextTask = runAt ? { state: nextState, runAt } : { state: nextState }; + return nextTask; + } + + private isTransformPath(path: string) { + const type = path.split('/')[2]; + return !path.endsWith('/') && type === ElasticsearchAssetType.transform; + } } diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index b697b54994adc..4da108859691f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -42,6 +42,8 @@ import { requestContextFactoryMock } from '../request_context_factory.mock'; import { EndpointMetadataService } from './services/metadata'; import { createFleetAuthzMock } from '../../../fleet/common'; import { createMockClients } from '../lib/detection_engine/routes/__mocks__/request_context'; +import { createEndpointMetadataServiceTestContextMock } from './services/metadata/mocks'; + import type { EndpointAuthz } from '../../common/endpoint/types/authz'; /** @@ -64,6 +66,7 @@ export const createMockEndpointAppContext = ( export const createMockEndpointAppContextService = ( mockManifestManager?: ManifestManager ): jest.Mocked => { + const mockEndpointMetadataContext = createEndpointMetadataServiceTestContextMock(); return { start: jest.fn(), stop: jest.fn(), @@ -71,6 +74,8 @@ export const createMockEndpointAppContextService = ( getAgentService: jest.fn(), getAgentPolicyService: jest.fn(), getManifestManager: jest.fn().mockReturnValue(mockManifestManager ?? jest.fn()), + getEndpointMetadataService: jest.fn(() => mockEndpointMetadataContext.endpointMetadataService), + getInternalFleetServices: jest.fn(() => mockEndpointMetadataContext.fleetServices), } as unknown as jest.Mocked; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index 8bc559f53a2bd..4b1ea1fe9efaa 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -29,7 +29,6 @@ import { AgentNotFoundError } from '../../../../../fleet/server'; import { EndpointAppContext, HostListQueryResult } from '../../types'; import { GetMetadataRequestSchema } from './index'; import { findAllUnenrolledAgentIds } from './support/unenroll'; -import { getAllEndpointPackagePolicies } from './support/endpoint_package_policies'; import { findAgentIdsByStatus } from './support/agent_status'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { fleetAgentStatusToEndpointHostStatus } from '../../utils'; @@ -115,10 +114,7 @@ export function getMetadataListRequestHandler( // If no unified Index present, then perform a search using the legacy approach if (!doesUnitedIndexExist || didUnitedIndexError) { - const endpointPolicies = await getAllEndpointPackagePolicies( - fleetServices.packagePolicy, - context.core.savedObjects.client - ); + const endpointPolicies = await endpointMetadataService.getAllEndpointPackagePolicies(); const legacyResponse = await legacyListMetadataQuery( context, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts index 918e8d8003715..bf920420d7a8b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts @@ -22,7 +22,7 @@ import { buildUnitedIndexQuery, } from '../../routes/metadata/query_builders'; import { HostMetadata } from '../../../../common/endpoint/types'; -import { Agent } from '../../../../../fleet/common'; +import { Agent, PackagePolicy } from '../../../../../fleet/common'; import { AgentPolicyServiceInterface } from '../../../../../fleet/server/services'; import { EndpointError } from '../../../../common/endpoint/errors'; @@ -214,4 +214,24 @@ describe('EndpointMetadataService', () => { }); }); }); + + describe('#getAllEndpointPackagePolicies', () => { + it('gets all endpoint package policies', async () => { + const mockPolicy: PackagePolicy = { + id: '1', + policy_id: 'test-id-1', + } as PackagePolicy; + const mockPackagePolicyService = testMockedContext.packagePolicyService; + mockPackagePolicyService.list.mockResolvedValueOnce({ + items: [mockPolicy], + total: 1, + perPage: 10, + page: 1, + }); + + const endpointPackagePolicies = await metadataService.getAllEndpointPackagePolicies(); + const expected: PackagePolicy[] = [mockPolicy]; + expect(endpointPackagePolicies).toEqual(expected); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts index 86f4fd28f506a..5251992a5d3d4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts @@ -409,10 +409,7 @@ export class EndpointMetadataService { fleetServices: EndpointFleetServicesInterface, queryOptions: GetMetadataListRequestQuery ): Promise> { - const endpointPolicies = await getAllEndpointPackagePolicies( - this.packagePolicyService, - this.DANGEROUS_INTERNAL_SO_CLIENT - ); + const endpointPolicies = await this.getAllEndpointPackagePolicies(); const endpointPolicyIds = endpointPolicies.map((policy) => policy.policy_id); const unitedIndexQuery = await buildUnitedIndexQuery(queryOptions, endpointPolicyIds); @@ -483,4 +480,11 @@ export class EndpointMetadataService { total: (docsCount as unknown as SearchTotalHits).value, }; } + + async getAllEndpointPackagePolicies() { + return getAllEndpointPackagePolicies( + this.packagePolicyService, + this.DANGEROUS_INTERNAL_SO_CLIENT + ); + } } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/mocks.ts index 42b0f4f44fdf8..3cd368d35b657 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata/mocks.ts @@ -7,7 +7,10 @@ import { SavedObjectsServiceStart } from 'kibana/server'; import { EndpointMetadataService } from './endpoint_metadata_service'; -import { savedObjectsServiceMock } from '../../../../../../../src/core/server/mocks'; +import { + loggingSystemMock, + savedObjectsServiceMock, +} from '../../../../../../../src/core/server/mocks'; import { createMockAgentPolicyService, createMockAgentService, @@ -17,7 +20,7 @@ import { import { AgentPolicyServiceInterface, AgentService } from '../../../../../fleet/server'; import { EndpointFleetServicesFactory, - EndpointFleetServicesInterface, + EndpointInternalFleetServicesInterface, } from '../endpoint_fleet_services'; const createCustomizedPackagePolicyService = () => { @@ -43,7 +46,8 @@ export interface EndpointMetadataServiceTestContextMock { agentPolicyService: jest.Mocked; packagePolicyService: ReturnType; endpointMetadataService: EndpointMetadataService; - fleetServices: EndpointFleetServicesInterface; + fleetServices: EndpointInternalFleetServicesInterface; + logger: ReturnType['get']>; } export const createEndpointMetadataServiceTestContextMock = ( @@ -53,7 +57,10 @@ export const createEndpointMetadataServiceTestContextMock = ( packagePolicyService: ReturnType< typeof createPackagePolicyServiceMock > = createCustomizedPackagePolicyService(), - packageService: ReturnType = createMockPackageService() + packageService: ReturnType = createMockPackageService(), + logger: ReturnType['get']> = loggingSystemMock + .create() + .get() ): EndpointMetadataServiceTestContextMock => { const fleetServices = new EndpointFleetServicesFactory({ agentService, @@ -65,7 +72,8 @@ export const createEndpointMetadataServiceTestContextMock = ( const endpointMetadataService = new EndpointMetadataService( savedObjectsStart, agentPolicyService, - packagePolicyService + packagePolicyService, + logger ); return { @@ -75,5 +83,6 @@ export const createEndpointMetadataServiceTestContextMock = ( packagePolicyService, endpointMetadataService, fleetServices, + logger, }; }; 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 ac889ebc55cdf..50d553045b5d5 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 13106ec3012be..6842cc5bda230 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 60df18847939b..4efe356525c1e 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/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 0b7285ca3d444..a8e6d49a994c0 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -433,10 +433,11 @@ export class Plugin implements ISecuritySolutionPlugin { this.telemetryReceiver ); - this.checkMetadataTransformsTask?.start({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - taskManager: plugins.taskManager!, - }); + if (plugins.taskManager) { + this.checkMetadataTransformsTask?.start({ + taskManager: plugins.taskManager, + }); + } return {}; } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 901fbbef6714b..36f16182b17e9 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 76eac37d763c9..4ec36c767a5d3 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 38ba7b7b3fd48..e9ea061fcf160 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 0000000000000..d8746d715581d --- /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 0000000000000..ba973a7aa8a61 --- /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 0000000000000..024e8e53c3b2a --- /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 89abed5ce8f29..6bdea1beb016b 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 3188c86f82049..6d2f1dd554108 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 fe4d3ff804bf9..0581692e0e278 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 f3971b6bd4bf3..35be0b19d4521 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 0000000000000..c414778f7769c --- /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 0000000000000..d7faf7b78f797 --- /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 ec6deef429ca9..dd2287b3b1642 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 cc831680dbf09..23f8fc9a8e58c 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 9ff6701d20f7a..b0fe387613f40 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 d352ccef51a94..14d3495cb96e2 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 75c94c2d07d1e..a0785df79bd75 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 82917fc4e1758..9f3da1674ca09 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 66f0f296b1248..58b8e7bb085da 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 8171f7e19865f..6c658ec7f5d40 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 024e387d23547..ffe7c61c7a4e3 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 988cb3ddb9447..18c72d7c35cb1 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 5aa6b7ea7c5a9..5fc99816df006 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 3cf37758b7cec..cd90828f93ccf 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 f028d5e154a56..496f39557adb1 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 d0de73b73e23e..847fcfa9db834 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 736e73da71134..1c55b8812d64f 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 82a901192b0ee..d6fe86453a1c0 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 692607041ea80..4c076db0255ef 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 ecf95c7e9175a..dfd0dcd1a9107 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 faefb71e34f66..47c25bca6f900 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/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index a6fc2e0d0ba92..f23c041b58149 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -91,7 +91,6 @@ const onlyNotInCoverageTests = [ require.resolve('../test/saved_object_tagging/api_integration/security_and_spaces/config.ts'), require.resolve('../test/saved_object_tagging/api_integration/tagging_api/config.ts'), require.resolve('../test/examples/config.ts'), - require.resolve('../test/performance/config.ts'), require.resolve('../test/functional_execution_context/config.ts'), ]; 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 beaaae24fb220..def2e2a4c48e5 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 c8ccea36bd6c3..950e3b23f6a34 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 783a9b60e22a6..eb95b32242e2b 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 a0d33c9ec09e8..885b3cbfbf000 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 e5b97fa2350f1..ce8b319b9d53f 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 bc15ba2f65ca7..0000000000000 --- 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 a2523c6d44244..d42d23b7578a5 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/lens/formula.ts b/x-pack/test/functional/apps/lens/formula.ts index 64cedd7e88e7c..12d7f9cf9036b 100644 --- a/x-pack/test/functional/apps/lens/formula.ts +++ b/x-pack/test/functional/apps/lens/formula.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); + const PageObjects = getPageObjects(['visualize', 'lens', 'common']); const find = getService('find'); const listingTable = getService('listingTable'); const browser = getService('browser'); @@ -32,7 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); await PageObjects.lens.switchToFormula(); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); // .echLegendItem__title is the only viable way of getting the xy chart's // legend item(s), so we're using a class selector here. // 4th item is the other bucket @@ -175,7 +175,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'formula', }); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); expect(await PageObjects.lens.getErrorCount()).to.eql(0); }); @@ -198,7 +198,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { keepOpen: true, }); await PageObjects.lens.setTableDynamicColoring('text'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); const styleObj = await PageObjects.lens.getDatatableCellStyle(1, 1); expect(styleObj['background-color']).to.be(undefined); expect(styleObj.color).not.to.be(undefined); @@ -302,7 +302,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); // check the numbers - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('14,005'); // add an advanced filter by filter @@ -310,7 +310,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.setFilterBy('bytes > 4000'); // check that numbers changed - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); await retry.try(async () => { expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('9,169'); }); @@ -322,7 +322,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await input.type(`bytes > 600000`); // the autocomplete will add quotes and closing brakets, so do not worry about that - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('0'); }); }); diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index b85859bf2d5d3..2e8bf8f4be725 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 0000000000000..11185fd271579 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 0000000000000..d0ddd1c4074d9 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 0000000000000..4e942b1e150ef --- /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 1d5c6d1bf84a3..7a84c41aa4a66 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 3b4251d20e368..b62e4e750518f 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 0000000000000..57136884e8129 --- /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 0000000000000..bb94e49e34b11 --- /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 b2bef848b18e2..634c7ace52735 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 a9889a81d5e4f..1f51df6b841d3 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 0e9f375ee8be1..3d6b78d0ae10a 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 2b570a4d7dae6..a51b878b6af30 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 039f4ff0fbc57..234e19bf90af0 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 15c87ea450425..29faf3cee3b51 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 6f204608df13a..96a6a88f11269 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 aafe96c2c4967..97834bc57c4ab 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 3e9584904c5f8..d776d07f35a75 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 0000000000000..c7ff4f79f3e36 --- /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 db256cc1f23ab..54d5f44d10545 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 434653be49c14..f876e2432931a 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 e40c821d98851..b2e27b30f0079 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 87132dd28ddfb..55b2b68ff8f82 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/performance/config.ts b/x-pack/test/performance/config.playwright.ts similarity index 76% rename from x-pack/test/performance/config.ts rename to x-pack/test/performance/config.playwright.ts index 82586ee62ad80..9cb7be02ed1b2 100644 --- a/x-pack/test/performance/config.ts +++ b/x-pack/test/performance/config.playwright.ts @@ -10,15 +10,17 @@ import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; import { pageObjects } from './page_objects'; -// These "secret" values are intentionally written in the source. We would make the APM server accept annonymous traffic if we could +// These "secret" values are intentionally written in the source. We would make the APM server accept anonymous traffic if we could const APM_SERVER_URL = 'https://2fad4006bf784bb8a54e52f4a5862609.apm.us-west1.gcp.cloud.es.io:443'; const APM_PUBLIC_TOKEN = 'Q5q5rWQEw6tKeirBpw'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const testFiles = [require.resolve('./tests/playwright/home.ts')]; + return { - testFiles: [require.resolve('./tests/index.ts')], + testFiles, services, pageObjects, servers: functionalConfig.get('servers'), @@ -31,21 +33,15 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { kbnTestServer: { ...functionalConfig.get('kbnTestServer'), env: { - ELASTIC_APM_ACTIVE: 'true', + ELASTIC_APM_ACTIVE: process.env.ELASTIC_APM_ACTIVE, ELASTIC_APM_CONTEXT_PROPAGATION_ONLY: 'false', ELASTIC_APM_ENVIRONMENT: process.env.CI ? 'ci' : 'development', ELASTIC_APM_TRANSACTION_SAMPLE_RATE: '1.0', ELASTIC_APM_SERVER_URL: APM_SERVER_URL, ELASTIC_APM_SECRET_TOKEN: APM_PUBLIC_TOKEN, ELASTIC_APM_GLOBAL_LABELS: Object.entries({ - ftrConfig: `x-pack/test/performance`, - jenkinsJobName: process.env.JOB_NAME, - jenkinsBuildNumber: process.env.BUILD_NUMBER, - prId: process.env.PR_NUMBER, - branch: process.env.GIT_BRANCH, - commit: process.env.GIT_COMMIT, - mergeBase: process.env.PR_MERGE_BASE, - targetBranch: process.env.PR_TARGET_BRANCH, + ftrConfig: `x-pack/test/performance/tests/config.playwright`, + performancePhase: process.env.PERF_TEST_PHASE, }) .filter(([, v]) => !!v) .reduce((acc, [k, v]) => (acc ? `${acc},${k}=${v}` : `${k}=${v}`), ''), diff --git a/x-pack/test/performance/tests/reporting_dashboard.ts b/x-pack/test/performance/tests/ftr/reporting_dashboard.ts similarity index 96% rename from x-pack/test/performance/tests/reporting_dashboard.ts rename to x-pack/test/performance/tests/ftr/reporting_dashboard.ts index 93b4010ab27a8..6a696dda5afb9 100644 --- a/x-pack/test/performance/tests/reporting_dashboard.ts +++ b/x-pack/test/performance/tests/ftr/reporting_dashboard.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObject }: FtrProviderContext) { const retry = getService('retry'); diff --git a/x-pack/test/performance/tests/home.ts b/x-pack/test/performance/tests/home.ts deleted file mode 100644 index eda690b9b0a19..0000000000000 --- a/x-pack/test/performance/tests/home.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['common', 'security']); - const testSubjects = getService('testSubjects'); - - describe('Login', () => { - it('login and navigate to homepage', async () => { - await PageObjects.common.navigateToApp('login'); - - await testSubjects.existOrFail('loginSubmit', { timeout: 2000 }); - - await PageObjects.security.login(); - - await testSubjects.existOrFail('homeApp', { timeout: 2000 }); - }); - }); -} diff --git a/x-pack/test/performance/tests/playwright/home.ts b/x-pack/test/performance/tests/playwright/home.ts new file mode 100644 index 0000000000000..2460464c80987 --- /dev/null +++ b/x-pack/test/performance/tests/playwright/home.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import Url from 'url'; +import { ChromiumBrowser, Page } from 'playwright'; +import testSetup from './setup'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + describe('perf_login_and_home', () => { + const config = getService('config'); + const kibanaUrl = Url.format({ + protocol: config.get('servers.kibana.protocol'), + hostname: config.get('servers.kibana.hostname'), + port: config.get('servers.kibana.port'), + }); + + let page: Page | null = null; + let browser: ChromiumBrowser | null = null; + + before(async () => { + const context = await testSetup(); + page = context.page; + browser = context.browser; + }); + + after(async () => { + await browser?.close(); + }); + + it('Go to Kibana login page', async () => { + await page?.goto(`${kibanaUrl}`); + }); + + it('Login to Kibana', async () => { + const usernameLocator = page?.locator('[data-test-subj=loginUsername]'); + const passwordLocator = page?.locator('[data-test-subj=loginPassword]'); + const submitButtonLocator = page?.locator('[data-test-subj=loginSubmit]'); + + await usernameLocator?.type('elastic', { delay: 500 }); + await passwordLocator?.type('changeme', { delay: 500 }); + await submitButtonLocator?.click({ delay: 1000 }); + }); + + it('Dismiss Welcome Screen', async () => { + await page?.waitForLoadState(); + const skipButtonLocator = page?.locator('[data-test-subj=skipWelcomeScreen]'); + await skipButtonLocator?.click({ delay: 1000 }); + await page?.waitForLoadState('networkidle'); + }); + }); +} diff --git a/x-pack/test/performance/tests/playwright/setup.ts b/x-pack/test/performance/tests/playwright/setup.ts new file mode 100644 index 0000000000000..b89335f19c101 --- /dev/null +++ b/x-pack/test/performance/tests/playwright/setup.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 playwright, { ChromiumBrowser, Page } from 'playwright'; + +interface ITestSetup { + browser: ChromiumBrowser; + page: Page; +} + +const headless = process.env.TEST_BROWSER_HEADLESS === '1'; + +export default async (): Promise => { + const browser = await playwright.chromium.launch({ headless }); + const page = await browser.newPage(); + const client = await page.context().newCDPSession(page); + + await client.send('Network.clearBrowserCache'); + await client.send('Network.setCacheDisabled', { cacheDisabled: true }); + await client.send('Network.emulateNetworkConditions', { + latency: 100, + downloadThroughput: 750_000, + uploadThroughput: 750_000, + offline: false, + }); + + await page.route('**', (route) => route.continue()); + + return { browser, page }; +}; diff --git a/x-pack/test/reporting_functional/services/scenarios.ts b/x-pack/test/reporting_functional/services/scenarios.ts index a1387127ffc0a..79a0c59cc5a3d 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 8561094890474..308dc472c29fe 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 b989ad1127306..922ecfc12dc4f 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 b7de2dba02d19..bc3c1c302c685 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 e63b86392756f..3b5cc2dae545c 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/x-pack/test/usage_collection/test_suites/application_usage/index.ts b/x-pack/test/usage_collection/test_suites/application_usage/index.ts index 4ba45b4bf9e12..fc53c8ddf5ed3 100644 --- a/x-pack/test/usage_collection/test_suites/application_usage/index.ts +++ b/x-pack/test/usage_collection/test_suites/application_usage/index.ts @@ -10,8 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { applicationUsageSchema } from '../../../../../src/plugins/kibana_usage_collection/server/collectors/application_usage/schema'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - // FLAKY: https://github.com/elastic/kibana/issues/90536 - describe.skip('Application Usage', function () { + describe('Application Usage', function () { this.tags('ciGroup1'); const { common } = getPageObjects(['common']); const browser = getService('browser'); diff --git a/yarn.lock b/yarn.lock index cc181351a3d96..802e94f30ea6c 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== @@ -10216,6 +10225,11 @@ commander@^7.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== +commander@^8.2.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" @@ -11170,10 +11184,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 +11201,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" @@ -21933,6 +21947,35 @@ playwright-chromium@=1.14.0: ws "^7.4.6" yazl "^2.5.1" +playwright-core@=1.17.1: + version "1.17.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.17.1.tgz#a16e0f89284a0ed8ae6d77e1c905c84b8a2ba022" + integrity sha512-C3c8RpPiC3qr15fRDN6dx6WnUkPLFmST37gms2aoHPDRvp7EaGDPMMZPpqIm/QWB5J40xDrQCD4YYHz2nBTojQ== + dependencies: + commander "^8.2.0" + debug "^4.1.1" + extract-zip "^2.0.1" + https-proxy-agent "^5.0.0" + jpeg-js "^0.4.2" + mime "^2.4.6" + pngjs "^5.0.0" + progress "^2.0.3" + proper-lockfile "^4.1.1" + proxy-from-env "^1.1.0" + rimraf "^3.0.2" + socks-proxy-agent "^6.1.0" + stack-utils "^2.0.3" + ws "^7.4.6" + yauzl "^2.10.0" + yazl "^2.5.1" + +playwright@^1.17.1: + version "1.17.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.17.1.tgz#a6d63302ee40f41283c4bf869de261c4743a787c" + integrity sha512-DisCkW9MblDJNS3rG61p8LiLA2WA7IY/4A4W7DX4BphWe/HuWjKmGQptuk4NVIh5UuSwXpW/jaH2+ZgjHs3GMA== + dependencies: + playwright-core "=1.17.1" + plugin-error@^1.0.0, plugin-error@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" @@ -25703,7 +25746,16 @@ socks-proxy-agent@^5.0.0: debug "4" socks "^2.3.3" -socks@^2.3.3: +socks-proxy-agent@^6.1.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz#e664e8f1aaf4e1fb3df945f09e3d94f911137f87" + integrity sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew== + dependencies: + agent-base "^6.0.2" + debug "^4.3.1" + socks "^2.6.1" + +socks@^2.3.3, socks@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e" integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==