diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 5340b4bf578cdf..f07ac997e31c24 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -32,6 +32,7 @@ disabled: - x-pack/test/security_solution_cypress/upgrade_config.ts - x-pack/test/security_solution_cypress/visual_config.ts - x-pack/test/functional_enterprise_search/with_host_configured.config.ts + - x-pack/plugins/apm/ftr_e2e/ftr_config_open.ts - x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts - x-pack/plugins/apm/ftr_e2e/ftr_config.ts diff --git a/.buildkite/pipelines/artifacts.yml b/.buildkite/pipelines/artifacts.yml index 5f3b0dac3af065..606ec6c2e038f3 100644 --- a/.buildkite/pipelines/artifacts.yml +++ b/.buildkite/pipelines/artifacts.yml @@ -52,6 +52,17 @@ steps: limit: 1 - command: KIBANA_DOCKER_CONTEXT=cloud .buildkite/scripts/steps/artifacts/docker_context.sh + label: 'Docker Context Verification' + agents: + queue: n2-2 + timeout_in_minutes: 30 + if: "build.env('RELEASE_BUILD') == null || build.env('RELEASE_BUILD') == '' || build.env('RELEASE_BUILD') == 'false'" + retry: + automatic: + - exit_status: '*' + limit: 1 + + - command: KIBANA_DOCKER_CONTEXT=ubi .buildkite/scripts/steps/artifacts/docker_context.sh label: 'Docker Context Verification' agents: queue: n2-2 diff --git a/.buildkite/pull_requests.json b/.buildkite/pull_requests.json index d54f637b8f6d1a..e0e74541277332 100644 --- a/.buildkite/pull_requests.json +++ b/.buildkite/pull_requests.json @@ -16,7 +16,25 @@ "trigger_comment_regex": "^(?:(?:buildkite\\W+)?(?:build|test)\\W+(?:this|it))", "always_trigger_comment_regex": "^(?:(?:buildkite\\W+)?(?:build|test)\\W+(?:this|it))", "skip_ci_labels": ["skip-ci", "jenkins-ci"], - "skip_target_branches": ["6.8", "7.11", "7.12"] + "skip_target_branches": ["6.8", "7.11", "7.12"], + "skip_ci_on_only_changed": [ + "^docs/", + "^rfcs/", + "^.ci/.+\\.yml$", + "^.ci/es-snapshots/", + "^.ci/pipeline-library/", + "^.ci/Jenkinsfile_[^/]+$", + "^\\.github/", + "\\.md$", + "^\\.backportrc\\.json$", + "^nav-kibana-dev\\.docnav\\.json$", + "^src/dev/prs/kibana_qa_pr_list\\.json$", + "^\\.buildkite/pull_requests\\.json$" + ], + "always_require_ci_on_changed": [ + "^docs/developer/plugin-list.asciidoc$", + "/plugins/[^/]+/readme\\.(md|asciidoc)$" + ] } ] } diff --git a/.buildkite/scripts/common/env.sh b/.buildkite/scripts/common/env.sh index b8b9ef2ffb7de0..344117b57c452d 100755 --- a/.buildkite/scripts/common/env.sh +++ b/.buildkite/scripts/common/env.sh @@ -38,6 +38,7 @@ export TEST_BROWSER_HEADLESS=1 export ELASTIC_APM_ENVIRONMENT=ci export ELASTIC_APM_TRANSACTION_SAMPLE_RATE=0.1 export ELASTIC_APM_SERVER_URL=https://kibana-ci-apm.apm.us-central1.gcp.cloud.es.io +# Not really a secret, if APM supported public auth we would use it and APM requires that we use this name export ELASTIC_APM_SECRET_TOKEN=7YKhoXsO4MzjhXjx2c if is_pr; then diff --git a/.buildkite/scripts/lifecycle/post_command.sh b/.buildkite/scripts/lifecycle/post_command.sh index df728b6ee67d83..9cd342ff69fa92 100755 --- a/.buildkite/scripts/lifecycle/post_command.sh +++ b/.buildkite/scripts/lifecycle/post_command.sh @@ -26,6 +26,7 @@ if [[ "$IS_TEST_EXECUTION_STEP" == "true" ]]; then buildkite-agent artifact upload 'x-pack/test/functional/apps/reporting/reports/session/*.pdf' buildkite-agent artifact upload 'x-pack/test/functional/failure_debug/html/*.html' buildkite-agent artifact upload '.es/**/*.hprof' + buildkite-agent artifact upload 'data/es_debug_*.tar.gz' echo "--- Run Failed Test Reporter" node scripts/report_failed_tests --build-url="${BUILDKITE_BUILD_URL}#${BUILDKITE_JOB_ID}" 'target/junit/**/*.xml' diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.js b/.buildkite/scripts/pipelines/pull_request/pipeline.js index 6a4610284e4009..c9f42dae1a776f 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.js +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.js @@ -9,14 +9,11 @@ const execSync = require('child_process').execSync; const fs = require('fs'); const { areChangesSkippable, doAnyChangesMatch } = require('kibana-buildkite-library'); -const { SKIPPABLE_PR_MATCHERS } = require('./skippable_pr_matchers'); - -const REQUIRED_PATHS = [ - // this file is auto-generated and changes to it need to be validated with CI - /^docs\/developer\/plugin-list.asciidoc$/, - // don't skip CI on prs with changes to plugin readme files /i is for case-insensitive matching - /\/plugins\/[^\/]+\/readme\.(md|asciidoc)$/i, -]; +const prConfigs = require('../../../pull_requests.json'); +const prConfig = prConfigs.jobs.find((job) => job.pipelineSlug === 'kibana-pull-request'); + +const REQUIRED_PATHS = prConfig.always_require_ci_on_changed.map((r) => new RegExp(r, 'i')); +const SKIPPABLE_PR_MATCHERS = prConfig.skip_ci_on_only_changed.map((r) => new RegExp(r, 'i')); const getPipeline = (filename, removeSteps = true) => { const str = fs.readFileSync(filename).toString(); diff --git a/.buildkite/scripts/pipelines/pull_request/skippable_pr_matchers.js b/.buildkite/scripts/pipelines/pull_request/skippable_pr_matchers.js deleted file mode 100644 index 2a36e66e11cd62..00000000000000 --- a/.buildkite/scripts/pipelines/pull_request/skippable_pr_matchers.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -module.exports = { - SKIPPABLE_PR_MATCHERS: [ - /^docs\//, - /^rfcs\//, - /^.ci\/.+\.yml$/, - /^.ci\/es-snapshots\//, - /^.ci\/pipeline-library\//, - /^.ci\/Jenkinsfile_[^\/]+$/, - /^\.github\//, - /\.md$/, - /^\.backportrc\.json$/, - /^nav-kibana-dev\.docnav\.json$/, - /^src\/dev\/prs\/kibana_qa_pr_list\.json$/, - /^\.buildkite\/scripts\/pipelines\/pull_request\/skippable_pr_matchers\.js$/, - ], -}; diff --git a/.buildkite/scripts/steps/artifacts/docker_context.sh b/.buildkite/scripts/steps/artifacts/docker_context.sh index d01cbccfc76c14..8076ebd0435456 100755 --- a/.buildkite/scripts/steps/artifacts/docker_context.sh +++ b/.buildkite/scripts/steps/artifacts/docker_context.sh @@ -19,6 +19,8 @@ if [[ "$KIBANA_DOCKER_CONTEXT" == "default" ]]; then DOCKER_CONTEXT_FILE="kibana-$FULL_VERSION-docker-build-context.tar.gz" elif [[ "$KIBANA_DOCKER_CONTEXT" == "cloud" ]]; then DOCKER_CONTEXT_FILE="kibana-cloud-$FULL_VERSION-docker-build-context.tar.gz" +elif [[ "$KIBANA_DOCKER_CONTEXT" == "ubi" ]]; then + DOCKER_CONTEXT_FILE="kibana-ubi8-$FULL_VERSION-docker-build-context.tar.gz" fi tar -xf "target/$DOCKER_CONTEXT_FILE" -C "$DOCKER_BUILD_FOLDER" diff --git a/.buildkite/scripts/steps/es_snapshots/build.sh b/.buildkite/scripts/steps/es_snapshots/build.sh index cdc1750e59bfc9..370ae275aa758b 100755 --- a/.buildkite/scripts/steps/es_snapshots/build.sh +++ b/.buildkite/scripts/steps/es_snapshots/build.sh @@ -69,7 +69,6 @@ echo "--- Build Elasticsearch" :distribution:archives:darwin-aarch64-tar:assemble \ :distribution:archives:darwin-tar:assemble \ :distribution:docker:docker-export:assemble \ - :distribution:docker:cloud-docker-export:assemble \ :distribution:archives:linux-aarch64-tar:assemble \ :distribution:archives:linux-tar:assemble \ :distribution:archives:windows-zip:assemble \ @@ -86,19 +85,26 @@ docker images "docker.elastic.co/elasticsearch/elasticsearch" --format "{{.Tag}} docker images "docker.elastic.co/elasticsearch/elasticsearch" --format "{{.Tag}}" | xargs -n1 bash -c 'docker save docker.elastic.co/elasticsearch/elasticsearch:${0} | gzip > ../es-build/elasticsearch-${0}-docker-image.tar.gz' echo "--- Create kibana-ci docker cloud image archives" -ES_CLOUD_ID=$(docker images "docker.elastic.co/elasticsearch-ci/elasticsearch-cloud" --format "{{.ID}}") -ES_CLOUD_VERSION=$(docker images "docker.elastic.co/elasticsearch-ci/elasticsearch-cloud" --format "{{.Tag}}") -KIBANA_ES_CLOUD_VERSION="$ES_CLOUD_VERSION-$ELASTICSEARCH_GIT_COMMIT" -KIBANA_ES_CLOUD_IMAGE="docker.elastic.co/kibana-ci/elasticsearch-cloud:$KIBANA_ES_CLOUD_VERSION" - -docker tag "$ES_CLOUD_ID" "$KIBANA_ES_CLOUD_IMAGE" - -echo "$KIBANA_DOCKER_PASSWORD" | docker login -u "$KIBANA_DOCKER_USERNAME" --password-stdin docker.elastic.co -trap 'docker logout docker.elastic.co' EXIT -docker image push "$KIBANA_ES_CLOUD_IMAGE" - -export ELASTICSEARCH_CLOUD_IMAGE="$KIBANA_ES_CLOUD_IMAGE" -export ELASTICSEARCH_CLOUD_IMAGE_CHECKSUM="$(docker images "$KIBANA_ES_CLOUD_IMAGE" --format "{{.Digest}}")" +# Ignore build failures. This docker image downloads metricbeat and filebeat. +# When we bump versions, these dependencies may not exist yet, but we don't want to +# block the rest of the snapshot promotion process +set +e +./gradlew :distribution:docker:cloud-docker-export:assemble && { + ES_CLOUD_ID=$(docker images "docker.elastic.co/elasticsearch-ci/elasticsearch-cloud" --format "{{.ID}}") + ES_CLOUD_VERSION=$(docker images "docker.elastic.co/elasticsearch-ci/elasticsearch-cloud" --format "{{.Tag}}") + KIBANA_ES_CLOUD_VERSION="$ES_CLOUD_VERSION-$ELASTICSEARCH_GIT_COMMIT" + KIBANA_ES_CLOUD_IMAGE="docker.elastic.co/kibana-ci/elasticsearch-cloud:$KIBANA_ES_CLOUD_VERSION" + echo $ES_CLOUD_ID $ES_CLOUD_VERSION $KIBANA_ES_CLOUD_VERSION $KIBANA_ES_CLOUD_IMAGE + docker tag "$ES_CLOUD_ID" "$KIBANA_ES_CLOUD_IMAGE" + + echo "$KIBANA_DOCKER_PASSWORD" | docker login -u "$KIBANA_DOCKER_USERNAME" --password-stdin docker.elastic.co + trap 'docker logout docker.elastic.co' EXIT + docker image push "$KIBANA_ES_CLOUD_IMAGE" + + export ELASTICSEARCH_CLOUD_IMAGE="$KIBANA_ES_CLOUD_IMAGE" + export ELASTICSEARCH_CLOUD_IMAGE_CHECKSUM="$(docker images "$KIBANA_ES_CLOUD_IMAGE" --format "{{.Digest}}")" +} +set -e echo "--- Create checksums for snapshot files" cd "$destination" diff --git a/dev_docs/getting_started/troubleshooting.mdx b/dev_docs/getting_started/troubleshooting.mdx index e0adfbad86a843..db52830bbae4f1 100644 --- a/dev_docs/getting_started/troubleshooting.mdx +++ b/dev_docs/getting_started/troubleshooting.mdx @@ -26,3 +26,17 @@ git clean -fdxn -e /config -e /.vscode # review the files which will be deleted, consider adding some more excludes (-e) # re-run without the dry-run (-n) flag to actually delete the files ``` + +### search.check_ccs_compatibility error + +If you run into an error that says something like: + +``` +[class org.elasticsearch.action.search.SearchRequest] is not compatible version 8.1.0 and the 'search.check_ccs_compatibility' setting is enabled. +``` + +it means you are using a new Elasticsearch feature that will not work in a CCS environment because the feature does not exist in older versions. If you are working on an experimental feature and are okay with this limitation, you will have to move the failing test into a special test suite that does not use this setting to get ci to pass. Take this path cautiously. If you do not remember to move the test back into the default test suite when the feature is GA'ed, it will not have proper CCS test coverage. + +We added this test coverage in version `8.1` because we accidentally broke core Kibana features (for example, when Discover started using the new fields parameter) for our CCS users. CCS is not a corner case and (excluding certain experimental features) Kibana should always work for our CCS users. This setting is our way of ensuring test coverage. + +Please reach out to the [Kibana Operations team](https://github.com/orgs/elastic/teams/kibana-operations) if you have further questions. diff --git a/dev_docs/operations/ci_stats.mdx b/dev_docs/operations/ci_stats.mdx new file mode 100644 index 00000000000000..01cc426369f5c8 --- /dev/null +++ b/dev_docs/operations/ci_stats.mdx @@ -0,0 +1,15 @@ +--- +id: kibDevDocsOpsCiStats +slug: /kibana-dev-docs/ops/ci-stats +title: "Kibana CI Stats" +description: A service that we run to track little bits of data about CI runs. +tags: ['kibana', 'dev', 'contributor', 'operations', 'ci'] +--- + +Kibana CI Stats (sometimes written "ci-stats") is a service run at https://ci-stats.kibana.dev. This service runs a series of APIs in front of an Elasticsearch Cluster which allows us to record data about each run of CI. This data includes metrics, times, test results, and more. We are working on storing performance data in there too, and eventually will spend time to write good UIs on top of this data. + +The service implementation is available at https://github.com/elastic/kibana-ci-stats/ (private because there doesn't seem to be a good reason to make it public). + +The service is run on Google Cloud Run, which allows us to build a container, push it to GCR, define a memory limit, vCPU count, and concurrent request limit per container, and Google will automatically scale the container for us. It works pretty well and hides a lot of the complexity of running the service. The repo uses Buildkite CI to build and deploy the container when pushing to the main branch. All changes to the main branch must come from PRs, but at this time we don't require review for PRs. + +The website at https://ci-stats.kibana.dev uses EUI and Elastic Charts, and currently has users powered by Github OAuth. When someone authenticates with ci-stats they are first redirected to Github for authentication, then their membership in the Elastic org is checked. Users in the Elastic org will be able to do things that other users can't, like [trigger flaky test runner jobs](https://ci-stats.kibana.dev/trigger_flaky_test_runner). \ No newline at end of file diff --git a/dev_docs/operations/operations_landing.mdx b/dev_docs/operations/operations_landing.mdx new file mode 100644 index 00000000000000..cda44a96fe4ddf --- /dev/null +++ b/dev_docs/operations/operations_landing.mdx @@ -0,0 +1,49 @@ +--- +id: kibDevDocsOpsOverview +slug: /kibana-dev-docs/ops +title: Kibana Operations +description: Links to all the documentation maintained by the Kibana Operations team +tags: ['kibana', 'dev', 'contributor', 'operations'] +layout: landing +--- + + + + + + + + + + \ No newline at end of file diff --git a/docs/apm/images/apm-service-group.png b/docs/apm/images/apm-service-group.png new file mode 100644 index 00000000000000..275b5a86621237 Binary files /dev/null and b/docs/apm/images/apm-service-group.png differ diff --git a/docs/apm/services.asciidoc b/docs/apm/services.asciidoc index 90ebff3d8ad71b..7009aac04ffa8f 100644 --- a/docs/apm/services.asciidoc +++ b/docs/apm/services.asciidoc @@ -12,3 +12,41 @@ and requires anomaly detection to be enabled. [role="screenshot"] image::apm/images/apm-services-overview.png[Example view of services table the APM app in Kibana] + +[float] +[[service-groups]] +==== Service groups + +preview::[] + +Group services together to build meaningful views that remove noise and simplify investigations across services. +Service groups are {kib} space-specific and available for any users with appropriate access. + +[role="screenshot"] +image::apm/images/apm-service-group.png[Example view of service group in the APM app in Kibana] + +To enable Service groups, open {kib} and navigate to **Stack Management** > **Advanced Settings** > **Observability**, +and enable the **Service groups feature**. + +To create a service group, navigate to **Observability** > **APM** > **Services** and select **Create group**. +Specify a name, color, and description. +Then, using the <>, specify a query to select services for the group. +Services that match the query within the last 24 hours will be assigned to the group. + +[NOTE] +==== +Once a service group has been saved, this list of services within it is static. +If a newly added service matches the KQL query, it will not be automatically added to the service group. +Similarly, if a service stops matching the KQL query, it will not be removed from the group. + +To update the list of services within a group, +edit the service group, click **Refresh** next to the KQL query, and click **Save group**. +==== + +**Examples** + +Not sure where to get started? Here are some sample queries you can build from: + +* Group services by environment--in this example, "production": `service.environment : "production"` +* Group services by name--this example groups those that end in "beat": `service.name : *beat` (matches services named "Auditbeat", "Heartbeat", "Filebeat", etc.) +* Group services with a high transaction duration in the last 24 hours: `transaction.duration.us >= 50000000` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateoptions.md index b81a59c745e7bf..7044f3007c3825 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateoptions.md @@ -18,6 +18,7 @@ export interface SavedObjectsUpdateOptions extends SavedOb | --- | --- | --- | | [references?](./kibana-plugin-core-server.savedobjectsupdateoptions.references.md) | SavedObjectReference\[\] | (Optional) A reference to another saved object. | | [refresh?](./kibana-plugin-core-server.savedobjectsupdateoptions.refresh.md) | MutatingOperationRefreshSetting | (Optional) The Elasticsearch Refresh setting for this operation | +| [retryOnConflict?](./kibana-plugin-core-server.savedobjectsupdateoptions.retryonconflict.md) | number | (Optional) The Elasticsearch retry_on_conflict setting for this operation. Defaults to 0 when version is provided, 3 otherwise. | | [upsert?](./kibana-plugin-core-server.savedobjectsupdateoptions.upsert.md) | Attributes | (Optional) If specified, will be used to perform an upsert if the document doesn't exist | | [version?](./kibana-plugin-core-server.savedobjectsupdateoptions.version.md) | string | (Optional) An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. | diff --git a/docs/management/cases/images/cases.png b/docs/management/cases/images/cases.png index 7b0c551cb69038..b244b3df16a209 100644 Binary files a/docs/management/cases/images/cases.png and b/docs/management/cases/images/cases.png differ diff --git a/docs/management/connectors/action-types/email.asciidoc b/docs/management/connectors/action-types/email.asciidoc index 3571185d0d510b..1c6c30526e0bb6 100644 --- a/docs/management/connectors/action-types/email.asciidoc +++ b/docs/management/connectors/action-types/email.asciidoc @@ -5,12 +5,21 @@ Email ++++ -The email connector uses the SMTP protocol to send mail messages, using an integration of https://nodemailer.com/[Nodemailer]. An exception is Microsoft Exchange, which uses HTTP protocol for sending emails, https://docs.microsoft.com/en-us/graph/api/user-sendmail[Send mail]. Email message text is sent as both plain text and html text. +The email connector uses the SMTP protocol to send mail messages, using an +integration of https://nodemailer.com/[Nodemailer]. An exception is Microsoft +Exchange, which uses HTTP protocol for sending emails, +https://docs.microsoft.com/en-us/graph/api/user-sendmail[Send mail]. Email +message text is sent as both plain text and html text. [NOTE] ==== -* For emails to have a footer with a link back to {kib}, set the <> configuration setting. -* When the <> configuration setting is used, the email addresses used for all of the Sender (from), To, CC, and BCC properties must have email domains specified in the configuration setting. +* For emails to have a footer with a link back to {kib}, set the +<> configuration setting. +* When the +<> +configuration setting is used, the email addresses used for all of the Sender +(from), To, CC, and BCC properties must have email domains specified in the +configuration setting. ==== [float] @@ -19,24 +28,73 @@ The email connector uses the SMTP protocol to send mail messages, using an integ Email connectors have the following configuration properties. -Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. -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. -Secure:: If true, the connection will use TLS when connecting to the service provider. Refer to the https://nodemailer.com/smtp/#tls-options[Nodemailer TLS documentation] for more information. If not true, the connection will initially connect over TCP, then attempt to switch to TLS via the SMTP STARTTLS command. -Tenant ID:: The directory tenant that the application plans to operate against, in GUID format. -Client ID:: The application ID that is assigned to your app, in GUID format. You can find this information in the portal where you registered your app. -Client Secret:: The client secret that you generated for your app in the app registration portal. The client secret must be URL-encoded before being sent. The Basic auth pattern of providing credentials in the Authorization header, per https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1[RFC 6749], is also supported. -Require authentication:: If true, a username and password for login type authentication must be provided. -Username:: Username for login type authentication. -Password:: Password for login type authentication. +Name:: +The name of the connector. The name is used to identify a connector in the +management UI connector listing, or in the connector list when configuring an +action. + +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. + +Secure:: +If true, the connection will use TLS when connecting to the service provider. +Refer to the +https://nodemailer.com/smtp/#tls-options[Nodemailer TLS documentation] for more +information. If not true, the connection will initially connect over TCP, then +attempt to switch to TLS via the SMTP STARTTLS command. + +Tenant ID:: +The directory tenant that the application plans to operate against, in GUID +format. + +Client ID:: +The application ID that is assigned to your app, in GUID format. You can find +this information in the portal where you registered your app. + +Client Secret:: +The client secret that you generated for your app in the app registration +portal. The client secret must be URL-encoded before being sent. The Basic auth +pattern of providing credentials in the Authorization header, per +https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1[RFC 6749], is also +supported. + +Require authentication:: +If true, a username and password for login type authentication must be provided. + +Username:: +Username for login type authentication. + +Password:: +Password for login type authentication. [float] [[email-connector-networking-configuration]] ==== Connector networking configuration -Use the <> to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations. +Use the <> to customize +connector networking configurations, such as proxies, certificates, or TLS +settings. You can set configurations that apply to all your connectors or use +`xpack.actions.customHostSettings` to set per-host configurations. [float] [[preconfigured-email-configuration]] @@ -60,20 +118,55 @@ Use the <> to customize connecto Config defines information for the connector type. -`service`:: The name of the email service. If `service` is `elastic_cloud` (for Elastic Cloud notifications) or one of Nodemailer's https://nodemailer.com/smtp/well-known/[well-known email service providers], the `host`, `port`, and `secure` properties are ignored. If `service` is `other`, the `host` and `port` properties must be defined. For more information on the `gmail` service value, refer to https://nodemailer.com/usage/using-gmail/[Nodemailer Gmail documentation]. If `service` is `exchange_server`, the `tenantId`, `clientId`, `clientSecret` properties are required instead of `host` and `port`. -`from`:: An email address that corresponds to *Sender*. -`host`:: A string that corresponds to *Host*. -`port`:: A number that corresponds to *Port*. -`secure`:: A boolean that corresponds to *Secure*. -`hasAuth`:: A boolean that corresponds to *Requires authentication*. If `true`, this connector will require values for `user` and `password` inside the secrets configuration. Defaults to `true`. -`tenantId`:: A GUID format value that corresponds to *Tenant ID*, which is a part of OAuth 2.0 Client Credentials Authentication. -`clientId`:: A GUID format value that corresponds to *Client ID*, which is a part of OAuth 2.0 Client Credentials Authentication. +`service`:: +The name of the email service. If `service` is `elastic_cloud` (for Elastic +Cloud notifications) or one of Nodemailer's +https://nodemailer.com/smtp/well-known/[well-known email service providers], the +`host`, `port`, and `secure` properties are ignored. If `service` is `other`, +the `host` and `port` properties must be defined. For more information on the +`gmail` service value, refer to +https://nodemailer.com/usage/using-gmail/[Nodemailer Gmail documentation]. If +`service` is `exchange_server`, the `tenantId`, `clientId`, `clientSecret` +properties are required instead of `host` and `port`. + +`from`:: +An email address that corresponds to *Sender*. + +`host`:: +A string that corresponds to *Host*. + +`port`:: +A number that corresponds to *Port*. + +`secure`:: +A boolean that corresponds to *Secure*. + +`hasAuth`:: +A boolean that corresponds to *Requires authentication*. If `true`, this +connector will require values for `user` and `password` inside the secrets +configuration. Defaults to `true`. + +`tenantId`:: +A GUID format value that corresponds to *Tenant ID*, which is a part of OAuth +2.0 Client Credentials Authentication. + +`clientId`:: +A GUID format value that corresponds to *Client ID*, which is a part of OAuth +2.0 Client Credentials Authentication. Secrets defines sensitive information for the connector type. -`user`:: A string that corresponds to *Username*. Required if `hasAuth` is set to `true`. -`password`:: A string that corresponds to *Password*. Should be stored in the <>. Required if `hasAuth` is set to `true`. -`clientSecret`:: A string that corresponds to *Client Secret*. Should be stored in the <>. Required if `service` is set to `exchange_server`, which uses OAuth 2.0 Client Credentials Authentication. +`user`:: +A string that corresponds to *Username*. Required if `hasAuth` is set to `true`. + +`password`:: +A string that corresponds to *Password*. Should be stored in the +<>. Required if `hasAuth` is set to `true`. + +`clientSecret`:: +A string that corresponds to *Client Secret*. Should be stored in the +<>. Required if `service` is set to +`exchange_server`, which uses OAuth 2.0 Client Credentials Authentication. [float] [[define-email-ui]] @@ -95,17 +188,26 @@ image::management/connectors/images/email-params-test.png[Email params test] Email actions have the following configuration properties. -To, CC, BCC:: Each item is a list of addresses. Addresses can be specified in `user@host-name` format, or in `name ` format. One of To, CC, or BCC must contain an entry. -Subject:: The subject line of the email. -Message:: The message text of the email. Markdown format is supported. +To, CC, BCC:: +Each item is a list of addresses. Addresses can be specified in `user@host-name` +format, or in `name ` format. One of To, CC, or BCC must contain +an entry. + +Subject:: +The subject line of the email. + +Message:: +The message text of the email. Markdown format is supported. [float] [[configuring-email]] ==== Configuring email accounts for well-known services -The email connector can send email using many popular SMTP email services and the Microsoft Exchange Graph API. +The email connector can send email using many popular SMTP email services and +the Microsoft Exchange Graph API. -For more information about configuring the email connector to work with different email systems, refer to: +For more information about configuring the email connector to work with +different email systems, refer to: * <> * <> @@ -113,33 +215,21 @@ For more information about configuring the email connector to work with differen * <> * <> -For other email servers, you can check the list of well-known services that Nodemailer supports in the JSON file https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json[well-known/services.json]. The properties of the objects in those files — `host`, `port`, and `secure` — correspond to the same email connector configuration properties. A missing `secure` property in the "well-known/services.json" file is considered `false`. Typically, `port: 465` uses `secure: true`, and `port: 25` and `port: 587` use `secure: false`. +For other email servers, you can check the list of well-known services that +Nodemailer supports in the JSON file +https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json[well-known/services.json]. +The properties of the objects in those files — `host`, `port`, and +`secure` — correspond to the same email connector configuration +properties. A missing `secure` property in the "well-known/services.json" file +is considered `false`. Typically, `port: 465` uses `secure: true`, and +`port: 25` and `port: 587` use `secure: false`. [float] [[elasticcloud]] ==== Sending email from Elastic Cloud -IMPORTANT: These instructions require you to link:{cloud}/ec-watcher.html#ec-watcher-whitelist[allowlist] the email addresses that notifications get sent. - -Use the following connector settings to send email from Elastic Cloud: - -Sender:: -`noreply@watcheralert.found.io` - -Service:: -`elastic_cloud` - -Host:: -`dockerhost` - -Port:: -`10025` - -Secure:: -Toggle off - -Authentication:: -Toggle off +Use the preconfigured email connector (`Elastic-Cloud-SMTP`) to send emails from +Elastic Cloud. [float] [[gmail]] @@ -162,9 +252,9 @@ https://mail.google.com[Gmail] SMTP service: -------------------------------------------------- If you get an authentication error that indicates that you need to continue the -sign-in process from a web browser when the action attempts to send email, you need -to configure Gmail to https://support.google.com/accounts/answer/6010255?hl=en[allow -less secure apps to access your account]. +sign-in process from a web browser when the action attempts to send email, you +need to configure Gmail to +https://support.google.com/accounts/answer/6010255?hl=en[allow less secure apps to access your account]. If two-step verification is enabled for your account, you must generate and use a unique App Password to send email from {kib}. See @@ -194,9 +284,10 @@ secrets: When sending emails, you must provide a `from` address, either as the default in your connector configuration or as part of the email action in the rule. -NOTE: You must use a unique App Password if two-step verification is enabled. - See http://windows.microsoft.com/en-us/windows/app-passwords-two-step-verification[App - passwords and two-step verification] for more information. +NOTE: You must use a unique App Password if two-step verification is enabled. +See +http://windows.microsoft.com/en-us/windows/app-passwords-two-step-verification[App passwords and two-step verification] +for more information. [float] [[amazon-ses]] @@ -219,13 +310,15 @@ secrets: -------------------------------------------------- <1> `config.host` varies depending on the region -NOTE: You must use your Amazon SES SMTP credentials to send email through - Amazon SES. For more information, see - http://docs.aws.amazon.com/ses/latest/DeveloperGuide/smtp-credentials.html[Obtaining - Your Amazon SES SMTP Credentials]. You might also need to verify - https://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-email-addresses.html[your email address] - or https://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-domains.html[your whole domain] - at AWS. +NOTE: You must use your Amazon SES SMTP credentials to send email through Amazon +SES. For more information, see +http://docs.aws.amazon.com/ses/latest/DeveloperGuide/smtp-credentials.html[Obtaining Your Amazon SES SMTP Credentials]. +You might also need to verify +https://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-email-addresses.html[your email address] +or +https://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-domains.html[your whole domain] +at AWS. + [float] [[exchange-basic-auth]] @@ -251,52 +344,72 @@ secrets: Check with your system administrator if you receive authentication-related failures. -To prepare for the removal of Basic Auth, you must update all existing Microsoft Exchange connectors with the new configuration based on the https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow[OAuth 2.0 Client Credentials Authentication]. +To prepare for the removal of Basic Auth, you must update all existing Microsoft +Exchange connectors with the new configuration based on the +https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow[OAuth 2.0 Client Credentials Authentication]. [float] [[exchange]] ==== Sending email from Microsoft Exchange with OAuth 2.0 -Before you create an email connector for Microsoft Exchange, you must create and register the client integration application on the https://go.microsoft.com/fwlink/?linkid=2083908[Azure portal]: +Before you create an email connector for Microsoft Exchange, you must create and +register the client integration application on the +https://go.microsoft.com/fwlink/?linkid=2083908[Azure portal]: [role="screenshot"] image::management/connectors/images/exchange-register-app.png[Register client application for MS Exchange] -Next, open *Manage > API permissions*, and then define the permissions for the registered application to send emails. Refer to the https://docs.microsoft.com/en-us/graph/api/user-sendmail?view=graph-rest-1.0&tabs=http#permissions[documentation] for the Microsoft Graph API. +Next, open *Manage > API permissions*, and then define the permissions for the +registered application to send emails. Refer to the +https://docs.microsoft.com/en-us/graph/api/user-sendmail?view=graph-rest-1.0&tabs=http#permissions[documentation] +for the Microsoft Graph API. + [role="screenshot"] image::management/connectors/images/exchange-api-permissions.png[MS Exchange API permissions] -Add the "Mail.Send" permission for Microsoft Graph. The permission appears in the list with the status "Not granted for ": +Add the "Mail.Send" permission for Microsoft Graph. The permission appears in +the list with the status "Not granted for ": + [role="screenshot"] image::management/connectors/images/exchange-not-granted.png[MS Exchange "Mail.Send" not granted] Click *Grant admin consent for *. + [role="screenshot"] image::management/connectors/images/exchange-grant-confirm.png[MS Exchange grant confirmation] Confirm that the status for the "Mail.Send" permission is now granted. + [role="screenshot"] image::management/connectors/images/exchange-granted.png[MS Exchange grant confirmation] [float] [[exchange-client-secret]] ===== Configure Microsoft Exchange Client secret + To configure the Client secret , open *Manage > Certificates & secrets*. + [role="screenshot"] image::management/connectors/images/exchange-secrets.png[MS Exchange secrets configuration] -Add a new client secret, then copy the value and put it to the proper field in the Microsoft Exchange email connector. +Add a new client secret, then copy the value and put it to the proper field in +the Microsoft Exchange email connector. [float] [[exchange-client-tenant-id]] ===== Configure Microsoft Exchange Client ID and Tenant ID -To find the application Client ID, open the *Overview* page. + +To find the application Client ID, open the *Overview* page. + [role="screenshot"] image::management/connectors/images/exchange-client-tenant.png[MS Exchange Client ID and Tenant ID configuration] -Copy and paste this values to the proper fields in the Microsoft Exchange email connector. +Copy and paste this values to the proper fields in the Microsoft Exchange email +connector. + +Use the following email connector configuration to send email from Microsoft +Exchange: -Use the following email connector configuration to send email from Microsoft Exchange: [source,text] -------------------------------------------------- config: diff --git a/docs/management/connectors/pre-configured-connectors.asciidoc b/docs/management/connectors/pre-configured-connectors.asciidoc index aaef1b673d0b66..27d1d80ea7305d 100644 --- a/docs/management/connectors/pre-configured-connectors.asciidoc +++ b/docs/management/connectors/pre-configured-connectors.asciidoc @@ -2,7 +2,8 @@ [[pre-configured-connectors]] === Preconfigured connectors -You can preconfigure a connector to have all the information it needs prior to startup by adding it to the `kibana.yml` file. +You can preconfigure a connector to have all the information it needs prior to +startup by adding it to the `kibana.yml` file. Preconfigured connectors offer the following benefits: @@ -18,7 +19,8 @@ NOTE: Preconfigured connectors cannot be used with cases. ==== Preconfigured connectors example This example shows a valid configuration for -two out-of-the box connectors: <> and <>. +two out-of-the box connectors: <> and +<>. ```js xpack.actions.preconfigured: @@ -49,27 +51,33 @@ two out-of-the box connectors: <> and <>. +Sensitive properties, such as passwords, can also be stored in the +<>. ============================================== [float] [[build-in-preconfigured-connectors]] ==== Built-in preconfigured connectors -{kib} provides one built-in preconfigured connector: +{kib} provides the following built-in preconfigured connectors: * <> +* <> [float] [[managing-pre-configured-connectors]] ==== View preconfigured connectors -When you open the main menu, click *Stack Management > Rules and Connectors*. Preconfigured connectors appear on the <>, regardless of which space you are in. They are tagged as “preconfigured”, and you cannot delete them. +When you open the main menu, click *Stack Management > Rules and Connectors*. +Preconfigured connectors appear on the +<>, regardless of which space you are +in. They are tagged as “preconfigured”, and you cannot delete them. [role="screenshot"] image::images/pre-configured-connectors-managing.png[Connectors managing tab with pre-configured] -Clicking a preconfigured connector shows the description, but not the configuration. A message indicates that this is a preconfigured connector. +Clicking a preconfigured connector shows the description, but not the +configuration. A message indicates that this is a preconfigured connector. [role="screenshot"] image::images/pre-configured-connectors-view-screen.png[Pre-configured connector view details] diff --git a/docs/maps/asset-tracking-tutorial.asciidoc b/docs/maps/asset-tracking-tutorial.asciidoc index 46248c5280b201..85629e0e611f6c 100644 --- a/docs/maps/asset-tracking-tutorial.asciidoc +++ b/docs/maps/asset-tracking-tutorial.asciidoc @@ -136,10 +136,10 @@ PUT _index_template/tri_met_tracks "type": "text" }, "lastLocID": { - "type": "integer" + "type": "keyword" }, "nextLocID": { - "type": "integer" + "type": "keyword" }, "locationInScheduleDay": { "type": "integer" @@ -163,13 +163,13 @@ PUT _index_template/tri_met_tracks "type": "keyword" }, "tripID": { - "type": "integer" + "type": "keyword" }, "delay": { "type": "integer" }, "extraBlockID": { - "type": "integer" + "type": "keyword" }, "messageCode": { "type": "integer" @@ -188,7 +188,7 @@ PUT _index_template/tri_met_tracks "doc_values": true }, "vehicleID": { - "type": "integer" + "type": "keyword" }, "offRoute": { "type": "boolean" diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 787efa64f0775a..6f7ada651ad3a4 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -112,34 +112,20 @@ In addition to <.credentials {ess-icon}:: -Credentials that {kib} should use internally to authenticate anonymous requests to {es}. Possible values are: username and password, API key, or the constant `elasticsearch_anonymous_user` if you want to leverage {ref}/anonymous-access.html[{es} anonymous access]. +Credentials that {kib} should use internally to authenticate anonymous requests to {es}. + For example: + [source,yaml] ---------------------------------------- -# Username and password credentials xpack.security.authc.providers.anonymous.anonymous1: credentials: username: "anonymous_service_account" password: "anonymous_service_account_password" - -# API key (concatenated and base64-encoded) -xpack.security.authc.providers.anonymous.anonymous1: - credentials: - apiKey: "VnVhQ2ZHY0JDZGJrUW0tZTVhT3g6dWkybHAyYXhUTm1zeWFrdzl0dk5udw==" - -# API key (as returned from Elasticsearch API) -xpack.security.authc.providers.anonymous.anonymous1: - credentials: - apiKey.id: "VuaCfGcBCdbkQm-e5aOx" - apiKey.key: "ui2lp2axTNmsyakw9tvNnw" - -# Elasticsearch anonymous access -xpack.security.authc.providers.anonymous.anonymous1: - credentials: "elasticsearch_anonymous_user" ---------------------------------------- +For more information, refer to <>. + [float] [[http-authentication-settings]] ==== HTTP authentication settings diff --git a/docs/user/alerting/alerting-setup.asciidoc b/docs/user/alerting/alerting-setup.asciidoc index 2b92e8caa7ef90..6643f8d0ec8708 100644 --- a/docs/user/alerting/alerting-setup.asciidoc +++ b/docs/user/alerting/alerting-setup.asciidoc @@ -65,10 +65,9 @@ Rules and connectors are isolated to the {kib} space in which they were created. Rules are authorized using an <> associated with the last user to edit the rule. This API key captures a snapshot of the user's privileges at the time of edit and is subsequently used to run all background tasks associated with the rule, including condition checks like {es} queries and triggered actions. The following rule actions will re-generate the API key: * Creating a rule -* Enabling a disabled rule * Updating a rule [IMPORTANT] ============================================== -If a rule requires certain privileges, such as index privileges, to run, and a user without those privileges updates, disables, or re-enables the rule, the rule will no longer function. Conversely, if a user with greater or administrator privileges modifies the rule, it will begin running with increased privileges. +If a rule requires certain privileges, such as index privileges, to run, and a user without those privileges updates the rule, the rule will no longer function. Conversely, if a user with greater or administrator privileges modifies the rule, it will begin running with increased privileges. ============================================== diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index 446de62326f8ed..007d1af017df3e 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -332,13 +332,11 @@ Anyone with access to the network {kib} is exposed to will be able to access {ki Anonymous authentication gives users access to {kib} without requiring them to provide credentials. This can be useful if you want your users to skip the login step when you embed dashboards in another application or set up a demo {kib} instance in your internal network, while still keeping other security features intact. -To enable anonymous authentication in {kib}, you must decide what credentials the anonymous service account {kib} should use internally to authenticate anonymous requests. +To enable anonymous authentication in {kib}, you must specify the credentials the anonymous service account {kib} should use internally to authenticate anonymous requests. NOTE: You can configure only one anonymous authentication provider per {kib} instance. -There are three ways to specify these credentials: - -If you have a user who can authenticate to {es} using username and password, for instance from the Native or LDAP security realms, you can also use these credentials to impersonate the anonymous users. Here is how your `kibana.yml` might look if you use username and password credentials: +You must have a user account that can authenticate to {es} using a username and password, for instance from the Native or LDAP security realms, so that you can use these credentials to impersonate the anonymous users. Here is how your `kibana.yml` might look: [source,yaml] ----------------------------------------------- @@ -350,45 +348,6 @@ xpack.security.authc.providers: password: "anonymous_service_account_password" ----------------------------------------------- -If using username and password credentials isn't desired or feasible, then you can create a dedicated <> for the anonymous service account. In this case, your `kibana.yml` might look like this: - -[source,yaml] ------------------------------------------------ -xpack.security.authc.providers: - anonymous.anonymous1: - order: 0 - credentials: - apiKey: "VnVhQ2ZHY0JDZGJrUW0tZTVhT3g6dWkybHAyYXhUTm1zeWFrdzl0dk5udw==" ------------------------------------------------ - -The previous configuration snippet uses an API key string that is the result of base64-encoding of the `id` and `api_key` fields returned from the {es} API, joined by a colon. You can also specify these fields separately, and {kib} will do the concatenation and base64-encoding for you: - -[source,yaml] ------------------------------------------------ -xpack.security.authc.providers: - anonymous.anonymous1: - order: 0 - credentials: - apiKey.id: "VuaCfGcBCdbkQm-e5aOx" - apiKey.key: "ui2lp2axTNmsyakw9tvNnw" ------------------------------------------------ - -It's also possible to use {kib} anonymous access in conjunction with the {es} anonymous access. - -Prior to configuring {kib}, ensure that anonymous access is enabled and properly configured in {es}. See {ref}/anonymous-access.html[Enabling anonymous access] for more information. - -Here is how your `kibana.yml` might look like if you want to use {es} anonymous access to impersonate anonymous users in {kib}: - -[source,yaml] ------------------------------------------------ -xpack.security.authc.providers: - anonymous.anonymous1: - order: 0 - credentials: "elasticsearch_anonymous_user" <1> ------------------------------------------------ - -<1> The `elasticsearch_anonymous_user` is a special constant that indicates you want to use the {es} anonymous user. - [float] ===== Anonymous access and other types of authentication diff --git a/nav-kibana-dev.docnav.json b/nav-kibana-dev.docnav.json index 4a1fd848a928eb..4704430ba94b68 100644 --- a/nav-kibana-dev.docnav.json +++ b/nav-kibana-dev.docnav.json @@ -165,6 +165,45 @@ { "id": "kibVisTypeTimeseriesPluginApi" }, { "id": "kibVisualizationsPluginApi" } ] + }, + { + "label": "Operations", + "items": [ + { "id": "kibDevDocsOpsOverview", "label": "Overview" }, + { + "label": "CI", + "items": [ + { "id": "kibDevDocsOpsCiStats" } + ] + }, + { + "label": "Build tooling", + "items": [ + { "id": "kibDevDocsOpsOptimizer" }, + { "id": "kibDevDocsOpsBabelPreset" }, + { "id": "kibDevDocsOpsTypeSummarizer" } + ] + }, + { + "label": "Linting & Validation", + "items": [ + { "id": "kibDevDocsOpsEslintConfig" }, + { "id": "kibDevDocsOpsEslintPluginEslint" }, + { "id": "kibDevDocsOpsEslintWithTypes" }, + { "id": "kibDevDocsOpsEslintPluginImports" } + ] + }, + { + "label": "Utilities", + "items": [ + { "id": "kibDevDocsToolingLog" }, + { "id": "kibDevDocsOpsJestSerializers" }, + { "id": "kibDevDocsOpsExpect" }, + { "id": "kibDevDocsOpsAmbientStorybookTypes" }, + { "id": "kibDevDocsOpsAmbientUiTypes" } + ] + } + ] } ] } diff --git a/package.json b/package.json index e0186602382592..2d3009b7b70997 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "puppeteer/node-fetch": "^2.6.7" }, "dependencies": { + "@appland/sql-parser": "^1.5.1", "@babel/runtime": "^7.17.9", "@dnd-kit/core": "^3.1.1", "@dnd-kit/sortable": "^4.0.0", @@ -219,6 +220,7 @@ "@types/mapbox__vector-tile": "1.3.0", "@types/moment-duration-format": "^2.2.3", "@types/react-is": "^16.7.1", + "@types/rrule": "^2.2.9", "JSONStream": "1.3.5", "abort-controller": "^3.0.0", "antlr4ts": "^0.5.0-alpha.3", @@ -240,7 +242,7 @@ "constate": "^1.3.2", "content-disposition": "0.5.3", "copy-to-clipboard": "^3.0.8", - "core-js": "^3.22.4", + "core-js": "^3.22.5", "cronstrue": "^1.51.0", "cytoscape": "^3.10.0", "cytoscape-dagre": "^2.2.2", @@ -297,7 +299,6 @@ "js-levenshtein": "^1.1.6", "js-search": "^1.4.3", "js-sha256": "^0.9.0", - "js-sql-parser": "^1.4.1", "js-yaml": "^3.14.1", "json-stable-stringify": "^1.0.1", "json-stringify-pretty-compact": "1.2.0", @@ -309,6 +310,7 @@ "loader-utils": "^1.2.3", "lodash": "^4.17.21", "lru-cache": "^4.1.5", + "luxon": "^2.3.2", "lz-string": "^1.4.4", "mapbox-gl-draw-rectangle-mode": "1.0.4", "maplibre-gl": "2.1.9", @@ -405,6 +407,7 @@ "reselect": "^4.0.0", "resize-observer-polyfill": "^1.5.1", "rison-node": "1.0.2", + "rrule": "2.6.4", "rxjs": "^7.5.5", "safe-squel": "^5.12.5", "seedrandom": "^3.0.5", @@ -488,6 +491,9 @@ "@kbn/ci-stats-core": "link:bazel-bin/packages/kbn-ci-stats-core", "@kbn/ci-stats-reporter": "link:bazel-bin/packages/kbn-ci-stats-reporter", "@kbn/cli-dev-mode": "link:bazel-bin/packages/kbn-cli-dev-mode", + "@kbn/dev-cli-errors": "link:bazel-bin/packages/kbn-dev-cli-errors", + "@kbn/dev-cli-runner": "link:bazel-bin/packages/kbn-dev-cli-runner", + "@kbn/dev-proc-runner": "link:bazel-bin/packages/kbn-dev-proc-runner", "@kbn/dev-utils": "link:bazel-bin/packages/kbn-dev-utils", "@kbn/docs-utils": "link:bazel-bin/packages/kbn-docs-utils", "@kbn/es": "link:bazel-bin/packages/kbn-es", @@ -625,6 +631,9 @@ "@types/kbn__config-schema": "link:bazel-bin/packages/kbn-config-schema/npm_module_types", "@types/kbn__crypto": "link:bazel-bin/packages/kbn-crypto/npm_module_types", "@types/kbn__datemath": "link:bazel-bin/packages/kbn-datemath/npm_module_types", + "@types/kbn__dev-cli-errors": "link:bazel-bin/packages/kbn-dev-cli-errors/npm_module_types", + "@types/kbn__dev-cli-runner": "link:bazel-bin/packages/kbn-dev-cli-runner/npm_module_types", + "@types/kbn__dev-proc-runner": "link:bazel-bin/packages/kbn-dev-proc-runner/npm_module_types", "@types/kbn__dev-utils": "link:bazel-bin/packages/kbn-dev-utils/npm_module_types", "@types/kbn__doc-links": "link:bazel-bin/packages/kbn-doc-links/npm_module_types", "@types/kbn__docs-utils": "link:bazel-bin/packages/kbn-docs-utils/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 5a06233d0e72d5..234a69cb4bdf70 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -36,6 +36,9 @@ filegroup( "//packages/kbn-config:build", "//packages/kbn-crypto:build", "//packages/kbn-datemath:build", + "//packages/kbn-dev-cli-errors:build", + "//packages/kbn-dev-cli-runner:build", + "//packages/kbn-dev-proc-runner:build", "//packages/kbn-dev-utils:build", "//packages/kbn-doc-links:build", "//packages/kbn-docs-utils:build", @@ -143,6 +146,9 @@ filegroup( "//packages/kbn-config:build_types", "//packages/kbn-crypto:build_types", "//packages/kbn-datemath:build_types", + "//packages/kbn-dev-cli-errors:build_types", + "//packages/kbn-dev-cli-runner:build_types", + "//packages/kbn-dev-proc-runner:build_types", "//packages/kbn-dev-utils:build_types", "//packages/kbn-doc-links:build_types", "//packages/kbn-docs-utils:build_types", diff --git a/packages/kbn-ambient-storybook-types/README.md b/packages/kbn-ambient-storybook-types/README.md deleted file mode 100644 index 865cf8d522d1b7..00000000000000 --- a/packages/kbn-ambient-storybook-types/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @kbn/ambient-storybook-types - -Ambient types needed to use storybook. \ No newline at end of file diff --git a/packages/kbn-ambient-storybook-types/README.mdx b/packages/kbn-ambient-storybook-types/README.mdx new file mode 100644 index 00000000000000..f0db9b552d6ee2 --- /dev/null +++ b/packages/kbn-ambient-storybook-types/README.mdx @@ -0,0 +1,18 @@ +--- +id: kibDevDocsOpsAmbientStorybookTypes +slug: /kibana-dev-docs/ops/ambient-storybook-types +title: "@kbn/ambient-storybook-types" +description: A package holding ambient type definitions for storybooks +date: 2022-05-18 +tags: ['kibana', 'dev', 'contributor', 'operations', 'ambient', 'storybook', 'types'] +--- + +This package holds ambient typescript definitions needed to use storybooks. + +## Packages + +To include these types in a package: + +- add `"//packages/kbn-ambient-storybook-types"` to the `RUNTIME_DEPS` portion of the `BUILD.bazel` file. +- add `"//packages/kbn-ambient-storybook-types:npm_module_types"` to the `TYPES_DEPS` portion of the `BUILD.bazel` file. +- add `"@kbn/ambient-storybook-types"` to the `types` portion of the `tsconfig.json` file. diff --git a/packages/kbn-ambient-ui-types/README.mdx b/packages/kbn-ambient-ui-types/README.mdx index d63d8567afe07e..dbff6fb8e18a2c 100644 --- a/packages/kbn-ambient-ui-types/README.mdx +++ b/packages/kbn-ambient-ui-types/README.mdx @@ -1,7 +1,15 @@ -# @kbn/ambient-ui-types +--- +id: kibDevDocsOpsAmbientUiTypes +slug: /kibana-dev-docs/ops/ambient-ui-types +title: "@kbn/ambient-ui-types" +description: A package holding ambient type definitions for files +date: 2022-05-18 +tags: ['kibana', 'dev', 'contributor', 'operations', 'ambient', 'ui', 'types'] +--- -This is a package of Typescript types for files that might get imported by Webpack and therefore need definitions. +This package holds ambient typescript definitions for files with extensions like `.html, .png, .svg, .mdx` that might get imported by Webpack and therefore needed. +## Plugins These types will automatically be included for plugins. ## Packages @@ -9,4 +17,5 @@ These types will automatically be included for plugins. To include these types in a package: - add `"//packages/kbn-ambient-ui-types"` to the `RUNTIME_DEPS` portion of the `BUILD.bazel` file. +- add `"//packages/kbn-ambient-ui-types:npm_module_types"` to the `TYPES_DEPS` portion of the `BUILD.bazel` file. - add `"@kbn/ambient-ui-types"` to the `types` portion of the `tsconfig.json` file. \ No newline at end of file diff --git a/packages/kbn-babel-preset/BUILD.bazel b/packages/kbn-babel-preset/BUILD.bazel index 1e64c1e9f9ef7c..54dc3bafd8ac8f 100644 --- a/packages/kbn-babel-preset/BUILD.bazel +++ b/packages/kbn-babel-preset/BUILD.bazel @@ -22,7 +22,6 @@ filegroup( NPM_MODULE_EXTRA_FILES = [ "package.json", - "README.md", ] RUNTIME_DEPS = [ diff --git a/packages/kbn-babel-preset/README.md b/packages/kbn-babel-preset/README.md deleted file mode 100644 index 56ad7de6001471..00000000000000 --- a/packages/kbn-babel-preset/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# @kbn/babel-preset - -This package contains the shared bits of babel config that we use for transpiling our source code to code compatible with Node.JS and the various [browsers we support](https://www.elastic.co/support/matrix#matrix_browsers). - -## usage - -To use our presets add the following to the devDependencies section of your package.json: - -``` -"@kbn/babel-preset": "1.0.0", -``` - -Then run `yarn kbn bootstrap` to properly link the package into your plugin/package. - -Finally, add either `@kbn/babel-preset/node_preset` or `@kbn/babel-preset/webpack_preset` to your babel config. - -`@kbn/babel-preset/node_preset` is usually placed in a [`babel.config.js` file](https://babeljs.io/docs/en/configuration#babelconfigjs). - -`@kbn/babel-preset/webpack_preset` is usually placed directly in your `webpack` configuration. - -***NOTE:*** If you're transpiling code that will be run in both the browser and node you must transpile your code twice, once for each target. Take a look at the build tasks for `@kbn/i18n` to see how that can look. \ No newline at end of file diff --git a/packages/kbn-babel-preset/README.mdx b/packages/kbn-babel-preset/README.mdx new file mode 100644 index 00000000000000..bfed36a84b1c35 --- /dev/null +++ b/packages/kbn-babel-preset/README.mdx @@ -0,0 +1,23 @@ +--- +id: kibDevDocsOpsBabelPreset +slug: /kibana-dev-docs/ops/babel-preset +title: "@kbn/babel-preset" +description: A package holding the main babel configs on Kibana +date: 2022-05-17 +tags: ['kibana', 'dev', 'contributor', 'operations', 'babel', 'custom', 'preset'] +--- + +This package contains the shared bits of babel config that we use for transpiling our source code to code compatible with Node.JS and the various [browsers we support](https://www.elastic.co/support/matrix#matrix_browsers). + +## Usage + +To use those presets add either `@kbn/babel-preset/node_preset` or `@kbn/babel-preset/webpack_preset` into the babel config file. + +`@kbn/babel-preset/node_preset` is usually placed in a [`babel.config.js` file](https://babeljs.io/docs/en/configuration#babelconfigjs). + +`@kbn/babel-preset/webpack_preset` is usually placed directly in a `webpack` configuration. + +If you're transpiling code that will be run in both the browser and node you must transpile your code twice, once for each target. + +Along with the introduction of Bazel to build packages we also provide now an easier way to easily use the babel transpiler by using the `js_ts_tarnspiler` rule with our without the `web` property enabled. +Take a look for example at `@kbn/i18n` to see how that can look. \ No newline at end of file diff --git a/packages/kbn-dev-cli-errors/BUILD.bazel b/packages/kbn-dev-cli-errors/BUILD.bazel new file mode 100644 index 00000000000000..e9a3eadc3fd58f --- /dev/null +++ b/packages/kbn-dev-cli-errors/BUILD.bazel @@ -0,0 +1,114 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "kbn-dev-cli-errors" +PKG_REQUIRE_NAME = "@kbn/dev-cli-errors" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-dev-cli-errors/README.md b/packages/kbn-dev-cli-errors/README.md new file mode 100644 index 00000000000000..96ea8b180fd70b --- /dev/null +++ b/packages/kbn-dev-cli-errors/README.md @@ -0,0 +1,3 @@ +# @kbn/dev-cli-errors + +This package contains the errors used to signal specific types of failures to `@kbn/dev-cli-runner`. The dependencies of `@kbn/dev-cli-runner` also produce these errors so this package needed to be extracted to prevent a circular dependency. diff --git a/packages/kbn-dev-cli-errors/jest.config.js b/packages/kbn-dev-cli-errors/jest.config.js new file mode 100644 index 00000000000000..58fce0eb5b4f2f --- /dev/null +++ b/packages/kbn-dev-cli-errors/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-dev-cli-errors'], +}; diff --git a/packages/kbn-dev-cli-errors/package.json b/packages/kbn-dev-cli-errors/package.json new file mode 100644 index 00000000000000..62e1465933a523 --- /dev/null +++ b/packages/kbn-dev-cli-errors/package.json @@ -0,0 +1,10 @@ +{ + "name": "@kbn/dev-cli-errors", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0", + "kibana": { + "devOnly": true + } +} diff --git a/packages/kbn-dev-utils/src/run/fail.ts b/packages/kbn-dev-cli-errors/src/dev_cli_errors.ts similarity index 100% rename from packages/kbn-dev-utils/src/run/fail.ts rename to packages/kbn-dev-cli-errors/src/dev_cli_errors.ts diff --git a/packages/kbn-dev-cli-errors/src/index.ts b/packages/kbn-dev-cli-errors/src/index.ts new file mode 100644 index 00000000000000..1a116eff645095 --- /dev/null +++ b/packages/kbn-dev-cli-errors/src/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 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 { combineErrors, createFailError, createFlagError, isFailError } from './dev_cli_errors'; diff --git a/packages/kbn-dev-cli-errors/tsconfig.json b/packages/kbn-dev-cli-errors/tsconfig.json new file mode 100644 index 00000000000000..a8cfc2cceb08b8 --- /dev/null +++ b/packages/kbn-dev-cli-errors/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-dev-cli-runner/BUILD.bazel b/packages/kbn-dev-cli-runner/BUILD.bazel new file mode 100644 index 00000000000000..a4ae09228a8b48 --- /dev/null +++ b/packages/kbn-dev-cli-runner/BUILD.bazel @@ -0,0 +1,136 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "kbn-dev-cli-runner" +PKG_REQUIRE_NAME = "@kbn/dev-cli-runner" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//chalk", + "@npm//dedent", + "@npm//execa", + "@npm//exit-hook", + "@npm//getopts", + "@npm//normalize-path", + "//packages/kbn-dev-cli-errors", + "//packages/kbn-ci-stats-reporter", + "//packages/kbn-dev-proc-runner", + "//packages/kbn-tooling-log", + "//packages/kbn-utils", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//@types/dedent", + "@npm//@types/normalize-path", + "@npm//chalk", + "@npm//execa", + "@npm//exit-hook", + "@npm//getopts", + "//packages/kbn-dev-cli-errors:npm_module_types", + "//packages/kbn-ci-stats-reporter:npm_module_types", + "//packages/kbn-dev-proc-runner:npm_module_types", + "//packages/kbn-tooling-log:npm_module_types", + "//packages/kbn-utils:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-dev-utils/src/run/README.md b/packages/kbn-dev-cli-runner/README.md similarity index 98% rename from packages/kbn-dev-utils/src/run/README.md rename to packages/kbn-dev-cli-runner/README.md index 99893a62376689..0ef87f0e8e0785 100644 --- a/packages/kbn-dev-utils/src/run/README.md +++ b/packages/kbn-dev-cli-runner/README.md @@ -1,4 +1,4 @@ -# @kbn/dev-utils > run() +# @kbn/dev-cli-runner Helper functions for writing little scripts for random build/ci/dev tasks. @@ -8,7 +8,7 @@ Define the function that should validate the CLI arguments and call your task fn ```ts // dev/my_task/run_my_task.ts -import { createFlagError, run } from '@kbn/dev-utils'; +import { createFlagError, run } from '@kbn/dev-cli-runner'; run( async ({ flags, log }) => { diff --git a/packages/kbn-dev-cli-runner/jest.config.js b/packages/kbn-dev-cli-runner/jest.config.js new file mode 100644 index 00000000000000..a4262cc6b0daff --- /dev/null +++ b/packages/kbn-dev-cli-runner/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-dev-cli-runner'], +}; diff --git a/packages/kbn-dev-cli-runner/package.json b/packages/kbn-dev-cli-runner/package.json new file mode 100644 index 00000000000000..779d6f48a4fb7a --- /dev/null +++ b/packages/kbn-dev-cli-runner/package.json @@ -0,0 +1,10 @@ +{ + "name": "@kbn/dev-cli-runner", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0", + "kibana": { + "devOnly": true + } +} diff --git a/packages/kbn-dev-utils/src/run/cleanup.ts b/packages/kbn-dev-cli-runner/src/cleanup.ts similarity index 97% rename from packages/kbn-dev-utils/src/run/cleanup.ts rename to packages/kbn-dev-cli-runner/src/cleanup.ts index 87da0f1e704df4..d52b8bd18f87cb 100644 --- a/packages/kbn-dev-utils/src/run/cleanup.ts +++ b/packages/kbn-dev-cli-runner/src/cleanup.ts @@ -10,8 +10,7 @@ import { inspect } from 'util'; import exitHook from 'exit-hook'; import { ToolingLog } from '@kbn/tooling-log'; - -import { isFailError } from './fail'; +import { isFailError } from '@kbn/dev-cli-errors'; /** * A function which will be called when the CLI is torn-down which should diff --git a/packages/kbn-dev-utils/src/run/flags.test.ts b/packages/kbn-dev-cli-runner/src/flags.test.ts similarity index 100% rename from packages/kbn-dev-utils/src/run/flags.test.ts rename to packages/kbn-dev-cli-runner/src/flags.test.ts diff --git a/packages/kbn-dev-utils/src/run/flags.ts b/packages/kbn-dev-cli-runner/src/flags.ts similarity index 100% rename from packages/kbn-dev-utils/src/run/flags.ts rename to packages/kbn-dev-cli-runner/src/flags.ts index 20357c8204ae77..919da586f7ba6e 100644 --- a/packages/kbn-dev-utils/src/run/flags.ts +++ b/packages/kbn-dev-cli-runner/src/flags.ts @@ -7,8 +7,8 @@ */ import getopts from 'getopts'; - import { LOG_LEVEL_FLAGS, DEFAULT_LOG_LEVEL } from '@kbn/tooling-log'; + import { RunOptions } from './run'; export interface Flags { diff --git a/packages/kbn-dev-utils/src/run/help.test.ts b/packages/kbn-dev-cli-runner/src/help.test.ts similarity index 100% rename from packages/kbn-dev-utils/src/run/help.test.ts rename to packages/kbn-dev-cli-runner/src/help.test.ts diff --git a/packages/kbn-dev-utils/src/run/help.ts b/packages/kbn-dev-cli-runner/src/help.ts similarity index 98% rename from packages/kbn-dev-utils/src/run/help.ts rename to packages/kbn-dev-cli-runner/src/help.ts index 3bf2738b82748b..a7dc17aa43f17f 100644 --- a/packages/kbn-dev-utils/src/run/help.ts +++ b/packages/kbn-dev-cli-runner/src/help.ts @@ -9,7 +9,6 @@ import Path from 'path'; import chalk from 'chalk'; -import 'core-js/features/string/repeat'; import dedent from 'dedent'; import { getLogLevelFlagsHelp } from '@kbn/tooling-log'; diff --git a/packages/kbn-dev-utils/src/run/index.ts b/packages/kbn-dev-cli-runner/src/index.ts similarity index 95% rename from packages/kbn-dev-utils/src/run/index.ts rename to packages/kbn-dev-cli-runner/src/index.ts index 505ef4ee264d69..048f4ae0bd66fe 100644 --- a/packages/kbn-dev-utils/src/run/index.ts +++ b/packages/kbn-dev-cli-runner/src/index.ts @@ -9,5 +9,4 @@ export * from './run'; export * from './run_with_commands'; export * from './flags'; -export * from './fail'; export type { CleanupTask } from './cleanup'; diff --git a/packages/kbn-dev-utils/src/run/metrics.ts b/packages/kbn-dev-cli-runner/src/metrics.ts similarity index 99% rename from packages/kbn-dev-utils/src/run/metrics.ts rename to packages/kbn-dev-cli-runner/src/metrics.ts index 36c80c659b0169..af9ee7478f637c 100644 --- a/packages/kbn-dev-utils/src/run/metrics.ts +++ b/packages/kbn-dev-cli-runner/src/metrics.ts @@ -7,8 +7,9 @@ */ import path from 'path'; -import { REPO_ROOT } from '@kbn/utils'; + import normalizePath from 'normalize-path'; +import { REPO_ROOT } from '@kbn/utils'; import { CiStatsReporter } from '@kbn/ci-stats-reporter'; import { ToolingLog } from '@kbn/tooling-log'; diff --git a/packages/kbn-dev-utils/src/run/run.ts b/packages/kbn-dev-cli-runner/src/run.ts similarity index 94% rename from packages/kbn-dev-utils/src/run/run.ts rename to packages/kbn-dev-cli-runner/src/run.ts index 17630826299f2b..bbccfdde564f81 100644 --- a/packages/kbn-dev-utils/src/run/run.ts +++ b/packages/kbn-dev-cli-runner/src/run.ts @@ -7,9 +7,10 @@ */ import { pickLevelFromFlags, ToolingLog, LogLevel } from '@kbn/tooling-log'; -import { createFlagError } from './fail'; +import { ProcRunner, withProcRunner } from '@kbn/dev-proc-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; + import { Flags, getFlags, FlagOptions } from './flags'; -import { ProcRunner, withProcRunner } from '../proc_runner'; import { getHelp } from './help'; import { CleanupTask, Cleanup } from './cleanup'; import { Metrics, MetricsMeta } from './metrics'; diff --git a/packages/kbn-dev-utils/src/run/run_with_commands.test.ts b/packages/kbn-dev-cli-runner/src/run_with_commands.test.ts similarity index 97% rename from packages/kbn-dev-utils/src/run/run_with_commands.test.ts rename to packages/kbn-dev-cli-runner/src/run_with_commands.test.ts index 0005ab8f27c018..c740087b40c30a 100644 --- a/packages/kbn-dev-utils/src/run/run_with_commands.test.ts +++ b/packages/kbn-dev-cli-runner/src/run_with_commands.test.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ -import { RunWithCommands } from './run_with_commands'; import { ToolingLog, ToolingLogCollectingWriter } from '@kbn/tooling-log'; -import { ProcRunner } from '../proc_runner'; +import { ProcRunner } from '@kbn/dev-proc-runner'; + +import { RunWithCommands } from './run_with_commands'; const testLog = new ToolingLog(); const testLogWriter = new ToolingLogCollectingWriter(); diff --git a/packages/kbn-dev-utils/src/run/run_with_commands.ts b/packages/kbn-dev-cli-runner/src/run_with_commands.ts similarity index 97% rename from packages/kbn-dev-utils/src/run/run_with_commands.ts rename to packages/kbn-dev-cli-runner/src/run_with_commands.ts index eb6df27a5fa53a..94b167671d21b9 100644 --- a/packages/kbn-dev-utils/src/run/run_with_commands.ts +++ b/packages/kbn-dev-cli-runner/src/run_with_commands.ts @@ -7,12 +7,13 @@ */ import { ToolingLog, pickLevelFromFlags } from '@kbn/tooling-log'; +import { withProcRunner } from '@kbn/dev-proc-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; + import { RunContext, RunOptions } from './run'; import { getFlags, FlagOptions, mergeFlagOptions } from './flags'; import { Cleanup } from './cleanup'; import { getHelpForAllCommands, getCommandLevelHelp } from './help'; -import { createFlagError } from './fail'; -import { withProcRunner } from '../proc_runner'; import { Metrics } from './metrics'; export type CommandRunFn = (context: RunContext & T) => Promise | void; diff --git a/packages/kbn-dev-cli-runner/tsconfig.json b/packages/kbn-dev-cli-runner/tsconfig.json new file mode 100644 index 00000000000000..a8cfc2cceb08b8 --- /dev/null +++ b/packages/kbn-dev-cli-runner/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-dev-proc-runner/BUILD.bazel b/packages/kbn-dev-proc-runner/BUILD.bazel new file mode 100644 index 00000000000000..7de9c5065e3f75 --- /dev/null +++ b/packages/kbn-dev-proc-runner/BUILD.bazel @@ -0,0 +1,130 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "kbn-dev-proc-runner" +PKG_REQUIRE_NAME = "@kbn/dev-proc-runner" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//chalk", + "@npm//exit-hook", + "@npm//execa", + "@npm//rxjs", + "@npm//tree-kill", + "//packages/kbn-dev-cli-errors", + "//packages/kbn-tooling-log", + "//packages/kbn-stdio-dev-helpers", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//chalk", + "@npm//exit-hook", + "@npm//execa", + "@npm//rxjs", + "@npm//tree-kill", + "//packages/kbn-dev-cli-errors:npm_module_types", + "//packages/kbn-tooling-log:npm_module_types", + "//packages/kbn-stdio-dev-helpers:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-dev-proc-runner/README.md b/packages/kbn-dev-proc-runner/README.md new file mode 100644 index 00000000000000..5e6741cc3581f3 --- /dev/null +++ b/packages/kbn-dev-proc-runner/README.md @@ -0,0 +1,3 @@ +# @kbn/dev-proc-runner + +A class orignally extracted from [`grunt-run`](https://github.com/spalger/grunt-run) which allows running processes without managing the state of the process while it's running, wait for a specific log message or other sign of readiness, and then stop all started processes with a single call. diff --git a/packages/kbn-dev-proc-runner/jest.config.js b/packages/kbn-dev-proc-runner/jest.config.js new file mode 100644 index 00000000000000..d01d0f243dd8aa --- /dev/null +++ b/packages/kbn-dev-proc-runner/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-dev-proc-runner'], +}; diff --git a/packages/kbn-dev-proc-runner/package.json b/packages/kbn-dev-proc-runner/package.json new file mode 100644 index 00000000000000..2de282a948edac --- /dev/null +++ b/packages/kbn-dev-proc-runner/package.json @@ -0,0 +1,10 @@ +{ + "name": "@kbn/dev-proc-runner", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0", + "kibana": { + "devOnly": true + } +} diff --git a/packages/kbn-dev-utils/src/proc_runner/index.ts b/packages/kbn-dev-proc-runner/src/index.ts similarity index 100% rename from packages/kbn-dev-utils/src/proc_runner/index.ts rename to packages/kbn-dev-proc-runner/src/index.ts diff --git a/packages/kbn-dev-utils/src/proc_runner/proc.ts b/packages/kbn-dev-proc-runner/src/proc.ts similarity index 98% rename from packages/kbn-dev-utils/src/proc_runner/proc.ts rename to packages/kbn-dev-proc-runner/src/proc.ts index 323c1fb674317c..ffe7cb64641239 100644 --- a/packages/kbn-dev-utils/src/proc_runner/proc.ts +++ b/packages/kbn-dev-proc-runner/src/proc.ts @@ -6,20 +6,19 @@ * Side Public License, v 1. */ -import execa from 'execa'; import { statSync } from 'fs'; +import { promisify } from 'util'; +import execa from 'execa'; import * as Rx from 'rxjs'; import { tap, share, take, mergeMap, map, ignoreElements } from 'rxjs/operators'; import chalk from 'chalk'; - import treeKill from 'tree-kill'; -import { promisify } from 'util'; -const treeKillAsync = promisify((...args: [number, string, any]) => treeKill(...args)); - import { ToolingLog } from '@kbn/tooling-log'; import { observeLines } from '@kbn/stdio-dev-helpers'; -import { createFailError } from '../run'; +import { createFailError } from '@kbn/dev-cli-errors'; + +const treeKillAsync = promisify((...args: [number, string, any]) => treeKill(...args)); const SECOND = 1000; const STOP_TIMEOUT = 30 * SECOND; diff --git a/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts b/packages/kbn-dev-proc-runner/src/proc_runner.ts similarity index 99% rename from packages/kbn-dev-utils/src/proc_runner/proc_runner.ts rename to packages/kbn-dev-proc-runner/src/proc_runner.ts index 654a9d1080135a..56a6ee48c31506 100644 --- a/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts +++ b/packages/kbn-dev-proc-runner/src/proc_runner.ts @@ -8,9 +8,9 @@ import * as Rx from 'rxjs'; import exitHook from 'exit-hook'; - import { ToolingLog } from '@kbn/tooling-log'; -import { createFailError } from '../run'; +import { createFailError } from '@kbn/dev-cli-errors'; + import { Proc, ProcOptions, startProc } from './proc'; const SECOND = 1000; diff --git a/packages/kbn-dev-utils/src/proc_runner/with_proc_runner.test.ts b/packages/kbn-dev-proc-runner/src/with_proc_runner.test.ts similarity index 100% rename from packages/kbn-dev-utils/src/proc_runner/with_proc_runner.test.ts rename to packages/kbn-dev-proc-runner/src/with_proc_runner.test.ts diff --git a/packages/kbn-dev-utils/src/proc_runner/with_proc_runner.ts b/packages/kbn-dev-proc-runner/src/with_proc_runner.ts similarity index 100% rename from packages/kbn-dev-utils/src/proc_runner/with_proc_runner.ts rename to packages/kbn-dev-proc-runner/src/with_proc_runner.ts diff --git a/packages/kbn-dev-proc-runner/tsconfig.json b/packages/kbn-dev-proc-runner/tsconfig.json new file mode 100644 index 00000000000000..a8cfc2cceb08b8 --- /dev/null +++ b/packages/kbn-dev-proc-runner/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-dev-utils/BUILD.bazel b/packages/kbn-dev-utils/BUILD.bazel index cf120920079879..6ed7342061178e 100644 --- a/packages/kbn-dev-utils/BUILD.bazel +++ b/packages/kbn-dev-utils/BUILD.bazel @@ -40,6 +40,9 @@ NPM_MODULE_EXTRA_FILES = [ ] RUNTIME_DEPS = [ + "//packages/kbn-dev-cli-runner", + "//packages/kbn-dev-cli-errors", + "//packages/kbn-dev-proc-runner", "//packages/kbn-std", "//packages/kbn-utils", "//packages/kbn-plugin-discovery", @@ -71,6 +74,9 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ + "//packages/kbn-dev-cli-runner:npm_module_types", + "//packages/kbn-dev-cli-errors:npm_module_types", + "//packages/kbn-dev-proc-runner:npm_module_types", "//packages/kbn-std:npm_module_types", "//packages/kbn-utils:npm_module_types", "//packages/kbn-plugin-discovery:npm_module_types", diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index ce8c54e0c7aba1..ddaa0ff37b47af 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -export { withProcRunner, ProcRunner } from './proc_runner'; export { CA_CERT_PATH, ES_KEY_PATH, @@ -20,7 +19,6 @@ export { KBN_P12_PATH, KBN_P12_PASSWORD, } from './certs'; -export * from './run'; export * from './axios'; export * from './ship_ci_stats_cli'; export * from './plugin_list'; diff --git a/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts b/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts index 556779233c18c9..c03658cad8e2ba 100644 --- a/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts +++ b/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts @@ -10,7 +10,7 @@ import Path from 'path'; import Fs from 'fs'; import { REPO_ROOT } from '@kbn/utils'; -import { run } from '../run'; +import { run } from '@kbn/dev-cli-runner'; import { discoverPlugins } from './discover_plugins'; import { generatePluginList } from './generate_plugin_list'; diff --git a/packages/kbn-dev-utils/src/precommit_hook/cli.ts b/packages/kbn-dev-utils/src/precommit_hook/cli.ts index 2fa8b2b725730b..b0d6e57eee6237 100644 --- a/packages/kbn-dev-utils/src/precommit_hook/cli.ts +++ b/packages/kbn-dev-utils/src/precommit_hook/cli.ts @@ -11,8 +11,8 @@ import { chmod, writeFile } from 'fs'; import { promisify } from 'util'; import { REPO_ROOT } from '@kbn/utils'; -import { run } from '../run'; -import { createFailError } from '../run'; +import { run } from '@kbn/dev-cli-runner'; +import { createFailError } from '@kbn/dev-cli-errors'; import { SCRIPT_SOURCE } from './script_source'; import { getGitDir, isCorrectGitVersionInstalled } from './git_utils'; diff --git a/packages/kbn-dev-utils/src/ship_ci_stats_cli.ts b/packages/kbn-dev-utils/src/ship_ci_stats_cli.ts index 6afd85e49361df..6f80a22a32ed9f 100644 --- a/packages/kbn-dev-utils/src/ship_ci_stats_cli.ts +++ b/packages/kbn-dev-utils/src/ship_ci_stats_cli.ts @@ -11,7 +11,8 @@ import Fs from 'fs'; import { CiStatsReporter } from '@kbn/ci-stats-reporter'; -import { run, createFlagError, createFailError } from './run'; +import { createFlagError, createFailError } from '@kbn/dev-cli-errors'; +import { run } from '@kbn/dev-cli-runner'; export function shipCiStatsCli() { run( diff --git a/packages/kbn-dev-utils/src/vscode_config/update_vscode_config_cli.ts b/packages/kbn-dev-utils/src/vscode_config/update_vscode_config_cli.ts index 70ae7c35765f58..8a2c85ad6ff27f 100644 --- a/packages/kbn-dev-utils/src/vscode_config/update_vscode_config_cli.ts +++ b/packages/kbn-dev-utils/src/vscode_config/update_vscode_config_cli.ts @@ -12,7 +12,7 @@ import Fsp from 'fs/promises'; import { REPO_ROOT } from '@kbn/utils'; import dedent from 'dedent'; -import { run } from '../run'; +import { run } from '@kbn/dev-cli-runner'; import { MANAGED_CONFIG_KEYS, MANAGED_CONFIG_FILES } from './managed_config_keys'; import { updateVscodeConfig } from './update_vscode_config'; diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts b/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts index 41bb6400b92ab6..460a695f83fcaf 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts @@ -9,7 +9,8 @@ import Fs from 'fs'; import Path from 'path'; -import { run, createFlagError } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; import { CiStatsReporter } from '@kbn/ci-stats-reporter'; import { REPO_ROOT } from '@kbn/utils'; import { Project } from 'ts-morph'; diff --git a/packages/kbn-es-archiver/src/cli.ts b/packages/kbn-es-archiver/src/cli.ts index cf871abe6f18fe..d1204f1a4facb5 100644 --- a/packages/kbn-es-archiver/src/cli.ts +++ b/packages/kbn-es-archiver/src/cli.ts @@ -17,7 +17,9 @@ import Url from 'url'; import readline from 'readline'; import Fs from 'fs'; -import { RunWithCommands, createFlagError, CA_CERT_PATH } from '@kbn/dev-utils'; +import { CA_CERT_PATH } from '@kbn/dev-utils'; +import { RunWithCommands } from '@kbn/dev-cli-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; import { readConfigFile, KbnClient, EsVersion } from '@kbn/test'; import { Client, HttpConnection } from '@elastic/elasticsearch'; diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index eecaef06be453c..5c410523d70ca6 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -325,29 +325,41 @@ exports.Cluster = class Cluster { this._log.info(chalk.bold('Starting')); this._log.indent(4); - const esArgs = [ - 'action.destructive_requires_name=true', - 'ingest.geoip.downloader.enabled=false', - 'search.check_ccs_compatibility=true', - 'cluster.routing.allocation.disk.threshold_enabled=false', - ].concat(options.esArgs || []); + const esArgs = new Map([ + ['action.destructive_requires_name', 'true'], + ['cluster.routing.allocation.disk.threshold_enabled', 'false'], + ['ingest.geoip.downloader.enabled', 'false'], + ['search.check_ccs_compatibility', 'true'], + ]); + + // options.esArgs overrides the default esArg values + for (const arg of [].concat(options.esArgs || [])) { + const [key, ...value] = arg.split('='); + esArgs.set(key.trim(), value.join('=').trim()); + } // Add to esArgs if ssl is enabled if (this._ssl) { - esArgs.push('xpack.security.http.ssl.enabled=true'); - - // Include default keystore settings only if keystore isn't configured. - if (!esArgs.some((arg) => arg.startsWith('xpack.security.http.ssl.keystore'))) { - esArgs.push(`xpack.security.http.ssl.keystore.path=${ES_NOPASSWORD_P12_PATH}`); - esArgs.push(`xpack.security.http.ssl.keystore.type=PKCS12`); + esArgs.set('xpack.security.http.ssl.enabled', 'true'); + // Include default keystore settings only if ssl isn't disabled by esArgs and keystore isn't configured. + if (!esArgs.get('xpack.security.http.ssl.keystore.path')) { // We are explicitly using ES_NOPASSWORD_P12_PATH instead of ES_P12_PATH + ES_P12_PASSWORD. The reasoning for this is that setting // the keystore password using environment variables causes Elasticsearch to emit deprecation warnings. + esArgs.set(`xpack.security.http.ssl.keystore.path`, ES_NOPASSWORD_P12_PATH); + esArgs.set(`xpack.security.http.ssl.keystore.type`, `PKCS12`); } } - const args = parseSettings(extractConfigFiles(esArgs, installPath, { log: this._log }), { - filter: SettingsFilter.NonSecureOnly, - }).reduce( + const args = parseSettings( + extractConfigFiles( + Array.from(esArgs).map((e) => e.join('=')), + installPath, + { log: this._log } + ), + { + filter: SettingsFilter.NonSecureOnly, + } + ).reduce( (acc, [settingName, settingValue]) => acc.concat(['-E', `${settingName}=${settingValue}`]), [] ); diff --git a/packages/kbn-es/src/integration_tests/cluster.test.js b/packages/kbn-es/src/integration_tests/cluster.test.js index 250bc9ac883b35..1a871667bd7a9e 100644 --- a/packages/kbn-es/src/integration_tests/cluster.test.js +++ b/packages/kbn-es/src/integration_tests/cluster.test.js @@ -304,9 +304,41 @@ describe('#start(installPath)', () => { Array [ Array [ "action.destructive_requires_name=true", + "cluster.routing.allocation.disk.threshold_enabled=false", "ingest.geoip.downloader.enabled=false", "search.check_ccs_compatibility=true", + ], + undefined, + Object { + "log": , + }, + ], + ] + `); + }); + + it(`allows overriding search.check_ccs_compatibility`, async () => { + mockEsBin({ start: true }); + + extractConfigFiles.mockReturnValueOnce([]); + + const cluster = new Cluster({ + log, + ssl: false, + }); + + await cluster.start(undefined, { + esArgs: ['search.check_ccs_compatibility=false'], + }); + + expect(extractConfigFiles.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Array [ + "action.destructive_requires_name=true", "cluster.routing.allocation.disk.threshold_enabled=false", + "ingest.geoip.downloader.enabled=false", + "search.check_ccs_compatibility=false", ], undefined, Object { @@ -384,9 +416,9 @@ describe('#run()', () => { Array [ Array [ "action.destructive_requires_name=true", + "cluster.routing.allocation.disk.threshold_enabled=false", "ingest.geoip.downloader.enabled=false", "search.check_ccs_compatibility=true", - "cluster.routing.allocation.disk.threshold_enabled=false", ], undefined, Object { diff --git a/packages/kbn-es/src/utils/build_snapshot.ts b/packages/kbn-es/src/utils/build_snapshot.ts index 2f5181a369f96d..46d14910a27c2e 100644 --- a/packages/kbn-es/src/utils/build_snapshot.ts +++ b/packages/kbn-es/src/utils/build_snapshot.ts @@ -9,7 +9,7 @@ import path from 'path'; import os from 'os'; -import { withProcRunner } from '@kbn/dev-utils'; +import { withProcRunner } from '@kbn/dev-proc-runner'; import { ToolingLog } from '@kbn/tooling-log'; import { createCliError } from '../errors'; diff --git a/packages/kbn-eslint-config/.eslintrc.js b/packages/kbn-eslint-config/.eslintrc.js index a12417411a1f8d..4fd29b8b3672eb 100644 --- a/packages/kbn-eslint-config/.eslintrc.js +++ b/packages/kbn-eslint-config/.eslintrc.js @@ -187,6 +187,44 @@ module.exports = { 'sortPackageJson', ] }, + { + fromPackage: '@kbn/dev-utils', + toPackage: '@kbn/dev-cli-runner', + exportNames: [ + 'run', + 'Command', + 'RunWithCommands', + 'CleanupTask', + 'Command', + 'CommandRunFn', + 'FlagOptions', + 'Flags', + 'RunContext', + 'RunFn', + 'RunOptions', + 'RunWithCommands', + 'RunWithCommandsOptions', + 'getFlags', + 'mergeFlagOptions' + ] + }, + { + fromPackage: '@kbn/dev-utils', + toPackage: '@kbn/dev-cli-errors', + exportNames: [ + 'createFailError', + 'createFlagError', + 'isFailError', + ] + }, + { + fromPackage: '@kbn/dev-utils', + toPackage: '@kbn/dev-proc-runner', + exportNames: [ + 'withProcRunner', + 'ProcRunner', + ] + }, ]], '@kbn/eslint/no_async_promise_body': 'error', diff --git a/packages/kbn-eslint-config/README.md b/packages/kbn-eslint-config/README.md deleted file mode 100644 index cca5551a07abac..00000000000000 --- a/packages/kbn-eslint-config/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# elastic-eslint-config-kibana - -The eslint config used by the kibana team - -## Usage - -To use this eslint config, just install the peer dependencies and reference it -in your `.eslintrc`: - -```javascript -{ - extends: [ - '@kbn/eslint-config' - ] -} -``` - -## Optional jest config - -If the project uses the [jest test runner](https://facebook.github.io/jest/), -the `@kbn/eslint-config/jest` config can be extended as well to use -`eslint-plugin-jest` and add settings specific to it: - -```javascript -{ - extends: [ - '@kbn/eslint-config', - '@kbn/eslint-config/jest' - ] -} -``` diff --git a/packages/kbn-eslint-config/README.mdx b/packages/kbn-eslint-config/README.mdx new file mode 100644 index 00000000000000..16eeed22b9e869 --- /dev/null +++ b/packages/kbn-eslint-config/README.mdx @@ -0,0 +1,39 @@ +--- +id: kibDevDocsOpsEslintConfig +slug: /kibana-dev-docs/ops/eslint-config +title: "@kbn/eslint-config" +description: A package holding the main eslint configs on Kibana +date: 2022-05-16 +tags: ['kibana', 'dev', 'contributor', 'operations', 'eslint', 'custom', 'config'] +--- + +This package is used to group and provide the eslint configurations used by the Kibana team. +It defines default plugins, env, parserOptions and rules for javascript, typescript, and react code bases. +Optionally it can also be used with jest as explained below. + +## Usage + +To use this eslint config, it needs to be referenced in the `.eslintrc` file: + +```javascript +module.exports = { + extends: [ + '@kbn/eslint-config' + ] +} +``` + +## Optional jest eslint config + +If [jest test runner](https://facebook.github.io/jest/) is used, +the `@kbn/eslint-config/jest` config can be extended as well to use +`eslint-plugin-jest` and add settings specific to it: + +```javascript +module.exports = { + extends: [ + '@kbn/eslint-config', + '@kbn/eslint-config/jest' + ] +} +``` diff --git a/packages/kbn-eslint-plugin-eslint/BUILD.bazel b/packages/kbn-eslint-plugin-eslint/BUILD.bazel index f8242afb6cf9e6..d9966e0a7068b4 100644 --- a/packages/kbn-eslint-plugin-eslint/BUILD.bazel +++ b/packages/kbn-eslint-plugin-eslint/BUILD.bazel @@ -26,7 +26,6 @@ filegroup( NPM_MODULE_EXTRA_FILES = [ "package.json", - "README.md", ] RUNTIME_DEPS = [ diff --git a/packages/kbn-eslint-plugin-eslint/README.md b/packages/kbn-eslint-plugin-eslint/README.md deleted file mode 100644 index 6b6d0a0059a1f9..00000000000000 --- a/packages/kbn-eslint-plugin-eslint/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Custom ESLint rules for Kibana - -This package contains custom ESLint rules used for Kibana development. \ No newline at end of file diff --git a/packages/kbn-eslint-plugin-eslint/README.mdx b/packages/kbn-eslint-plugin-eslint/README.mdx new file mode 100644 index 00000000000000..3c7eff4620b0aa --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/README.mdx @@ -0,0 +1,147 @@ +--- +id: kibDevDocsOpsEslintPluginEslint +slug: /kibana-dev-docs/ops/eslint-plugin-eslint +title: "@kbn/eslint-plugin-eslint" +description: A package holding an eslint plugin with custom rules used on Kibana +date: 2022-05-17 +tags: ['kibana', 'dev', 'contributor', 'operations', 'eslint', 'plugin'] +--- + +An ESLint plugin exposing custom rules used and built specifically for development within Kibana. +Next you can find information on each on. + +## disallow-license-headers + +Disallows a given group of license header texts on a group of files. + +```javascript +module.exports = { + overrides: [ + { + files: ['**/*.{js,mjs,ts,tsx}'], + rules: { + '@kbn/eslint/disallow-license-headers': [ + 'error', + { + licenses: [ + "LICENSE_TEXT" + ], + }, + ], + } + } + ] +} +``` + +## module_migration + +Offers a way to force a migration from a given node module into another as an alternative. + +```javascript +module.exports = { + overrides: [ + { + files: ['**/*.{js,mjs,ts,tsx}'], + rules: { + '@kbn/eslint/module_migration': [ + 'error', + [ + { + from: 'expect.js', + to: '@kbn/expect', + } + ], + ], + } + } + ] +} +``` + +## no_async_foreach + +Disallows passing an async function to .forEach which will avoid promise rejections from being handled. asyncForEach() or a similar helper from "@kbn/std" should be used instead. + +## no_async_promise_body + +Disallows the usage of an async function as a constructor for a Promise function without a try catch in place. + +## no_constructor_args_in_property_initializers + +Disallows the usage of constructor arguments into class property initializers. + +## no_export_all + +Disables the usage of `export *`. + +## no_this_in_property_initializers + +Disallows the usage of `this` into class property initializers and enforce to define the property value into the constructor. + +## no_trailing_import_slash + +Disables the usage of a trailing slash in a node module import. + +## no-restricted-paths + +Defines a set of import paths valid to be imported for a given group of files. + +``` +module.exports = { + overrides: [ + { + files: ['**/*.{js,mjs,ts,tsx}'], + rules: { + '@kbn/eslint/no-restricted-paths': [ + 'error', + { + basePath: __dirname, + zones: [ + { + target: [ + '(src|x-pack)/plugins/**/(public|server)/**/*', + 'examples/**/*', + '!(src|x-pack)/**/*.test.*', + '!(x-pack/)?test/**/*', + ], + from: [ + '(src|x-pack)/plugins/**/(public|server)/**/*', + '!(src|x-pack)/plugins/**/(public|server)/mocks/index.{js,mjs,ts}', + '!(src|x-pack)/plugins/**/(public|server)/(index|mocks).{js,mjs,ts,tsx}', + '!(src|x-pack)/plugins/**/__stories__/index.{js,mjs,ts,tsx}', + '!(src|x-pack)/plugins/**/__fixtures__/index.{js,mjs,ts,tsx}', + ], + allowSameFolder: true, + errorMessage: 'Plugins may only import from top-level public and server modules.', + }, + ], + }, + ], + }, + } + ] +} +``` + +## require-license-header + +Requires a given license header text on a group of files. + +```javascript +module.exports = { + overrides: [ + { + files: ['**/*.{js,mjs,ts,tsx}'], + rules: { + '@kbn/eslint/require-license-header': [ + 'error', + { + license: "LICENSE_TEXT" + }, + ], + } + } + ] +} +``` \ No newline at end of file diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_constructor_args_in_property_initializers.js b/packages/kbn-eslint-plugin-eslint/rules/no_constructor_args_in_property_initializers.js index 672892730feb10..bda21b4e7378ec 100644 --- a/packages/kbn-eslint-plugin-eslint/rules/no_constructor_args_in_property_initializers.js +++ b/packages/kbn-eslint-plugin-eslint/rules/no_constructor_args_in_property_initializers.js @@ -28,7 +28,7 @@ function isTsParameterProperty(param) { * @param {string} arg */ const errorMsg = (arg) => - `The constructor argument "${arg}" can't be used in a class property intializer, define the property in the constructor instead`; + `The constructor argument "${arg}" can't be used in a class property initializer, define the property in the constructor instead`; /** @type {Rule} */ module.exports = { diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_constructor_args_in_property_initializers.test.js b/packages/kbn-eslint-plugin-eslint/rules/no_constructor_args_in_property_initializers.test.js index ca73b8947265e6..05a58902427c54 100644 --- a/packages/kbn-eslint-plugin-eslint/rules/no_constructor_args_in_property_initializers.test.js +++ b/packages/kbn-eslint-plugin-eslint/rules/no_constructor_args_in_property_initializers.test.js @@ -63,7 +63,7 @@ ruleTester.run('@kbn/eslint/no_constructor_args_in_property_initializers', rule, errors: [ { line: 2, - message: `The constructor argument "foo" can't be used in a class property intializer, define the property in the constructor instead`, + message: `The constructor argument "foo" can't be used in a class property initializer, define the property in the constructor instead`, }, ], }, @@ -77,7 +77,7 @@ ruleTester.run('@kbn/eslint/no_constructor_args_in_property_initializers', rule, errors: [ { line: 2, - message: `The constructor argument "foo" can't be used in a class property intializer, define the property in the constructor instead`, + message: `The constructor argument "foo" can't be used in a class property initializer, define the property in the constructor instead`, }, ], }, @@ -91,7 +91,7 @@ ruleTester.run('@kbn/eslint/no_constructor_args_in_property_initializers', rule, errors: [ { line: 2, - message: `The constructor argument "foo" can't be used in a class property intializer, define the property in the constructor instead`, + message: `The constructor argument "foo" can't be used in a class property initializer, define the property in the constructor instead`, }, ], }, @@ -111,7 +111,7 @@ ruleTester.run('@kbn/eslint/no_constructor_args_in_property_initializers', rule, errors: [ { line: 2, - message: `The constructor argument "deps" can't be used in a class property intializer, define the property in the constructor instead`, + message: `The constructor argument "deps" can't be used in a class property initializer, define the property in the constructor instead`, }, ], }, diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_this_in_property_initializers.js b/packages/kbn-eslint-plugin-eslint/rules/no_this_in_property_initializers.js index 4754dc144bd9b8..9b8349cc38a299 100644 --- a/packages/kbn-eslint-plugin-eslint/rules/no_this_in_property_initializers.js +++ b/packages/kbn-eslint-plugin-eslint/rules/no_this_in_property_initializers.js @@ -17,7 +17,7 @@ const esTypes = tsEstree.AST_NODE_TYPES; /** * @param {string} arg */ -const ERROR_MSG = `"this" is not fully initialized in class property intializers, define the value for this property in the constructor instead`; +const ERROR_MSG = `"this" is not fully initialized in class property initializers, define the value for this property in the constructor instead`; /** @type {Rule} */ module.exports = { diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_this_in_property_initializers.test.js b/packages/kbn-eslint-plugin-eslint/rules/no_this_in_property_initializers.test.js index 05a41d09d9ff83..88d894375c59a7 100644 --- a/packages/kbn-eslint-plugin-eslint/rules/no_this_in_property_initializers.test.js +++ b/packages/kbn-eslint-plugin-eslint/rules/no_this_in_property_initializers.test.js @@ -71,7 +71,7 @@ ruleTester.run('@kbn/eslint/no_this_in_property_initializers', rule, { errors: [ { line: 2, - message: `"this" is not fully initialized in class property intializers, define the value for this property in the constructor instead`, + message: `"this" is not fully initialized in class property initializers, define the value for this property in the constructor instead`, }, ], }, diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_trailing_import_slash.js b/packages/kbn-eslint-plugin-eslint/rules/no_trailing_import_slash.js index bd315bee93110b..81b2d64c432aa4 100644 --- a/packages/kbn-eslint-plugin-eslint/rules/no_trailing_import_slash.js +++ b/packages/kbn-eslint-plugin-eslint/rules/no_trailing_import_slash.js @@ -10,7 +10,7 @@ /** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.ImportDeclaration} ImportDeclaration */ const ERROR_MSG = - 'Using a trailing slash in package import statements causes issues with webpack and is inconsistent with the rest of the respository.'; + 'Using a trailing slash in package import statements causes issues with webpack and is inconsistent with the rest of the repository.'; /** @type {Rule} */ module.exports = { diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_trailing_import_slash.test.js b/packages/kbn-eslint-plugin-eslint/rules/no_trailing_import_slash.test.js index 0b122dfae0cf3f..119d6b3246a5bf 100644 --- a/packages/kbn-eslint-plugin-eslint/rules/no_trailing_import_slash.test.js +++ b/packages/kbn-eslint-plugin-eslint/rules/no_trailing_import_slash.test.js @@ -49,7 +49,7 @@ ruleTester.run('@kbn/eslint/no_trailing_import_slash', rule, { { line: 1, message: - 'Using a trailing slash in package import statements causes issues with webpack and is inconsistent with the rest of the respository.', + 'Using a trailing slash in package import statements causes issues with webpack and is inconsistent with the rest of the repository.', }, ], output: dedent` @@ -64,7 +64,7 @@ ruleTester.run('@kbn/eslint/no_trailing_import_slash', rule, { { line: 1, message: - 'Using a trailing slash in package import statements causes issues with webpack and is inconsistent with the rest of the respository.', + 'Using a trailing slash in package import statements causes issues with webpack and is inconsistent with the rest of the repository.', }, ], output: dedent` diff --git a/packages/kbn-eslint-plugin-imports/README.md b/packages/kbn-eslint-plugin-imports/README.mdx similarity index 86% rename from packages/kbn-eslint-plugin-imports/README.md rename to packages/kbn-eslint-plugin-imports/README.mdx index e4708eb3f925f1..1d6971d30e97f2 100644 --- a/packages/kbn-eslint-plugin-imports/README.md +++ b/packages/kbn-eslint-plugin-imports/README.mdx @@ -1,6 +1,12 @@ -# @kbn/eslint-plugin-imports - -ESLint plugin providing custom rules for validating imports in the Kibana repo with custom logic beyond what's possible with custom config to eslint-plugin-imports and even a custom resolver. +--- +id: kibDevDocsOpsEslintPluginImports +slug: /kibana-dev-docs/ops/kbn-eslint-plugin-imports +title: "@kbn/eslint-plugin-imports" +description: Custom ESLint rules for managing `imports` in the Kibana repository +tags: ['kibana', 'dev', 'contributor', 'operations', 'eslint', 'imports'] +--- + +`@kbn/eslint-plugin-imports` is an ESLint plugin providing custom rules for validating imports in the Kibana repo with custom logic beyond what's possible with custom config to eslint-plugin-imports and even a custom resolver. For the purposes of this ESLint plugin "imports" include: diff --git a/packages/kbn-expect/BUILD.bazel b/packages/kbn-expect/BUILD.bazel index 9f74cfe6a093da..415b402a3d05cf 100644 --- a/packages/kbn-expect/BUILD.bazel +++ b/packages/kbn-expect/BUILD.bazel @@ -19,7 +19,6 @@ filegroup( NPM_MODULE_EXTRA_FILES = [ "LICENSE.txt", "package.json", - "README.md", ] js_library( diff --git a/packages/kbn-expect/README.md b/packages/kbn-expect/README.mdx similarity index 95% rename from packages/kbn-expect/README.md rename to packages/kbn-expect/README.mdx index 51cf5bcf2ee528..38518b31093f47 100644 --- a/packages/kbn-expect/README.md +++ b/packages/kbn-expect/README.mdx @@ -1,6 +1,11 @@ -> NOTE: This is a local fork of https://github.com/Automattic/expect.js - -# @kbn/expect +--- +id: kibDevDocsOpsExpect +slug: /kibana-dev-docs/ops/expect +title: "@kbn/expect" +description: An assertion toolkit based on should.js +date: 2022-05-17 +tags: ['kibana', 'dev', 'contributor', 'operations', 'expect'] +--- Minimalistic BDD assertion toolkit based on [should.js](http://github.com/visionmedia/should.js) @@ -13,6 +18,8 @@ expect([]).to.be.an('array'); expect(window).not.to.be.an(Image); ``` +> NOTE: This is a local fork of https://github.com/Automattic/expect.js + ## Features - Cross-browser: works on IE6+, Firefox, Safari, Chrome, Opera. diff --git a/packages/kbn-generate/src/cli.ts b/packages/kbn-generate/src/cli.ts index 0b52f5bb4da72d..81e7a8cb85b55b 100644 --- a/packages/kbn-generate/src/cli.ts +++ b/packages/kbn-generate/src/cli.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { RunWithCommands } from '@kbn/dev-utils'; +import { RunWithCommands } from '@kbn/dev-cli-runner'; import { Render } from './lib/render'; import { ContextExtensions } from './generate_command'; diff --git a/packages/kbn-generate/src/commands/package_command.ts b/packages/kbn-generate/src/commands/package_command.ts index 92362e7feb35fd..4f5d58184aeac7 100644 --- a/packages/kbn-generate/src/commands/package_command.ts +++ b/packages/kbn-generate/src/commands/package_command.ts @@ -16,7 +16,7 @@ import { ESLint } from 'eslint'; import micromatch from 'micromatch'; import { REPO_ROOT } from '@kbn/utils'; import { discoverBazelPackages, BAZEL_PACKAGE_DIRS } from '@kbn/bazel-packages'; -import { createFailError, createFlagError, isFailError } from '@kbn/dev-utils'; +import { createFailError, createFlagError, isFailError } from '@kbn/dev-cli-errors'; import { sortPackageJson } from '@kbn/sort-package-json'; import { TEMPLATE_DIR, ROOT_PKG_DIR, PKG_TEMPLATE_DIR } from '../paths'; diff --git a/packages/kbn-generate/src/generate_command.ts b/packages/kbn-generate/src/generate_command.ts index 180fcee890c4fe..395a20b928b8e4 100644 --- a/packages/kbn-generate/src/generate_command.ts +++ b/packages/kbn-generate/src/generate_command.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Command } from '@kbn/dev-utils'; +import { Command } from '@kbn/dev-cli-runner'; import { Render } from './lib/render'; diff --git a/packages/kbn-generate/src/lib/validate_file.ts b/packages/kbn-generate/src/lib/validate_file.ts index 9a9ca28691537d..342a25e3d193d7 100644 --- a/packages/kbn-generate/src/lib/validate_file.ts +++ b/packages/kbn-generate/src/lib/validate_file.ts @@ -9,7 +9,8 @@ import Fsp from 'fs/promises'; import Path from 'path'; -import { createFailError, diffStrings } from '@kbn/dev-utils'; +import { diffStrings } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; import { ToolingLog } from '@kbn/tooling-log'; export async function validateFile(log: ToolingLog, usage: string, path: string, expected: string) { diff --git a/packages/kbn-jest-serializers/README.md b/packages/kbn-jest-serializers/README.md deleted file mode 100644 index 20d411e0db535a..00000000000000 --- a/packages/kbn-jest-serializers/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# @kbn/jest-serializers - -Shared serializers that may be useful when you're writing jest tests. To use them import the package and call one of the functions, passing the result to `expect.addSnapshotSerializer()`. - -Example: - -```ts -import { createAbsolutePathSerializer } from '@kbn/jest-serializers' - -expect.addSnapshotSerializer(createAbsolutePathSerializer()); -``` \ No newline at end of file diff --git a/packages/kbn-jest-serializers/README.mdx b/packages/kbn-jest-serializers/README.mdx new file mode 100644 index 00000000000000..279d12c2c545d2 --- /dev/null +++ b/packages/kbn-jest-serializers/README.mdx @@ -0,0 +1,38 @@ +--- +id: kibDevDocsOpsJestSerializers +slug: /kibana-dev-docs/ops/jest-serializers +title: "@kbn/jest-serializers" +description: A set of shared serializers to help on writing jest tests +date: 2022-05-17 +tags: ['kibana', 'dev', 'contributor', 'operations', 'jest', 'serializers'] +--- + +This package holds a set of shared serializers that may be useful when you're writing jest tests. To use them import the package and call one of the functions, passing the result to `expect.addSnapshotSerializer()`. + +## createAbsolutePathSerializer + +Replaces a given path starting a string with the provided replacer. Additionally also replaces any `\\` with `/` it founds. + +## createStripAnsiSerializer + +Strips ansi from a string. + +## createRecursiveSerializer + +It helps on printing recursive nodes. + +## createAnyInstanceSerializer + +It serializes any kind of instance inside `<>`. If it is a function calls the function inside the node otherwise prints as `Class.name`. + +## createReplaceSerializer + +Search for a substring using given Regex or string and replaces with a provided replacer. + +## Example + +```ts +import { createAbsolutePathSerializer } from '@kbn/jest-serializers' + +expect.addSnapshotSerializer(createAbsolutePathSerializer()); +``` \ No newline at end of file diff --git a/packages/kbn-optimizer/BUILD.bazel b/packages/kbn-optimizer/BUILD.bazel index 94da078df54b2e..2e0590af9ea4de 100644 --- a/packages/kbn-optimizer/BUILD.bazel +++ b/packages/kbn-optimizer/BUILD.bazel @@ -27,7 +27,6 @@ NPM_MODULE_EXTRA_FILES = [ "limits.yml", "package.json", "postcss.config.js", - "README.md" ] RUNTIME_DEPS = [ diff --git a/packages/kbn-optimizer/README.md b/packages/kbn-optimizer/README.mdx similarity index 81% rename from packages/kbn-optimizer/README.md rename to packages/kbn-optimizer/README.mdx index 337b729edf4409..f092c4cc5ffba0 100644 --- a/packages/kbn-optimizer/README.md +++ b/packages/kbn-optimizer/README.mdx @@ -1,4 +1,11 @@ -# @kbn/optimizer +--- +id: kibDevDocsOpsOptimizer +slug: /kibana-dev-docs/ops/optimizer +title: "@kbn/optimizer" +description: 'The tool which builds front-end bundles for plugins.' +date: 2022-05-16 +tags: ['kibana', 'dev', 'contributor', 'operations', 'optimizer', 'webpack'] +--- `@kbn/optimizer` is a package for building Kibana platform UI plugins (and hopefully more soon). @@ -61,7 +68,7 @@ KBN_OPTIMIZER_THEMES=* yarn start ## API -To run the optimizer from code, you can import the [`OptimizerConfig`][OptimizerConfig] class and [`runOptimizer`][Optimizer] function. Create an [`OptimizerConfig`][OptimizerConfig] instance by calling it's static `create()` method with some options, then pass it to the [`runOptimizer`][Optimizer] function. `runOptimizer()` returns an observable of update objects, which are summaries of the optimizer state plus an optional `event` property which describes the internal events occuring and may be of use. You can use the [`logOptimizerState()`][LogOptimizerState] helper to write the relevant bits of state to a tooling log or checkout it's implementation to see how the internal events like [`WorkerStdio`][ObserveWorker] and [`WorkerStarted`][ObserveWorker] are used. +To run the optimizer from code, you can import the [`OptimizerConfig`][OptimizerConfig] class and [`runOptimizer`][runOptimizer] function. Create an [`OptimizerConfig`][OptimizerConfig] instance by calling it's static `create()` method with some options, then pass it to the [`runOptimizer`][runOptimizer] function. `runOptimizer()` returns an observable of update objects, which are summaries of the optimizer state plus an optional `event` property which describes the internal events occuring and may be of use. You can use the [`logOptimizerState()`][LogOptimizerState] helper to write the relevant bits of state to a tooling log or checkout it's implementation to see how the internal events like [`WorkerStdio`][ObserveWorker] and [`WorkerStarted`][ObserveWorker] are used. Example: ```ts @@ -125,16 +132,15 @@ Workers have several error message they may emit which indicate unrecoverable er For an example of how to handle these states checkout the [`logOptimizerState()`][LogOptimizerState] helper. [PostCss]: https://postcss.org/ -[Cli]: src/cli.ts -[Optimizer]: src/optimizer.ts -[ObserveWorker]: src/observe_worker.ts -[CompilerMsg]: src/common/compiler_messages.ts -[WorkerMsg]: src/common/worker_messages.ts -[Bundle]: src/common/bundle.ts -[WebpackConfig]: src/worker/webpack.config.ts -[BundleDefinition]: src/common/bundle_definition.ts -[WorkerConfig]: src/common/worker_config.ts -[OptimizerConfig]: src/optimizer_config.ts -[LogOptimizerState]: src/log_optimizer_state.ts -[AssignBundlesToWorkers]: src/assign_bundles_to_workers.ts -[BuildTask]: ../../src/dev/build/tasks/build_kibana_platform_plugins.js \ No newline at end of file +[Cli]: https://github.com/elastic/kibana/blob/main/packages/kbn-optimizer/src/cli.ts +[runOptimizer]: https://github.com/elastic/kibana/blob/main/packages/kbn-optimizer/src/run_optimizer.ts +[ObserveWorker]: https://github.com/elastic/kibana/blob/main/packages/kbn-optimizer/src/optimizer/observe_worker.ts +[CompilerMsg]: https://github.com/elastic/kibana/blob/main/packages/kbn-optimizer/src/common/compiler_messages.ts +[WorkerMsg]: https://github.com/elastic/kibana/blob/main/packages/kbn-optimizer/src/common/worker_messages.ts +[Bundle]: https://github.com/elastic/kibana/blob/main/packages/kbn-optimizer/src/common/bundle.ts +[WebpackConfig]: https://github.com/elastic/kibana/blob/main/packages/kbn-optimizer/src/worker/webpack.config.ts +[WorkerConfig]: https://github.com/elastic/kibana/blob/main/packages/kbn-optimizer/src/common/worker_config.ts +[OptimizerConfig]: https://github.com/elastic/kibana/blob/main/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +[LogOptimizerState]: https://github.com/elastic/kibana/blob/main/packages/kbn-optimizer/src/log_optimizer_state.ts +[AssignBundlesToWorkers]: https://github.com/elastic/kibana/blob/main/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.ts +[BuildTask]: https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/build_kibana_platform_plugins.js \ No newline at end of file diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index c0b87ab7178de8..874f7c50477363 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -58,7 +58,7 @@ pageLoadAssetSize: telemetry: 51957 telemetryManagementSection: 38586 transform: 41007 - triggersActionsUi: 105800 #This is temporary. Check https://github.com/elastic/kibana/pull/130710#issuecomment-1119843458 & https://github.com/elastic/kibana/issues/130728 + triggersActionsUi: 107800 #This is temporary. Check https://github.com/elastic/kibana/pull/130710#issuecomment-1119843458 & https://github.com/elastic/kibana/issues/130728 upgradeAssistant: 81241 urlForwarding: 32579 usageCollection: 39762 diff --git a/packages/kbn-optimizer/src/audit_bundle_dependencies/find_babel_runtime_helpers_in_entry_bundles.ts b/packages/kbn-optimizer/src/audit_bundle_dependencies/find_babel_runtime_helpers_in_entry_bundles.ts index c07a9764af76ff..3846476869489c 100644 --- a/packages/kbn-optimizer/src/audit_bundle_dependencies/find_babel_runtime_helpers_in_entry_bundles.ts +++ b/packages/kbn-optimizer/src/audit_bundle_dependencies/find_babel_runtime_helpers_in_entry_bundles.ts @@ -8,7 +8,7 @@ import Path from 'path'; -import { run } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; import { REPO_ROOT } from '@kbn/utils'; import { OptimizerConfig } from '../optimizer'; diff --git a/packages/kbn-optimizer/src/audit_bundle_dependencies/find_node_libs_browser_polyfills_in_entry_bundles.ts b/packages/kbn-optimizer/src/audit_bundle_dependencies/find_node_libs_browser_polyfills_in_entry_bundles.ts index 06ad13da2b2f2c..4d283e95b1f6f6 100644 --- a/packages/kbn-optimizer/src/audit_bundle_dependencies/find_node_libs_browser_polyfills_in_entry_bundles.ts +++ b/packages/kbn-optimizer/src/audit_bundle_dependencies/find_node_libs_browser_polyfills_in_entry_bundles.ts @@ -8,7 +8,7 @@ import Path from 'path'; -import { run } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; import { REPO_ROOT } from '@kbn/utils'; import { OptimizerConfig } from '../optimizer'; diff --git a/packages/kbn-optimizer/src/audit_bundle_dependencies/find_target_node_imports.ts b/packages/kbn-optimizer/src/audit_bundle_dependencies/find_target_node_imports.ts index dd1309f838e41f..6021950cdb40aa 100644 --- a/packages/kbn-optimizer/src/audit_bundle_dependencies/find_target_node_imports.ts +++ b/packages/kbn-optimizer/src/audit_bundle_dependencies/find_target_node_imports.ts @@ -8,7 +8,7 @@ import Path from 'path'; -import { run } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; import { REPO_ROOT } from '@kbn/utils'; import { OptimizerConfig } from '../optimizer'; diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index 6da282b8995db0..449c58fdee2182 100644 --- a/packages/kbn-optimizer/src/cli.ts +++ b/packages/kbn-optimizer/src/cli.ts @@ -10,7 +10,8 @@ import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; import { lastValueFrom } from 'rxjs'; -import { run, createFlagError, Flags } from '@kbn/dev-utils'; +import { run, Flags } from '@kbn/dev-cli-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; import { logOptimizerState } from './log_optimizer_state'; import { logOptimizerProgress } from './log_optimizer_progress'; diff --git a/packages/kbn-optimizer/src/limits.ts b/packages/kbn-optimizer/src/limits.ts index a63012f37a52ea..bd7ed501d3a5ff 100644 --- a/packages/kbn-optimizer/src/limits.ts +++ b/packages/kbn-optimizer/src/limits.ts @@ -11,7 +11,7 @@ import Path from 'path'; import dedent from 'dedent'; import Yaml from 'js-yaml'; -import { createFailError } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; import { ToolingLog } from '@kbn/tooling-log'; import { CiStatsMetric } from '@kbn/ci-stats-reporter'; diff --git a/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.ts b/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.ts index 19dd6492625bab..7127b4c0684d62 100644 --- a/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.ts +++ b/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.ts @@ -7,7 +7,7 @@ */ import { tap } from 'rxjs/operators'; -import { createFailError } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; import { pipeClosure } from '../common'; import { OptimizerUpdate$ } from '../run_optimizer'; diff --git a/packages/kbn-performance-testing-dataset-extractor/src/cli.ts b/packages/kbn-performance-testing-dataset-extractor/src/cli.ts index 7d16f625e4874a..972da35785c586 100644 --- a/packages/kbn-performance-testing-dataset-extractor/src/cli.ts +++ b/packages/kbn-performance-testing-dataset-extractor/src/cli.ts @@ -12,7 +12,8 @@ * *************************************************************/ -import { run, createFlagError } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; import { extractor } from './extractor'; export async function runExtractor() { diff --git a/packages/kbn-plugin-generator/src/cli.ts b/packages/kbn-plugin-generator/src/cli.ts index 259dbe66d7e590..a4a18317283c74 100644 --- a/packages/kbn-plugin-generator/src/cli.ts +++ b/packages/kbn-plugin-generator/src/cli.ts @@ -11,7 +11,8 @@ import Fs from 'fs'; import execa from 'execa'; import { REPO_ROOT } from '@kbn/utils'; -import { run, createFailError, createFlagError } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; +import { createFailError, createFlagError } from '@kbn/dev-cli-errors'; import { snakeCase } from './casing'; import { askQuestions, getDefaultAnswers } from './ask_questions'; diff --git a/packages/kbn-plugin-helpers/src/cli.ts b/packages/kbn-plugin-helpers/src/cli.ts index a10aa6ce16f8ef..7109f3aa2ebc02 100644 --- a/packages/kbn-plugin-helpers/src/cli.ts +++ b/packages/kbn-plugin-helpers/src/cli.ts @@ -8,7 +8,8 @@ import Path from 'path'; -import { RunWithCommands, createFlagError, createFailError } from '@kbn/dev-utils'; +import { RunWithCommands } from '@kbn/dev-cli-runner'; +import { createFlagError, createFailError } from '@kbn/dev-cli-errors'; import { findKibanaJson } from './find_kibana_json'; import { loadKibanaPlatformPlugin } from './load_kibana_platform_plugin'; diff --git a/packages/kbn-plugin-helpers/src/load_kibana_platform_plugin.ts b/packages/kbn-plugin-helpers/src/load_kibana_platform_plugin.ts index f2587473505205..fb01d6deeded78 100644 --- a/packages/kbn-plugin-helpers/src/load_kibana_platform_plugin.ts +++ b/packages/kbn-plugin-helpers/src/load_kibana_platform_plugin.ts @@ -10,7 +10,7 @@ import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; import { parseKibanaPlatformPlugin, KibanaPlatformPlugin } from '@kbn/plugin-discovery'; -import { createFailError } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; export type Plugin = KibanaPlatformPlugin; diff --git a/packages/kbn-shared-ux-components/src/empty_state/assets/index.tsx b/packages/kbn-shared-ux-components/src/empty_state/assets/index.tsx index 001efa143b1cc8..e42627da6983ff 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/assets/index.tsx +++ b/packages/kbn-shared-ux-components/src/empty_state/assets/index.tsx @@ -8,7 +8,7 @@ import React from 'react'; -import { EuiLoadingSpinner } from '@elastic/eui'; +import { EuiPanel } from '@elastic/eui'; import { withSuspense } from '@kbn/shared-ux-utility'; @@ -20,5 +20,5 @@ export const LazyDataViewIllustration = React.lazy(() => export const DataViewIllustration = withSuspense( LazyDataViewIllustration, - + ); diff --git a/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.stories.tsx b/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.stories.tsx index 6d0c240dd4c051..f544f21c353877 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.stories.tsx +++ b/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.stories.tsx @@ -32,15 +32,16 @@ const noDataConfig = { title: 'Add Integrations', }, }, - docsLink: 'http://www.docs.com', + docsLink: 'http://docs.elastic.dev', }; type Params = Pick & DataServiceFactoryConfig; export const PureComponent = (params: Params) => { const { solution, logo, hasESData, hasUserDataView } = params; + const serviceParams = { hasESData, hasUserDataView, hasDataViews: false }; - const services = servicesFactory(serviceParams); + const services = servicesFactory({ ...serviceParams, hasESData, hasUserDataView }); return ( { ); }; +export const PureComponentLoadingState = () => { + const dataCheck = () => new Promise((resolve, reject) => {}); + const services = { + ...servicesFactory({ hasESData: false, hasUserDataView: false, hasDataViews: false }), + data: { + hasESData: dataCheck, + hasUserDataView: dataCheck, + hasDataView: dataCheck, + }, + }; + return ( + + + + ); +}; + PureComponent.argTypes = { solution: { control: 'text', diff --git a/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.test.tsx b/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.test.tsx index 82fbd222b36406..4f565e55ef52ce 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.test.tsx +++ b/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; +import { EuiLoadingElastic } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { SharedUxServicesProvider, mockServicesFactory } from '@kbn/shared-ux-services'; @@ -68,4 +69,28 @@ describe('Kibana No Data Page', () => { expect(component.find(NoDataViews).length).toBe(1); expect(component.find(NoDataConfigPage).length).toBe(0); }); + + test('renders loading indicator', async () => { + const dataCheck = () => new Promise((resolve, reject) => {}); + const services = { + ...mockServicesFactory(), + data: { + hasESData: dataCheck, + hasUserDataView: dataCheck, + hasDataView: dataCheck, + }, + }; + const component = mountWithIntl( + + + + ); + + await act(() => new Promise(setImmediate)); + component.update(); + + expect(component.find(EuiLoadingElastic).length).toBe(1); + expect(component.find(NoDataViews).length).toBe(0); + expect(component.find(NoDataConfigPage).length).toBe(0); + }); }); diff --git a/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.tsx b/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.tsx index 2e54d0d9f6a675..89ba915c07cfda 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.tsx +++ b/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.tsx @@ -6,6 +6,7 @@ * Side Public License, v 1. */ import React, { useEffect, useState } from 'react'; +import { EuiLoadingElastic } from '@elastic/eui'; import { useData } from '@kbn/shared-ux-services'; import { NoDataConfigPage, NoDataPageProps } from '../page_template'; import { NoDataViews } from './no_data_views'; @@ -17,6 +18,7 @@ export interface Props { export const KibanaNoDataPage = ({ onDataViewCreated, noDataConfig }: Props) => { const { hasESData, hasUserDataView } = useData(); + const [isLoading, setIsLoading] = useState(true); const [dataExists, setDataExists] = useState(false); const [hasUserDataViews, setHasUserDataViews] = useState(false); @@ -24,12 +26,19 @@ export const KibanaNoDataPage = ({ onDataViewCreated, noDataConfig }: Props) => const checkData = async () => { setDataExists(await hasESData()); setHasUserDataViews(await hasUserDataView()); + setIsLoading(false); }; // TODO: add error handling // https://github.com/elastic/kibana/issues/130913 - checkData().catch(() => {}); + checkData().catch(() => { + setIsLoading(false); + }); }, [hasESData, hasUserDataView]); + if (isLoading) { + return ; + } + if (!dataExists) { return ; } diff --git a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.component.test.tsx b/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.component.test.tsx index 1d8028d4889a04..87dd68e202bc2d 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.component.test.tsx +++ b/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.component.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { EuiButton, EuiPanel } from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { NoDataViews } from './no_data_views.component'; import { DocumentationLink } from './documentation_link'; @@ -21,7 +21,7 @@ describe('', () => { dataViewsDocLink={'dummy'} /> ); - expect(component.find(EuiPanel).length).toBe(1); + expect(component.find(EuiEmptyPrompt).length).toBe(1); expect(component.find(EuiButton).length).toBe(1); expect(component.find(DocumentationLink).length).toBe(1); }); diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap b/packages/kbn-shared-ux-components/src/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap index 0046e9c3fd3c1d..45b168c34034a0 100644 --- a/packages/kbn-shared-ux-components/src/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap +++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap @@ -29,6 +29,7 @@ exports[`NoDataPage render 1`] = ` Object { "link": + } title="Add Elastic Agent" /> `; @@ -13,7 +27,21 @@ exports[`ElasticAgentCardComponent props href 1`] = ` + } title="Add Elastic Agent" /> `; @@ -21,7 +49,21 @@ exports[`ElasticAgentCardComponent props href 1`] = ` exports[`ElasticAgentCardComponent renders 1`] = ` + } title="Add Elastic Agent" /> `; @@ -35,7 +77,21 @@ exports[`ElasticAgentCardComponent renders with canAccessFleet false 1`] = ` This integration is not yet enabled. Your administrator has the required permissions to turn it on. } - image="test-file-stub" + image={ + + } isDisabled={true} title={ + } title="Add Elastic Agent" > } href="/app/integrations/browse" - image="test-file-stub" + image={ + + } paddingSize="l" title="Add Elastic Agent" > @@ -287,10 +315,37 @@ exports[`ElasticAgentCard renders 1`] = `
- + size="fullWidth" + style={ + Object { + "background": "aliceblue", + "height": 240, + "objectFit": "cover", + "width": "max(100%, 360px)", + } + } + url="test-file-stub" + > +
+ +
+
; + const image = ( + + ); + + return ; }; diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_page.tsx b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_page.tsx index 837eb5282507fa..e240e5c2798cf4 100644 --- a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_page.tsx +++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_page.tsx @@ -58,7 +58,7 @@ export const NoDataPage: FunctionComponent = ({ values={{ solution, link: ( - + ) => Promise; stop: () => Promise; kill: () => Promise; + _process?: ChildProcess; } export interface ICluster { @@ -284,6 +291,51 @@ export function createTestEsCluster< ); log.info('[es] stopped'); + + await this.captureDebugFiles(); + } + + async captureDebugFiles() { + const debugFiles = await globby([`**/hs_err_pid*.log`, `**/replay_pid*.log`, `**/*.hprof`], { + cwd: config.installPath, + absolute: true, + }); + + if (!debugFiles.length) { + log.info('[es] no debug files found, assuming es did not write any'); + return; + } + + const uuid = Uuid.v4(); + const debugPath = Path.resolve(KIBANA_ROOT, `data/es_debug_${uuid}.tar.gz`); + log.error(`[es] debug files found, archiving install to ${debugPath}`); + const archiver = createArchiver('tar', { gzip: true }); + const promise = pipeline(archiver, Fs.createWriteStream(debugPath)); + + const archiveDirname = `es_debug_${uuid}`; + for (const name of Fs.readdirSync(config.installPath)) { + if (name === 'modules' || name === 'jdk') { + // drop these large and unnecessary directories + continue; + } + + const src = Path.resolve(config.installPath, name); + const dest = Path.join(archiveDirname, name); + const stat = Fs.statSync(src); + if (stat.isDirectory()) { + archiver.directory(src, dest); + } else { + archiver.file(src, { name: dest }); + } + } + + archiver.finalize(); + await promise; + + // cleanup the captured debug files + for (const path of debugFiles) { + Fs.rmSync(path, { force: true }); + } } async cleanup() { @@ -296,6 +348,7 @@ export function createTestEsCluster< }) ); + await this.captureDebugFiles(); await del(config.installPath, { force: true }); log.info('[es] cleanup complete'); } diff --git a/packages/kbn-test/src/failed_tests_reporter/report_failures_to_es.ts b/packages/kbn-test/src/failed_tests_reporter/report_failures_to_es.ts index 061a5feafa9948..39874211c49263 100644 --- a/packages/kbn-test/src/failed_tests_reporter/report_failures_to_es.ts +++ b/packages/kbn-test/src/failed_tests_reporter/report_failures_to_es.ts @@ -7,7 +7,7 @@ */ import { Client, HttpConnection } from '@elastic/elasticsearch'; -import { createFailError } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; import { ToolingLog } from '@kbn/tooling-log'; import { TestFailure } from './get_failures'; diff --git a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts index 4c539b3ab12b17..5702372aab7be1 100644 --- a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts +++ b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts @@ -9,7 +9,8 @@ import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; -import { run, createFailError, createFlagError } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; +import { createFailError, createFlagError } from '@kbn/dev-cli-errors'; import { CiStatsReporter } from '@kbn/ci-stats-reporter'; import globby from 'globby'; import normalize from 'normalize-path'; diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts index f71e4ac7d6ccd1..de20c93c399950 100644 --- a/packages/kbn-test/src/functional_test_runner/cli.ts +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -9,7 +9,8 @@ import Path from 'path'; import { inspect } from 'util'; -import { run, createFlagError, Flags } from '@kbn/dev-utils'; +import { run, Flags } from '@kbn/dev-cli-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; import { ToolingLog } from '@kbn/tooling-log'; import { getTimeReporter } from '@kbn/ci-stats-reporter'; import exitHook from 'exit-hook'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts index 49a6ef16d6685e..142e5c9da9b3b7 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts @@ -9,7 +9,7 @@ import Path from 'path'; import { ToolingLog } from '@kbn/tooling-log'; import { defaultsDeep } from 'lodash'; -import { createFlagError } from '@kbn/dev-utils'; +import { createFlagError } from '@kbn/dev-cli-errors'; import { REPO_ROOT } from '@kbn/utils'; import { Config } from './config'; diff --git a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts index b5026d397139d8..2e1e2889daf45c 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import type { ProcRunner } from '@kbn/dev-utils'; +import type { ProcRunner } from '@kbn/dev-proc-runner'; import { resolve, relative } from 'path'; import { KIBANA_ROOT, KIBANA_EXEC, KIBANA_EXEC_PATH } from './paths'; import type { Config } from '../../functional_test_runner'; diff --git a/packages/kbn-test/src/functional_tests/tasks.ts b/packages/kbn-test/src/functional_tests/tasks.ts index 33a49ae2c80d1a..76cc4dde1f36b2 100644 --- a/packages/kbn-test/src/functional_tests/tasks.ts +++ b/packages/kbn-test/src/functional_tests/tasks.ts @@ -10,7 +10,7 @@ import { relative } from 'path'; import * as Rx from 'rxjs'; import { setTimeout } from 'timers/promises'; import { startWith, switchMap, take } from 'rxjs/operators'; -import { withProcRunner } from '@kbn/dev-utils'; +import { withProcRunner } from '@kbn/dev-proc-runner'; import { ToolingLog } from '@kbn/tooling-log'; import { getTimeReporter } from '@kbn/ci-stats-reporter'; import { REPO_ROOT } from '@kbn/utils'; diff --git a/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts b/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts index 7a50e88b3396da..e263ca9de08602 100644 --- a/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts +++ b/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts @@ -10,7 +10,8 @@ import { writeFileSync } from 'fs'; import path from 'path'; import Mustache from 'mustache'; -import { run, createFailError } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; +import { createFailError } from '@kbn/dev-cli-errors'; import { REPO_ROOT } from '@kbn/utils'; import { getAllRepoRelativeBazelPackageDirs } from '@kbn/bazel-packages'; diff --git a/packages/kbn-test/src/kbn_archiver_cli.ts b/packages/kbn-test/src/kbn_archiver_cli.ts index f7f17900efcfff..6cf1a0496146f6 100644 --- a/packages/kbn-test/src/kbn_archiver_cli.ts +++ b/packages/kbn-test/src/kbn_archiver_cli.ts @@ -9,7 +9,8 @@ import Path from 'path'; import Url from 'url'; -import { RunWithCommands, createFlagError, Flags } from '@kbn/dev-utils'; +import { RunWithCommands, Flags } from '@kbn/dev-cli-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; import { KbnClient } from './kbn_client'; import { readConfigFile, EsVersion } from './functional_test_runner'; diff --git a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts index c63864857c253e..4b2b4da3f75c28 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts @@ -12,7 +12,8 @@ import { existsSync } from 'fs'; import Path from 'path'; import FormData from 'form-data'; -import { isAxiosResponseError, createFailError } from '@kbn/dev-utils'; +import { isAxiosResponseError } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; import { ToolingLog } from '@kbn/tooling-log'; import { REPO_ROOT } from '@kbn/utils'; diff --git a/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts index 02640cd2b8268d..3e8782b2a5f782 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts @@ -10,7 +10,8 @@ import { inspect } from 'util'; import * as Rx from 'rxjs'; import { mergeMap } from 'rxjs/operators'; -import { isAxiosResponseError, createFailError } from '@kbn/dev-utils'; +import { isAxiosResponseError } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; import { ToolingLog } from '@kbn/tooling-log'; import { KbnClientRequester, uriencode } from './kbn_client_requester'; diff --git a/packages/kbn-tooling-log/README.md b/packages/kbn-tooling-log/README.md deleted file mode 100644 index d0cef9945d3a7f..00000000000000 --- a/packages/kbn-tooling-log/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @kbn/tooling-log - -Empty package generated by @kbn/generate diff --git a/packages/kbn-tooling-log/README.mdx b/packages/kbn-tooling-log/README.mdx new file mode 100644 index 00000000000000..6a0bb2b8129549 --- /dev/null +++ b/packages/kbn-tooling-log/README.mdx @@ -0,0 +1,24 @@ +--- +id: kibDevDocsToolingLog +slug: /kibana-dev-docs/tooling-log +title: "@kbn/tooling-log" +description: Standard logger used by lots of tooling in the Kibana repository. +tags: ['kibana', 'dev', 'contributor', 'operations', 'logging'] +--- + +The Kibana ToolingLog is a basic logger used by just about all tooling in the Kibana respository that gives us a single indented logging interface to manage. + +In the Functional Test Runner, or in the vast majority of CLIs/scripts, a log instance is created for you and should be reused so that the level/output/indent level are synced across uses. + +To get the `ToolingLog` instance in the Functional Test Runner use `getService('log')`. + +To get the `ToolingLog` instance in CLIs, it's passed to the function at the root of the CLI: +```ts +import { run } from '@kbn/dev-utils' +run(async ({ flags, log }) => { + log.info('hello') + await ... +}); +``` + +API Reference: \ No newline at end of file diff --git a/packages/kbn-type-summarizer/BUILD.bazel b/packages/kbn-type-summarizer/BUILD.bazel index 13a28657f2f0e1..8068c30a05d98c 100644 --- a/packages/kbn-type-summarizer/BUILD.bazel +++ b/packages/kbn-type-summarizer/BUILD.bazel @@ -20,7 +20,6 @@ filegroup( NPM_MODULE_EXTRA_FILES = [ "package.json", - "README.md", ] RUNTIME_DEPS = [ diff --git a/packages/kbn-type-summarizer/README.md b/packages/kbn-type-summarizer/README.mdx similarity index 53% rename from packages/kbn-type-summarizer/README.md rename to packages/kbn-type-summarizer/README.mdx index fdd58886a0a691..bb92e110a48972 100644 --- a/packages/kbn-type-summarizer/README.md +++ b/packages/kbn-type-summarizer/README.mdx @@ -1,6 +1,13 @@ -# @kbn/type-summarizer +--- +id: kibDevDocsOpsTypeSummarizer +slug: /kibana-dev-docs/ops/type-summarizer +title: "@kbn/type-summarizer (BETA)" +description: A tool to summarize and produce a single .d.ts file from tsc output supporting sourcemaps +date: 2022-05-17 +tags: ['kibana', 'dev', 'contributor', 'operations', 'type', 'summarizer', 'typescript', 'bundler', 'sourcemap'] +--- -Consume the .d.ts files for a package, produced by `tsc`, and generate a single `.d.ts` file of the public types along with a source map that points back to the original source. +Consumes the .d.ts files for a package, produced by `tsc`, and generates a single `.d.ts` file of the public types along with a source map that points back to the original source. ## You mean like API Extractor? @@ -14,4 +21,4 @@ The plan is to expand to other packages in the Kibana repo, adding support for l ## Something isn't working and I'm blocked! -If there's a problem with the implmentation blocking another team at any point we can move the package back to using api-extractor by removing the package from the `TYPE_SUMMARIZER_PACKAGES` list at the top of [packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts](./src/lib/bazel_cli_config.ts). \ No newline at end of file +If there's a problem with the implementation blocking another team at any point we can move the package back to using api-extractor by removing the package from the `TYPE_SUMMARIZER_PACKAGES` list at the top of [packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts](https://github.com/elastic/kibana/blob/main/packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts). \ No newline at end of file diff --git a/packages/shared-ux/page/analytics_no_data/src/services.tsx b/packages/shared-ux/page/analytics_no_data/src/services.tsx index 7868014749997a..fdb7059900f0c6 100644 --- a/packages/shared-ux/page/analytics_no_data/src/services.tsx +++ b/packages/shared-ux/page/analytics_no_data/src/services.tsx @@ -77,9 +77,7 @@ export interface AnalyticsNoDataPageKibanaDependencies { coreStart: { application: { capabilities: { - navLinks: { - integrations: boolean; - }; + navLinks: Record; }; currentAppId$: Observable; navigateToUrl: (url: string) => Promise; diff --git a/src/core/server/saved_objects/service/lib/repository.test.ts b/src/core/server/saved_objects/service/lib/repository.test.ts index 746ed0033a1d20..313ca2bd07e735 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.ts +++ b/src/core/server/saved_objects/service/lib/repository.test.ts @@ -1688,20 +1688,6 @@ describe('SavedObjectsRepository', () => { ); }); - it(`defaults to the version of the existing document for multi-namespace types`, async () => { - // only multi-namespace documents are obtained using a pre-flight mget request - const objects = [ - { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE }, - { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, - ]; - await bulkUpdateSuccess(objects); - const overrides = { - if_seq_no: mockVersionProps._seq_no, - if_primary_term: mockVersionProps._primary_term, - }; - expectClientCallArgsAction(objects, { method: 'update', overrides }); - }); - it(`defaults to no version for types that are not multi-namespace`, async () => { const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; await bulkUpdateSuccess(objects); @@ -1759,12 +1745,6 @@ describe('SavedObjectsRepository', () => { it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { const getId = (type: string, id: string) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) - const overrides = { - // bulkUpdate uses a preflight `get` request for multi-namespace saved objects, and specifies that version on `update` - // we aren't testing for this here, but we need to include Jest assertions so this test doesn't fail - if_primary_term: expect.any(Number), - if_seq_no: expect.any(Number), - }; const _obj1 = { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }; const _obj2 = { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }; @@ -1772,7 +1752,7 @@ describe('SavedObjectsRepository', () => { expectClientCallArgsAction([_obj1], { method: 'update', getId }); client.bulk.mockClear(); await bulkUpdateSuccess([_obj2], { namespace }); - expectClientCallArgsAction([_obj2], { method: 'update', getId, overrides }); + expectClientCallArgsAction([_obj2], { method: 'update', getId }); jest.clearAllMocks(); // test again with object namespace string that supersedes the operation's namespace ID @@ -1780,7 +1760,7 @@ describe('SavedObjectsRepository', () => { expectClientCallArgsAction([_obj1], { method: 'update', getId }); client.bulk.mockClear(); await bulkUpdateSuccess([{ ..._obj2, namespace }]); - expectClientCallArgsAction([_obj2], { method: 'update', getId, overrides }); + expectClientCallArgsAction([_obj2], { method: 'update', getId }); }); }); @@ -2723,14 +2703,14 @@ describe('SavedObjectsRepository', () => { expect(client.delete).toHaveBeenCalledTimes(1); }); - it(`includes the version of the existing document when using a multi-namespace type`, async () => { + it(`does not includes the version of the existing document when using a multi-namespace type`, async () => { await deleteSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id); const versionProperties = { if_seq_no: mockVersionProps._seq_no, if_primary_term: mockVersionProps._primary_term, }; expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining(versionProperties), + expect.not.objectContaining(versionProperties), expect.anything() ); }); @@ -4605,14 +4585,14 @@ describe('SavedObjectsRepository', () => { ); }); - it(`defaults to the version of the existing document when type is multi-namespace`, async () => { + it(`does not default to the version of the existing document when type is multi-namespace`, async () => { await updateSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes, { references }); const versionProperties = { if_seq_no: mockVersionProps._seq_no, if_primary_term: mockVersionProps._primary_term, }; expect(client.update).toHaveBeenCalledWith( - expect.objectContaining(versionProperties), + expect.not.objectContaining(versionProperties), expect.anything() ); }); @@ -4627,6 +4607,35 @@ describe('SavedObjectsRepository', () => { ); }); + it('default to a `retry_on_conflict` setting of `3` when `version` is not provided', async () => { + await updateSuccess(type, id, attributes, {}); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ retry_on_conflict: 3 }), + expect.anything() + ); + }); + + it('default to a `retry_on_conflict` setting of `0` when `version` is provided', async () => { + await updateSuccess(type, id, attributes, { + version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), + }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ retry_on_conflict: 0, if_seq_no: 100, if_primary_term: 200 }), + expect.anything() + ); + }); + + it('accepts a `retryOnConflict` option', async () => { + await updateSuccess(type, id, attributes, { + version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), + retryOnConflict: 42, + }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ retry_on_conflict: 42, if_seq_no: 100, if_primary_term: 200 }), + expect.anything() + ); + }); + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { await updateSuccess(type, id, attributes, { namespace }); expect(client.update).toHaveBeenCalledWith( diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index db57e74bae1386..287c78d3b26189 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -151,6 +151,7 @@ export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOp } export const DEFAULT_REFRESH_SETTING = 'wait_for'; +export const DEFAULT_RETRY_COUNT = 3; /** * See {@link SavedObjectsRepository} @@ -523,7 +524,7 @@ export class SavedObjectsRepository { } savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace, existingDocument); - versionProperties = getExpectedVersionProperties(version, existingDocument); + versionProperties = getExpectedVersionProperties(version); } else { if (this._registry.isSingleNamespace(object.type)) { savedObjectNamespace = initialNamespaces @@ -761,7 +762,7 @@ export class SavedObjectsRepository { { id: rawId, index: this.getIndexForType(type), - ...getExpectedVersionProperties(undefined, preflightResult?.rawDocSource), + ...getExpectedVersionProperties(undefined), refresh, }, { ignore: [404], meta: true } @@ -1312,7 +1313,13 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createBadRequestError('id cannot be empty'); // prevent potentially upserting a saved object with an empty ID } - const { version, references, upsert, refresh = DEFAULT_REFRESH_SETTING } = options; + const { + version, + references, + upsert, + refresh = DEFAULT_REFRESH_SETTING, + retryOnConflict = version ? 0 : DEFAULT_RETRY_COUNT, + } = options; const namespace = normalizeNamespace(options.namespace); let preflightResult: PreflightCheckNamespacesResult | undefined; @@ -1373,8 +1380,9 @@ export class SavedObjectsRepository { .update({ id: this._serializer.generateRawId(namespace, type, id), index: this.getIndexForType(type), - ...getExpectedVersionProperties(version, preflightResult?.rawDocSource), + ...getExpectedVersionProperties(version), refresh, + retry_on_conflict: retryOnConflict, body: { doc, ...(rawUpsert && { upsert: rawUpsert._source }), @@ -1608,8 +1616,7 @@ export class SavedObjectsRepository { // @ts-expect-error MultiGetHit is incorrectly missing _id, _source SavedObjectsUtils.namespaceIdToString(actualResult!._source.namespace), ]; - // @ts-expect-error MultiGetHit is incorrectly missing _id, _source - versionProperties = getExpectedVersionProperties(version, actualResult!); + versionProperties = getExpectedVersionProperties(version); } else { if (this._registry.isSingleNamespace(type)) { // if `objectNamespace` is undefined, fall back to `options.namespace` diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index 7ebea2d8ff26e8..ba40127958aabe 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -221,7 +221,10 @@ export interface SavedObjectsCheckConflictsResponse { * @public */ export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { - /** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */ + /** + * An opaque version number which changes on each successful write operation. + * Can be used for implementing optimistic concurrency control. + */ version?: string; /** {@inheritdoc SavedObjectReference} */ references?: SavedObjectReference[]; @@ -229,6 +232,11 @@ export interface SavedObjectsUpdateOptions extends SavedOb refresh?: MutatingOperationRefreshSetting; /** If specified, will be used to perform an upsert if the document doesn't exist */ upsert?: Attributes; + /** + * The Elasticsearch `retry_on_conflict` setting for this operation. + * Defaults to `0` when `version` is provided, `3` otherwise. + */ + retryOnConflict?: number; } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 4ff42f95b571ae..3ff44c5b10fb13 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2939,6 +2939,7 @@ export interface SavedObjectsUpdateObjectsSpacesResponseObject { export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { references?: SavedObjectReference[]; refresh?: MutatingOperationRefreshSetting; + retryOnConflict?: number; upsert?: Attributes; version?: string; } diff --git a/src/dev/build/README.md b/src/dev/build/README.mdx similarity index 53% rename from src/dev/build/README.md rename to src/dev/build/README.mdx index 776cdf4e4d8d03..411b1584951589 100644 --- a/src/dev/build/README.md +++ b/src/dev/build/README.mdx @@ -1,11 +1,14 @@ -# dev/build +--- +id: kibDevDocsOpsBuildSystem +slug: /kibana-dev-docs/ops/build-system +title: "Build System" +description: Build the Kibana distributable +tags: ['kibana', 'dev', 'contributor', 'operations', 'build'] +--- -Build the default and OSS distributables of Kibana. - -# Quick Start - -```sh +## Quick Start +```shell # checkout the help for this script node scripts/build --help @@ -16,36 +19,34 @@ node scripts/build --release node scripts/build --skip-node-download --debug ``` -# Fixing out of memory issues +## Fixing out of memory issues Building Kibana and its distributables can take a lot of memory to finish successfully. Builds do make use of child processes, which means you can increase the amount of memory available by specifying `NODE_OPTIONS="--max-old-space-size=VALUE-IN-MEGABYTES"`. -```sh - +```shell # Use 4GB instead of the standard 1GB for building NODE_OPTIONS="--max-old-space-size=4096" node scripts/build --release ``` -# Structure +## Structure -The majority of this logic is extracted from the legacy grunt build, and is designed to maintain the general structure grunt provides including tasks and config. The [build_distributables.js] file defines which tasks are run. +The majority of this logic is extracted from the legacy grunt build, and is designed to maintain the general structure grunt provides including tasks and config. The [build_distributables.ts] file defines which tasks are run. -**Task**: [tasks/\*] define individual parts of the build. Each task is an object with a `run()` method, a `description` property, and optionally a `global` property. They are executed with the runner either once (if they are global) or once for each build. Non-global/local tasks are called once for each build, meaning they will be called twice be default, once for the OSS build and once for the default build and receive a build object as the third argument to `run()` which can be used to determine paths and properties for that build. +**Task**: [tasks/\*] define individual parts of the build. Each task is an object with a `run()` method, a `description` property, and optionally a `global` property. They are executed with the runner either once (if they are global) or once for each build. Non-global/local tasks are called once for each build and receive a build object as the third argument to `run()` which can be used to determine paths and properties for that build. -**Config**: [lib/config.js] defines the config used to execute tasks. It is mostly used to determine absolute paths to specific locations, and to get access to the Platforms. +**Config**: [lib/config.ts] defines the config used to execute tasks. It is mostly used to determine absolute paths to specific locations, and to get access to the Platforms. -**Platform**: [lib/platform.js] defines the Platform objects, which define the different platforms we build for. Use `config.getTargetPlatforms()` to get the list of platforms we are targeting in this build, `config.getNodePlatforms()` to get the list of platform we will download node for, or `config.getPlatform` to get a specific platform and architecture. +**Platform**: [lib/platform.ts] defines the Platform objects, which define the different platforms we build for. Use `config.getTargetPlatforms()` to get the list of platforms we are targeting in this build, `config.getNodePlatforms()` to get the list of platform we will download node for, or `config.getPlatform` to get a specific platform and architecture. -**Log**: We use the `ToolingLog` defined in [@kbn/tooling-log] +**Log**: We use the `ToolingLog` defined in -**Runner**: [lib/runner.js] defines the runner used to execute tasks. It calls tasks with specific arguments based on whether they are global or not. +**Runner**: [lib/runner.ts] defines the runner used to execute tasks. It calls tasks with specific arguments based on whether they are global or not. -**Build**: [lib/build.js], created by the runner and passed to tasks so they can resolve paths and get information about the build they are operating on. +**Build**: [lib/build.ts], created by the runner and passed to tasks so they can resolve paths and get information about the build they are operating on. -[tasks/\*]: ./tasks -[lib/config.js]: ./lib/config.js -[lib/platform.js]: ./lib/platform.js -[lib/runner.js]: ./lib/runner.js -[lib/build.js]: ./lib/build.js -[build_distributables.js]: ./build_distributables.js -[@kbn/tooling-log]: ../../../packages/kbn-tooling-log +[tasks/\*]: https://github.com/elastic/kibana/tree/main/src/dev/build/tasks +[lib/config.ts]: https://github.com/elastic/kibana/blob/main/src/dev/build/lib/config.ts +[lib/platform.ts]: https://github.com/elastic/kibana/blob/main/src/dev/build/lib/platform.ts +[lib/runner.ts]: https://github.com/elastic/kibana/blob/main/src/dev/build/lib/runner.ts +[lib/build.ts]: https://github.com/elastic/kibana/blob/main/src/dev/build/lib/build.ts +[build_distributables.ts]: https://github.com/elastic/kibana/blob/main/src/dev/build/build_distributables.ts diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile index a7bfc6b22a531a..4dc44d861f1048 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile @@ -4,7 +4,7 @@ ################################################################################ ARG BASE_REGISTRY=registry1.dso.mil ARG BASE_IMAGE=redhat/ubi/ubi8 -ARG BASE_TAG=8.5 +ARG BASE_TAG=8.6 FROM ${BASE_REGISTRY}/${BASE_IMAGE}:${BASE_TAG} as prep_files diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/hardening_manifest.yaml b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/hardening_manifest.yaml index 1c7926c2fcbc28..9ec34316262c57 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/hardening_manifest.yaml +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/hardening_manifest.yaml @@ -14,7 +14,7 @@ tags: # Build args passed to Dockerfile ARGs args: BASE_IMAGE: 'redhat/ubi/ubi8' - BASE_TAG: '8.5' + BASE_TAG: '8.6' # Docker image labels labels: diff --git a/src/dev/chromium_version.ts b/src/dev/chromium_version.ts index 0af4c9c2441f14..7bbdfcd59d7688 100644 --- a/src/dev/chromium_version.ts +++ b/src/dev/chromium_version.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { run } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; import { ToolingLog } from '@kbn/tooling-log'; import { REPO_ROOT } from '@kbn/utils'; import chalk from 'chalk'; diff --git a/src/dev/code_coverage/ingest_coverage/index.js b/src/dev/code_coverage/ingest_coverage/index.js index 9c8afecc2ac8c9..dbe9ab7ca60b3e 100644 --- a/src/dev/code_coverage/ingest_coverage/index.js +++ b/src/dev/code_coverage/ingest_coverage/index.js @@ -8,7 +8,8 @@ import { resolve } from 'path'; import { prok } from './process'; -import { run, createFlagError, createFailError } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; +import { createFlagError, createFailError } from '@kbn/dev-cli-errors'; import { pathExists } from './team_assignment/enumeration_helpers'; import { always, ccMark } from './utils'; diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/index.js b/src/dev/code_coverage/ingest_coverage/team_assignment/index.js index a38c4ee50b40a0..f47cf6e9d535c1 100644 --- a/src/dev/code_coverage/ingest_coverage/team_assignment/index.js +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/index.js @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { run, createFlagError } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; import { REPO_ROOT } from '@kbn/utils'; import { parse } from './parse_owners'; import { flush } from './flush'; diff --git a/src/dev/ensure_all_tests_in_ci_group.ts b/src/dev/ensure_all_tests_in_ci_group.ts index a2d9729d3352bd..3f4824a4290487 100644 --- a/src/dev/ensure_all_tests_in_ci_group.ts +++ b/src/dev/ensure_all_tests_in_ci_group.ts @@ -12,7 +12,7 @@ import Fs from 'fs/promises'; import execa from 'execa'; import { safeLoad } from 'js-yaml'; -import { run } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; import { REPO_ROOT } from '@kbn/utils'; import { schema } from '@kbn/config-schema'; diff --git a/src/dev/eslint/eslint_with_types.mdx b/src/dev/eslint/eslint_with_types.mdx new file mode 100644 index 00000000000000..e8b4edd3b60e2a --- /dev/null +++ b/src/dev/eslint/eslint_with_types.mdx @@ -0,0 +1,31 @@ +--- +id: kibDevDocsOpsEslintWithTypes +slug: /kibana-dev-docs/ops/eslint-with-types +title: "ESLint (with types)" +description: Running ESLint in a way that enabled advanced rules which consume the TypeScript type checker +tags: ['kibana', 'dev', 'contributor', 'operations', 'eslint', 'ts'] +--- + +By default ESLint works on a single file, parses that files to an abtract-syntax-tree (AST), and then writes validations against the syntax in that one file. This prevents us from using fancy rules which validate code based on the types of a specific variable, or anything along those lines. Running ESLint *with types* loads a specific TypeScript project into the ESLint context and uses it to include properties of the types to inform specific validations. + +The only rule we have enabled so far is `@typescript-eslint/consistent-type-exports` [[docs](https://typescript-eslint.io/rules/consistent-type-exports/)]. + +## Usage + +``` +node scripts/eslint_with_types --help +``` + +## How it works + +Because ESLint needs to load up the TypeScript project into memory this script can only lint the code for one TypeScript project at a time. To take advantage of modern CPUs and introduce parallelsim to ESLint we run a number of ESLint processes in parallel, each loading a different TypeScript project into memory and then exitting, allowing a new worker to be spawned. + +To tell ESLint which project to load, we actually generate an ESLint config file which points to the right tsconfig.json file and includes the rules we are going to validate for that project. + +## Parallel ESLint?! + +Yep! If you've ever tried to run ESLint on the whole project you know that it takes approximately 40 years. It's actually one of the slowest parts single parts of CI. We are aiming to make ESLint (with types) the default linter soon, deprecating or removing the global ESLint config. + +## How do I customize the rules for my plugin? + +We don't support that yet and are trying to come up with a way that supports some amount of customizability without support for 200+ unique configurations for ESLint based on which directory you're in an thousands of `// eslint-disable-next-line`s all over the repo because rules are incorrectly managed. diff --git a/src/dev/eslint/lint_files.ts b/src/dev/eslint/lint_files.ts index d74e3cafc8de61..d85bed0fe3d4cc 100644 --- a/src/dev/eslint/lint_files.ts +++ b/src/dev/eslint/lint_files.ts @@ -9,7 +9,7 @@ import { CLIEngine } from 'eslint'; import { REPO_ROOT } from '@kbn/utils'; -import { createFailError } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; import { ToolingLog } from '@kbn/tooling-log'; import { File } from '../file'; diff --git a/src/dev/eslint/run_eslint_with_types.ts b/src/dev/eslint/run_eslint_with_types.ts index 1ac7e1c6a91c72..e6eac71b15d0b7 100644 --- a/src/dev/eslint/run_eslint_with_types.ts +++ b/src/dev/eslint/run_eslint_with_types.ts @@ -14,7 +14,8 @@ import execa from 'execa'; import * as Rx from 'rxjs'; import { mergeMap, reduce } from 'rxjs/operators'; import { supportsColor } from 'chalk'; -import { run, createFailError } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; +import { createFailError } from '@kbn/dev-cli-errors'; import { REPO_ROOT } from '@kbn/utils'; import { PROJECTS } from '../typescript/projects'; diff --git a/src/dev/github/download_pr_list_cli.ts b/src/dev/github/download_pr_list_cli.ts index fed7bc8b4f086b..717d4ac8a08cc8 100644 --- a/src/dev/github/download_pr_list_cli.ts +++ b/src/dev/github/download_pr_list_cli.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { run, createFlagError, Flags } from '@kbn/dev-utils'; +import { run, Flags } from '@kbn/dev-cli-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; import fs from 'fs'; import Path from 'path'; import { savePrsToCsv } from './search_and_save_pr_list'; diff --git a/src/dev/i18n/extract_default_translations.js b/src/dev/i18n/extract_default_translations.js index 7d86105fed7fd7..59220a285d6f59 100644 --- a/src/dev/i18n/extract_default_translations.js +++ b/src/dev/i18n/extract_default_translations.js @@ -11,7 +11,7 @@ import path from 'path'; import { extractCodeMessages } from './extractors'; import { globAsync, readFileAsync, normalizePath } from './utils'; -import { createFailError, isFailError } from '@kbn/dev-utils'; +import { createFailError, isFailError } from '@kbn/dev-cli-errors'; function addMessageToMap(targetMap, key, value, reporter) { const existingValue = targetMap.get(key); diff --git a/src/dev/i18n/extractors/code.js b/src/dev/i18n/extractors/code.js index e11e973b689bca..0ac42e69488707 100644 --- a/src/dev/i18n/extractors/code.js +++ b/src/dev/i18n/extractors/code.js @@ -18,7 +18,7 @@ import { import { extractI18nCallMessages } from './i18n_call'; import { createParserErrorMessage, isI18nTranslateFunction, traverseNodes } from '../utils'; import { extractIntlMessages, extractFormattedMessages } from './react'; -import { createFailError, isFailError } from '@kbn/dev-utils'; +import { createFailError, isFailError } from '@kbn/dev-cli-errors'; /** * Detect Intl.formatMessage() function call (React). diff --git a/src/dev/i18n/extractors/i18n_call.js b/src/dev/i18n/extractors/i18n_call.js index 8fcd4f2f79a603..cc2f57b0dd62f4 100644 --- a/src/dev/i18n/extractors/i18n_call.js +++ b/src/dev/i18n/extractors/i18n_call.js @@ -18,7 +18,7 @@ import { extractValuesKeysFromNode, } from '../utils'; import { DEFAULT_MESSAGE_KEY, DESCRIPTION_KEY, VALUES_KEY } from '../constants'; -import { createFailError } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; /** * Extract messages from `funcName('id', { defaultMessage: 'Message text' })` call expression AST diff --git a/src/dev/i18n/extractors/react.js b/src/dev/i18n/extractors/react.js index 65ab64f75c4a73..ab8bd65329b5fc 100644 --- a/src/dev/i18n/extractors/react.js +++ b/src/dev/i18n/extractors/react.js @@ -19,7 +19,7 @@ import { checkValuesProperty, } from '../utils'; import { DEFAULT_MESSAGE_KEY, VALUES_KEY, DESCRIPTION_KEY } from '../constants'; -import { createFailError } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; /** * Extract default messages from ReactJS intl.formatMessage(...) AST diff --git a/src/dev/i18n/integrate_locale_files.ts b/src/dev/i18n/integrate_locale_files.ts index 65558c4624064c..0cdfd58e323772 100644 --- a/src/dev/i18n/integrate_locale_files.ts +++ b/src/dev/i18n/integrate_locale_files.ts @@ -10,7 +10,7 @@ import { ToolingLog } from '@kbn/tooling-log'; import { i18n } from '@kbn/i18n'; import path from 'path'; -import { createFailError } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; import { accessAsync, checkValuesProperty, diff --git a/src/dev/i18n/tasks/extract_default_translations.ts b/src/dev/i18n/tasks/extract_default_translations.ts index db1ee6c8b0c53e..57de2148d5ff44 100644 --- a/src/dev/i18n/tasks/extract_default_translations.ts +++ b/src/dev/i18n/tasks/extract_default_translations.ts @@ -7,7 +7,7 @@ */ import chalk from 'chalk'; -import { createFailError } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; import { ErrorReporter, extractMessagesFromPathToMap, filterConfigPaths, I18nConfig } from '..'; export function extractDefaultMessages(config: I18nConfig, inputPaths: string[]) { diff --git a/src/dev/i18n/tasks/extract_untracked_translations.ts b/src/dev/i18n/tasks/extract_untracked_translations.ts index 1455a9a00f7661..2ef27d581ab709 100644 --- a/src/dev/i18n/tasks/extract_untracked_translations.ts +++ b/src/dev/i18n/tasks/extract_untracked_translations.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createFailError } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; import { I18nConfig, matchEntriesWithExctractors, diff --git a/src/dev/i18n/utils/utils.js b/src/dev/i18n/utils/utils.js index fb1739a708ffee..ce688a0a2c07a7 100644 --- a/src/dev/i18n/utils/utils.js +++ b/src/dev/i18n/utils/utils.js @@ -25,7 +25,7 @@ import path from 'path'; import chalk from 'chalk'; import parser from 'intl-messageformat-parser'; -import { createFailError } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; const ESCAPE_LINE_BREAK_REGEX = /(? { return /^\d+$/.test(input); diff --git a/src/dev/prs/run_update_prs_cli.ts b/src/dev/prs/run_update_prs_cli.ts index cde7f495b1eb60..74a879bb95dc46 100644 --- a/src/dev/prs/run_update_prs_cli.ts +++ b/src/dev/prs/run_update_prs_cli.ts @@ -14,7 +14,8 @@ import chalk from 'chalk'; import { first, tap } from 'rxjs/operators'; import dedent from 'dedent'; -import { run, createFlagError } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; import { getLine$ } from './helpers'; import { Pr } from './pr'; import { GithubApi } from './github_api'; diff --git a/src/dev/run_build_docs_cli.ts b/src/dev/run_build_docs_cli.ts index 8ee75912c1a7e8..c121dc69de3dd8 100644 --- a/src/dev/run_build_docs_cli.ts +++ b/src/dev/run_build_docs_cli.ts @@ -9,7 +9,8 @@ import Path from 'path'; import dedent from 'dedent'; -import { run, createFailError } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; +import { createFailError } from '@kbn/dev-cli-errors'; import { REPO_ROOT } from '@kbn/utils'; const DEFAULT_DOC_REPO_PATH = Path.resolve(REPO_ROOT, '..', 'docs'); diff --git a/src/dev/run_check_file_casing.ts b/src/dev/run_check_file_casing.ts index 554aa2418f579b..9cc28ec8de91ce 100644 --- a/src/dev/run_check_file_casing.ts +++ b/src/dev/run_check_file_casing.ts @@ -9,7 +9,7 @@ import globby from 'globby'; import { REPO_ROOT } from '@kbn/utils'; -import { run } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; import { File } from './file'; // @ts-expect-error precommit hooks aren't migrated to TypeScript yet. import { checkFileCasing } from './precommit_hook/check_file_casing'; diff --git a/src/dev/run_find_plugins_ready_migrate_to_ts_refs.ts b/src/dev/run_find_plugins_ready_migrate_to_ts_refs.ts index 39fc75fc2e3795..bb94c7d375ba99 100644 --- a/src/dev/run_find_plugins_ready_migrate_to_ts_refs.ts +++ b/src/dev/run_find_plugins_ready_migrate_to_ts_refs.ts @@ -10,7 +10,7 @@ import Path from 'path'; import Fs from 'fs'; import JSON5 from 'json5'; import { get } from 'lodash'; -import { run } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; import { KibanaPlatformPlugin } from '@kbn/plugin-discovery'; import { getPluginDeps, findPlugins } from './plugin_discovery'; diff --git a/src/dev/run_find_plugins_with_circular_deps.ts b/src/dev/run_find_plugins_with_circular_deps.ts index f174561b49f9bd..67a67a2f1729ba 100644 --- a/src/dev/run_find_plugins_with_circular_deps.ts +++ b/src/dev/run_find_plugins_with_circular_deps.ts @@ -10,7 +10,7 @@ import dedent from 'dedent'; import { parseDependencyTree, parseCircular, prettyCircular } from 'dpdm'; import { relative } from 'path'; import { getPluginSearchPaths } from '@kbn/plugin-discovery'; -import { run } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; import { REPO_ROOT } from '@kbn/utils'; interface Options { diff --git a/src/dev/run_find_plugins_without_ts_refs.ts b/src/dev/run_find_plugins_without_ts_refs.ts index f00c01e83c1ca7..6f444aa51b2120 100644 --- a/src/dev/run_find_plugins_without_ts_refs.ts +++ b/src/dev/run_find_plugins_without_ts_refs.ts @@ -10,7 +10,7 @@ import Path from 'path'; import Fs from 'fs'; import JSON5 from 'json5'; import { get } from 'lodash'; -import { run } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; import { getPluginDeps, findPlugins } from './plugin_discovery'; interface AllOptions { diff --git a/src/dev/run_i18n_check.ts b/src/dev/run_i18n_check.ts index 074fd47d956835..2aa9f65f4d3db7 100644 --- a/src/dev/run_i18n_check.ts +++ b/src/dev/run_i18n_check.ts @@ -9,7 +9,8 @@ import chalk from 'chalk'; import Listr from 'listr'; -import { createFailError, run } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; +import { run } from '@kbn/dev-cli-runner'; import { ToolingLog } from '@kbn/tooling-log'; import { getTimeReporter } from '@kbn/ci-stats-reporter'; import { ErrorReporter, I18nConfig } from './i18n'; diff --git a/src/dev/run_i18n_extract.ts b/src/dev/run_i18n_extract.ts index e4b5bd2afdb1a4..6ff9eb5164ad2a 100644 --- a/src/dev/run_i18n_extract.ts +++ b/src/dev/run_i18n_extract.ts @@ -10,7 +10,8 @@ import chalk from 'chalk'; import Listr from 'listr'; import { resolve } from 'path'; -import { createFailError, run } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; +import { run } from '@kbn/dev-cli-runner'; import { ErrorReporter, serializeToJson, serializeToJson5, writeFileAsync } from './i18n'; import { extractDefaultMessages, mergeConfigs } from './i18n/tasks'; diff --git a/src/dev/run_i18n_integrate.ts b/src/dev/run_i18n_integrate.ts index 7f78dbc11d464b..29696ca5f9aa50 100644 --- a/src/dev/run_i18n_integrate.ts +++ b/src/dev/run_i18n_integrate.ts @@ -9,7 +9,8 @@ import chalk from 'chalk'; import Listr from 'listr'; -import { createFailError, run } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; +import { run } from '@kbn/dev-cli-runner'; import { ErrorReporter, integrateLocaleFiles } from './i18n'; import { extractDefaultMessages, mergeConfigs } from './i18n/tasks'; diff --git a/src/dev/run_licenses_csv_report.js b/src/dev/run_licenses_csv_report.js index 391361f2ccfec3..f5fc309a5c3d91 100644 --- a/src/dev/run_licenses_csv_report.js +++ b/src/dev/run_licenses_csv_report.js @@ -10,7 +10,7 @@ import { writeFileSync } from 'fs'; import { resolve } from 'path'; import { isNull, isUndefined } from 'lodash'; -import { run } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; import { getInstalledPackages } from './npm'; import { engines } from '../../package.json'; diff --git a/src/dev/run_precommit_hook.js b/src/dev/run_precommit_hook.js index a86bb5c7dabcc1..cc5aca8e0cd6cb 100644 --- a/src/dev/run_precommit_hook.js +++ b/src/dev/run_precommit_hook.js @@ -8,7 +8,9 @@ import SimpleGit from 'simple-git/promise'; -import { run, combineErrors, createFlagError } from '@kbn/dev-utils'; +import { combineErrors } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; import { REPO_ROOT } from '@kbn/utils'; import * as Eslint from './eslint'; import * as Stylelint from './stylelint'; diff --git a/src/dev/storybook/run_storybook_cli.ts b/src/dev/storybook/run_storybook_cli.ts index 77c7e45205d81f..7a6ea7f34d825a 100644 --- a/src/dev/storybook/run_storybook_cli.ts +++ b/src/dev/storybook/run_storybook_cli.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { run, createFlagError } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; import { runStorybookCli } from '@kbn/storybook'; import { storybookAliases } from './aliases'; import { clean } from './commands/clean'; diff --git a/src/dev/stylelint/lint_files.js b/src/dev/stylelint/lint_files.js index 6e62c85d44ae85..20581639489ab6 100644 --- a/src/dev/stylelint/lint_files.js +++ b/src/dev/stylelint/lint_files.js @@ -10,7 +10,7 @@ import stylelint from 'stylelint'; import path from 'path'; import { safeLoad } from 'js-yaml'; import fs from 'fs'; -import { createFailError } from '@kbn/dev-utils'; +import { createFailError } from '@kbn/dev-cli-errors'; // load the include globs from .stylelintrc and convert them to regular expressions for filtering files const stylelintPath = path.resolve(__dirname, '..', '..', '..', '.stylelintrc'); diff --git a/src/dev/typescript/build_ts_refs.ts b/src/dev/typescript/build_ts_refs.ts index 9640e0951965bc..b01251e99b27b5 100644 --- a/src/dev/typescript/build_ts_refs.ts +++ b/src/dev/typescript/build_ts_refs.ts @@ -8,7 +8,7 @@ import Path from 'path'; -import { ProcRunner } from '@kbn/dev-utils'; +import { ProcRunner } from '@kbn/dev-proc-runner'; import { ToolingLog } from '@kbn/tooling-log'; import { REPO_ROOT } from '@kbn/utils'; diff --git a/src/dev/typescript/build_ts_refs_cli.ts b/src/dev/typescript/build_ts_refs_cli.ts index 09866315fc8dd6..22b616faf6fb40 100644 --- a/src/dev/typescript/build_ts_refs_cli.ts +++ b/src/dev/typescript/build_ts_refs_cli.ts @@ -8,7 +8,8 @@ import Path from 'path'; -import { run, createFlagError } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; import { REPO_ROOT } from '@kbn/utils'; import del from 'del'; diff --git a/src/dev/typescript/convert_all_to_composite.ts b/src/dev/typescript/convert_all_to_composite.ts index 9b9dd3468747b8..f3c2bcdd0b5357 100644 --- a/src/dev/typescript/convert_all_to_composite.ts +++ b/src/dev/typescript/convert_all_to_composite.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { run } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; import { PROJECTS } from './projects'; diff --git a/src/dev/typescript/run_check_ts_projects_cli.ts b/src/dev/typescript/run_check_ts_projects_cli.ts index 5e6386d4f465fb..1f5284f11c8cd7 100644 --- a/src/dev/typescript/run_check_ts_projects_cli.ts +++ b/src/dev/typescript/run_check_ts_projects_cli.ts @@ -10,7 +10,7 @@ import { resolve, relative } from 'path'; import execa from 'execa'; -import { run } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; import { REPO_ROOT } from '@kbn/utils'; import { File } from '../file'; diff --git a/src/dev/typescript/run_type_check_cli.ts b/src/dev/typescript/run_type_check_cli.ts index bafd2406c1095a..f59e6c47242bf8 100644 --- a/src/dev/typescript/run_type_check_cli.ts +++ b/src/dev/typescript/run_type_check_cli.ts @@ -12,7 +12,8 @@ import Os from 'os'; import * as Rx from 'rxjs'; import { mergeMap, reduce } from 'rxjs/operators'; import execa from 'execa'; -import { run, createFailError } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-cli-runner'; +import { createFailError } from '@kbn/dev-cli-errors'; import { PROJECTS } from './projects'; import { buildTsRefs } from './build_ts_refs'; diff --git a/src/plugins/chart_expressions/expression_metric/public/components/__snapshots__/metric_component.test.tsx.snap b/src/plugins/chart_expressions/expression_metric/public/components/__snapshots__/metric_component.test.tsx.snap index b18c521bea653e..04291835a6f323 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/__snapshots__/metric_component.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_metric/public/components/__snapshots__/metric_component.test.tsx.snap @@ -63,7 +63,6 @@ Array [ "value": 445842.4634666484, } } - onFilter={[Function]} style={ Object { "bgColor": false, diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_component.test.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_component.test.tsx index f91d3adcd16a8e..314f8379862583 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric_component.test.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_component.test.tsx @@ -72,6 +72,7 @@ describe('MetricVisComponent', function () { visData, renderComplete: jest.fn(), fireEvent: jest.fn(), + filterable: [true], ...propOverrides, }; @@ -88,6 +89,7 @@ describe('MetricVisComponent', function () { it('should render correct structure for multi-value metrics', function () { const component = getComponent({ + filterable: [true, false], visData: { type: 'datatable', columns: [ diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_component.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_component.tsx index 6fe19c0e725154..a30b24287cc697 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric_component.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_component.tsx @@ -28,6 +28,7 @@ import './metric.scss'; export interface MetricVisComponentProps { visParams: Pick; visData: Datatable; + filterable: boolean[]; fireEvent: (event: any) => void; renderComplete: () => void; } @@ -127,6 +128,7 @@ class MetricVisComponent extends Component { }; private renderMetric = (metric: MetricOptions, index: number) => { + const hasBuckets = this.props.visParams.dimensions.bucket !== undefined; const MetricComponent = this.props.visParams.metric.autoScale ? AutoScaleMetricVisValue : MetricVisValue; @@ -147,7 +149,11 @@ class MetricVisComponent extends Component { key={index} metric={metric} style={this.props.visParams.metric.style} - onFilter={() => this.filterColumn(metric.rowIndex, metric.colIndex)} + onFilter={ + hasBuckets || this.props.filterable[index] + ? () => this.filterColumn(metric.rowIndex, metric.colIndex) + : undefined + } autoScale={this.props.visParams.metric.autoScale} colorFullBackground={this.props.visParams.metric.colorFullBackground} labelConfig={this.props.visParams.metric.labels} diff --git a/src/plugins/chart_expressions/expression_metric/public/expression_renderers/metric_vis_renderer.tsx b/src/plugins/chart_expressions/expression_metric/public/expression_renderers/metric_vis_renderer.tsx index 4026d3c2d1bc6b..0607854206e908 100644 --- a/src/plugins/chart_expressions/expression_metric/public/expression_renderers/metric_vis_renderer.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/expression_renderers/metric_vis_renderer.tsx @@ -11,13 +11,48 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { ThemeServiceStart } from '@kbn/core/public'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; -import { VisualizationContainer } from '@kbn/visualizations-plugin/public'; -import { ExpressionRenderDefinition } from '@kbn/expressions-plugin/common/expression_renderers'; -import { EXPRESSION_METRIC_NAME, MetricVisRenderConfig } from '../../common'; +import { + ExpressionValueVisDimension, + VisualizationContainer, +} from '@kbn/visualizations-plugin/public'; +import { + ExpressionRenderDefinition, + IInterpreterRenderHandlers, +} from '@kbn/expressions-plugin/common/expression_renderers'; +import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils'; +import { Datatable } from '@kbn/expressions-plugin'; +import { EXPRESSION_METRIC_NAME, MetricVisRenderConfig, VisParams } from '../../common'; // @ts-ignore const MetricVisComponent = lazy(() => import('../components/metric_component')); +async function metricFilterable( + dimensions: VisParams['dimensions'], + table: Datatable, + handlers: IInterpreterRenderHandlers +) { + return Promise.all( + dimensions.metrics.map(async (metric: string | ExpressionValueVisDimension) => { + const column = getColumnByAccessor(metric, table.columns); + const colIndex = table.columns.indexOf(column!); + return Boolean( + await handlers.hasCompatibleActions?.({ + name: 'filter', + data: { + data: [ + { + table, + column: colIndex, + row: 0, + }, + ], + }, + }) + ); + }) + ); +} + export const getMetricVisRenderer = ( theme: ThemeServiceStart ): (() => ExpressionRenderDefinition) => { @@ -30,6 +65,8 @@ export const getMetricVisRenderer = ( unmountComponentAtNode(domNode); }); + const filterable = await metricFilterable(visConfig.dimensions, visData, handlers); + render( , diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/__snapshots__/xy_vis.test.ts.snap b/src/plugins/chart_expressions/expression_xy/common/expression_functions/__snapshots__/xy_vis.test.ts.snap index 4f3ad589f1eea5..3a33797bc0cbf0 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/__snapshots__/xy_vis.test.ts.snap +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/__snapshots__/xy_vis.test.ts.snap @@ -1,5 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`xyVis it should throw error if minTimeBarInterval applied for not time bar chart 1`] = `"\`minTimeBarInterval\` argument is applicable only for time bar charts."`; + +exports[`xyVis it should throw error if minTimeBarInterval is invalid 1`] = `"Provided x-axis interval is invalid. The interval should include quantity and unit names. Examples: 1d, 24h, 1w."`; + exports[`xyVis it should throw error if splitColumnAccessor is pointing to the absent column 1`] = `"Provided column name or index is invalid: absent-accessor"`; exports[`xyVis it should throw error if splitRowAccessor is pointing to the absent column 1`] = `"Provided column name or index is invalid: absent-accessor"`; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts index 0dbe71ef554cc2..a09212d59cce39 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts @@ -128,4 +128,8 @@ export const commonXYArgs: CommonXYFn['args'] = { types: ['string'], help: strings.getAriaLabelHelp(), }, + minTimeBarInterval: { + types: ['string'], + help: strings.getMinTimeBarIntervalHelp(), + }, }; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts index 4b7de0eba31669..c4e2decb3279d9 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts @@ -7,15 +7,20 @@ */ import { XY_VIS_RENDERER } from '../constants'; -import { appendLayerIds } from '../helpers'; +import { appendLayerIds, getDataLayers } from '../helpers'; import { LayeredXyVisFn } from '../types'; import { logDatatables } from '../utils'; +import { validateMinTimeBarInterval, hasBarLayer } from './validate'; export const layeredXyVisFn: LayeredXyVisFn['fn'] = async (data, args, handlers) => { const layers = appendLayerIds(args.layers ?? [], 'layers'); logDatatables(layers, handlers); + const dataLayers = getDataLayers(layers); + const hasBar = hasBarLayer(dataLayers); + validateMinTimeBarInterval(dataLayers, hasBar, args.minTimeBarInterval); + return { type: 'render', as: XY_VIS_RENDERER, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts index 55d7cb12382c0f..2d1ecb2840c0ad 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts @@ -7,13 +7,16 @@ */ import { i18n } from '@kbn/i18n'; +import { isValidInterval } from '@kbn/data-plugin/common'; import { AxisExtentModes, ValueLabelModes } from '../constants'; import { AxisExtentConfigResult, DataLayerConfigResult, + CommonXYDataLayerConfigResult, ValueLabelMode, CommonXYDataLayerConfig, } from '../types'; +import { isTimeChart } from '../helpers'; const errors = { extendBoundsAreInvalidError: () => @@ -37,6 +40,18 @@ const errors = { i18n.translate('expressionXY.reusable.function.xyVis.errors.dataBoundsForNotLineChartError', { defaultMessage: 'Only line charts can be fit to the data bounds', }), + isInvalidIntervalError: () => + i18n.translate('expressionXY.reusable.function.xyVis.errors.isInvalidIntervalError', { + defaultMessage: + 'Provided x-axis interval is invalid. The interval should include quantity and unit names. Examples: 1d, 24h, 1w.', + }), + minTimeBarIntervalNotForTimeBarChartError: () => + i18n.translate( + 'expressionXY.reusable.function.xyVis.errors.minTimeBarIntervalNotForTimeBarChartError', + { + defaultMessage: '`minTimeBarInterval` argument is applicable only for time bar charts.', + } + ), }; export const hasBarLayer = (layers: Array) => @@ -101,3 +116,19 @@ export const validateValueLabels = ( throw new Error(errors.valueLabelsForNotBarsOrHistogramBarsChartsError()); } }; + +export const validateMinTimeBarInterval = ( + dataLayers: CommonXYDataLayerConfigResult[], + hasBar: boolean, + minTimeBarInterval?: string +) => { + if (minTimeBarInterval) { + if (!isValidInterval(minTimeBarInterval)) { + throw new Error(errors.isInvalidIntervalError()); + } + + if (!hasBar || !isTimeChart(dataLayers)) { + throw new Error(errors.minTimeBarIntervalNotForTimeBarChartError()); + } + } +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts index 68d2598954db26..6d7f637e3fdf08 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts @@ -51,6 +51,44 @@ describe('xyVis', () => { }); }); + test('it should throw error if minTimeBarInterval is invalid', async () => { + const { data, args } = sampleArgs(); + const { layers, ...rest } = args; + const { layerId, layerType, table, type, ...restLayerArgs } = sampleLayer; + expect( + xyVisFunction.fn( + data, + { + ...rest, + ...restLayerArgs, + minTimeBarInterval: '1q', + referenceLineLayers: [], + annotationLayers: [], + }, + createMockExecutionContext() + ) + ).rejects.toThrowErrorMatchingSnapshot(); + }); + + test('it should throw error if minTimeBarInterval applied for not time bar chart', async () => { + const { data, args } = sampleArgs(); + const { layers, ...rest } = args; + const { layerId, layerType, table, type, ...restLayerArgs } = sampleLayer; + expect( + xyVisFunction.fn( + data, + { + ...rest, + ...restLayerArgs, + minTimeBarInterval: '1h', + referenceLineLayers: [], + annotationLayers: [], + }, + createMockExecutionContext() + ) + ).rejects.toThrowErrorMatchingSnapshot(); + }); + test('it should throw error if splitRowAccessor is pointing to the absent column', async () => { const { data, args } = sampleArgs(); const { layers, ...rest } = args; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts index 16f218251fc02d..5781184b277022 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts @@ -24,6 +24,7 @@ import { validateExtent, validateFillOpacity, validateValueLabels, + validateMinTimeBarInterval, } from './validate'; const createDataLayer = (args: XYArgs, table: Datatable): DataLayerConfigResult => { @@ -99,6 +100,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { validateExtent(args.yLeftExtent, hasBar || hasArea, dataLayers); validateExtent(args.yRightExtent, hasBar || hasArea, dataLayers); validateFillOpacity(args.fillOpacity, hasArea); + validateMinTimeBarInterval(dataLayers, hasBar, args.minTimeBarInterval); const hasNotHistogramBars = !hasHistogramBarLayer(dataLayers); diff --git a/src/plugins/chart_expressions/expression_xy/common/helpers/index.ts b/src/plugins/chart_expressions/expression_xy/common/helpers/index.ts index 9359678fbb1873..584fbd28867265 100644 --- a/src/plugins/chart_expressions/expression_xy/common/helpers/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/helpers/index.ts @@ -6,5 +6,6 @@ * Side Public License, v 1. */ -export { appendLayerIds, getAccessors } from './layers'; +export { appendLayerIds, getDataLayers, getAccessors } from './layers'; +export { isTimeChart } from './visualization'; export { normalizeTable } from './table'; diff --git a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts index ac44ef18fc505e..a3eea973fbf912 100644 --- a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { generateLayerId, appendLayerIds } from './layers'; +import { XYExtendedLayerConfigResult } from '../types'; +import { generateLayerId, appendLayerIds, getDataLayers } from './layers'; describe('#generateLayerId', () => { it('should return the combination of keyword and index', () => { @@ -47,3 +48,28 @@ describe('#appendLayerIds', () => { expect(layersWithIds).toStrictEqual(expectedLayerIds); }); }); + +describe('#getDataLayers', () => { + it('should return only data layers', () => { + const layers: XYExtendedLayerConfigResult[] = [ + { + type: 'extendedDataLayer', + layerType: 'data', + accessors: ['y'], + seriesType: 'bar', + xScaleType: 'time', + isHistogram: false, + table: { rows: [], columns: [], type: 'datatable' }, + palette: { type: 'system_palette', name: 'system' }, + }, + { + type: 'extendedReferenceLineLayer', + layerType: 'referenceLine', + accessors: ['y'], + table: { rows: [], columns: [], type: 'datatable' }, + }, + ]; + + expect(getDataLayers(layers)).toStrictEqual([layers[0]]); + }); +}); diff --git a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts index 667b2697e480f1..23aa8bd3218d28 100644 --- a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts +++ b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts @@ -7,7 +7,8 @@ */ import { Datatable, PointSeriesColumnNames } from '@kbn/expressions-plugin/common'; -import { WithLayerId } from '../types'; +import { WithLayerId, ExtendedDataLayerConfig, XYExtendedLayerConfigResult } from '../types'; +import { LayerTypes } from '../constants'; function isWithLayerId(layer: T): layer is T & WithLayerId { return (layer as T & WithLayerId).layerId ? true : false; @@ -27,6 +28,13 @@ export function appendLayerIds( })); } +export function getDataLayers(layers: XYExtendedLayerConfigResult[]) { + return layers.filter( + (layer): layer is ExtendedDataLayerConfig => + layer.layerType === LayerTypes.DATA || !layer.layerType + ); +} + export function getAccessors( args: U, table: Datatable diff --git a/src/plugins/chart_expressions/expression_xy/common/helpers/visualization.test.ts b/src/plugins/chart_expressions/expression_xy/common/helpers/visualization.test.ts new file mode 100644 index 00000000000000..678c342e38b492 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/helpers/visualization.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { CommonXYDataLayerConfigResult } from '../types'; +import { isTimeChart } from './visualization'; + +describe('#isTimeChart', () => { + it('should return true if all data layers have xAccessor collumn type `date` and scaleType is `time`', () => { + const layers: CommonXYDataLayerConfigResult[] = [ + { + type: 'extendedDataLayer', + layerType: 'data', + xAccessor: 'x', + accessors: ['y'], + seriesType: 'bar', + xScaleType: 'time', + isHistogram: false, + table: { + rows: [], + columns: [ + { + id: 'x', + name: 'x', + meta: { + type: 'date', + }, + }, + ], + type: 'datatable', + }, + palette: { type: 'system_palette', name: 'system' }, + }, + ]; + + expect(isTimeChart(layers)).toBeTruthy(); + + layers[0].xScaleType = 'linear'; + + expect(isTimeChart(layers)).toBeFalsy(); + + layers[0].xScaleType = 'time'; + layers[0].table.columns[0].meta.type = 'number'; + + expect(isTimeChart(layers)).toBeFalsy(); + }); +}); diff --git a/src/plugins/chart_expressions/expression_xy/common/helpers/visualization.ts b/src/plugins/chart_expressions/expression_xy/common/helpers/visualization.ts new file mode 100644 index 00000000000000..8ddbc4bc97f104 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/helpers/visualization.ts @@ -0,0 +1,18 @@ +/* + * Copyright 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 { XScaleTypes } from '../constants'; +import { CommonXYDataLayerConfigResult } from '../types'; + +export function isTimeChart(layers: CommonXYDataLayerConfigResult[]) { + return layers.every( + (l): l is CommonXYDataLayerConfigResult => + l.table.columns.find((col) => col.id === l.xAccessor)?.meta.type === 'date' && + l.xScaleType === XScaleTypes.TIME + ); +} diff --git a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx index 81b417b3cb5951..5833af9085f265 100644 --- a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx +++ b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx @@ -121,6 +121,10 @@ export const strings = { i18n.translate('expressionXY.xyVis.ariaLabel.help', { defaultMessage: 'Specifies the aria label of the xy chart', }), + getMinTimeBarIntervalHelp: () => + i18n.translate('expressionXY.xyVis.xAxisInterval.help', { + defaultMessage: 'Specifies the min interval for time bar chart', + }), getSplitColumnAccessorHelp: () => i18n.translate('expressionXY.xyVis.splitColumnAccessor.help', { defaultMessage: 'Specifies split column of the xy chart', diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts index 606281b181016c..ef348efed74e04 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -204,6 +204,7 @@ export interface XYArgs extends DataLayerArgs { hideEndzones?: boolean; valuesInLegend?: boolean; ariaLabel?: string; + minTimeBarInterval?: string; splitRowAccessor?: ExpressionValueVisDimension | string; splitColumnAccessor?: ExpressionValueVisDimension | string; } @@ -231,6 +232,7 @@ export interface LayeredXYArgs { hideEndzones?: boolean; valuesInLegend?: boolean; ariaLabel?: string; + minTimeBarInterval?: string; } export interface XYProps { @@ -256,6 +258,7 @@ export interface XYProps { hideEndzones?: boolean; valuesInLegend?: boolean; ariaLabel?: string; + minTimeBarInterval?: string; splitRowAccessor?: ExpressionValueVisDimension | string; splitColumnAccessor?: ExpressionValueVisDimension | string; } diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/interval.test.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/interval.test.ts index 6721c293dbe57d..43f9cb750b1510 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/interval.test.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/interval.test.ts @@ -86,4 +86,18 @@ describe('calculateMinInterval', () => { const result = await calculateMinInterval(xyProps); expect(result).toEqual(undefined); }); + + it('should return specified interval if user provided it as `xAxisInterval`', async () => { + layer.table.columns[2].meta.source = 'esaggs'; + layer.table.columns[2].meta.sourceParams = { + type: 'date_histogram', + params: { + used_interval: '5m', + }, + }; + xyProps.args.layers[0] = layer; + xyProps.args.minTimeBarInterval = '1h'; + const result = await calculateMinInterval(xyProps); + expect(result).toEqual(60 * 60 * 1000); + }); }); diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts index 015cab5431e9eb..a9f68ffc0a29bd 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts @@ -12,7 +12,7 @@ import { XYChartProps } from '../../common'; import { getFilteredLayers } from './layers'; import { isDataLayer } from './visualization'; -export function calculateMinInterval({ args: { layers } }: XYChartProps) { +export function calculateMinInterval({ args: { layers, minTimeBarInterval } }: XYChartProps) { const filteredLayers = getFilteredLayers(layers); if (filteredLayers.length === 0) return; const isTimeViz = filteredLayers.every((l) => isDataLayer(l) && l.xScaleType === 'time'); @@ -22,6 +22,9 @@ export function calculateMinInterval({ args: { layers } }: XYChartProps) { getColumnByAccessor(filteredLayers[0].xAccessor, filteredLayers[0].table.columns); if (!xColumn) return; + if (minTimeBarInterval) { + return search.aggs.parseInterval(minTimeBarInterval)?.as('milliseconds'); + } if (!isTimeViz) { const histogramInterval = search.aggs.getNumberHistogramIntervalByDatatableColumn(xColumn); if (typeof histogramInterval === 'number') { diff --git a/src/plugins/controls/common/control_group/control_group_constants.ts b/src/plugins/controls/common/control_group/control_group_constants.ts index 604e411279bad1..88c08556096827 100644 --- a/src/plugins/controls/common/control_group/control_group_constants.ts +++ b/src/plugins/controls/common/control_group/control_group_constants.ts @@ -8,5 +8,6 @@ import { ControlStyle, ControlWidth } from '../types'; -export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'auto'; +export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'medium'; +export const DEFAULT_CONTROL_GROW: boolean = true; export const DEFAULT_CONTROL_STYLE: ControlStyle = 'oneLine'; diff --git a/src/plugins/controls/common/control_group/control_group_persistence.ts b/src/plugins/controls/common/control_group/control_group_persistence.ts index 55a7ad4b5a854b..16c06297b6fde1 100644 --- a/src/plugins/controls/common/control_group/control_group_persistence.ts +++ b/src/plugins/controls/common/control_group/control_group_persistence.ts @@ -11,7 +11,11 @@ import deepEqual from 'fast-deep-equal'; import { pick } from 'lodash'; import { ControlGroupInput } from '..'; -import { DEFAULT_CONTROL_STYLE, DEFAULT_CONTROL_WIDTH } from './control_group_constants'; +import { + DEFAULT_CONTROL_GROW, + DEFAULT_CONTROL_STYLE, + DEFAULT_CONTROL_WIDTH, +} from './control_group_constants'; import { PersistableControlGroupInput, RawControlGroupAttributes } from './types'; const safeJSONParse = (jsonString?: string): OutType | undefined => { @@ -26,6 +30,7 @@ const safeJSONParse = (jsonString?: string): OutType | undefined => { export const getDefaultControlGroupInput = (): Omit => ({ panels: {}, defaultControlWidth: DEFAULT_CONTROL_WIDTH, + defaultControlGrow: DEFAULT_CONTROL_GROW, controlStyle: DEFAULT_CONTROL_STYLE, chainingSystem: 'HIERARCHICAL', ignoreParentSettings: { diff --git a/src/plugins/controls/common/control_group/types.ts b/src/plugins/controls/common/control_group/types.ts index 9fbfc54b09a173..be24d8790dae82 100644 --- a/src/plugins/controls/common/control_group/types.ts +++ b/src/plugins/controls/common/control_group/types.ts @@ -15,6 +15,7 @@ export interface ControlPanelState { order: number; width: ControlWidth; + grow: boolean; } export type ControlGroupChainingSystem = 'HIERARCHICAL' | 'NONE'; @@ -26,6 +27,7 @@ export interface ControlsPanels { export interface ControlGroupInput extends EmbeddableInput, ControlInput { chainingSystem: ControlGroupChainingSystem; defaultControlWidth?: ControlWidth; + defaultControlGrow?: boolean; controlStyle: ControlStyle; panels: ControlsPanels; } diff --git a/src/plugins/controls/common/types.ts b/src/plugins/controls/common/types.ts index abb24299e8180b..4108e886e757dc 100644 --- a/src/plugins/controls/common/types.ts +++ b/src/plugins/controls/common/types.ts @@ -10,7 +10,7 @@ import { Filter, Query } from '@kbn/es-query'; import { TimeRange } from '@kbn/data-plugin/common'; import { EmbeddableInput } from '@kbn/embeddable-plugin/common/types'; -export type ControlWidth = 'auto' | 'small' | 'medium' | 'large'; +export type ControlWidth = 'small' | 'medium' | 'large'; export type ControlStyle = 'twoLine' | 'oneLine'; export interface ParentIgnoreSettings { diff --git a/src/plugins/controls/public/__stories__/controls.stories.tsx b/src/plugins/controls/public/__stories__/controls.stories.tsx index e8133e7dae503e..94c2084096b014 100644 --- a/src/plugins/controls/public/__stories__/controls.stories.tsx +++ b/src/plugins/controls/public/__stories__/controls.stories.tsx @@ -138,7 +138,8 @@ export const ConfiguredControlGroupStory = () => ( optionsList1: { type: OPTIONS_LIST_CONTROL, order: 1, - width: 'auto', + width: 'small', + grow: true, explicitInput: { title: 'Origin City', id: 'optionsList1', @@ -150,7 +151,8 @@ export const ConfiguredControlGroupStory = () => ( optionsList2: { type: OPTIONS_LIST_CONTROL, order: 2, - width: 'auto', + width: 'medium', + grow: true, explicitInput: { title: 'Destination City', id: 'optionsList2', @@ -162,7 +164,8 @@ export const ConfiguredControlGroupStory = () => ( optionsList3: { type: 'TIME_SLIDER', order: 3, - width: 'auto', + width: 'large', + grow: true, explicitInput: { title: 'Carrier', id: 'optionsList3', @@ -173,7 +176,8 @@ export const ConfiguredControlGroupStory = () => ( rangeSlider1: { type: RANGE_SLIDER_CONTROL, order: 4, - width: 'auto', + width: 'medium', + grow: true, explicitInput: { id: 'rangeSlider1', title: 'Average ticket price', @@ -193,7 +197,8 @@ export const RangeSliderControlGroupStory = () => ( rangeSlider1: { type: RANGE_SLIDER_CONTROL, order: 1, - width: 'auto', + width: 'medium', + grow: true, explicitInput: { id: 'rangeSlider1', title: 'Average ticket price', @@ -206,7 +211,8 @@ export const RangeSliderControlGroupStory = () => ( rangeSlider2: { type: RANGE_SLIDER_CONTROL, order: 2, - width: 'auto', + width: 'medium', + grow: true, explicitInput: { id: 'rangeSlider2', title: 'Total distance in miles', @@ -219,7 +225,8 @@ export const RangeSliderControlGroupStory = () => ( rangeSlider3: { type: RANGE_SLIDER_CONTROL, order: 3, - width: 'auto', + width: 'medium', + grow: true, explicitInput: { id: 'rangeSlider3', title: 'Flight duration in hour', diff --git a/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx b/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx index bdf1851a0daa15..715b3c3ca6794c 100644 --- a/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx +++ b/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx @@ -69,6 +69,7 @@ const SortableControlInner = forwardRef< const { useEmbeddableSelector } = useReduxContainerContext(); const { panels } = useEmbeddableSelector((state) => state); + const grow = panels[embeddableId].grow; const width = panels[embeddableId].width; const dragHandle = ( @@ -79,7 +80,7 @@ const SortableControlInner = forwardRef< return ( i18n.translate('controls.controlGroup.manageControl.widthInputTitle', { - defaultMessage: 'Control size', + defaultMessage: 'Minimum width', }), getSaveChangesTitle: () => i18n.translate('controls.controlGroup.manageControl.saveChangesTitle', { @@ -64,6 +64,10 @@ export const ControlGroupStrings = { i18n.translate('controls.controlGroup.manageControl.cancelTitle', { defaultMessage: 'Cancel', }), + getGrowSwitchTitle: () => + i18n.translate('controls.controlGroup.manageControl.growSwitchTitle', { + defaultMessage: 'Expand width to fit available space', + }), }, management: { getAddControlTitle: () => @@ -78,18 +82,10 @@ export const ControlGroupStrings = { i18n.translate('controls.controlGroup.management.flyoutTitle', { defaultMessage: 'Control settings', }), - getDefaultWidthTitle: () => - i18n.translate('controls.controlGroup.management.defaultWidthTitle', { - defaultMessage: 'Default size', - }), getDeleteButtonTitle: () => i18n.translate('controls.controlGroup.management.delete', { defaultMessage: 'Delete control', }), - getSetAllWidthsToDefaultTitle: () => - i18n.translate('controls.controlGroup.management.setAllWidths', { - defaultMessage: 'Set all sizes to default', - }), getDeleteAllButtonTitle: () => i18n.translate('controls.controlGroup.management.deleteAll', { defaultMessage: 'Delete all', diff --git a/src/plugins/controls/public/control_group/editor/control_editor.tsx b/src/plugins/controls/public/control_group/editor/control_editor.tsx index eb7eff4abb42a8..fdf99dc0f9c48d 100644 --- a/src/plugins/controls/public/control_group/editor/control_editor.tsx +++ b/src/plugins/controls/public/control_group/editor/control_editor.tsx @@ -33,6 +33,7 @@ import { EuiKeyPadMenuItem, EuiIcon, EuiToolTip, + EuiSwitch, } from '@elastic/eui'; import { EmbeddableFactoryDefinition } from '@kbn/embeddable-plugin/public'; @@ -52,9 +53,11 @@ interface EditControlProps { isCreate: boolean; title?: string; width: ControlWidth; + grow: boolean; onSave: (type: string) => void; onCancel: () => void; removeControl?: () => void; + updateGrow?: (grow: boolean) => void; updateTitle: (title?: string) => void; updateWidth: (newWidth: ControlWidth) => void; getRelevantDataViewId?: () => string | undefined; @@ -67,9 +70,11 @@ export const ControlEditor = ({ isCreate, title, width, + grow, onSave, onCancel, removeControl, + updateGrow, updateTitle, updateWidth, onTypeEditorChange, @@ -85,6 +90,7 @@ export const ControlEditor = ({ const [defaultTitle, setDefaultTitle] = useState(); const [currentTitle, setCurrentTitle] = useState(title); const [currentWidth, setCurrentWidth] = useState(width); + const [currentGrow, setCurrentGrow] = useState(grow); const [controlEditorValid, setControlEditorValid] = useState(false); const [selectedField, setSelectedField] = useState( embeddable @@ -192,6 +198,20 @@ export const ControlEditor = ({ }} /> + {updateGrow ? ( + + { + setCurrentGrow(!currentGrow); + updateGrow(!currentGrow); + }} + data-test-subj="control-editor-grow-switch" + /> + + ) : null} {removeControl && ( void; } -type EditorControlGroupInput = ControlGroupInput & - Required>; +type EditorControlGroupInput = ControlGroupInput; const editorControlGroupInputIsEqual = (a: ControlGroupInput, b: ControlGroupInput) => fastIsEqual(a, b); @@ -64,10 +61,7 @@ export const ControlGroupEditor = ({ onDeleteAll, onClose, }: EditControlGroupProps) => { - const [resetAllWidths, setResetAllWidths] = useState(false); - const [controlGroupEditorState, setControlGroupEditorState] = useState({ - defaultControlWidth: DEFAULT_CONTROL_WIDTH, ...getDefaultControlGroupInput(), ...initialInput, }); @@ -97,19 +91,8 @@ export const ControlGroupEditor = ({ const applyChangesToInput = useCallback(() => { const inputToApply = { ...controlGroupEditorState }; - if (resetAllWidths) { - const newPanels = {} as ControlsPanels; - Object.entries(initialInput.panels).forEach( - ([id, panel]) => - (newPanels[id] = { - ...panel, - width: inputToApply.defaultControlWidth, - }) - ); - inputToApply.panels = newPanels; - } if (!editorControlGroupInputIsEqual(inputToApply, initialInput)) updateInput(inputToApply); - }, [controlGroupEditorState, resetAllWidths, initialInput, updateInput]); + }, [controlGroupEditorState, initialInput, updateInput]); return ( <> @@ -133,37 +116,6 @@ export const ControlGroupEditor = ({ }} /> - - - <> - { - updateControlGroupEditorSetting({ - defaultControlWidth: newWidth as ControlWidth, - }); - }} - /> - {controlCount > 0 && ( - <> - - { - setResetAllWidths(e.target.checked); - }} - /> - - )} - - diff --git a/src/plugins/controls/public/control_group/editor/create_control.tsx b/src/plugins/controls/public/control_group/editor/create_control.tsx index 085f0d21e7dc05..2f791ac74d3ae8 100644 --- a/src/plugins/controls/public/control_group/editor/create_control.tsx +++ b/src/plugins/controls/public/control_group/editor/create_control.tsx @@ -15,13 +15,18 @@ import { pluginServices } from '../../services'; import { ControlEditor } from './control_editor'; import { ControlGroupStrings } from '../control_group_strings'; import { ControlWidth, ControlInput, IEditableControlFactory } from '../../types'; -import { DEFAULT_CONTROL_WIDTH } from '../../../common/control_group/control_group_constants'; +import { + DEFAULT_CONTROL_WIDTH, + DEFAULT_CONTROL_GROW, +} from '../../../common/control_group/control_group_constants'; import { setFlyoutRef } from '../embeddable/control_group_container'; export type CreateControlButtonTypes = 'toolbar' | 'callout'; export interface CreateControlButtonProps { defaultControlWidth?: ControlWidth; + defaultControlGrow?: boolean; updateDefaultWidth: (defaultControlWidth: ControlWidth) => void; + updateDefaultGrow: (defaultControlGrow: boolean) => void; addNewEmbeddable: (type: string, input: Omit) => void; setLastUsedDataViewId?: (newDataViewId: string) => void; getRelevantDataViewId?: () => string | undefined; @@ -35,13 +40,15 @@ interface CreateControlResult { } export const CreateControlButton = ({ + buttonType, defaultControlWidth, - updateDefaultWidth, + defaultControlGrow, addNewEmbeddable, - buttonType, closePopover, - setLastUsedDataViewId, getRelevantDataViewId, + setLastUsedDataViewId, + updateDefaultWidth, + updateDefaultGrow, }: CreateControlButtonProps) => { // Controls Services Context const { overlays, controls } = pluginServices.getServices(); @@ -81,8 +88,10 @@ export const CreateControlButton = ({ getRelevantDataViewId={getRelevantDataViewId} isCreate={true} width={defaultControlWidth ?? DEFAULT_CONTROL_WIDTH} + grow={defaultControlGrow ?? DEFAULT_CONTROL_GROW} updateTitle={(newTitle) => (inputToReturn.title = newTitle)} updateWidth={updateDefaultWidth} + updateGrow={updateDefaultGrow} onSave={(type: string) => { const factory = getControlFactory(type) as IEditableControlFactory; if (factory.presaveTransformFunction) { diff --git a/src/plugins/controls/public/control_group/editor/edit_control.tsx b/src/plugins/controls/public/control_group/editor/edit_control.tsx index 6866148ac7e9da..b3fa8834da5e0b 100644 --- a/src/plugins/controls/public/control_group/editor/edit_control.tsx +++ b/src/plugins/controls/public/control_group/editor/edit_control.tsx @@ -40,7 +40,7 @@ export const EditControlButton = ({ embeddableId }: { embeddableId: string }) => >(); const { containerActions: { untilEmbeddableLoaded, removeEmbeddable, replaceEmbeddable }, - actions: { setControlWidth }, + actions: { setControlWidth, setControlGrow }, useEmbeddableSelector, useEmbeddableDispatch, } = reduxContainerContext; @@ -114,12 +114,14 @@ export const EditControlButton = ({ embeddableId }: { embeddableId: string }) => onCancel(flyoutInstance)} updateTitle={(newTitle) => (inputToReturn.title = newTitle)} setLastUsedDataViewId={(lastUsed) => controlGroup.setLastUsedDataViewId(lastUsed)} updateWidth={(newWidth) => dispatch(setControlWidth({ width: newWidth, embeddableId }))} + updateGrow={(grow) => dispatch(setControlGrow({ grow, embeddableId }))} onTypeEditorChange={(partialInput) => { inputToReturn = { ...inputToReturn, ...partialInput }; }} diff --git a/src/plugins/controls/public/control_group/editor/editor_constants.ts b/src/plugins/controls/public/control_group/editor/editor_constants.ts index 5acad90cfbf8fe..4e0f47c9658131 100644 --- a/src/plugins/controls/public/control_group/editor/editor_constants.ts +++ b/src/plugins/controls/public/control_group/editor/editor_constants.ts @@ -9,11 +9,6 @@ import { ControlGroupStrings } from '../control_group_strings'; export const CONTROL_WIDTH_OPTIONS = [ - { - id: `auto`, - 'data-test-subj': 'control-editor-width-auto', - label: ControlGroupStrings.management.controlWidth.getAutoWidthTitle(), - }, { id: `small`, 'data-test-subj': 'control-editor-width-small', diff --git a/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx b/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx index 7d892d2a90d0e9..7fda112d83c77d 100644 --- a/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx +++ b/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx @@ -122,7 +122,11 @@ export class ControlGroupContainer extends Container< this.updateInput({ defaultControlWidth })} + updateDefaultGrow={(defaultControlGrow: boolean) => + this.updateInput({ defaultControlGrow }) + } addNewEmbeddable={(type, input) => this.addNewEmbeddable(type, input)} closePopover={closePopover} getRelevantDataViewId={() => this.getMostRelevantDataViewId()} @@ -303,6 +307,7 @@ export class ControlGroupContainer extends Container< return { order: nextOrder, width: this.getInput().defaultControlWidth, + grow: this.getInput().defaultControlGrow, ...panelState, } as ControlPanelState; } diff --git a/src/plugins/controls/public/control_group/state/control_group_reducers.ts b/src/plugins/controls/public/control_group/state/control_group_reducers.ts index 5ec4463c3bc109..25167ac9247ddb 100644 --- a/src/plugins/controls/public/control_group/state/control_group_reducers.ts +++ b/src/plugins/controls/public/control_group/state/control_group_reducers.ts @@ -25,12 +25,24 @@ export const controlGroupReducers = { ) => { state.defaultControlWidth = action.payload; }, + setDefaultControlGrow: ( + state: WritableDraft, + action: PayloadAction + ) => { + state.defaultControlGrow = action.payload; + }, setControlWidth: ( state: WritableDraft, action: PayloadAction<{ width: ControlWidth; embeddableId: string }> ) => { state.panels[action.payload.embeddableId].width = action.payload.width; }, + setControlGrow: ( + state: WritableDraft, + action: PayloadAction<{ grow: boolean; embeddableId: string }> + ) => { + state.panels[action.payload.embeddableId].grow = action.payload.grow; + }, setControlOrders: ( state: WritableDraft, action: PayloadAction<{ ids: string[] }> diff --git a/src/plugins/controls/public/control_types/range_slider/range_slider.component.tsx b/src/plugins/controls/public/control_types/range_slider/range_slider.component.tsx index 259b6bd7f66a1d..54b53f25da89f7 100644 --- a/src/plugins/controls/public/control_types/range_slider/range_slider.component.tsx +++ b/src/plugins/controls/public/control_types/range_slider/range_slider.component.tsx @@ -20,6 +20,7 @@ import './range_slider.scss'; interface Props { componentStateSubject: BehaviorSubject; + ignoreValidation: boolean; } // Availableoptions and loading state is controled by the embeddable, but is not considered embeddable input. export interface RangeSliderComponentState { @@ -28,9 +29,10 @@ export interface RangeSliderComponentState { min: string; max: string; loading: boolean; + isInvalid?: boolean; } -export const RangeSliderComponent: FC = ({ componentStateSubject }) => { +export const RangeSliderComponent: FC = ({ componentStateSubject, ignoreValidation }) => { // Redux embeddable Context to get state from Embeddable input const { useEmbeddableDispatch, @@ -40,10 +42,11 @@ export const RangeSliderComponent: FC = ({ componentStateSubject }) => { const dispatch = useEmbeddableDispatch(); // useStateObservable to get component state from Embeddable - const { loading, min, max, fieldFormatter } = useStateObservable( - componentStateSubject, - componentStateSubject.getValue() - ); + const { loading, min, max, fieldFormatter, isInvalid } = + useStateObservable( + componentStateSubject, + componentStateSubject.getValue() + ); const { value, id, title } = useEmbeddableSelector((state) => state); @@ -64,6 +67,7 @@ export const RangeSliderComponent: FC = ({ componentStateSubject }) => { value={value ?? ['', '']} onChange={onChangeComplete} fieldFormatter={fieldFormatter} + isInvalid={!ignoreValidation && isInvalid} /> ); }; diff --git a/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable.tsx b/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable.tsx index 1ad34fd361ac64..d7e1984b7c54cc 100644 --- a/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable.tsx +++ b/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable.tsx @@ -12,12 +12,14 @@ import { buildRangeFilter, COMPARE_ALL_OPTIONS, RangeFilterParams, + Filter, + Query, } from '@kbn/es-query'; import React from 'react'; import ReactDOM from 'react-dom'; import { get, isEqual } from 'lodash'; import deepEqual from 'fast-deep-equal'; -import { Subscription, BehaviorSubject } from 'rxjs'; +import { Subscription, BehaviorSubject, lastValueFrom } from 'rxjs'; import { debounceTime, distinctUntilChanged, skip, map } from 'rxjs/operators'; import { @@ -59,6 +61,7 @@ interface RangeSliderDataFetchProps { dataViewId: string; query?: ControlInput['query']; filters?: ControlInput['filters']; + validate?: boolean; } const fieldMissingError = (fieldName: string) => @@ -99,6 +102,7 @@ export class RangeSliderEmbeddable extends Embeddable value, + isInvalid: false, }; this.updateComponentState(this.componentState); @@ -111,7 +115,7 @@ export class RangeSliderEmbeddable extends Embeddable { + this.runRangeSliderQuery().then(async () => { if (initialValue) { this.setInitializationFinished(); } @@ -122,6 +126,7 @@ export class RangeSliderEmbeddable extends Embeddable { const dataFetchPipe = this.getInput$().pipe( map((newInput) => ({ + validate: !Boolean(newInput.ignoreParentSettings?.ignoreValidations), lastReloadRequestTime: newInput.lastReloadRequestTime, dataViewId: newInput.dataViewId, fieldName: newInput.fieldName, @@ -134,7 +139,7 @@ export class RangeSliderEmbeddable extends Embeddable { - const aggBody: any = {}; - if (field) { - if (field.scripted) { - aggBody.script = { - source: field.script, - lang: field.lang, - }; - } else { - aggBody.field = field.name; - } - } - - return { - maxAgg: { - max: aggBody, - }, - minAgg: { - min: aggBody, - }, - }; - }; - - private fetchMinMax = async () => { + private runRangeSliderQuery = async () => { this.updateComponentState({ loading: true }); this.updateOutput({ loading: true }); const { dataView, field } = await this.getCurrentDataViewAndField(); @@ -220,7 +202,7 @@ export class RangeSliderEmbeddable extends Embeddable { const searchSource = await this.dataService.searchSource.create(); searchSource.setField('size', 0); searchSource.setField('index', dataView); - const aggs = this.minMaxAgg(field); - searchSource.setField('aggs', aggs); - searchSource.setField('filter', filters); - if (!ignoreParentSettings?.ignoreQuery) { + if (query) { searchSource.setField('query', query); } - const resp = await searchSource.fetch$().toPromise(); + const aggBody: any = {}; + + if (field) { + if (field.scripted) { + aggBody.script = { + source: field.script, + lang: field.lang, + }; + } else { + aggBody.field = field.name; + } + } + + const aggs = { + maxAgg: { + max: aggBody, + }, + minAgg: { + min: aggBody, + }, + }; + + searchSource.setField('aggs', aggs); + + const resp = await lastValueFrom(searchSource.fetch$()); const min = get(resp, 'rawResponse.aggregations.minAgg.value', ''); const max = get(resp, 'rawResponse.aggregations.maxAgg.value', ''); - this.updateComponentState({ - min: `${min ?? ''}`, - max: `${max ?? ''}`, - }); - - // build filter with new min/max - await this.buildFilter(); + return { min, max }; }; private buildFilter = async () => { - const { value: [selectedMin, selectedMax] = ['', ''], ignoreParentSettings } = this.getInput(); + const { + value: [selectedMin, selectedMax] = ['', ''], + query, + timeRange, + filters = [], + ignoreParentSettings, + } = this.getInput(); + const availableMin = this.componentState.min; const availableMax = this.componentState.max; @@ -271,22 +302,14 @@ export class RangeSliderEmbeddable extends Embeddable parseFloat(selectedMax); - const isLowerSelectionOutOfRange = - hasLowerSelection && parseFloat(selectedMin) > parseFloat(availableMax); - const isUpperSelectionOutOfRange = - hasUpperSelection && parseFloat(selectedMax) < parseFloat(availableMin); - const isSelectionOutOfRange = - (!ignoreParentSettings?.ignoreValidations && hasData && isLowerSelectionOutOfRange) || - isUpperSelectionOutOfRange; + const { dataView, field } = await this.getCurrentDataViewAndField(); - if (!hasData || !hasEitherSelection || hasInvalidSelection || isSelectionOutOfRange) { - this.updateComponentState({ loading: false }); + if (!hasData || !hasEitherSelection) { + this.updateComponentState({ + loading: false, + isInvalid: !ignoreParentSettings?.ignoreValidations && hasEitherSelection, + }); this.updateOutput({ filters: [], dataViews: [dataView], loading: false }); return; } @@ -307,12 +330,52 @@ export class RangeSliderEmbeddable extends Embeddable { - this.fetchMinMax(); + this.runRangeSliderQuery(); }; public destroy = () => { @@ -327,7 +390,14 @@ export class RangeSliderEmbeddable extends Embeddable - + , node ); diff --git a/src/plugins/controls/public/control_types/range_slider/range_slider_popover.tsx b/src/plugins/controls/public/control_types/range_slider/range_slider_popover.tsx index 1bb7501f7104f8..fce3dbdfe7009e 100644 --- a/src/plugins/controls/public/control_types/range_slider/range_slider_popover.tsx +++ b/src/plugins/controls/public/control_types/range_slider/range_slider_popover.tsx @@ -23,8 +23,11 @@ import { import { RangeSliderStrings } from './range_slider_strings'; import { RangeValue } from './types'; +const INVALID_CLASS = 'rangeSliderAnchor__fieldNumber--invalid'; + export interface Props { id: string; + isInvalid?: boolean; isLoading?: boolean; min: string; max: string; @@ -36,6 +39,7 @@ export interface Props { export const RangeSliderPopover: FC = ({ id, + isInvalid, isLoading, min, max, @@ -52,6 +56,13 @@ export const RangeSliderPopover: FC = ({ let helpText = ''; const hasAvailableRange = min !== '' && max !== ''; + + if (!hasAvailableRange) { + helpText = RangeSliderStrings.popover.getNoAvailableDataHelpText(); + } else if (isInvalid) { + helpText = RangeSliderStrings.popover.getNoDataHelpText(); + } + const hasLowerBoundSelection = value[0] !== ''; const hasUpperBoundSelection = value[1] !== ''; @@ -60,23 +71,10 @@ export const RangeSliderPopover: FC = ({ const minValue = parseFloat(min); const maxValue = parseFloat(max); - if (!hasAvailableRange) { - helpText = 'There is no data to display. Adjust the time range and filters.'; - } - // EuiDualRange can only handle integers as min/max const roundedMin = hasAvailableRange ? Math.floor(minValue) : minValue; const roundedMax = hasAvailableRange ? Math.ceil(maxValue) : maxValue; - const isLowerSelectionInvalid = hasLowerBoundSelection && lowerBoundValue > roundedMax; - const isUpperSelectionInvalid = hasUpperBoundSelection && upperBoundValue < roundedMin; - const isSelectionInvalid = - hasAvailableRange && (isLowerSelectionInvalid || isUpperSelectionInvalid); - - if (isSelectionInvalid) { - helpText = RangeSliderStrings.popover.getNoDataHelpText(); - } - if (lowerBoundValue > upperBoundValue) { errorMessage = RangeSliderStrings.errors.getUpperLessThanLowerErrorMessage(); } @@ -89,7 +87,7 @@ export const RangeSliderPopover: FC = ({ const ticks = []; const levels = []; - if (hasAvailableRange) { + if (hasAvailableRange && isPopoverOpen) { ticks.push({ value: rangeSliderMin, label: fieldFormatter(String(rangeSliderMin)) }); ticks.push({ value: rangeSliderMax, label: fieldFormatter(String(rangeSliderMax)) }); levels.push({ min: roundedMin, max: roundedMax, color: 'success' }); @@ -127,17 +125,15 @@ export const RangeSliderPopover: FC = ({ controlOnly fullWidth className={`rangeSliderAnchor__fieldNumber ${ - hasLowerBoundSelection && isSelectionInvalid - ? 'rangeSliderAnchor__fieldNumber--invalid' - : '' + hasLowerBoundSelection && isInvalid ? INVALID_CLASS : '' }`} value={hasLowerBoundSelection ? lowerBoundValue : ''} onChange={(event) => { onChange([event.target.value, isNaN(upperBoundValue) ? '' : String(upperBoundValue)]); }} - disabled={!hasAvailableRange || isLoading} + disabled={isLoading} placeholder={`${hasAvailableRange ? roundedMin : ''}`} - isInvalid={isLowerSelectionInvalid} + isInvalid={isInvalid} data-test-subj="rangeSlider__lowerBoundFieldNumber" /> @@ -151,17 +147,15 @@ export const RangeSliderPopover: FC = ({ controlOnly fullWidth className={`rangeSliderAnchor__fieldNumber ${ - hasUpperBoundSelection && isSelectionInvalid - ? 'rangeSliderAnchor__fieldNumber--invalid' - : '' + hasUpperBoundSelection && isInvalid ? INVALID_CLASS : '' }`} value={hasUpperBoundSelection ? upperBoundValue : ''} onChange={(event) => { onChange([isNaN(lowerBoundValue) ? '' : String(lowerBoundValue), event.target.value]); }} - disabled={!hasAvailableRange || isLoading} + disabled={isLoading} placeholder={`${hasAvailableRange ? roundedMax : ''}`} - isInvalid={isUpperSelectionInvalid} + isInvalid={isInvalid} data-test-subj="rangeSlider__upperBoundFieldNumber" /> @@ -234,19 +228,17 @@ export const RangeSliderPopover: FC = ({ {errorMessage || helpText} - {hasAvailableRange ? ( - - - onChange(['', ''])} - aria-label={RangeSliderStrings.popover.getClearRangeButtonTitle()} - data-test-subj="rangeSlider__clearRangeButton" - /> - - - ) : null} + + + onChange(['', ''])} + aria-label={RangeSliderStrings.popover.getClearRangeButtonTitle()} + data-test-subj="rangeSlider__clearRangeButton" + /> + + ); diff --git a/src/plugins/controls/public/control_types/range_slider/range_slider_strings.ts b/src/plugins/controls/public/control_types/range_slider/range_slider_strings.ts index a901f79ba20f57..53d614fd54a2e3 100644 --- a/src/plugins/controls/public/control_types/range_slider/range_slider_strings.ts +++ b/src/plugins/controls/public/control_types/range_slider/range_slider_strings.ts @@ -42,7 +42,11 @@ export const RangeSliderStrings = { }), getNoDataHelpText: () => i18n.translate('controls.rangeSlider.popover.noDataHelpText', { - defaultMessage: 'Selected range is outside of available data. No filter was applied.', + defaultMessage: 'Selected range resulted in no data. No filter was applied.', + }), + getNoAvailableDataHelpText: () => + i18n.translate('controls.rangeSlider.popover.noAvailableDataHelpText', { + defaultMessage: 'There is no data to display. Adjust the time range and filters.', }), }, errors: { diff --git a/src/plugins/controls/public/services/kibana/data.ts b/src/plugins/controls/public/services/kibana/data.ts index 29a96a98c7e763..0dc702542633b0 100644 --- a/src/plugins/controls/public/services/kibana/data.ts +++ b/src/plugins/controls/public/services/kibana/data.ts @@ -8,7 +8,7 @@ import { DataViewField } from '@kbn/data-views-plugin/common'; import { get } from 'lodash'; -import { from } from 'rxjs'; +import { from, lastValueFrom } from 'rxjs'; import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; import { ControlsDataService } from '../data'; import { ControlsPluginStartDeps } from '../../types'; @@ -78,7 +78,7 @@ export const dataServiceFactory: DataServiceFactory = ({ startPlugins }) => { searchSource.setField('filter', ignoreParentSettings?.ignoreFilters ? [] : filters); searchSource.setField('query', ignoreParentSettings?.ignoreQuery ? undefined : query); - const resp = await searchSource.fetch$().toPromise(); + const resp = await lastValueFrom(searchSource.fetch$()); const min = get(resp, 'rawResponse.aggregations.minAgg.value', undefined); const max = get(resp, 'rawResponse.aggregations.maxAgg.value', undefined); diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index c95a2308c39659..5dbc5de1fa8959 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -35,8 +35,16 @@ export function DashboardApp({ redirectTo, history, }: DashboardAppProps) { - const { core, chrome, embeddable, onAppLeave, uiSettings, data, spacesService } = - useKibana().services; + const { + core, + chrome, + embeddable, + onAppLeave, + uiSettings, + data, + spacesService, + screenshotModeService, + } = useKibana().services; const kbnUrlStateStorage = useMemo( () => @@ -137,7 +145,13 @@ export function DashboardApp({ )}${history.location.search}`, }) : null} -
+
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 f9fc2c0a216331..5fc17b73bcd953 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/_dashboard_viewport.scss +++ b/src/plugins/dashboard/public/application/embeddable/viewport/_dashboard_viewport.scss @@ -14,3 +14,7 @@ .dshDashboardEmptyScreen { margin-top: $euiSizeS; } + +.dashboardViewport--screenshotMode .controlsWrapper--empty { + display:none +} 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 82c5bab836a853..7ce3a139f773ac 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx +++ b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx @@ -117,7 +117,15 @@ export class DashboardViewport extends React.Component ) : null} -
+ +
0 + ? 'dshDashboardViewport-controls' + : '' + } + ref={this.controlsRoot} + /> ) : null}
true, + isCompatible: async (context: ValueClickContext) => { + const filters = await createFiltersFromValueClickAction(context.data); + return filters.length > 0; + }, execute: async (context: ValueClickActionContext) => { try { const filters: Filter[] = await createFiltersFromValueClickAction(context.data); diff --git a/src/plugins/data_views/common/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts index 098e895324aa75..b263c7d77fe3be 100644 --- a/src/plugins/data_views/common/data_views/data_views.ts +++ b/src/plugins/data_views/common/data_views/data_views.ts @@ -113,7 +113,17 @@ export class DataViewsService { private savedObjectsCache?: Array> | null; private apiClient: IDataViewsApiClient; private fieldFormats: FieldFormatsStartCommon; + /** + * Handler for service notifications + * @param toastInputFields notification content in toast format + * @param key used to indicate uniqueness of the notification + */ private onNotification: OnNotification; + /* + * Handler for service errors + * @param error notification content in toast format + * @param key used to indicate uniqueness of the error + */ private onError: OnError; private dataViewCache: ReturnType; public getCanSave: () => Promise; @@ -333,15 +343,22 @@ export class DataViewsService { indexPattern.fields.replaceAll(fieldsWithSavedAttrs); } catch (err) { if (err instanceof DataViewMissingIndices) { - this.onNotification({ title: err.message, color: 'danger', iconType: 'alert' }); + this.onNotification( + { title: err.message, color: 'danger', iconType: 'alert' }, + `refreshFields:${indexPattern.title}` + ); } - this.onError(err, { - title: i18n.translate('dataViews.fetchFieldErrorTitle', { - defaultMessage: 'Error fetching fields for data view {title} (ID: {id})', - values: { id: indexPattern.id, title: indexPattern.title }, - }), - }); + this.onError( + err, + { + title: i18n.translate('dataViews.fetchFieldErrorTitle', { + defaultMessage: 'Error fetching fields for data view {title} (ID: {id})', + values: { id: indexPattern.id, title: indexPattern.title }, + }), + }, + indexPattern.title + ); } }; @@ -378,16 +395,23 @@ export class DataViewsService { return this.fieldArrayToMap(updatedFieldList, fieldAttrs); } catch (err) { if (err instanceof DataViewMissingIndices) { - this.onNotification({ title: err.message, color: 'danger', iconType: 'alert' }); + this.onNotification( + { title: err.message, color: 'danger', iconType: 'alert' }, + `refreshFieldSpecMap:${title}` + ); return {}; } - this.onError(err, { - title: i18n.translate('dataViews.fetchFieldErrorTitle', { - defaultMessage: 'Error fetching fields for data view {title} (ID: {id})', - values: { id, title }, - }), - }); + this.onError( + err, + { + title: i18n.translate('dataViews.fetchFieldErrorTitle', { + defaultMessage: 'Error fetching fields for data view {title} (ID: {id})', + values: { id, title }, + }), + }, + title + ); throw err; } }; @@ -530,18 +554,25 @@ export class DataViewsService { } } catch (err) { if (err instanceof DataViewMissingIndices) { - this.onNotification({ - title: err.message, - color: 'danger', - iconType: 'alert', - }); + this.onNotification( + { + title: err.message, + color: 'danger', + iconType: 'alert', + }, + `initFromSavedObject:${title}` + ); } else { - this.onError(err, { - title: i18n.translate('dataViews.fetchFieldErrorTitle', { - defaultMessage: 'Error fetching fields for data view {title} (ID: {id})', - values: { id: savedObject.id, title }, - }), - }); + this.onError( + err, + { + title: i18n.translate('dataViews.fetchFieldErrorTitle', { + defaultMessage: 'Error fetching fields for data view {title} (ID: {id})', + values: { id: savedObject.id, title }, + }), + }, + title || '' + ); } } @@ -718,7 +749,10 @@ export class DataViewsService { 'Unable to write data view! Refresh the page to get the most up to date changes for this data view.', }); - this.onNotification({ title, color: 'danger' }); + this.onNotification( + { title, color: 'danger' }, + `updateSavedObject:${indexPattern.title}` + ); throw err; } diff --git a/src/plugins/data_views/common/types.ts b/src/plugins/data_views/common/types.ts index 4032d83b24c5a1..f4bed383d8447f 100644 --- a/src/plugins/data_views/common/types.ts +++ b/src/plugins/data_views/common/types.ts @@ -123,8 +123,8 @@ export interface FieldAttrSet { count?: number; } -export type OnNotification = (toastInputFields: ToastInputFields) => void; -export type OnError = (error: Error, toastInputFields: ErrorToastOptions) => void; +export type OnNotification = (toastInputFields: ToastInputFields, key: string) => void; +export type OnError = (error: Error, toastInputFields: ErrorToastOptions, key: string) => void; export interface UiSettingsCommon { get: (key: string) => Promise; diff --git a/src/plugins/data_views/public/debounce_by_key.test.ts b/src/plugins/data_views/public/debounce_by_key.test.ts new file mode 100644 index 00000000000000..c5fba82fdcfdf9 --- /dev/null +++ b/src/plugins/data_views/public/debounce_by_key.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { debounceByKey } from './debounce_by_key'; + +describe('debounceByKey', () => { + test('debounce, confirm params', async () => { + const fn = jest.fn(); + const fn2 = jest.fn(); + + const debouncedFn = debounceByKey(fn, 1000); + const debouncedFn2 = debounceByKey(fn2, 1000); + + // debounces based on key, not params + debouncedFn('a')(1); + debouncedFn('a')(2); + + debouncedFn2('b')(2); + debouncedFn2('b')(1); + + expect(fn).toBeCalledTimes(1); + expect(fn).toBeCalledWith(1); + expect(fn2).toBeCalledTimes(1); + expect(fn2).toBeCalledWith(2); + }); +}); diff --git a/src/plugins/data_views/public/debounce_by_key.ts b/src/plugins/data_views/public/debounce_by_key.ts new file mode 100644 index 00000000000000..c8ae7094a6437b --- /dev/null +++ b/src/plugins/data_views/public/debounce_by_key.ts @@ -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 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 { debounce } from 'lodash'; + +export const debounceByKey = any>( + fn: F, + waitInMs: number +): ((key: string) => F) => { + const debouncerCollector: Record = {}; + return (key: string) => { + if (!debouncerCollector[key]) { + debouncerCollector[key] = debounce(fn, waitInMs, { + leading: true, + }); + } + return debouncerCollector[key]; + }; +}; diff --git a/src/plugins/data_views/public/plugin.ts b/src/plugins/data_views/public/plugin.ts index 95378df130c469..5c3ad2c33307dd 100644 --- a/src/plugins/data_views/public/plugin.ts +++ b/src/plugins/data_views/public/plugin.ts @@ -25,6 +25,8 @@ import { import { DataViewsServicePublic } from './data_views_service_public'; import { HasData } from './services'; +import { debounceByKey } from './debounce_by_key'; + export class DataViewsPublicPlugin implements Plugin< @@ -50,16 +52,28 @@ export class DataViewsPublicPlugin { fieldFormats }: DataViewsPublicStartDependencies ): DataViewsPublicPluginStart { const { uiSettings, http, notifications, savedObjects, theme, overlays, application } = core; + + const onNotifDebounced = debounceByKey( + notifications.toasts.add.bind(notifications.toasts), + 10000 + ); + const onErrorDebounced = debounceByKey( + notifications.toasts.addError.bind(notifications.toasts), + 10000 + ); + return new DataViewsServicePublic({ hasData: this.hasData.start(core), uiSettings: new UiSettingsPublicToCommon(uiSettings), savedObjectsClient: new SavedObjectsClientPublicToCommon(savedObjects.client), apiClient: new DataViewsApiClient(http), fieldFormats, - onNotification: (toastInputFields) => { - notifications.toasts.add(toastInputFields); + onNotification: (toastInputFields, key) => { + onNotifDebounced(key)(toastInputFields); + }, + onError: (error, toastInputFields, key) => { + onErrorDebounced(key)(error, toastInputFields); }, - onError: notifications.toasts.addError.bind(notifications.toasts), onRedirectNoIndexPattern: onRedirectNoIndexPattern( application.capabilities, application.navigateToApp, diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 173264aee731ee..2ea0215cad04d0 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +export const PLUGIN_ID = 'discover'; export const APP_ICON = 'discoverApp'; export const DEFAULT_COLUMNS_SETTING = 'defaultColumns'; export const SAMPLE_SIZE_SETTING = 'discover:sampleSize'; diff --git a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.test.tsx b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.test.tsx index 5bf5107dc03494..44dcb0901dd7cf 100644 --- a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.test.tsx +++ b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.test.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { mountWithIntl, findTestSubject } from '@kbn/test-jest-helpers'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { CALLOUT_STATE_KEY, @@ -15,11 +15,12 @@ import { } from './document_explorer_update_callout'; import { LocalStorageMock } from '../../../../__mocks__/local_storage_mock'; import { DiscoverServices } from '../../../../build_services'; +import { discoverServiceMock } from '../../../../__mocks__/services'; +import { DiscoverTourProvider } from '../../../../components/discover_tour'; const defaultServices = { - addBasePath: () => '', - docLinks: { links: { discover: { documentExplorer: '' } } }, - capabilities: { advancedSettings: { save: true } }, + ...discoverServiceMock, + capabilities: { ...discoverServiceMock.capabilities, advancedSettings: { save: true } }, storage: new LocalStorageMock({ [CALLOUT_STATE_KEY]: false }), } as unknown as DiscoverServices; @@ -57,4 +58,18 @@ describe('Document Explorer Update callout', () => { expect(result.find('.dscDocumentExplorerCallout').exists()).toBeFalsy(); }); + + it('should start a tour when the button is clicked', () => { + const result = mountWithIntl( + + + + + + ); + + expect(result.find({ isStepOpen: true })).toHaveLength(0); + findTestSubject(result, 'discoverTakeTourButton').simulate('click'); + expect(result.find({ isStepOpen: true })).toHaveLength(1); + }); }); diff --git a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx index 5a9c6a68d6bb3b..2fe073946627ad 100644 --- a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx +++ b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx @@ -6,22 +6,21 @@ * Side Public License, v 1. */ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import './document_explorer_callout.scss'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButton, + EuiButtonEmpty, EuiButtonIcon, EuiCallOut, EuiFlexGroup, EuiFlexItem, - EuiLink, - useEuiTheme, } from '@elastic/eui'; -import { css } from '@emotion/react'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { useDiscoverServices } from '../../../../utils/use_discover_services'; +import { useDiscoverTourContext } from '../../../../components/discover_tour'; export const CALLOUT_STATE_KEY = 'discover:docExplorerUpdateCalloutClosed'; @@ -37,16 +36,9 @@ const updateStoredCalloutState = (newState: boolean, storage: Storage) => { * The callout that's displayed when Document explorer is enabled */ export const DocumentExplorerUpdateCallout = () => { - const { euiTheme } = useEuiTheme(); - const { storage, capabilities, docLinks } = useDiscoverServices(); + const { storage, capabilities } = useDiscoverServices(); const [calloutClosed, setCalloutClosed] = useState(getStoredCalloutState(storage)); - - const semiBoldStyle = useMemo( - () => css` - font-weight: ${euiTheme.font.weight.semiBold}; - `, - [euiTheme.font.weight.semiBold] - ); + const { onStartTour } = useDiscoverTourContext(); const onCloseCallout = useCallback(() => { updateStoredCalloutState(true, storage); @@ -67,44 +59,37 @@ export const DocumentExplorerUpdateCallout = () => { >

- - - - - ), - documentExplorer: ( - - - - - - ), - }} + id="discover.docExplorerUpdateCallout.description" + defaultMessage="Add relevant fields, reorder and sort columns, resize rows, and more in the document table." />

- - - + + + + + + + + + + + ); }; @@ -114,8 +99,8 @@ function CalloutTitle({ onCloseCallout }: { onCloseCallout: () => void }) { 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 7b715bb56a74c7..1a84516fbdd8d6 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 @@ -35,6 +35,7 @@ import { SortPairArr } from '../../../../components/doc_table/lib/get_sort'; import { ElasticSearchHit } from '../../../../types'; import { DocumentExplorerCallout } from '../document_explorer_callout'; import { DocumentExplorerUpdateCallout } from '../document_explorer_callout/document_explorer_update_callout'; +import { DiscoverTourProvider } from '../../../../components/discover_tour'; const DocTableInfiniteMemoized = React.memo(DocTableInfinite); const DataGridMemoized = React.memo(DiscoverGrid); @@ -157,7 +158,9 @@ function DiscoverDocumentsComponent({ )} {!isLegacy && ( <> - + + +
- +
setIsFlyoutVisible(true)} > diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index 0361cf1764419e..c69cb448a195ca 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -15,6 +15,10 @@ import { } from '@kbn/data-views-plugin/public'; import { redirectWhenMissing } from '@kbn/kibana-utils-plugin/public'; import { useExecutionContext } from '@kbn/kibana-react-plugin/public'; +import { + AnalyticsNoDataPageKibanaProvider, + AnalyticsNoDataPage, +} from '@kbn/shared-ux-page-analytics-no-data'; import { SavedSearch, getSavedSearch, @@ -45,6 +49,7 @@ export function DiscoverMainRoute() { data, toastNotifications, http: { basePath }, + dataViewEditor, } = services; const [error, setError] = useState(); const [savedSearch, setSavedSearch] = useState(); @@ -52,6 +57,7 @@ export function DiscoverMainRoute() { const [indexPatternList, setIndexPatternList] = useState>>( [] ); + const [showNoDataPage, setShowNoDataPage] = useState(false); const { id } = useParams(); useExecutionContext(core.executionContext, { @@ -60,27 +66,20 @@ export function DiscoverMainRoute() { id: id || 'new', }); - const navigateToOverview = useCallback(() => { - core.application.navigateToApp('kibanaOverview', { path: '#' }); - }, [core.application]); - - const checkForDataViews = useCallback(async () => { - const hasUserDataView = await data.dataViews.hasUserDataView().catch(() => true); - if (!hasUserDataView) { - navigateToOverview(); - } - const defaultDataView = await data.dataViews.getDefaultDataView(); - if (!defaultDataView) { - navigateToOverview(); - } - }, [navigateToOverview, data.dataViews]); - - useEffect(() => { - const savedSearchId = id; - - async function loadDefaultOrCurrentIndexPattern(searchSource: ISearchSource) { + const loadDefaultOrCurrentIndexPattern = useCallback( + async (searchSource: ISearchSource) => { try { - await checkForDataViews(); + const hasUserDataView = await data.dataViews.hasData.hasUserDataView().catch(() => false); + const hasEsData = await data.dataViews.hasData.hasESData().catch(() => false); + if (!hasUserDataView || !hasEsData) { + setShowNoDataPage(true); + return; + } + const defaultDataView = await data.dataViews.getDefaultDataView(); + if (!defaultDataView) { + setShowNoDataPage(true); + return; + } const { appStateContainer } = getState({ history, uiSettings: config }); const { index } = appStateContainer.getState(); const ip = await loadIndexPattern(index || '', data.dataViews, config); @@ -94,78 +93,91 @@ export function DiscoverMainRoute() { } catch (e) { setError(e); } - } + }, + [config, data.dataViews, history, toastNotifications] + ); - async function loadSavedSearch() { - try { - const currentSavedSearch = await getSavedSearch(savedSearchId, { - search: services.data.search, - savedObjectsClient: core.savedObjects.client, - spaces: services.spaces, - }); - - const loadedIndexPattern = await loadDefaultOrCurrentIndexPattern( - currentSavedSearch.searchSource - ); + const loadSavedSearch = useCallback(async () => { + try { + const currentSavedSearch = await getSavedSearch(id, { + search: services.data.search, + savedObjectsClient: core.savedObjects.client, + spaces: services.spaces, + }); - if (!loadedIndexPattern) { - return; - } + const loadedIndexPattern = await loadDefaultOrCurrentIndexPattern( + currentSavedSearch.searchSource + ); - if (!currentSavedSearch.searchSource.getField('index')) { - currentSavedSearch.searchSource.setField('index', loadedIndexPattern); - } + if (!loadedIndexPattern) { + return; + } - setSavedSearch(currentSavedSearch); + if (!currentSavedSearch.searchSource.getField('index')) { + currentSavedSearch.searchSource.setField('index', loadedIndexPattern); + } - if (currentSavedSearch.id) { - chrome.recentlyAccessed.add( - getSavedSearchFullPathUrl(currentSavedSearch.id), - currentSavedSearch.title ?? '', - currentSavedSearch.id - ); - } - } catch (e) { - if (e instanceof DataViewSavedObjectConflictError) { - setError(e); - } else { - redirectWhenMissing({ - history, - navigateToApp: core.application.navigateToApp, - basePath, - mapping: { - search: '/', - 'index-pattern': { - app: 'management', - path: `kibana/objects/savedSearches/${id}`, - }, - }, - toastNotifications, - onBeforeRedirect() { - getUrlTracker().setTrackedUrl('/'); + setSavedSearch(currentSavedSearch); + + if (currentSavedSearch.id) { + chrome.recentlyAccessed.add( + getSavedSearchFullPathUrl(currentSavedSearch.id), + currentSavedSearch.title ?? '', + currentSavedSearch.id + ); + } + } catch (e) { + if (e instanceof DataViewSavedObjectConflictError) { + setError(e); + } else { + redirectWhenMissing({ + history, + navigateToApp: core.application.navigateToApp, + basePath, + mapping: { + search: '/', + 'index-pattern': { + app: 'management', + path: `kibana/objects/savedSearches/${id}`, }, - theme: core.theme, - })(e); - } + }, + toastNotifications, + onBeforeRedirect() { + getUrlTracker().setTrackedUrl('/'); + }, + theme: core.theme, + })(e); } } - - loadSavedSearch(); }, [ + id, + services.data.search, + services.spaces, core.savedObjects.client, - basePath, - chrome.recentlyAccessed, - config, core.application.navigateToApp, - data.dataViews, + core.theme, + loadDefaultOrCurrentIndexPattern, + chrome.recentlyAccessed, history, - id, - services, + basePath, toastNotifications, - core.theme, - checkForDataViews, ]); + const onDataViewCreated = useCallback( + async (dataView: unknown) => { + if (dataView) { + setShowNoDataPage(false); + setError(undefined); + await loadSavedSearch(); + } + }, + [loadSavedSearch] + ); + + useEffect(() => { + loadSavedSearch(); + }, [loadSavedSearch]); + useEffect(() => { chrome.setBreadcrumbs( savedSearch && savedSearch.title @@ -174,6 +186,19 @@ export function DiscoverMainRoute() { ); }, [chrome, savedSearch]); + if (showNoDataPage) { + const analyticsServices = { + coreStart: core, + dataViews: data.dataViews, + dataViewEditor, + }; + return ( + + + + ); + } + if (error) { return ; } diff --git a/src/plugins/discover/public/assets/discover_tour/add_fields.gif b/src/plugins/discover/public/assets/discover_tour/add_fields.gif new file mode 100644 index 00000000000000..c955a9aa99e08d Binary files /dev/null and b/src/plugins/discover/public/assets/discover_tour/add_fields.gif differ diff --git a/src/plugins/discover/public/assets/discover_tour/expand.gif b/src/plugins/discover/public/assets/discover_tour/expand.gif new file mode 100644 index 00000000000000..7131fed839478d Binary files /dev/null and b/src/plugins/discover/public/assets/discover_tour/expand.gif differ diff --git a/src/plugins/discover/public/assets/discover_tour/reorder_columns.gif b/src/plugins/discover/public/assets/discover_tour/reorder_columns.gif new file mode 100644 index 00000000000000..d3aeedb513c1ee Binary files /dev/null and b/src/plugins/discover/public/assets/discover_tour/reorder_columns.gif differ diff --git a/src/plugins/discover/public/assets/discover_tour/rows_per_line.gif b/src/plugins/discover/public/assets/discover_tour/rows_per_line.gif new file mode 100644 index 00000000000000..66033d03d8fd28 Binary files /dev/null and b/src/plugins/discover/public/assets/discover_tour/rows_per_line.gif differ diff --git a/src/plugins/discover/public/assets/discover_tour/sort.gif b/src/plugins/discover/public/assets/discover_tour/sort.gif new file mode 100644 index 00000000000000..6d22b947a206f1 Binary files /dev/null and b/src/plugins/discover/public/assets/discover_tour/sort.gif differ diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx index da9892f343d706..64cbab5c1511b2 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx @@ -6,12 +6,11 @@ * Side Public License, v 1. */ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState, useRef } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import './discover_grid.scss'; import { EuiDataGridSorting, - EuiDataGridProps, EuiDataGrid, EuiScreenReaderOnly, EuiSpacer, @@ -19,6 +18,7 @@ import { htmlIdGenerator, EuiLoadingSpinner, EuiIcon, + EuiDataGridRefProps, } from '@elastic/eui'; import type { DataView } from '@kbn/data-views-plugin/public'; import { flattenHit } from '@kbn/data-plugin/public'; @@ -165,9 +165,7 @@ export interface DiscoverGridProps { onUpdateRowHeight?: (rowHeight: number) => void; } -export const EuiDataGridMemoized = React.memo((props: EuiDataGridProps) => { - return ; -}); +export const EuiDataGridMemoized = React.memo(EuiDataGrid); const CONTROL_COLUMN_IDS_DEFAULT = ['openDetails', 'select']; @@ -199,6 +197,7 @@ export const DiscoverGrid = ({ rowHeightState, onUpdateRowHeight, }: DiscoverGridProps) => { + const dataGridRef = useRef(null); const services = useDiscoverServices(); const [selectedDocs, setSelectedDocs] = useState([]); const [isFilterActive, setIsFilterActive] = useState(false); @@ -232,6 +231,12 @@ export const DiscoverGrid = ({ return rowsFiltered; }, [rows, usedSelectedDocs, isFilterActive]); + const displayedRowsFlattened = useMemo(() => { + return displayedRows.map((hit) => { + return flattenHit(hit, indexPattern, { includeIgnoredValues: true }); + }); + }, [displayedRows, indexPattern]); + /** * Pagination */ @@ -290,16 +295,20 @@ export const DiscoverGrid = ({ getRenderCellValueFn( indexPattern, displayedRows, - displayedRows - ? displayedRows.map((hit) => - flattenHit(hit, indexPattern, { includeIgnoredValues: true }) - ) - : [], + displayedRowsFlattened, useNewFieldsApi, fieldsToShow, - services.uiSettings.get(MAX_DOC_FIELDS_DISPLAYED) + services.uiSettings.get(MAX_DOC_FIELDS_DISPLAYED), + () => dataGridRef.current?.closeCellPopover() ), - [indexPattern, displayedRows, useNewFieldsApi, fieldsToShow, services.uiSettings] + [ + indexPattern, + displayedRowsFlattened, + displayedRows, + useNewFieldsApi, + fieldsToShow, + services.uiSettings, + ] ); /** @@ -432,6 +441,7 @@ export const DiscoverGrid = ({ expanded: expandedDoc, setExpanded: setExpandedDoc, rows: displayedRows, + rowsFlattened: displayedRowsFlattened, onFilter, indexPattern, isDarkMode: services.uiSettings.get('theme:darkMode'), @@ -463,6 +473,7 @@ export const DiscoverGrid = ({ onColumnResize={onResize} pagination={paginationObj} renderCellValue={renderCellValue} + ref={dataGridRef} rowCount={rowCount} schemaDetectors={schemaDetectors} sorting={sorting as EuiDataGridSorting} diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx index 9a75a74396ff05..5ce0befcf93050 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx @@ -5,17 +5,50 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +const mockCopyToClipboard = jest.fn(); +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...original, + copyToClipboard: (value: string) => mockCopyToClipboard(value), + }; +}); + +jest.mock('../../utils/use_discover_services', () => { + const services = { + toastNotifications: { + addInfo: jest.fn(), + }, + }; + const originalModule = jest.requireActual('../../utils/use_discover_services'); + return { + ...originalModule, + useDiscoverServices: () => services, + }; +}); import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { FilterInBtn, FilterOutBtn, buildCellActions } from './discover_grid_cell_actions'; +import { FilterInBtn, FilterOutBtn, buildCellActions, CopyBtn } from './discover_grid_cell_actions'; import { DiscoverGridContext } from './discover_grid_context'; - +import { EuiButton } from '@elastic/eui'; import { indexPatternMock } from '../../__mocks__/index_pattern'; import { esHits } from '../../__mocks__/es_hits'; -import { EuiButton } from '@elastic/eui'; import { DataViewField } from '@kbn/data-views-plugin/public'; +import { flattenHit } from '@kbn/data-plugin/common'; + +const contextMock = { + expanded: undefined, + setExpanded: jest.fn(), + rows: esHits, + rowsFlattened: esHits.map((hit) => flattenHit(hit, indexPatternMock)), + onFilter: jest.fn(), + indexPattern: indexPatternMock, + isDarkMode: false, + selectedDocs: [], + setSelectedDocs: jest.fn(), +}; describe('Discover cell actions ', function () { it('should not show cell actions for unfilterable fields', async () => { @@ -23,17 +56,6 @@ describe('Discover cell actions ', function () { }); it('triggers filter function when FilterInBtn is clicked', async () => { - const contextMock = { - expanded: undefined, - setExpanded: jest.fn(), - rows: esHits, - onFilter: jest.fn(), - indexPattern: indexPatternMock, - isDarkMode: false, - selectedDocs: [], - setSelectedDocs: jest.fn(), - }; - const component = mountWithIntl( { - const contextMock = { - expanded: undefined, - setExpanded: jest.fn(), - rows: esHits, - onFilter: jest.fn(), - indexPattern: indexPatternMock, - isDarkMode: false, - selectedDocs: [], - setSelectedDocs: jest.fn(), - }; - const component = mountWithIntl( { + const component = mountWithIntl( + + } + rowIndex={1} + colIndex={1} + columnId="extension" + isExpanded={false} + /> + + ); + const button = findTestSubject(component, 'copyClipboardButton'); + await button.simulate('click'); + expect(mockCopyToClipboard).toHaveBeenCalledWith('jpg'); + }); }); diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx index 318e1719c08f89..df07478dae5c6c 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx @@ -7,11 +7,12 @@ */ import React, { useContext } from 'react'; -import { EuiDataGridColumnCellActionProps } from '@elastic/eui'; +import { copyToClipboard, EuiDataGridColumnCellActionProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataViewField } from '@kbn/data-views-plugin/public'; -import { flattenHit } from '@kbn/data-plugin/public'; import { DiscoverGridContext, GridContext } from './discover_grid_context'; +import { useDiscoverServices } from '../../utils/use_discover_services'; +import { formatFieldValue } from '../../utils/format_value'; function onFilterCell( context: GridContext, @@ -19,12 +20,12 @@ function onFilterCell( columnId: EuiDataGridColumnCellActionProps['columnId'], mode: '+' | '-' ) { - const row = context.rows[rowIndex]; - const flattened = flattenHit(row, context.indexPattern); + const row = context.rowsFlattened[rowIndex]; + const value = String(row[columnId]); const field = context.indexPattern.fields.getByName(columnId); - if (flattened && field) { - context.onFilter(field, flattened[columnId], mode); + if (value && field) { + context.onFilter(field, value, mode); } } @@ -84,8 +85,52 @@ export const FilterOutBtn = ({ ); }; +export const CopyBtn = ({ Component, rowIndex, columnId }: EuiDataGridColumnCellActionProps) => { + const { indexPattern: dataView, rowsFlattened, rows } = useContext(DiscoverGridContext); + const { fieldFormats, toastNotifications } = useDiscoverServices(); + + const buttonTitle = i18n.translate('discover.grid.copyClipboardButtonTitle', { + defaultMessage: 'Copy value of {column}', + values: { column: columnId }, + }); + + return ( + { + const rowFlattened = rowsFlattened[rowIndex]; + const field = dataView.fields.getByName(columnId); + const value = rowFlattened[columnId]; + + const valueFormatted = + field?.type === '_source' + ? JSON.stringify(rowFlattened, null, 2) + : formatFieldValue(value, rows[rowIndex], fieldFormats, dataView, field, 'text'); + copyToClipboard(valueFormatted); + const infoTitle = i18n.translate('discover.grid.copyClipboardToastTitle', { + defaultMessage: 'Copied value of {column} to clipboard.', + values: { column: columnId }, + }); + + toastNotifications.addInfo({ + title: infoTitle, + }); + }} + iconType="copyClipboard" + aria-label={buttonTitle} + title={buttonTitle} + data-test-subj="copyClipboardButton" + > + {i18n.translate('discover.grid.copyClipboardButton', { + defaultMessage: 'Copy to clipboard', + })} + + ); +}; + export function buildCellActions(field: DataViewField) { - if (!field.filterable) { + if (field?.type === '_source') { + return [CopyBtn]; + } else if (!field.filterable) { return undefined; } diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx index 41d58cf2133361..f1b21dabab86ee 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx @@ -15,6 +15,7 @@ export interface GridContext { expanded?: ElasticSearchHit; setExpanded: (hit?: ElasticSearchHit) => void; rows: ElasticSearchHit[]; + rowsFlattened: Array>; onFilter: DocViewFilterFn; indexPattern: DataView; isDarkMode: boolean; diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx index d416372ac183fd..f1d8ab9fcb86df 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx @@ -21,6 +21,7 @@ const baseContextMock = { expanded: undefined, setExpanded: jest.fn(), rows: esHits, + rowsFlattened: esHits, onFilter: jest.fn(), indexPattern: indexPatternMock, isDarkMode: false, diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx index 27ee307d746ebf..903d0bc4bedcd4 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx @@ -18,6 +18,7 @@ const baseContextMock = { expanded: undefined, setExpanded: jest.fn(), rows: esHits, + rowsFlattened: esHits, onFilter: jest.fn(), indexPattern: indexPatternMock, isDarkMode: false, diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx index 6765a8d24f91a6..a64d8521f503e6 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx @@ -12,6 +12,8 @@ import { euiLightVars as themeLight, euiDarkVars as themeDark } from '@kbn/ui-th import { i18n } from '@kbn/i18n'; import { DiscoverGridContext } from './discover_grid_context'; import { EsHitRecord } from '../../application/types'; +import { DISCOVER_TOUR_STEP_ANCHOR_IDS } from '../discover_tour'; + /** * Button to expand a given row */ @@ -42,6 +44,7 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle return ( key === 'discover:maxDocFieldsDisplayed' && 200, + }, + fieldFormats: { + getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => (value ? value : '-') })), + }, +}; jest.mock('../../utils/use_discover_services', () => { - const services = { - uiSettings: { - get: (key: string) => key === 'discover:maxDocFieldsDisplayed' && 200, - }, - fieldFormats: { - getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => (value ? value : '-') })), - }, - }; const originalModule = jest.requireActual('../../utils/use_discover_services'); return { ...originalModule, - useDiscoverServices: () => services, + useDiscoverServices: () => mockServices, }; }); @@ -79,7 +83,8 @@ describe('Discover grid cell rendering', function () { rowsSource.map(flatten), false, [], - 100 + 100, + jest.fn() ); const component = shallow( ); expect(component.html()).toMatchInlineSnapshot( - `"100"` + `"
100
"` ); }); it('renders bytes column correctly using fields when details is true', () => { + const closePopoverMockFn = jest.fn(); const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, rowsFields, rowsFields.map(flatten), false, [], - 100 + 100, + closePopoverMockFn ); - const component = shallow( + const component = mountWithIntl( ); expect(component.html()).toMatchInlineSnapshot( - `"100"` + `"
100
"` ); + findTestSubject(component, 'docTableClosePopover').simulate('click'); + expect(closePopoverMockFn).toHaveBeenCalledTimes(1); }); it('renders _source column correctly', () => { @@ -154,7 +164,8 @@ describe('Discover grid cell rendering', function () { rowsSource.map(flatten), false, ['extension', 'bytes'], - 100 + 100, + jest.fn() ); const component = shallow( ); expect(component).toMatchInlineSnapshot(` - + + + + + + + + + + + + `); }); @@ -271,7 +313,8 @@ describe('Discover grid cell rendering', function () { rowsFields.map(flatten), true, ['extension', 'bytes'], - 100 + 100, + jest.fn() ); const component = shallow( ); expect(component).toMatchInlineSnapshot(` - + + + + + + + + + + + + `); }); @@ -476,7 +551,8 @@ describe('Discover grid cell rendering', function () { rowsFieldsWithTopLevelObject.map(flatten), true, ['object.value', 'extension', 'bytes'], - 100 + 100, + jest.fn() ); const component = shallow( { + const closePopoverMockFn = jest.fn(); const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, rowsFieldsWithTopLevelObject, rowsFieldsWithTopLevelObject.map(flatten), true, [], - 100 + 100, + closePopoverMockFn ); const component = shallow( ); expect(component).toMatchInlineSnapshot(` - + + + + + + + + + + + + `); }); + it('renders a functional close button when CodeEditor is rendered', () => { + const closePopoverMockFn = jest.fn(); + const DiscoverGridCellValue = getRenderCellValueFn( + indexPatternMock, + rowsFieldsWithTopLevelObject, + rowsFieldsWithTopLevelObject.map(flatten), + true, + [], + 100, + closePopoverMockFn + ); + const component = mountWithIntl( + + + + ); + const gridSelectionBtn = findTestSubject(component, 'docTableClosePopover'); + gridSelectionBtn.simulate('click'); + expect(closePopoverMockFn).toHaveBeenCalledTimes(1); + }); + it('does not collect subfields when the the column is unmapped but part of fields response', () => { (indexPatternMock.getFieldByName as jest.Mock).mockReturnValueOnce(undefined); const DiscoverGridCellValue = getRenderCellValueFn( @@ -594,7 +732,8 @@ describe('Discover grid cell rendering', function () { rowsFieldsWithTopLevelObject.map(flatten), true, [], - 100 + 100, + jest.fn() ); const component = shallow( ); expect(componentWithDetails).toMatchInlineSnapshot(` - + + + + + + + + `); }); }); diff --git a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx index b6a63d47b7a0f6..4175ff1bdd7b5a 100644 --- a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx @@ -8,6 +8,7 @@ import React, { Fragment, useContext, useEffect, useMemo } from 'react'; import classnames from 'classnames'; +import { i18n } from '@kbn/i18n'; import { euiLightVars as themeLight, euiDarkVars as themeDark } from '@kbn/ui-theme'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import { @@ -15,6 +16,9 @@ import { EuiDescriptionList, EuiDescriptionListTitle, EuiDescriptionListDescription, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { DiscoverGridContext } from './discover_grid_context'; @@ -36,7 +40,8 @@ export const getRenderCellValueFn = rowsFlattened: Array>, useNewFieldsApi: boolean, fieldsToShow: string[], - maxDocFieldsDisplayed: number + maxDocFieldsDisplayed: number, + closePopover: () => void ) => ({ rowIndex, columnId, isDetails, setCellProps }: EuiDataGridCellValueElementProps) => { const { uiSettings, fieldFormats } = useDiscoverServices(); @@ -93,6 +98,7 @@ export const getRenderCellValueFn = dataView, useTopLevelObjectColumns, fieldFormats, + closePopover, }); } @@ -147,6 +153,13 @@ function getInnerColumns(fields: Record, columnId: string) { ); } +function getJSON(columnId: string, rowRaw: ElasticSearchHit, useTopLevelObjectColumns: boolean) { + const json = useTopLevelObjectColumns + ? getInnerColumns(rowRaw.fields as Record, columnId) + : rowRaw; + return json as Record; +} + /** * Helper function for the cell popover */ @@ -158,6 +171,7 @@ function renderPopoverContent({ dataView, useTopLevelObjectColumns, fieldFormats, + closePopover, }: { rowRaw: ElasticSearchHit; rowFlattened: Record; @@ -166,25 +180,53 @@ function renderPopoverContent({ dataView: DataView; useTopLevelObjectColumns: boolean; fieldFormats: FieldFormatsStart; + closePopover: () => void; }) { + const closeButton = ( + + ); if (useTopLevelObjectColumns || field?.type === '_source') { - const json = useTopLevelObjectColumns - ? getInnerColumns(rowRaw.fields as Record, columnId) - : rowRaw; return ( - } width={defaultMonacoEditorWidth} /> + + + + {closeButton} + + + + + + ); } return ( - + + + + + {closeButton} + ); } /** diff --git a/src/plugins/discover/public/components/discover_tour/discover_tour.test.tsx b/src/plugins/discover/public/components/discover_tour/discover_tour.test.tsx new file mode 100644 index 00000000000000..cc25819443d22c --- /dev/null +++ b/src/plugins/discover/public/components/discover_tour/discover_tour.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 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 React from 'react'; +import { EuiTourStep, EuiButton } from '@elastic/eui'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { discoverServiceMock } from '../../__mocks__/services'; +import { DiscoverTourProvider } from './discover_tour_provider'; +import { useDiscoverTourContext } from './discover_tour_context'; +import { DISCOVER_TOUR_STEP_ANCHORS } from './discover_tour_anchors'; + +describe('Discover tour', () => { + const mountComponent = (innerContent?: JSX.Element) => { + return mountWithIntl( + + {innerContent} + + ); + }; + + it('should start successfully', () => { + const buttonSubjToTestStart = 'discoverTourButtonTestStart'; + const InnerComponent = () => { + const tourContext = useDiscoverTourContext(); + + return ( + + {'Start the tour'} + + ); + }; + + const component = mountComponent(); + // all steps are hidden by default + expect(component.find(EuiTourStep)).toHaveLength(0); + + // one step should become visible after the tour is triggered + component.find(`[data-test-subj="${buttonSubjToTestStart}"]`).at(0).simulate('click'); + + expect(component.find(EuiTourStep)).toHaveLength(5); + expect( + component.find({ anchor: DISCOVER_TOUR_STEP_ANCHORS.addFields, isStepOpen: true }) + ).toHaveLength(1); + expect( + component.find({ anchor: DISCOVER_TOUR_STEP_ANCHORS.reorderColumns, isStepOpen: false }) + ).toHaveLength(1); + expect( + component.find({ anchor: DISCOVER_TOUR_STEP_ANCHORS.sort, isStepOpen: false }) + ).toHaveLength(1); + expect( + component.find({ anchor: DISCOVER_TOUR_STEP_ANCHORS.changeRowHeight, isStepOpen: false }) + ).toHaveLength(1); + expect( + component.find({ anchor: DISCOVER_TOUR_STEP_ANCHORS.expandDocument, isStepOpen: false }) + ).toHaveLength(1); + }); +}); diff --git a/src/plugins/discover/public/components/discover_tour/discover_tour_anchors.tsx b/src/plugins/discover/public/components/discover_tour/discover_tour_anchors.tsx new file mode 100644 index 00000000000000..030876291aeea3 --- /dev/null +++ b/src/plugins/discover/public/components/discover_tour/discover_tour_anchors.tsx @@ -0,0 +1,22 @@ +/* + * Copyright 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 { htmlIdGenerator } from '@elastic/eui'; + +export const DISCOVER_TOUR_STEP_ANCHOR_IDS = { + addFields: htmlIdGenerator('dsc-tour-step-add-fields')(), + expandDocument: htmlIdGenerator('dsc-tour-step-expand')(), +}; + +export const DISCOVER_TOUR_STEP_ANCHORS = { + addFields: `#${DISCOVER_TOUR_STEP_ANCHOR_IDS.addFields}`, + reorderColumns: '[data-test-subj="dataGridColumnSelectorButton"]', + sort: '[data-test-subj="dataGridColumnSortingButton"]', + changeRowHeight: '[data-test-subj="dataGridDisplaySelectorButton"]', + expandDocument: `#${DISCOVER_TOUR_STEP_ANCHOR_IDS.expandDocument}`, +}; diff --git a/src/plugins/discover/public/components/discover_tour/discover_tour_context.ts b/src/plugins/discover/public/components/discover_tour/discover_tour_context.ts new file mode 100644 index 00000000000000..94bbfaf6555d2e --- /dev/null +++ b/src/plugins/discover/public/components/discover_tour/discover_tour_context.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createContext, useContext } from 'react'; + +export interface DiscoverTourContextProps { + onStartTour: () => void; + onNextTourStep: () => void; + onFinishTour: () => void; +} + +export const DiscoverTourContext = createContext({ + onStartTour: () => {}, + onNextTourStep: () => {}, + onFinishTour: () => {}, +}); + +export const useDiscoverTourContext = () => { + return useContext(DiscoverTourContext); +}; diff --git a/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx b/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx new file mode 100644 index 00000000000000..610fc0907ea5a1 --- /dev/null +++ b/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx @@ -0,0 +1,306 @@ +/* + * Copyright 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 React, { useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + useEuiTour, + EuiTourState, + EuiTourStep, + EuiTourStepProps, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiButtonProps, + EuiImage, + EuiSpacer, + EuiI18n, + EuiIcon, + EuiText, +} from '@elastic/eui'; +import { PLUGIN_ID } from '../../../common'; +import { useDiscoverServices } from '../../utils/use_discover_services'; +import { DiscoverTourContext, DiscoverTourContextProps } from './discover_tour_context'; +import { DISCOVER_TOUR_STEP_ANCHORS } from './discover_tour_anchors'; + +const MAX_WIDTH = 350; + +interface TourStepDefinition { + anchor: EuiTourStepProps['anchor']; + title: EuiTourStepProps['title']; + content: EuiTourStepProps['content']; + imageName: string; + imageAltText: string; +} + +const tourStepDefinitions: TourStepDefinition[] = [ + { + anchor: DISCOVER_TOUR_STEP_ANCHORS.addFields, + title: i18n.translate('discover.dscTour.stepAddFields.title', { + defaultMessage: 'Add fields to the table', + }), + content: ( + , + }} + /> + ), + imageName: 'add_fields.gif', + imageAltText: i18n.translate('discover.dscTour.stepAddFields.imageAltText', { + defaultMessage: + 'In the Available fields list, click the plus icon to toggle a field into the document table.', + }), + }, + { + anchor: DISCOVER_TOUR_STEP_ANCHORS.reorderColumns, + title: i18n.translate('discover.dscTour.stepReorderColumns.title', { + defaultMessage: 'Order the table columns', + }), + content: ( + + ), + imageName: 'reorder_columns.gif', + imageAltText: i18n.translate('discover.dscTour.stepReorderColumns.imageAltText', { + defaultMessage: 'Use the Columns popover to drag the columns to the order you prefer.', + }), + }, + { + anchor: DISCOVER_TOUR_STEP_ANCHORS.sort, + title: i18n.translate('discover.dscTour.stepSort.title', { + defaultMessage: 'Sort on one or more fields', + }), + content: ( + + ), + imageName: 'sort.gif', + imageAltText: i18n.translate('discover.dscTour.stepSort.imageAltText', { + defaultMessage: + 'Click a column header and select the desired sort order. Adjust a multi-field sort using the fields sorted popover.', + }), + }, + { + anchor: DISCOVER_TOUR_STEP_ANCHORS.changeRowHeight, + title: i18n.translate('discover.dscTour.stepChangeRowHeight.title', { + defaultMessage: 'Change the row height', + }), + content: ( + + ), + imageName: 'rows_per_line.gif', + imageAltText: i18n.translate('discover.dscTour.stepChangeRowHeight.imageAltText', { + defaultMessage: + 'Click the display options icon to adjust the row height to fit the contents.', + }), + }, + { + anchor: DISCOVER_TOUR_STEP_ANCHORS.expandDocument, + title: i18n.translate('discover.dscTour.stepExpand.title', { + defaultMessage: 'Expand documents', + }), + content: ( + + ), + }} + /> + ), + imageName: 'expand.gif', + imageAltText: i18n.translate('discover.dscTour.stepExpand.imageAltText', { + defaultMessage: + 'Click the expand icon to inspect and filter the fields in the document and view the document in context.', + }), + }, +]; + +const FIRST_STEP = 1; + +const prepareTourSteps = ( + stepDefinitions: TourStepDefinition[], + getAssetPath: (imageName: string) => string +): EuiTourStepProps[] => + stepDefinitions.map((stepDefinition, index) => ({ + step: index + 1, + anchor: stepDefinition.anchor, + title: stepDefinition.title, + maxWidth: MAX_WIDTH, + content: ( + <> + +

{stepDefinition.content}

+
+ {stepDefinition.imageName && ( + <> + + + + )} + + ), + })) as EuiTourStepProps[]; + +const findNextAvailableStep = ( + steps: EuiTourStepProps[], + currentTourStep: number +): number | null => { + const nextStep = steps.find( + (step) => + step.step > currentTourStep && + typeof step.anchor === 'string' && + document.querySelector(step.anchor) + ); + + return nextStep?.step ?? null; +}; + +const tourConfig: EuiTourState = { + currentTourStep: FIRST_STEP, + isTourActive: false, + tourPopoverWidth: MAX_WIDTH, + tourSubtitle: '', +}; + +export const DiscoverTourProvider: React.FC = ({ children }) => { + const services = useDiscoverServices(); + const prependToBasePath = services.core.http.basePath.prepend; + const getAssetPath = useCallback( + (imageName: string) => { + return prependToBasePath(`/plugins/${PLUGIN_ID}/assets/discover_tour/${imageName}`); + }, + [prependToBasePath] + ); + const tourSteps = useMemo( + () => prepareTourSteps(tourStepDefinitions, getAssetPath), + [getAssetPath] + ); + const [steps, actions, reducerState] = useEuiTour(tourSteps, tourConfig); + const currentTourStep = reducerState.currentTourStep; + const isTourActive = reducerState.isTourActive; + + const onStartTour = useCallback(() => { + actions.resetTour(); + actions.goToStep(FIRST_STEP, true); + }, [actions]); + + const onNextTourStep = useCallback(() => { + const nextAvailableStep = findNextAvailableStep(steps, currentTourStep); + if (nextAvailableStep) { + actions.goToStep(nextAvailableStep); + } else { + actions.finishTour(); + } + }, [actions, steps, currentTourStep]); + + const onFinishTour = useCallback(() => { + actions.finishTour(); + }, [actions]); + + const contextValue: DiscoverTourContextProps = useMemo( + () => ({ + onStartTour, + onNextTourStep, + onFinishTour, + }), + [onStartTour, onNextTourStep, onFinishTour] + ); + + return ( + + {isTourActive && + steps.map((step) => ( + + } + /> + ))} + {children} + + ); +}; + +export const DiscoverTourStepFooterAction: React.FC<{ + isLastStep: boolean; + onNextTourStep: DiscoverTourContextProps['onNextTourStep']; + onFinishTour: DiscoverTourContextProps['onFinishTour']; +}> = ({ isLastStep, onNextTourStep, onFinishTour }) => { + const actionButtonProps: Partial = { + size: 's', + color: 'success', + }; + + return ( + + {!isLastStep && ( + + + {EuiI18n({ token: 'core.euiTourStep.skipTour', default: 'Skip tour' })} + + + )} + + {isLastStep ? ( + + {EuiI18n({ token: 'core.euiTourStep.endTour', default: 'End tour' })} + + ) : ( + + {EuiI18n({ token: 'core.euiTourStep.nextStep', default: 'Next' })} + + )} + + + ); +}; diff --git a/src/plugins/discover/public/components/discover_tour/index.ts b/src/plugins/discover/public/components/discover_tour/index.ts new file mode 100644 index 00000000000000..b60c79a80f54d9 --- /dev/null +++ b/src/plugins/discover/public/components/discover_tour/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { DiscoverTourProvider } from './discover_tour_provider'; +export { useDiscoverTourContext } from './discover_tour_context'; +export { DISCOVER_TOUR_STEP_ANCHOR_IDS, DISCOVER_TOUR_STEP_ANCHORS } from './discover_tour_anchors'; diff --git a/src/plugins/discover/public/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap b/src/plugins/discover/public/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap index 31dd6347218b5a..7af546298e0d86 100644 --- a/src/plugins/discover/public/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap +++ b/src/plugins/discover/public/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap @@ -2,6 +2,7 @@ exports[`returns the \`JsonCodeEditor\` component 1`] = ` ; width?: string | number; + height?: string | number; hasLineNumbers?: boolean; } -export const JsonCodeEditor = ({ json, width, hasLineNumbers }: JsonCodeEditorProps) => { +export const JsonCodeEditor = ({ json, width, height, hasLineNumbers }: JsonCodeEditorProps) => { const jsonValue = JSON.stringify(json, null, 2); - // setting editor height based on lines height and count to stretch and fit its content - const setEditorCalculatedHeight = useCallback((editor) => { - const editorElement = editor.getDomNode(); - - if (!editorElement) { - return; - } - - const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight); - const lineCount = editor.getModel()?.getLineCount() || 1; - const height = editor.getTopForLineNumber(lineCount + 1) + lineHeight; - - editorElement.style.height = `${height}px`; - editor.layout(); - }, []); - return ( void 0} + hideCopyButton={true} /> ); }; diff --git a/src/plugins/discover/public/components/json_code_editor/json_code_editor_common.tsx b/src/plugins/discover/public/components/json_code_editor/json_code_editor_common.tsx index 5f6faa8ac0e9d3..777240fe2f5bb0 100644 --- a/src/plugins/discover/public/components/json_code_editor/json_code_editor_common.tsx +++ b/src/plugins/discover/public/components/json_code_editor/json_code_editor_common.tsx @@ -27,6 +27,7 @@ interface JsonCodeEditorCommonProps { width?: string | number; height?: string | number; hasLineNumbers?: boolean; + hideCopyButton?: boolean; } export const JsonCodeEditorCommon = ({ @@ -35,10 +36,40 @@ export const JsonCodeEditorCommon = ({ height, hasLineNumbers, onEditorDidMount, + hideCopyButton, }: JsonCodeEditorCommonProps) => { if (jsonValue === '') { return null; } + const codeEditor = ( + + ); + if (hideCopyButton) { + return codeEditor; + } return ( @@ -53,32 +84,7 @@ export const JsonCodeEditorCommon = ({
- - - + {codeEditor} ); }; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 8330751cf13f04..b4a33119c23b29 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -35,6 +35,7 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; +import { PLUGIN_ID } from '../common'; import { DocViewInput, DocViewInputFn } from './services/doc_views/doc_views_types'; import { DocViewsRegistry } from './services/doc_views/doc_views_registry'; import { @@ -257,7 +258,7 @@ export class DiscoverPlugin }; core.application.register({ - id: 'discover', + id: PLUGIN_ID, title: 'Discover', updater$: this.appStateUpdater.asObservable(), order: 1000, diff --git a/src/plugins/discover/public/utils/format_value.ts b/src/plugins/discover/public/utils/format_value.ts index 74331e946682e4..b7ee9af7f6873f 100644 --- a/src/plugins/discover/public/utils/format_value.ts +++ b/src/plugins/discover/public/utils/format_value.ts @@ -10,6 +10,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; import { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import { FieldFormatsContentType } from '@kbn/field-formats-plugin/common/types'; /** * Formats the value of a specific field using the appropriate field formatter if available @@ -26,16 +27,18 @@ export function formatFieldValue( hit: estypes.SearchHit, fieldFormats: FieldFormatsStart, dataView?: DataView, - field?: DataViewField + field?: DataViewField, + contentType?: FieldFormatsContentType | undefined ): string { + const usedContentType = contentType ?? 'html'; if (!dataView || !field) { // If either no field is available or no data view, we'll use the default // string formatter to format that field. return fieldFormats .getDefaultInstance(KBN_FIELD_TYPES.STRING) - .convert(value, 'html', { hit, field }); + .convert(value, usedContentType, { hit, field }); } // If we have a data view and field we use that fields field formatter - return dataView.getFormatterForField(field).convert(value, 'html', { hit, field }); + return dataView.getFormatterForField(field).convert(value, usedContentType, { hit, field }); } diff --git a/src/plugins/expressions/common/expression_functions/arguments.ts b/src/plugins/expressions/common/expression_functions/arguments.ts index bfe79e46c6226e..1d2e19436887bc 100644 --- a/src/plugins/expressions/common/expression_functions/arguments.ts +++ b/src/plugins/expressions/common/expression_functions/arguments.ts @@ -54,6 +54,10 @@ type UnresolvedArrayTypeToArgumentString = interface BaseArgumentType { /** Alternate names for the Function valid for use in the Expression Editor */ aliases?: string[]; + /** + * The flag to mark the function parameter as deprecated. + */ + deprecated?: boolean; /** Help text for the Argument to be displayed in the Expression Editor */ help: string; /** Default options for the Argument */ diff --git a/src/plugins/expressions/common/expression_functions/expression_function.ts b/src/plugins/expressions/common/expression_functions/expression_function.ts index 287b126cafbd85..7ce51e3a7d36d9 100644 --- a/src/plugins/expressions/common/expression_functions/expression_function.ts +++ b/src/plugins/expressions/common/expression_functions/expression_function.ts @@ -63,6 +63,12 @@ export class ExpressionFunction implements PersistableState @@ -88,6 +94,7 @@ export class ExpressionFunction implements PersistableState c); this.inject = inject || identity; this.extract = extract || ((s) => ({ state: s, references: [] })); diff --git a/src/plugins/expressions/common/expression_functions/expression_function_parameter.ts b/src/plugins/expressions/common/expression_functions/expression_function_parameter.ts index 242af9fb439a51..530d0bc5a9c5c7 100644 --- a/src/plugins/expressions/common/expression_functions/expression_function_parameter.ts +++ b/src/plugins/expressions/common/expression_functions/expression_function_parameter.ts @@ -16,6 +16,7 @@ export class ExpressionFunctionParameter { types: ArgumentType['types']; default?: ArgumentType['default']; aliases: string[]; + deprecated: boolean; multi: boolean; resolve: boolean; /** @@ -25,7 +26,7 @@ export class ExpressionFunctionParameter { options: T[]; constructor(name: string, arg: ArgumentType) { - const { required, help, types, aliases, multi, options, resolve, strict } = arg; + const { required, help, types, aliases, deprecated, multi, options, resolve, strict } = arg; if (name === '_') { throw Error('Arg names must not be _. Use it in aliases instead.'); @@ -37,6 +38,7 @@ export class ExpressionFunctionParameter { this.types = types || []; this.default = arg.default; this.aliases = aliases || []; + this.deprecated = !!deprecated; this.multi = !!multi; this.options = options || []; this.resolve = resolve == null ? true : resolve; diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts index 14f7c14a6759d3..018ee9e9fac0ca 100644 --- a/src/plugins/expressions/common/expression_functions/types.ts +++ b/src/plugins/expressions/common/expression_functions/types.ts @@ -39,6 +39,11 @@ export interface ExpressionFunctionDefinition< */ name: Name; + /** + * The flag to mark the function as deprecated. + */ + deprecated?: boolean; + /** * if set to true function will be disabled (but its migrate function will still be available) */ diff --git a/src/plugins/kibana_react/public/code_editor/editor_theme.ts b/src/plugins/kibana_react/public/code_editor/editor_theme.ts index 9242b7319e5c94..70d3267338ed93 100644 --- a/src/plugins/kibana_react/public/code_editor/editor_theme.ts +++ b/src/plugins/kibana_react/public/code_editor/editor_theme.ts @@ -76,6 +76,8 @@ export function createTheme( { token: 'keyword.json', foreground: euiTheme.euiColorPrimary }, { token: 'keyword.flow', foreground: euiTheme.euiColorWarning }, { token: 'keyword.flow.scss', foreground: euiTheme.euiColorPrimary }, + // Monaco editor supports strikethrough font style only starting from 0.32.0. + { token: 'keyword.deprecated', foreground: euiTheme.euiColorAccent }, { token: 'operator.scss', foreground: euiTheme.euiColorDarkShade }, { token: 'operator.sql', foreground: euiTheme.euiColorMediumShade }, diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap index f6dcf46b27d8c4..c11c1a11e73693 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap @@ -27,7 +27,21 @@ exports[`ElasticAgentCard props button 1`] = ` } href="/app/integrations/browse" - image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + image={ + + } paddingSize="l" title={ @@ -67,7 +81,21 @@ exports[`ElasticAgentCard props category 1`] = ` } href="/app/integrations/browse/custom" - image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + image={ + + } paddingSize="l" title={ @@ -107,7 +135,21 @@ exports[`ElasticAgentCard props href 1`] = ` } href="#" - image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + image={ + + } paddingSize="l" title={ @@ -147,7 +189,21 @@ exports[`ElasticAgentCard props recommended 1`] = ` } href="/app/integrations/browse" - image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + image={ + + } paddingSize="l" title={ @@ -187,7 +243,21 @@ exports[`ElasticAgentCard renders 1`] = ` } href="/app/integrations/browse" - image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + image={ + + } paddingSize="l" title={ diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx index 2cec3045c31c11..a8701fb7b7a344 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx @@ -9,7 +9,7 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { CoreStart } from '@kbn/core/public'; -import { EuiButton, EuiCard, EuiTextColor, EuiScreenReaderOnly } from '@elastic/eui'; +import { EuiButton, EuiCard, EuiTextColor, EuiScreenReaderOnly, EuiImage } from '@elastic/eui'; import { useKibana } from '../../../context'; import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page'; import { RedirectAppLinks } from '../../../app_links'; @@ -35,10 +35,24 @@ export const ElasticAgentCard: FunctionComponent = ({ services: { http, application }, } = useKibana(); const addBasePath = http.basePath.prepend; - const image = addBasePath(`/plugins/kibanaReact/assets/elastic_agent_card.svg`); + const imageUrl = addBasePath(`/plugins/kibanaReact/assets/elastic_agent_card.svg`); const canAccessFleet = application.capabilities.navLinks.integrations; const hasCategory = category ? `/${category}` : ''; + const image = ( + + ); + if (!canAccessFleet) { return ( = { help: 'A string of text that contains Markdown. To concatenate, pass the `string` function multiple times.', types: ['string'], default: '', + deprecated: false, aliases: ['_', 'expression'], multi: true, resolve: false, @@ -33,6 +34,7 @@ const font: ExpressionFunctionParameter